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, buildName, serverUrl) => { window.showToast(qsTr("Запущено: %1 — %2").arg(profileName).arg(buildName), "#4b7a1f") } onLaunchProgress: (message) => window.showToast(message, "#3a5a8c", 0) onLaunchError: (message) => window.showToast(message, "#cc3333", 8000) onTwoFactorRequired: (profileName) => { twoFactorDialog.profileName = profileName twoFactorCode.text = "" twoFactorDialog.open() } onGameFinished: (exitCode, crashed) => { window.showToast(crashed || exitCode !== 0 ? qsTr("Игра завершилась с ошибкой (код %1)").arg(exitCode) : qsTr("Игра закрыта"), crashed || exitCode !== 0 ? "#cc3333" : "#555555") } onGameOutput: (line) => console.log(line) } // Единая всплывающая плашка: сообщения о ходе запуска, ошибки, статус игры. // timeout === 0 — держим до следующего сообщения. function showToast(text, color, timeout) { toastLabel.text = text toastBackground.color = color toastTimer.stop() if (timeout === undefined) timeout = 4000 if (timeout > 0) { toastTimer.interval = timeout toastTimer.restart() } } function formatMb(bytes) { return (bytes / 1048576).toFixed(1) } // ── Background ───────────────────────────────────────────────────────── Image { id: image opacity: 0.296 anchors.fill: parent source: "images/GovuztTW8AAHqBf.jpeg" fillMode: Image.Stretch } // ── Status toast ─────────────────────────────────────────────────────── Rectangle { id: toastBackground visible: toastLabel.text !== "" anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: (downloadPanel.visible || switchPanel.visible) ? 110 : 40 width: Math.min(window.width - 80, toastLabel.implicitWidth + 32) height: toastLabel.implicitHeight + 20 radius: 8 color: "#cc3333" Text { id: toastLabel anchors.centerIn: parent width: parent.width - 32 color: "#ffffff" font.pixelSize: 14 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap } Timer { id: toastTimer interval: 4000 onTriggered: toastLabel.text = "" } } // ── Панели хода работ ────────────────────────────────────────────────── // Загрузка и смена сборки взаимоисключающи по построению: busy() не даёт // начать переключение во время установки и наоборот. Поэтому у панелей // одни и те же якоря. ProgressPanel { id: downloadPanel visible: backend.downloading anchors.left: parent.left anchors.bottom: parent.bottom anchors.leftMargin: 24 anchors.bottomMargin: 24 title: qsTr("Загрузка %1").arg(backend.downloadVersion) fraction: backend.downloadProgress status: backend.downloadStatus detail: backend.downloadBytesTotal > 0 ? qsTr("%1 — %2 / %3 МБ").arg(backend.downloadStatus) .arg(window.formatMb(backend.downloadBytesDone)) .arg(window.formatMb(backend.downloadBytesTotal)) : "" onCancelRequested: backend.cancelDownload() } ProgressPanel { id: switchPanel visible: backend.switching anchors.left: parent.left anchors.bottom: parent.bottom anchors.leftMargin: 24 anchors.bottomMargin: 24 title: backend.switchStage fraction: backend.switchProgress status: backend.switchStatus // Отменять можно только пока идёт архивация: после очистки .minecraft // отступать некуда, операцию нужно довести до конца. cancellable: false } // ── Play button ──────────────────────────────────────────────────────── Button { id: button width: 335 height: 170 anchors.centerIn: parent hoverEnabled: false enabled: !backend.busy && !backend.gameRunning background: Image { id: buttonImage source: "images/Play_Button/Play_Active.svg" opacity: button.enabled ? 1.0 : 0.45 } onPressed: buttonImage.source = "images/Play_Button/Play_pressed.svg" onReleased: buttonImage.source = "images/Play_Button/Play_Active.svg" onClicked: backend.launchGame(profileBox.currentIndex, backend.activeBuildIndex) } // ── 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 hoverEnabled: true model: backend.profileNames // Пока список закрыт: открытый popup накрывает кнопку, и подсказка // висела бы поверх собственного меню. ToolTip.visible: profileBox.hovered && !profileBox.down ToolTip.text: backend.profileNames.length === 0 ? qsTr("Профили игрока — добавьте первый профиль") : qsTr("Профиль игрока: %1").arg(profileBox.displayText) 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" } } } // ── Активная сборка ──────────────────────────────────────────────────── // Кнопка занимает место бывшего выпадающего списка версий: и сама сборка, // и её содержимое правятся теперь в модальном окне. Button { id: buildButton x: 624 y: 451 width: 183 height: 42 anchors.right: button.right anchors.bottom: button.top anchors.rightMargin: 0 anchors.bottomMargin: -218 hoverEnabled: true enabled: !backend.gameRunning && !backend.switching contentItem: Text { text: backend.activeBuildName !== "" ? backend.activeBuildName : qsTr("Сборка не выбрана") color: "#ffffff" font: buildButton.font verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft elide: Text.ElideRight leftPadding: 30 rightPadding: 5 } background: Rectangle { color: buildButton.pressed || buildsDialog.opened ? "#91B315" : "#232323" radius: 6 border.color: buildButton.hovered ? "#91B315" : "#232323" border.width: 1 Image { width: 10; height: 10 anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter source: buildsDialog.opened ? "images/Profile_Box/Asset_23.svg" : "images/Profile_Box/Asset_24.svg" rotation: 180 sourceSize.width: 10; sourceSize.height: 10 fillMode: Image.PreserveAspectFit } } ToolTip.visible: hovered && !buildsDialog.opened ToolTip.text: qsTr("Пользовательские сборки") onClicked: buildsDialog.open() } // ── Open mods folder button ──────────────────────────────────────────── Button { id: folderButton width: 70 height: 40 anchors.left: buildButton.left anchors.bottom: profileBox.bottom anchors.leftMargin: -59 anchors.bottomMargin: -45 hoverEnabled: true background: Image { id: folderButtonImage anchors.fill: parent source: folderButton.pressed ? "images/Folder/Folder_Pressed.svg" : folderButton.hovered ? "images/Folder/Folder_Active.svg" : "images/Folder/Folder_Idle.svg" fillMode: Image.PreserveAspectFit } ToolTip.visible: hovered ToolTip.text: qsTr("Открыть папку с модами Minecraft") onClicked: backend.openMinecraftFolder() } // ── Settings button ──────────────────────────────────────────────────── Button { id: settingsButton width: 70 height: 40 anchors.left: folderButton.right anchors.verticalCenter: folderButton.verticalCenter anchors.leftMargin: 8 hoverEnabled: true background: Image { anchors.fill: parent // Ассеты Options названы наоборот относительно Folder: зелёный // #637a10 лежит в Options_Pressed.svg, а Options_active.svg — // тёмный, как Idle. Берём их по цвету, а не по имени, чтобы // наведение выглядело так же, как у кнопки папки модов. source: settingsButton.pressed ? "images/Options/Options_active.svg" : settingsButton.hovered ? "images/Options/Options_Pressed.svg" : "images/Options/Options_Idle.svg" fillMode: Image.PreserveAspectFit } ToolTip.visible: hovered ToolTip.text: qsTr("Настройки запуска: Java, память, папка игры") onClicked: settingsDialog.load() } // ── 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 enabled: pfAuth.currentIndex === 1 placeholderText: pfAuth.currentIndex === 1 ? "Пароль Ely.by" : "Не нужен в офлайне" echoMode: TextInput.Password color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: pfPassword.activeFocus ? "#91B315" : "#444444" border.width: 1 } } DarkCombo { id: pfAuth x: 20 width: parent.width - 40 model: ["Офлайн (без пароля)", "Ely.by"] } } 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, pfAuth.currentIndex === 1 ? "elyby" : "offline") profileBox.currentIndex = backend.profileNames.length - 1 } pfName.text = ""; pfLogin.text = ""; pfPassword.text = ""; pfAuth.currentIndex = 0 } onRejected: { pfName.text = ""; pfLogin.text = ""; pfPassword.text = ""; pfAuth.currentIndex = 0 } } // ── 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 || "" epAuth.currentIndex = data.authType === "elyby" ? 1 : 0 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 enabled: epAuth.currentIndex === 1 placeholderText: epAuth.currentIndex === 1 ? "Пароль Ely.by" : "Не нужен в офлайне" echoMode: TextInput.Password color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: epPassword.activeFocus ? "#91B315" : "#444444" border.width: 1 } } DarkCombo { id: epAuth x: 20 width: parent.width - 40 model: ["Офлайн (без пароля)", "Ely.by"] } } 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, epAuth.currentIndex === 1 ? "elyby" : "offline") editIndex = -1 epName.text = ""; epLogin.text = ""; epPassword.text = "" } onRejected: { editIndex = -1 epName.text = ""; epLogin.text = ""; epPassword.text = "" } } // ── Builds Dialog ────────────────────────────────────────────────────── BuildsDialog { id: buildsDialog backend: backend x: (window.width - width) / 2 y: (window.height - height) / 2 } // ── Two-factor Dialog ────────────────────────────────────────────────── // Ely.by отклоняет пароль с пометкой two factor — код добираем здесь и // продолжаем прерванный запуск. Dialog { id: twoFactorDialog modal: true width: 320 x: (window.width - width) / 2 y: (window.height - height) / 2 padding: 0 closePolicy: Popup.NoAutoClose property string profileName: "" 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: 15 font.bold: true } Rectangle { anchors.bottom: parent.bottom width: parent.width height: 1 color: "#333333" } } contentItem: Column { spacing: 12 topPadding: 20 bottomPadding: 20 Text { x: 20 width: parent.width - 40 text: qsTr("Введите код из приложения-аутентификатора для «%1»") .arg(twoFactorDialog.profileName) color: "#aaaaaa" font.pixelSize: 12 wrapMode: Text.Wrap } TextField { id: twoFactorCode x: 20 width: parent.width - 40 placeholderText: "000000" inputMethodHints: Qt.ImhDigitsOnly color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#2a2a2a" radius: 6 border.color: twoFactorCode.activeFocus ? "#91B315" : "#444444" border.width: 1 } onAccepted: twoFactorDialog.accept() } } 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: twoFactorDialog.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: twoFactorDialog.accept() } } } onAccepted: backend.submitTwoFactorCode(twoFactorCode.text) onRejected: backend.cancelPendingLaunch() } // ── Settings Dialog ──────────────────────────────────────────────────── Dialog { id: settingsDialog modal: true width: 460 x: (window.width - width) / 2 y: (window.height - height) / 2 padding: 0 function load() { const s = backend.settings() stGameDir.text = s.gameDir || "" stJavaPath.text = s.javaPath || "" stMinMemory.text = String(s.minMemoryMb) stMaxMemory.text = String(s.maxMemoryMb) stJvmArgs.text = s.jvmArgs || "" stWidth.text = String(s.windowWidth) stHeight.text = String(s.windowHeight) stFullscreen.checked = s.fullscreen === true stResolved.text = qsTr("Папка игры: %1").arg(s.resolvedGameDir) stJavaList.text = backend.detectedJava().join("\n") || qsTr("Java не найдена") 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: 10 topPadding: 18 bottomPadding: 18 LabelledField { id: stGameDir x: 20 width: parent.width - 40 label: "Папка .minecraft" placeholder: "по умолчанию" } LabelledField { id: stJavaPath x: 20 width: parent.width - 40 label: "Путь к Java" placeholder: "автоопределение" } Row { x: 20 spacing: 10 LabelledField { id: stMinMemory width: 100 label: "-Xms, МБ" validator: IntValidator { bottom: 0; top: 1048576 } } LabelledField { id: stMaxMemory width: 100 label: "-Xmx, МБ" validator: IntValidator { bottom: 0; top: 1048576 } } LabelledField { id: stWidth width: 100 label: "Ширина окна" validator: IntValidator { bottom: 0; top: 16384 } } LabelledField { id: stHeight width: 100 label: "Высота окна" validator: IntValidator { bottom: 0; top: 16384 } } } LabelledField { id: stJvmArgs x: 20 width: parent.width - 40 label: "Дополнительные аргументы JVM" placeholder: "-XX:+UseG1GC …" } CheckBox { id: stFullscreen x: 20 text: "Запускать в полноэкранном режиме" // Индикатор Basic-стиля — крупный светлый квадрат, на тёмном // фоне диалога он не читается. Рисуем свой в палитре окна. indicator: Rectangle { implicitWidth: 18 implicitHeight: 18 x: stFullscreen.leftPadding y: (stFullscreen.height - height) / 2 radius: 4 color: "#2a2a2a" border.color: stFullscreen.checked ? "#91B315" : "#444444" border.width: 1 Rectangle { anchors.centerIn: parent width: 10; height: 10 radius: 2 color: "#91B315" visible: stFullscreen.checked } } contentItem: Text { text: stFullscreen.text color: "#ffffff" leftPadding: (stFullscreen.indicator ? stFullscreen.indicator.width : 0) + 6 verticalAlignment: Text.AlignVCenter } } Text { id: stResolved x: 20 width: parent.width - 40 color: "#888888" font.pixelSize: 11 wrapMode: Text.Wrap } Text { id: stJavaList x: 20 width: parent.width - 40 color: "#888888" font.pixelSize: 11 wrapMode: Text.Wrap } } 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: settingsDialog.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: settingsDialog.accept() } } } onAccepted: backend.updateSettings({ "gameDir": stGameDir.text.trim(), "javaPath": stJavaPath.text.trim(), "minMemoryMb": parseInt(stMinMemory.text) || 0, "maxMemoryMb": parseInt(stMaxMemory.text) || 0, "jvmArgs": stJvmArgs.text.trim(), "windowWidth": parseInt(stWidth.text) || 0, "windowHeight": parseInt(stHeight.text) || 0, "fullscreen": stFullscreen.checked }) } }