Files
minecraft-launcher/Main.qml
T
2026-08-27 16:44:42 +03:00

1122 lines
41 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
})
}
}