From abb8f3a817f3bdd5589c70e68f0347e499a12f2b Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:13:13 +0330 Subject: [PATCH 001/104] fix(database): use named connection for game mutations --- src/core/GameModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index e843ecbf..7a579d43 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -342,7 +342,7 @@ void GameModel::setFavorite(int id, bool favorite) void GameModel::hideGame(int id, bool hidden) { - QSqlQuery q(QSqlDatabase::database()); + QSqlQuery q(QSqlDatabase::database(QStringLiteral("voidone-main"), false)); q.prepare("UPDATE games SET hidden=:hidden WHERE id=:id"); q.bindValue(":hidden", hidden ? 1 : 0); q.bindValue(":id", id); if (q.exec()) loadGamesFromDatabase(); From 15529bfe187b142d80b07530a5cfc4a1e58a15b6 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:13:16 +0330 Subject: [PATCH 002/104] fix(ui): load bundled sidebar icons from correct resource path --- src/ui/qml/Sidebar.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/qml/Sidebar.qml b/src/ui/qml/Sidebar.qml index 7649b08d..f9dc8539 100644 --- a/src/ui/qml/Sidebar.qml +++ b/src/ui/qml/Sidebar.qml @@ -75,7 +75,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/icons/home.svg" + iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/home.svg" voLabel: qsTr("Home") voSelected: root.currentPage === "home" onClicked: root.pageChanged("home") @@ -84,7 +84,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/icons/library.svg" + iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/library.svg" voLabel: qsTr("Library") voSelected: root.currentPage === "library" onClicked: root.pageChanged("library") @@ -93,7 +93,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/icons/activity.svg" + iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/activity.svg" voLabel: qsTr("Activity") voSelected: root.currentPage === "activity" onClicked: root.pageChanged("activity") @@ -113,7 +113,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/icons/settings.svg" + iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/settings.svg" voLabel: qsTr("Settings") voSelected: root.currentPage === "settings" onClicked: root.pageChanged("settings") From 05a655fb2d99b16296d75fe2586e9952396b2ba3 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:13:19 +0330 Subject: [PATCH 003/104] fix(ui): load add-game icons from correct resource path --- src/ui/qml/AddGameDialog.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index 8b802e02..d7c2af5f 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -61,7 +61,7 @@ Dialog { anchors.centerIn: parent width: 23 height: 23 - source: "qrc:/qt/qml/VoidOne.App/assets/icons/add.svg" + source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/add.svg" fillMode: Image.PreserveAspectFit } } @@ -149,7 +149,7 @@ Dialog { Layout.fillWidth: true title: qsTr("Select .exe") subtitle: qsTr("Add one game directly") - iconSource: "qrc:/qt/qml/VoidOne.App/assets/icons/file.svg" + iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/file.svg" onClicked: exeDialog.open() } @@ -157,7 +157,7 @@ Dialog { Layout.fillWidth: true title: qsTr("Select folder") subtitle: qsTr("Let VoidOne find executables") - iconSource: "qrc:/qt/qml/VoidOne.App/assets/icons/folder.svg" + iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/folder.svg" onClicked: folderDialog.open() } } From 1ac53273c8b465d63783f0e465318dcc59a4c9e1 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:16:01 +0330 Subject: [PATCH 004/104] fix(model): use named database connection for launch options --- src/core/GameModel.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index 7a579d43..4b58c2f3 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -350,10 +350,19 @@ void GameModel::hideGame(int id, bool hidden) void GameModel::updateLaunchOptions(int id, const QString &args, const QString &workingDir) { - QSqlQuery q(QSqlDatabase::database()); + const QSqlDatabase db = QSqlDatabase::database(QStringLiteral("voidone-main"), false); + if (!db.isValid() || !db.isOpen()) { + qWarning() << "[VoidOne] Cannot update launch options: database is unavailable."; + return; + } + QSqlQuery q(db); q.prepare("UPDATE games SET launch_args=:args, working_dir=:dir WHERE id=:id"); q.bindValue(":args", args); q.bindValue(":dir", workingDir); q.bindValue(":id", id); - if (q.exec()) loadGamesFromDatabase(); + if (!q.exec()) { + qWarning() << "[VoidOne] Failed to update launch options:" << q.lastError().text(); + return; + } + loadGamesFromDatabase(); } QVariantMap GameModel::getGameDetails(int id) const From eb761f7f4d128c264f1352d0075b5a3df570caff Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:16:04 +0330 Subject: [PATCH 005/104] fix(i18n): remove misleading product and cloud-backup labels --- src/core/TranslationManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/TranslationManager.cpp b/src/core/TranslationManager.cpp index 6e499e9b..64f6e899 100644 --- a/src/core/TranslationManager.cpp +++ b/src/core/TranslationManager.cpp @@ -13,17 +13,17 @@ void TranslationManager::setCurrentLanguage(const QString &lang) { void TranslationManager::initDictionary() { // English - m_dictionary["en"]["app_title"] = "VoidOne Pro"; + m_dictionary["en"]["app_title"] = "VoidOne"; m_dictionary["en"]["launch"] = "Play Game"; m_dictionary["en"]["scan_steam"] = "Scan Steam Library"; - m_dictionary["en"]["auto_save"] = "Real-Time Cloud Backup"; + m_dictionary["en"]["auto_save"] = "Automatic Backup"; m_dictionary["en"]["settings"] = "Settings"; // Persian - m_dictionary["fa"]["app_title"] = "VoidOne پرو"; + m_dictionary["fa"]["app_title"] = "VoidOne"; m_dictionary["fa"]["launch"] = "اجرای بازی"; m_dictionary["fa"]["scan_steam"] = "اسکن کتابخانه استیم"; - m_dictionary["fa"]["auto_save"] = "پشتیبان‌گیری ابری خودکار"; + m_dictionary["fa"]["auto_save"] = "پشتیبان‌گیری خودکار"; m_dictionary["fa"]["settings"] = "تنظیمات"; } From 39a763f5a4feb8b6b0d5bde78002f3e92594f81b Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:19:20 +0330 Subject: [PATCH 006/104] feat(branding): adopt canonical VoidOne website mark --- assets/branding/voidone-mark.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 assets/branding/voidone-mark.svg diff --git a/assets/branding/voidone-mark.svg b/assets/branding/voidone-mark.svg new file mode 100644 index 00000000..d06e204a --- /dev/null +++ b/assets/branding/voidone-mark.svg @@ -0,0 +1,8 @@ + + VoidOne + VoidOne master gaming-platform mark. + + + + + \ No newline at end of file From 6915f36293e3dceb9b3e1aa4d6a2c6dfa988fccf Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:19:26 +0330 Subject: [PATCH 007/104] fix(branding): register canonical VoidOne mark as app resource --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d1d1ff1..6599eac5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,7 @@ voidone_apply_compile_policy(voidone_core) set(VOIDONE_EXE_EXTRA_SOURCES "") if(WIN32) set(APP_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.ico") + set(BRAND_MARK_PATH "${CMAKE_CURRENT_SOURCE_DIR}/assets/branding/voidone-mark.svg") set(VOIDONE_RC_PATH "${CMAKE_CURRENT_BINARY_DIR}/voidone.rc") if(EXISTS "${APP_ICON_PATH}") set(VOIDONE_RC_VERSION "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") @@ -175,6 +176,7 @@ qt_add_qml_module(VoidOne src/ui/qml/ActionButton.qml src/ui/qml/GameDetailsDialog.qml RESOURCES + assets/branding/voidone-mark.svg assets/ui/icons/home.svg assets/ui/icons/library.svg assets/ui/icons/activity.svg From 65292023ed9e0d6f5b70d178cdb7b572e17d5e2d Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:19:32 +0330 Subject: [PATCH 008/104] fix(branding): use canonical VoidOne mark for application icon --- src/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 5383c966..f86e711f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -256,6 +257,7 @@ int main(int argc, char *argv[]) qInfo() << "[Bootstrap] QML2_IMPORT_PATH:" << qEnvironmentVariable("QML2_IMPORT_PATH"); QGuiApplication app(argc, argv); + app.setWindowIcon(QIcon(QStringLiteral(":/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg"))); // AppDataLocation must exist before anything such as QLockFile uses it. const QString appDataDir = appDataDirectory(); From 939fc1ff1072d9f69268a18e06b5bf6560e49e07 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:19:39 +0330 Subject: [PATCH 009/104] fix(ui): use canonical VoidOne mark in game cards --- src/ui/qml/GameCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/qml/GameCard.qml b/src/ui/qml/GameCard.qml index 95264a66..aa766ab5 100644 --- a/src/ui/qml/GameCard.qml +++ b/src/ui/qml/GameCard.qml @@ -75,7 +75,7 @@ Rectangle { anchors.centerIn: parent width: parent.width * 0.48 height: width - source: "qrc:/qt/qml/VoidOne.App/assets/icons/library.svg" + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit } } From 33f63d5867bac8c802a28b2c2effa63445400a49 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:19:44 +0330 Subject: [PATCH 010/104] fix(ui): use canonical VoidOne mark in sidebar --- src/ui/qml/Sidebar.qml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/qml/Sidebar.qml b/src/ui/qml/Sidebar.qml index f9dc8539..81ba30ad 100644 --- a/src/ui/qml/Sidebar.qml +++ b/src/ui/qml/Sidebar.qml @@ -30,12 +30,13 @@ Rectangle { color: "#00e5ff12" border.color: "#00e5ff38" - Text { + Image { anchors.centerIn: parent - text: "V" - color: "#00e5ff" - font.pixelSize: 18 - font.bold: true + width: 27 + height: 27 + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + fillMode: Image.PreserveAspectFit + smooth: true } } From 426e621054bfed10ddd758726f92f82146753987 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:19:48 +0330 Subject: [PATCH 011/104] chore(build): remove unused branding path variable --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6599eac5..88e785d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,7 +151,6 @@ voidone_apply_compile_policy(voidone_core) set(VOIDONE_EXE_EXTRA_SOURCES "") if(WIN32) set(APP_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.ico") - set(BRAND_MARK_PATH "${CMAKE_CURRENT_SOURCE_DIR}/assets/branding/voidone-mark.svg") set(VOIDONE_RC_PATH "${CMAKE_CURRENT_BINARY_DIR}/voidone.rc") if(EXISTS "${APP_ICON_PATH}") set(VOIDONE_RC_VERSION "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") From e5a89ed89f34fb23eafcedad099ff38b73237ac2 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 01:19:57 +0330 Subject: [PATCH 012/104] fix(branding): enable SVG support for canonical app mark --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 88e785d0..691b8967 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,7 +87,7 @@ set(VOIDONE_VERSION_DISPLAY "${VOIDONE_VERSION_INPUT}") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/VoidOneVersion.h.in" "${VOIDONE_GENERATED_DIR}/VoidOneVersion.h" @ONLY) -find_package(Qt6 6.11.2 REQUIRED COMPONENTS Core Gui Quick Sql Network) +find_package(Qt6 6.11.2 REQUIRED COMPONENTS Core Gui Quick Sql Network Svg) if(VOIDONE_BUILD_TESTS) find_package(Qt6 6.11.2 REQUIRED COMPONENTS Test) endif() @@ -184,7 +184,7 @@ qt_add_qml_module(VoidOne assets/ui/icons/folder.svg assets/ui/icons/file.svg ) -target_link_libraries(VoidOne PRIVATE voidone_core Qt6::Gui Qt6::Quick) +target_link_libraries(VoidOne PRIVATE voidone_core Qt6::Gui Qt6::Quick Qt6::Svg) if(WIN32) target_link_libraries(VoidOne PRIVATE Dbghelp) endif() From 2e0a8d3983dad89d97245ed8e7025fdc0b24da35 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 15:26:57 +0330 Subject: [PATCH 013/104] ui(beta.2): refine balanced dashboard layout --- src/ui/qml/Main.qml | 49 +++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 8b751ee4..1ebb8308 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -15,14 +15,15 @@ Window { width: Screen.desktopAvailableWidth * 0.86 height: Screen.desktopAvailableHeight * 0.86 - minimumWidth: 1040 - minimumHeight: 700 + minimumWidth: 900 + minimumHeight: 600 visible: true title: qsTr("VoidOne") color: theme.background property string currentPage: "home" property bool sidebarCompact: false + readonly property bool narrowLayout: width < 1100 QtObject { id: theme @@ -131,7 +132,7 @@ Window { } Rectangle { - Layout.preferredWidth: 300 + Layout.preferredWidth: root.narrowLayout ? 190 : 300 Layout.preferredHeight: 38 radius: 10 color: "#0b1118" @@ -184,11 +185,6 @@ Window { height: 7 radius: 4 color: theme.success - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.25; duration: 900 } - NumberAnimation { to: 1; duration: 900 } - } } Text { text: qsTr("LOCAL") @@ -237,12 +233,12 @@ Window { ColumnLayout { id: homeColumn - width: parent.width + width: Math.max(0, parent.width - 56) anchors.top: parent.top - anchors.topMargin: 30 - anchors.leftMargin: 34 - anchors.rightMargin: 34 - spacing: 24 + anchors.topMargin: 26 + anchors.left: parent.left + anchors.leftMargin: 28 + spacing: 22 RowLayout { Layout.fillWidth: true @@ -253,7 +249,7 @@ Window { Text { text: qsTr("Your games. Your hardware. Your rules.") color: theme.text - font.pixelSize: 29 + font.pixelSize: root.narrowLayout ? 23 : 29 font.bold: true } Text { @@ -285,8 +281,8 @@ Window { // Hero Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 220 - radius: 22 + Layout.preferredHeight: root.narrowLayout ? 185 : 220 + radius: 18 color: "#0d151e" border.color: "#1b2b3b" clip: true @@ -312,8 +308,8 @@ Window { anchors.left: parent.left anchors.leftMargin: 68 anchors.verticalCenter: parent.verticalCenter - width: parent.width * 0.58 - spacing: 12 + width: parent.width * (root.narrowLayout ? 0.72 : 0.58) + spacing: 10 Text { text: qsTr("WELCOME BACK") @@ -326,7 +322,7 @@ Window { Text { text: gameModel && gameModel.count > 0 ? qsTr("Ready to play.") : qsTr("Build your library.") color: theme.text - font.pixelSize: 31 + font.pixelSize: root.narrowLayout ? 25 : 31 font.bold: true } @@ -396,14 +392,14 @@ Window { } StatCard { Layout.fillWidth: true - label: qsTr("CONTROL") - value: "100%" - detail: qsTr("your library") + label: qsTr("LAUNCH") + value: qsTr("DIRECT") + detail: qsTr("from your library") } } Text { - text: qsTr("RECENTLY ADDED") + text: qsTr("YOUR LIBRARY") color: theme.muted font.pixelSize: 10 font.bold: true @@ -412,9 +408,10 @@ Window { GridView { Layout.fillWidth: true - Layout.preferredHeight: Math.max(190, Math.ceil((gameModel ? gameModel.count : 0) / 3) * 190) + property int columns: width < 560 ? 1 : (width < 900 ? 2 : 3) + Layout.preferredHeight: Math.max(190, Math.ceil((gameModel ? gameModel.count : 0) / columns) * 190) interactive: false - cellWidth: width / 3 + cellWidth: width / columns cellHeight: 180 model: typeof gameModel !== "undefined" ? gameModel : null @@ -445,7 +442,7 @@ Window { Item { ColumnLayout { anchors.fill: parent - anchors.margins: 32 + anchors.margins: 24 spacing: 18 RowLayout { From de3ddf197ad4ad00a7574f82d720bd7e179c5e3c Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 15:27:01 +0330 Subject: [PATCH 014/104] ui(beta.2): show game icons with lightweight cards --- src/ui/qml/GameCard.qml | 46 ++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/ui/qml/GameCard.qml b/src/ui/qml/GameCard.qml index aa766ab5..ec999978 100644 --- a/src/ui/qml/GameCard.qml +++ b/src/ui/qml/GameCard.qml @@ -1,8 +1,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtQuick.Effects - Rectangle { id: cardRoot @@ -20,23 +18,25 @@ Rectangle { signal launchRequested(string path) - radius: 16 - color: hoverArea.containsMouse ? "#111d29" : "#0c131b" - border.color: hoverArea.containsMouse ? "#00e5ff66" : "#172533" + function iconUrl(path) { + if (!path || path.length === 0) + return "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + if (path.startsWith("qrc:/") || path.startsWith("file:/") || path.startsWith("http://") || path.startsWith("https://")) + return path + if (path.startsWith(":/")) + return "qrc" + path + if (path.startsWith("/")) + return "file://" + path + return "file:///" + path.replace(/\\/g, "/") + } + + radius: 14 + color: hoverArea.containsMouse ? "#111e2a" : "#0c141d" + border.color: hoverArea.containsMouse ? "#00e5ff66" : "#1a2a39" border.width: 1 - scale: hoverArea.containsMouse ? 1.018 : 1.0 - Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } - Behavior on color { ColorAnimation { duration: 150 } } - Behavior on border.color { ColorAnimation { duration: 150 } } - - layer.enabled: hoverArea.containsMouse - layer.effect: MultiEffect { - shadowEnabled: true - shadowColor: "#00e5ff" - shadowBlur: 0.22 - shadowOpacity: 0.25 - } + Behavior on color { ColorAnimation { duration: 120 } } + Behavior on border.color { ColorAnimation { duration: 120 } } Rectangle { anchors.left: parent.left @@ -71,12 +71,24 @@ Rectangle { color: "#00e5ff10" border.color: "#00e5ff22" + Image { + id: gameIcon + anchors.centerIn: parent + width: parent.width * 0.72 + height: width + source: cardRoot.iconUrl(cardRoot.iconPath) + fillMode: Image.PreserveAspectFit + visible: status === Image.Ready + asynchronous: true + } + Image { anchors.centerIn: parent width: parent.width * 0.48 height: width source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit + visible: gameIcon.status !== Image.Ready } } From fc26c8fdce8db71b7524e77dba5c16546fbfb068 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 15:27:03 +0330 Subject: [PATCH 015/104] ui(beta.2): tighten sidebar spacing --- src/ui/qml/Sidebar.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/Sidebar.qml b/src/ui/qml/Sidebar.qml index 81ba30ad..016ee32d 100644 --- a/src/ui/qml/Sidebar.qml +++ b/src/ui/qml/Sidebar.qml @@ -13,9 +13,9 @@ Rectangle { ColumnLayout { anchors.fill: parent - anchors.topMargin: 18 - anchors.bottomMargin: 16 - spacing: 14 + anchors.topMargin: 16 + anchors.bottomMargin: 14 + spacing: 12 RowLayout { Layout.fillWidth: true From 82e2abfb7c0a4e0ab4b9dafdd7c43e821ecce4b5 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Fri, 9 Oct 2026 15:27:50 +0330 Subject: [PATCH 016/104] ui(beta.2): prevent hero overlap at narrow widths --- src/ui/qml/Main.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 1ebb8308..80b947e1 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -306,7 +306,7 @@ Window { ColumnLayout { anchors.left: parent.left - anchors.leftMargin: 68 + anchors.leftMargin: root.narrowLayout ? 28 : 68 anchors.verticalCenter: parent.verticalCenter width: parent.width * (root.narrowLayout ? 0.72 : 0.58) spacing: 10 @@ -364,6 +364,7 @@ Window { } Text { + visible: !root.narrowLayout anchors.right: parent.right anchors.rightMargin: 42 anchors.verticalCenter: parent.verticalCenter From e364fea36a2a90346718a1a5148dbc4910ec8686 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:32:06 +0330 Subject: [PATCH 017/104] fix(database): include complete QSqlError type --- src/core/GameModel.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index 4b58c2f3..b46bb0cc 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include From 8f9bd56adc627fa21fdd329cb8e3631559b10e14 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:34:24 +0330 Subject: [PATCH 018/104] ui(beta.2): use the original VoidOne mark in the shell --- src/ui/qml/Main.qml | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 80b947e1..c58f3c0a 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -201,11 +201,14 @@ Window { radius: 18 color: theme.cyanSoft border.color: theme.cyanLine - Text { + Image { anchors.centerIn: parent - text: "V" - color: theme.cyan - font.bold: true + width: 22 + height: 22 + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + fillMode: Image.PreserveAspectFit + smooth: true + asynchronous: true } } } @@ -363,15 +366,17 @@ Window { } } - Text { + Image { visible: !root.narrowLayout anchors.right: parent.right - anchors.rightMargin: 42 + anchors.rightMargin: 34 anchors.verticalCenter: parent.verticalCenter - text: "V" - color: "#00e5ff10" - font.pixelSize: 150 - font.bold: true + width: 150 + height: 150 + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + fillMode: Image.PreserveAspectFit + opacity: 0.12 + asynchronous: true } } From 93fb058c0c2325951da6a28338bc3622b2742284 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:34:45 +0330 Subject: [PATCH 019/104] ui(beta.2): make the library empty state actionable --- src/ui/qml/Main.qml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index c58f3c0a..fa259a31 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -553,10 +553,30 @@ Window { } Text { Layout.alignment: Qt.AlignHCenter - text: qsTr("Add an executable or choose a game folder.") + text: searchInput.text.length > 0 + ? qsTr("Try a different search term.") + : qsTr("Add an executable or choose a game folder.") color: theme.muted font.pixelSize: 12 } + + Button { + Layout.alignment: Qt.AlignHCenter + visible: searchInput.text.length === 0 + text: qsTr("+ Add your first game") + onClicked: addGameDialog.open() + background: Rectangle { + radius: 10 + color: theme.cyan + } + contentItem: Text { + text: parent.text + color: "#041015" + font.bold: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } } } } From 323e674768acf0e43396779f464b4ce55cae6cbb Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:37:40 +0330 Subject: [PATCH 020/104] ui(beta.2): use the original mark in game details --- src/ui/qml/GameDetailsDialog.qml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ui/qml/GameDetailsDialog.qml b/src/ui/qml/GameDetailsDialog.qml index 5e742dc3..a8fd8cc9 100644 --- a/src/ui/qml/GameDetailsDialog.qml +++ b/src/ui/qml/GameDetailsDialog.qml @@ -65,7 +65,14 @@ Dialog { Rectangle { width: 70; height: 70; radius: 17 color: "#00e5ff10"; border.color: "#00e5ff32" - Text { anchors.centerIn: parent; text: "V"; color: "#00e5ff"; font.pixelSize: 30; font.bold: true } + Image { + anchors.centerIn: parent + width: 40 + height: 40 + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + fillMode: Image.PreserveAspectFit + asynchronous: true + } } ColumnLayout { From 5a533a5a7404fbd45e6283fe87666ae1bf1227e3 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:37:43 +0330 Subject: [PATCH 021/104] ui(beta.2): align settings page with shared layout --- src/ui/qml/Main.qml | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index fa259a31..ce5244c1 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -604,8 +604,40 @@ Window { // SETTINGS Item { - SaveBackupView { - anchors.centerIn: parent + ColumnLayout { + anchors.fill: parent + anchors.margins: 24 + spacing: 16 + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + Text { + text: qsTr("Settings") + color: theme.text + font.pixelSize: 28 + font.bold: true + } + Text { + text: qsTr("Manage local save backups and automatic backup behavior.") + color: theme.muted + font.pixelSize: 12 + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: "#14202c" + } + + SaveBackupView { + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumWidth: 820 + Layout.minimumHeight: 360 + } } } } From d4ec31cb862f0f725c8fa7204181b8e348a32ae8 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:37:49 +0330 Subject: [PATCH 022/104] ui(beta.2): unify save backup accent colors --- src/ui/qml/SaveBackupView.qml | 46 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/ui/qml/SaveBackupView.qml b/src/ui/qml/SaveBackupView.qml index cae02c7c..29c3facf 100644 --- a/src/ui/qml/SaveBackupView.qml +++ b/src/ui/qml/SaveBackupView.qml @@ -8,7 +8,7 @@ Rectangle { implicitWidth: 600 implicitHeight: 450 radius: 12 - border.color: "#00ffee30" + border.color: "#00e5ff30" border.width: 1 property string saveDirPath: "" @@ -24,7 +24,7 @@ Rectangle { target: saveBackupManager function onBackupCompleted(success, message) { statusText.text = message - statusText.color = success ? "#00ffee" : "#ef4444" + statusText.color = success ? "#00e5ff" : "#ef4444" } } @@ -39,7 +39,7 @@ Rectangle { Text { text: "💾 " + (trManager.currentLanguage === "fa" ? "پشتیبان‌گیری و ذخیره خودکار سیو" : "Save Backup & Auto-Save") - color: "#00ffee" + color: "#00e5ff" font.pixelSize: 18 font.bold: true renderType: Text.NativeRendering @@ -51,14 +51,14 @@ Rectangle { Button { text: trManager.currentLanguage === "en" ? "FA / فارسی" : "EN / English" background: Rectangle { - color: "#00ffee20" - border.color: "#00ffee" + color: "#00e5ff20" + border.color: "#00e5ff" border.width: 1 radius: 8 } contentItem: Text { text: parent.text - color: "#00ffee" + color: "#00e5ff" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.bold: true @@ -73,7 +73,7 @@ Rectangle { Rectangle { Layout.fillWidth: true height: 1 - color: "#00ffee20" + color: "#00e5ff20" } // Settings Form Layout @@ -86,7 +86,7 @@ Rectangle { // Auto Save Switch Label Text { text: trManager.currentLanguage === "fa" ? "فعال‌سازی ذخیره خودکار لحظه‌ای:" : "Enable Real-Time Auto-Save:" - color: "#00ffee80" + color: "#00e5ff80" font.pixelSize: 14 } @@ -96,7 +96,7 @@ Rectangle { checked: false onCheckedChanged: { root.refreshAutoSaveConfiguration() - statusText.color = "#00ffee" + statusText.color = "#00e5ff" statusText.text = checked ? (trManager.currentLanguage === "fa" ? "وضعیت: ذخیره خودکار فعال شد" : "Status: Auto-save enabled") : (trManager.currentLanguage === "fa" ? "وضعیت: ذخیره خودکار غیرفعال شد" : "Status: Auto-save disabled") @@ -107,8 +107,8 @@ Rectangle { x: autoSaveSwitch.leftPadding y: parent.height / 2 - height / 2 radius: 12 - color: autoSaveSwitch.checked ? "#00ffee" : "#1a222d" - border.color: "#00ffee" + color: autoSaveSwitch.checked ? "#00e5ff" : "#1a222d" + border.color: "#00e5ff" border.width: 1 Rectangle { @@ -117,7 +117,7 @@ Rectangle { width: 20 height: 20 radius: 10 - color: autoSaveSwitch.checked ? "#11151b" : "#00ffee80" + color: autoSaveSwitch.checked ? "#11151b" : "#00e5ff80" } } } @@ -125,7 +125,7 @@ Rectangle { // Interval Label Text { text: trManager.currentLanguage === "fa" ? "بازه زمانی (ثانیه):" : "Interval (Seconds):" - color: "#00ffee80" + color: "#00e5ff80" font.pixelSize: 14 } @@ -142,7 +142,7 @@ Rectangle { Text { text: trManager.currentLanguage === "fa" ? "مسیر سیو بازی:" : "Save Folder:" - color: "#00ffee80" + color: "#00e5ff80" font.pixelSize: 14 } @@ -160,7 +160,7 @@ Rectangle { Text { text: trManager.currentLanguage === "fa" ? "مقصد بکاپ:" : "Backup Destination:" - color: "#00ffee80" + color: "#00e5ff80" font.pixelSize: 14 } @@ -184,14 +184,14 @@ Rectangle { Layout.fillWidth: true height: 40 color: "#0a0d12" - border.color: "#00ffee30" + border.color: "#00e5ff30" radius: 8 Text { id: statusText anchors.centerIn: parent text: trManager.currentLanguage === "fa" ? "وضعیت: آماده برای پایش و پشتیبان‌گیری..." : "Status: Ready for backup monitoring..." - color: "#00ffee" + color: "#00e5ff" font.pixelSize: 12 } } @@ -205,13 +205,13 @@ Rectangle { Layout.fillWidth: true height: 40 background: Rectangle { - color: parent.pressed ? "#00cccc" : "#00ffee20" - border.color: "#00ffee" + color: parent.pressed ? "#00b8ce" : "#00e5ff20" + border.color: "#00e5ff" radius: 8 } contentItem: Text { text: trManager.currentLanguage === "fa" ? "ایجاد بکاپ الان" : "Create Backup Now" - color: "#00ffee" + color: "#00e5ff" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.bold: true @@ -237,12 +237,12 @@ Rectangle { height: 40 background: Rectangle { color: "transparent" - border.color: "#00ffee50" + border.color: "#00e5ff50" radius: 8 } contentItem: Text { text: trManager.currentLanguage === "fa" ? "بازگردانی بکاپ" : "Restore Backup" - color: "#00ffee80" + color: "#00e5ff80" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } @@ -268,7 +268,7 @@ Rectangle { } statusText.text = trManager.currentLanguage === "fa" ? "وضعیت: در حال بازگردانی بکاپ..." : "Status: Restoring backup..." - statusText.color = "#00ffee" + statusText.color = "#00e5ff" saveBackupManager.restoreBackup(latestPath, root.saveDirPath) } } From cfb9a09f813c6fb91947a3b9edee4c41810aced2 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:43:06 +0330 Subject: [PATCH 023/104] fix(library): extract and cache executable icons --- src/core/GameModel.cpp | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index b46bb0cc..d0a6f7b1 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -9,6 +9,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -36,6 +42,38 @@ bool isBlockedExecutable(const QFileInfo &file) return false; } +QString extractExecutableIcon(const QString &executablePath) +{ + const QFileInfo executable(executablePath); + if (!executable.isFile() || executable.isSymLink()) + return {}; + + const QString dataRoot = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (dataRoot.isEmpty()) + return {}; + + QDir iconDirectory(QDir(dataRoot).filePath(QStringLiteral("game-icons"))); + if (!iconDirectory.exists() && !QDir().mkpath(iconDirectory.absolutePath())) + return {}; + + const QByteArray digest = QCryptographicHash::hash( + executable.absoluteFilePath().toUtf8(), QCryptographicHash::Sha256).toHex(); + const QString iconPath = iconDirectory.filePath(QString::fromLatin1(digest) + QStringLiteral(".png")); + if (QFileInfo::exists(iconPath)) + return iconPath; + + QFileIconProvider provider; + const QIcon icon = provider.icon(executable); + if (icon.isNull()) + return {}; + + const QPixmap pixmap = icon.pixmap(96, 96); + if (pixmap.isNull() || !pixmap.save(iconPath, "PNG")) + return {}; + + return iconPath; +} + } // namespace GameModel::GameModel(QObject *parent) : QAbstractListModel(parent) {} @@ -90,6 +128,29 @@ void GameModel::loadGamesFromDatabase() { beginResetModel(); m_allGames = Database::getAllGames(); + + // Backfill missing icons for games already present in the user's library. + // The cache is stored in app data; the executable and database schema stay untouched. + const QSqlDatabase db = QSqlDatabase::database(QStringLiteral("voidone-main"), false); + for (GameRecord &game : m_allGames) { + if (!game.iconPath.isEmpty() && QFileInfo::exists(game.iconPath)) + continue; + + const QString extractedIcon = extractExecutableIcon(game.exePath); + if (extractedIcon.isEmpty()) + continue; + + game.iconPath = extractedIcon; + if (db.isValid() && db.isOpen()) { + QSqlQuery update(db); + update.prepare("UPDATE games SET icon_path = :icon WHERE id = :id"); + update.bindValue(":icon", extractedIcon); + update.bindValue(":id", game.id); + if (!update.exec()) + qWarning() << "[VoidOne] Could not persist cached game icon:" << update.lastError().text(); + } + } + m_games.clear(); for (const auto &game : std::as_const(m_allGames)) { if (!game.hidden) @@ -130,6 +191,8 @@ bool GameModel::addNewGame(const QString &name, const QString &exePath, const QS game.name = trimmedName; game.exePath = exeInfo.absoluteFilePath(); game.iconPath = iconPath.trimmed(); + if (game.iconPath.isEmpty() || !QFileInfo::exists(game.iconPath)) + game.iconPath = extractExecutableIcon(game.exePath); game.platform = QStringLiteral("Custom"); game.source = QStringLiteral("Local"); game.workingDir = exeInfo.absolutePath(); From 180af4ab4c551487fb90aa68968d3f1e46eb7669 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:43:39 +0330 Subject: [PATCH 024/104] feat(settings): polish save backup controls and folder picking --- src/ui/qml/SaveBackupView.qml | 513 ++++++++++++++++++++++------------ 1 file changed, 333 insertions(+), 180 deletions(-) diff --git a/src/ui/qml/SaveBackupView.qml b/src/ui/qml/SaveBackupView.qml index 29c3facf..84c47abb 100644 --- a/src/ui/qml/SaveBackupView.qml +++ b/src/ui/qml/SaveBackupView.qml @@ -1,275 +1,428 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Dialogs Rectangle { id: root - color: "#11151b" - implicitWidth: 600 - implicitHeight: 450 - radius: 12 - border.color: "#00e5ff30" + color: "#0b121a" + implicitWidth: 640 + implicitHeight: backupColumn.implicitHeight + 40 + radius: 16 + border.color: "#1b2b3a" border.width: 1 property string saveDirPath: "" property string backupDestinationPath: "" + readonly property bool isPersian: trManager.currentLanguage === "fa" function refreshAutoSaveConfiguration() { saveBackupManager.configureAutoSave(saveDirPath, backupDestinationPath) saveBackupManager.autoSaveIntervalSeconds = intervalSpinBox.value - saveBackupManager.autoSaveEnabled = autoSaveSwitch.checked && saveDirPath.length > 0 && backupDestinationPath.length > 0 + saveBackupManager.autoSaveEnabled = autoSaveSwitch.checked + && saveDirPath.length > 0 && backupDestinationPath.length > 0 + } + + onSaveDirPathChanged: savePathField.text = saveDirPath + onBackupDestinationPathChanged: backupPathField.text = backupDestinationPath + + FolderDialog { + id: saveFolderDialog + title: root.isPersian ? "انتخاب پوشهٔ سیو بازی" : "Choose game save folder" + onAccepted: { + root.saveDirPath = selectedFolder.toLocalFile() + root.refreshAutoSaveConfiguration() + } + } + + FolderDialog { + id: backupFolderDialog + title: root.isPersian ? "انتخاب مقصد بکاپ" : "Choose backup destination" + onAccepted: { + root.backupDestinationPath = selectedFolder.toLocalFile() + root.refreshAutoSaveConfiguration() + } } Connections { target: saveBackupManager function onBackupCompleted(success, message) { statusText.text = message - statusText.color = success ? "#00e5ff" : "#ef4444" + statusText.color = success ? "#38d996" : "#ff6878" } } ColumnLayout { + id: backupColumn anchors.fill: parent anchors.margins: 20 - spacing: 20 + spacing: 16 - // Header & Language Switcher RowLayout { Layout.fillWidth: true - - Text { - text: "💾 " + (trManager.currentLanguage === "fa" ? "پشتیبان‌گیری و ذخیره خودکار سیو" : "Save Backup & Auto-Save") - color: "#00e5ff" - font.pixelSize: 18 - font.bold: true - renderType: Text.NativeRendering - } + spacing: 12 - Item { Layout.fillWidth: true } - - // Language Toggle Button - Button { - text: trManager.currentLanguage === "en" ? "FA / فارسی" : "EN / English" - background: Rectangle { - color: "#00e5ff20" - border.color: "#00e5ff" - border.width: 1 - radius: 8 - } - contentItem: Text { - text: parent.text + Rectangle { + Layout.preferredWidth: 42 + Layout.preferredHeight: 42 + radius: 12 + color: "#00e5ff12" + border.color: "#00e5ff2a" + Text { + anchors.centerIn: parent + text: "↻" color: "#00e5ff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + font.pixelSize: 23 font.bold: true } - onClicked: { - trManager.currentLanguage = (trManager.currentLanguage === "en") ? "fa" : "en" + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { + text: root.isPersian ? "پشتیبان‌گیری از سیوها" : "Save backups" + color: "#edf5fa" + font.pixelSize: 17 + font.bold: true + } + Text { + text: root.isPersian + ? "پوشهٔ سیو و مقصد امن بکاپ را مشخص کن." + : "Choose a save folder and a safe backup destination." + color: "#8194a7" + font.pixelSize: 11 + wrapMode: Text.WordWrap + Layout.fillWidth: true } } } - // Divider Rectangle { Layout.fillWidth: true height: 1 - color: "#00e5ff20" + color: "#1b2b3a" } - // Settings Form Layout GridLayout { - columns: 2 Layout.fillWidth: true - rowSpacing: 15 - columnSpacing: 15 + columns: root.width < 590 ? 1 : 2 + columnSpacing: 12 + rowSpacing: 7 - // Auto Save Switch Label Text { - text: trManager.currentLanguage === "fa" ? "فعال‌سازی ذخیره خودکار لحظه‌ای:" : "Enable Real-Time Auto-Save:" - color: "#00e5ff80" - font.pixelSize: 14 + text: root.isPersian ? "پوشهٔ سیو بازی" : "Game save folder" + color: "#b9c8d5" + font.pixelSize: 11 + font.bold: true + Layout.fillWidth: true } - // Custom Switch - Switch { - id: autoSaveSwitch - checked: false - onCheckedChanged: { - root.refreshAutoSaveConfiguration() - statusText.color = "#00e5ff" - statusText.text = checked - ? (trManager.currentLanguage === "fa" ? "وضعیت: ذخیره خودکار فعال شد" : "Status: Auto-save enabled") - : (trManager.currentLanguage === "fa" ? "وضعیت: ذخیره خودکار غیرفعال شد" : "Status: Auto-save disabled") - } - indicator: Rectangle { - implicitWidth: 48 - implicitHeight: 24 - x: autoSaveSwitch.leftPadding - y: parent.height / 2 - height / 2 - radius: 12 - color: autoSaveSwitch.checked ? "#00e5ff" : "#1a222d" - border.color: "#00e5ff" - border.width: 1 + Item { visible: root.width >= 590; Layout.fillWidth: true; height: 1 } - Rectangle { - x: autoSaveSwitch.checked ? parent.width - width - 2 : 2 - y: 2 - width: 20 - height: 20 - radius: 10 - color: autoSaveSwitch.checked ? "#11151b" : "#00e5ff80" + RowLayout { + Layout.fillWidth: true + spacing: 8 + TextField { + id: savePathField + Layout.fillWidth: true + placeholderText: root.isPersian ? "مسیر پوشهٔ سیو" : "Path to save folder" + text: root.saveDirPath + color: "#edf5fa" + placeholderTextColor: "#5f7387" + selectByMouse: true + onEditingFinished: { + root.saveDirPath = text.trim() + root.refreshAutoSaveConfiguration() + } + background: Rectangle { + radius: 9 + color: "#080e14" + border.color: savePathField.activeFocus ? "#00e5ff70" : "#233545" + } + } + Button { + text: "…" + implicitWidth: 40 + implicitHeight: 38 + onClicked: saveFolderDialog.open() + background: Rectangle { + radius: 9 + color: parent.hovered ? "#142533" : "#0d1721" + border.color: "#2a3d4e" + } + contentItem: Text { + text: parent.text + color: "#dce8ef" + font.pixelSize: 19 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter } + ToolTip.visible: hovered + ToolTip.text: root.isPersian ? "انتخاب پوشه" : "Browse folder" } } - // Interval Label Text { - text: trManager.currentLanguage === "fa" ? "بازه زمانی (ثانیه):" : "Interval (Seconds):" - color: "#00e5ff80" - font.pixelSize: 14 - } - - // Interval SpinBox - SpinBox { - id: intervalSpinBox - from: 5 - to: 3600 - value: 30 - editable: true - Layout.preferredWidth: 120 - onValueModified: root.refreshAutoSaveConfiguration() + text: root.isPersian ? "مقصد پشتیبان‌گیری" : "Backup destination" + color: "#b9c8d5" + font.pixelSize: 11 + font.bold: true + Layout.fillWidth: true } - Text { - text: trManager.currentLanguage === "fa" ? "مسیر سیو بازی:" : "Save Folder:" - color: "#00e5ff80" - font.pixelSize: 14 - } + Item { visible: root.width >= 590; Layout.fillWidth: true; height: 1 } - TextField { + RowLayout { Layout.fillWidth: true - placeholderText: trManager.currentLanguage === "fa" ? "مثال: /home/user/Game/Saves" : "Example: /home/user/Game/Saves" - text: root.saveDirPath - color: "#f8fafc" - placeholderTextColor: "#64748b" - onEditingFinished: { - root.saveDirPath = text.trim() - root.refreshAutoSaveConfiguration() + spacing: 8 + TextField { + id: backupPathField + Layout.fillWidth: true + placeholderText: root.isPersian ? "مسیر ذخیرهٔ بکاپ‌ها" : "Path for backups" + text: root.backupDestinationPath + color: "#edf5fa" + placeholderTextColor: "#5f7387" + selectByMouse: true + onEditingFinished: { + root.backupDestinationPath = text.trim() + root.refreshAutoSaveConfiguration() + } + background: Rectangle { + radius: 9 + color: "#080e14" + border.color: backupPathField.activeFocus ? "#00e5ff70" : "#233545" + } + } + Button { + text: "…" + implicitWidth: 40 + implicitHeight: 38 + onClicked: backupFolderDialog.open() + background: Rectangle { + radius: 9 + color: parent.hovered ? "#142533" : "#0d1721" + border.color: "#2a3d4e" + } + contentItem: Text { + text: parent.text + color: "#dce8ef" + font.pixelSize: 19 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + ToolTip.visible: hovered + ToolTip.text: root.isPersian ? "انتخاب پوشه" : "Browse folder" } } + } - Text { - text: trManager.currentLanguage === "fa" ? "مقصد بکاپ:" : "Backup Destination:" - color: "#00e5ff80" - font.pixelSize: 14 - } + Rectangle { + Layout.fillWidth: true + radius: 12 + color: "#0a1017" + border.color: "#1b2b3a" + implicitHeight: optionsColumn.implicitHeight + 24 - TextField { - Layout.fillWidth: true - placeholderText: trManager.currentLanguage === "fa" ? "مثال: /home/user/VoidOneBackups" : "Example: /home/user/VoidOneBackups" - text: root.backupDestinationPath - color: "#f8fafc" - placeholderTextColor: "#64748b" - onEditingFinished: { - root.backupDestinationPath = text.trim() - root.refreshAutoSaveConfiguration() + ColumnLayout { + id: optionsColumn + anchors.fill: parent + anchors.margins: 12 + spacing: 12 + + RowLayout { + Layout.fillWidth: true + spacing: 12 + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { + text: root.isPersian ? "پشتیبان‌گیری خودکار" : "Automatic backups" + color: "#e3edf4" + font.pixelSize: 12 + font.bold: true + } + Text { + text: root.isPersian + ? "تا وقتی برنامه باز است، طبق بازهٔ تعیین‌شده بکاپ می‌گیرد." + : "Creates backups on the selected interval while VoidOne is running." + color: "#75899c" + font.pixelSize: 10 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + Switch { + id: autoSaveSwitch + checked: saveBackupManager.autoSaveEnabled + onToggled: { + root.refreshAutoSaveConfiguration() + if (checked && (root.saveDirPath.length === 0 || root.backupDestinationPath.length === 0)) { + statusText.text = root.isPersian + ? "ابتدا هر دو مسیر را مشخص کن." + : "Choose both folders before enabling automatic backups." + statusText.color = "#f5b84b" + } + } + indicator: Rectangle { + implicitWidth: 42 + implicitHeight: 24 + x: autoSaveSwitch.leftPadding + y: parent.height / 2 - height / 2 + radius: 12 + color: autoSaveSwitch.checked ? "#00b8ce" : "#17232e" + border.color: autoSaveSwitch.checked ? "#00e5ff" : "#3a4b5a" + Rectangle { + width: 18 + height: 18 + radius: 9 + y: 3 + x: autoSaveSwitch.checked ? parent.width - width - 3 : 3 + color: autoSaveSwitch.checked ? "#041015" : "#8ca0b1" + } + } + } + } + + Rectangle { Layout.fillWidth: true; height: 1; color: "#182735" } + + GridLayout { + Layout.fillWidth: true + columns: 2 + rowSpacing: 6 + columnSpacing: 12 + + Text { + text: root.isPersian ? "فاصلهٔ بکاپ (ثانیه)" : "Backup interval (seconds)" + color: "#a9bac8" + font.pixelSize: 11 + } + SpinBox { + id: intervalSpinBox + Layout.fillWidth: true + from: 5 + to: 3600 + value: saveBackupManager.autoSaveIntervalSeconds + editable: true + onValueModified: root.refreshAutoSaveConfiguration() + } + + Text { + text: root.isPersian ? "حداکثر نسخه‌های نگه‌داری‌شده" : "Maximum backups to keep" + color: "#a9bac8" + font.pixelSize: 11 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + SpinBox { + Layout.fillWidth: true + from: 1 + to: 1000 + value: saveBackupManager.maxBackups + editable: true + onValueModified: saveBackupManager.maxBackups = value + } } } } - Item { Layout.fillHeight: true } - - // Action Status / Message Box Rectangle { Layout.fillWidth: true - height: 40 - color: "#0a0d12" - border.color: "#00e5ff30" - radius: 8 - - Text { - id: statusText - anchors.centerIn: parent - text: trManager.currentLanguage === "fa" ? "وضعیت: آماده برای پایش و پشتیبان‌گیری..." : "Status: Ready for backup monitoring..." - color: "#00e5ff" - font.pixelSize: 12 + implicitHeight: statusText.implicitHeight + 18 + radius: 9 + color: "#080e14" + border.color: "#1b2b3a" + RowLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + Rectangle { + width: 7 + height: 7 + radius: 4 + color: statusText.color + } + Text { + id: statusText + Layout.fillWidth: true + text: root.isPersian ? "آمادهٔ پشتیبان‌گیری" : "Ready to create a backup" + color: "#8ba0b2" + font.pixelSize: 11 + wrapMode: Text.WordWrap + } } } - // Manual Backup Action Buttons RowLayout { Layout.fillWidth: true - spacing: 15 - + spacing: 10 Button { Layout.fillWidth: true - height: 40 - background: Rectangle { - color: parent.pressed ? "#00b8ce" : "#00e5ff20" - border.color: "#00e5ff" - radius: 8 - } - contentItem: Text { - text: trManager.currentLanguage === "fa" ? "ایجاد بکاپ الان" : "Create Backup Now" - color: "#00e5ff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.bold: true - } + implicitHeight: 40 + text: root.isPersian ? "ایجاد بکاپ" : "Create backup" onClicked: { - root.saveDirPath = root.saveDirPath.trim() - root.backupDestinationPath = root.backupDestinationPath.trim() - - if (root.saveDirPath.length === 0 || root.backupDestinationPath.length === 0) { - statusText.color = "#f59e0b" - statusText.text = trManager.currentLanguage === "fa" - ? "وضعیت: مسیر سیو و مقصد بکاپ را وارد کنید" - : "Status: Enter both save and backup paths" + root.saveDirPath = savePathField.text.trim() + root.backupDestinationPath = backupPathField.text.trim() + if (!root.saveDirPath.length || !root.backupDestinationPath.length) { + statusText.text = root.isPersian + ? "مسیر سیو و مقصد بکاپ را مشخص کن." + : "Choose both the save folder and backup destination." + statusText.color = "#f5b84b" return } - saveBackupManager.createBackup(root.saveDirPath, root.backupDestinationPath) } - } - - Button { - Layout.fillWidth: true - height: 40 background: Rectangle { - color: "transparent" - border.color: "#00e5ff50" - radius: 8 + radius: 9 + color: parent.down ? "#00b8ce" : (parent.hovered ? "#27eaff" : "#00d8ef") } contentItem: Text { - text: trManager.currentLanguage === "fa" ? "بازگردانی بکاپ" : "Restore Backup" - color: "#00e5ff80" + text: parent.text + color: "#041015" + font.pixelSize: 11 + font.bold: true horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } + } + Button { + Layout.fillWidth: true + implicitHeight: 40 + text: root.isPersian ? "بازگردانی آخرین بکاپ" : "Restore latest backup" onClicked: { - root.saveDirPath = root.saveDirPath.trim() - root.backupDestinationPath = root.backupDestinationPath.trim() - - if (root.saveDirPath.length === 0 || root.backupDestinationPath.length === 0) { - statusText.color = "#f59e0b" - statusText.text = trManager.currentLanguage === "fa" - ? "وضعیت: مسیر سیو و مقصد بکاپ را وارد کنید" - : "Status: Enter both save and backup paths" + root.saveDirPath = savePathField.text.trim() + root.backupDestinationPath = backupPathField.text.trim() + if (!root.saveDirPath.length || !root.backupDestinationPath.length) { + statusText.text = root.isPersian + ? "مسیر سیو و مقصد بکاپ را مشخص کن." + : "Choose both folders before restoring." + statusText.color = "#f5b84b" return } - - var latestPath = saveBackupManager.latestBackupPath(root.backupDestinationPath) - if (latestPath.length === 0) { - statusText.color = "#ef4444" - statusText.text = trManager.currentLanguage === "fa" - ? "وضعیت: هیچ بکاپی یافت نشد" - : "Status: No backups found" + const latest = saveBackupManager.latestBackupPath(root.backupDestinationPath) + if (!latest.length) { + statusText.text = root.isPersian ? "هیچ بکاپی پیدا نشد." : "No backup was found." + statusText.color = "#ff6878" return } - - statusText.text = trManager.currentLanguage === "fa" ? "وضعیت: در حال بازگردانی بکاپ..." : "Status: Restoring backup..." + statusText.text = root.isPersian + ? "در حال بازگردانی…" + : "Restoring backup…" statusText.color = "#00e5ff" - saveBackupManager.restoreBackup(latestPath, root.saveDirPath) + saveBackupManager.restoreBackup(latest, root.saveDirPath) + } + background: Rectangle { + radius: 9 + color: parent.hovered ? "#142533" : "#0b121a" + border.color: "#2a3d4e" + } + contentItem: Text { + text: parent.text + color: "#c4d2dd" + font.pixelSize: 11 + font.bold: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter } } } From 38dc6b8abab8c655372b24e525e98eea387815a3 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:43:58 +0330 Subject: [PATCH 025/104] feat(ui): organize settings into functional sections --- src/ui/qml/Main.qml | 219 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 207 insertions(+), 12 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index ce5244c1..a4fa0a29 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -603,15 +603,25 @@ Window { } // SETTINGS - Item { + Flickable { + id: settingsFlickable + clip: true + contentWidth: width + contentHeight: settingsColumn.implicitHeight + 48 + boundsBehavior: Flickable.StopAtBounds + ColumnLayout { - anchors.fill: parent - anchors.margins: 24 - spacing: 16 + id: settingsColumn + width: Math.max(0, settingsFlickable.width - 48) + anchors.top: parent.top + anchors.topMargin: 24 + anchors.left: parent.left + anchors.leftMargin: 24 + spacing: 18 ColumnLayout { Layout.fillWidth: true - spacing: 4 + spacing: 5 Text { text: qsTr("Settings") color: theme.text @@ -619,24 +629,209 @@ Window { font.bold: true } Text { - text: qsTr("Manage local save backups and automatic backup behavior.") + text: qsTr("Configure VoidOne's real features and local preferences.") color: theme.muted font.pixelSize: 12 + wrapMode: Text.WordWrap + Layout.fillWidth: true } } + // Language preferences Rectangle { Layout.fillWidth: true - height: 1 - color: "#14202c" + implicitHeight: languageRow.implicitHeight + 28 + radius: 14 + color: theme.surface + border.color: "#1b2b3a" + + RowLayout { + id: languageRow + anchors.fill: parent + anchors.margins: 14 + spacing: 14 + + Rectangle { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + radius: 11 + color: theme.cyanSoft + border.color: theme.cyanLine + Text { + anchors.centerIn: parent + text: "文" + color: theme.cyan + font.pixelSize: 18 + font.bold: true + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { + text: qsTr("Language") + color: theme.text + font.pixelSize: 13 + font.bold: true + } + Text { + text: trManager.currentLanguage === "fa" + ? "فارسی" + : "English" + color: theme.muted + font.pixelSize: 11 + } + } + + Button { + text: trManager.currentLanguage === "en" ? "فارسی" : "English" + onClicked: trManager.currentLanguage = + trManager.currentLanguage === "en" ? "fa" : "en" + background: Rectangle { + radius: 9 + color: parent.hovered ? "#142b37" : "#0b1720" + border.color: theme.cyanLine + } + contentItem: Text { + text: parent.text + color: theme.cyan + font.pixelSize: 11 + font.bold: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + } + + // Library tools: real Steam scan action, no pretend preferences. + Rectangle { + Layout.fillWidth: true + implicitHeight: libraryRow.implicitHeight + 28 + radius: 14 + color: theme.surface + border.color: "#1b2b3a" + + RowLayout { + id: libraryRow + anchors.fill: parent + anchors.margins: 14 + spacing: 14 + + Rectangle { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + radius: 11 + color: theme.cyanSoft + border.color: theme.cyanLine + Image { + anchors.centerIn: parent + width: 21 + height: 21 + source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/library.svg" + fillMode: Image.PreserveAspectFit + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { + text: qsTr("Game library") + color: theme.text + font.pixelSize: 13 + font.bold: true + } + Text { + text: qsTr("Scan detected Steam libraries for installed games.") + color: theme.muted + font.pixelSize: 11 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Button { + text: qsTr("Scan Steam") + onClicked: { + steamScanner.startAsyncScan() + root.showNotification(qsTr("Scanning installed Steam games...")) + } + background: Rectangle { + radius: 9 + color: parent.hovered ? "#142b37" : "#0b1720" + border.color: "#2a4051" + } + contentItem: Text { + text: parent.text + color: theme.text + font.pixelSize: 11 + font.bold: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } } SaveBackupView { - Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true - Layout.fillHeight: true - Layout.maximumWidth: 820 - Layout.minimumHeight: 360 + Layout.maximumWidth: 900 + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: aboutRow.implicitHeight + 28 + radius: 14 + color: theme.surface + border.color: "#1b2b3a" + + RowLayout { + id: aboutRow + anchors.fill: parent + anchors.margins: 14 + spacing: 14 + + Rectangle { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + radius: 11 + color: theme.cyanSoft + border.color: theme.cyanLine + Image { + anchors.centerIn: parent + width: 25 + height: 25 + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + fillMode: Image.PreserveAspectFit + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { + text: qsTr("About VoidOne") + color: theme.text + font.pixelSize: 13 + font.bold: true + } + Text { + text: qsTr("Local-first. Private by design. Your games stay yours.") + color: theme.muted + font.pixelSize: 11 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Text { + text: "v" + Qt.application.version + color: theme.dim + font.pixelSize: 10 + } + } } } } From d5b30cd7527334e4864d5ad508698e36ecb9a494 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:44:16 +0330 Subject: [PATCH 026/104] fix(ui): center compact navigation icons --- src/ui/qml/SidebarButton.qml | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/ui/qml/SidebarButton.qml b/src/ui/qml/SidebarButton.qml index 1630b7a1..be448c16 100644 --- a/src/ui/qml/SidebarButton.qml +++ b/src/ui/qml/SidebarButton.qml @@ -25,25 +25,29 @@ Button { Behavior on color { ColorAnimation { duration: 140 } } } - contentItem: RowLayout { - anchors.fill: parent - anchors.leftMargin: compact ? 17 : 17 - anchors.rightMargin: compact ? 17 : 13 - spacing: 12 - + contentItem: Item { Image { - Layout.preferredWidth: 20 - Layout.preferredHeight: 20 + id: navIcon + x: button.compact ? (parent.width - width) / 2 : 17 + anchors.verticalCenter: parent.verticalCenter + width: 20 + height: 20 source: button.iconSource - opacity: button.voSelected ? 1 : 0.65 + opacity: button.voSelected ? 1 : 0.68 fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true } Text { - visible: !compact - Layout.fillWidth: true + visible: !button.compact + anchors.left: navIcon.right + anchors.leftMargin: 12 + anchors.right: selectedMarker.left + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter text: button.voLabel - color: button.voSelected ? "#eaf8fc" : "#74889c" + color: button.voSelected ? "#eaf8fc" : "#8193a5" font.pixelSize: 12 font.bold: button.voSelected elide: Text.ElideRight @@ -52,8 +56,12 @@ Button { } Rectangle { - visible: !compact && button.voSelected - width: 4 + id: selectedMarker + visible: !button.compact && button.voSelected + anchors.right: parent.right + anchors.rightMargin: 13 + anchors.verticalCenter: parent.verticalCenter + width: 3 height: 18 radius: 2 color: "#00e5ff" From fb75d46018ab93ab4a412abf4af3d0a6bbede14c Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:44:43 +0330 Subject: [PATCH 027/104] fix(library): cache native Windows executable icons --- src/core/GameModel.cpp | 68 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index d0a6f7b1..fd5ced30 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -10,14 +10,17 @@ #include #include #include -#include -#include +#include #include -#include #include +#ifdef Q_OS_WIN +#include +#include +#endif #include #include #include +#include namespace { @@ -62,16 +65,65 @@ QString extractExecutableIcon(const QString &executablePath) if (QFileInfo::exists(iconPath)) return iconPath; - QFileIconProvider provider; - const QIcon icon = provider.icon(executable); - if (icon.isNull()) +#ifdef Q_OS_WIN + SHFILEINFOW fileInfo{}; + const DWORD_PTR result = SHGetFileInfoW( + reinterpret_cast(executable.absoluteFilePath().utf16()), + 0, &fileInfo, sizeof(fileInfo), SHGFI_ICON | SHGFI_LARGEICON); + if (result == 0 || fileInfo.hIcon == nullptr) + return {}; + + constexpr int iconSize = 96; + BITMAPINFO bitmapInfo{}; + bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bitmapInfo.bmiHeader.biWidth = iconSize; + bitmapInfo.bmiHeader.biHeight = -iconSize; + bitmapInfo.bmiHeader.biPlanes = 1; + bitmapInfo.bmiHeader.biBitCount = 32; + bitmapInfo.bmiHeader.biCompression = BI_RGB; + + HDC screenDc = GetDC(nullptr); + if (screenDc == nullptr) { + DestroyIcon(fileInfo.hIcon); return {}; + } - const QPixmap pixmap = icon.pixmap(96, 96); - if (pixmap.isNull() || !pixmap.save(iconPath, "PNG")) + void *pixels = nullptr; + HBITMAP bitmap = CreateDIBSection(screenDc, &bitmapInfo, DIB_RGB_COLORS, + &pixels, nullptr, 0); + HDC memoryDc = CreateCompatibleDC(screenDc); + if (bitmap == nullptr || memoryDc == nullptr || pixels == nullptr) { + if (memoryDc != nullptr) + DeleteDC(memoryDc); + if (bitmap != nullptr) + DeleteObject(bitmap); + ReleaseDC(nullptr, screenDc); + DestroyIcon(fileInfo.hIcon); return {}; + } + HGDIOBJ previousObject = SelectObject(memoryDc, bitmap); + std::memset(pixels, 0, iconSize * iconSize * 4); + const BOOL drawn = DrawIconEx(memoryDc, 0, 0, fileInfo.hIcon, + iconSize, iconSize, 0, nullptr, DI_NORMAL); + QImage image(static_cast(pixels), iconSize, iconSize, + iconSize * 4, QImage::Format_ARGB32); + const QImage cachedImage = image.copy(); + + SelectObject(memoryDc, previousObject); + DeleteDC(memoryDc); + DeleteObject(bitmap); + ReleaseDC(nullptr, screenDc); + DestroyIcon(fileInfo.hIcon); + + if (!drawn || cachedImage.isNull() || !cachedImage.save(iconPath, "PNG")) + return {}; return iconPath; +#else + Q_UNUSED(executable) + Q_UNUSED(iconPath) + return {}; +#endif } } // namespace From 0c2a642ddb0a1f20e35a93af8be0d59e3a08012c Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:44:48 +0330 Subject: [PATCH 028/104] build(windows): link shell icon extraction API --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 691b8967..2e49a933 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,7 +186,7 @@ qt_add_qml_module(VoidOne ) target_link_libraries(VoidOne PRIVATE voidone_core Qt6::Gui Qt6::Quick Qt6::Svg) if(WIN32) - target_link_libraries(VoidOne PRIVATE Dbghelp) + target_link_libraries(VoidOne PRIVATE Dbghelp Shell32) endif() voidone_apply_compile_policy(VoidOne) From 0b3d35fbc91e5d34514b60a90c80769f06141bd3 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:45:01 +0330 Subject: [PATCH 029/104] fix(settings): keep backup form responsive and validate auto-save --- src/ui/qml/SaveBackupView.qml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/ui/qml/SaveBackupView.qml b/src/ui/qml/SaveBackupView.qml index 84c47abb..4fcedc38 100644 --- a/src/ui/qml/SaveBackupView.qml +++ b/src/ui/qml/SaveBackupView.qml @@ -104,22 +104,17 @@ Rectangle { color: "#1b2b3a" } - GridLayout { + ColumnLayout { Layout.fillWidth: true - columns: root.width < 590 ? 1 : 2 - columnSpacing: 12 - rowSpacing: 7 + spacing: 7 Text { text: root.isPersian ? "پوشهٔ سیو بازی" : "Game save folder" color: "#b9c8d5" font.pixelSize: 11 font.bold: true - Layout.fillWidth: true } - Item { visible: root.width >= 590; Layout.fillWidth: true; height: 1 } - RowLayout { Layout.fillWidth: true spacing: 8 @@ -162,17 +157,19 @@ Rectangle { ToolTip.text: root.isPersian ? "انتخاب پوشه" : "Browse folder" } } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 7 Text { text: root.isPersian ? "مقصد پشتیبان‌گیری" : "Backup destination" color: "#b9c8d5" font.pixelSize: 11 font.bold: true - Layout.fillWidth: true } - Item { visible: root.width >= 590; Layout.fillWidth: true; height: 1 } - RowLayout { Layout.fillWidth: true spacing: 8 @@ -256,13 +253,14 @@ Rectangle { id: autoSaveSwitch checked: saveBackupManager.autoSaveEnabled onToggled: { - root.refreshAutoSaveConfiguration() if (checked && (root.saveDirPath.length === 0 || root.backupDestinationPath.length === 0)) { + checked = false statusText.text = root.isPersian ? "ابتدا هر دو مسیر را مشخص کن." : "Choose both folders before enabling automatic backups." statusText.color = "#f5b84b" } + root.refreshAutoSaveConfiguration() } indicator: Rectangle { implicitWidth: 42 From d1109a41907ccfef74123bcb8eb98e0dfc01c4ec Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:45:28 +0330 Subject: [PATCH 030/104] fix(settings): clarify language scope --- src/ui/qml/Main.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index a4fa0a29..8e72c074 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -670,15 +670,15 @@ Window { Layout.fillWidth: true spacing: 3 Text { - text: qsTr("Language") + text: qsTr("Backup panel language") color: theme.text font.pixelSize: 13 font.bold: true } Text { text: trManager.currentLanguage === "fa" - ? "فارسی" - : "English" + ? "فقط برچسب‌های بخش پشتیبان‌گیری به فارسی نمایش داده می‌شوند." + : "Changes labels in the backup controls only." color: theme.muted font.pixelSize: 11 } From 15f3a55763208d59074a5b1def82e846eaf31c7c Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:45:41 +0330 Subject: [PATCH 031/104] fix(settings): sync backup controls with manager state --- src/ui/qml/SaveBackupView.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ui/qml/SaveBackupView.qml b/src/ui/qml/SaveBackupView.qml index 4fcedc38..0c7a4521 100644 --- a/src/ui/qml/SaveBackupView.qml +++ b/src/ui/qml/SaveBackupView.qml @@ -50,6 +50,15 @@ Rectangle { statusText.text = message statusText.color = success ? "#38d996" : "#ff6878" } + function onAutoSaveEnabledChanged(enabled) { + autoSaveSwitch.checked = enabled + } + function onAutoSaveIntervalChanged(seconds) { + intervalSpinBox.value = seconds + } + function onMaxBackupsChanged(count) { + maxBackupsSpinBox.value = count + } } ColumnLayout { @@ -313,6 +322,7 @@ Rectangle { Layout.fillWidth: true } SpinBox { + id: maxBackupsSpinBox Layout.fillWidth: true from: 1 to: 1000 From 74b810d7e5eca5fe5aca577f771942437c50084f Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:46:19 +0330 Subject: [PATCH 032/104] feat(settings): expose persisted backup paths --- src/core/SaveBackupManager.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/SaveBackupManager.h b/src/core/SaveBackupManager.h index 9070d422..6cfe8069 100644 --- a/src/core/SaveBackupManager.h +++ b/src/core/SaveBackupManager.h @@ -10,6 +10,8 @@ class SaveBackupManager : public QObject { Q_PROPERTY(bool autoSaveEnabled READ isAutoSaveEnabled WRITE setAutoSaveEnabled NOTIFY autoSaveEnabledChanged) Q_PROPERTY(int autoSaveIntervalSeconds READ autoSaveIntervalSeconds WRITE setAutoSaveIntervalSeconds NOTIFY autoSaveIntervalChanged) Q_PROPERTY(int maxBackups READ maxBackups WRITE setMaxBackups NOTIFY maxBackupsChanged) + Q_PROPERTY(QString saveDirPath READ saveDirPath NOTIFY pathsChanged) + Q_PROPERTY(QString backupDestinationPath READ backupDestinationPath NOTIFY pathsChanged) public: explicit SaveBackupManager(QObject *parent = nullptr); @@ -27,6 +29,9 @@ class SaveBackupManager : public QObject { int maxBackups() const { return m_maxBackups; } void setMaxBackups(int count); + QString saveDirPath() const { return m_targetSaveDir; } + QString backupDestinationPath() const { return m_destinationDir; } + Q_INVOKABLE void configureAutoSave(const QString &saveDirPath, const QString &backupDestinationPath); signals: @@ -34,6 +39,7 @@ class SaveBackupManager : public QObject { void autoSaveEnabledChanged(bool enabled); void autoSaveIntervalChanged(int seconds); void maxBackupsChanged(int count); + void pathsChanged(); private slots: void performAutoSave(); From c5cf61de711290e85886e0125b8afa7831aefd76 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:46:30 +0330 Subject: [PATCH 033/104] feat(settings): persist backup preferences across restarts --- src/core/SaveBackupManager.cpp | 43 +++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/core/SaveBackupManager.cpp b/src/core/SaveBackupManager.cpp index 4306b2ba..02762bd5 100644 --- a/src/core/SaveBackupManager.cpp +++ b/src/core/SaveBackupManager.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace { @@ -84,7 +85,24 @@ bool pathsOverlap(const QString &firstPath, const QString &secondPath) SaveBackupManager::SaveBackupManager(QObject *parent) : QObject(parent), m_autoSaveTimer(new QTimer(this)) { + QSettings settings; + settings.beginGroup(QStringLiteral("backups")); + m_targetSaveDir = settings.value(QStringLiteral("saveDirPath")).toString(); + m_destinationDir = settings.value(QStringLiteral("destinationPath")).toString(); + m_intervalSeconds = std::clamp(settings.value(QStringLiteral("intervalSeconds"), 60).toInt(), 5, 3600); + m_maxBackups = std::clamp(settings.value(QStringLiteral("maxBackups"), 20).toInt(), 1, 1000); + + const bool requestedAutoSave = settings.value(QStringLiteral("autoSaveEnabled"), false).toBool(); + m_autoSaveEnabled = requestedAutoSave + && QDir(m_targetSaveDir).exists() + && !m_destinationDir.trimmed().isEmpty(); + if (requestedAutoSave != m_autoSaveEnabled) + settings.setValue(QStringLiteral("autoSaveEnabled"), m_autoSaveEnabled); + settings.endGroup(); + connect(m_autoSaveTimer, &QTimer::timeout, this, &SaveBackupManager::performAutoSave); + if (m_autoSaveEnabled) + m_autoSaveTimer->start(m_intervalSeconds * 1000); } bool SaveBackupManager::createBackup(const QString &saveDirPath, const QString &backupDestinationPath) @@ -208,6 +226,9 @@ void SaveBackupManager::setAutoSaveEnabled(bool enabled) m_autoSaveTimer->start(m_intervalSeconds * 1000); else m_autoSaveTimer->stop(); + + QSettings settings; + settings.setValue(QStringLiteral("backups/autoSaveEnabled"), m_autoSaveEnabled); emit autoSaveEnabledChanged(m_autoSaveEnabled); } @@ -216,9 +237,12 @@ void SaveBackupManager::setAutoSaveIntervalSeconds(int seconds) if (seconds <= 0 || m_intervalSeconds == seconds) return; - m_intervalSeconds = seconds; + m_intervalSeconds = std::clamp(seconds, 5, 3600); if (m_autoSaveEnabled) m_autoSaveTimer->start(m_intervalSeconds * 1000); + + QSettings settings; + settings.setValue(QStringLiteral("backups/intervalSeconds"), m_intervalSeconds); emit autoSaveIntervalChanged(m_intervalSeconds); } @@ -231,13 +255,26 @@ void SaveBackupManager::setMaxBackups(int count) m_maxBackups = bounded; if (!m_destinationDir.isEmpty()) pruneOldBackups(m_destinationDir); + + QSettings settings; + settings.setValue(QStringLiteral("backups/maxBackups"), m_maxBackups); emit maxBackupsChanged(m_maxBackups); } void SaveBackupManager::configureAutoSave(const QString &saveDirPath, const QString &backupDestinationPath) { - m_targetSaveDir = saveDirPath.trimmed(); - m_destinationDir = backupDestinationPath.trimmed(); + const QString savePath = saveDirPath.trimmed(); + const QString destinationPath = backupDestinationPath.trimmed(); + if (m_targetSaveDir == savePath && m_destinationDir == destinationPath) + return; + + m_targetSaveDir = savePath; + m_destinationDir = destinationPath; + + QSettings settings; + settings.setValue(QStringLiteral("backups/saveDirPath"), m_targetSaveDir); + settings.setValue(QStringLiteral("backups/destinationPath"), m_destinationDir); + emit pathsChanged(); } void SaveBackupManager::performAutoSave() From 788337c6cf604841a844316473d5ba86c3656db8 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:46:39 +0330 Subject: [PATCH 034/104] feat(settings): restore saved backup paths on startup --- src/ui/qml/SaveBackupView.qml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/qml/SaveBackupView.qml b/src/ui/qml/SaveBackupView.qml index 0c7a4521..b5c9e8b1 100644 --- a/src/ui/qml/SaveBackupView.qml +++ b/src/ui/qml/SaveBackupView.qml @@ -12,8 +12,8 @@ Rectangle { border.color: "#1b2b3a" border.width: 1 - property string saveDirPath: "" - property string backupDestinationPath: "" + property string saveDirPath: saveBackupManager.saveDirPath + property string backupDestinationPath: saveBackupManager.backupDestinationPath readonly property bool isPersian: trManager.currentLanguage === "fa" function refreshAutoSaveConfiguration() { @@ -59,6 +59,10 @@ Rectangle { function onMaxBackupsChanged(count) { maxBackupsSpinBox.value = count } + function onPathsChanged() { + root.saveDirPath = saveBackupManager.saveDirPath + root.backupDestinationPath = saveBackupManager.backupDestinationPath + } } ColumnLayout { From cf8f310b8cba3f5010fd7823e612b9b5b5b2ea4a Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:46:57 +0330 Subject: [PATCH 035/104] fix(settings): confirm before replacing game saves --- src/ui/qml/SaveBackupView.qml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/ui/qml/SaveBackupView.qml b/src/ui/qml/SaveBackupView.qml index b5c9e8b1..d4c2ae89 100644 --- a/src/ui/qml/SaveBackupView.qml +++ b/src/ui/qml/SaveBackupView.qml @@ -14,6 +14,8 @@ Rectangle { property string saveDirPath: saveBackupManager.saveDirPath property string backupDestinationPath: saveBackupManager.backupDestinationPath + property string pendingRestorePath: "" + property string pendingRestoreTarget: "" readonly property bool isPersian: trManager.currentLanguage === "fa" function refreshAutoSaveConfiguration() { @@ -44,6 +46,20 @@ Rectangle { } } + MessageDialog { + id: restoreConfirmation + title: root.isPersian ? "تأیید بازگردانی سیو" : "Confirm save restore" + text: root.isPersian + ? "بازگردانی، پوشهٔ سیو فعلی را جایگزین می‌کند. قبل از ادامه مطمئن شو." + : "Restoring replaces the current save folder. Make sure you want to continue." + buttons: MessageDialog.Yes | MessageDialog.Cancel + onAccepted: { + statusText.text = root.isPersian ? "در حال بازگردانی…" : "Restoring backup…" + statusText.color = "#00e5ff" + saveBackupManager.restoreBackup(root.pendingRestorePath, root.pendingRestoreTarget) + } + } + Connections { target: saveBackupManager function onBackupCompleted(success, message) { @@ -417,11 +433,9 @@ Rectangle { statusText.color = "#ff6878" return } - statusText.text = root.isPersian - ? "در حال بازگردانی…" - : "Restoring backup…" - statusText.color = "#00e5ff" - saveBackupManager.restoreBackup(latest, root.saveDirPath) + root.pendingRestorePath = latest + root.pendingRestoreTarget = root.saveDirPath + restoreConfirmation.open() } background: Rectangle { radius: 9 From 33a2fa38fbe46badf99e97c69e40d3d5d0af3363 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:47:17 +0330 Subject: [PATCH 036/104] build(core): link Qt Gui for icon cache --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e49a933..96346bf9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,7 +144,7 @@ target_include_directories(voidone_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src/core" "${VOIDONE_GENERATED_DIR}" ) -target_link_libraries(voidone_core PUBLIC Qt6::Core Qt6::Sql Qt6::Network) +target_link_libraries(voidone_core PUBLIC Qt6::Core Qt6::Gui Qt6::Sql Qt6::Network) target_precompile_headers(voidone_core PRIVATE ) voidone_apply_compile_policy(voidone_core) From 935aa016f709cca9533f07df509023edb53d6b3a Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:47:30 +0330 Subject: [PATCH 037/104] chore(core): tidy icon cache includes --- src/core/GameModel.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index fd5ced30..42ec2ea3 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #ifdef Q_OS_WIN #include #include From ab14202f96cae766a407525288bace7a56c1c856 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:47:43 +0330 Subject: [PATCH 038/104] polish(ui): add settings scrollbar and responsive copy --- src/ui/qml/Main.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 8e72c074..4fe78b95 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -609,6 +609,9 @@ Window { contentWidth: width contentHeight: settingsColumn.implicitHeight + 48 boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } ColumnLayout { id: settingsColumn @@ -681,6 +684,8 @@ Window { : "Changes labels in the backup controls only." color: theme.muted font.pixelSize: 11 + wrapMode: Text.WordWrap + Layout.fillWidth: true } } From b9fdcbcd3d5816f033f599fdfefa73504929258f Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:47:58 +0330 Subject: [PATCH 039/104] feat(settings): remember backup panel language --- src/core/TranslationManager.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/TranslationManager.cpp b/src/core/TranslationManager.cpp index 64f6e899..9ccf5474 100644 --- a/src/core/TranslationManager.cpp +++ b/src/core/TranslationManager.cpp @@ -1,12 +1,21 @@ #include "TranslationManager.h" +#include + TranslationManager::TranslationManager(QObject *parent) : QObject(parent) { initDictionary(); + + QSettings settings; + const QString savedLanguage = settings.value(QStringLiteral("ui/backupLanguage"), QStringLiteral("en")).toString(); + if (savedLanguage == QStringLiteral("fa")) + m_currentLanguage = savedLanguage; } void TranslationManager::setCurrentLanguage(const QString &lang) { if (m_currentLanguage != lang && (lang == "en" || lang == "fa")) { m_currentLanguage = lang; + QSettings settings; + settings.setValue(QStringLiteral("ui/backupLanguage"), m_currentLanguage); emit languageChanged(); } } From b4526e17a14399bf50c60bddcfa9bff86e3be501 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:48:25 +0330 Subject: [PATCH 040/104] fix(settings): persist paths before backup actions --- src/ui/qml/SaveBackupView.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/qml/SaveBackupView.qml b/src/ui/qml/SaveBackupView.qml index d4c2ae89..da463838 100644 --- a/src/ui/qml/SaveBackupView.qml +++ b/src/ui/qml/SaveBackupView.qml @@ -391,6 +391,7 @@ Rectangle { onClicked: { root.saveDirPath = savePathField.text.trim() root.backupDestinationPath = backupPathField.text.trim() + root.refreshAutoSaveConfiguration() if (!root.saveDirPath.length || !root.backupDestinationPath.length) { statusText.text = root.isPersian ? "مسیر سیو و مقصد بکاپ را مشخص کن." @@ -420,6 +421,7 @@ Rectangle { onClicked: { root.saveDirPath = savePathField.text.trim() root.backupDestinationPath = backupPathField.text.trim() + root.refreshAutoSaveConfiguration() if (!root.saveDirPath.length || !root.backupDestinationPath.length) { statusText.text = root.isPersian ? "مسیر سیو و مقصد بکاپ را مشخص کن." From e0b99bff28f3d6f1fe678263abbe0d0257ee3918 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:51:41 +0330 Subject: [PATCH 041/104] feat(library): add real favorites and recently played filters --- src/core/GameModel.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/GameModel.h b/src/core/GameModel.h index 35d3351c..a3d193f6 100644 --- a/src/core/GameModel.h +++ b/src/core/GameModel.h @@ -43,14 +43,19 @@ class GameModel : public QAbstractListModel Q_INVOKABLE void updateLaunchOptions(int id, const QString &args, const QString &workingDir); Q_INVOKABLE QVariantMap getGameDetails(int id) const; Q_INVOKABLE void filter(const QString &searchText); + Q_INVOKABLE void filterGames(const QString &searchText, const QString &mode); signals: void countChanged(); void gameLaunched(int id); private: + void rebuildVisibleGames(); + QVector m_allGames; QVector m_games; + QString m_filterText; + QString m_filterMode = QStringLiteral("all"); }; #endif // GAMEMODEL_H From e1a1d74bc162410105a57ea439ba46875553f59f Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:51:52 +0330 Subject: [PATCH 042/104] feat(library): implement favorites and recent filters --- src/core/GameModel.cpp | 56 +++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index 42ec2ea3..da3edf6c 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -177,7 +177,6 @@ QHash GameModel::roleNames() const void GameModel::loadGamesFromDatabase() { - beginResetModel(); m_allGames = Database::getAllGames(); // Backfill missing icons for games already present in the user's library. @@ -202,13 +201,7 @@ void GameModel::loadGamesFromDatabase() } } - m_games.clear(); - for (const auto &game : std::as_const(m_allGames)) { - if (!game.hidden) - m_games.append(game); - } - endResetModel(); - emit countChanged(); + rebuildVisibleGames(); } bool GameModel::addNewGame(const QString &name, const QString &exePath, const QString &iconPath) @@ -428,25 +421,58 @@ void GameModel::launchGame(const QString &exePath) void GameModel::filter(const QString &searchText) { - const QString needle = searchText.trimmed(); + m_filterText = searchText.trimmed(); + rebuildVisibleGames(); +} + +void GameModel::filterGames(const QString &searchText, const QString &mode) +{ + const QStringList supportedModes = { + QStringLiteral("all"), QStringLiteral("favorites"), QStringLiteral("recent") + }; + m_filterMode = supportedModes.contains(mode) ? mode : QStringLiteral("all"); + m_filterText = searchText.trimmed(); + rebuildVisibleGames(); +} +void GameModel::rebuildVisibleGames() +{ beginResetModel(); m_games.clear(); + for (const auto &game : std::as_const(m_allGames)) { if (game.hidden) continue; - if (needle.isEmpty() - || game.name.contains(needle, Qt::CaseInsensitive) - || game.platform.contains(needle, Qt::CaseInsensitive) - || game.exePath.contains(needle, Qt::CaseInsensitive)) { - m_games.append(game); + if (m_filterMode == QStringLiteral("favorites") && !game.favorite) + continue; + if (m_filterMode == QStringLiteral("recent") && game.lastPlayed <= 0) + continue; + if (!m_filterText.isEmpty() + && !game.name.contains(m_filterText, Qt::CaseInsensitive) + && !game.platform.contains(m_filterText, Qt::CaseInsensitive) + && !game.exePath.contains(m_filterText, Qt::CaseInsensitive) + && !game.source.contains(m_filterText, Qt::CaseInsensitive)) { + continue; } + m_games.append(game); + } + + if (m_filterMode == QStringLiteral("recent")) { + std::stable_sort(m_games.begin(), m_games.end(), + [](const GameRecord &left, const GameRecord &right) { + return left.lastPlayed > right.lastPlayed; + }); + } else if (m_filterMode == QStringLiteral("favorites")) { + std::stable_sort(m_games.begin(), m_games.end(), + [](const GameRecord &left, const GameRecord &right) { + return left.name.compare(right.name, Qt::CaseInsensitive) < 0; + }); } + endResetModel(); emit countChanged(); } - void GameModel::setFavorite(int id, bool favorite) { QSqlQuery q(QSqlDatabase::database(QStringLiteral("voidone-main"), false)); From 7a82a414b9b20705dd015662269d10c8c48ae131 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:52:09 +0330 Subject: [PATCH 043/104] redesign(library): make game cards gamer-first and icon-led --- src/ui/qml/GameCard.qml | 161 +++++++++++++++++++++++----------------- 1 file changed, 92 insertions(+), 69 deletions(-) diff --git a/src/ui/qml/GameCard.qml b/src/ui/qml/GameCard.qml index ec999978..d2246724 100644 --- a/src/ui/qml/GameCard.qml +++ b/src/ui/qml/GameCard.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts + Rectangle { id: cardRoot @@ -14,8 +15,8 @@ Rectangle { property bool favorite: false property int playCount: 0 property string source: "" - signal detailsRequested() + signal detailsRequested() signal launchRequested(string path) function iconUrl(path) { @@ -30,103 +31,119 @@ Rectangle { return "file:///" + path.replace(/\\/g, "/") } + implicitHeight: compact ? 164 : 190 radius: 14 - color: hoverArea.containsMouse ? "#111e2a" : "#0c141d" - border.color: hoverArea.containsMouse ? "#00e5ff66" : "#1a2a39" + color: cardHover.hovered ? "#111e2a" : "#0c141d" + border.color: cardHover.hovered ? "#00e5ff65" : "#1a2a39" border.width: 1 + clip: true Behavior on color { ColorAnimation { duration: 120 } } Behavior on border.color { ColorAnimation { duration: 120 } } + HoverHandler { + id: cardHover + } + Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top height: 2 - radius: 1 - color: hoverArea.containsMouse ? "#00e5ff" : "#00e5ff20" - } - - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onDoubleClicked: cardRoot.launchRequested(cardRoot.exePath) + color: cardHover.hovered ? "#00e5ff" : "#00e5ff20" } ColumnLayout { anchors.fill: parent - anchors.margins: compact ? 13 : 16 - spacing: compact ? 8 : 11 + anchors.margins: compact ? 12 : 15 + spacing: compact ? 9 : 12 RowLayout { Layout.fillWidth: true - spacing: 10 + spacing: 11 Rectangle { - width: compact ? 34 : 40 - height: width - radius: compact ? 9 : 11 - color: "#00e5ff10" - border.color: "#00e5ff22" + Layout.preferredWidth: compact ? 48 : 58 + Layout.preferredHeight: compact ? 48 : 58 + radius: 13 + color: "#071019" + border.color: "#243746" + clip: true Image { id: gameIcon anchors.centerIn: parent - width: parent.width * 0.72 + width: parent.width * 0.78 height: width source: cardRoot.iconUrl(cardRoot.iconPath) fillMode: Image.PreserveAspectFit - visible: status === Image.Ready asynchronous: true + smooth: true + visible: status === Image.Ready } Image { anchors.centerIn: parent - width: parent.width * 0.48 + width: parent.width * 0.62 height: width source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit + asynchronous: true visible: gameIcon.status !== Image.Ready } } ColumnLayout { Layout.fillWidth: true - spacing: 2 + spacing: 4 Text { Layout.fillWidth: true - text: gameName - color: "#eaf2f7" - font.pixelSize: compact ? 13 : 15 + text: cardRoot.gameName + color: "#edf5fa" + font.pixelSize: compact ? 13 : 14 font.bold: true elide: Text.ElideRight + maximumLineCount: 1 } Text { - text: platform - color: "#506477" - font.pixelSize: 9 - font.bold: true + Layout.fillWidth: true + text: (cardRoot.platform || qsTr("Local")) + (cardRoot.source.length ? " · " + cardRoot.source : "") + color: "#70869a" + font.pixelSize: 10 + elide: Text.ElideRight } - } - Text { visible: cardRoot.favorite; text: "★"; color: "#00e5ff"; font.pixelSize: 13 } + Text { + Layout.fillWidth: true + text: cardRoot.playCount > 0 + ? qsTr("%1 launches").arg(cardRoot.playCount) + : qsTr("Not launched yet") + color: "#4f667a" + font.pixelSize: 10 + elide: Text.ElideRight + } + } ToolButton { - implicitWidth: 26 - implicitHeight: 26 - text: "⋯" - onClicked: cardRoot.detailsRequested() + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + text: cardRoot.favorite ? "★" : "☆" + onClicked: { + if (typeof gameModel !== "undefined" && gameModel !== null) + gameModel.setFavorite(cardRoot.gameId, !cardRoot.favorite) + } + ToolTip.visible: hovered + ToolTip.text: cardRoot.favorite ? qsTr("Remove from favorites") : qsTr("Add to favorites") background: Rectangle { radius: 8 - color: parent.hovered ? "#ff5f7018" : "transparent" + color: parent.hovered ? "#00e5ff14" : "transparent" } contentItem: Text { text: parent.text - color: "#53697c" + color: cardRoot.favorite ? "#00e5ff" : "#6f8395" + font.pixelSize: 18 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } @@ -135,42 +152,48 @@ Rectangle { Item { Layout.fillHeight: true } - Text { - visible: !compact - Layout.fillWidth: true - text: exePath - color: "#405467" - font.pixelSize: 9 - elide: Text.ElideMiddle - } - - Button { + RowLayout { Layout.fillWidth: true - implicitHeight: compact ? 34 : 38 - text: qsTr("Play") - onClicked: cardRoot.launchRequested(cardRoot.exePath) + spacing: 8 - background: Rectangle { - radius: 9 - color: parent.pressed ? "#00b8ce" : (parent.hovered ? "#19eaff" : "#00d8ef") + Button { + Layout.fillWidth: true + implicitHeight: 36 + text: qsTr("Play") + onClicked: cardRoot.launchRequested(cardRoot.exePath) + background: Rectangle { + radius: 9 + color: parent.down ? "#00b8ce" : (parent.hovered ? "#20eaff" : "#00d8ef") + } + contentItem: RowLayout { + spacing: 7 + Item { Layout.fillWidth: true } + Text { text: "▶"; color: "#041015"; font.pixelSize: 9; font.bold: true } + Text { text: parent.parent.text; color: "#041015"; font.pixelSize: 11; font.bold: true } + Item { Layout.fillWidth: true } + } } - contentItem: RowLayout { - spacing: 7 - Item { Layout.fillWidth: true } - Text { - text: "▶" - color: "#041015" - font.pixelSize: 10 - font.bold: true + Button { + implicitWidth: 40 + implicitHeight: 36 + text: "···" + onClicked: cardRoot.detailsRequested() + ToolTip.visible: hovered + ToolTip.text: qsTr("Game details") + background: Rectangle { + radius: 9 + color: parent.hovered ? "#152635" : "#0a1118" + border.color: "#263a4b" } - Text { - text: qsTr("Play") - color: "#041015" - font.pixelSize: 11 + contentItem: Text { + text: parent.text + color: "#a5b7c6" + font.pixelSize: 15 font.bold: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter } - Item { Layout.fillWidth: true } } } } From 49807b2243082c912599a8ea91bad3bbaa0b90de Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:53:16 +0330 Subject: [PATCH 044/104] redesign(ui): unify gamer-first home, library, and activity --- src/ui/qml/Main.qml | 212 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 181 insertions(+), 31 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 4fe78b95..8423c2c8 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -23,6 +23,7 @@ Window { property string currentPage: "home" property bool sidebarCompact: false + property string libraryFilter: "all" readonly property bool narrowLayout: width < 1100 QtObject { @@ -55,6 +56,19 @@ Window { toastAnimation.restart() } + function applyPageFilter() { + if (typeof gameModel === "undefined" || gameModel === null) + return + if (currentPage === "activity") + gameModel.filterGames(searchInput.text, "recent") + else if (currentPage === "library") + gameModel.filterGames(searchInput.text, libraryFilter) + else + gameModel.filterGames(searchInput.text, "all") + } + + onCurrentPageChanged: applyPageFilter() + Rectangle { anchors.fill: parent color: theme.background @@ -159,10 +173,7 @@ Window { background: Item {} font.pixelSize: 12 selectByMouse: true - onTextChanged: { - if (typeof gameModel !== "undefined" && gameModel !== null) - gameModel.filter(text) - } + onTextChanged: root.applyPageFilter() } } } @@ -171,7 +182,7 @@ Window { Rectangle { Layout.preferredHeight: 36 - Layout.preferredWidth: 100 + Layout.preferredWidth: 116 radius: 10 color: "#0c141c" border.color: "#172533" @@ -187,7 +198,7 @@ Window { color: theme.success } Text { - text: qsTr("LOCAL") + text: qsTr("LOCAL-FIRST") color: theme.muted font.pixelSize: 10 font.bold: true @@ -233,6 +244,8 @@ Window { contentWidth: width contentHeight: homeColumn.implicitHeight + 48 clip: true + boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } ColumnLayout { id: homeColumn @@ -250,15 +263,19 @@ Window { Layout.fillWidth: true spacing: 6 Text { + Layout.fillWidth: true text: qsTr("Your games. Your hardware. Your rules.") color: theme.text font.pixelSize: root.narrowLayout ? 23 : 29 font.bold: true + wrapMode: Text.WordWrap } Text { + Layout.fillWidth: true text: qsTr("A local-first gaming platform that puts your library back in your hands.") color: theme.muted font.pixelSize: 13 + wrapMode: Text.WordWrap } } @@ -392,15 +409,15 @@ Window { } StatCard { Layout.fillWidth: true - label: qsTr("PLATFORM") - value: "LOCAL" + label: qsTr("ACCOUNT") + value: qsTr("OPTIONAL") detail: qsTr("no account required") } StatCard { Layout.fillWidth: true - label: qsTr("LAUNCH") - value: qsTr("DIRECT") - detail: qsTr("from your library") + label: qsTr("CONTROL") + value: qsTr("YOURS") + detail: qsTr("local-first by design") } } @@ -438,7 +455,7 @@ Window { gameModel.launchGame(path) root.showNotification(qsTr("Launching game...")) } - onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) + onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, iconPath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) } } } @@ -498,6 +515,50 @@ Window { color: "#14202c" } + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + Layout.fillWidth: true + text: qsTr("YOUR GAMES") + color: theme.muted + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.4 + } + + Repeater { + model: [ + { label: qsTr("All"), value: "all" }, + { label: qsTr("Favorites"), value: "favorites" }, + { label: qsTr("Recently played"), value: "recent" } + ] + delegate: Button { + required property var modelData + text: modelData.label + implicitHeight: 32 + onClicked: { + root.libraryFilter = modelData.value + root.applyPageFilter() + } + background: Rectangle { + radius: 9 + color: root.libraryFilter === modelData.value ? "#00e5ff16" : (parent.hovered ? "#101c27" : "#0a1118") + border.color: root.libraryFilter === modelData.value ? "#00e5ff58" : "#1a2a39" + } + contentItem: Text { + text: parent.text + color: root.libraryFilter === modelData.value ? theme.cyan : theme.muted + font.pixelSize: 10 + font.bold: root.libraryFilter === modelData.value + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + } + GridView { id: gameGrid Layout.fillWidth: true @@ -546,7 +607,13 @@ Window { } Text { Layout.alignment: Qt.AlignHCenter - text: qsTr("Your library is empty") + text: searchInput.text.length > 0 + ? qsTr("No games match this search") + : root.libraryFilter === "favorites" + ? qsTr("No favorite games yet") + : root.libraryFilter === "recent" + ? qsTr("No recently played games") + : qsTr("Your library is empty") color: theme.text font.pixelSize: 18 font.bold: true @@ -554,8 +621,12 @@ Window { Text { Layout.alignment: Qt.AlignHCenter text: searchInput.text.length > 0 - ? qsTr("Try a different search term.") - : qsTr("Add an executable or choose a game folder.") + ? qsTr("Try another search or clear the search field.") + : root.libraryFilter === "favorites" + ? qsTr("Mark games as favorites with the star on each game card.") + : root.libraryFilter === "recent" + ? qsTr("Launch a game and it will appear here.") + : qsTr("Add an executable or choose a game folder.") color: theme.muted font.pixelSize: 12 } @@ -563,8 +634,15 @@ Window { Button { Layout.alignment: Qt.AlignHCenter visible: searchInput.text.length === 0 - text: qsTr("+ Add your first game") - onClicked: addGameDialog.open() + text: root.libraryFilter === "all" ? qsTr("+ Add your first game") : qsTr("Show all games") + onClicked: { + if (root.libraryFilter !== "all") { + root.libraryFilter = "all" + root.applyPageFilter() + } else { + addGameDialog.open() + } + } background: Rectangle { radius: 10 color: theme.cyan @@ -581,23 +659,95 @@ Window { } } - // ACTIVITY + // ACTIVITY — actual launch history from the local game database. Item { ColumnLayout { - anchors.centerIn: parent - spacing: 10 - Text { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Activity") - color: theme.text - font.pixelSize: 28 - font.bold: true + anchors.fill: parent + anchors.margins: 24 + spacing: 16 + + ColumnLayout { + Layout.fillWidth: true + spacing: 5 + Text { + text: qsTr("Recently played") + color: theme.text + font.pixelSize: 28 + font.bold: true + } + Text { + text: qsTr("Your launch history, stored locally with your game library.") + color: theme.muted + font.pixelSize: 12 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } } - Text { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Play history and platform events are coming next.") - color: theme.muted - font.pixelSize: 12 + + Rectangle { Layout.fillWidth: true; height: 1; color: "#14202c" } + + GridView { + id: activityGrid + Layout.fillWidth: true + Layout.fillHeight: true + cellWidth: Math.max(240, Math.floor(width / Math.max(1, Math.floor(width / 285)))) + cellHeight: 202 + clip: true + boundsBehavior: Flickable.StopAtBounds + model: typeof gameModel !== "undefined" ? gameModel : null + delegate: GameCard { + width: activityGrid.cellWidth - 12 + height: 190 + gameId: model.id + gameName: model.name + exePath: model.exePath + iconPath: model.iconPath + platform: model.platform + source: model.source + favorite: model.favorite + playCount: model.playCount + itemIndex: index + onLaunchRequested: function(path) { + gameModel.launchGame(path) + root.showNotification(qsTr("Launching game...")) + } + onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, iconPath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) + } + } + + ColumnLayout { + anchors.centerIn: activityGrid + visible: activityGrid.count === 0 + spacing: 10 + Image { + Layout.alignment: Qt.AlignHCenter + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + width: 44 + height: 44 + fillMode: Image.PreserveAspectFit + opacity: 0.75 + } + Text { + Layout.alignment: Qt.AlignHCenter + text: qsTr("No play history yet") + color: theme.text + font.pixelSize: 18 + font.bold: true + } + Text { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: 360 + text: qsTr("Games appear here after you launch them from VoidOne. No activity is invented or sent to a server.") + color: theme.muted + font.pixelSize: 12 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Open library") + onClicked: root.currentPage = "library" + } } } } From 2da3a6b1a270417b609fe9f47411480b059506e6 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:53:26 +0330 Subject: [PATCH 045/104] redesign(details): show the actual game icon in details --- src/ui/qml/GameDetailsDialog.qml | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/GameDetailsDialog.qml b/src/ui/qml/GameDetailsDialog.qml index a8fd8cc9..2cf8af78 100644 --- a/src/ui/qml/GameDetailsDialog.qml +++ b/src/ui/qml/GameDetailsDialog.qml @@ -14,6 +14,7 @@ Dialog { property int gameId: -1 property string gameName: "" property string exePath: "" + property string iconPath: "" property string platform: "" property string source: "" property string workingDir: "" @@ -21,8 +22,20 @@ Dialog { property int playCount: 0 property bool favorite: false - function openFor(id, name, exe, plat, src, dir, args, count, fav) { - gameId=id; gameName=name; exePath=exe; platform=plat; source=src; workingDir=dir + function iconUrl(path) { + if (!path || path.length === 0) + return "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + if (path.startsWith("qrc:/") || path.startsWith("file:/")) + return path + if (path.startsWith(":/")) + return "qrc" + path + if (path.startsWith("/")) + return "file://" + path + return "file:///" + path.replace(/\\/g, "/") + } + + function openFor(id, name, exe, icon, plat, src, dir, args, count, fav) { + gameId=id; gameName=name; exePath=exe; iconPath=icon; platform=plat; source=src; workingDir=dir launchArgs=args; playCount=count; favorite=fav argsField.text=args dirField.text=dir @@ -65,6 +78,17 @@ Dialog { Rectangle { width: 70; height: 70; radius: 17 color: "#00e5ff10"; border.color: "#00e5ff32" + Image { + id: detailsGameIcon + anchors.centerIn: parent + width: 48 + height: 48 + source: dialog.iconUrl(dialog.iconPath) + fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true + visible: status === Image.Ready + } Image { anchors.centerIn: parent width: 40 @@ -72,6 +96,7 @@ Dialog { source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit asynchronous: true + visible: detailsGameIcon.status !== Image.Ready } } @@ -79,7 +104,7 @@ Dialog { Layout.fillWidth: true spacing: 5 Text { text: gameName; color: "#f0f6fa"; font.pixelSize: 25; font.bold: true; elide: Text.ElideRight } - Text { text: platform + " • " + source; color: "#71869a"; font.pixelSize: 11 } + Text { Layout.fillWidth: true; text: platform + " • " + source; color: "#71869a"; font.pixelSize: 11; elide: Text.ElideRight } Text { text: playCount + " " + qsTr("launches"); color: "#4e6579"; font.pixelSize: 10 } } From 8870ccd3fa77d3f0f1af022e1c00fea277a547aa Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:53:42 +0330 Subject: [PATCH 046/104] redesign(settings): group real preferences by purpose --- src/ui/qml/Main.qml | 106 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 8423c2c8..8e0cdf21 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -585,7 +585,7 @@ Window { gameModel.launchGame(path) root.showNotification(qsTr("Launching game...")) } - onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) + onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, iconPath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) } footer: Item { @@ -790,6 +790,94 @@ Window { } } + Text { + text: qsTr("INTERFACE") + color: theme.dim + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.5 + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: navigationRow.implicitHeight + 28 + radius: 14 + color: theme.surface + border.color: "#1b2b3a" + + RowLayout { + id: navigationRow + anchors.fill: parent + anchors.margins: 14 + spacing: 14 + + Rectangle { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + radius: 11 + color: theme.cyanSoft + border.color: theme.cyanLine + Image { + anchors.centerIn: parent + width: 20 + height: 20 + source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/settings.svg" + fillMode: Image.PreserveAspectFit + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { + text: qsTr("Sidebar layout") + color: theme.text + font.pixelSize: 13 + font.bold: true + } + Text { + text: root.sidebarCompact + ? qsTr("Icon-only navigation is active.") + : qsTr("Show navigation labels for easier scanning.") + color: theme.muted + font.pixelSize: 11 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Switch { + checked: root.sidebarCompact + onToggled: root.sidebarCompact = checked + indicator: Rectangle { + implicitWidth: 42 + implicitHeight: 24 + x: parent.leftPadding + y: parent.height / 2 - height / 2 + radius: 12 + color: root.sidebarCompact ? theme.cyan : "#17232e" + border.color: root.sidebarCompact ? theme.cyan : "#3a4b5a" + Rectangle { + width: 18 + height: 18 + radius: 9 + y: 3 + x: root.sidebarCompact ? parent.width - width - 3 : 3 + color: root.sidebarCompact ? "#041015" : "#8ca0b1" + } + } + } + } + } + + Text { + text: qsTr("LOCAL LIBRARY") + color: theme.dim + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.5 + } + // Language preferences Rectangle { Layout.fillWidth: true @@ -930,11 +1018,27 @@ Window { } } + Text { + text: qsTr("SAVE MANAGEMENT") + color: theme.dim + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.5 + } + SaveBackupView { Layout.fillWidth: true Layout.maximumWidth: 900 } + Text { + text: qsTr("ABOUT") + color: theme.dim + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.5 + } + Rectangle { Layout.fillWidth: true implicitHeight: aboutRow.implicitHeight + 28 From 1432afe780f5982092a9a413f12d9c3f8c8e62b8 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:53:52 +0330 Subject: [PATCH 047/104] polish(add-game): make drag-and-drop target discoverable --- src/ui/qml/AddGameDialog.qml | 38 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index d7c2af5f..5fba9bad 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -102,13 +102,15 @@ Dialog { } DropArea { + id: gameDropArea + property bool dragActive: false Layout.fillWidth: true - Layout.preferredHeight: 54 + Layout.preferredHeight: 62 keys: ["text/uri-list"] - onEntered: dragHint.visible = true - onExited: dragHint.visible = false + onEntered: dragActive = true + onExited: dragActive = false onDropped: function(drop) { - dragHint.visible = false + dragActive = false if (drop.urls && drop.urls.length > 0) { var path = drop.urls[0].toLocalFile() if (path.toLowerCase().endsWith(".exe")) { @@ -127,16 +129,26 @@ Dialog { Rectangle { anchors.fill: parent radius: 13 - color: dragHint.visible ? "#00e5ff10" : "#080c12" - border.color: dragHint.visible ? "#00e5ff88" : "#172533" - Text { - id: dragHint + color: gameDropArea.dragActive ? "#00e5ff14" : "#080c12" + border.color: gameDropArea.dragActive ? "#00e5ff88" : "#233545" + RowLayout { anchors.centerIn: parent - visible: false - text: qsTr("Drop a game .exe or folder here") - color: "#00e5ff" - font.pixelSize: 11 - font.bold: true + spacing: 9 + Image { + width: 18 + height: 18 + source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/folder.svg" + fillMode: Image.PreserveAspectFit + opacity: 0.8 + } + Text { + text: gameDropArea.dragActive + ? qsTr("Release to add this game") + : qsTr("Drop a game .exe or folder here") + color: gameDropArea.dragActive ? "#00e5ff" : "#8296a9" + font.pixelSize: 11 + font.bold: gameDropArea.dragActive + } } } } From 53bd0707d7d5677beacbca7e6c49774ebed98695 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:54:12 +0330 Subject: [PATCH 048/104] polish(library): surface real recent-play metadata --- src/ui/qml/GameCard.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ui/qml/GameCard.qml b/src/ui/qml/GameCard.qml index d2246724..b9e17c98 100644 --- a/src/ui/qml/GameCard.qml +++ b/src/ui/qml/GameCard.qml @@ -14,6 +14,7 @@ Rectangle { property bool compact: false property bool favorite: false property int playCount: 0 + property double lastPlayed: 0 property string source: "" signal detailsRequested() @@ -124,6 +125,15 @@ Rectangle { font.pixelSize: 10 elide: Text.ElideRight } + + Text { + visible: !cardRoot.compact && cardRoot.lastPlayed > 0 + Layout.fillWidth: true + text: qsTr("Last played · %1").arg(Qt.formatDateTime(new Date(cardRoot.lastPlayed * 1000), "MMM d, h:mm AP")) + color: "#5d7488" + font.pixelSize: 9 + elide: Text.ElideRight + } } ToolButton { From eb78f555749e03d8c0c519682bc82a26700e9d3c Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:54:18 +0330 Subject: [PATCH 049/104] polish(ui): add timestamps to activity and organize settings --- src/ui/qml/Main.qml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 8e0cdf21..e8fb5f38 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -449,6 +449,7 @@ Window { source: model.source favorite: model.favorite playCount: model.playCount + lastPlayed: model.lastPlayed itemIndex: index compact: true onLaunchRequested: function(path) { @@ -580,6 +581,7 @@ Window { source: model.source favorite: model.favorite playCount: model.playCount + lastPlayed: model.lastPlayed itemIndex: index onLaunchRequested: function(path) { gameModel.launchGame(path) @@ -706,6 +708,7 @@ Window { source: model.source favorite: model.favorite playCount: model.playCount + lastPlayed: model.lastPlayed itemIndex: index onLaunchRequested: function(path) { gameModel.launchGame(path) @@ -878,6 +881,14 @@ Window { font.letterSpacing: 1.5 } + Text { + text: qsTr("PREFERENCES") + color: theme.dim + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.5 + } + // Language preferences Rectangle { Layout.fillWidth: true @@ -948,6 +959,14 @@ Window { } } + Text { + text: qsTr("LOCAL LIBRARY") + color: theme.dim + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.5 + } + // Library tools: real Steam scan action, no pretend preferences. Rectangle { Layout.fillWidth: true From 40a2e8b036ba10f0e44bbf4678f2da5ca8e71e4e Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:54:24 +0330 Subject: [PATCH 050/104] polish(navigation): use product branding instead of placeholder initials --- src/ui/qml/Sidebar.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/qml/Sidebar.qml b/src/ui/qml/Sidebar.qml index 016ee32d..75c1f20e 100644 --- a/src/ui/qml/Sidebar.qml +++ b/src/ui/qml/Sidebar.qml @@ -153,13 +153,13 @@ Rectangle { Layout.fillWidth: true spacing: 1 Text { - text: qsTr("Local player") + text: qsTr("Local profile") color: "#d9e4ec" font.pixelSize: 10 font.bold: true } Text { - text: qsTr("Offline mode") + text: qsTr("Library stays yours") color: "#4e6578" font.pixelSize: 9 } From 6ac49f28256f2e666d9e8a3ffaf829b1a416a9ae Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:54:44 +0330 Subject: [PATCH 051/104] fix(settings): correct section hierarchy --- src/ui/qml/Main.qml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index e8fb5f38..39a8072b 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -873,14 +873,6 @@ Window { } } - Text { - text: qsTr("LOCAL LIBRARY") - color: theme.dim - font.pixelSize: 10 - font.bold: true - font.letterSpacing: 1.5 - } - Text { text: qsTr("PREFERENCES") color: theme.dim From aba133e3a1538e503964565907ae31b65c46d00f Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:55:20 +0330 Subject: [PATCH 052/104] polish(navigation): replace placeholder initials with VoidOne mark --- src/ui/qml/Sidebar.qml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ui/qml/Sidebar.qml b/src/ui/qml/Sidebar.qml index 75c1f20e..f6a1050f 100644 --- a/src/ui/qml/Sidebar.qml +++ b/src/ui/qml/Sidebar.qml @@ -137,14 +137,16 @@ Rectangle { Rectangle { width: 32 height: 32 - radius: 16 + radius: 11 color: "#00e5ff12" - Text { + border.color: "#00e5ff28" + Image { anchors.centerIn: parent - text: "MK" - color: "#00e5ff" - font.pixelSize: 9 - font.bold: true + width: 22 + height: 22 + source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + fillMode: Image.PreserveAspectFit + asynchronous: true } } From 6b76285c9a3daf4e545cd094130d40a1a00f5f18 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:55:28 +0330 Subject: [PATCH 053/104] polish(navigation): add scroll affordances to game grids --- src/ui/qml/Main.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 39a8072b..2f6e1dfd 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -568,6 +568,7 @@ Window { cellHeight: 190 clip: true boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } model: typeof gameModel !== "undefined" ? gameModel : null delegate: GameCard { @@ -696,6 +697,7 @@ Window { cellHeight: 202 clip: true boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } model: typeof gameModel !== "undefined" ? gameModel : null delegate: GameCard { width: activityGrid.cellWidth - 12 From 4e0eceaedffd6d927d4712bd1893831ac4577f56 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:55:42 +0330 Subject: [PATCH 054/104] fix(launch): return the actual launch result --- src/core/GameModel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/GameModel.h b/src/core/GameModel.h index a3d193f6..fcb7c0ec 100644 --- a/src/core/GameModel.h +++ b/src/core/GameModel.h @@ -37,7 +37,7 @@ class GameModel : public QAbstractListModel Q_INVOKABLE bool addNewGame(const QString &name, const QString &exePath, const QString &iconPath); Q_INVOKABLE QStringList suggestExecutables(const QString &folderPath) const; Q_INVOKABLE bool deleteGame(int id, int index); - Q_INVOKABLE void launchGame(const QString &exePath); + Q_INVOKABLE bool launchGame(const QString &exePath); Q_INVOKABLE void setFavorite(int id, bool favorite); Q_INVOKABLE void hideGame(int id, bool hidden); Q_INVOKABLE void updateLaunchOptions(int id, const QString &args, const QString &workingDir); From f8b6081814fb53f9b736ed10964263ed44c6fc9e Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:55:45 +0330 Subject: [PATCH 055/104] fix(launch): report failure instead of fake success --- src/core/GameModel.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index da3edf6c..fdb66781 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -354,29 +354,29 @@ bool GameModel::deleteGame(int id, int index) return true; } -void GameModel::launchGame(const QString &exePath) +bool GameModel::launchGame(const QString &exePath) { const QString trimmedPath = exePath.trimmed(); if (trimmedPath.isEmpty()) { qWarning() << "[VoidOne] Refusing to launch an empty game path."; - return; + return false; } const QFileInfo targetInfo(trimmedPath); if (!targetInfo.isFile() || targetInfo.isSymLink()) { qWarning() << "[VoidOne] Refusing to launch non-file or symlink:" << trimmedPath; - return; + return false; } #if defined(Q_OS_WIN) if (!targetInfo.fileName().endsWith(".exe", Qt::CaseInsensitive)) { qWarning() << "[VoidOne] Refusing non-executable Windows target:" << trimmedPath; - return; + return false; } #else if (!targetInfo.isExecutable()) { qWarning() << "[VoidOne] Refusing non-executable target:" << trimmedPath; - return; + return false; } #endif @@ -406,7 +406,7 @@ void GameModel::launchGame(const QString &exePath) qint64 pid = -1; if (!QProcess::startDetached(absolutePath, arguments, workingDirectory, &pid)) { qWarning() << "[VoidOne] Failed to launch game:" << absolutePath; - return; + return false; } QSqlQuery stats(QSqlDatabase::database(QStringLiteral("voidone-main"), false)); @@ -417,6 +417,7 @@ void GameModel::launchGame(const QString &exePath) loadGamesFromDatabase(); emit gameLaunched(launchedId); qInfo() << "[VoidOne] Game launched:" << absolutePath << "PID:" << pid; + return true; } void GameModel::filter(const QString &searchText) From a4736fdb4f851412bd2f5e4afb97c74148cb6b2c Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:55:55 +0330 Subject: [PATCH 056/104] fix(ux): show the real result of game launch attempts --- src/ui/qml/Main.qml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 2f6e1dfd..557e391b 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -453,8 +453,10 @@ Window { itemIndex: index compact: true onLaunchRequested: function(path) { - gameModel.launchGame(path) - root.showNotification(qsTr("Launching game...")) + if (gameModel.launchGame(path)) + root.showNotification(qsTr("Launch request sent.")) + else + root.showNotification(qsTr("Could not start game. Check the executable path."), true) } onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, iconPath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) } @@ -585,8 +587,10 @@ Window { lastPlayed: model.lastPlayed itemIndex: index onLaunchRequested: function(path) { - gameModel.launchGame(path) - root.showNotification(qsTr("Launching game...")) + if (gameModel.launchGame(path)) + root.showNotification(qsTr("Launch request sent.")) + else + root.showNotification(qsTr("Could not start game. Check the executable path."), true) } onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, iconPath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) } @@ -713,8 +717,10 @@ Window { lastPlayed: model.lastPlayed itemIndex: index onLaunchRequested: function(path) { - gameModel.launchGame(path) - root.showNotification(qsTr("Launching game...")) + if (gameModel.launchGame(path)) + root.showNotification(qsTr("Launch request sent.")) + else + root.showNotification(qsTr("Could not start game. Check the executable path."), true) } onDetailsRequested: gameDetails.openFor(gameId, gameName, exePath, iconPath, platform, source, model.workingDir, model.launchArgs, playCount, favorite) } @@ -1158,6 +1164,9 @@ Window { GameDetailsDialog { id: gameDetails + onNotificationRequested: function(message, isError) { + root.showNotification(message, isError) + } } AddGameDialog { From 07b87fc230b7a213cade38dcaeeac90ce6ec0d99 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:55:58 +0330 Subject: [PATCH 057/104] fix(details): report actual game launch results --- src/ui/qml/GameDetailsDialog.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ui/qml/GameDetailsDialog.qml b/src/ui/qml/GameDetailsDialog.qml index 2cf8af78..5a935e8c 100644 --- a/src/ui/qml/GameDetailsDialog.qml +++ b/src/ui/qml/GameDetailsDialog.qml @@ -21,6 +21,7 @@ Dialog { property string launchArgs: "" property int playCount: 0 property bool favorite: false + signal notificationRequested(string message, bool isError) function iconUrl(path) { if (!path || path.length === 0) @@ -152,7 +153,14 @@ Dialog { Button { text: qsTr("▶ Play") Layout.preferredWidth: 130; implicitHeight: 40 - onClicked: { gameModel.launchGame(exePath); dialog.close() } + onClicked: { + if (gameModel.launchGame(exePath)) { + dialog.close() + notificationRequested(qsTr("Launch request sent."), false) + } else { + notificationRequested(qsTr("Could not start game. Check the executable path."), true) + } + } background: Rectangle { radius: 10; color: "#00d8ef" } contentItem: Text { text: parent.text; color: "#041015"; font.bold: true; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } } From 934139572ef76b317f597b2acf36e82bc6f25665 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:56:16 +0330 Subject: [PATCH 058/104] fix(add-game): avoid selecting stale scan results --- src/ui/qml/AddGameDialog.qml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index 5fba9bad..d2707b3f 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -406,11 +406,6 @@ Dialog { scanning = true scanAnimation.restart() scanTimer.restart() - if (candidates.length === 1) { - selectedExecutable = candidates[0] - selectedName = candidates[0].split("/").pop().split("\\").pop().replace(/\.exe$/i, "") - nameField.text = selectedName - } } } From 9181b2ea0ad4891270303b0e4743e8ea189184d0 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:56:19 +0330 Subject: [PATCH 059/104] fix(details): open install folders with native file URLs --- src/ui/qml/GameDetailsDialog.qml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/GameDetailsDialog.qml b/src/ui/qml/GameDetailsDialog.qml index 5a935e8c..d9923a4a 100644 --- a/src/ui/qml/GameDetailsDialog.qml +++ b/src/ui/qml/GameDetailsDialog.qml @@ -23,6 +23,16 @@ Dialog { property bool favorite: false signal notificationRequested(string message, bool isError) + function folderUrl(path) { + if (!path || path.length === 0) + return "" + if (path.startsWith("file://")) + return path + if (path.startsWith("/")) + return "file://" + path + return "file:///" + path.replace(/\\/g, "/") + } + function iconUrl(path) { if (!path || path.length === 0) return "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" @@ -104,7 +114,7 @@ Dialog { ColumnLayout { Layout.fillWidth: true spacing: 5 - Text { text: gameName; color: "#f0f6fa"; font.pixelSize: 25; font.bold: true; elide: Text.ElideRight } + Text { Layout.fillWidth: true; text: gameName; color: "#f0f6fa"; font.pixelSize: 25; font.bold: true; elide: Text.ElideRight } Text { Layout.fillWidth: true; text: platform + " • " + source; color: "#71869a"; font.pixelSize: 11; elide: Text.ElideRight } Text { text: playCount + " " + qsTr("launches"); color: "#4e6579"; font.pixelSize: 10 } } @@ -167,7 +177,7 @@ Dialog { Button { text: qsTr("Open Folder") implicitHeight: 40 - onClicked: Qt.openUrlExternally("file:///" + workingDir.replace(/\\/g, "/")) + onClicked: Qt.openUrlExternally(dialog.folderUrl(workingDir)) } Item { Layout.fillWidth: true } Button { @@ -208,7 +218,7 @@ Dialog { Button { Layout.alignment: Qt.AlignRight text: qsTr("Open Folder") - onClicked: Qt.openUrlExternally("file:///" + workingDir.replace(/\\/g, "/")) + onClicked: Qt.openUrlExternally(dialog.folderUrl(workingDir)) } } } From 0daf18fef0c27e9ab366327ac36aebcacb762083 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:56:40 +0330 Subject: [PATCH 060/104] redesign(details): unify tabs and secondary actions with VoidOne theme --- src/ui/qml/GameDetailsDialog.qml | 82 ++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/src/ui/qml/GameDetailsDialog.qml b/src/ui/qml/GameDetailsDialog.qml index d9923a4a..9d24c29d 100644 --- a/src/ui/qml/GameDetailsDialog.qml +++ b/src/ui/qml/GameDetailsDialog.qml @@ -140,9 +140,58 @@ Dialog { id: tabs Layout.fillWidth: true Layout.topMargin: 12 - TabButton { text: qsTr("Overview") } - TabButton { text: qsTr("Launch Options") } - TabButton { text: qsTr("Files") } + Layout.leftMargin: 16 + Layout.rightMargin: 16 + spacing: 6 + background: Rectangle { color: "#0b1118"; radius: 10 } + TabButton { + text: qsTr("Overview") + background: Rectangle { + radius: 8 + color: tabs.currentIndex === 0 ? "#00e5ff14" : "transparent" + border.color: tabs.currentIndex === 0 ? "#00e5ff35" : "transparent" + } + contentItem: Text { + text: parent.text + color: tabs.currentIndex === 0 ? "#00e5ff" : "#71869a" + font.pixelSize: 11 + font.bold: tabs.currentIndex === 0 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + TabButton { + text: qsTr("Launch Options") + background: Rectangle { + radius: 8 + color: tabs.currentIndex === 1 ? "#00e5ff14" : "transparent" + border.color: tabs.currentIndex === 1 ? "#00e5ff35" : "transparent" + } + contentItem: Text { + text: parent.text + color: tabs.currentIndex === 1 ? "#00e5ff" : "#71869a" + font.pixelSize: 11 + font.bold: tabs.currentIndex === 1 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + TabButton { + text: qsTr("Installation") + background: Rectangle { + radius: 8 + color: tabs.currentIndex === 2 ? "#00e5ff14" : "transparent" + border.color: tabs.currentIndex === 2 ? "#00e5ff35" : "transparent" + } + contentItem: Text { + text: parent.text + color: tabs.currentIndex === 2 ? "#00e5ff" : "#71869a" + font.pixelSize: 11 + font.bold: tabs.currentIndex === 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } } StackLayout { @@ -181,9 +230,21 @@ Dialog { } Item { Layout.fillWidth: true } Button { - text: qsTr("Hide") + text: qsTr("Hide from library") implicitHeight: 40 onClicked: { gameModel.hideGame(gameId, true); dialog.close() } + background: Rectangle { + radius: 10 + color: parent.hovered ? "#26151d" : "#0a1118" + border.color: parent.hovered ? "#6b3543" : "#26313e" + } + contentItem: Text { + text: parent.text + color: "#d7a4af" + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } } } @@ -201,8 +262,21 @@ Dialog { Item { Layout.fillHeight: true } Button { Layout.alignment: Qt.AlignRight + implicitHeight: 40 text: qsTr("Save Options") onClicked: { gameModel.updateLaunchOptions(gameId, argsField.text, dirField.text); dialog.close() } + background: Rectangle { + radius: 10 + color: parent.hovered ? "#20eaff" : "#00d8ef" + } + contentItem: Text { + text: parent.text + color: "#041015" + font.pixelSize: 11 + font.bold: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } } } From add1cbaf1fb0dc14c527c8a353e932141b3956d9 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:57:03 +0330 Subject: [PATCH 061/104] polish(details): theme launch fields and secondary actions --- src/ui/qml/GameDetailsDialog.qml | 54 ++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/ui/qml/GameDetailsDialog.qml b/src/ui/qml/GameDetailsDialog.qml index 9d24c29d..d0102e29 100644 --- a/src/ui/qml/GameDetailsDialog.qml +++ b/src/ui/qml/GameDetailsDialog.qml @@ -227,6 +227,18 @@ Dialog { text: qsTr("Open Folder") implicitHeight: 40 onClicked: Qt.openUrlExternally(dialog.folderUrl(workingDir)) + background: Rectangle { + radius: 10 + color: parent.hovered ? "#142331" : "#0a1118" + border.color: "#263a4b" + } + contentItem: Text { + text: parent.text + color: "#b9c8d5" + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } Item { Layout.fillWidth: true } Button { @@ -255,9 +267,34 @@ Dialog { anchors.fill: parent; anchors.margins: 24; spacing: 14 Text { text: qsTr("COMMAND"); color: "#4d6377"; font.pixelSize: 10; font.bold: true; font.letterSpacing: 1.4 } Text { text: qsTr("Arguments"); color: "#71869a"; font.pixelSize: 11 } - TextField { id: argsField; Layout.fillWidth: true; placeholderText: qsTr("e.g. -windowed -novid"); selectByMouse: true } + TextField { + id: argsField + Layout.fillWidth: true + implicitHeight: 40 + placeholderText: qsTr("e.g. -windowed -novid") + selectByMouse: true + color: "#eaf2f8" + placeholderTextColor: "#506277" + background: Rectangle { + radius: 9 + color: "#080d13" + border.color: argsField.activeFocus ? "#00e5ff66" : "#172635" + } + } Text { text: qsTr("Working directory"); color: "#71869a"; font.pixelSize: 11 } - TextField { id: dirField; Layout.fillWidth: true; selectByMouse: true } + TextField { + id: dirField + Layout.fillWidth: true + implicitHeight: 40 + selectByMouse: true + color: "#eaf2f8" + placeholderTextColor: "#506277" + background: Rectangle { + radius: 9 + color: "#080d13" + border.color: dirField.activeFocus ? "#00e5ff66" : "#172635" + } + } Text { Layout.fillWidth: true; text: qsTr("Use this for games that need a custom working directory or startup arguments."); color: "#4e6579"; font.pixelSize: 10; wrapMode: Text.WordWrap } Item { Layout.fillHeight: true } Button { @@ -291,8 +328,21 @@ Dialog { Item { Layout.fillHeight: true } Button { Layout.alignment: Qt.AlignRight + implicitHeight: 40 text: qsTr("Open Folder") onClicked: Qt.openUrlExternally(dialog.folderUrl(workingDir)) + background: Rectangle { + radius: 10 + color: parent.hovered ? "#142331" : "#0a1118" + border.color: "#263a4b" + } + contentItem: Text { + text: parent.text + color: "#b9c8d5" + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } } } From d7c9a0c1400ad9b1a6e66a73a7fa35dd58a2557f Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:57:17 +0330 Subject: [PATCH 062/104] feat(settings): remember sidebar and library view preferences --- src/ui/qml/Main.qml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 557e391b..7d9180da 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -9,6 +9,7 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls import QtQuick.Effects +import Qt.labs.settings Window { id: root @@ -22,10 +23,20 @@ Window { color: theme.background property string currentPage: "home" - property bool sidebarCompact: false - property string libraryFilter: "all" + property bool sidebarCompact: uiSettings.compactSidebar + property string libraryFilter: uiSettings.libraryFilter readonly property bool narrowLayout: width < 1100 + onSidebarCompactChanged: uiSettings.compactSidebar = sidebarCompact + onLibraryFilterChanged: uiSettings.libraryFilter = libraryFilter + + Settings { + id: uiSettings + category: "ui" + property bool compactSidebar: false + property string libraryFilter: "all" + } + QtObject { id: theme property color background: "#070a0f" From 5071a40350af4baad9d7bb0363bc68fa58688277 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:57:34 +0330 Subject: [PATCH 063/104] chore(ui): stop packaging fake unused music and telemetry widgets --- CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 96346bf9..4d989c76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,8 +169,6 @@ qt_add_qml_module(VoidOne src/ui/qml/SaveBackupView.qml src/ui/qml/SidebarButton.qml src/ui/qml/EnterpriseDialog.qml - src/ui/qml/MusicControllerWidget.qml - src/ui/qml/SystemTelemetryWidget.qml src/ui/qml/AddGameDialog.qml src/ui/qml/ActionButton.qml src/ui/qml/GameDetailsDialog.qml From 1b184f013f13cd4937668f31a5b5ed52eea91441 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:58:12 +0330 Subject: [PATCH 064/104] polish(add-game): style cancel action consistently --- src/ui/qml/AddGameDialog.qml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index d2707b3f..ed962b4f 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -354,7 +354,20 @@ Dialog { Button { text: qsTr("Cancel") + implicitHeight: 40 onClicked: dialog.close() + background: Rectangle { + radius: 10 + color: parent.hovered ? "#142331" : "#0a1118" + border.color: "#263a4b" + } + contentItem: Text { + text: parent.text + color: "#b9c8d5" + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } Button { From ee24d9679e4cb6a08406118194de4d37599e73a5 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:58:17 +0330 Subject: [PATCH 065/104] polish(shell): align local-first status with cyan brand --- src/ui/qml/Main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 7d9180da..cabd84c2 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -206,7 +206,7 @@ Window { width: 7 height: 7 radius: 4 - color: theme.success + color: theme.cyan } Text { text: qsTr("LOCAL-FIRST") From a24f44adf42b1ece862e8ac3e3f20c87728e10e5 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:59:07 +0330 Subject: [PATCH 066/104] fix(layout): overlay empty states correctly on game grids --- src/ui/qml/Main.qml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index cabd84c2..287d23e7 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -573,10 +573,13 @@ Window { } } - GridView { - id: gameGrid + Item { Layout.fillWidth: true Layout.fillHeight: true + + GridView { + id: gameGrid + anchors.fill: parent cellWidth: Math.max(240, Math.floor(width / Math.max(1, Math.floor(width / 285)))) cellHeight: 190 clip: true @@ -612,9 +615,9 @@ Window { } } - ColumnLayout { - anchors.centerIn: gameGrid - visible: gameGrid.count === 0 + ColumnLayout { + anchors.centerIn: parent + visible: gameGrid.count === 0 spacing: 10 Text { @@ -673,6 +676,7 @@ Window { verticalAlignment: Text.AlignVCenter } } + } } } } @@ -704,10 +708,13 @@ Window { Rectangle { Layout.fillWidth: true; height: 1; color: "#14202c" } - GridView { - id: activityGrid + Item { Layout.fillWidth: true Layout.fillHeight: true + + GridView { + id: activityGrid + anchors.fill: parent cellWidth: Math.max(240, Math.floor(width / Math.max(1, Math.floor(width / 285)))) cellHeight: 202 clip: true @@ -737,9 +744,9 @@ Window { } } - ColumnLayout { - anchors.centerIn: activityGrid - visible: activityGrid.count === 0 + ColumnLayout { + anchors.centerIn: parent + visible: activityGrid.count === 0 spacing: 10 Image { Layout.alignment: Qt.AlignHCenter @@ -770,6 +777,7 @@ Window { text: qsTr("Open library") onClicked: root.currentPage = "library" } + } } } } From 5921b366d55eae0dceea24b5d0015af964bd2075 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 12:59:27 +0330 Subject: [PATCH 067/104] fix(ui): guard initial page filter before search is ready --- src/ui/qml/Main.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 287d23e7..77c767ec 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -68,6 +68,8 @@ Window { } function applyPageFilter() { + if (typeof searchInput === "undefined" || searchInput === null) + return if (typeof gameModel === "undefined" || gameModel === null) return if (currentPage === "activity") From 8738eced39bc452383337896f5c8a1429d622885 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:00:01 +0330 Subject: [PATCH 068/104] feat(ux): add keyboard shortcuts for power users --- src/ui/qml/Main.qml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 77c767ec..b811ad7c 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -1183,6 +1183,25 @@ Window { } } + // Keyboard-first navigation for players who prefer to keep their hands on the keyboard. + Shortcut { + sequence: "Ctrl+K" + context: Qt.ApplicationShortcut + onActivated: { + searchInput.forceActiveFocus() + searchInput.selectAll() + } + } + Shortcut { sequence: "Ctrl+1"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "home" } + Shortcut { sequence: "Ctrl+2"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "library" } + Shortcut { sequence: "Ctrl+3"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "activity" } + Shortcut { sequence: "Ctrl+,"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "settings" } + Shortcut { + sequence: "Ctrl+Shift+A" + context: Qt.ApplicationShortcut + onActivated: addGameDialog.open() + } + GameDetailsDialog { id: gameDetails onNotificationRequested: function(message, isError) { From a251cd5ac00149a8a84187e8e4cee927318d19d0 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:00:10 +0330 Subject: [PATCH 069/104] fix(ux): use predictable numeric shortcut for settings --- src/ui/qml/Main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index b811ad7c..7f29bcb6 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -1195,7 +1195,7 @@ Window { Shortcut { sequence: "Ctrl+1"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "home" } Shortcut { sequence: "Ctrl+2"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "library" } Shortcut { sequence: "Ctrl+3"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "activity" } - Shortcut { sequence: "Ctrl+,"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "settings" } + Shortcut { sequence: "Ctrl+4"; context: Qt.ApplicationShortcut; onActivated: root.currentPage = "settings" } Shortcut { sequence: "Ctrl+Shift+A" context: Qt.ApplicationShortcut From af7635347827b188f0f9b6a76ffa5caea2df21bb Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:27:44 +0330 Subject: [PATCH 070/104] fix(beta.2): repair Qt resources and Steam game launching --- src/core/Database.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/Database.h b/src/core/Database.h index 19b4df73..5246cd3c 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -15,6 +15,7 @@ struct GameRecord { QString source; QString workingDir; QString launchArgs; + int steamAppId = 0; qint64 playSeconds = 0; int playCount = 0; qint64 lastPlayed = 0; From 2e3e798a5f5ffce4530920e05b1a22ed98edb66d Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:27:47 +0330 Subject: [PATCH 071/104] fix(beta.2): repair Qt resources and Steam game launching --- src/core/Database.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 68aacee4..e9136458 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -74,7 +74,8 @@ bool prepareSchema(const QSqlDatabase &db) "source TEXT NOT NULL DEFAULT 'Custom', " "working_dir TEXT, launch_args TEXT, " "play_seconds INTEGER NOT NULL DEFAULT 0, play_count INTEGER NOT NULL DEFAULT 0, " - "last_played INTEGER NOT NULL DEFAULT 0, favorite INTEGER NOT NULL DEFAULT 0, hidden INTEGER NOT NULL DEFAULT 0" + "last_played INTEGER NOT NULL DEFAULT 0, favorite INTEGER NOT NULL DEFAULT 0, hidden INTEGER NOT NULL DEFAULT 0, " + "steam_app_id INTEGER NOT NULL DEFAULT 0" ")")) { qCritical() << "[Database] Schema creation failed:" << schema.lastError().text(); return false; @@ -88,7 +89,8 @@ bool prepareSchema(const QSqlDatabase &db) "ALTER TABLE games ADD COLUMN play_count INTEGER NOT NULL DEFAULT 0", "ALTER TABLE games ADD COLUMN last_played INTEGER NOT NULL DEFAULT 0", "ALTER TABLE games ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0", - "ALTER TABLE games ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0" + "ALTER TABLE games ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE games ADD COLUMN steam_app_id INTEGER NOT NULL DEFAULT 0" }; for (const QString &migration : migrations) { QSqlQuery migrate(db); @@ -165,10 +167,10 @@ bool Database::addGame(const GameRecord &game) QSqlQuery query(db); query.prepare( - "INSERT INTO games (name, exe_path, icon_path, platform, source, working_dir, launch_args) " - "VALUES (:name, :exe_path, :icon_path, :platform, :source, :working_dir, :launch_args) " + "INSERT INTO games (name, exe_path, icon_path, platform, source, working_dir, launch_args, steam_app_id) " + "VALUES (:name, :exe_path, :icon_path, :platform, :source, :working_dir, :launch_args, :steam_app_id) " "ON CONFLICT(exe_path) DO UPDATE SET " - "name=excluded.name, icon_path=excluded.icon_path, platform=excluded.platform, source=excluded.source, working_dir=excluded.working_dir, launch_args=excluded.launch_args"); + "name=excluded.name, icon_path=excluded.icon_path, platform=excluded.platform, source=excluded.source, working_dir=excluded.working_dir, launch_args=excluded.launch_args, steam_app_id=excluded.steam_app_id"); query.bindValue(":name", game.name.trimmed()); query.bindValue(":exe_path", game.exePath.trimmed()); query.bindValue(":icon_path", game.iconPath.trimmed()); @@ -176,6 +178,7 @@ bool Database::addGame(const GameRecord &game) query.bindValue(":source", game.source.trimmed().isEmpty() ? QStringLiteral("Custom") : game.source.trimmed()); query.bindValue(":working_dir", game.workingDir.trimmed().isEmpty() ? QFileInfo(game.exePath).absolutePath() : game.workingDir.trimmed()); query.bindValue(":launch_args", game.launchArgs); + query.bindValue(":steam_app_id", game.steamAppId); if (!query.exec()) { qWarning() << "[Database] Insert/update failed:" << query.lastError().text(); @@ -202,10 +205,10 @@ bool Database::addGamesBatch(const QVector &games) QSqlQuery query(db); query.prepare( - "INSERT INTO games (name, exe_path, icon_path, platform, source, working_dir, launch_args) " - "VALUES (:name, :exe_path, :icon_path, :platform, :source, :working_dir, :launch_args) " + "INSERT INTO games (name, exe_path, icon_path, platform, source, working_dir, launch_args, steam_app_id) " + "VALUES (:name, :exe_path, :icon_path, :platform, :source, :working_dir, :launch_args, :steam_app_id) " "ON CONFLICT(exe_path) DO UPDATE SET " - "name=excluded.name, icon_path=excluded.icon_path, platform=excluded.platform, source=excluded.source, working_dir=excluded.working_dir, launch_args=excluded.launch_args"); + "name=excluded.name, icon_path=excluded.icon_path, platform=excluded.platform, source=excluded.source, working_dir=excluded.working_dir, launch_args=excluded.launch_args, steam_app_id=excluded.steam_app_id"); for (const auto &game : games) { if (game.name.trimmed().isEmpty() || game.exePath.trimmed().isEmpty()) { @@ -221,6 +224,7 @@ bool Database::addGamesBatch(const QVector &games) query.bindValue(":source", game.source.trimmed().isEmpty() ? QStringLiteral("Custom") : game.source.trimmed()); query.bindValue(":working_dir", game.workingDir.trimmed().isEmpty() ? QFileInfo(game.exePath).absolutePath() : game.workingDir.trimmed()); query.bindValue(":launch_args", game.launchArgs); + query.bindValue(":steam_app_id", game.steamAppId); if (!query.exec()) { qWarning() << "[Database] Batch insert/update failed:" << query.lastError().text(); db.rollback(); @@ -246,7 +250,7 @@ QVector Database::getAllGames() } QSqlQuery query(db); - if (!query.exec("SELECT id, name, exe_path, icon_path, platform, source, working_dir, launch_args, play_seconds, play_count, last_played, favorite, hidden FROM games ORDER BY favorite DESC, name COLLATE NOCASE ASC")) { + if (!query.exec("SELECT id, name, exe_path, icon_path, platform, source, working_dir, launch_args, play_seconds, play_count, last_played, favorite, hidden, steam_app_id FROM games ORDER BY favorite DESC, name COLLATE NOCASE ASC")) { qWarning() << "[Database] Query failed:" << query.lastError().text(); return games; } @@ -266,6 +270,7 @@ QVector Database::getAllGames() rec.lastPlayed = query.value(10).toLongLong(); rec.favorite = query.value(11).toInt() != 0; rec.hidden = query.value(12).toInt() != 0; + rec.steamAppId = query.value(13).toInt(); games.append(rec); } return games; From 0bb9c29e6f3e7f9182c736cbe0880d86136ff0cb Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:27:49 +0330 Subject: [PATCH 072/104] fix(beta.2): repair Qt resources and Steam game launching --- src/core/SteamScanner.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/SteamScanner.cpp b/src/core/SteamScanner.cpp index fe03838d..b9dd0cfe 100644 --- a/src/core/SteamScanner.cpp +++ b/src/core/SteamScanner.cpp @@ -262,6 +262,10 @@ void SteamScannerWorker::doScan() continue; GameRecord rec; + const QRegularExpression appIdRx(QStringLiteral("appmanifest_(\\d+)\\.acf"), + QRegularExpression::CaseInsensitiveOption); + const auto appIdMatch = appIdRx.match(fileInfo.fileName()); + rec.steamAppId = appIdMatch.hasMatch() ? appIdMatch.captured(1).toInt() : 0; rec.name = nameMatch.captured(1).trimmed(); const QString gameDir = QDir(steamappsPath).filePath("common/" + dirMatch.captured(1)); From e990a4df25c798d9bd6fa6ea9b3c88c71900163f Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:27:52 +0330 Subject: [PATCH 073/104] fix(beta.2): repair Qt resources and Steam game launching --- src/core/GameModel.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index fdb66781..4a134632 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -396,15 +398,28 @@ bool GameModel::launchGame(const QString &exePath) } int launchedId = -1; + int steamAppId = 0; + QString gameSource; for (const auto &game : std::as_const(m_allGames)) { if (QFileInfo(game.exePath).absoluteFilePath() == absolutePath) { launchedId = game.id; + steamAppId = game.steamAppId; + gameSource = game.source; break; } } qint64 pid = -1; - if (!QProcess::startDetached(absolutePath, arguments, workingDirectory, &pid)) { + bool launchAccepted = false; + if (steamAppId > 0 && gameSource.compare(QStringLiteral("Steam"), Qt::CaseInsensitive) == 0) { + const QUrl steamUrl(QStringLiteral("steam://rungameid/%1").arg(steamAppId)); + launchAccepted = QDesktopServices::openUrl(steamUrl); + if (!launchAccepted) + qWarning() << "[VoidOne] Steam protocol launch failed; trying executable directly:" << steamUrl; + } + if (!launchAccepted) + launchAccepted = QProcess::startDetached(absolutePath, arguments, workingDirectory, &pid); + if (!launchAccepted) { qWarning() << "[VoidOne] Failed to launch game:" << absolutePath; return false; } @@ -479,7 +494,13 @@ void GameModel::setFavorite(int id, bool favorite) QSqlQuery q(QSqlDatabase::database(QStringLiteral("voidone-main"), false)); q.prepare("UPDATE games SET favorite=:favorite WHERE id=:id"); q.bindValue(":favorite", favorite ? 1 : 0); q.bindValue(":id", id); - if (q.exec()) loadGamesFromDatabase(); + if (q.exec()) { + if (q.numRowsAffected() == 0) + qWarning() << "[VoidOne] Favorite update matched no game row for id:" << id; + loadGamesFromDatabase(); + } else { + qWarning() << "[VoidOne] Favorite update failed:" << q.lastError().text(); + } } void GameModel::hideGame(int id, bool hidden) From 4eca656e6448fb2628bf7ed381e36b643a6effcf Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:27:55 +0330 Subject: [PATCH 074/104] fix(beta.2): repair Qt resources and Steam game launching --- src/ui/qml/Main.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 7f29bcb6..64e51691 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -229,7 +229,7 @@ Window { anchors.centerIn: parent width: 22 height: 22 - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit smooth: true asynchronous: true @@ -403,7 +403,7 @@ Window { anchors.verticalCenter: parent.verticalCenter width: 150 height: 150 - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit opacity: 0.12 asynchronous: true @@ -752,7 +752,7 @@ Window { spacing: 10 Image { Layout.alignment: Qt.AlignHCenter - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" width: 44 height: 44 fillMode: Image.PreserveAspectFit @@ -853,7 +853,7 @@ Window { anchors.centerIn: parent width: 20 height: 20 - source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/settings.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/settings.svg" fillMode: Image.PreserveAspectFit } } @@ -1012,7 +1012,7 @@ Window { anchors.centerIn: parent width: 21 height: 21 - source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/library.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/library.svg" fillMode: Image.PreserveAspectFit } } @@ -1102,7 +1102,7 @@ Window { anchors.centerIn: parent width: 25 height: 25 - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit } } From 1162ccb652de99cc6c19cdec3d4f168d1362feba Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:27:58 +0330 Subject: [PATCH 075/104] fix(beta.2): repair Qt resources and Steam game launching --- src/ui/qml/GameCard.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/qml/GameCard.qml b/src/ui/qml/GameCard.qml index b9e17c98..432ec982 100644 --- a/src/ui/qml/GameCard.qml +++ b/src/ui/qml/GameCard.qml @@ -22,7 +22,7 @@ Rectangle { function iconUrl(path) { if (!path || path.length === 0) - return "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + return "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" if (path.startsWith("qrc:/") || path.startsWith("file:/") || path.startsWith("http://") || path.startsWith("https://")) return path if (path.startsWith(":/")) @@ -87,7 +87,7 @@ Rectangle { anchors.centerIn: parent width: parent.width * 0.62 height: width - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit asynchronous: true visible: gameIcon.status !== Image.Ready From 4b7e7c96eb13c2a780a2ec6ef1007e308baf111c Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:28:01 +0330 Subject: [PATCH 076/104] fix(beta.2): repair Qt resources and Steam game launching --- src/ui/qml/Sidebar.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/qml/Sidebar.qml b/src/ui/qml/Sidebar.qml index f6a1050f..22eb6b0a 100644 --- a/src/ui/qml/Sidebar.qml +++ b/src/ui/qml/Sidebar.qml @@ -34,7 +34,7 @@ Rectangle { anchors.centerIn: parent width: 27 height: 27 - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit smooth: true } @@ -76,7 +76,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/home.svg" + iconSource: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/home.svg" voLabel: qsTr("Home") voSelected: root.currentPage === "home" onClicked: root.pageChanged("home") @@ -85,7 +85,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/library.svg" + iconSource: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/library.svg" voLabel: qsTr("Library") voSelected: root.currentPage === "library" onClicked: root.pageChanged("library") @@ -94,7 +94,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/activity.svg" + iconSource: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/activity.svg" voLabel: qsTr("Activity") voSelected: root.currentPage === "activity" onClicked: root.pageChanged("activity") @@ -114,7 +114,7 @@ Rectangle { SidebarButton { Layout.fillWidth: true compact: root.compact - iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/settings.svg" + iconSource: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/settings.svg" voLabel: qsTr("Settings") voSelected: root.currentPage === "settings" onClicked: root.pageChanged("settings") @@ -144,7 +144,7 @@ Rectangle { anchors.centerIn: parent width: 22 height: 22 - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit asynchronous: true } From df41bb87774c3d259c26266c162776ac3e10204d Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:28:04 +0330 Subject: [PATCH 077/104] fix(beta.2): repair Qt resources and Steam game launching --- src/ui/qml/AddGameDialog.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index ed962b4f..73f64bdf 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -61,7 +61,7 @@ Dialog { anchors.centerIn: parent width: 23 height: 23 - source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/add.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/add.svg" fillMode: Image.PreserveAspectFit } } @@ -137,7 +137,7 @@ Dialog { Image { width: 18 height: 18 - source: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/folder.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/folder.svg" fillMode: Image.PreserveAspectFit opacity: 0.8 } @@ -161,7 +161,7 @@ Dialog { Layout.fillWidth: true title: qsTr("Select .exe") subtitle: qsTr("Add one game directly") - iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/file.svg" + iconSource: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/file.svg" onClicked: exeDialog.open() } @@ -169,7 +169,7 @@ Dialog { Layout.fillWidth: true title: qsTr("Select folder") subtitle: qsTr("Let VoidOne find executables") - iconSource: "qrc:/qt/qml/VoidOne.App/assets/ui/icons/folder.svg" + iconSource: "qrc:/qt/qml/VoidOne/App/assets/ui/icons/folder.svg" onClicked: folderDialog.open() } } From a388c0b9c094e0e60f5ce100650072d1a7d37810 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:28:07 +0330 Subject: [PATCH 078/104] fix(beta.2): repair Qt resources and Steam game launching --- src/ui/qml/GameDetailsDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/qml/GameDetailsDialog.qml b/src/ui/qml/GameDetailsDialog.qml index d0102e29..3e47fbdd 100644 --- a/src/ui/qml/GameDetailsDialog.qml +++ b/src/ui/qml/GameDetailsDialog.qml @@ -35,7 +35,7 @@ Dialog { function iconUrl(path) { if (!path || path.length === 0) - return "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + return "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" if (path.startsWith("qrc:/") || path.startsWith("file:/")) return path if (path.startsWith(":/")) @@ -104,7 +104,7 @@ Dialog { anchors.centerIn: parent width: 40 height: 40 - source: "qrc:/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg" + source: "qrc:/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg" fillMode: Image.PreserveAspectFit asynchronous: true visible: detailsGameIcon.status !== Image.Ready From 6a7695c41a596f9a22c1a5165138e37306066372 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:28:10 +0330 Subject: [PATCH 079/104] fix(beta.2): repair Qt resources and Steam game launching --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index f86e711f..d1793f6a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -257,7 +257,7 @@ int main(int argc, char *argv[]) qInfo() << "[Bootstrap] QML2_IMPORT_PATH:" << qEnvironmentVariable("QML2_IMPORT_PATH"); QGuiApplication app(argc, argv); - app.setWindowIcon(QIcon(QStringLiteral(":/qt/qml/VoidOne.App/assets/branding/voidone-mark.svg"))); + app.setWindowIcon(QIcon(QStringLiteral(":/qt/qml/VoidOne/App/assets/branding/voidone-mark.svg"))); // AppDataLocation must exist before anything such as QLockFile uses it. const QString appDataDir = appDataDirectory(); From 16a719ee36f23e84f6af6c6cb0df2cd75ef03506 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:29:06 +0330 Subject: [PATCH 080/104] fix(beta.2): update favorites without resetting the clicked card --- src/core/GameModel.cpp | 49 +++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index 4a134632..b6ecdb0a 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -491,18 +491,47 @@ void GameModel::rebuildVisibleGames() void GameModel::setFavorite(int id, bool favorite) { - QSqlQuery q(QSqlDatabase::database(QStringLiteral("voidone-main"), false)); - q.prepare("UPDATE games SET favorite=:favorite WHERE id=:id"); - q.bindValue(":favorite", favorite ? 1 : 0); q.bindValue(":id", id); - if (q.exec()) { - if (q.numRowsAffected() == 0) - qWarning() << "[VoidOne] Favorite update matched no game row for id:" << id; - loadGamesFromDatabase(); - } else { - qWarning() << "[VoidOne] Favorite update failed:" << q.lastError().text(); + const QSqlDatabase db = QSqlDatabase::database(QStringLiteral("voidone-main"), false); + if (!db.isValid() || !db.isOpen()) { + qWarning() << "[VoidOne] Favorite update requested while the database is unavailable."; + return; } -} + QSqlQuery query(db); + query.prepare("UPDATE games SET favorite = :favorite WHERE id = :id"); + query.bindValue(":favorite", favorite ? 1 : 0); + query.bindValue(":id", id); + if (!query.exec()) { + qWarning() << "[VoidOne] Favorite update failed:" << query.lastError().text(); + return; + } + if (query.numRowsAffected() == 0) { + qWarning() << "[VoidOne] Favorite update matched no game row for id:" << id; + return; + } + + for (GameRecord &game : m_allGames) { + if (game.id == id) { + game.favorite = favorite; + break; + } + } + + // Update the visible role in place instead of resetting the whole model. + // This keeps the clicked card stable and avoids losing its interaction mid-click. + if (m_filterMode == QStringLiteral("favorites") && !favorite) { + rebuildVisibleGames(); + return; + } + + for (int row = 0; row < m_games.size(); ++row) { + if (m_games[row].id == id) { + m_games[row].favorite = favorite; + emit dataChanged(index(row, 0), index(row, 0), {FavoriteRole}); + return; + } + } +} void GameModel::hideGame(int id, bool hidden) { QSqlQuery q(QSqlDatabase::database(QStringLiteral("voidone-main"), false)); From 8153e3da59735ddd0a3816b13ef2d90333ea8938 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:31:38 +0330 Subject: [PATCH 081/104] feat(beta.2): expose safe game executable path editing --- src/core/GameModel.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/GameModel.h b/src/core/GameModel.h index fcb7c0ec..beea8ed7 100644 --- a/src/core/GameModel.h +++ b/src/core/GameModel.h @@ -41,6 +41,7 @@ class GameModel : public QAbstractListModel Q_INVOKABLE void setFavorite(int id, bool favorite); Q_INVOKABLE void hideGame(int id, bool hidden); Q_INVOKABLE void updateLaunchOptions(int id, const QString &args, const QString &workingDir); + Q_INVOKABLE bool updateGamePath(int id, const QString &newExecutablePath); Q_INVOKABLE QVariantMap getGameDetails(int id) const; Q_INVOKABLE void filter(const QString &searchText); Q_INVOKABLE void filterGames(const QString &searchText, const QString &mode); From 1fb4dd309bdb6234ac8aa1396213cc522bb7b658 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:31:42 +0330 Subject: [PATCH 082/104] feat(beta.2): support editing game executable paths --- src/core/GameModel.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index b6ecdb0a..231d8b96 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -540,6 +540,37 @@ void GameModel::hideGame(int id, bool hidden) if (q.exec()) loadGamesFromDatabase(); } +bool GameModel::updateGamePath(int id, const QString &newExecutablePath) +{ + const QString path = newExecutablePath.trimmed(); + const QFileInfo info(path); + if (!info.isFile() || info.isSymLink()) { + qWarning() << "[VoidOne] Refusing invalid replacement executable:" << path; + return false; + } +#if defined(Q_OS_WIN) + if (!info.fileName().endsWith(QStringLiteral(".exe"), Qt::CaseInsensitive)) + return false; +#else + if (!info.isExecutable()) + return false; +#endif + const QString absolutePath = info.absoluteFilePath(); + const QSqlDatabase db = QSqlDatabase::database(QStringLiteral("voidone-main"), false); + if (!db.isValid() || !db.isOpen()) + return false; + QSqlQuery query(db); + query.prepare("UPDATE games SET exe_path = :path, working_dir = :dir, icon_path = '' WHERE id = :id"); + query.bindValue(":path", absolutePath); + query.bindValue(":dir", info.absolutePath()); + query.bindValue(":id", id); + if (!query.exec() || query.numRowsAffected() == 0) { + qWarning() << "[VoidOne] Failed to update game executable path:" << query.lastError().text(); + return false; + } + loadGamesFromDatabase(); + return true; +} void GameModel::updateLaunchOptions(int id, const QString &args, const QString &workingDir) { const QSqlDatabase db = QSqlDatabase::database(QStringLiteral("voidone-main"), false); From 5e7e45096972b5dbc595a3a8dec5ed0df7914f6e Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:31:57 +0330 Subject: [PATCH 083/104] feat(beta.2): add safe remove-by-id for path management --- src/core/GameModel.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/GameModel.h b/src/core/GameModel.h index beea8ed7..318a4b88 100644 --- a/src/core/GameModel.h +++ b/src/core/GameModel.h @@ -37,6 +37,7 @@ class GameModel : public QAbstractListModel Q_INVOKABLE bool addNewGame(const QString &name, const QString &exePath, const QString &iconPath); Q_INVOKABLE QStringList suggestExecutables(const QString &folderPath) const; Q_INVOKABLE bool deleteGame(int id, int index); + Q_INVOKABLE bool removeGameById(int id); Q_INVOKABLE bool launchGame(const QString &exePath); Q_INVOKABLE void setFavorite(int id, bool favorite); Q_INVOKABLE void hideGame(int id, bool hidden); From e4f5c7aae2ba69817bb985acc6b88f26bec73334 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:32:00 +0330 Subject: [PATCH 084/104] feat(beta.2): add safe remove-by-id for path management --- src/core/GameModel.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index 231d8b96..a3266a3c 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -356,6 +356,13 @@ bool GameModel::deleteGame(int id, int index) return true; } +bool GameModel::removeGameById(int id) +{ + if (id <= 0 || !Database::removeGame(id)) + return false; + loadGamesFromDatabase(); + return true; +} bool GameModel::launchGame(const QString &exePath) { const QString trimmedPath = exePath.trimmed(); From 18fe9f76dd676f0c1290469377d68f9630c24ace Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:32:42 +0330 Subject: [PATCH 085/104] feat(beta.2): add visible app menu and game path manager --- src/ui/qml/Main.qml | 161 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 64e51691..ccca08bb 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -10,6 +10,7 @@ import QtQuick.Layouts import QtQuick.Controls import QtQuick.Effects import Qt.labs.settings +import QtQuick.Dialogs as NativeDialogs Window { id: root @@ -61,6 +62,14 @@ Window { return fallback } + function displayGameName(name, path) { + if (name && String(name).trim().length > 0) + return String(name).trim() + var normalized = String(path || "").replace(/\\/g, "/") + var leaf = normalized.substring(normalized.lastIndexOf("/") + 1) + return leaf.replace(/\.exe$/i, "") || qsTr("Unknown game") + } + function showNotification(message, isError) { toast.text = message toastAccent.color = isError ? "#ff5f70" : theme.cyan @@ -219,6 +228,25 @@ Window { } } + Button { + text: qsTr("☰ Menu") + implicitHeight: 36 + onClicked: appMenu.open() + background: Rectangle { + radius: 9 + color: parent.hovered ? "#142331" : "#0b1118" + border.color: "#263746" + } + contentItem: Text { + text: parent.text + color: theme.text + font.pixelSize: 11 + font.bold: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + Rectangle { width: 36 height: 36 @@ -455,7 +483,7 @@ Window { width: GridView.view.cellWidth - 10 height: 168 gameId: model.id - gameName: model.name + gameName: root.displayGameName(model.name, model.exePath) exePath: model.exePath iconPath: model.iconPath platform: model.platform @@ -593,7 +621,7 @@ Window { width: gameGrid.cellWidth - 12 height: 174 gameId: model.id - gameName: model.name + gameName: root.displayGameName(model.name, model.exePath) exePath: model.exePath iconPath: model.iconPath platform: model.platform @@ -727,7 +755,7 @@ Window { width: activityGrid.cellWidth - 12 height: 190 gameId: model.id - gameName: model.name + gameName: root.displayGameName(model.name, model.exePath) exePath: model.exePath iconPath: model.iconPath platform: model.platform @@ -1058,6 +1086,113 @@ Window { } } + Text { + text: qsTr("GAME PATHS") + color: theme.dim + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.5 + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: pathsColumn.implicitHeight + 28 + radius: 14 + color: theme.surface + border.color: "#1b2b3a" + + ColumnLayout { + id: pathsColumn + anchors.fill: parent + anchors.margins: 14 + spacing: 12 + + RowLayout { + Layout.fillWidth: true + spacing: 12 + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { text: qsTr("Installed game locations"); color: theme.text; font.pixelSize: 13; font.bold: true } + Text { text: qsTr("View, change, or remove each game executable path. Removing a game only removes its library entry; it does not uninstall the game."); color: theme.muted; font.pixelSize: 11; wrapMode: Text.WordWrap; Layout.fillWidth: true } + } + Button { + text: qsTr("+ Add Game") + onClicked: addGameDialog.open() + background: Rectangle { radius: 9; color: parent.hovered ? "#20eaff" : theme.cyan } + contentItem: Text { text: parent.text; color: "#041015"; font.pixelSize: 11; font.bold: true; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } + } + } + + Rectangle { Layout.fillWidth: true; height: 1; color: "#1b2b3a" } + + Repeater { + model: typeof gameModel !== "undefined" ? gameModel : null + delegate: Rectangle { + id: gamePathRow + required property int index + property int gameId: model.id + property string gameTitle: root.displayGameName(model.name, model.exePath) + property string executablePath: model.exePath || "" + Layout.fillWidth: true + implicitHeight: pathRowLayout.implicitHeight + 20 + radius: 10 + color: "#090f16" + border.color: "#192a39" + + RowLayout { + id: pathRowLayout + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + Text { text: gamePathRow.gameTitle; color: theme.text; font.pixelSize: 12; font.bold: true; elide: Text.ElideRight; Layout.fillWidth: true } + Text { text: gamePathRow.executablePath; color: theme.muted; font.pixelSize: 10; elide: Text.ElideMiddle; Layout.fillWidth: true } + } + Button { + text: qsTr("Change") + onClicked: pathFileDialog.open() + background: Rectangle { radius: 8; color: parent.hovered ? "#142b37" : "#0b1720"; border.color: theme.cyanLine } + contentItem: Text { text: parent.text; color: theme.cyan; font.pixelSize: 10; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } + } + Button { + text: qsTr("Remove") + onClicked: { + if (gameModel.removeGameById(gamePathRow.gameId)) root.showNotification(qsTr("Game removed from your library.")) + else root.showNotification(qsTr("Could not remove this library entry."), true) + } + background: Rectangle { radius: 8; color: parent.hovered ? "#351820" : "#1b1016"; border.color: "#6b2935" } + contentItem: Text { text: parent.text; color: "#ff8795"; font.pixelSize: 10; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } + } + } + + NativeDialogs.FileDialog { + id: pathFileDialog + title: qsTr("Choose the new executable for ") + gamePathRow.gameTitle + fileMode: NativeDialogs.FileDialog.OpenFile + nameFilters: [qsTr("Windows executables (*.exe)"), qsTr("All files (*)")] + onAccepted: { + var newPath = selectedFile.toLocalFile() + if (gameModel.updateGamePath(gamePathRow.gameId, newPath)) root.showNotification(qsTr("Game path updated.")) + else root.showNotification(qsTr("Could not update the path. Choose a valid executable that is not already in the library."), true) + } + } + } + } + + Text { + visible: !gameModel || gameModel.count === 0 + text: qsTr("No games in the library yet. Use Add Game to select an executable or scan an installation folder.") + color: theme.muted + font.pixelSize: 11 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + } + Text { text: qsTr("SAVE MANAGEMENT") color: theme.dim @@ -1183,6 +1318,26 @@ Window { } } + Menu { + id: appMenu + title: qsTr("VoidOne Menu") + Action { text: qsTr("Home"); onTriggered: root.currentPage = "home" } + Action { text: qsTr("Library"); onTriggered: root.currentPage = "library" } + Action { text: qsTr("Activity"); onTriggered: root.currentPage = "activity" } + MenuSeparator {} + Action { text: qsTr("Add Game…"); onTriggered: addGameDialog.open() } + Action { + text: qsTr("Scan Steam Library") + onTriggered: { + steamScanner.startAsyncScan() + root.showNotification(qsTr("Scanning installed Steam games...")) + } + } + MenuSeparator {} + Action { text: qsTr("Manage Game Paths…"); onTriggered: root.currentPage = "settings" } + Action { text: qsTr("Settings"); onTriggered: root.currentPage = "settings" } + } + // Keyboard-first navigation for players who prefer to keep their hands on the keyboard. Shortcut { sequence: "Ctrl+K" From 610d14e9c2b6e5b22eda0acca093b32c3de1a412 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:33:03 +0330 Subject: [PATCH 086/104] fix(beta.2): make folder selection and executable naming reliable --- src/ui/qml/AddGameDialog.qml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index 73f64bdf..04f888a1 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -117,8 +117,14 @@ Dialog { selectedExecutable = path selectedName = path.split("/").pop().split("\\").pop().replace(/\.exe$/i, "") nameField.text = selectedName + selectedFolder = "" + candidates = [] } else { selectedFolder = path + selectedExecutable = "" + selectedName = "" + nameField.text = "" + candidates = [] scanning = true scanAnimation.restart() scanTimer.restart() @@ -292,8 +298,7 @@ Dialog { onClicked: { selectedExecutable = modelData selectedName = modelData.split("/").pop().split("\\").pop().replace(/\.exe$/i, "") - if (!nameField.text.length) - nameField.text = selectedName + nameField.text = selectedName } } @@ -430,7 +435,7 @@ Dialog { candidates = gameModel.suggestExecutables(selectedFolder) scanning = false if (candidates.length === 0) - notificationRequested(qsTr("No likely game executable was found in that folder."), true) + notificationRequested(qsTr("No likely game executable was found in: ") + selectedFolder, true) else if (candidates.length === 1) { selectedExecutable = candidates[0] selectedName = candidates[0].split("/").pop().split("\\").pop().replace(/\.exe$/i, "") @@ -439,5 +444,6 @@ Dialog { } } + onOpened: reset() Component.onCompleted: reset() } From af1dc1cd545a6e91e9f34a4644edcbd056a77c44 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:33:07 +0330 Subject: [PATCH 087/104] fix(beta.2): scan deeper for game executables in selected folders --- src/core/GameModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/GameModel.cpp b/src/core/GameModel.cpp index a3266a3c..6072570f 100644 --- a/src/core/GameModel.cpp +++ b/src/core/GameModel.cpp @@ -313,7 +313,7 @@ QStringList GameModel::suggestExecutables(const QString &folderPath) const candidates.append({score, file.size(), file.absoluteFilePath()}); } - if (depth >= 2) + if (depth >= 4) return; const QFileInfoList dirs = dir.entryInfoList( From 2ec06603ffa7179e8040a663ab6eb85f76d23884 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:33:18 +0330 Subject: [PATCH 088/104] fix(beta.2): validate folder picker results before scanning --- src/ui/qml/AddGameDialog.qml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index 04f888a1..ff6ada62 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -421,9 +421,18 @@ Dialog { onAccepted: { selectedFolder = folderDialog.selectedFolder.toLocalFile() - scanning = true - scanAnimation.restart() - scanTimer.restart() + selectedExecutable = "" + selectedName = "" + nameField.text = "" + candidates = [] + if (!selectedFolder.length) { + scanning = false + notificationRequested(qsTr("The selected folder path could not be read. Try choosing the folder again."), true) + } else { + scanning = true + scanAnimation.restart() + scanTimer.restart() + } } } From 9d01d76264d278abbc5cec6265681054d78b749b Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:33:33 +0330 Subject: [PATCH 089/104] fix(beta.2): use the qualified Qt Quick file dialog enum --- src/ui/qml/AddGameDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/qml/AddGameDialog.qml b/src/ui/qml/AddGameDialog.qml index ff6ada62..cf56273f 100644 --- a/src/ui/qml/AddGameDialog.qml +++ b/src/ui/qml/AddGameDialog.qml @@ -404,7 +404,7 @@ Dialog { NativeDialogs.FileDialog { id: exeDialog title: qsTr("Choose a game executable") - fileMode: FileDialog.OpenFile + fileMode: NativeDialogs.FileDialog.OpenFile nameFilters: ["Windows executable (*.exe)", "All files (*)"] onAccepted: { From 73c06ce52ea317a45f56d58c04b6310eff0593e3 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:36:22 +0330 Subject: [PATCH 090/104] feat(ui): add cyan gamepad SVG icon --- assets/ui/icons/gamepad.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/gamepad.svg diff --git a/assets/ui/icons/gamepad.svg b/assets/ui/icons/gamepad.svg new file mode 100644 index 00000000..89e5741c --- /dev/null +++ b/assets/ui/icons/gamepad.svg @@ -0,0 +1 @@ + \ No newline at end of file From 89df9d81da298e313b5e71896006cbffa9a67f74 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:36:27 +0330 Subject: [PATCH 091/104] feat(ui): add cyan play SVG icon --- assets/ui/icons/play.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/play.svg diff --git a/assets/ui/icons/play.svg b/assets/ui/icons/play.svg new file mode 100644 index 00000000..996aee97 --- /dev/null +++ b/assets/ui/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file From d4c189853b5e0cc22c0beb1ea0dafb438d28724f Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:36:33 +0330 Subject: [PATCH 092/104] feat(ui): add cyan clock SVG icon --- assets/ui/icons/clock.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/clock.svg diff --git a/assets/ui/icons/clock.svg b/assets/ui/icons/clock.svg new file mode 100644 index 00000000..7379478b --- /dev/null +++ b/assets/ui/icons/clock.svg @@ -0,0 +1 @@ + \ No newline at end of file From 31f87faa221f707beb5cc53a5f12636bd78ef3d3 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:36:38 +0330 Subject: [PATCH 093/104] feat(ui): add cyan shield SVG icon --- assets/ui/icons/shield.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/shield.svg diff --git a/assets/ui/icons/shield.svg b/assets/ui/icons/shield.svg new file mode 100644 index 00000000..9a18eb6e --- /dev/null +++ b/assets/ui/icons/shield.svg @@ -0,0 +1 @@ + \ No newline at end of file From 0685e3287101c5898876029acc53146a174df4ea Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:36:47 +0330 Subject: [PATCH 094/104] feat(ui): add restrained animated ambient backdrop --- src/ui/qml/AmbientBackdrop.qml | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/ui/qml/AmbientBackdrop.qml diff --git a/src/ui/qml/AmbientBackdrop.qml b/src/ui/qml/AmbientBackdrop.qml new file mode 100644 index 00000000..da928bfe --- /dev/null +++ b/src/ui/qml/AmbientBackdrop.qml @@ -0,0 +1,63 @@ +import QtQuick + +// Lightweight ambient treatment: no timers, particles, shaders, or per-frame work +// when the window is hidden. Motion stays subtle so the launcher remains responsive. +Item { + id: root + anchors.fill: parent + clip: true + + Rectangle { + id: cyanBloom + width: Math.min(root.width * 0.62, 720) + height: width + radius: width / 2 + x: root.width - width * 0.56 + y: -height * 0.62 + color: "#00e5ff" + opacity: 0.025 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.045; duration: 4200; easing.type: Easing.InOutSine } + NumberAnimation { to: 0.025; duration: 4200; easing.type: Easing.InOutSine } + } + } + + Rectangle { + width: Math.min(root.width * 0.44, 520) + height: width + radius: width / 2 + x: -width * 0.55 + y: root.height - height * 0.45 + color: "#2478ff" + opacity: 0.018 + } + + // Thin, low-contrast technical lines add depth without competing with content. + Repeater { + model: Math.ceil(root.width / 72) + Rectangle { + required property int index + x: index * 72 + y: 0 + width: 1 + height: root.height + color: "#193042" + opacity: 0.12 + } + } + + Repeater { + model: Math.ceil(root.height / 72) + Rectangle { + required property int index + x: 0 + y: index * 72 + width: root.width + height: 1 + color: "#193042" + opacity: 0.09 + } + } +} \ No newline at end of file From 9f4c9e264ebe308a6a0d0acd9db2a040747e9414 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:36:53 +0330 Subject: [PATCH 095/104] feat(ui): add reusable animated SVG icon component --- src/ui/qml/AnimatedIcon.qml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/ui/qml/AnimatedIcon.qml diff --git a/src/ui/qml/AnimatedIcon.qml b/src/ui/qml/AnimatedIcon.qml new file mode 100644 index 00000000..c3838604 --- /dev/null +++ b/src/ui/qml/AnimatedIcon.qml @@ -0,0 +1,31 @@ +import QtQuick + +// Reusable SVG icon with a restrained hover response; keeps the source asset crisp. +Item { + id: root + property url source + property color tint: "#00e5ff" + property real iconOpacity: 0.92 + property bool hovered: false + implicitWidth: 24 + implicitHeight: 24 + + Image { + id: iconImage + anchors.centerIn: parent + width: parent.width + height: parent.height + source: root.source + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + opacity: root.iconOpacity + scale: root.hovered ? 1.08 : 1.0 + Behavior on scale { + NumberAnimation { duration: 140; easing.type: Easing.OutCubic } + } + Behavior on opacity { + NumberAnimation { duration: 140; easing.type: Easing.OutCubic } + } + } +} \ No newline at end of file From 1fabee5f219eb4ee625ebeeb47bc058826d91ab8 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:36:59 +0330 Subject: [PATCH 096/104] build(ui): package animated backdrop and SVG icon assets --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d989c76..3211f12f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,8 @@ qt_add_qml_module(VoidOne src/ui/qml/AddGameDialog.qml src/ui/qml/ActionButton.qml src/ui/qml/GameDetailsDialog.qml + src/ui/qml/AmbientBackdrop.qml + src/ui/qml/AnimatedIcon.qml RESOURCES assets/branding/voidone-mark.svg assets/ui/icons/home.svg @@ -181,6 +183,10 @@ qt_add_qml_module(VoidOne assets/ui/icons/add.svg assets/ui/icons/folder.svg assets/ui/icons/file.svg + assets/ui/icons/gamepad.svg + assets/ui/icons/play.svg + assets/ui/icons/clock.svg + assets/ui/icons/shield.svg ) target_link_libraries(VoidOne PRIVATE voidone_core Qt6::Gui Qt6::Quick Qt6::Svg) if(WIN32) From 789d1bbf153216fe334afc6f8d4447f147ca5823 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:37:15 +0330 Subject: [PATCH 097/104] feat(ui): integrate subtle animated ambient backdrop --- src/ui/qml/Main.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index ccca08bb..53194036 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -95,6 +95,8 @@ Window { anchors.fill: parent color: theme.background + AmbientBackdrop { anchors.fill: parent } + // Ambient platform lighting Rectangle { width: 520 From be1bada9f1f231cb35f1e3f93ac405fe5c9d621d Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:37:22 +0330 Subject: [PATCH 098/104] feat(ui): animate sidebar SVG icons on hover --- src/ui/qml/SidebarButton.qml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ui/qml/SidebarButton.qml b/src/ui/qml/SidebarButton.qml index be448c16..93256fef 100644 --- a/src/ui/qml/SidebarButton.qml +++ b/src/ui/qml/SidebarButton.qml @@ -26,17 +26,15 @@ Button { } contentItem: Item { - Image { + AnimatedIcon { id: navIcon x: button.compact ? (parent.width - width) / 2 : 17 anchors.verticalCenter: parent.verticalCenter width: 20 height: 20 source: button.iconSource - opacity: button.voSelected ? 1 : 0.68 - fillMode: Image.PreserveAspectFit - asynchronous: true - smooth: true + iconOpacity: button.voSelected ? 1 : (button.hovered ? 0.9 : 0.68) + hovered: button.hovered } Text { From b8029715af7f0305a9d4a73cc3402f096afd7329 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:37:28 +0330 Subject: [PATCH 099/104] feat(ui): add cyan favorites SVG icon --- assets/ui/icons/heart.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/heart.svg diff --git a/assets/ui/icons/heart.svg b/assets/ui/icons/heart.svg new file mode 100644 index 00000000..3e20e8be --- /dev/null +++ b/assets/ui/icons/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file From dcbc3c463f2e56c655cdf49338f3a2e94938345e Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:37:33 +0330 Subject: [PATCH 100/104] feat(ui): add cyan scan SVG icon --- assets/ui/icons/scan.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/scan.svg diff --git a/assets/ui/icons/scan.svg b/assets/ui/icons/scan.svg new file mode 100644 index 00000000..f93b68a8 --- /dev/null +++ b/assets/ui/icons/scan.svg @@ -0,0 +1 @@ + \ No newline at end of file From 350032119175e1975a870705c67c7610f05d5857 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:37:38 +0330 Subject: [PATCH 101/104] feat(ui): add cyan settings sliders SVG icon --- assets/ui/icons/sliders.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/sliders.svg diff --git a/assets/ui/icons/sliders.svg b/assets/ui/icons/sliders.svg new file mode 100644 index 00000000..2c4c1532 --- /dev/null +++ b/assets/ui/icons/sliders.svg @@ -0,0 +1 @@ + \ No newline at end of file From 1516343ed7742103453edea5c152924157abe323 Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:37:43 +0330 Subject: [PATCH 102/104] feat(ui): add cyan accent spark SVG icon --- assets/ui/icons/spark.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/ui/icons/spark.svg diff --git a/assets/ui/icons/spark.svg b/assets/ui/icons/spark.svg new file mode 100644 index 00000000..9b9dcb46 --- /dev/null +++ b/assets/ui/icons/spark.svg @@ -0,0 +1 @@ + \ No newline at end of file From d358df1e8ef47c755e56a9053af0648ff7234d2a Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:37:49 +0330 Subject: [PATCH 103/104] build(ui): package additional favorites, scan and settings SVGs --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3211f12f..783edaa8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -187,6 +187,10 @@ qt_add_qml_module(VoidOne assets/ui/icons/play.svg assets/ui/icons/clock.svg assets/ui/icons/shield.svg + assets/ui/icons/heart.svg + assets/ui/icons/scan.svg + assets/ui/icons/sliders.svg + assets/ui/icons/spark.svg ) target_link_libraries(VoidOne PRIVATE voidone_core Qt6::Gui Qt6::Quick Qt6::Svg) if(WIN32) From f7b7062c1d171297a3f8980e97cc381cd2cd787a Mon Sep 17 00:00:00 2001 From: mohammadmk47 Date: Sat, 10 Oct 2026 13:38:00 +0330 Subject: [PATCH 104/104] feat(ui): add smooth hover motion to game artwork --- src/ui/qml/GameCard.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/qml/GameCard.qml b/src/ui/qml/GameCard.qml index 432ec982..6a2fc7d0 100644 --- a/src/ui/qml/GameCard.qml +++ b/src/ui/qml/GameCard.qml @@ -80,6 +80,8 @@ Rectangle { fillMode: Image.PreserveAspectFit asynchronous: true smooth: true + scale: cardHover.hovered ? 1.06 : 1.0 + Behavior on scale { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } visible: status === Image.Ready }