Doxygen and test (#385)
Some checks failed
Check Whitespace and New Line / check (push) Has been cancelled
Deploy Doxygen to Pages / pages (push) Has been cancelled

- 删除ModMaker
- 拆出main.cpp以便CMake
- Doxygen注释,以及CI(没测试过)
This commit is contained in:
notify 2024-10-17 19:00:02 +08:00 committed by GitHub
parent 354e0ba42e
commit f0ffd68ff2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 983 additions and 1441 deletions

View File

@ -1,4 +1,4 @@
name: Deploy Sphinx documentation to Pages
name: Deploy Doxygen to Pages
# Runs on pushes targeting the default branch
on:
@ -15,5 +15,15 @@ jobs:
pages: write
id-token: write
steps:
- id: deployment
uses: sphinx-notes/pages@v3
- id: build
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'

View File

@ -77,3 +77,6 @@ add_custom_command(
)
add_subdirectory(src)
enable_testing()
add_subdirectory(test)

View File

@ -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: {
}
}

View File

@ -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');
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}
}

View File

@ -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);
}
}

View File

@ -1,2 +0,0 @@
module Fk.ModMaker
ModMaker 1.0 main.qml

364
docs/Doxyfile Normal file
View 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 =

View File

@ -1,7 +1,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
set(freekill_SRCS
"main.cpp"
# "main.cpp"
"freekill.cpp"
"core/player.cpp"
"core/util.cpp"
"core/packman.cpp"
@ -95,9 +96,9 @@ else ()
set(GIT_LIB git2)
endif ()
target_sources(FreeKill PRIVATE ${freekill_SRCS})
target_precompile_headers(FreeKill PRIVATE "pch.h")
target_link_libraries(FreeKill PRIVATE
add_library(libFreeKill STATIC ${freekill_SRCS})
target_precompile_headers(libFreeKill PRIVATE "pch.h")
target_link_libraries(libFreeKill PRIVATE
${LUA_LIB}
${SQLITE3_LIB}
${CRYPTO_LIB}
@ -108,6 +109,10 @@ target_link_libraries(FreeKill PRIVATE
${GIT_LIB}
${IDBFS_LIB}
)
target_sources(FreeKill PRIVATE main.cpp)
target_link_libraries(FreeKill PRIVATE
libFreeKill
)
install(TARGETS FreeKill DESTINATION bin)
install(DIRECTORY

View File

@ -1,5 +1,14 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/** @file util.h
util.h负责提供各种全局函数
@todo C库时C++
*/
#ifndef _GLOBAL_H
#define _GLOBAL_H

374
src/freekill.cpp Normal file
View 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
View 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

View File

@ -1,374 +1,39 @@
// 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
// 为了写测试而特意给程序本身单独分出一个main.cpp 顺便包含项目文档(这样真的好吗)
#include "freekill.h"
int main(int argc, char **argv) {
return freekill_main(argc, argv);
}
#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
/** @mainpage 新月杀文档 - Cpp代码部分
static FILE *info_log = nullptr;
static FILE *err_log = nullptr;
C++Doxygen生成
https://fkbook-all-in-one.readthedocs.io/
void fkMsgHandler(QtMsgType type, const QMessageLogContext &context,
const QString &msg) {
auto date = QDate::currentDate();
> Doxygen页面而不是合并在新月之书中
submodule =.=
FILE *file;
switch (type) {
case QtDebugMsg:
case QtInfoMsg:
file = info_log;
break;
case QtWarningMsg:
case QtCriticalMsg:
case QtFatalMsg:
file = err_log;
break;
}
C++src/
#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());
- freekill.cpp: Linux与Android环境下提前部署环境等
- swig/: SWIG的Lua-cpp接口
- ui/: Qml-cpp接口Lua-cpp接口lua和qml之间的交互
- core/:
- client/: client侧的Lua
- network/: Qt Network模块封装/JSON通信协议
- server/: shell
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;
}
@note private成员也将会在文档中呈现
*/
#ifdef FK_USE_READLINE
if (ShellInstance && !ShellInstance->lineDone()) {
ShellInstance->redisplay();
}
#endif
}
/** @page page_network 网络连接
// FreeKill 的程序主入口。整个程序就是从这里开始执行的。
int 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
}
@ref ServerSocket
@ref ClientSocket
@ref Router
*/

View File

@ -5,40 +5,96 @@
#include <openssl/aes.h>
/**
@brief TCP协议实现双端消息收发
QTcpSocket的封装
ClientSocket对象connectToHost
ClientSocket对象用来与其进行一对一的通信send便可触发另一方的
message_got信号
### 压缩传输
1024qCompress进行压缩base64编码并
"Compressed"
> send方法与getMessage方法
### 加密传输
AES密钥时使AES将数据加密后再传输
AES-128-CFB模式RSA与口令一同加密
AES密钥IVIV与
base64编码后密文拼接后作为最终要发送至服务器的密文IV与原始密文
AES解密获取明文消息
> aesEnc与aesDec私有方法
*/
class ClientSocket : public QObject {
Q_OBJECT
public:
/// 客户端使用的构造函数构造QTcpSocket和ClientSocket本身
ClientSocket();
// For server use
/** 服务端使用的构造函数当新连接传入后Qt库已为此构造了QTcpSocket
Qt构造的QTcpSocket构造新的ClientSocket
*/
ClientSocket(QTcpSocket *socket);
/// 客户端使用,用于连接到远程服务器
void connectToHost(const QString &address = "127.0.0.1", ushort port = 9527u);
/// 双端都可使用。禁用加密传输并断开TCP连接。
void disconnectFromHost();
/// 设置AES密钥同时启用加密传输。
void installAESKey(const QByteArray &key);
/// 发送消息。参见加密传输与压缩传输
void send(const QByteArray& msg);
/// 判断是否处于已连接状态
///
/// @todo 这个函数好好像没用上产生bloat了
bool isConnected() const;
/// 对等端的名字(地址:端口)
QString peerName() const;
/// 对等端的地址
QString peerAddress() const;
QTimer timerSignup;
QTimer timerSignup; ///< 创建连接时,若该计时器超时,则断开连接
signals:
/// 收到一条消息时触发的信号
void message_got(const QByteArray& msg);
/// 产生报错信息触发的信号连接到UI中的函数
void error_message(const QString &msg);
/// 断开连接时的信号
void disconnected();
/// 连接创建时的信号
void connected();
private slots:
/**
QTcpSocket::messageReadymessage_get信号
"Compressed"base64解码并解压缩
便message_get信号传给上层处理
*/
void getMessage();
/// 连接QTcpSocket::errorOccured负责在UI显示网络错误信息
void raiseError(QAbstractSocket::SocketError error);
private:
/// AES加密
QByteArray aesEnc(const QByteArray &in);
/// AES解密
QByteArray aesDec(const QByteArray &out);
AES_KEY aes_key;
bool aes_ready;
QTcpSocket *socket;
/// 与QTcpSocket连接信号槽
void init();
AES_KEY aes_key; ///< AES密钥
bool aes_ready; ///< 表明是否启用AES加密传输
QTcpSocket *socket; ///< 用于实际发送数据的socket
};
#endif // _CLIENT_SOCKET_H

View File

@ -5,16 +5,26 @@
class ClientSocket;
/** @brief 实现通信协议,负责传输结构化消息而不是字面上的文本信息。
Router是对\ref ClientSocket ClientSocket解决的是传输字符串的
Router要解决的则是实现协议中的两种类型消息的传输Request-Reply以及
Notify这两种
*/
class Router : public QObject {
Q_OBJECT
public:
/**
Packet的类型使Packet的类型以TYPESRC
DEST这几种枚举通过按位与的方式拼接而成
*/
enum PacketType {
TYPE_REQUEST = 0x100,
TYPE_REPLY = 0x200,
TYPE_NOTIFICATION = 0x400,
SRC_CLIENT = 0x010,
SRC_SERVER = 0x020,
TYPE_REQUEST = 0x100, ///< 类型为Request的包
TYPE_REPLY = 0x200, ///< 类型为Reply的包
TYPE_NOTIFICATION = 0x400, ///< 类型为Notify的包
SRC_CLIENT = 0x010, ///< 从客户端发出的包
SRC_SERVER = 0x020, ///< 从服务端发出的包
SRC_LOBBY = 0x040,
DEST_CLIENT = 0x001,
DEST_SERVER = 0x002,

View File

@ -5,26 +5,44 @@
class ClientSocket;
// 只是对QTcpServer的简单封装
/**
@brief Server转达新的连接请求
ServerSocket是对QTcpServer与QUdpSocket的封装
- TCP连接请求时@ref ClientSocket @ref Server
- UDP报文时
*/
class ServerSocket : public QObject {
Q_OBJECT
public:
/**
ServerSocket对象
@ref Server Server的一个子成员
*/
ServerSocket(QObject *parent = nullptr);
/// 监听端口portTCP和UDP的都监听
bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u);
signals:
/// 接收到新连接时创建新的socket对象并发出该信号
void new_connection(ClientSocket *socket);
private slots:
// 新建一个ClientSocket然后立刻交给Server相关函数处理。
/// 新建一个ClientSocket然后立刻交给Server相关函数处理。
void processNewConnection();
/// 对每条收到的UDP报文调用processDatagram
void readPendingDatagrams();
private:
QTcpServer *server;
QUdpSocket *udpSocket; // 服务器列表页面显示服务器信息用
QTcpServer *server; ///< 监听TCP连接用
QUdpSocket *udpSocket; ///< 显示服务器信息用
/// 对udp报文`msg`进行分析addr和port是报文发送者传来的信息
void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port);
};

View File

@ -106,13 +106,12 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity,
nextRoomId++;
room->setAbandoned(false);
thread->addRoom(room);
rooms.insert(room->getId(), room);
} else {
room = new Room(thread);
connect(room, &Room::abandoned, this, &Server::onRoomAbandoned);
rooms.insert(room->getId(), room);
}
rooms.insert(room->getId(), room);
room->setName(name);
room->setCapacity(capacity);
room->setTimeout(timeout);

View File

@ -12,29 +12,60 @@ class Lobby;
#include "server/room.h"
/**
@brief Server类负责管理游戏服务端的运行
使Server类也会被实例化
Server的具体运行逻辑依托于Qt的事件循环与信号槽机制Server被创建后
listen方法即可完成监听Server的槽函数
### 配置信息
### 用户管理
*/
class Server : public QObject {
Q_OBJECT
public:
/// 构造Server对象。见于main函数
explicit Server(QObject *parent = nullptr);
~Server();
/// 监听端口
bool listen(const QHostAddress &address = QHostAddress::Any,
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,
int timeout = 15, const QByteArray &settings = "{}");
Room *findRoom(int id) const;
Lobby *lobby() const;
Room *findRoom(int id) const; /// 获取对应id的房间
Lobby *lobby() const; /// 获取大厅对象
RoomThread *createThread();
void removeThread(RoomThread *thread);
RoomThread *createThread(); /// 创建新的RoomThread并加入列表
void removeThread(RoomThread *thread); /// 从列表中移除thread
ServerPlayer *findPlayer(int id) const;
void addPlayer(ServerPlayer *player);
void removePlayer(int id);
auto getPlayers() { return players; }
ServerPlayer *findPlayer(int id) const; /// 获取对应id的玩家
void addPlayer(ServerPlayer *player); /// 将玩家加入表中,若重复则覆盖旧的
void removePlayer(int id); /// 从表中删除对应id的玩家
auto getPlayers() { return players; } /// 获取players表
void updateRoomList(ServerPlayer *teller);
void updateOnlineInfo();
@ -81,14 +112,20 @@ private:
QList<QString> temp_banlist;
AuthManager *auth;
sqlite3 *db;
QMutex transaction_mutex;
QString md5;
sqlite3 *db; ///< sqlite数据库连接实例
QMutex transaction_mutex; ///< 可能有多线程同时对数据库请求,需要加锁
QString md5; ///< 服务端当前允许用户登录的MD5值
QJsonObject config;
/**
`<pwd>/freekill.server.config.json`
使JSON对象
使JSON对象key设置默认值
*/
void readConfig();
QJsonObject config; ///< 配置文件其实就是一个JSON对象
};
extern Server *ServerInstance;
extern Server *ServerInstance; ///< 全局Server对象
#endif // _SERVER_H

View File

@ -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;
}

View File

@ -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
View 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
View 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"