new builds menu and modloaders implementation
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user