Files
minecraft-launcher/Main.qml
T

1231 lines
46 KiB
QML
Raw Normal View History

2025-12-29 02:18:35 +03:00
import QtQuick
import QtQuick.Layouts 2.15
import QtQuick.Controls 2.15
import Minecraft_launcher
2025-12-29 02:18:35 +03:00
Window {
id: window
width: 1280
height: 720
visible: true
flags: Qt.Window
title: qsTr("Minecraft Launcher")
// ── Backend ────────────────────────────────────────────────────────────
LauncherBackend {
id: backend
2026-08-22 18:19:13 +03:00
onLaunched: (profileName, buildName, serverUrl) => {
window.showToast(qsTr("Запущено: %1 — %2").arg(profileName).arg(buildName), "#4b7a1f")
2026-08-22 18:19:13 +03:00
}
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")
}
2026-08-22 18:19:13 +03:00
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()
}
}
2025-12-29 02:18:35 +03:00
2026-08-24 21:09:52 +03:00
function formatMb(bytes) {
return (bytes / 1048576).toFixed(1)
}
// ── Background ─────────────────────────────────────────────────────────
2025-12-29 02:18:35 +03:00
Image {
id: image
opacity: 0.296
anchors.fill: parent
source: "images/GovuztTW8AAHqBf.jpeg"
fillMode: Image.Stretch
}
2026-08-22 18:19:13 +03:00
// ── Status toast ───────────────────────────────────────────────────────
Rectangle {
2026-08-22 18:19:13 +03:00
id: toastBackground
visible: toastLabel.text !== ""
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: (downloadPanel.visible || switchPanel.visible) ? 110 : 40
2026-08-22 18:19:13 +03:00
width: Math.min(window.width - 80, toastLabel.implicitWidth + 32)
height: toastLabel.implicitHeight + 20
radius: 8
color: "#cc3333"
Text {
2026-08-22 18:19:13 +03:00
id: toastLabel
anchors.centerIn: parent
2026-08-22 18:19:13 +03:00
width: parent.width - 32
color: "#ffffff"
font.pixelSize: 14
2026-08-22 18:19:13 +03:00
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Timer {
2026-08-22 18:19:13 +03:00
id: toastTimer
interval: 4000
onTriggered: toastLabel.text = ""
}
}
// ── Панели хода работ ──────────────────────────────────────────────────
// Загрузка и смена сборки взаимоисключающи по построению: busy() не даёт
// начать переключение во время установки и наоборот. Поэтому у панелей
// одни и те же якоря.
ProgressPanel {
2026-08-24 21:09:52 +03:00
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()
}
2026-08-24 21:09:52 +03:00
ProgressPanel {
id: switchPanel
visible: backend.switching
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.leftMargin: 24
anchors.bottomMargin: 24
2026-08-24 21:09:52 +03:00
title: backend.switchStage
fraction: backend.switchProgress
status: backend.switchStatus
// Отменять можно только пока идёт архивация: после очистки .minecraft
// отступать некуда, операцию нужно довести до конца.
cancellable: false
2026-08-24 21:09:52 +03:00
}
// ── Play button ────────────────────────────────────────────────────────
2025-12-29 02:18:35 +03:00
Button {
id: button
width: 335
height: 170
anchors.centerIn: parent
hoverEnabled: false
2026-08-22 18:19:13 +03:00
enabled: !backend.busy && !backend.gameRunning
2025-12-29 02:18:35 +03:00
background: Image {
id: buttonImage
source: "images/Play_Button/Play_Active.svg"
2026-08-22 18:19:13 +03:00
opacity: button.enabled ? 1.0 : 0.45
2025-12-29 02:18:35 +03:00
}
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)
2025-12-29 02:18:35 +03:00
}
// ── Profile ComboBox ───────────────────────────────────────────────────
2025-12-29 02:18:35 +03:00
ComboBox {
id: profileBox
y: 451
width: 146
2025-12-29 02:18:35 +03:00
height: 42
anchors.left: button.right
anchors.bottom: button.top
anchors.leftMargin: -335
anchors.bottomMargin: -218
2026-06-14 20:04:17 +03:00
rotation: 0
2025-12-29 02:18:35 +03:00
editable: false
2026-08-27 16:44:42 +03:00
hoverEnabled: true
2025-12-29 02:18:35 +03:00
model: backend.profileNames
2025-12-29 02:18:35 +03:00
2026-08-27 16:44:42 +03:00
// Пока список закрыт: открытый popup накрывает кнопку, и подсказка
// висела бы поверх собственного меню.
ToolTip.visible: profileBox.hovered && !profileBox.down
ToolTip.text: backend.profileNames.length === 0
? qsTr("Профили игрока — добавьте первый профиль")
: qsTr("Профиль игрока: %1").arg(profileBox.displayText)
2026-06-14 20:04:17 +03:00
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
}
2025-12-29 02:18:35 +03:00
indicator: Image {
2026-06-14 20:04:17 +03:00
id: profileArrow
width: 10; height: 10
2025-12-29 02:18:35 +03:00
visible: true
anchors.left: parent.left
anchors.leftMargin: 10
2025-12-29 02:18:35 +03:00
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
2025-12-29 02:18:35 +03:00
fillMode: Image.PreserveAspectFit
autoTransform: true
}
background: Rectangle {
implicitWidth: 120; implicitHeight: 40
2025-12-29 02:18:35 +03:00
color: profileBox.down ? "#91B315" : "#232323"
radius: 6
border.color: profileBox.pressed ? "#ffffff" : "#232323"
border.width: profileBox.visualFocus ? 2 : 1
2025-12-29 02:18:35 +03:00
}
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: "+ Добавить профиль"
2025-12-29 02:18:35 +03:00
color: "#91B315"
font.pixelSize: 12
}
MouseArea {
id: profileAddArea
anchors.fill: parent
onClicked: {
profileBox.popup.close()
addProfileDialog.open()
}
}
}
2025-12-29 02:18:35 +03:00
// ── Добавленные профили — всегда ниже кнопки ───────────
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
2026-06-16 12:16:59 +03:00
rightPadding: 62
}
background: Rectangle {
color: (profileBox.currentIndex === profileItem.index || profileItem.hovered)
? "#91B315" : "#232323"
radius: 5
}
onClicked: {
profileBox.currentIndex = profileItem.index
profileBox.popup.close()
}
2026-06-16 12:16:59 +03:00
// ── Карандаш — видна при наведении на строку ────
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
}
2025-12-29 02:18:35 +03:00
}
}
background: Rectangle {
color: "#232323"
radius: 6
border.color: "#232323"
2025-12-29 02:18:35 +03:00
}
}
}
// ── Активная сборка ────────────────────────────────────────────────────
// Кнопка занимает место бывшего выпадающего списка версий: и сама сборка,
// и её содержимое правятся теперь в модальном окне.
Button {
id: buildButton
x: 624
y: 451
width: 183
2025-12-29 02:18:35 +03:00
height: 42
anchors.right: button.right
anchors.bottom: button.top
2026-06-14 20:04:17 +03:00
anchors.rightMargin: 0
2025-12-29 02:18:35 +03:00
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
2025-12-29 02:18:35 +03:00
}
}
ToolTip.visible: hovered && !buildsDialog.opened
ToolTip.text: qsTr("Пользовательские сборки")
onClicked: buildsDialog.open()
}
2026-06-19 21:03:37 +03:00
// ── Open mods folder button ────────────────────────────────────────────
Button {
id: folderButton
2026-07-05 19:35:30 +03:00
width: 70
height: 40
anchors.left: buildButton.left
2026-07-05 19:35:30 +03:00
anchors.bottom: profileBox.bottom
anchors.leftMargin: -59
anchors.bottomMargin: -45
2026-06-19 21:03:37 +03:00
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()
}
2026-08-22 18:19:13 +03:00
// ── 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
2026-08-27 16:44:42 +03:00
// Ассеты 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"
2026-08-22 18:19:13 +03:00
: "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
2025-12-29 02:18:35 +03:00
}
header: Item {
implicitHeight: 52 // tells Dialog how tall the header is
Text {
anchors.centerIn: parent
text: "Новый профиль"
2025-12-29 02:18:35 +03:00
color: "#ffffff"
font.pixelSize: 17
font.bold: true
2025-12-29 02:18:35 +03:00
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: "#333333"
2025-12-29 02:18:35 +03:00
}
}
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
2026-08-22 18:19:13 +03:00
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
}
}
2026-08-22 18:19:13 +03:00
DarkCombo {
id: pfAuth
x: 20
width: parent.width - 40
model: ["Офлайн (без пароля)", "Ely.by"]
}
2025-12-29 02:18:35 +03:00
}
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()
}
}
2025-12-29 02:18:35 +03:00
}
onAccepted: {
const name = pfName.text.trim()
if (name !== "") {
2026-08-22 18:19:13 +03:00
backend.addProfile(name, pfLogin.text.trim(), pfPassword.text,
pfAuth.currentIndex === 1 ? "elyby" : "offline")
profileBox.currentIndex = backend.profileNames.length - 1
}
2026-08-22 18:19:13 +03:00
pfName.text = ""; pfLogin.text = ""; pfPassword.text = ""; pfAuth.currentIndex = 0
2025-12-29 02:18:35 +03:00
}
onRejected: {
2026-08-22 18:19:13 +03:00
pfName.text = ""; pfLogin.text = ""; pfPassword.text = ""; pfAuth.currentIndex = 0
2025-12-29 02:18:35 +03:00
}
}
// ── 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
}
}
2026-08-22 18:19:13 +03:00
TextField {
id: epPassword
2026-08-22 18:19:13 +03:00
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
}
2026-08-22 18:19:13 +03:00
}
DarkCombo {
id: epAuth
2026-08-22 18:19:13 +03:00
x: 20
width: parent.width - 40
model: ["Офлайн (без пароля)", "Ely.by"]
2026-08-22 18:19:13 +03:00
}
}
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 = ""
}
}
2026-06-16 12:16:59 +03:00
// ── Builds Dialog ──────────────────────────────────────────────────────
BuildsDialog {
id: buildsDialog
backend: backend
x: (window.width - width) / 2
y: (window.height - height) / 2
}
// ── Two-factor Dialog ──────────────────────────────────────────────────
// Ely.by отклоняет пароль с пометкой two factor — код добираем здесь и
// продолжаем прерванный запуск.
2026-06-16 12:16:59 +03:00
Dialog {
id: twoFactorDialog
2026-06-16 12:16:59 +03:00
modal: true
width: 320
x: (window.width - width) / 2
y: (window.height - height) / 2
padding: 0
closePolicy: Popup.NoAutoClose
2026-06-16 12:16:59 +03:00
property string profileName: ""
2026-06-16 12:16:59 +03:00
background: Rectangle {
color: "#1e1e1e"
radius: 10
border.color: "#91B315"
border.width: 1
}
header: Item {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: "Двухфакторная аутентификация"
2026-06-16 12:16:59 +03:00
color: "#ffffff"
font.pixelSize: 15
2026-06-16 12:16:59 +03:00
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
}
2026-06-16 12:16:59 +03:00
TextField {
id: twoFactorCode
2026-08-22 18:19:13 +03:00
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
2026-08-27 19:35:11 +03:00
// Выбранная сборка Java живёт в свойстве, а не в поле ввода: её выбирают
// в отдельном окне, а записывается она только по «Сохранить».
property string javaRuntimeId: ""
// Не привязка: javaRuntimeInfo() — обычный вызов, и сам он не
// пересчитается, когда сборка докачается. Обновляем по событиям.
property var javaRuntimeInfo: null
onJavaRuntimeIdChanged: settingsDialog.refreshJavaInfo()
function refreshJavaInfo() {
javaRuntimeInfo = javaRuntimeId === "" ? null
: backend.javaRuntimeInfo(javaRuntimeId)
}
Connections {
target: backend
function onJavaRuntimeInstalled(runtimeId) { settingsDialog.refreshJavaInfo() }
}
2026-08-22 18:19:13 +03:00
function load() {
const s = backend.settings()
stGameDir.text = s.gameDir || ""
stJavaPath.text = s.javaPath || ""
2026-08-27 19:35:11 +03:00
settingsDialog.javaRuntimeId = s.javaRuntime || ""
settingsDialog.refreshJavaInfo()
2026-08-22 18:19:13 +03:00
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: "автоопределение"
}
2026-08-27 19:35:11 +03:00
// Сборка Java из папки лаунчера. Выбрана — запуск идёт ей, а не тем,
// что нашлось в системе; путь выше остаётся запасным вариантом.
Column {
x: 20
width: parent.width - 40
spacing: 3
Text {
text: "Версия Java из папки лаунчера"
color: "#aaaaaa"
font.pixelSize: 11
}
Rectangle {
width: parent.width
height: 32
radius: 6
color: "#2a2a2a"
border.color: javaFieldArea.containsMouse ? "#91B315" : "#444444"
border.width: 1
Text {
anchors.fill: parent
leftPadding: 10
rightPadding: 150
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
color: settingsDialog.javaRuntimeId === "" ? "#666666" : "#ffffff"
text: {
if (settingsDialog.javaRuntimeId === "")
return qsTr("не выбрана — искать в системе")
const info = settingsDialog.javaRuntimeInfo
if (!info)
return settingsDialog.javaRuntimeId
return info.installed ? info.label
: qsTr("%1 — скачивается").arg(info.label)
}
}
Text {
anchors.right: clearJava.left
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Выбрать…")
color: "#91B315"
font.pixelSize: 12
}
Text {
id: clearJava
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
visible: settingsDialog.javaRuntimeId !== ""
text: "✕"
color: clearJavaArea.containsMouse ? "#cc6666" : "#666666"
font.pixelSize: 12
MouseArea {
id: clearJavaArea
anchors.fill: parent
anchors.margins: -6
hoverEnabled: true
onClicked: settingsDialog.javaRuntimeId = ""
}
}
MouseArea {
id: javaFieldArea
anchors.fill: parent
anchors.rightMargin: 26
hoverEnabled: true
onClicked: javaPicker.openFor(settingsDialog.javaRuntimeId,
backend.requiredJavaMajor(backend.activeBuildIndex))
}
}
}
2026-08-22 18:19:13 +03:00
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(),
2026-08-27 19:35:11 +03:00
"javaRuntime": settingsDialog.javaRuntimeId,
2026-08-22 18:19:13 +03:00
"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
})
}
2026-08-27 19:35:11 +03:00
// Окно выбора Java. Живёт рядом с настройками, а не внутри: оно шире их и
// центрируется по самому окну лаунчера.
JavaPickerDialog {
id: javaPicker
backend: backend
x: (window.width - width) / 2
y: (window.height - height) / 2
onRuntimeChosen: (runtimeId) => settingsDialog.javaRuntimeId = runtimeId
}
2025-12-29 02:18:35 +03:00
}