added profile and version adding through comboBox
This commit is contained in:
@@ -4,7 +4,7 @@ project(Minecraft_launcher VERSION 0.1 LANGUAGES CXX)
|
|||||||
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
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)
|
qt_standard_project_setup(REQUIRES 6.8)
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ qt_add_executable(appMinecraft_launcher
|
|||||||
|
|
||||||
qt_add_qml_module(appMinecraft_launcher
|
qt_add_qml_module(appMinecraft_launcher
|
||||||
URI Minecraft_launcher
|
URI Minecraft_launcher
|
||||||
|
SOURCES
|
||||||
|
launcherbackend.h
|
||||||
|
launcherbackend.cpp
|
||||||
QML_FILES
|
QML_FILES
|
||||||
Main.qml
|
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
|
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
|
target_link_libraries(appMinecraft_launcher
|
||||||
PRIVATE Qt6::Quick
|
PRIVATE Qt6::Quick Qt6::Core
|
||||||
)
|
)
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|||||||
714
Main.qml
714
Main.qml
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts 2.15
|
import QtQuick.Layouts 2.15
|
||||||
import QtQuick.Controls 2.15
|
import QtQuick.Controls 2.15
|
||||||
|
import Minecraft_launcher
|
||||||
|
|
||||||
Window {
|
Window {
|
||||||
id: window
|
id: window
|
||||||
@@ -8,62 +9,79 @@ Window {
|
|||||||
height: 720
|
height: 720
|
||||||
visible: true
|
visible: true
|
||||||
flags: Qt.Window
|
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 {
|
Image {
|
||||||
id: image
|
id: image
|
||||||
opacity: 0.296
|
opacity: 0.296
|
||||||
visible: true
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
horizontalAlignment: Image.AlignHCenter
|
|
||||||
verticalAlignment: Image.AlignVCenter
|
|
||||||
source: "images/GovuztTW8AAHqBf.jpeg"
|
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
|
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 {
|
Button {
|
||||||
id: button
|
id: button
|
||||||
x: 440
|
|
||||||
y: 105
|
|
||||||
width: 335
|
width: 335
|
||||||
height: 170
|
height: 170
|
||||||
hoverEnabled: false
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
opacity: 1
|
hoverEnabled: false
|
||||||
|
|
||||||
background: Image {
|
background: Image {
|
||||||
id: buttonImage
|
id: buttonImage
|
||||||
source: "images/Play_Button/Play_Active.svg"
|
source: "images/Play_Button/Play_Active.svg"
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
onPressed: buttonImage.source = "images/Play_Button/Play_pressed.svg"
|
||||||
target: button
|
onReleased: buttonImage.source = "images/Play_Button/Play_Active.svg"
|
||||||
function onPressed() { buttonImage.source = "images/Play_Button/Play_pressed.svg" }
|
onClicked: backend.launchGame(profileBox.currentIndex, versionBox.currentIndex)
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: button
|
|
||||||
function onReleased() { buttonImage.source = "images/Play_Button/Play_Active.svg" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Profile ComboBox ───────────────────────────────────────────────────
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: profileBox
|
id: profileBox
|
||||||
width: 117
|
y: 451
|
||||||
|
width: 146
|
||||||
height: 42
|
height: 42
|
||||||
anchors.left: button.right
|
anchors.left: button.right
|
||||||
anchors.bottom: button.top
|
anchors.bottom: button.top
|
||||||
@@ -72,235 +90,541 @@ Window {
|
|||||||
editable: false
|
editable: false
|
||||||
leftPadding: -30
|
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 {
|
indicator: Image {
|
||||||
id: dropDownArrow
|
width: 10; height: 10
|
||||||
visible: true
|
visible: true
|
||||||
horizontalAlignment: Image.AlignRight
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
source: profileBox.down ? "images/Profile_Box/Asset_23.svg" : "images/Profile_Box/Asset_24.svg"
|
source: profileBox.down ? "images/Profile_Box/Asset_23.svg"
|
||||||
transformOrigin: Item.Center
|
: "images/Profile_Box/Asset_24.svg"
|
||||||
sourceSize.height: 10
|
rotation: 180
|
||||||
sourceSize.width: 10
|
sourceSize.width: 10; sourceSize.height: 10
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
autoTransform: true
|
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 {
|
contentItem: Text {
|
||||||
leftPadding: 0
|
x: 26
|
||||||
|
width: 114
|
||||||
rightPadding: profileBox.indicator.width + profileBox.spacing
|
rightPadding: profileBox.indicator.width + profileBox.spacing
|
||||||
|
|
||||||
text: profileBox.displayText
|
text: profileBox.displayText
|
||||||
font: profileBox.font
|
font: profileBox.font
|
||||||
color: profileBox.pressed ? "#ffffff" : "#ffffff"
|
color: "#ffffff"
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
implicitWidth: 120
|
implicitWidth: 120; implicitHeight: 40
|
||||||
implicitHeight: 40
|
|
||||||
border.color: profileBox.pressed ? "#ffffff" : "#232323"
|
|
||||||
border.width: profileBox.visualFocus ? 2 : 1
|
|
||||||
color: profileBox.down ? "#91B315" : "#232323"
|
color: profileBox.down ? "#91B315" : "#232323"
|
||||||
radius: 6
|
radius: 6
|
||||||
|
border.color: profileBox.pressed ? "#ffffff" : "#232323"
|
||||||
|
border.width: profileBox.visualFocus ? 2 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
popup: Popup {
|
popup: Popup {
|
||||||
y: profileBox.height - 1
|
y: profileBox.height - 1
|
||||||
width: profileBox.width
|
width: profileBox.width
|
||||||
height: Math.min(contentItem.implicitHeight, profileBox.Window.height - topMargin - bottomMargin)
|
padding: 0
|
||||||
padding: 1
|
// Высота = кнопка "+" (32) + элементы списка, не более 200
|
||||||
|
height: 32 + Math.min(profileItemList.contentHeight, 200)
|
||||||
|
|
||||||
contentItem: ListView {
|
contentItem: Item {
|
||||||
rotation: 0
|
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"
|
||||||
|
|
||||||
|
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
|
clip: true
|
||||||
implicitHeight: contentHeight
|
model: backend.profileNames
|
||||||
model: profileBox.popup.visible ? profileBox.delegateModel : null
|
|
||||||
currentIndex: profileBox.highlightedIndex
|
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
parent: ListView.parent
|
parent: profileItemList.parent
|
||||||
anchors.top: ListView.top
|
anchors.top: profileItemList.top
|
||||||
anchors.left: ListView.right
|
anchors.left: profileItemList.right
|
||||||
anchors.bottom: ListView.bottom
|
anchors.bottom: profileItemList.bottom
|
||||||
|
}
|
||||||
/*background: Rectangle{
|
|
||||||
opacity: 0.5
|
|
||||||
color: "#91B315"
|
|
||||||
}*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
border.color: "#232323"
|
|
||||||
color: "#232323"
|
color: "#232323"
|
||||||
radius: 6
|
radius: 6
|
||||||
|
border.color: "#232323"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// ── Version ComboBox ───────────────────────────────────────────────────
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: versionBox
|
id: versionBox
|
||||||
width: 210
|
x: 624
|
||||||
|
y: 451
|
||||||
|
width: 183
|
||||||
height: 42
|
height: 42
|
||||||
anchors.right: button.right
|
anchors.right: button.right
|
||||||
anchors.bottom: button.top
|
anchors.bottom: button.top
|
||||||
anchors.bottomMargin: -218
|
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 {
|
popup: Popup {
|
||||||
y: versionBox.height - 1
|
y: versionBox.height - 1
|
||||||
width: versionBox.width
|
width: versionBox.width
|
||||||
height: Math.min(contentItem.implicitHeight, versionBox.Window.height - topMargin - bottomMargin)
|
padding: 0
|
||||||
padding: 1
|
height: 32 + Math.min(versionItemList.contentHeight, 200)
|
||||||
contentItem: ListView {
|
|
||||||
rotation: 0
|
contentItem: Item {
|
||||||
model: versionBox.popup.visible ? versionBox.delegateModel : null
|
anchors.fill: parent
|
||||||
implicitHeight: contentHeight
|
|
||||||
currentIndex: versionBox.highlightedIndex
|
// ── "+" 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
|
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 {
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
anchors.left: ListView.right
|
parent: versionItemList.parent
|
||||||
anchors.top: ListView.top
|
anchors.top: versionItemList.top
|
||||||
anchors.bottom: ListView.bottom
|
anchors.left: versionItemList.right
|
||||||
parent: ListView.parent
|
anchors.bottom: versionItemList.bottom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: "#232323"
|
color: "#232323"
|
||||||
radius: 6
|
radius: 6
|
||||||
border.color: "#232323"
|
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 {
|
// ── Add Profile Dialog ─────────────────────────────────────────────────
|
||||||
id: delegate1
|
// Bug fix: header/footer must be Item (not Rectangle) with explicit
|
||||||
width: versionBox.width
|
// implicitHeight so Dialog correctly computes its own total height.
|
||||||
required property var model
|
// Rectangle.implicitHeight defaults to 0 regardless of height:.
|
||||||
required property int index
|
Dialog {
|
||||||
highlighted: versionBox.highlightedIndex === index
|
id: addProfileDialog
|
||||||
contentItem: Text {
|
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 // tells Dialog how tall the header is
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Новый профиль"
|
||||||
color: "#ffffff"
|
color: "#ffffff"
|
||||||
text: delegate1.model[versionBox.textRole]
|
font.pixelSize: 17
|
||||||
elide: Text.ElideRight
|
font.bold: true
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
font: versionBox.font
|
|
||||||
}
|
}
|
||||||
background: Rectangle {
|
Rectangle {
|
||||||
color: delegate1.highlighted ? "#91B315" : "#232323"
|
anchors.bottom: parent.bottom
|
||||||
radius: 5
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: "#333333"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Connections {
|
|
||||||
target: versionBox
|
|
||||||
function onActivated() { canvas.requestPaint(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
contentItem: Column {
|
||||||
target: versionBox
|
spacing: 12
|
||||||
function onActivated() { dropDownArrow1.visible = true }
|
topPadding: 20
|
||||||
}
|
bottomPadding: 20
|
||||||
contentItem: Text {
|
|
||||||
color: versionBox.pressed ? "#ffffff" : "#ffffff"
|
TextField {
|
||||||
text: versionBox.displayText
|
id: pfName
|
||||||
elide: Text.ElideRight
|
x: 20
|
||||||
horizontalAlignment: Text.AlignLeft
|
width: parent.width - 40
|
||||||
verticalAlignment: Text.AlignVCenter
|
placeholderText: "Имя профиля"
|
||||||
rightPadding: versionBox.indicator.width + versionBox.spacing
|
color: "#ffffff"
|
||||||
leftPadding: 56
|
placeholderTextColor: "#666666"
|
||||||
font: versionBox.font
|
|
||||||
}
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: versionBox.down ? "#91B315" : "#232323"
|
color: "#2a2a2a"
|
||||||
radius: 6
|
radius: 6
|
||||||
border.color: versionBox.pressed ? "#ffffff" : "#232323"
|
border.color: pfName.activeFocus ? "#91B315" : "#444444"
|
||||||
border.width: versionBox.visualFocus ? 2 : 1
|
border.width: 1
|
||||||
implicitWidth: 120
|
|
||||||
implicitHeight: 40
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
launcherbackend.cpp
Normal file
114
launcherbackend.cpp
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#include "launcherbackend.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
44
launcherbackend.h
Normal file
44
launcherbackend.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
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<Profile> m_profiles;
|
||||||
|
QList<Version> m_versions;
|
||||||
|
QString m_dataDir;
|
||||||
|
int m_nextProfileId = 1;
|
||||||
|
int m_nextVersionId = 1;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user