diff --git a/CMakeLists.txt b/CMakeLists.txt index a4642bd..6c4ebaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(Minecraft_launcher VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Quick) +find_package(Qt6 REQUIRED COMPONENTS Quick Core) qt_standard_project_setup(REQUIRES 6.8) @@ -14,6 +14,9 @@ qt_add_executable(appMinecraft_launcher qt_add_qml_module(appMinecraft_launcher URI Minecraft_launcher + SOURCES + launcherbackend.h + launcherbackend.cpp QML_FILES Main.qml RESOURCES images/Folder/Folder_Active.svg images/Folder/Folder_Idle.svg images/Folder/Folder_Pressed.svg images/Options/Options_active.svg images/Options/Options_Idle.svg images/Options/Options_Pressed.svg images/Play_Button/Play_Active.svg images/Play_Button/Play_Idle.svg images/Play_Button/Play_pressed.svg images/Profile_Box/Asset_23.svg images/Profile_Box/Asset_24.svg images/Profile_Box/Kishka_Profile_Active.svg images/Profile_Box/Kishka_Profile_Idle.svg images/Profile_Box/Kishka_Profile_open.svg images/Scroll/Scroll_Lever.svg images/Scroll/Scroll_palka.svg images/Version_box/Kishka_Profile_Open.svg images/Version_box/Kishka_Ver_Active.svg images/Version_box/Kishka_Ver_idle.svg images/GovuztTW8AAHqBf.jpeg images/photo_2025-12-16_15-04-17.jpg images/photo_2025-12-21_02-30-09.jpg @@ -32,7 +35,7 @@ set_target_properties(appMinecraft_launcher PROPERTIES ) target_link_libraries(appMinecraft_launcher - PRIVATE Qt6::Quick + PRIVATE Qt6::Quick Qt6::Core ) include(GNUInstallDirs) diff --git a/Main.qml b/Main.qml index 4c81fa2..f81f410 100644 --- a/Main.qml +++ b/Main.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Layouts 2.15 import QtQuick.Controls 2.15 +import Minecraft_launcher Window { id: window @@ -8,62 +9,79 @@ Window { height: 720 visible: true flags: Qt.Window - title: qsTr("Hello World") + title: qsTr("Minecraft Launcher") + // ── Backend ──────────────────────────────────────────────────────────── + LauncherBackend { + id: backend + onLaunched: (profileName, versionName, serverUrl) => { + console.log("Запуск: профиль=" + profileName + + " версия=" + versionName + + " сервер=" + serverUrl) + } + onLaunchError: (message) => { + errorLabel.text = message + errorTimer.restart() + } + } + + // ── Background ───────────────────────────────────────────────────────── Image { id: image opacity: 0.296 - visible: true anchors.fill: parent - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter source: "images/GovuztTW8AAHqBf.jpeg" - focus: false - activeFocusOnTab: false - state: "" - layer.enabled: false - antialiasing: false - clip: false - transformOrigin: Item.Center - scale: 1 - mirror: false - mipmap: false - cache: true - autoTransform: true - asynchronous: false - sourceSize.height: 0 - sourceSize.width: 0 fillMode: Image.Stretch } + // ── Error toast ──────────────────────────────────────────────────────── + Rectangle { + visible: errorLabel.text !== "" + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 40 + width: errorLabel.implicitWidth + 32 + height: 40 + radius: 8 + color: "#cc3333" + + Text { + id: errorLabel + anchors.centerIn: parent + color: "#ffffff" + font.pixelSize: 14 + } + + Timer { + id: errorTimer + interval: 3000 + onTriggered: errorLabel.text = "" + } + } + + // ── Play button ──────────────────────────────────────────────────────── Button { id: button - x: 440 - y: 105 width: 335 height: 170 - hoverEnabled: false anchors.centerIn: parent - opacity: 1 + hoverEnabled: false + background: Image { id: buttonImage source: "images/Play_Button/Play_Active.svg" } - Connections { - target: button - function onPressed() { buttonImage.source = "images/Play_Button/Play_pressed.svg" } - } - - Connections { - target: button - function onReleased() { buttonImage.source = "images/Play_Button/Play_Active.svg" } - } + onPressed: buttonImage.source = "images/Play_Button/Play_pressed.svg" + onReleased: buttonImage.source = "images/Play_Button/Play_Active.svg" + onClicked: backend.launchGame(profileBox.currentIndex, versionBox.currentIndex) } + // ── Profile ComboBox ─────────────────────────────────────────────────── ComboBox { id: profileBox - width: 117 + y: 451 + width: 146 height: 42 anchors.left: button.right anchors.bottom: button.top @@ -72,235 +90,541 @@ Window { editable: false leftPadding: -30 - model: ["First","Second","Third"] + model: backend.profileNames - - delegate: ItemDelegate { - id: delegate - - required property var model - required property int index - - width: profileBox.width - contentItem: Text { - text: delegate.model[profileBox.textRole] - color: "#ffffff" - font: profileBox.font - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - - - } - highlighted: profileBox.highlightedIndex === index - background: Rectangle{ - color: delegate.highlighted ? "#91B315" : "#232323" - radius: 5 - - } - } indicator: Image { - id: dropDownArrow + width: 10; height: 10 visible: true - horizontalAlignment: Image.AlignRight - anchors.leftMargin: 10 anchors.left: parent.left + anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter - source: profileBox.down ? "images/Profile_Box/Asset_23.svg" : "images/Profile_Box/Asset_24.svg" - transformOrigin: Item.Center - sourceSize.height: 10 - sourceSize.width: 10 + source: profileBox.down ? "images/Profile_Box/Asset_23.svg" + : "images/Profile_Box/Asset_24.svg" + rotation: 180 + sourceSize.width: 10; sourceSize.height: 10 fillMode: Image.PreserveAspectFit autoTransform: true - width: 10 - height: 10 } - /*indicator: Canvas { - id: canvas - y: profileBox.topPadding + (profileBox.availableHeight - height) / 2 - width: 12 - height: 8 - anchors.left: parent.left - anchors.leftMargin: 117 - contextType: "2d" - - onPaint: { - context.reset(); - context.moveTo(0, 0); - context.lineTo(width, 0); - context.lineTo(width / 2, height); - context.closePath(); - context.fillStyle = profileBox.pressed ? "#ffffff" : "#232323"; - context.fill(); - } - }*/ - - Connections { - target: profileBox - function onActivated() { canvas.requestPaint(); } - } - - Connections { - target: profileBox - function onActivated() { dropDownArrow.visible = true } - } contentItem: Text { - leftPadding: 0 + x: 26 + width: 114 rightPadding: profileBox.indicator.width + profileBox.spacing - text: profileBox.displayText font: profileBox.font - color: profileBox.pressed ? "#ffffff" : "#ffffff" + color: "#ffffff" verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter - + elide: Text.ElideRight } background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - border.color: profileBox.pressed ? "#ffffff" : "#232323" - border.width: profileBox.visualFocus ? 2 : 1 + implicitWidth: 120; implicitHeight: 40 color: profileBox.down ? "#91B315" : "#232323" radius: 6 + border.color: profileBox.pressed ? "#ffffff" : "#232323" + border.width: profileBox.visualFocus ? 2 : 1 } popup: Popup { y: profileBox.height - 1 width: profileBox.width - height: Math.min(contentItem.implicitHeight, profileBox.Window.height - topMargin - bottomMargin) - padding: 1 + padding: 0 + // Высота = кнопка "+" (32) + элементы списка, не более 200 + height: 32 + Math.min(profileItemList.contentHeight, 200) - contentItem: ListView { - rotation: 0 - clip: true - implicitHeight: contentHeight - model: profileBox.popup.visible ? profileBox.delegateModel : null - currentIndex: profileBox.highlightedIndex + contentItem: Item { + anchors.fill: parent + // ── "+" button — всегда сверху ────────────────────────── + Rectangle { + id: profileAddBtnBg + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 32 + color: profileAddArea.containsPress ? "#2d2d2d" : "transparent" - - ScrollIndicator.vertical: ScrollIndicator { - parent: ListView.parent - anchors.top: ListView.top - anchors.left: ListView.right - anchors.bottom: ListView.bottom - - /*background: Rectangle{ - opacity: 0.5 + Text { + anchors.centerIn: parent + text: "+ Добавить профиль" color: "#91B315" - }*/ + font.pixelSize: 12 + } + MouseArea { + id: profileAddArea + anchors.fill: parent + onClicked: { + profileBox.popup.close() + addProfileDialog.open() + } + } + } + + // ── Добавленные профили — всегда ниже кнопки ─────────── + ListView { + id: profileItemList + anchors.top: profileAddBtnBg.bottom + anchors.left: parent.left + anchors.right: parent.right + height: Math.min(contentHeight, 200) + clip: true + model: backend.profileNames + + delegate: ItemDelegate { + id: profileItem + required property string modelData + required property int index + width: profileItemList.width + height: 40 + contentItem: Text { + text: profileItem.modelData + color: "#ffffff" + font: profileBox.font + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + leftPadding: 8 + } + background: Rectangle { + color: (profileBox.currentIndex === profileItem.index || profileItem.hovered) + ? "#91B315" : "#232323" + radius: 5 + } + onClicked: { + profileBox.currentIndex = profileItem.index + profileBox.popup.close() + } + } + + ScrollIndicator.vertical: ScrollIndicator { + parent: profileItemList.parent + anchors.top: profileItemList.top + anchors.left: profileItemList.right + anchors.bottom: profileItemList.bottom + } } } background: Rectangle { - border.color: "#232323" color: "#232323" radius: 6 + border.color: "#232323" } } - } + // ── Version ComboBox ─────────────────────────────────────────────────── ComboBox { id: versionBox - width: 210 + x: 624 + y: 451 + width: 183 height: 42 anchors.right: button.right anchors.bottom: button.top anchors.bottomMargin: -218 + editable: false + leftPadding: -30 + + model: backend.versionNames + + indicator: Image { + width: 10; height: 10 + visible: true + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + source: versionBox.down ? "images/Profile_Box/Asset_23.svg" + : "images/Profile_Box/Asset_24.svg" + rotation: 180 + sourceSize.width: 10; sourceSize.height: 10 + fillMode: Image.PreserveAspectFit + autoTransform: true + } + + contentItem: Text { + x: 26 + width: 150 + leftPadding: 56 + rightPadding: versionBox.indicator.width + versionBox.spacing + text: versionBox.displayText + font: versionBox.font + color: "#ffffff" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + + background: Rectangle { + implicitWidth: 120; implicitHeight: 40 + color: versionBox.down ? "#91B315" : "#232323" + radius: 6 + border.color: versionBox.pressed ? "#ffffff" : "#232323" + border.width: versionBox.visualFocus ? 2 : 1 + } + popup: Popup { y: versionBox.height - 1 width: versionBox.width - height: Math.min(contentItem.implicitHeight, versionBox.Window.height - topMargin - bottomMargin) - padding: 1 - contentItem: ListView { - rotation: 0 - model: versionBox.popup.visible ? versionBox.delegateModel : null - implicitHeight: contentHeight - currentIndex: versionBox.highlightedIndex - clip: true - ScrollIndicator.vertical: ScrollIndicator { - anchors.left: ListView.right - anchors.top: ListView.top - anchors.bottom: ListView.bottom - parent: ListView.parent + padding: 0 + height: 32 + Math.min(versionItemList.contentHeight, 200) + + contentItem: Item { + anchors.fill: parent + + // ── "+" button — всегда сверху ────────────────────────── + Rectangle { + id: versionAddBtnBg + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 32 + color: versionAddArea.containsPress ? "#2d2d2d" : "transparent" + + Text { + anchors.centerIn: parent + text: "+ Добавить версию" + color: "#91B315" + font.pixelSize: 12 + } + + MouseArea { + id: versionAddArea + anchors.fill: parent + onClicked: { + versionBox.popup.close() + addVersionDialog.open() + } + } + } + + // ── Добавленные версии — всегда ниже кнопки ──────────── + ListView { + id: versionItemList + anchors.top: versionAddBtnBg.bottom + anchors.left: parent.left + anchors.right: parent.right + height: Math.min(contentHeight, 200) + clip: true + model: backend.versionNames + + delegate: ItemDelegate { + id: versionItem + required property string modelData + required property int index + width: versionItemList.width + height: 40 + contentItem: Text { + text: versionItem.modelData + color: "#ffffff" + font: versionBox.font + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + leftPadding: 8 + } + background: Rectangle { + color: (versionBox.currentIndex === versionItem.index || versionItem.hovered) + ? "#91B315" : "#232323" + radius: 5 + } + onClicked: { + versionBox.currentIndex = versionItem.index + versionBox.popup.close() + } + } + + ScrollIndicator.vertical: ScrollIndicator { + parent: versionItemList.parent + anchors.top: versionItemList.top + anchors.left: versionItemList.right + anchors.bottom: versionItemList.bottom + } } } + background: Rectangle { color: "#232323" radius: 6 border.color: "#232323" } } - model: ["Version 1","Version 2","Version 3"] - leftPadding: -30 - indicator: Image { - id: dropDownArrow1 - width: 10 - height: 10 - visible: true - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 10 - horizontalAlignment: Image.AlignRight - source: versionBox.down ? "images/Profile_Box/Asset_23.svg" : "images/Profile_Box/Asset_24.svg" - transformOrigin: Item.Center - sourceSize.width: 10 - sourceSize.height: 10 - fillMode: Image.PreserveAspectFit - autoTransform: true - } - editable: false - delegate: ItemDelegate { - id: delegate1 - width: versionBox.width - required property var model - required property int index - highlighted: versionBox.highlightedIndex === index - contentItem: Text { - color: "#ffffff" - text: delegate1.model[versionBox.textRole] - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - font: versionBox.font - } - background: Rectangle { - color: delegate1.highlighted ? "#91B315" : "#232323" - radius: 5 - } - } - Connections { - target: versionBox - function onActivated() { canvas.requestPaint(); } + } + + // ── Add Profile Dialog ───────────────────────────────────────────────── + // Bug fix: header/footer must be Item (not Rectangle) with explicit + // implicitHeight so Dialog correctly computes its own total height. + // Rectangle.implicitHeight defaults to 0 regardless of height:. + Dialog { + id: addProfileDialog + modal: true + width: 320 + x: (window.width - width) / 2 + y: (window.height - height) / 2 + padding: 0 + + background: Rectangle { + color: "#1e1e1e" + radius: 10 + border.color: "#91B315" + border.width: 1 } - Connections { - target: versionBox - function onActivated() { dropDownArrow1.visible = true } + header: Item { + implicitHeight: 52 // tells Dialog how tall the header is + Text { + anchors.centerIn: parent + text: "Новый профиль" + color: "#ffffff" + font.pixelSize: 17 + font.bold: true + } + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: 1 + color: "#333333" + } } - contentItem: Text { - color: versionBox.pressed ? "#ffffff" : "#ffffff" - text: versionBox.displayText - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - rightPadding: versionBox.indicator.width + versionBox.spacing - leftPadding: 56 - font: versionBox.font + + contentItem: Column { + spacing: 12 + topPadding: 20 + bottomPadding: 20 + + TextField { + id: pfName + x: 20 + width: parent.width - 40 + placeholderText: "Имя профиля" + color: "#ffffff" + placeholderTextColor: "#666666" + background: Rectangle { + color: "#2a2a2a" + radius: 6 + border.color: pfName.activeFocus ? "#91B315" : "#444444" + border.width: 1 + } + } + + TextField { + id: pfLogin + x: 20 + width: parent.width - 40 + placeholderText: "Логин" + color: "#ffffff" + placeholderTextColor: "#666666" + background: Rectangle { + color: "#2a2a2a" + radius: 6 + border.color: pfLogin.activeFocus ? "#91B315" : "#444444" + border.width: 1 + } + } + + TextField { + id: pfPassword + x: 20 + width: parent.width - 40 + placeholderText: "Пароль" + echoMode: TextInput.Password + color: "#ffffff" + placeholderTextColor: "#666666" + background: Rectangle { + color: "#2a2a2a" + radius: 6 + border.color: pfPassword.activeFocus ? "#91B315" : "#444444" + border.width: 1 + } + } } - background: Rectangle { - color: versionBox.down ? "#91B315" : "#232323" - radius: 6 - border.color: versionBox.pressed ? "#ffffff" : "#232323" - border.width: versionBox.visualFocus ? 2 : 1 - implicitWidth: 120 - implicitHeight: 40 + + footer: Item { + implicitHeight: 60 // tells Dialog how tall the footer is + Rectangle { + anchors.top: parent.top + width: parent.width + height: 1 + color: "#333333" + } + Row { + anchors.centerIn: parent + spacing: 12 + + Button { + text: "Отмена" + width: 110; height: 36 + contentItem: Text { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + background: Rectangle { + color: parent.pressed ? "#444444" : "#333333" + radius: 6 + } + onClicked: addProfileDialog.reject() + } + + Button { + text: "Добавить" + width: 110; height: 36 + contentItem: Text { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + background: Rectangle { + color: parent.pressed ? "#6a8510" : "#91B315" + radius: 6 + } + onClicked: addProfileDialog.accept() + } + } + } + + onAccepted: { + const name = pfName.text.trim() + if (name !== "") { + backend.addProfile(name, pfLogin.text.trim(), pfPassword.text) + profileBox.currentIndex = backend.profileNames.length - 1 + } + pfName.text = ""; pfLogin.text = ""; pfPassword.text = "" + } + onRejected: { + pfName.text = ""; pfLogin.text = ""; pfPassword.text = "" } } + // ── Add Version Dialog ───────────────────────────────────────────────── + Dialog { + id: addVersionDialog + modal: true + width: 320 + x: (window.width - width) / 2 + y: (window.height - height) / 2 + padding: 0 + + background: Rectangle { + color: "#1e1e1e" + radius: 10 + border.color: "#91B315" + border.width: 1 + } + + header: Item { + implicitHeight: 52 + Text { + anchors.centerIn: parent + text: "Новая версия" + color: "#ffffff" + font.pixelSize: 17 + font.bold: true + } + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: 1 + color: "#333333" + } + } + + contentItem: Column { + spacing: 12 + topPadding: 20 + bottomPadding: 20 + + TextField { + id: verName + x: 20 + width: parent.width - 40 + placeholderText: "Название версии" + color: "#ffffff" + placeholderTextColor: "#666666" + background: Rectangle { + color: "#2a2a2a" + radius: 6 + border.color: verName.activeFocus ? "#91B315" : "#444444" + border.width: 1 + } + } + + TextField { + id: verServer + x: 20 + width: parent.width - 40 + placeholderText: "URL сервера загрузки" + color: "#ffffff" + placeholderTextColor: "#666666" + background: Rectangle { + color: "#2a2a2a" + radius: 6 + border.color: verServer.activeFocus ? "#91B315" : "#444444" + border.width: 1 + } + } + } + + footer: Item { + implicitHeight: 60 + Rectangle { + anchors.top: parent.top + width: parent.width + height: 1 + color: "#333333" + } + Row { + anchors.centerIn: parent + spacing: 12 + + Button { + text: "Отмена" + width: 110; height: 36 + contentItem: Text { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + background: Rectangle { + color: parent.pressed ? "#444444" : "#333333" + radius: 6 + } + onClicked: addVersionDialog.reject() + } + + Button { + text: "Добавить" + width: 110; height: 36 + contentItem: Text { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + background: Rectangle { + color: parent.pressed ? "#6a8510" : "#91B315" + radius: 6 + } + onClicked: addVersionDialog.accept() + } + } + } + + onAccepted: { + const name = verName.text.trim() + if (name !== "") { + backend.addVersion(name, verServer.text.trim()) + versionBox.currentIndex = backend.versionNames.length - 1 + } + verName.text = ""; verServer.text = "" + } + onRejected: { + verName.text = ""; verServer.text = "" + } + } } diff --git a/launcherbackend.cpp b/launcherbackend.cpp new file mode 100644 index 0000000..def6a30 --- /dev/null +++ b/launcherbackend.cpp @@ -0,0 +1,114 @@ +#include "launcherbackend.h" + +#include +#include +#include +#include +#include +#include +#include + +LauncherBackend::LauncherBackend(QObject *parent) + : QObject(parent) + , m_dataDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)) +{ + QDir().mkpath(m_dataDir); + loadData(); +} + +QStringList LauncherBackend::profileNames() const +{ + QStringList names; + for (const auto &p : m_profiles) + names << p.name; + return names; +} + +QStringList LauncherBackend::versionNames() const +{ + QStringList names; + for (const auto &v : m_versions) + names << v.name; + return names; +} + +void LauncherBackend::addProfile(const QString &name, const QString &login, const QString &password) +{ + m_profiles.append({m_nextProfileId++, name, login, password}); + saveProfiles(); + emit profilesChanged(); +} + +void LauncherBackend::addVersion(const QString &name, const QString &serverUrl) +{ + m_versions.append({m_nextVersionId++, name, serverUrl}); + saveVersions(); + emit versionsChanged(); +} + +void LauncherBackend::launchGame(int profileIndex, int versionIndex) +{ + if (profileIndex < 0 || profileIndex >= m_profiles.size()) { + emit launchError("Выберите профиль перед запуском"); + return; + } + if (versionIndex < 0 || versionIndex >= m_versions.size()) { + emit launchError("Выберите версию перед запуском"); + return; + } + + const auto &profile = m_profiles[profileIndex]; + const auto &version = m_versions[versionIndex]; + emit launched(profile.name, version.name, version.serverUrl); +} + +void LauncherBackend::loadData() +{ + auto loadFile = [](const QString &path, auto handler) { + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) + return; + for (const auto &val : QJsonDocument::fromJson(f.readAll()).array()) + handler(val.toObject()); + }; + + loadFile(m_dataDir + "/profiles.json", [this](const QJsonObject &o) { + int id = o["id"].toInt(); + m_profiles.append({id, o["name"].toString(), o["login"].toString(), o["password"].toString()}); + if (id >= m_nextProfileId) + m_nextProfileId = id + 1; + }); + std::sort(m_profiles.begin(), m_profiles.end(), + [](const Profile &a, const Profile &b) { return a.id < b.id; }); + + loadFile(m_dataDir + "/versions.json", [this](const QJsonObject &o) { + int id = o["id"].toInt(); + m_versions.append({id, o["name"].toString(), o["serverUrl"].toString()}); + if (id >= m_nextVersionId) + m_nextVersionId = id + 1; + }); + std::sort(m_versions.begin(), m_versions.end(), + [](const Version &a, const Version &b) { return a.id < b.id; }); +} + +void LauncherBackend::saveProfiles() +{ + QJsonArray arr; + for (const auto &p : m_profiles) + arr.append(QJsonObject{{"id", p.id}, {"name", p.name}, {"login", p.login}, {"password", p.password}}); + + QFile f(m_dataDir + "/profiles.json"); + if (f.open(QIODevice::WriteOnly)) + f.write(QJsonDocument(arr).toJson()); +} + +void LauncherBackend::saveVersions() +{ + QJsonArray arr; + for (const auto &v : m_versions) + arr.append(QJsonObject{{"id", v.id}, {"name", v.name}, {"serverUrl", v.serverUrl}}); + + QFile f(m_dataDir + "/versions.json"); + if (f.open(QIODevice::WriteOnly)) + f.write(QJsonDocument(arr).toJson()); +} diff --git a/launcherbackend.h b/launcherbackend.h new file mode 100644 index 0000000..b8ad4e6 --- /dev/null +++ b/launcherbackend.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +class LauncherBackend : public QObject +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QStringList profileNames READ profileNames NOTIFY profilesChanged) + Q_PROPERTY(QStringList versionNames READ versionNames NOTIFY versionsChanged) + +public: + explicit LauncherBackend(QObject *parent = nullptr); + + QStringList profileNames() const; + QStringList versionNames() const; + + Q_INVOKABLE void addProfile(const QString &name, const QString &login, const QString &password); + Q_INVOKABLE void addVersion(const QString &name, const QString &serverUrl); + Q_INVOKABLE void launchGame(int profileIndex, int versionIndex); + +signals: + void profilesChanged(); + void versionsChanged(); + void launched(const QString &profileName, const QString &versionName, const QString &serverUrl); + void launchError(const QString &message); + +private: + struct Profile { int id; QString name, login, password; }; + struct Version { int id; QString name, serverUrl; }; + + void loadData(); + void saveProfiles(); + void saveVersions(); + + QList m_profiles; + QList m_versions; + QString m_dataDir; + int m_nextProfileId = 1; + int m_nextVersionId = 1; +};