import QtQuick import QtQuick.Layouts 2.15 import QtQuick.Controls 2.15 import Minecraft_launcher Window { id: window width: 1280 height: 720 visible: true flags: Qt.Window 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 anchors.fill: parent source: "images/GovuztTW8AAHqBf.jpeg" 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 width: 335 height: 170 anchors.centerIn: parent hoverEnabled: false background: Image { id: 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 y: 451 width: 146 height: 42 anchors.left: button.right anchors.bottom: button.top anchors.leftMargin: -335 anchors.bottomMargin: -218 rotation: 0 editable: false model: backend.profileNames contentItem: Text { id: profileText text: profileBox.displayText font: profileBox.font color: "#ffffff" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft elide: Text.ElideRight anchors.fill: parent anchors.leftMargin: 30 anchors.rightMargin: 5 anchors.topMargin: 5 anchors.bottomMargin: 5 } indicator: Image { id: profileArrow width: 10; height: 10 visible: true 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" rotation: 180 sourceSize.width: 10; sourceSize.height: 10 fillMode: Image.PreserveAspectFit autoTransform: true } background: Rectangle { 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 padding: 0 // Высота = кнопка "+" (32) + элементы списка, не более 200 height: 32 + Math.min(profileItemList.contentHeight, 200) 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" 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 rightPadding: 62 } background: Rectangle { color: (profileBox.currentIndex === profileItem.index || profileItem.hovered) ? "#91B315" : "#232323" radius: 5 } onClicked: { profileBox.currentIndex = profileItem.index profileBox.popup.close() } // ── Карандаш — видна при наведении на строку ──── Image { id: profilePencil width: 16; height: 16 anchors.right: profileTrash.left anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter visible: profileItem.hovered opacity: profilePencilArea.containsMouse ? 1.0 : 0.7 source: "images/Pencil.svg" sourceSize.width: 16; sourceSize.height: 16 fillMode: Image.PreserveAspectFit MouseArea { id: profilePencilArea anchors.fill: parent anchors.margins: -6 hoverEnabled: true onClicked: editProfileDialog.openFor(profileItem.index) } } // ── Корзина — видна при наведении на строку ───── Image { id: profileTrash width: 16; height: 16 anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter visible: profileItem.hovered opacity: profileTrashArea.containsMouse ? 1.0 : 0.7 source: "images/Trash.svg" sourceSize.width: 16; sourceSize.height: 16 fillMode: Image.PreserveAspectFit MouseArea { id: profileTrashArea anchors.fill: parent anchors.margins: -6 hoverEnabled: true onClicked: backend.removeProfile(profileItem.index) } } } ScrollIndicator.vertical: ScrollIndicator { parent: profileItemList.parent anchors.top: profileItemList.top anchors.left: profileItemList.right anchors.bottom: profileItemList.bottom } } } background: Rectangle { color: "#232323" radius: 6 border.color: "#232323" } } } // ── Version ComboBox ─────────────────────────────────────────────────── ComboBox { id: versionBox x: 624 y: 451 width: 183 height: 42 anchors.right: button.right anchors.bottom: button.top anchors.rightMargin: 0 anchors.bottomMargin: -218 editable: false model: backend.versionNames indicator: Image { id: versionArrow 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 { id: versionText text: versionBox.displayText font: versionBox.font color: "#ffffff" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft elide: Text.ElideRight anchors.fill: parent anchors.leftMargin: 30 anchors.rightMargin: 5 anchors.topMargin: 5 anchors.bottomMargin: 5 } 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 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 rightPadding: 62 } background: Rectangle { color: (versionBox.currentIndex === versionItem.index || versionItem.hovered) ? "#91B315" : "#232323" radius: 5 } onClicked: { versionBox.currentIndex = versionItem.index versionBox.popup.close() } // ── Карандаш — видна при наведении на строку ──── Image { id: versionPencil width: 16; height: 16 anchors.right: versionTrash.left anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter visible: versionItem.hovered opacity: versionPencilArea.containsMouse ? 1.0 : 0.7 source: "images/Pencil.svg" sourceSize.width: 16; sourceSize.height: 16 fillMode: Image.PreserveAspectFit MouseArea { id: versionPencilArea anchors.fill: parent anchors.margins: -6 hoverEnabled: true onClicked: editVersionDialog.openFor(versionItem.index) } } // ── Корзина — видна при наведении на строку ───── Image { id: versionTrash width: 16; height: 16 anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter visible: versionItem.hovered opacity: versionTrashArea.containsMouse ? 1.0 : 0.7 source: "images/Trash.svg" sourceSize.width: 16; sourceSize.height: 16 fillMode: Image.PreserveAspectFit MouseArea { id: versionTrashArea anchors.fill: parent anchors.margins: -6 hoverEnabled: true onClicked: backend.removeVersion(versionItem.index) } } } 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" } } } // ── 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 } 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: 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 } } } 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 = "" } } // ── Edit Profile Dialog ──────────────────────────────────────────────── // Идентичен диалогу создания, но с предзаполненными данными выбранного // профиля. По нажатию ОК данные перезаписываются под тем же индексом. Dialog { id: editProfileDialog modal: true width: 320 x: (window.width - width) / 2 y: (window.height - height) / 2 padding: 0 property int editIndex: -1 function openFor(index) { const data = backend.profileAt(index) editIndex = index epName.text = data.name || "" epLogin.text = data.login || "" epPassword.text = data.password || "" profileBox.popup.close() open() } 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: epName x: 20 width: parent.width - 40 placeholderText: "Имя профиля" color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: epName.activeFocus ? "#91B315" : "#444444" border.width: 1 } } TextField { id: epLogin x: 20 width: parent.width - 40 placeholderText: "Логин" color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: epLogin.activeFocus ? "#91B315" : "#444444" border.width: 1 } } TextField { id: epPassword x: 20 width: parent.width - 40 placeholderText: "Пароль" echoMode: TextInput.Password color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: epPassword.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: editProfileDialog.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: editProfileDialog.accept() } } } onAccepted: { const name = epName.text.trim() if (editIndex >= 0 && name !== "") backend.updateProfile(editIndex, name, epLogin.text.trim(), epPassword.text) editIndex = -1 epName.text = ""; epLogin.text = ""; epPassword.text = "" } onRejected: { editIndex = -1 epName.text = ""; epLogin.text = ""; epPassword.text = "" } } // ── Edit Version Dialog ──────────────────────────────────────────────── // Идентичен диалогу создания, но с предзаполненными данными выбранной // версии. По нажатию ОК данные перезаписываются под тем же индексом. Dialog { id: editVersionDialog modal: true width: 320 x: (window.width - width) / 2 y: (window.height - height) / 2 padding: 0 property int editIndex: -1 function openFor(index) { const data = backend.versionAt(index) editIndex = index evName.text = data.name || "" evServer.text = data.serverUrl || "" versionBox.popup.close() open() } 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: evName x: 20 width: parent.width - 40 placeholderText: "Название версии" color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: evName.activeFocus ? "#91B315" : "#444444" border.width: 1 } } TextField { id: evServer x: 20 width: parent.width - 40 placeholderText: "URL сервера загрузки" color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: evServer.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: editVersionDialog.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: editVersionDialog.accept() } } } onAccepted: { const name = evName.text.trim() if (editIndex >= 0 && name !== "") backend.updateVersion(editIndex, name, evServer.text.trim()) editIndex = -1 evName.text = ""; evServer.text = "" } onRejected: { editIndex = -1 evName.text = ""; evServer.text = "" } } }