1980 lines
73 KiB
QML
1980 lines
73 KiB
QML
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
|
||
}
|
||
}
|
||
}
|
||
}
|