Files
minecraft-launcher/BuildsDialog.qml
T

461 lines
18 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls 2.15
import QtQuick.Layouts 2.15
// Пользовательские сборки одним окном: слева список со сменой активной,
// справа карточка выбранной — имя, сервер, версия Minecraft и модлоадеры.
//
// Карточка сохраняет правки по ходу редактирования, отдельной кнопки
// «Сохранить» нет: иначе появляется неочевидное несохранённое состояние,
// пока пользователь переключается между сборками в левом списке.
Dialog {
id: buildsDialog
required property var backend
// Сборка, открытая в карточке справа. С активной не связана: активную
// меняет отдельная кнопка, чтобы случайный клик по списку не запускал
// архивацию .minecraft.
property int editIndex: -1
// Пока карточка заполняется, обработчики полей не должны писать обратно.
property bool loading: false
modal: true
padding: 0
width: 880
height: 600
closePolicy: buildsDialog.backend.switching ? Popup.NoAutoClose
: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: "#1e1e1e"
radius: 10
border.color: "#91B315"
border.width: 1
}
function selectBuild(index) {
const data = buildsDialog.backend.customBuildAt(index)
buildsDialog.loading = true
buildsDialog.editIndex = index
bdName.text = data.name || ""
bdServer.text = data.serverUrl || ""
bdMinecraft.selectedId = data.minecraftVersion || ""
loaderPanel.applyBuild(data.loader || "", data.loaderVersion || "")
buildsDialog.loading = false
buildsDialog.refreshStatus()
}
function commit(fields) {
if (buildsDialog.loading || buildsDialog.editIndex < 0)
return
buildsDialog.backend.updateCustomBuild(buildsDialog.editIndex, fields)
buildsDialog.refreshStatus()
}
function refreshStatus() {
if (buildsDialog.editIndex < 0) {
bdStatus.text = ""
return
}
const problems = buildsDialog.backend.checkInstallation(buildsDialog.editIndex)
bdStatus.ok = problems.length === 0
bdStatus.text = problems.length === 0
? qsTr("Всё на месте, сборку можно запускать")
: qsTr("Не хватает файлов (%1): %2").arg(problems.length).arg(problems[0])
}
function newBuildName() {
const taken = buildsDialog.backend.customBuildNames
for (var n = 1; ; ++n) {
const candidate = qsTr("Сборка %1").arg(n)
if (taken.indexOf(candidate) === -1)
return candidate
}
}
// Установка версии или модлоадера меняет комплектность сборки — строка
// состояния должна это заметить, не дожидаясь переоткрытия окна.
Connections {
target: buildsDialog.backend
function onInstalledVersionsChanged() { buildsDialog.refreshStatus() }
}
onAboutToShow: {
buildsDialog.backend.refreshVersionCatalog()
const names = buildsDialog.backend.customBuildNames
if (names.length === 0)
buildsDialog.editIndex = -1
else
buildsDialog.selectBuild(Math.max(0, buildsDialog.backend.activeBuildIndex))
}
header: Item {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: qsTr("Пользовательские сборки")
color: "#ffffff"
font.pixelSize: 17
font.bold: true
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: "#333333"
}
}
contentItem: RowLayout {
spacing: 0
// ── Список сборок ───────────────────────────────────────────────
Item {
Layout.preferredWidth: 260
Layout.fillHeight: true
enabled: !buildsDialog.backend.switching
ListView {
id: buildList
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: addBuildRow.top
anchors.margins: 8
clip: true
spacing: 2
model: buildsDialog.backend.customBuildNames
delegate: ItemDelegate {
id: buildRow
required property string modelData
required property int index
width: buildList.width
height: 40
contentItem: Item {
Text {
anchors.left: parent.left
anchors.leftMargin: 8
anchors.right: activeBadge.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: buildRow.modelData
color: "#ffffff"
elide: Text.ElideRight
font.pixelSize: 13
}
Text {
id: activeBadge
anchors.right: buildTrash.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
visible: buildRow.index === buildsDialog.backend.activeBuildIndex
text: qsTr("активна")
color: "#91B315"
font.pixelSize: 10
}
Image {
id: buildTrash
width: 16; height: 16
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
visible: buildRow.hovered
opacity: buildTrashArea.containsMouse ? 1.0 : 0.7
source: "images/Trash.svg"
sourceSize.width: 16; sourceSize.height: 16
fillMode: Image.PreserveAspectFit
MouseArea {
id: buildTrashArea
anchors.fill: parent
anchors.margins: -6
hoverEnabled: true
onClicked: {
buildsDialog.backend.removeCustomBuild(buildRow.index)
const left = buildsDialog.backend.customBuildNames.length
if (left === 0)
buildsDialog.editIndex = -1
else
buildsDialog.selectBuild(Math.min(buildRow.index, left - 1))
}
}
}
}
background: Rectangle {
color: (buildsDialog.editIndex === buildRow.index || buildRow.hovered)
? "#91B315" : "#232323"
radius: 5
}
onClicked: buildsDialog.selectBuild(buildRow.index)
}
ScrollIndicator.vertical: ScrollIndicator {}
}
Rectangle {
id: addBuildRow
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
height: 34
radius: 5
color: addBuildArea.containsPress ? "#2d2d2d" : "transparent"
Text {
anchors.centerIn: parent
text: qsTr("+ Новая сборка")
color: "#91B315"
font.pixelSize: 12
}
MouseArea {
id: addBuildArea
anchors.fill: parent
onClicked: {
buildsDialog.backend.addCustomBuild(buildsDialog.newBuildName(), "", "")
buildsDialog.selectBuild(buildsDialog.backend.customBuildNames.length - 1)
}
}
}
}
Rectangle {
Layout.preferredWidth: 1
Layout.fillHeight: true
color: "#333333"
}
// ── Карточка выбранной сборки ───────────────────────────────────
Item {
Layout.fillWidth: true
Layout.fillHeight: true
enabled: buildsDialog.editIndex >= 0 && !buildsDialog.backend.switching
opacity: enabled ? 1.0 : 0.4
Flickable {
anchors.fill: parent
anchors.margins: 20
contentHeight: detailsColumn.height
clip: true
boundsBehavior: Flickable.StopAtBounds
ScrollIndicator.vertical: ScrollIndicator {}
Column {
id: detailsColumn
width: parent.width
spacing: 12
LabelledField {
id: bdName
width: parent.width
label: qsTr("Название сборки")
placeholder: qsTr("как её видно в списке")
onEditingFinished: buildsDialog.commit({"name": bdName.text.trim()})
}
LabelledField {
id: bdServer
width: parent.width
label: qsTr("Адрес сервера")
placeholder: qsTr("host:port (необязательно)")
onEditingFinished: buildsDialog.commit({"serverUrl": bdServer.text.trim()})
}
Text {
text: qsTr("Версия Minecraft")
color: "#aaaaaa"
font.pixelSize: 11
}
VersionCatalogCombo {
id: bdMinecraft
width: parent.width
catalog: buildsDialog.backend.versionCatalog
onAboutToOpen: buildsDialog.backend.refreshVersionCatalog()
onSelectedIdChanged: buildsDialog.commit({"minecraftVersion":
bdMinecraft.selectedId})
}
Text {
text: qsTr("Модлоадер")
color: "#aaaaaa"
font.pixelSize: 11
}
// Лоадеры несовместимы между собой: игра запускается ровно с
// одним профилем versions/<id>. Поэтому отметка одного снимает
// остальные, а «ничего не отмечено» — это чистая ваниль.
Column {
id: loaderPanel
width: parent.width
spacing: 8
property var rows: [bdForge, bdFabric, bdNeoForge, bdQuilt]
function applyBuild(loader, loaderVersion) {
for (var i = 0; i < loaderPanel.rows.length; ++i)
loaderPanel.rows[i].applyBuild(loader, loaderVersion)
}
function selectedRow() {
for (var i = 0; i < loaderPanel.rows.length; ++i) {
if (loaderPanel.rows[i].checked)
return loaderPanel.rows[i]
}
return null
}
function keepOnly(row) {
for (var i = 0; i < loaderPanel.rows.length; ++i) {
if (loaderPanel.rows[i] === row)
continue
loaderPanel.rows[i].applyBuild("", "")
}
}
function commitSelection() {
const row = loaderPanel.selectedRow()
buildsDialog.commit({
"loader": row ? row.loaderKey : "",
"loaderVersion": row ? row.selectedVersion : "",
// Профиль появится только после установки: пока
// сборка запускается на чистой ванили.
"resolvedVersionId": ""
})
}
LoaderRow {
id: bdForge
width: parent.width
backend: buildsDialog.backend
loaderKey: "forge"
title: qsTr("Minecraft Forge")
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdForge)
onChanged: loaderPanel.commitSelection()
}
LoaderRow {
id: bdFabric
width: parent.width
backend: buildsDialog.backend
loaderKey: "fabric"
title: qsTr("Fabric Loader")
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdFabric)
onChanged: loaderPanel.commitSelection()
}
LoaderRow {
id: bdNeoForge
width: parent.width
backend: buildsDialog.backend
loaderKey: "neoforge"
title: qsTr("NeoForge")
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdNeoForge)
onChanged: loaderPanel.commitSelection()
}
LoaderRow {
id: bdQuilt
width: parent.width
backend: buildsDialog.backend
loaderKey: "quilt"
title: qsTr("Quilt Loader")
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdQuilt)
onChanged: loaderPanel.commitSelection()
}
}
Text {
id: bdStatus
property bool ok: false
width: parent.width
color: bdStatus.ok ? "#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: qsTr("Установить")
width: 140; height: 36
enabled: buildsDialog.editIndex >= 0 && !buildsDialog.backend.busy
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: buildsDialog.backend.installCustomBuild(buildsDialog.editIndex)
}
Button {
text: qsTr("Сделать активной")
width: 170; height: 36
enabled: buildsDialog.editIndex >= 0
&& buildsDialog.editIndex !== buildsDialog.backend.activeBuildIndex
&& !buildsDialog.backend.busy
opacity: enabled ? 1.0 : 0.45
contentItem: Text {
text: parent.text
color: "#ffffff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.pressed ? "#6a8510" : "#91B315"
radius: 6
}
onClicked: buildsDialog.backend.activeBuildIndex = buildsDialog.editIndex
}
Button {
text: qsTr("Закрыть")
width: 120; height: 36
enabled: !buildsDialog.backend.switching
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: buildsDialog.close()
}
}
}
}