Files
minecraft-launcher/Main.qml
T

1980 lines
73 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, versionName, serverUrl) => {
window.showToast(qsTr("Запущено: %1 — %2").arg(profileName).arg(versionName), "#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 ? 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 = ""
}
}
// ── Download panel ─────────────────────────────────────────────────────
// Левый нижний угол: не задевает кнопку запуска, плашку по центру и
// кнопки папки с настройками.
Rectangle {
id: downloadPanel
visible: backend.downloading
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.leftMargin: 24
anchors.bottomMargin: 24
width: 320
height: 72
radius: 8
color: "#1e1e1e"
opacity: 0.94
border.color: "#91B315"
border.width: 1
Text {
x: 12; y: 8
width: parent.width - 70
elide: Text.ElideRight
color: "#ffffff"
font.pixelSize: 12
font.bold: true
text: qsTr("Загрузка %1").arg(backend.downloadVersion)
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
y: 8
color: "#91B315"
font.pixelSize: 12
// Пока не известен общий объём, доля равна -1.
text: backend.downloadProgress < 0
? "…" : Math.round(backend.downloadProgress * 100) + "%"
}
Rectangle {
id: dlTrack
x: 12; y: 30
width: parent.width - 24
height: 6
radius: 3
color: "#2a2a2a"
Rectangle {
width: backend.downloadProgress > 0 ? dlTrack.width * backend.downloadProgress : 0
height: parent.height
radius: 3
color: "#91B315"
Behavior on width { NumberAnimation { duration: 120 } }
}
}
Text {
x: 12; y: 44
width: parent.width - 40
elide: Text.ElideMiddle
color: "#888888"
font.pixelSize: 10
text: backend.downloadBytesTotal > 0
? qsTr("%1 — %2 / %3 МБ").arg(backend.downloadStatus)
.arg(window.formatMb(backend.downloadBytesDone))
.arg(window.formatMb(backend.downloadBytesTotal))
: backend.downloadStatus
}
Text {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 10
anchors.bottomMargin: 6
text: "✕"
color: cancelDownloadArea.containsMouse ? "#cc6666" : "#666666"
font.pixelSize: 12
MouseArea {
id: cancelDownloadArea
anchors.fill: parent
anchors.margins: -6
hoverEnabled: true
onClicked: backend.cancelDownload()
}
}
}
// ── 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, 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"
}
}
}
// ── Open mods folder button ────────────────────────────────────────────
Button {
id: folderButton
width: 70
height: 40
anchors.left: versionBox.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
source: settingsButton.pressed ? "images/Options/Options_Pressed.svg"
: settingsButton.hovered ? "images/Options/Options_active.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
}
}
// ── 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: "Адрес сервера, host:port (необязательно)"
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
color: "#2a2a2a"
radius: 6
border.color: verServer.activeFocus ? "#91B315" : "#444444"
border.width: 1
}
}
// Установленные версии и весь каталог Mojang одним списком.
VersionCatalogCombo {
id: verVersion
x: 20
width: parent.width - 40
catalog: backend.versionCatalog
onAboutToOpen: backend.refreshVersionCatalog()
}
Text {
x: 20
width: parent.width - 40
text: verVersion.selectedId === ""
? qsTr("Выберите версию из списка")
: backend.isVersionInstalled(verVersion.selectedId)
? qsTr("Версия уже установлена в .minecraft")
: qsTr("Версия будет загружена в .minecraft после сохранения")
color: verVersion.selectedId !== ""
&& !backend.isVersionInstalled(verVersion.selectedId)
? "#91B315" : "#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: 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()
}
}
}
// Манифест прогреваем заранее — тогда список открывается мгновенно.
onAboutToShow: {
verVersion.selectedId = ""
backend.refreshVersionCatalog()
}
onAccepted: {
const name = verName.text.trim()
if (name !== "") {
backend.addVersion(name, verServer.text.trim(), verVersion.selectedId)
versionBox.currentIndex = backend.versionNames.length - 1
}
verName.text = ""; verServer.text = ""; verVersion.selectedId = ""
}
onRejected: {
verName.text = ""; verServer.text = ""; verVersion.selectedId = ""
}
}
// ── 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 = ""
}
}
// ── Edit Version Dialog ────────────────────────────────────────────────
// Идентичен диалогу создания, но с предзаполненными данными выбранной
// версии. По нажатию ОК данные перезаписываются под тем же индексом.
Dialog {
id: editVersionDialog
modal: true
width: 380
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 || ""
evVersion.selectedId = data.versionId || ""
backend.refreshVersionCatalog()
const problems = backend.checkInstallation(index)
evStatus.text = problems.length === 0
? qsTr("Всё на месте, версию можно запускать")
: qsTr("Не хватает файлов (%1): %2").arg(problems.length).arg(problems[0])
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: "Адрес сервера, host:port (необязательно)"
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
color: "#2a2a2a"
radius: 6
border.color: evServer.activeFocus ? "#91B315" : "#444444"
border.width: 1
}
}
VersionCatalogCombo {
id: evVersion
x: 20
width: parent.width - 40
catalog: backend.versionCatalog
onAboutToOpen: backend.refreshVersionCatalog()
}
// Показываем, чего не хватает в .minecraft именно для этой версии.
Text {
id: evStatus
x: 20
width: parent.width - 40
color: text.indexOf("Всё на месте") === 0 ? "#8fbf5a" : "#cc6666"
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
enabled: evVersion.selectedId !== "" && !backend.downloading
opacity: enabled ? 1.0 : 0.45
contentItem: Text {
text: parent.text
color: "#ffffff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.pressed ? "#444444" : "#333333"
radius: 6
}
onClicked: backend.installVersion(evVersion.selectedId)
}
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(), evVersion.selectedId)
editIndex = -1
evName.text = ""; evServer.text = ""; evVersion.selectedId = ""
}
onRejected: {
editIndex = -1
evName.text = ""; evServer.text = ""; evVersion.selectedId = ""
}
}
// ── 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
})
}
// ── Переиспользуемые элементы в общем стиле окна ───────────────────────
component DarkCombo: ComboBox {
id: darkCombo
// Та же стрелка, что у комбобоксов профиля и версии на главном окне,
// вместо двойного шеврона Basic-стиля.
indicator: Image {
width: 10; height: 10
x: darkCombo.width - width - 12
y: (darkCombo.height - height) / 2
source: darkCombo.down ? "images/Profile_Box/Asset_23.svg"
: "images/Profile_Box/Asset_24.svg"
rotation: 180
sourceSize.width: 10; sourceSize.height: 10
fillMode: Image.PreserveAspectFit
}
contentItem: Text {
leftPadding: 10
rightPadding: darkCombo.indicator.width + 22
text: darkCombo.displayText
color: "#ffffff"
font: darkCombo.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: "#2a2a2a"
radius: 6
border.color: darkCombo.activeFocus ? "#91B315" : "#444444"
border.width: 1
}
delegate: ItemDelegate {
required property var modelData
required property int index
width: darkCombo.width
highlighted: darkCombo.highlightedIndex === index
contentItem: Text {
text: modelData
color: "#ffffff"
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
background: Rectangle { color: highlighted ? "#3a3a3a" : "#2a2a2a" }
}
popup: Popup {
y: darkCombo.height
width: darkCombo.width
implicitHeight: Math.min(contentItem.implicitHeight, 220)
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: darkCombo.delegateModel
currentIndex: darkCombo.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
}
background: Rectangle {
color: "#2a2a2a"
radius: 6
border.color: "#444444"
border.width: 1
}
}
}
// Выпадающий список версий: сверху установленные, ниже через разделитель
// весь каталог Mojang, сверху поле поиска.
//
// Это Item с Popup, а не ComboBox: editable-ComboBox привязывает поле ввода
// к currentText и запускает своё автодополнение, а при активном фильтре
// currentIndex перестаёт указывать в исходную модель.
component VersionCatalogCombo: Item {
id: vcombo
property var catalog: []
property string selectedId: ""
property string placeholder: qsTr("Выберите версию")
property string filterText: ""
signal aboutToOpen()
implicitHeight: 36
height: implicitHeight
// Фильтр по номеру версии. ~900 строк за нажатие — доли миллисекунды,
// а делегаты ListView создаёт только для видимых строк.
function filterEntries(needle) {
const query = needle.trim().toLowerCase()
if (query === "")
return vcombo.catalog
const out = []
for (var i = 0; i < vcombo.catalog.length; ++i) {
const entry = vcombo.catalog[i]
if (entry.selectable === false)
continue
if (entry.search.indexOf(query) !== -1)
out.push(entry)
}
return out
}
readonly property var visibleEntries: vcombo.filterEntries(vcombo.filterText)
function openPopup() {
vcombo.aboutToOpen()
vcombo.filterText = ""
vcomboFilter.text = ""
vcomboPopup.open()
vcomboFilter.forceActiveFocus()
}
Rectangle {
id: vcomboField
anchors.fill: parent
color: "#2a2a2a"
radius: 6
border.color: vcomboPopup.opened ? "#91B315" : "#444444"
border.width: 1
Text {
anchors.fill: parent
leftPadding: 10
rightPadding: 32
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
color: vcombo.selectedId === "" ? "#666666" : "#ffffff"
text: vcombo.selectedId === "" ? vcombo.placeholder : vcombo.selectedId
}
// Та же стрелка, что у остальных списков окна.
Image {
width: 10; height: 10
x: vcomboField.width - width - 12
y: (vcomboField.height - height) / 2
source: vcomboPopup.opened ? "images/Profile_Box/Asset_23.svg"
: "images/Profile_Box/Asset_24.svg"
rotation: 180
sourceSize.width: 10; sourceSize.height: 10
fillMode: Image.PreserveAspectFit
}
MouseArea {
anchors.fill: parent
onClicked: vcombo.openPopup()
}
}
Popup {
id: vcomboPopup
y: vcombo.height + 2
width: vcombo.width
padding: 1
height: 44 + Math.min(vcomboList.contentHeight, 240)
background: Rectangle {
color: "#2a2a2a"
radius: 6
border.color: "#444444"
border.width: 1
}
contentItem: Item {
TextField {
id: vcomboFilter
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 4
height: 30
placeholderText: qsTr("Поиск версии…")
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
color: "#232323"
radius: 5
border.color: vcomboFilter.activeFocus ? "#91B315" : "#444444"
border.width: 1
}
onTextChanged: {
vcombo.filterText = text
vcomboList.currentIndex = vcomboList.firstSelectable(0, 1)
}
Keys.onEscapePressed: vcomboPopup.close()
Keys.onDownPressed: vcomboList.step(1)
Keys.onUpPressed: vcomboList.step(-1)
Keys.onReturnPressed: vcomboList.acceptCurrent()
Keys.onEnterPressed: vcomboList.acceptCurrent()
}
ListView {
id: vcomboList
anchors.top: vcomboFilter.bottom
anchors.topMargin: 4
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
model: vcombo.visibleEntries
ScrollIndicator.vertical: ScrollIndicator {}
// Разделитель и служебные строки клавишами пропускаем.
function firstSelectable(from, delta) {
for (var i = from; i >= 0 && i < count; i += delta) {
if (model[i].selectable !== false)
return i
}
return -1
}
function step(delta) {
const next = firstSelectable(currentIndex + delta, delta)
if (next >= 0) {
currentIndex = next
positionViewAtIndex(next, ListView.Contain)
}
}
function acceptCurrent() {
if (currentIndex < 0 || currentIndex >= count)
return
const entry = model[currentIndex]
if (entry.selectable === false)
return
vcombo.selectedId = entry.id
vcomboPopup.close()
}
delegate: Item {
id: vcomboRow
required property var modelData
required property int index
width: vcomboList.width
height: modelData.kind === "separator" ? 26
: modelData.kind === "status" ? 30 : 34
// Разделитель между установленными и каталогом.
Item {
anchors.fill: parent
visible: vcomboRow.modelData.kind === "separator"
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
height: 1
color: "#3a3a3a"
}
Rectangle {
anchors.centerIn: parent
color: "#2a2a2a"
width: separatorLabel.implicitWidth + 12
height: 18
Text {
id: separatorLabel
anchors.centerIn: parent
text: vcomboRow.modelData.label
color: "#888888"
font.pixelSize: 10
}
}
}
// «Загрузка списка…» или сообщение о недоступности.
Text {
anchors.centerIn: parent
visible: vcomboRow.modelData.kind === "status"
text: vcomboRow.modelData.label
color: "#888888"
font.pixelSize: 11
font.italic: true
}
Rectangle {
anchors.fill: parent
visible: vcomboRow.modelData.selectable !== false
color: (vcomboRowArea.containsMouse || vcomboList.currentIndex === vcomboRow.index)
? "#3a3a3a" : "#2a2a2a"
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 100
elide: Text.ElideRight
text: vcomboRow.modelData.label
color: "#ffffff"
font.pixelSize: 12
}
Text {
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: vcomboRow.modelData.installed ? qsTr("установлена")
: vcomboRow.modelData.type
color: vcomboRow.modelData.installed ? "#91B315" : "#666666"
font.pixelSize: 10
}
MouseArea {
id: vcomboRowArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
vcombo.selectedId = vcomboRow.modelData.id
vcomboPopup.close()
}
}
}
}
}
}
}
}
// Подпись + поле ввода одной колонкой — используется в настройках.
component LabelledField: Column {
id: labelled
property alias text: field.text
property alias validator: field.validator
property string label: ""
property string placeholder: ""
spacing: 3
Text {
text: labelled.label
color: "#aaaaaa"
font.pixelSize: 11
}
TextField {
id: field
width: labelled.width
height: 32
placeholderText: labelled.placeholder
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
color: "#2a2a2a"
radius: 6
border.color: field.activeFocus ? "#91B315" : "#444444"
border.width: 1
}
}
}
}