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 editable: false leftPadding: -30 model: backend.profileNames indicator: Image { 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 } contentItem: Text { x: 26 width: 114 rightPadding: profileBox.indicator.width + profileBox.spacing text: profileBox.displayText font: profileBox.font color: "#ffffff" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight } 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 } 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 { 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.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 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" } } } // ── 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 = "" } } }