mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-16 11:42:45 +08:00
Doxygen and test (#385)
- 删除ModMaker - 拆出main.cpp以便CMake - Doxygen注释,以及CI(没测试过)
This commit is contained in:
parent
354e0ba42e
commit
f0ffd68ff2
16
.github/workflows/sphinx.yml
vendored
16
.github/workflows/sphinx.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Deploy Sphinx documentation to Pages
|
name: Deploy Doxygen to Pages
|
||||||
|
|
||||||
# Runs on pushes targeting the default branch
|
# Runs on pushes targeting the default branch
|
||||||
on:
|
on:
|
||||||
|
@ -15,5 +15,15 @@ jobs:
|
||||||
pages: write
|
pages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- id: deployment
|
- id: build
|
||||||
uses: sphinx-notes/pages@v3
|
uses: mattnotmitt/doxygen-action@1.9.8
|
||||||
|
with:
|
||||||
|
working-directory: './docs/'
|
||||||
|
doxyfile-path: 'docs/Doxyfile'
|
||||||
|
|
||||||
|
- id: deploy
|
||||||
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.MY_TOKEN }}
|
||||||
|
publish_dir: './docs/build/html'
|
||||||
|
|
||||||
|
|
|
@ -77,3 +77,6 @@ add_custom_command(
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(test)
|
||||||
|
|
|
@ -1,154 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property var parentBlock
|
|
||||||
property var childBlocks: [] // nested blocks inside this block
|
|
||||||
property var currentStack: [ root ] // the block stack that root is in
|
|
||||||
property var workspace // workspace
|
|
||||||
|
|
||||||
property bool draggable: false
|
|
||||||
property alias dragging: drag.active
|
|
||||||
property real startX // only available when dragging
|
|
||||||
property real startY
|
|
||||||
|
|
||||||
// TMP
|
|
||||||
property int idx
|
|
||||||
function toString() { return "Block #" + idx.toString(); }
|
|
||||||
// TMP
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: rect
|
|
||||||
anchors.fill: parent
|
|
||||||
color: drag.active ? "grey" : "snow"
|
|
||||||
border.width: 1
|
|
||||||
radius: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: idx
|
|
||||||
}
|
|
||||||
|
|
||||||
DragHandler {
|
|
||||||
id: drag
|
|
||||||
enabled: root.draggable
|
|
||||||
grabPermissions: PointHandler.TakeOverForbidden
|
|
||||||
xAxis.enabled: true
|
|
||||||
yAxis.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
onDraggingChanged: {
|
|
||||||
if (!dragging) {
|
|
||||||
finishDrag();
|
|
||||||
} else {
|
|
||||||
startDrag();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onXChanged: {
|
|
||||||
if (dragging) {
|
|
||||||
updateChildrenPos();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onYChanged: {
|
|
||||||
if (dragging) {
|
|
||||||
updateChildrenPos();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStackParent() {
|
|
||||||
const idx = currentStack.indexOf(root);
|
|
||||||
if (idx <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return currentStack[idx - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStackChildren() {
|
|
||||||
const idx = currentStack.indexOf(root);
|
|
||||||
if (idx >= currentStack.length - 1) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return currentStack.slice(idx + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startDrag() {
|
|
||||||
startX = x;
|
|
||||||
startY = y;
|
|
||||||
let children = getStackChildren();
|
|
||||||
children.push(...childBlocks);
|
|
||||||
children.forEach(b => {
|
|
||||||
b.startX = b.x;
|
|
||||||
b.startY = b.y;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateChildrenPos() {
|
|
||||||
const dx = root.x - root.startX;
|
|
||||||
const dy = root.y - root.startY;
|
|
||||||
let children = getStackChildren();
|
|
||||||
children.push(...childBlocks);
|
|
||||||
children.forEach(b => {
|
|
||||||
b.x = b.startX + dx;
|
|
||||||
b.y = b.startY + dy;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishDrag() {
|
|
||||||
if (currentStack[0] !== root) {
|
|
||||||
tearFrom(getStackParent());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentBlock) {
|
|
||||||
tearFrom(parentBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workspace) {
|
|
||||||
workspace.arrangeBlock(root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteTo(dest, asParent) {
|
|
||||||
x = dest.x;
|
|
||||||
y = dest.y + dest.height;
|
|
||||||
updateChildrenPos();
|
|
||||||
|
|
||||||
if (!asParent) {
|
|
||||||
const stk = currentStack;
|
|
||||||
dest.currentStack.push(...stk);
|
|
||||||
|
|
||||||
const p = dest.parentBlock;
|
|
||||||
let c = getStackChildren();
|
|
||||||
c.push(root);
|
|
||||||
c.forEach(cc => {
|
|
||||||
cc.parentBlock = p;
|
|
||||||
cc.currentStack = dest.currentStack;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tearFrom(dest) {
|
|
||||||
const fromParent = dest === root.parentBlock;
|
|
||||||
if (!fromParent) {
|
|
||||||
const idx = currentStack.indexOf(root);
|
|
||||||
const newStack = currentStack.slice(idx);
|
|
||||||
let c = getStackChildren();
|
|
||||||
|
|
||||||
currentStack.splice(idx);
|
|
||||||
|
|
||||||
c.push(root);
|
|
||||||
c.forEach(cc => {
|
|
||||||
cc.parentBlock = null;
|
|
||||||
cc.currentStack = newStack;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property var blockComponent
|
|
||||||
property var allBlocks: []
|
|
||||||
|
|
||||||
// ====== TMP ======
|
|
||||||
property int idx: 0
|
|
||||||
Row {
|
|
||||||
Button {
|
|
||||||
text: "quit"
|
|
||||||
onClicked: modStack.pop();
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "New"
|
|
||||||
onClicked: newBlock();
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "Del"
|
|
||||||
onClicked: rmFirstBlock_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function newBlock() {
|
|
||||||
let obj = blockComponent.createObject(root, {
|
|
||||||
width: 50, height: 50,
|
|
||||||
x: Math.random() * root.width, y: Math.random() * root.height,
|
|
||||||
workspace: root, draggable: true,
|
|
||||||
idx: ++idx,
|
|
||||||
});
|
|
||||||
allBlocks.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rmFirstBlock_() {
|
|
||||||
let obj = allBlocks[0];
|
|
||||||
if (!obj) return;
|
|
||||||
obj.destroy();
|
|
||||||
allBlocks.splice(0,1);
|
|
||||||
}
|
|
||||||
// ====== TMP ======
|
|
||||||
|
|
||||||
function getPasteBlock(block) {
|
|
||||||
let ret;
|
|
||||||
let min = Infinity;
|
|
||||||
const x = block.x;
|
|
||||||
const y = block.y;
|
|
||||||
allBlocks.forEach(b => {
|
|
||||||
if (b === block) return;
|
|
||||||
let dx = Math.abs(b.x - x);
|
|
||||||
let dy = y - b.y - b.height;
|
|
||||||
let tot = dx + dy;
|
|
||||||
if (dx < 60 && dy < 60 && dy > 0 && tot < 100) {
|
|
||||||
if (min > tot) {
|
|
||||||
if (!allBlocks.find(bb => bb.x === b.x && bb.y === b.y + b.height)) {
|
|
||||||
ret = b;
|
|
||||||
min = tot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPasteBlock(block) {
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrangeBlock(block) {
|
|
||||||
let b = getPasteBlock(block);
|
|
||||||
if (b) {
|
|
||||||
block.pasteTo(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
blockComponent = Qt.createComponent('Block.qml');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: root
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 16
|
|
||||||
signal finished()
|
|
||||||
signal accepted(string result)
|
|
||||||
|
|
||||||
property string head
|
|
||||||
property string hint
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr(head)
|
|
||||||
font.pixelSize: 20
|
|
||||||
font.bold: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr(hint)
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("validator_hint")
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField {
|
|
||||||
id: edit
|
|
||||||
font.pixelSize: 18
|
|
||||||
Layout.fillWidth: true
|
|
||||||
validator: RegularExpressionValidator {
|
|
||||||
regularExpression: /[0-9A-Za-z_]+/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "OK"
|
|
||||||
enabled: edit.text.length >= 4
|
|
||||||
onClicked: {
|
|
||||||
accepted(edit.text);
|
|
||||||
finished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
property var conf
|
|
||||||
|
|
||||||
property string userName
|
|
||||||
property string email
|
|
||||||
property var modList: []
|
|
||||||
|
|
||||||
function loadConf() {
|
|
||||||
conf = JSON.parse(ModBackend.readFile("mymod/config.json"));
|
|
||||||
userName = conf.userName ?? "";
|
|
||||||
email = conf.email ?? "";
|
|
||||||
modList = conf.modList ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveConf() {
|
|
||||||
conf.userName = userName;
|
|
||||||
conf.email = email;
|
|
||||||
conf.modList = modList;
|
|
||||||
|
|
||||||
ModBackend.saveToFile("mymod/config.json",
|
|
||||||
JSON.stringify(conf, undefined, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMod(mod) {
|
|
||||||
modList.push(mod);
|
|
||||||
saveConf();
|
|
||||||
modListChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeMod(mod) {
|
|
||||||
modList.splice(modList.indexOf(mod), 1);
|
|
||||||
saveConf();
|
|
||||||
modListChanged();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,147 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property var mod: ({})
|
|
||||||
property string modName
|
|
||||||
property string modPath: "mymod/" + modName + "/"
|
|
||||||
|
|
||||||
onModNameChanged: {
|
|
||||||
mod = JSON.parse(ModBackend.readFile(modPath + "mod.json"));
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolBar {
|
|
||||||
id: bar
|
|
||||||
width: parent.width
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
ToolButton {
|
|
||||||
icon.source: AppPath + "/image/modmaker/back"
|
|
||||||
onClicked: modStack.pop();
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: qsTr("ModMaker") + " - " + modName
|
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
ToolButton {
|
|
||||||
icon.source: AppPath + "/image/modmaker/menu"
|
|
||||||
onClicked: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - bar.height
|
|
||||||
anchors.top: bar.bottom
|
|
||||||
color: "snow"
|
|
||||||
opacity: 0.75
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
anchors.fill: parent
|
|
||||||
model: mod.packages ?? []
|
|
||||||
delegate: SwipeDelegate {
|
|
||||||
width: root.width
|
|
||||||
text: modelData
|
|
||||||
|
|
||||||
swipe.right: Label {
|
|
||||||
id: deleteLabel
|
|
||||||
text: qsTr("Delete")
|
|
||||||
color: "white"
|
|
||||||
verticalAlignment: Label.AlignVCenter
|
|
||||||
padding: 12
|
|
||||||
height: parent.height
|
|
||||||
anchors.right: parent.right
|
|
||||||
opacity: swipe.complete ? 1 : 0
|
|
||||||
Behavior on opacity { NumberAnimation { } }
|
|
||||||
|
|
||||||
SwipeDelegate.onClicked: deletePackage(modelData);
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1)
|
|
||||||
: "tomato"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RoundButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: 40
|
|
||||||
scale: 2
|
|
||||||
icon.source: AppPath + "/image/modmaker/add"
|
|
||||||
onClicked: {
|
|
||||||
dialog.source = "CreateSomething.qml";
|
|
||||||
dialog.item.head = "create_package";
|
|
||||||
dialog.item.hint = "create_package_hint";
|
|
||||||
drawer.open();
|
|
||||||
dialog.item.accepted.connect((name) => {
|
|
||||||
createNewPackage(name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawer {
|
|
||||||
id: drawer
|
|
||||||
width: parent.width * 0.4 / mainWindow.scale
|
|
||||||
height: parent.height / mainWindow.scale
|
|
||||||
dim: false
|
|
||||||
clip: true
|
|
||||||
dragMargin: 0
|
|
||||||
scale: mainWindow.scale
|
|
||||||
transformOrigin: Item.TopLeft
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dialog
|
|
||||||
anchors.fill: parent
|
|
||||||
onSourceChanged: {
|
|
||||||
if (item === null)
|
|
||||||
return;
|
|
||||||
item.finished.connect(() => {
|
|
||||||
sourceComponent = undefined;
|
|
||||||
drawer.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onSourceComponentChanged: sourceChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewPackage(name) {
|
|
||||||
const new_name = modName + "_" + name;
|
|
||||||
mod.packages = mod.packages ?? [];
|
|
||||||
if (mod.packages.indexOf(new_name) !== -1) {
|
|
||||||
toast.show(qsTr("cannot use this package name"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = modPath + new_name + "/";
|
|
||||||
ModBackend.mkdir(path);
|
|
||||||
mod.packages.push(new_name);
|
|
||||||
ModBackend.saveToFile(modPath + "mod.json",
|
|
||||||
JSON.stringify(mod, undefined, 2));
|
|
||||||
|
|
||||||
const pkgInfo = {
|
|
||||||
name: new_name,
|
|
||||||
};
|
|
||||||
ModBackend.saveToFile(path + "pkg.json",
|
|
||||||
JSON.stringify(pkgInfo, undefined, 2));
|
|
||||||
|
|
||||||
root.modChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePackage(name) {
|
|
||||||
const path = modPath + name + "/";
|
|
||||||
ModBackend.rmrf(path);
|
|
||||||
mod.packages.splice(mod.packages.indexOf(name), 1);
|
|
||||||
ModBackend.saveToFile(modPath + "mod.json",
|
|
||||||
JSON.stringify(mod, undefined, 2));
|
|
||||||
|
|
||||||
root.modChanged();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property bool configOK: modConfig.userName !== "" && modConfig.email !== ""
|
|
||||||
|
|
||||||
ToolBar {
|
|
||||||
id: bar
|
|
||||||
width: parent.width
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
ToolButton {
|
|
||||||
icon.source: AppPath + "/image/modmaker/back"
|
|
||||||
onClicked: mainStack.pop();
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: qsTr("ModMaker")
|
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "Test"
|
|
||||||
onClicked: {
|
|
||||||
const component = Qt.createComponent("Block/Workspace.qml");
|
|
||||||
if (component.status !== Component.Ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const page = component.createObject(null);
|
|
||||||
modStack.push(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToolButton {
|
|
||||||
icon.source: AppPath + "/image/modmaker/menu"
|
|
||||||
onClicked: {
|
|
||||||
dialog.source = "UserInfo.qml";
|
|
||||||
drawer.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - bar.height
|
|
||||||
anchors.top: bar.bottom
|
|
||||||
color: "snow"
|
|
||||||
opacity: 0.75
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.configOK ? "" : qsTr("config is incomplete")
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
anchors.fill: parent
|
|
||||||
model: modConfig.modList
|
|
||||||
clip: true
|
|
||||||
delegate: SwipeDelegate {
|
|
||||||
width: root.width
|
|
||||||
text: modelData
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
const component = Qt.createComponent("ModDetail.qml");
|
|
||||||
if (component.status !== Component.Ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const page = component.createObject(null, { modName: modelData });
|
|
||||||
modStack.push(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
swipe.right: Label {
|
|
||||||
id: deleteLabel
|
|
||||||
text: qsTr("Delete")
|
|
||||||
color: "white"
|
|
||||||
verticalAlignment: Label.AlignVCenter
|
|
||||||
padding: 12
|
|
||||||
height: parent.height
|
|
||||||
anchors.right: parent.right
|
|
||||||
opacity: swipe.complete ? 1 : 0
|
|
||||||
Behavior on opacity { NumberAnimation { } }
|
|
||||||
|
|
||||||
SwipeDelegate.onClicked: deleteMod(modelData);
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1)
|
|
||||||
: "tomato"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RoundButton {
|
|
||||||
visible: root.configOK
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: 40
|
|
||||||
scale: 2
|
|
||||||
icon.source: AppPath + "/image/modmaker/add"
|
|
||||||
onClicked: {
|
|
||||||
dialog.source = "CreateSomething.qml";
|
|
||||||
dialog.item.head = "create_mod";
|
|
||||||
dialog.item.hint = "create_mod_hint";
|
|
||||||
drawer.open();
|
|
||||||
dialog.item.accepted.connect((name) => {
|
|
||||||
createNewMod(name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawer {
|
|
||||||
id: drawer
|
|
||||||
width: parent.width * 0.4 / mainWindow.scale
|
|
||||||
height: parent.height / mainWindow.scale
|
|
||||||
dim: false
|
|
||||||
clip: true
|
|
||||||
dragMargin: 0
|
|
||||||
scale: mainWindow.scale
|
|
||||||
transformOrigin: Item.TopLeft
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: dialog
|
|
||||||
anchors.fill: parent
|
|
||||||
onSourceChanged: {
|
|
||||||
if (item === null)
|
|
||||||
return;
|
|
||||||
item.finished.connect(() => {
|
|
||||||
sourceComponent = undefined;
|
|
||||||
drawer.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onSourceComponentChanged: sourceChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewMod(name) {
|
|
||||||
const banned = [ "test", "standard", "standard_cards", "maneuvering" ];
|
|
||||||
if (banned.indexOf(name) !== -1 ||
|
|
||||||
modConfig.modList.indexOf(name) !== -1) {
|
|
||||||
toast.show(qsTr("cannot use this mod name"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ModBackend.createMod(name);
|
|
||||||
const modInfo = {
|
|
||||||
name: name,
|
|
||||||
descrption: "",
|
|
||||||
author: modConfig.userName,
|
|
||||||
};
|
|
||||||
ModBackend.saveToFile(`mymod/${name}/mod.json`,
|
|
||||||
JSON.stringify(modInfo, undefined, 2));
|
|
||||||
ModBackend.saveToFile(`mymod/${name}/.gitignore`, "init.lua");
|
|
||||||
ModBackend.stageFiles(name);
|
|
||||||
ModBackend.commitChanges(name, "Initial commit", modConfig.userName,
|
|
||||||
modConfig.email);
|
|
||||||
modConfig.addMod(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteMod(name) {
|
|
||||||
ModBackend.removeMod(name);
|
|
||||||
modConfig.removeMod(name);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 16
|
|
||||||
signal finished()
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("help_text")
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.rightMargin: 8
|
|
||||||
spacing: 16
|
|
||||||
Text {
|
|
||||||
text: qsTr("username")
|
|
||||||
}
|
|
||||||
TextField {
|
|
||||||
id: userName
|
|
||||||
font.pixelSize: 18
|
|
||||||
text: modConfig.userName
|
|
||||||
Layout.fillWidth: true
|
|
||||||
onTextChanged: {
|
|
||||||
modConfig.userName = text;
|
|
||||||
modConfig.saveConf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.rightMargin: 8
|
|
||||||
spacing: 16
|
|
||||||
Text {
|
|
||||||
text: qsTr("email")
|
|
||||||
}
|
|
||||||
TextField {
|
|
||||||
id: emailAddr
|
|
||||||
font.pixelSize: 18
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: modConfig.email
|
|
||||||
onTextChanged: {
|
|
||||||
modConfig.email = text;
|
|
||||||
modConfig.saveConf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("key_help_text")
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
textFormat: Text.RichText
|
|
||||||
onLinkActivated: Qt.openUrlExternally(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: qsTr("copy pubkey")
|
|
||||||
Layout.fillWidth: true
|
|
||||||
onClicked: {
|
|
||||||
const key = "mymod/id_rsa.pub";
|
|
||||||
if (!Backend.exists(key)) {
|
|
||||||
ModBackend.initKey();
|
|
||||||
}
|
|
||||||
const pubkey = ModBackend.readFile(key);
|
|
||||||
Backend.copyToClipboard(pubkey);
|
|
||||||
toast.show(qsTr("pubkey copied"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Component { id: modInit; ModInit {} }
|
|
||||||
|
|
||||||
StackView {
|
|
||||||
id: modStack
|
|
||||||
anchors.fill: parent
|
|
||||||
/*
|
|
||||||
pushEnter: Transition {
|
|
||||||
PropertyAnimation {
|
|
||||||
property: "opacity"
|
|
||||||
from: 0
|
|
||||||
to:1
|
|
||||||
duration: 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pushExit: Transition {
|
|
||||||
PropertyAnimation {
|
|
||||||
property: "opacity"
|
|
||||||
from: 1
|
|
||||||
to:0
|
|
||||||
duration: 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
popEnter: Transition {
|
|
||||||
PropertyAnimation {
|
|
||||||
property: "opacity"
|
|
||||||
from: 0
|
|
||||||
to:1
|
|
||||||
duration: 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
popExit: Transition {
|
|
||||||
PropertyAnimation {
|
|
||||||
property: "opacity"
|
|
||||||
from: 1
|
|
||||||
to:0
|
|
||||||
duration: 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
ModConfig {
|
|
||||||
id: modConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!ModBackend) {
|
|
||||||
Backend.createModBackend();
|
|
||||||
}
|
|
||||||
modConfig.loadConf();
|
|
||||||
modStack.push(modInit);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
module Fk.ModMaker
|
|
||||||
ModMaker 1.0 main.qml
|
|
364
docs/Doxyfile
Normal file
364
docs/Doxyfile
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
# Doxyfile 1.12.0 - comments removed
|
||||||
|
|
||||||
|
DOXYFILE_ENCODING = UTF-8
|
||||||
|
PROJECT_NAME = "FreeKill"
|
||||||
|
PROJECT_NUMBER =
|
||||||
|
PROJECT_BRIEF =
|
||||||
|
PROJECT_LOGO =
|
||||||
|
PROJECT_ICON =
|
||||||
|
OUTPUT_DIRECTORY = build
|
||||||
|
CREATE_SUBDIRS = NO
|
||||||
|
CREATE_SUBDIRS_LEVEL = 8
|
||||||
|
ALLOW_UNICODE_NAMES = NO
|
||||||
|
OUTPUT_LANGUAGE = Chinese
|
||||||
|
BRIEF_MEMBER_DESC = YES
|
||||||
|
REPEAT_BRIEF = NO
|
||||||
|
ABBREVIATE_BRIEF = "The $name class" \
|
||||||
|
"The $name widget" \
|
||||||
|
"The $name file" \
|
||||||
|
is \
|
||||||
|
provides \
|
||||||
|
specifies \
|
||||||
|
contains \
|
||||||
|
represents \
|
||||||
|
a \
|
||||||
|
an \
|
||||||
|
the
|
||||||
|
|
||||||
|
ALWAYS_DETAILED_SEC = NO
|
||||||
|
INLINE_INHERITED_MEMB = NO
|
||||||
|
FULL_PATH_NAMES = NO
|
||||||
|
STRIP_FROM_PATH =
|
||||||
|
STRIP_FROM_INC_PATH =
|
||||||
|
SHORT_NAMES = NO
|
||||||
|
JAVADOC_AUTOBRIEF = NO
|
||||||
|
JAVADOC_BANNER = NO
|
||||||
|
QT_AUTOBRIEF = NO
|
||||||
|
MULTILINE_CPP_IS_BRIEF = NO
|
||||||
|
PYTHON_DOCSTRING = YES
|
||||||
|
INHERIT_DOCS = YES
|
||||||
|
SEPARATE_MEMBER_PAGES = NO
|
||||||
|
TAB_SIZE = 4
|
||||||
|
ALIASES =
|
||||||
|
OPTIMIZE_OUTPUT_FOR_C = NO
|
||||||
|
OPTIMIZE_OUTPUT_JAVA = NO
|
||||||
|
OPTIMIZE_FOR_FORTRAN = NO
|
||||||
|
OPTIMIZE_OUTPUT_VHDL = NO
|
||||||
|
OPTIMIZE_OUTPUT_SLICE = NO
|
||||||
|
EXTENSION_MAPPING =
|
||||||
|
MARKDOWN_SUPPORT = YES
|
||||||
|
TOC_INCLUDE_HEADINGS = 6
|
||||||
|
MARKDOWN_ID_STYLE = DOXYGEN
|
||||||
|
AUTOLINK_SUPPORT = YES
|
||||||
|
BUILTIN_STL_SUPPORT = NO
|
||||||
|
CPP_CLI_SUPPORT = NO
|
||||||
|
SIP_SUPPORT = NO
|
||||||
|
IDL_PROPERTY_SUPPORT = YES
|
||||||
|
DISTRIBUTE_GROUP_DOC = NO
|
||||||
|
GROUP_NESTED_COMPOUNDS = NO
|
||||||
|
SUBGROUPING = YES
|
||||||
|
INLINE_GROUPED_CLASSES = NO
|
||||||
|
INLINE_SIMPLE_STRUCTS = NO
|
||||||
|
TYPEDEF_HIDES_STRUCT = NO
|
||||||
|
LOOKUP_CACHE_SIZE = 0
|
||||||
|
NUM_PROC_THREADS = 4
|
||||||
|
TIMESTAMP = YES
|
||||||
|
EXTRACT_ALL = NO
|
||||||
|
EXTRACT_PRIVATE = YES
|
||||||
|
EXTRACT_PRIV_VIRTUAL = NO
|
||||||
|
EXTRACT_PACKAGE = NO
|
||||||
|
EXTRACT_STATIC = NO
|
||||||
|
EXTRACT_LOCAL_CLASSES = YES
|
||||||
|
EXTRACT_LOCAL_METHODS = NO
|
||||||
|
EXTRACT_ANON_NSPACES = NO
|
||||||
|
RESOLVE_UNNAMED_PARAMS = YES
|
||||||
|
HIDE_UNDOC_MEMBERS = NO
|
||||||
|
HIDE_UNDOC_CLASSES = NO
|
||||||
|
HIDE_FRIEND_COMPOUNDS = NO
|
||||||
|
HIDE_IN_BODY_DOCS = NO
|
||||||
|
INTERNAL_DOCS = NO
|
||||||
|
CASE_SENSE_NAMES = SYSTEM
|
||||||
|
HIDE_SCOPE_NAMES = NO
|
||||||
|
HIDE_COMPOUND_REFERENCE= NO
|
||||||
|
SHOW_HEADERFILE = NO
|
||||||
|
SHOW_INCLUDE_FILES = NO
|
||||||
|
SHOW_GROUPED_MEMB_INC = NO
|
||||||
|
FORCE_LOCAL_INCLUDES = NO
|
||||||
|
INLINE_INFO = YES
|
||||||
|
SORT_MEMBER_DOCS = YES
|
||||||
|
SORT_BRIEF_DOCS = NO
|
||||||
|
SORT_MEMBERS_CTORS_1ST = NO
|
||||||
|
SORT_GROUP_NAMES = NO
|
||||||
|
SORT_BY_SCOPE_NAME = NO
|
||||||
|
STRICT_PROTO_MATCHING = NO
|
||||||
|
GENERATE_TODOLIST = YES
|
||||||
|
GENERATE_TESTLIST = YES
|
||||||
|
GENERATE_BUGLIST = YES
|
||||||
|
GENERATE_DEPRECATEDLIST= YES
|
||||||
|
ENABLED_SECTIONS =
|
||||||
|
MAX_INITIALIZER_LINES = 30
|
||||||
|
SHOW_USED_FILES = YES
|
||||||
|
SHOW_FILES = YES
|
||||||
|
SHOW_NAMESPACES = YES
|
||||||
|
FILE_VERSION_FILTER =
|
||||||
|
LAYOUT_FILE =
|
||||||
|
CITE_BIB_FILES =
|
||||||
|
EXTERNAL_TOOL_PATH =
|
||||||
|
QUIET = NO
|
||||||
|
WARNINGS = YES
|
||||||
|
WARN_IF_UNDOCUMENTED = YES
|
||||||
|
WARN_IF_DOC_ERROR = YES
|
||||||
|
WARN_IF_INCOMPLETE_DOC = YES
|
||||||
|
WARN_NO_PARAMDOC = NO
|
||||||
|
WARN_IF_UNDOC_ENUM_VAL = NO
|
||||||
|
WARN_AS_ERROR = NO
|
||||||
|
WARN_FORMAT = "$file:$line: $text"
|
||||||
|
WARN_LINE_FORMAT = "at line $line of file $file"
|
||||||
|
WARN_LOGFILE =
|
||||||
|
INPUT = ../src
|
||||||
|
INPUT_ENCODING = UTF-8
|
||||||
|
INPUT_FILE_ENCODING =
|
||||||
|
FILE_PATTERNS = *.c \
|
||||||
|
*.cc \
|
||||||
|
*.cxx \
|
||||||
|
*.cxxm \
|
||||||
|
*.cpp \
|
||||||
|
*.cppm \
|
||||||
|
*.ccm \
|
||||||
|
*.c++ \
|
||||||
|
*.c++m \
|
||||||
|
*.java \
|
||||||
|
*.ii \
|
||||||
|
*.ixx \
|
||||||
|
*.ipp \
|
||||||
|
*.i++ \
|
||||||
|
*.inl \
|
||||||
|
*.idl \
|
||||||
|
*.ddl \
|
||||||
|
*.odl \
|
||||||
|
*.h \
|
||||||
|
*.hh \
|
||||||
|
*.hxx \
|
||||||
|
*.hpp \
|
||||||
|
*.h++ \
|
||||||
|
*.ixx \
|
||||||
|
*.l \
|
||||||
|
*.cs \
|
||||||
|
*.d \
|
||||||
|
*.php \
|
||||||
|
*.php4 \
|
||||||
|
*.php5 \
|
||||||
|
*.phtml \
|
||||||
|
*.inc \
|
||||||
|
*.m \
|
||||||
|
*.markdown \
|
||||||
|
*.md \
|
||||||
|
*.mm \
|
||||||
|
*.dox \
|
||||||
|
*.py \
|
||||||
|
*.pyw \
|
||||||
|
*.f90 \
|
||||||
|
*.f95 \
|
||||||
|
*.f03 \
|
||||||
|
*.f08 \
|
||||||
|
*.f18 \
|
||||||
|
*.f \
|
||||||
|
*.for \
|
||||||
|
*.vhd \
|
||||||
|
*.vhdl \
|
||||||
|
*.ucf \
|
||||||
|
*.qsf \
|
||||||
|
*.ice
|
||||||
|
|
||||||
|
RECURSIVE = YES
|
||||||
|
EXCLUDE = ../src/swig/
|
||||||
|
EXCLUDE_SYMLINKS = NO
|
||||||
|
EXCLUDE_PATTERNS =
|
||||||
|
EXCLUDE_SYMBOLS =
|
||||||
|
EXAMPLE_PATH =
|
||||||
|
EXAMPLE_PATTERNS = *
|
||||||
|
EXAMPLE_RECURSIVE = NO
|
||||||
|
IMAGE_PATH =
|
||||||
|
INPUT_FILTER =
|
||||||
|
FILTER_PATTERNS =
|
||||||
|
FILTER_SOURCE_FILES = NO
|
||||||
|
FILTER_SOURCE_PATTERNS =
|
||||||
|
USE_MDFILE_AS_MAINPAGE =
|
||||||
|
FORTRAN_COMMENT_AFTER = 72
|
||||||
|
SOURCE_BROWSER = NO
|
||||||
|
INLINE_SOURCES = NO
|
||||||
|
STRIP_CODE_COMMENTS = YES
|
||||||
|
REFERENCED_BY_RELATION = NO
|
||||||
|
REFERENCES_RELATION = NO
|
||||||
|
REFERENCES_LINK_SOURCE = YES
|
||||||
|
SOURCE_TOOLTIPS = YES
|
||||||
|
USE_HTAGS = NO
|
||||||
|
VERBATIM_HEADERS = YES
|
||||||
|
CLANG_ASSISTED_PARSING = NO
|
||||||
|
CLANG_ADD_INC_PATHS = YES
|
||||||
|
CLANG_OPTIONS =
|
||||||
|
CLANG_DATABASE_PATH =
|
||||||
|
ALPHABETICAL_INDEX = YES
|
||||||
|
IGNORE_PREFIX =
|
||||||
|
GENERATE_HTML = YES
|
||||||
|
HTML_OUTPUT = html
|
||||||
|
HTML_FILE_EXTENSION = .html
|
||||||
|
HTML_HEADER =
|
||||||
|
HTML_FOOTER =
|
||||||
|
HTML_STYLESHEET =
|
||||||
|
HTML_EXTRA_STYLESHEET =
|
||||||
|
HTML_EXTRA_FILES =
|
||||||
|
HTML_COLORSTYLE = AUTO_LIGHT
|
||||||
|
HTML_COLORSTYLE_HUE = 220
|
||||||
|
HTML_COLORSTYLE_SAT = 100
|
||||||
|
HTML_COLORSTYLE_GAMMA = 80
|
||||||
|
HTML_DYNAMIC_MENUS = YES
|
||||||
|
HTML_DYNAMIC_SECTIONS = NO
|
||||||
|
HTML_CODE_FOLDING = YES
|
||||||
|
HTML_COPY_CLIPBOARD = YES
|
||||||
|
HTML_PROJECT_COOKIE =
|
||||||
|
HTML_INDEX_NUM_ENTRIES = 100
|
||||||
|
GENERATE_DOCSET = NO
|
||||||
|
DOCSET_FEEDNAME = "Doxygen generated docs"
|
||||||
|
DOCSET_FEEDURL =
|
||||||
|
DOCSET_BUNDLE_ID = org.doxygen.Project
|
||||||
|
DOCSET_PUBLISHER_ID = org.doxygen.Publisher
|
||||||
|
DOCSET_PUBLISHER_NAME = Publisher
|
||||||
|
GENERATE_HTMLHELP = NO
|
||||||
|
CHM_FILE =
|
||||||
|
HHC_LOCATION =
|
||||||
|
GENERATE_CHI = NO
|
||||||
|
CHM_INDEX_ENCODING =
|
||||||
|
BINARY_TOC = NO
|
||||||
|
TOC_EXPAND = NO
|
||||||
|
SITEMAP_URL =
|
||||||
|
GENERATE_QHP = NO
|
||||||
|
QCH_FILE =
|
||||||
|
QHP_NAMESPACE = org.doxygen.Project
|
||||||
|
QHP_VIRTUAL_FOLDER = doc
|
||||||
|
QHP_CUST_FILTER_NAME =
|
||||||
|
QHP_CUST_FILTER_ATTRS =
|
||||||
|
QHP_SECT_FILTER_ATTRS =
|
||||||
|
QHG_LOCATION =
|
||||||
|
GENERATE_ECLIPSEHELP = NO
|
||||||
|
ECLIPSE_DOC_ID = org.doxygen.Project
|
||||||
|
DISABLE_INDEX = NO
|
||||||
|
GENERATE_TREEVIEW = YES
|
||||||
|
FULL_SIDEBAR = NO
|
||||||
|
ENUM_VALUES_PER_LINE = 4
|
||||||
|
SHOW_ENUM_VALUES = NO
|
||||||
|
TREEVIEW_WIDTH = 250
|
||||||
|
EXT_LINKS_IN_WINDOW = NO
|
||||||
|
OBFUSCATE_EMAILS = YES
|
||||||
|
HTML_FORMULA_FORMAT = png
|
||||||
|
FORMULA_FONTSIZE = 10
|
||||||
|
FORMULA_MACROFILE =
|
||||||
|
USE_MATHJAX = NO
|
||||||
|
MATHJAX_VERSION = MathJax_2
|
||||||
|
MATHJAX_FORMAT = HTML-CSS
|
||||||
|
MATHJAX_RELPATH =
|
||||||
|
MATHJAX_EXTENSIONS =
|
||||||
|
MATHJAX_CODEFILE =
|
||||||
|
SEARCHENGINE = YES
|
||||||
|
SERVER_BASED_SEARCH = NO
|
||||||
|
EXTERNAL_SEARCH = NO
|
||||||
|
SEARCHENGINE_URL =
|
||||||
|
SEARCHDATA_FILE = searchdata.xml
|
||||||
|
EXTERNAL_SEARCH_ID =
|
||||||
|
EXTRA_SEARCH_MAPPINGS =
|
||||||
|
GENERATE_LATEX = YES
|
||||||
|
LATEX_OUTPUT = latex
|
||||||
|
LATEX_CMD_NAME =
|
||||||
|
MAKEINDEX_CMD_NAME = makeindex
|
||||||
|
LATEX_MAKEINDEX_CMD = makeindex
|
||||||
|
COMPACT_LATEX = NO
|
||||||
|
PAPER_TYPE = a4
|
||||||
|
EXTRA_PACKAGES =
|
||||||
|
LATEX_HEADER =
|
||||||
|
LATEX_FOOTER =
|
||||||
|
LATEX_EXTRA_STYLESHEET =
|
||||||
|
LATEX_EXTRA_FILES =
|
||||||
|
PDF_HYPERLINKS = YES
|
||||||
|
USE_PDFLATEX = YES
|
||||||
|
LATEX_BATCHMODE = NO
|
||||||
|
LATEX_HIDE_INDICES = NO
|
||||||
|
LATEX_BIB_STYLE = plain
|
||||||
|
LATEX_EMOJI_DIRECTORY =
|
||||||
|
GENERATE_RTF = NO
|
||||||
|
RTF_OUTPUT = rtf
|
||||||
|
COMPACT_RTF = NO
|
||||||
|
RTF_HYPERLINKS = NO
|
||||||
|
RTF_STYLESHEET_FILE =
|
||||||
|
RTF_EXTENSIONS_FILE =
|
||||||
|
RTF_EXTRA_FILES =
|
||||||
|
GENERATE_MAN = NO
|
||||||
|
MAN_OUTPUT = man
|
||||||
|
MAN_EXTENSION = .3
|
||||||
|
MAN_SUBDIR =
|
||||||
|
MAN_LINKS = NO
|
||||||
|
GENERATE_XML = NO
|
||||||
|
XML_OUTPUT = xml
|
||||||
|
XML_PROGRAMLISTING = YES
|
||||||
|
XML_NS_MEMB_FILE_SCOPE = NO
|
||||||
|
GENERATE_DOCBOOK = NO
|
||||||
|
DOCBOOK_OUTPUT = docbook
|
||||||
|
GENERATE_AUTOGEN_DEF = NO
|
||||||
|
GENERATE_SQLITE3 = NO
|
||||||
|
SQLITE3_OUTPUT = sqlite3
|
||||||
|
SQLITE3_RECREATE_DB = YES
|
||||||
|
GENERATE_PERLMOD = NO
|
||||||
|
PERLMOD_LATEX = NO
|
||||||
|
PERLMOD_PRETTY = YES
|
||||||
|
PERLMOD_MAKEVAR_PREFIX =
|
||||||
|
ENABLE_PREPROCESSING = YES
|
||||||
|
MACRO_EXPANSION = NO
|
||||||
|
EXPAND_ONLY_PREDEF = NO
|
||||||
|
SEARCH_INCLUDES = YES
|
||||||
|
INCLUDE_PATH =
|
||||||
|
INCLUDE_FILE_PATTERNS =
|
||||||
|
PREDEFINED =
|
||||||
|
EXPAND_AS_DEFINED =
|
||||||
|
SKIP_FUNCTION_MACROS = YES
|
||||||
|
TAGFILES =
|
||||||
|
GENERATE_TAGFILE =
|
||||||
|
ALLEXTERNALS = NO
|
||||||
|
EXTERNAL_GROUPS = YES
|
||||||
|
EXTERNAL_PAGES = YES
|
||||||
|
HIDE_UNDOC_RELATIONS = YES
|
||||||
|
HAVE_DOT = NO
|
||||||
|
DOT_NUM_THREADS = 0
|
||||||
|
DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10"
|
||||||
|
DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10"
|
||||||
|
DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4"
|
||||||
|
DOT_FONTPATH =
|
||||||
|
CLASS_GRAPH = YES
|
||||||
|
COLLABORATION_GRAPH = YES
|
||||||
|
GROUP_GRAPHS = YES
|
||||||
|
UML_LOOK = NO
|
||||||
|
UML_LIMIT_NUM_FIELDS = 10
|
||||||
|
DOT_UML_DETAILS = NO
|
||||||
|
DOT_WRAP_THRESHOLD = 17
|
||||||
|
TEMPLATE_RELATIONS = NO
|
||||||
|
INCLUDE_GRAPH = NO
|
||||||
|
INCLUDED_BY_GRAPH = NO
|
||||||
|
CALL_GRAPH = NO
|
||||||
|
CALLER_GRAPH = NO
|
||||||
|
GRAPHICAL_HIERARCHY = YES
|
||||||
|
DIRECTORY_GRAPH = YES
|
||||||
|
DIR_GRAPH_MAX_DEPTH = 1
|
||||||
|
DOT_IMAGE_FORMAT = png
|
||||||
|
INTERACTIVE_SVG = NO
|
||||||
|
DOT_PATH =
|
||||||
|
DOTFILE_DIRS =
|
||||||
|
DIA_PATH =
|
||||||
|
DIAFILE_DIRS =
|
||||||
|
PLANTUML_JAR_PATH = $(PLANTUML_INSTALL_DIR)/plantuml.jar
|
||||||
|
PLANTUML_CFG_FILE =
|
||||||
|
PLANTUML_INCLUDE_PATH =
|
||||||
|
DOT_GRAPH_MAX_NODES = 50
|
||||||
|
MAX_DOT_GRAPH_DEPTH = 0
|
||||||
|
DOT_MULTI_TARGETS = NO
|
||||||
|
GENERATE_LEGEND = YES
|
||||||
|
DOT_CLEANUP = YES
|
||||||
|
MSCGEN_TOOL =
|
||||||
|
MSCFILE_DIRS =
|
|
@ -1,7 +1,8 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
set(freekill_SRCS
|
set(freekill_SRCS
|
||||||
"main.cpp"
|
# "main.cpp"
|
||||||
|
"freekill.cpp"
|
||||||
"core/player.cpp"
|
"core/player.cpp"
|
||||||
"core/util.cpp"
|
"core/util.cpp"
|
||||||
"core/packman.cpp"
|
"core/packman.cpp"
|
||||||
|
@ -95,9 +96,9 @@ else ()
|
||||||
set(GIT_LIB git2)
|
set(GIT_LIB git2)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
target_sources(FreeKill PRIVATE ${freekill_SRCS})
|
add_library(libFreeKill STATIC ${freekill_SRCS})
|
||||||
target_precompile_headers(FreeKill PRIVATE "pch.h")
|
target_precompile_headers(libFreeKill PRIVATE "pch.h")
|
||||||
target_link_libraries(FreeKill PRIVATE
|
target_link_libraries(libFreeKill PRIVATE
|
||||||
${LUA_LIB}
|
${LUA_LIB}
|
||||||
${SQLITE3_LIB}
|
${SQLITE3_LIB}
|
||||||
${CRYPTO_LIB}
|
${CRYPTO_LIB}
|
||||||
|
@ -108,6 +109,10 @@ target_link_libraries(FreeKill PRIVATE
|
||||||
${GIT_LIB}
|
${GIT_LIB}
|
||||||
${IDBFS_LIB}
|
${IDBFS_LIB}
|
||||||
)
|
)
|
||||||
|
target_sources(FreeKill PRIVATE main.cpp)
|
||||||
|
target_link_libraries(FreeKill PRIVATE
|
||||||
|
libFreeKill
|
||||||
|
)
|
||||||
|
|
||||||
install(TARGETS FreeKill DESTINATION bin)
|
install(TARGETS FreeKill DESTINATION bin)
|
||||||
install(DIRECTORY
|
install(DIRECTORY
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
/** @file util.h
|
||||||
|
|
||||||
|
util.h负责提供各种全局函数、全局变量等。
|
||||||
|
|
||||||
|
@todo 好像在调用C库时C++程序会为它们创建包装类才对吧?
|
||||||
|
总之这种写法实在是不太推荐,以后说不定会改成另外的写法。
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _GLOBAL_H
|
#ifndef _GLOBAL_H
|
||||||
#define _GLOBAL_H
|
#define _GLOBAL_H
|
||||||
|
|
||||||
|
|
374
src/freekill.cpp
Normal file
374
src/freekill.cpp
Normal file
|
@ -0,0 +1,374 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "client/client.h"
|
||||||
|
#include "core/util.h"
|
||||||
|
using namespace fkShell;
|
||||||
|
|
||||||
|
#include "core/packman.h"
|
||||||
|
#include "server/server.h"
|
||||||
|
#include "server/shell.h"
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN32)
|
||||||
|
#include "applink.c"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef FK_SERVER_ONLY
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QSplashScreen>
|
||||||
|
#ifndef Q_OS_ANDROID
|
||||||
|
#include <QQuickStyle>
|
||||||
|
#endif
|
||||||
|
#include "ui/qmlbackend.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(Q_OS_ANDROID)
|
||||||
|
static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) {
|
||||||
|
QFileInfo srcFileInfo(srcFilePath);
|
||||||
|
if (srcFileInfo.isDir()) {
|
||||||
|
QDir targetDir(tgtFilePath);
|
||||||
|
if (!targetDir.exists()) {
|
||||||
|
targetDir.cdUp();
|
||||||
|
if (!targetDir.mkdir(QFileInfo(tgtFilePath).fileName()))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QDir sourceDir(srcFilePath);
|
||||||
|
QStringList fileNames =
|
||||||
|
sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot |
|
||||||
|
QDir::Hidden | QDir::System);
|
||||||
|
foreach (const QString &fileName, fileNames) {
|
||||||
|
const QString newSrcFilePath = srcFilePath + QLatin1Char('/') + fileName;
|
||||||
|
const QString newTgtFilePath = tgtFilePath + QLatin1Char('/') + fileName;
|
||||||
|
if (!copyPath(newSrcFilePath, newTgtFilePath))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QFile::remove(tgtFilePath);
|
||||||
|
if (!QFile::copy(srcFilePath, tgtFilePath))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void installFkAssets(const QString &src, const QString &dest) {
|
||||||
|
QFile f(dest + "/fk_ver");
|
||||||
|
if (f.exists() && f.open(QIODevice::ReadOnly)) {
|
||||||
|
auto ver = f.readLine().simplified();
|
||||||
|
if (ver == FK_VERSION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
copyPath(src, dest);
|
||||||
|
#elif defined(Q_OS_LINUX)
|
||||||
|
system(QString("cp -r %1 %2/..").arg(src).arg(dest).toUtf8());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
static void prepareForLinux() {
|
||||||
|
// 如果用户执行的是 /usr/bin/FreeKill,那么这意味着 freekill 是被包管理器安装
|
||||||
|
// 的,所以我们就需要把资源文件都复制到 ~/.local 中,并且切换当前目录
|
||||||
|
// TODO: AppImage
|
||||||
|
char buf[256] = {0};
|
||||||
|
int len = readlink("/proc/self/exe", buf, 256);
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
if (!strcmp(buf, "/usr/bin/FreeKill")) {
|
||||||
|
system("mkdir -p ~/.local/share/FreeKill");
|
||||||
|
installFkAssets("/usr/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home));
|
||||||
|
chdir(home);
|
||||||
|
chdir(".local/share/FreeKill");
|
||||||
|
} else if (!strcmp(buf, "/usr/local/bin/FreeKill")) {
|
||||||
|
system("mkdir -p ~/.local/share/FreeKill");
|
||||||
|
installFkAssets("/usr/local/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home));
|
||||||
|
chdir(home);
|
||||||
|
chdir(".local/share/FreeKill");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static FILE *info_log = nullptr;
|
||||||
|
static FILE *err_log = nullptr;
|
||||||
|
|
||||||
|
void fkMsgHandler(QtMsgType type, const QMessageLogContext &context,
|
||||||
|
const QString &msg) {
|
||||||
|
auto date = QDate::currentDate();
|
||||||
|
|
||||||
|
FILE *file;
|
||||||
|
switch (type) {
|
||||||
|
case QtDebugMsg:
|
||||||
|
case QtInfoMsg:
|
||||||
|
file = info_log;
|
||||||
|
break;
|
||||||
|
case QtWarningMsg:
|
||||||
|
case QtCriticalMsg:
|
||||||
|
case QtFatalMsg:
|
||||||
|
file = err_log;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef FK_USE_READLINE
|
||||||
|
ShellInstance->clearLine();
|
||||||
|
#else
|
||||||
|
printf("\r");
|
||||||
|
#endif
|
||||||
|
printf("%02d/%02d ", date.month(), date.day());
|
||||||
|
printf("%s ",
|
||||||
|
QTime::currentTime().toString("hh:mm:ss").toLatin1().constData());
|
||||||
|
fprintf(file, "%02d/%02d ", date.month(), date.day());
|
||||||
|
fprintf(file, "%s ",
|
||||||
|
QTime::currentTime().toString("hh:mm:ss").toLatin1().constData());
|
||||||
|
|
||||||
|
auto localMsg = msg.toUtf8();
|
||||||
|
auto threadName = QThread::currentThread()->objectName().toLatin1();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case QtDebugMsg:
|
||||||
|
printf("%s[D] %s\n", threadName.constData(),
|
||||||
|
localMsg.constData());
|
||||||
|
fprintf(file, "%s[D] %s\n", threadName.constData(),
|
||||||
|
localMsg.constData());
|
||||||
|
break;
|
||||||
|
case QtInfoMsg:
|
||||||
|
printf("%s[%s] %s\n", threadName.constData(),
|
||||||
|
Color("I", Green).toUtf8().constData(), localMsg.constData());
|
||||||
|
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
||||||
|
"I", localMsg.constData());
|
||||||
|
break;
|
||||||
|
case QtWarningMsg:
|
||||||
|
printf("%s[%s] %s\n", threadName.constData(),
|
||||||
|
Color("W", Yellow, Bold).toUtf8().constData(),
|
||||||
|
localMsg.constData());
|
||||||
|
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
||||||
|
"W", localMsg.constData());
|
||||||
|
break;
|
||||||
|
case QtCriticalMsg:
|
||||||
|
printf("%s[%s] %s\n", threadName.constData(),
|
||||||
|
Color("C", Red, Bold).toUtf8().constData(), localMsg.constData());
|
||||||
|
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
||||||
|
"C", localMsg.constData());
|
||||||
|
#ifndef FK_SERVER_ONLY
|
||||||
|
if (Backend != nullptr) {
|
||||||
|
Backend->notifyUI("ErrorDialog",
|
||||||
|
QString("⛔ %1/Error occured!\n %2").arg(threadName).arg(localMsg));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
case QtFatalMsg:
|
||||||
|
printf("%s[%s] %s\n", threadName.constData(),
|
||||||
|
Color("E", Red, Bold).toUtf8().constData(), localMsg.constData());
|
||||||
|
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
||||||
|
"E", localMsg.constData());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef FK_USE_READLINE
|
||||||
|
if (ShellInstance && !ShellInstance->lineDone()) {
|
||||||
|
ShellInstance->redisplay();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeKill 的程序主入口。整个程序就是从这里开始执行的。
|
||||||
|
int freekill_main(int argc, char *argv[]) {
|
||||||
|
// 初始化一下各种杂项信息
|
||||||
|
QThread::currentThread()->setObjectName("Main");
|
||||||
|
|
||||||
|
qInstallMessageHandler(fkMsgHandler);
|
||||||
|
QCoreApplication *app;
|
||||||
|
QCoreApplication::setApplicationName("FreeKill");
|
||||||
|
QCoreApplication::setApplicationVersion(FK_VERSION);
|
||||||
|
|
||||||
|
if (GetDeviceUuid() == "c5e8983a3d85a07c") return 1;
|
||||||
|
|
||||||
|
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||||
|
prepareForLinux();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!info_log) {
|
||||||
|
info_log = fopen("freekill.server.info.log", "w+");
|
||||||
|
if (!info_log) {
|
||||||
|
qFatal("Cannot open info.log");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!err_log) {
|
||||||
|
err_log = fopen("freekill.server.error.log", "w+");
|
||||||
|
if (!err_log) {
|
||||||
|
qFatal("Cannot open error.log");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析命令行,如果有 -s 或者 --server 就在命令行直接开服务器
|
||||||
|
QCommandLineParser parser;
|
||||||
|
parser.setApplicationDescription("FreeKill server");
|
||||||
|
parser.addVersionOption();
|
||||||
|
parser.addOption({{"s", "server"}, "start server at <port>", "port"});
|
||||||
|
parser.addOption({{"h", "help"}, "display help information"});
|
||||||
|
QStringList cliOptions;
|
||||||
|
for (int i = 0; i < argc; i++)
|
||||||
|
cliOptions << argv[i];
|
||||||
|
|
||||||
|
parser.parse(cliOptions);
|
||||||
|
if (parser.isSet("version")) {
|
||||||
|
parser.showVersion();
|
||||||
|
return 0;
|
||||||
|
} else if (parser.isSet("help")) {
|
||||||
|
parser.showHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool startServer = parser.isSet("server");
|
||||||
|
ushort serverPort = 9527;
|
||||||
|
|
||||||
|
if (startServer) {
|
||||||
|
app = new QCoreApplication(argc, argv);
|
||||||
|
QTranslator translator;
|
||||||
|
Q_UNUSED(translator.load("zh_CN.qm"));
|
||||||
|
QCoreApplication::installTranslator(&translator);
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
if (parser.value("server").toInt(&ok) && ok)
|
||||||
|
serverPort = parser.value("server").toInt();
|
||||||
|
|
||||||
|
Pacman = new PackMan;
|
||||||
|
Server *server = new Server;
|
||||||
|
if (!server->listen(QHostAddress::Any, serverPort)) {
|
||||||
|
qFatal("cannot listen on port %d!\n", serverPort);
|
||||||
|
app->exit(1);
|
||||||
|
} else {
|
||||||
|
qInfo("Server is listening on port %d", serverPort);
|
||||||
|
auto shell = new Shell;
|
||||||
|
shell->start();
|
||||||
|
}
|
||||||
|
return app->exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef FK_SERVER_ONLY
|
||||||
|
// 根本没编译 GUI 相关的功能,直接在此退出
|
||||||
|
qFatal("This is server-only build and have no GUI support.\n\
|
||||||
|
Please use ./FreeKill -s to start a server in command line.");
|
||||||
|
#else
|
||||||
|
|
||||||
|
app = new QApplication(argc, argv);
|
||||||
|
#ifdef DESKTOP_BUILD
|
||||||
|
((QApplication *)app)->setWindowIcon(QIcon("image/icon.png"));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SHOW_SPLASH_MSG(msg) \
|
||||||
|
splash.showMessage(msg, Qt::AlignHCenter | Qt::AlignBottom);
|
||||||
|
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
// 投降喵,设为android根本无效
|
||||||
|
// 直接改用Android原生Mediaplayer了,不用你Qt家的
|
||||||
|
// qputenv("QT_MEDIA_BACKEND", "android");
|
||||||
|
|
||||||
|
// 安卓:获取系统语言需要使用Java才行
|
||||||
|
QString localeName = QJniObject::callStaticObjectMethod("org/notify/FreeKill/Helper", "GetLocaleCode", "()Ljava/lang/String;").toString();
|
||||||
|
|
||||||
|
// 安卓:先切换到我们安装程序的那个外部存储目录去
|
||||||
|
QJniObject::callStaticMethod<void>("org/notify/FreeKill/Helper", "InitView",
|
||||||
|
"()V");
|
||||||
|
QDir::setCurrent(
|
||||||
|
"/storage/emulated/0/Android/data/org.notify.FreeKill/files");
|
||||||
|
|
||||||
|
// 然后显示欢迎界面,并在需要时复制资源素材等
|
||||||
|
QScreen *screen = qobject_cast<QApplication *>(app)->primaryScreen();
|
||||||
|
QRect screenGeometry = screen->geometry();
|
||||||
|
int screenWidth = screenGeometry.width();
|
||||||
|
int screenHeight = screenGeometry.height();
|
||||||
|
QSplashScreen splash(QPixmap("assets:/res/image/splash.jpg")
|
||||||
|
.scaled(screenWidth, screenHeight));
|
||||||
|
splash.showFullScreen();
|
||||||
|
SHOW_SPLASH_MSG("Copying resources...");
|
||||||
|
installFkAssets("assets:/res", QDir::currentPath());
|
||||||
|
|
||||||
|
info_log = freopen("freekill.server.info.log", "w+", info_log);
|
||||||
|
err_log = freopen("freekill.server.error.log", "w+", err_log);
|
||||||
|
#else
|
||||||
|
// 不是安卓,使用QLocale获得系统语言
|
||||||
|
QLocale l = QLocale::system();
|
||||||
|
auto localeName = l.name();
|
||||||
|
|
||||||
|
// 不是安卓,那么直接启动欢迎界面,也就是不复制东西了
|
||||||
|
QSplashScreen splash(QPixmap("image/splash.jpg"));
|
||||||
|
splash.show();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SHOW_SPLASH_MSG("Loading qml files...");
|
||||||
|
QQmlApplicationEngine *engine = new QQmlApplicationEngine;
|
||||||
|
#ifndef Q_OS_ANDROID
|
||||||
|
QQuickStyle::setStyle("Material");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
QTranslator translator;
|
||||||
|
if (localeName.startsWith("zh_")) {
|
||||||
|
Q_UNUSED(translator.load("zh_CN.qm"));
|
||||||
|
} else {
|
||||||
|
Q_UNUSED(translator.load("en_US.qm"));
|
||||||
|
}
|
||||||
|
QCoreApplication::installTranslator(&translator);
|
||||||
|
|
||||||
|
QmlBackend backend;
|
||||||
|
backend.setEngine(engine);
|
||||||
|
|
||||||
|
Pacman = new PackMan;
|
||||||
|
|
||||||
|
// 向 Qml 中先定义几个全局变量
|
||||||
|
auto root = engine->rootContext();
|
||||||
|
root->setContextProperty("FkVersion", FK_VERSION);
|
||||||
|
root->setContextProperty("Backend", &backend);
|
||||||
|
root->setContextProperty("ModBackend", nullptr);
|
||||||
|
root->setContextProperty("Pacman", Pacman);
|
||||||
|
root->setContextProperty("SysLocale", localeName);
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
bool debugging = true;
|
||||||
|
#else
|
||||||
|
bool debugging = false;
|
||||||
|
#endif
|
||||||
|
engine->rootContext()->setContextProperty("Debugging", debugging);
|
||||||
|
|
||||||
|
QString system;
|
||||||
|
#if defined(Q_OS_ANDROID)
|
||||||
|
system = "Android";
|
||||||
|
#elif defined(Q_OS_WIN32)
|
||||||
|
qputenv("QT_MEDIA_BACKEND", "windows");
|
||||||
|
system = "Win";
|
||||||
|
::system("chcp 65001");
|
||||||
|
#elif defined(Q_OS_LINUX)
|
||||||
|
system = "Linux";
|
||||||
|
#else
|
||||||
|
system = "Other";
|
||||||
|
#endif
|
||||||
|
root->setContextProperty("OS", system);
|
||||||
|
|
||||||
|
root->setContextProperty(
|
||||||
|
"AppPath", QUrl::fromLocalFile(QDir::currentPath()));
|
||||||
|
|
||||||
|
engine->addImportPath(QDir::currentPath());
|
||||||
|
|
||||||
|
// 加载完全局变量后,就再去加载 main.qml,此时UI界面正式显示
|
||||||
|
engine->load("Fk/main.qml");
|
||||||
|
|
||||||
|
// qml 报错了就直接退出吧
|
||||||
|
if (engine->rootObjects().isEmpty())
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// 关闭欢迎界面,然后进入Qt主循环
|
||||||
|
splash.close();
|
||||||
|
int ret = app->exec();
|
||||||
|
|
||||||
|
// 先删除 engine
|
||||||
|
// 防止报一堆错 "TypeError: Cannot read property 'xxx' of null"
|
||||||
|
delete engine;
|
||||||
|
delete Pacman;
|
||||||
|
|
||||||
|
if (info_log) fclose(info_log);
|
||||||
|
if (err_log) fclose(err_log);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
#endif
|
||||||
|
}
|
8
src/freekill.h
Normal file
8
src/freekill.h
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#ifndef _FREEKILL_H
|
||||||
|
#define _FREEKILL_H
|
||||||
|
|
||||||
|
int freekill_main(int argc, char **argv);
|
||||||
|
|
||||||
|
#endif // _FREEKILL_H
|
387
src/main.cpp
387
src/main.cpp
|
@ -1,374 +1,39 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include "client/client.h"
|
// 为了写测试而特意给程序本身单独分出一个main.cpp 顺便包含项目文档(这样真的好吗)
|
||||||
#include "core/util.h"
|
#include "freekill.h"
|
||||||
using namespace fkShell;
|
int main(int argc, char **argv) {
|
||||||
|
return freekill_main(argc, argv);
|
||||||
#include "core/packman.h"
|
|
||||||
#include "server/server.h"
|
|
||||||
#include "server/shell.h"
|
|
||||||
|
|
||||||
#if defined(Q_OS_WIN32)
|
|
||||||
#include "applink.c"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef FK_SERVER_ONLY
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QScreen>
|
|
||||||
#include <QSplashScreen>
|
|
||||||
#ifndef Q_OS_ANDROID
|
|
||||||
#include <QQuickStyle>
|
|
||||||
#endif
|
|
||||||
#include "ui/qmlbackend.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID)
|
|
||||||
static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) {
|
|
||||||
QFileInfo srcFileInfo(srcFilePath);
|
|
||||||
if (srcFileInfo.isDir()) {
|
|
||||||
QDir targetDir(tgtFilePath);
|
|
||||||
if (!targetDir.exists()) {
|
|
||||||
targetDir.cdUp();
|
|
||||||
if (!targetDir.mkdir(QFileInfo(tgtFilePath).fileName()))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
QDir sourceDir(srcFilePath);
|
|
||||||
QStringList fileNames =
|
|
||||||
sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot |
|
|
||||||
QDir::Hidden | QDir::System);
|
|
||||||
foreach (const QString &fileName, fileNames) {
|
|
||||||
const QString newSrcFilePath = srcFilePath + QLatin1Char('/') + fileName;
|
|
||||||
const QString newTgtFilePath = tgtFilePath + QLatin1Char('/') + fileName;
|
|
||||||
if (!copyPath(newSrcFilePath, newTgtFilePath))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
QFile::remove(tgtFilePath);
|
|
||||||
if (!QFile::copy(srcFilePath, tgtFilePath))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static void installFkAssets(const QString &src, const QString &dest) {
|
|
||||||
QFile f(dest + "/fk_ver");
|
|
||||||
if (f.exists() && f.open(QIODevice::ReadOnly)) {
|
|
||||||
auto ver = f.readLine().simplified();
|
|
||||||
if (ver == FK_VERSION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#ifdef Q_OS_ANDROID
|
|
||||||
copyPath(src, dest);
|
|
||||||
#elif defined(Q_OS_LINUX)
|
|
||||||
system(QString("cp -r %1 %2/..").arg(src).arg(dest).toUtf8());
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
/** @mainpage 新月杀文档 - Cpp代码部分
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
static void prepareForLinux() {
|
|
||||||
// 如果用户执行的是 /usr/bin/FreeKill,那么这意味着 freekill 是被包管理器安装
|
|
||||||
// 的,所以我们就需要把资源文件都复制到 ~/.local 中,并且切换当前目录
|
|
||||||
// TODO: AppImage
|
|
||||||
char buf[256] = {0};
|
|
||||||
int len = readlink("/proc/self/exe", buf, 256);
|
|
||||||
const char *home = getenv("HOME");
|
|
||||||
if (!strcmp(buf, "/usr/bin/FreeKill")) {
|
|
||||||
system("mkdir -p ~/.local/share/FreeKill");
|
|
||||||
installFkAssets("/usr/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home));
|
|
||||||
chdir(home);
|
|
||||||
chdir(".local/share/FreeKill");
|
|
||||||
} else if (!strcmp(buf, "/usr/local/bin/FreeKill")) {
|
|
||||||
system("mkdir -p ~/.local/share/FreeKill");
|
|
||||||
installFkAssets("/usr/local/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home));
|
|
||||||
chdir(home);
|
|
||||||
chdir(".local/share/FreeKill");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static FILE *info_log = nullptr;
|
本文档专门针对新月杀的C++代码部分,采用Doxygen生成。
|
||||||
static FILE *err_log = nullptr;
|
关于项目的主文档请参见新月之书: https://fkbook-all-in-one.readthedocs.io/
|
||||||
|
|
||||||
void fkMsgHandler(QtMsgType type, const QMessageLogContext &context,
|
> 单独分出一个Doxygen页面而不是合并在新月之书中,
|
||||||
const QString &msg) {
|
完全因为懒得给新月之书拖一个新的submodule =.=
|
||||||
auto date = QDate::currentDate();
|
|
||||||
|
|
||||||
FILE *file;
|
C++的代码位于src/文件夹下,其覆盖的功能为:
|
||||||
switch (type) {
|
|
||||||
case QtDebugMsg:
|
|
||||||
case QtInfoMsg:
|
|
||||||
file = info_log;
|
|
||||||
break;
|
|
||||||
case QtWarningMsg:
|
|
||||||
case QtCriticalMsg:
|
|
||||||
case QtFatalMsg:
|
|
||||||
file = err_log;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef FK_USE_READLINE
|
- freekill.cpp: 程序入口,Linux与Android环境下提前部署环境等
|
||||||
ShellInstance->clearLine();
|
- swig/: 基于SWIG的Lua-cpp接口。需要测试的是里面的函数定义(而非函数声明)。
|
||||||
#else
|
- ui/: Qml-cpp接口,以及一些Lua-cpp接口,借此实现lua和qml之间的交互
|
||||||
printf("\r");
|
- core/: 主要是拓展包管理,以及对于用户(玩家)的定义,以及一些第三方库的封装
|
||||||
#endif
|
- client/: 主要负责录像,以及加载client侧的Lua,功能其实不多
|
||||||
printf("%02d/%02d ", date.month(), date.day());
|
- network/: 对Qt Network模块封装,加密/压缩传输,基于JSON通信协议
|
||||||
printf("%s ",
|
- server/: 登录,大厅,房间管理,房间调度,管理员shell
|
||||||
QTime::currentTime().toString("hh:mm:ss").toLatin1().constData());
|
|
||||||
fprintf(file, "%02d/%02d ", date.month(), date.day());
|
|
||||||
fprintf(file, "%s ",
|
|
||||||
QTime::currentTime().toString("hh:mm:ss").toLatin1().constData());
|
|
||||||
|
|
||||||
auto localMsg = msg.toUtf8();
|
比较复杂的主要是服务端代码。具体可以查看每个类的文档
|
||||||
auto threadName = QThread::currentThread()->objectName().toLatin1();
|
|
||||||
|
|
||||||
switch (type) {
|
@note 为了详细说明程序运行原理,private成员也将会在文档中呈现。
|
||||||
case QtDebugMsg:
|
*/
|
||||||
printf("%s[D] %s\n", threadName.constData(),
|
|
||||||
localMsg.constData());
|
|
||||||
fprintf(file, "%s[D] %s\n", threadName.constData(),
|
|
||||||
localMsg.constData());
|
|
||||||
break;
|
|
||||||
case QtInfoMsg:
|
|
||||||
printf("%s[%s] %s\n", threadName.constData(),
|
|
||||||
Color("I", Green).toUtf8().constData(), localMsg.constData());
|
|
||||||
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
|
||||||
"I", localMsg.constData());
|
|
||||||
break;
|
|
||||||
case QtWarningMsg:
|
|
||||||
printf("%s[%s] %s\n", threadName.constData(),
|
|
||||||
Color("W", Yellow, Bold).toUtf8().constData(),
|
|
||||||
localMsg.constData());
|
|
||||||
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
|
||||||
"W", localMsg.constData());
|
|
||||||
break;
|
|
||||||
case QtCriticalMsg:
|
|
||||||
printf("%s[%s] %s\n", threadName.constData(),
|
|
||||||
Color("C", Red, Bold).toUtf8().constData(), localMsg.constData());
|
|
||||||
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
|
||||||
"C", localMsg.constData());
|
|
||||||
#ifndef FK_SERVER_ONLY
|
|
||||||
if (Backend != nullptr) {
|
|
||||||
Backend->notifyUI("ErrorDialog",
|
|
||||||
QString("⛔ %1/Error occured!\n %2").arg(threadName).arg(localMsg));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
break;
|
|
||||||
case QtFatalMsg:
|
|
||||||
printf("%s[%s] %s\n", threadName.constData(),
|
|
||||||
Color("E", Red, Bold).toUtf8().constData(), localMsg.constData());
|
|
||||||
fprintf(file, "%s[%s] %s\n", threadName.constData(),
|
|
||||||
"E", localMsg.constData());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef FK_USE_READLINE
|
/** @page page_network 网络连接
|
||||||
if (ShellInstance && !ShellInstance->lineDone()) {
|
|
||||||
ShellInstance->redisplay();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// FreeKill 的程序主入口。整个程序就是从这里开始执行的。
|
相关类:
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
// 初始化一下各种杂项信息
|
|
||||||
QThread::currentThread()->setObjectName("Main");
|
|
||||||
|
|
||||||
qInstallMessageHandler(fkMsgHandler);
|
@ref ServerSocket
|
||||||
QCoreApplication *app;
|
@ref ClientSocket
|
||||||
QCoreApplication::setApplicationName("FreeKill");
|
@ref Router
|
||||||
QCoreApplication::setApplicationVersion(FK_VERSION);
|
*/
|
||||||
|
|
||||||
if (GetDeviceUuid() == "c5e8983a3d85a07c") return 1;
|
|
||||||
|
|
||||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
|
||||||
prepareForLinux();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!info_log) {
|
|
||||||
info_log = fopen("freekill.server.info.log", "w+");
|
|
||||||
if (!info_log) {
|
|
||||||
qFatal("Cannot open info.log");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!err_log) {
|
|
||||||
err_log = fopen("freekill.server.error.log", "w+");
|
|
||||||
if (!err_log) {
|
|
||||||
qFatal("Cannot open error.log");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分析命令行,如果有 -s 或者 --server 就在命令行直接开服务器
|
|
||||||
QCommandLineParser parser;
|
|
||||||
parser.setApplicationDescription("FreeKill server");
|
|
||||||
parser.addVersionOption();
|
|
||||||
parser.addOption({{"s", "server"}, "start server at <port>", "port"});
|
|
||||||
parser.addOption({{"h", "help"}, "display help information"});
|
|
||||||
QStringList cliOptions;
|
|
||||||
for (int i = 0; i < argc; i++)
|
|
||||||
cliOptions << argv[i];
|
|
||||||
|
|
||||||
parser.parse(cliOptions);
|
|
||||||
if (parser.isSet("version")) {
|
|
||||||
parser.showVersion();
|
|
||||||
return 0;
|
|
||||||
} else if (parser.isSet("help")) {
|
|
||||||
parser.showHelp();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool startServer = parser.isSet("server");
|
|
||||||
ushort serverPort = 9527;
|
|
||||||
|
|
||||||
if (startServer) {
|
|
||||||
app = new QCoreApplication(argc, argv);
|
|
||||||
QTranslator translator;
|
|
||||||
Q_UNUSED(translator.load("zh_CN.qm"));
|
|
||||||
QCoreApplication::installTranslator(&translator);
|
|
||||||
|
|
||||||
bool ok = false;
|
|
||||||
if (parser.value("server").toInt(&ok) && ok)
|
|
||||||
serverPort = parser.value("server").toInt();
|
|
||||||
|
|
||||||
Pacman = new PackMan;
|
|
||||||
Server *server = new Server;
|
|
||||||
if (!server->listen(QHostAddress::Any, serverPort)) {
|
|
||||||
qFatal("cannot listen on port %d!\n", serverPort);
|
|
||||||
app->exit(1);
|
|
||||||
} else {
|
|
||||||
qInfo("Server is listening on port %d", serverPort);
|
|
||||||
auto shell = new Shell;
|
|
||||||
shell->start();
|
|
||||||
}
|
|
||||||
return app->exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef FK_SERVER_ONLY
|
|
||||||
// 根本没编译 GUI 相关的功能,直接在此退出
|
|
||||||
qFatal("This is server-only build and have no GUI support.\n\
|
|
||||||
Please use ./FreeKill -s to start a server in command line.");
|
|
||||||
#else
|
|
||||||
|
|
||||||
app = new QApplication(argc, argv);
|
|
||||||
#ifdef DESKTOP_BUILD
|
|
||||||
((QApplication *)app)->setWindowIcon(QIcon("image/icon.png"));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define SHOW_SPLASH_MSG(msg) \
|
|
||||||
splash.showMessage(msg, Qt::AlignHCenter | Qt::AlignBottom);
|
|
||||||
|
|
||||||
#ifdef Q_OS_ANDROID
|
|
||||||
// 投降喵,设为android根本无效
|
|
||||||
// 直接改用Android原生Mediaplayer了,不用你Qt家的
|
|
||||||
// qputenv("QT_MEDIA_BACKEND", "android");
|
|
||||||
|
|
||||||
// 安卓:获取系统语言需要使用Java才行
|
|
||||||
QString localeName = QJniObject::callStaticObjectMethod("org/notify/FreeKill/Helper", "GetLocaleCode", "()Ljava/lang/String;").toString();
|
|
||||||
|
|
||||||
// 安卓:先切换到我们安装程序的那个外部存储目录去
|
|
||||||
QJniObject::callStaticMethod<void>("org/notify/FreeKill/Helper", "InitView",
|
|
||||||
"()V");
|
|
||||||
QDir::setCurrent(
|
|
||||||
"/storage/emulated/0/Android/data/org.notify.FreeKill/files");
|
|
||||||
|
|
||||||
// 然后显示欢迎界面,并在需要时复制资源素材等
|
|
||||||
QScreen *screen = qobject_cast<QApplication *>(app)->primaryScreen();
|
|
||||||
QRect screenGeometry = screen->geometry();
|
|
||||||
int screenWidth = screenGeometry.width();
|
|
||||||
int screenHeight = screenGeometry.height();
|
|
||||||
QSplashScreen splash(QPixmap("assets:/res/image/splash.jpg")
|
|
||||||
.scaled(screenWidth, screenHeight));
|
|
||||||
splash.showFullScreen();
|
|
||||||
SHOW_SPLASH_MSG("Copying resources...");
|
|
||||||
installFkAssets("assets:/res", QDir::currentPath());
|
|
||||||
|
|
||||||
info_log = freopen("freekill.server.info.log", "w+", info_log);
|
|
||||||
err_log = freopen("freekill.server.error.log", "w+", err_log);
|
|
||||||
#else
|
|
||||||
// 不是安卓,使用QLocale获得系统语言
|
|
||||||
QLocale l = QLocale::system();
|
|
||||||
auto localeName = l.name();
|
|
||||||
|
|
||||||
// 不是安卓,那么直接启动欢迎界面,也就是不复制东西了
|
|
||||||
QSplashScreen splash(QPixmap("image/splash.jpg"));
|
|
||||||
splash.show();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SHOW_SPLASH_MSG("Loading qml files...");
|
|
||||||
QQmlApplicationEngine *engine = new QQmlApplicationEngine;
|
|
||||||
#ifndef Q_OS_ANDROID
|
|
||||||
QQuickStyle::setStyle("Material");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
QTranslator translator;
|
|
||||||
if (localeName.startsWith("zh_")) {
|
|
||||||
Q_UNUSED(translator.load("zh_CN.qm"));
|
|
||||||
} else {
|
|
||||||
Q_UNUSED(translator.load("en_US.qm"));
|
|
||||||
}
|
|
||||||
QCoreApplication::installTranslator(&translator);
|
|
||||||
|
|
||||||
QmlBackend backend;
|
|
||||||
backend.setEngine(engine);
|
|
||||||
|
|
||||||
Pacman = new PackMan;
|
|
||||||
|
|
||||||
// 向 Qml 中先定义几个全局变量
|
|
||||||
auto root = engine->rootContext();
|
|
||||||
root->setContextProperty("FkVersion", FK_VERSION);
|
|
||||||
root->setContextProperty("Backend", &backend);
|
|
||||||
root->setContextProperty("ModBackend", nullptr);
|
|
||||||
root->setContextProperty("Pacman", Pacman);
|
|
||||||
root->setContextProperty("SysLocale", localeName);
|
|
||||||
|
|
||||||
#ifdef QT_DEBUG
|
|
||||||
bool debugging = true;
|
|
||||||
#else
|
|
||||||
bool debugging = false;
|
|
||||||
#endif
|
|
||||||
engine->rootContext()->setContextProperty("Debugging", debugging);
|
|
||||||
|
|
||||||
QString system;
|
|
||||||
#if defined(Q_OS_ANDROID)
|
|
||||||
system = "Android";
|
|
||||||
#elif defined(Q_OS_WIN32)
|
|
||||||
qputenv("QT_MEDIA_BACKEND", "windows");
|
|
||||||
system = "Win";
|
|
||||||
::system("chcp 65001");
|
|
||||||
#elif defined(Q_OS_LINUX)
|
|
||||||
system = "Linux";
|
|
||||||
#else
|
|
||||||
system = "Other";
|
|
||||||
#endif
|
|
||||||
root->setContextProperty("OS", system);
|
|
||||||
|
|
||||||
root->setContextProperty(
|
|
||||||
"AppPath", QUrl::fromLocalFile(QDir::currentPath()));
|
|
||||||
|
|
||||||
engine->addImportPath(QDir::currentPath());
|
|
||||||
|
|
||||||
// 加载完全局变量后,就再去加载 main.qml,此时UI界面正式显示
|
|
||||||
engine->load("Fk/main.qml");
|
|
||||||
|
|
||||||
// qml 报错了就直接退出吧
|
|
||||||
if (engine->rootObjects().isEmpty())
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
// 关闭欢迎界面,然后进入Qt主循环
|
|
||||||
splash.close();
|
|
||||||
int ret = app->exec();
|
|
||||||
|
|
||||||
// 先删除 engine
|
|
||||||
// 防止报一堆错 "TypeError: Cannot read property 'xxx' of null"
|
|
||||||
delete engine;
|
|
||||||
delete Pacman;
|
|
||||||
|
|
||||||
if (info_log) fclose(info_log);
|
|
||||||
if (err_log) fclose(err_log);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,40 +5,96 @@
|
||||||
|
|
||||||
#include <openssl/aes.h>
|
#include <openssl/aes.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief 基于TCP协议实现双端消息收发,支持加密传输和压缩传输
|
||||||
|
|
||||||
|
QTcpSocket的封装,提供收发数据的功能。当客户端想要向服务端发起连接时,客户端
|
||||||
|
先构造ClientSocket对象,然后调用connectToHost;服务端收到后也构造一个
|
||||||
|
ClientSocket对象用来与其进行一对一的通信,一方调用send便可触发另一方的
|
||||||
|
message_got信号。
|
||||||
|
|
||||||
|
### 压缩传输
|
||||||
|
|
||||||
|
当消息长度超过1024时,利用qCompress进行压缩,将压缩后的内容通过base64编码并
|
||||||
|
携带上"Compressed"头部形成新消息。后续也根据这个规则解码。
|
||||||
|
|
||||||
|
> 参见send方法与getMessage方法。
|
||||||
|
|
||||||
|
### 加密传输
|
||||||
|
|
||||||
|
当设置了AES密钥时,使用AES将数据加密后再传输。
|
||||||
|
|
||||||
|
加密算法采用AES-128-CFB模式,密钥由用户提供(随机生成),通过RSA与口令一同加密
|
||||||
|
后发送至服务器,这样服务器就得到了一致的AES密钥。加密时,先随机生成IV,将IV与
|
||||||
|
base64编码后密文拼接后作为最终要发送至服务器的密文。解密时先处理好IV与原始密文,
|
||||||
|
再通过AES解密获取明文消息。
|
||||||
|
|
||||||
|
> 参见aesEnc与aesDec私有方法。
|
||||||
|
*/
|
||||||
class ClientSocket : public QObject {
|
class ClientSocket : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/// 客户端使用的构造函数,构造QTcpSocket和ClientSocket本身
|
||||||
ClientSocket();
|
ClientSocket();
|
||||||
// For server use
|
/** 服务端使用的构造函数,当新连接传入后Qt库已为此构造了QTcpSocket,
|
||||||
|
基于Qt构造的QTcpSocket构造新的ClientSocket。
|
||||||
|
*/
|
||||||
ClientSocket(QTcpSocket *socket);
|
ClientSocket(QTcpSocket *socket);
|
||||||
|
|
||||||
|
/// 客户端使用,用于连接到远程服务器
|
||||||
void connectToHost(const QString &address = "127.0.0.1", ushort port = 9527u);
|
void connectToHost(const QString &address = "127.0.0.1", ushort port = 9527u);
|
||||||
|
/// 双端都可使用。禁用加密传输并断开TCP连接。
|
||||||
void disconnectFromHost();
|
void disconnectFromHost();
|
||||||
|
/// 设置AES密钥,同时启用加密传输。
|
||||||
void installAESKey(const QByteArray &key);
|
void installAESKey(const QByteArray &key);
|
||||||
|
/// 发送消息。参见加密传输与压缩传输
|
||||||
void send(const QByteArray& msg);
|
void send(const QByteArray& msg);
|
||||||
|
/// 判断是否处于已连接状态
|
||||||
|
///
|
||||||
|
/// @todo 这个函数好好像没用上?产生bloat了?
|
||||||
bool isConnected() const;
|
bool isConnected() const;
|
||||||
|
/// 对等端的名字(地址:端口)
|
||||||
QString peerName() const;
|
QString peerName() const;
|
||||||
|
/// 对等端的地址
|
||||||
QString peerAddress() const;
|
QString peerAddress() const;
|
||||||
QTimer timerSignup;
|
QTimer timerSignup; ///< 创建连接时,若该计时器超时,则断开连接
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
/// 收到一条消息时触发的信号
|
||||||
void message_got(const QByteArray& msg);
|
void message_got(const QByteArray& msg);
|
||||||
|
/// 产生报错信息触发的信号,连接到UI中的函数
|
||||||
void error_message(const QString &msg);
|
void error_message(const QString &msg);
|
||||||
|
/// 断开连接时的信号
|
||||||
void disconnected();
|
void disconnected();
|
||||||
|
/// 连接创建时的信号
|
||||||
void connected();
|
void connected();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
/**
|
||||||
|
连接QTcpSocket::messageReady,按每行一条消息依次触发message_get信号。
|
||||||
|
|
||||||
|
若启用了加密传输,则消息在取出时先被解密。
|
||||||
|
|
||||||
|
若消息以"Compressed"开头,则将剩余部分作为被压缩内容,进行base64解码并解压缩。
|
||||||
|
|
||||||
|
完成上述预处理后便取得了消息的实际内容,再触发message_get信号传给上层处理。
|
||||||
|
*/
|
||||||
void getMessage();
|
void getMessage();
|
||||||
|
/// 连接QTcpSocket::errorOccured,负责在UI显示网络错误信息
|
||||||
void raiseError(QAbstractSocket::SocketError error);
|
void raiseError(QAbstractSocket::SocketError error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/// AES加密
|
||||||
QByteArray aesEnc(const QByteArray &in);
|
QByteArray aesEnc(const QByteArray &in);
|
||||||
|
/// AES解密
|
||||||
QByteArray aesDec(const QByteArray &out);
|
QByteArray aesDec(const QByteArray &out);
|
||||||
AES_KEY aes_key;
|
/// 与QTcpSocket连接信号槽
|
||||||
bool aes_ready;
|
|
||||||
QTcpSocket *socket;
|
|
||||||
void init();
|
void init();
|
||||||
|
|
||||||
|
AES_KEY aes_key; ///< AES密钥
|
||||||
|
bool aes_ready; ///< 表明是否启用AES加密传输
|
||||||
|
QTcpSocket *socket; ///< 用于实际发送数据的socket
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // _CLIENT_SOCKET_H
|
#endif // _CLIENT_SOCKET_H
|
||||||
|
|
|
@ -5,16 +5,26 @@
|
||||||
|
|
||||||
class ClientSocket;
|
class ClientSocket;
|
||||||
|
|
||||||
|
/** @brief 实现通信协议,负责传输结构化消息而不是字面上的文本信息。
|
||||||
|
|
||||||
|
Router是对\ref ClientSocket 的又一次封装。ClientSocket解决的是传输字符串的
|
||||||
|
问题,Router要解决的则是实现协议中的两种类型消息的传输:Request-Reply以及
|
||||||
|
Notify这两种。
|
||||||
|
*/
|
||||||
class Router : public QObject {
|
class Router : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
该枚举揭示了一个Packet的类型,在实际使用中,Packet的类型以TYPE、SRC、
|
||||||
|
DEST这几种枚举通过按位与的方式拼接而成。
|
||||||
|
*/
|
||||||
enum PacketType {
|
enum PacketType {
|
||||||
TYPE_REQUEST = 0x100,
|
TYPE_REQUEST = 0x100, ///< 类型为Request的包
|
||||||
TYPE_REPLY = 0x200,
|
TYPE_REPLY = 0x200, ///< 类型为Reply的包
|
||||||
TYPE_NOTIFICATION = 0x400,
|
TYPE_NOTIFICATION = 0x400, ///< 类型为Notify的包
|
||||||
SRC_CLIENT = 0x010,
|
SRC_CLIENT = 0x010, ///< 从客户端发出的包
|
||||||
SRC_SERVER = 0x020,
|
SRC_SERVER = 0x020, ///< 从服务端发出的包
|
||||||
SRC_LOBBY = 0x040,
|
SRC_LOBBY = 0x040,
|
||||||
DEST_CLIENT = 0x001,
|
DEST_CLIENT = 0x001,
|
||||||
DEST_SERVER = 0x002,
|
DEST_SERVER = 0x002,
|
||||||
|
|
|
@ -5,26 +5,44 @@
|
||||||
|
|
||||||
class ClientSocket;
|
class ClientSocket;
|
||||||
|
|
||||||
// 只是对QTcpServer的简单封装
|
/**
|
||||||
|
@brief 向Server转达新的连接请求,并负责显示服务器信息。
|
||||||
|
|
||||||
|
ServerSocket是对QTcpServer与QUdpSocket的封装。
|
||||||
|
|
||||||
|
功能有:
|
||||||
|
- 当接受到TCP连接请求时,创建新的@ref ClientSocket 并向@ref Server 发送信号。
|
||||||
|
- 当接受到格式正确的UDP报文时,发回关于服务器的信息。
|
||||||
|
*/
|
||||||
class ServerSocket : public QObject {
|
class ServerSocket : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
创建新的ServerSocket对象。
|
||||||
|
|
||||||
|
仅用于@ref Server 的构造函数中,作为Server的一个子成员。
|
||||||
|
*/
|
||||||
ServerSocket(QObject *parent = nullptr);
|
ServerSocket(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/// 监听端口port,TCP和UDP的都监听
|
||||||
bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u);
|
bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
/// 接收到新连接时,创建新的socket对象并发出该信号
|
||||||
void new_connection(ClientSocket *socket);
|
void new_connection(ClientSocket *socket);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
// 新建一个ClientSocket,然后立刻交给Server相关函数处理。
|
/// 新建一个ClientSocket,然后立刻交给Server相关函数处理。
|
||||||
void processNewConnection();
|
void processNewConnection();
|
||||||
|
/// 对每条收到的UDP报文调用processDatagram
|
||||||
void readPendingDatagrams();
|
void readPendingDatagrams();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTcpServer *server;
|
QTcpServer *server; ///< 监听TCP连接用
|
||||||
QUdpSocket *udpSocket; // 服务器列表页面显示服务器信息用
|
QUdpSocket *udpSocket; ///< 显示服务器信息用
|
||||||
|
|
||||||
|
/// 对udp报文`msg`进行分析,addr和port是报文发送者传来的信息
|
||||||
void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port);
|
void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -106,13 +106,12 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity,
|
||||||
nextRoomId++;
|
nextRoomId++;
|
||||||
room->setAbandoned(false);
|
room->setAbandoned(false);
|
||||||
thread->addRoom(room);
|
thread->addRoom(room);
|
||||||
rooms.insert(room->getId(), room);
|
|
||||||
} else {
|
} else {
|
||||||
room = new Room(thread);
|
room = new Room(thread);
|
||||||
connect(room, &Room::abandoned, this, &Server::onRoomAbandoned);
|
connect(room, &Room::abandoned, this, &Server::onRoomAbandoned);
|
||||||
rooms.insert(room->getId(), room);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rooms.insert(room->getId(), room);
|
||||||
room->setName(name);
|
room->setName(name);
|
||||||
room->setCapacity(capacity);
|
room->setCapacity(capacity);
|
||||||
room->setTimeout(timeout);
|
room->setTimeout(timeout);
|
||||||
|
|
|
@ -12,29 +12,60 @@ class Lobby;
|
||||||
|
|
||||||
#include "server/room.h"
|
#include "server/room.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Server类负责管理游戏服务端的运行。
|
||||||
|
|
||||||
|
该类用于服务端程序运行。当客户端使用单机启动时,Server类也会被实例化。
|
||||||
|
Server的具体运行逻辑依托于Qt的事件循环与信号槽机制:当Server被创建后,
|
||||||
|
调用listen方法即可完成监听,然后程序进入事件循环,通过触发Server的槽函数
|
||||||
|
以实现各种功能。
|
||||||
|
|
||||||
|
### 配置信息
|
||||||
|
|
||||||
|
### 用户管理
|
||||||
|
|
||||||
|
*/
|
||||||
class Server : public QObject {
|
class Server : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/// 构造Server对象。见于main函数
|
||||||
explicit Server(QObject *parent = nullptr);
|
explicit Server(QObject *parent = nullptr);
|
||||||
~Server();
|
~Server();
|
||||||
|
|
||||||
|
/// 监听端口
|
||||||
bool listen(const QHostAddress &address = QHostAddress::Any,
|
bool listen(const QHostAddress &address = QHostAddress::Any,
|
||||||
ushort port = 9527u);
|
ushort port = 9527u);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief 创建新的房间并加入到房间列表中。
|
||||||
|
|
||||||
|
创建新的房间。
|
||||||
|
|
||||||
|
首先,房间名是用户自定义输入内容,需要先进行内容安全检查;然后创建新房间。
|
||||||
|
在创建新房间时,游戏会避免创建新的Room对象;有一个idle_rooms存储着创建过
|
||||||
|
但目前没在使用中的房间,游戏优先从其中选择一个作为“新房间”,如果没有这样的
|
||||||
|
房间,那么就构造一个新的Room对象。
|
||||||
|
|
||||||
|
之后,将新的room添加到rooms表中;然后将参数中指定的各个属性都赋予给新的
|
||||||
|
房间,通过addPlayer将房主添加到房间中,并使用setOwner将其设为房主。
|
||||||
|
|
||||||
|
@param owner 创建房间的那名玩家;房主
|
||||||
|
@param settings 表示JSON对象的字符串,用作房间配置
|
||||||
|
*/
|
||||||
void createRoom(ServerPlayer *owner, const QString &name, int capacity,
|
void createRoom(ServerPlayer *owner, const QString &name, int capacity,
|
||||||
int timeout = 15, const QByteArray &settings = "{}");
|
int timeout = 15, const QByteArray &settings = "{}");
|
||||||
|
|
||||||
Room *findRoom(int id) const;
|
Room *findRoom(int id) const; /// 获取对应id的房间
|
||||||
Lobby *lobby() const;
|
Lobby *lobby() const; /// 获取大厅对象
|
||||||
|
|
||||||
RoomThread *createThread();
|
RoomThread *createThread(); /// 创建新的RoomThread,并加入列表
|
||||||
void removeThread(RoomThread *thread);
|
void removeThread(RoomThread *thread); /// 从列表中移除thread
|
||||||
|
|
||||||
ServerPlayer *findPlayer(int id) const;
|
ServerPlayer *findPlayer(int id) const; /// 获取对应id的玩家
|
||||||
void addPlayer(ServerPlayer *player);
|
void addPlayer(ServerPlayer *player); /// 将玩家加入表中,若重复则覆盖旧的
|
||||||
void removePlayer(int id);
|
void removePlayer(int id); /// 从表中删除对应id的玩家
|
||||||
auto getPlayers() { return players; }
|
auto getPlayers() { return players; } /// 获取players表
|
||||||
|
|
||||||
void updateRoomList(ServerPlayer *teller);
|
void updateRoomList(ServerPlayer *teller);
|
||||||
void updateOnlineInfo();
|
void updateOnlineInfo();
|
||||||
|
@ -81,14 +112,20 @@ private:
|
||||||
QList<QString> temp_banlist;
|
QList<QString> temp_banlist;
|
||||||
|
|
||||||
AuthManager *auth;
|
AuthManager *auth;
|
||||||
sqlite3 *db;
|
sqlite3 *db; ///< sqlite数据库连接实例
|
||||||
QMutex transaction_mutex;
|
QMutex transaction_mutex; ///< 可能有多线程同时对数据库请求,需要加锁
|
||||||
QString md5;
|
QString md5; ///< 服务端当前允许用户登录的MD5值
|
||||||
|
|
||||||
QJsonObject config;
|
/**
|
||||||
|
读取配置文件。配置文件的路径是`<pwd>/freekill.server.config.json`。
|
||||||
|
|
||||||
|
若读取失败(包含文件不存在、有语法错误等情况),则使用一个空JSON对象;
|
||||||
|
否则使用从文件读取并解析后的JSON对象。最后为一些必须存在而实际为空值的key设置默认值。
|
||||||
|
*/
|
||||||
void readConfig();
|
void readConfig();
|
||||||
|
QJsonObject config; ///< 配置文件其实就是一个JSON对象
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Server *ServerInstance;
|
extern Server *ServerInstance; ///< 全局Server对象
|
||||||
|
|
||||||
#endif // _SERVER_H
|
#endif // _SERVER_H
|
||||||
|
|
242
src/ui/mod.cpp
242
src/ui/mod.cpp
|
@ -1,242 +0,0 @@
|
||||||
#include "mod.h"
|
|
||||||
#include "git2.h"
|
|
||||||
#include "util.h"
|
|
||||||
#include <openssl/rsa.h>
|
|
||||||
#include <openssl/bn.h>
|
|
||||||
#include <openssl/pem.h>
|
|
||||||
#include <qfiledevice.h>
|
|
||||||
|
|
||||||
ModMaker::ModMaker(QObject *parent) : QObject(parent) {
|
|
||||||
git_libgit2_init();
|
|
||||||
#ifdef Q_OS_ANDROID
|
|
||||||
git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, "./certs");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!QDir("mymod").exists()) {
|
|
||||||
QDir(".").mkdir("mymod");
|
|
||||||
}
|
|
||||||
|
|
||||||
// db = OpenDatabase("mymod/packages.db", "packages/mymod.sql");
|
|
||||||
}
|
|
||||||
|
|
||||||
ModMaker::~ModMaker() {
|
|
||||||
// git_libgit2_shutdown();
|
|
||||||
// sqlite3_close(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
// copied from https://stackoverflow.com/questions/1011572/convert-pem-key-to-ssh-rsa-format
|
|
||||||
static unsigned char pSshHeader[11] = { 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2D, 0x72, 0x73, 0x61};
|
|
||||||
|
|
||||||
static int SshEncodeBuffer(unsigned char *pEncoding, int bufferLen, unsigned char* pBuffer) {
|
|
||||||
int adjustedLen = bufferLen, index;
|
|
||||||
if (*pBuffer & 0x80) {
|
|
||||||
adjustedLen++;
|
|
||||||
pEncoding[4] = 0;
|
|
||||||
index = 5;
|
|
||||||
} else {
|
|
||||||
index = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
pEncoding[0] = (unsigned char) (adjustedLen >> 24);
|
|
||||||
pEncoding[1] = (unsigned char) (adjustedLen >> 16);
|
|
||||||
pEncoding[2] = (unsigned char) (adjustedLen >> 8);
|
|
||||||
pEncoding[3] = (unsigned char) (adjustedLen );
|
|
||||||
memcpy(&pEncoding[index], pBuffer, bufferLen);
|
|
||||||
return index + bufferLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void initSSHKeyPair() {
|
|
||||||
if (!QFile::exists("mymod/id_rsa.pub")) {
|
|
||||||
RSA *rsa = RSA_new();
|
|
||||||
BIGNUM *bne = BN_new();
|
|
||||||
BN_set_word(bne, RSA_F4);
|
|
||||||
RSA_generate_key_ex(rsa, 3072, bne, NULL);
|
|
||||||
|
|
||||||
BIO *bp_pri = BIO_new_file("mymod/id_rsa", "w");
|
|
||||||
PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL);
|
|
||||||
BIO_free_all(bp_pri);
|
|
||||||
QFile("mymod/id_rsa").setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
|
||||||
|
|
||||||
auto n = RSA_get0_n(rsa);
|
|
||||||
auto e = RSA_get0_e(rsa);
|
|
||||||
auto nLen = BN_num_bytes(n);
|
|
||||||
auto eLen = BN_num_bytes(e);
|
|
||||||
auto nBytes = (unsigned char *)malloc(nLen);
|
|
||||||
auto eBytes = (unsigned char *)malloc(eLen);
|
|
||||||
BN_bn2bin(n, nBytes);
|
|
||||||
BN_bn2bin(e, eBytes);
|
|
||||||
|
|
||||||
auto encodingLength = 11 + 4 + eLen + 4 + nLen;
|
|
||||||
// correct depending on the MSB of e and N
|
|
||||||
if (eBytes[0] & 0x80)
|
|
||||||
encodingLength++;
|
|
||||||
if (nBytes[0] & 0x80)
|
|
||||||
encodingLength++;
|
|
||||||
|
|
||||||
auto pEncoding = (unsigned char *)malloc(encodingLength);
|
|
||||||
memcpy(pEncoding, pSshHeader, 11);
|
|
||||||
int index = 0;
|
|
||||||
index = SshEncodeBuffer(&pEncoding[11], eLen, eBytes);
|
|
||||||
index = SshEncodeBuffer(&pEncoding[11 + index], nLen, nBytes);
|
|
||||||
|
|
||||||
auto b64 = BIO_new(BIO_f_base64());
|
|
||||||
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
|
|
||||||
auto bio = BIO_new_file("mymod/id_rsa.pub", "w");
|
|
||||||
BIO_printf(bio, "ssh-rsa ");
|
|
||||||
bio = BIO_push(b64, bio);
|
|
||||||
BIO_write(bio, pEncoding, encodingLength);
|
|
||||||
BIO_flush(bio);
|
|
||||||
bio = BIO_pop(b64);
|
|
||||||
BIO_printf(bio, " FreeKill\n");
|
|
||||||
BIO_flush(bio);
|
|
||||||
|
|
||||||
BIO_free_all(bio);
|
|
||||||
BIO_free_all(b64);
|
|
||||||
BN_free(bne);
|
|
||||||
RSA_free(rsa);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModMaker::initKey() { initSSHKeyPair(); }
|
|
||||||
|
|
||||||
QString ModMaker::readFile(const QString &fileName) {
|
|
||||||
QFile conf(fileName);
|
|
||||||
if (!conf.exists()) {
|
|
||||||
conf.open(QIODevice::WriteOnly);
|
|
||||||
static const char *init_conf = "{}";
|
|
||||||
conf.write(init_conf);
|
|
||||||
conf.close();
|
|
||||||
return init_conf;
|
|
||||||
}
|
|
||||||
conf.open(QIODevice::ReadOnly);
|
|
||||||
QString ret = conf.readAll();
|
|
||||||
conf.close();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModMaker::saveToFile(const QString &fName, const QString &content) {
|
|
||||||
QFile c(fName);
|
|
||||||
c.open(QIODevice::WriteOnly);
|
|
||||||
c.write(content.toUtf8());
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModMaker::mkdir(const QString &path) {
|
|
||||||
QDir(".").mkdir(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModMaker::rmrf(const QString &path) {
|
|
||||||
QDir(path).removeRecursively();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModMaker::createMod(const QString &name) {
|
|
||||||
init(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModMaker::removeMod(const QString &name) {
|
|
||||||
QDir("mymod/" + name).removeRecursively();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModMaker::commitChanges(const QString &name, const QString &msg,
|
|
||||||
const QString &user, const QString &email)
|
|
||||||
{
|
|
||||||
auto userBytes = user.toUtf8();
|
|
||||||
auto emailBytes = email.toUtf8();
|
|
||||||
commit(name, msg, userBytes, emailBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#define GIT_FAIL \
|
|
||||||
const git_error *e = git_error_last(); \
|
|
||||||
qCritical("Error %d/%d: %s\n", error, e->klass, e->message)
|
|
||||||
|
|
||||||
#define GIT_CHK(s) do { \
|
|
||||||
error = (s); \
|
|
||||||
if (error < 0) { \
|
|
||||||
GIT_FAIL; \
|
|
||||||
goto clean; \
|
|
||||||
}} while (0)
|
|
||||||
|
|
||||||
static int fk_cred_cb(git_cred **out, const char *url, const char *name,
|
|
||||||
unsigned int allowed_types, void *payload)
|
|
||||||
{
|
|
||||||
initSSHKeyPair();
|
|
||||||
return git_cred_ssh_key_new(out, "git", "mymod/id_rsa.pub", "mymod/id_rsa", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
int ModMaker::init(const QString &pkg) {
|
|
||||||
QString path = "mymod/" + pkg;
|
|
||||||
int error;
|
|
||||||
git_repository *repo = NULL;
|
|
||||||
git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
|
|
||||||
opts.flags |= GIT_REPOSITORY_INIT_MKPATH; /* mkdir as needed to create repo */
|
|
||||||
error = git_repository_init_ext(&repo, path.toLatin1().constData(), &opts);
|
|
||||||
if (error < 0) {
|
|
||||||
GIT_FAIL;
|
|
||||||
}
|
|
||||||
git_repository_free(repo);
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ModMaker::add(const QString &pkg) {
|
|
||||||
QString path = "mymod/" + pkg;
|
|
||||||
int error;
|
|
||||||
git_repository *repo = NULL;
|
|
||||||
git_index *index = NULL;
|
|
||||||
|
|
||||||
GIT_CHK(git_repository_open(&repo, path.toLatin1()));
|
|
||||||
GIT_CHK(git_repository_index(&index, repo));
|
|
||||||
GIT_CHK(git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL));
|
|
||||||
GIT_CHK(git_index_write(index));
|
|
||||||
|
|
||||||
clean:
|
|
||||||
git_repository_free(repo);
|
|
||||||
git_index_free(index);
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ModMaker::commit(const QString &pkg, const QString &msg, const char *user, const char *email) {
|
|
||||||
QString path = "mymod/" + pkg;
|
|
||||||
int error;
|
|
||||||
git_repository *repo = NULL;
|
|
||||||
git_oid commit_oid,tree_oid;
|
|
||||||
git_tree *tree;
|
|
||||||
git_index *index;
|
|
||||||
git_object *parent = NULL;
|
|
||||||
git_reference *ref = NULL;
|
|
||||||
git_signature *signature;
|
|
||||||
|
|
||||||
GIT_CHK(git_repository_open(&repo, path.toLatin1()));
|
|
||||||
error = git_revparse_ext(&parent, &ref, repo, "HEAD");
|
|
||||||
if (error == GIT_ENOTFOUND) {
|
|
||||||
// printf("HEAD not found. Creating first commit\n");
|
|
||||||
error = 0;
|
|
||||||
} else if (error != 0) {
|
|
||||||
GIT_FAIL;
|
|
||||||
goto clean;
|
|
||||||
}
|
|
||||||
|
|
||||||
GIT_CHK(git_repository_index(&index, repo));
|
|
||||||
GIT_CHK(git_index_write_tree(&tree_oid, index));
|
|
||||||
GIT_CHK(git_index_write(index));
|
|
||||||
GIT_CHK(git_tree_lookup(&tree, repo, &tree_oid));
|
|
||||||
GIT_CHK(git_signature_now(&signature, user, email));
|
|
||||||
GIT_CHK(git_commit_create_v(
|
|
||||||
&commit_oid,
|
|
||||||
repo,
|
|
||||||
"HEAD",
|
|
||||||
signature,
|
|
||||||
signature,
|
|
||||||
NULL,
|
|
||||||
msg.toUtf8(),
|
|
||||||
tree,
|
|
||||||
parent ? 1 : 0, parent));
|
|
||||||
|
|
||||||
clean:
|
|
||||||
git_repository_free(repo);
|
|
||||||
git_index_free(index);
|
|
||||||
git_signature_free(signature);
|
|
||||||
git_tree_free(tree);
|
|
||||||
git_object_free(parent);
|
|
||||||
git_reference_free(ref);
|
|
||||||
return error;
|
|
||||||
}
|
|
35
src/ui/mod.h
35
src/ui/mod.h
|
@ -1,35 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
#ifndef _DIY_H
|
|
||||||
#define _DIY_H
|
|
||||||
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
class ModMaker : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
ModMaker(QObject *parent = nullptr);
|
|
||||||
~ModMaker();
|
|
||||||
|
|
||||||
Q_INVOKABLE void initKey();
|
|
||||||
|
|
||||||
Q_INVOKABLE QString readFile(const QString &fileName);
|
|
||||||
Q_INVOKABLE void saveToFile(const QString &fileName, const QString &content);
|
|
||||||
Q_INVOKABLE void mkdir(const QString &path);
|
|
||||||
Q_INVOKABLE void rmrf(const QString &path);
|
|
||||||
|
|
||||||
Q_INVOKABLE void createMod(const QString &name);
|
|
||||||
Q_INVOKABLE void removeMod(const QString &name);
|
|
||||||
Q_INVOKABLE void stageFiles(const QString &name) { add(name); }
|
|
||||||
Q_INVOKABLE void commitChanges(const QString &name, const QString &msg,
|
|
||||||
const QString &user, const QString &email);
|
|
||||||
|
|
||||||
private:
|
|
||||||
sqlite3 *db;
|
|
||||||
|
|
||||||
// git functions
|
|
||||||
int init(const QString &pkg);
|
|
||||||
int add(const QString &pkg);
|
|
||||||
int commit(const QString &pkg, const QString &msg, const char *user, const char *email);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
13
test/CMakeLists.txt
Normal file
13
test/CMakeLists.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||||
|
|
||||||
|
set (TEST_LIB
|
||||||
|
libFreeKill
|
||||||
|
${QT_LIB}
|
||||||
|
Qt6::Test
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_executable(Test test.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(Test PRIVATE ${TEST_LIB})
|
||||||
|
|
||||||
|
add_test(NAME mytest COMMAND Test)
|
15
test/test.cpp
Normal file
15
test/test.cpp
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
class TestQString: public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
void toUpper();
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestQString::toUpper() {
|
||||||
|
QString str = "Hello";
|
||||||
|
QCOMPARE(str.toUpper(), QString("HELLO,"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestQString)
|
||||||
|
#include "test.moc"
|
Loading…
Reference in New Issue
Block a user