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 askRemove(index) { const info = buildsDialog.backend.customBuildRemovalInfo(index) if (!info || info.name === undefined) return removeConfirm.buildIndex = index removeConfirm.buildName = info.name removeConfirm.hasArchive = info.hasArchive === true removeConfirm.active = info.active === true removeConfirm.lastOne = info.lastOne === true removeConfirm.open() } function performRemove(index) { buildsDialog.backend.removeCustomBuild(index) const left = buildsDialog.backend.customBuildNames.length if (left === 0) buildsDialog.editIndex = -1 else buildsDialog.selectBuild(Math.min(index, left - 1)) } 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 ? Loc.t.build.status.ready : Loc.t.build.error.missingFiles.arg(problems.length).arg(problems[0]) } function newBuildName() { const taken = buildsDialog.backend.customBuildNames for (var n = 1; ; ++n) { const candidate = Loc.t.build.title.numbered.arg(n) if (taken.indexOf(candidate) === -1) return candidate } } Dialog { id: removeConfirm property int buildIndex: -1 property string buildName: "" property bool hasArchive: false property bool active: false property bool lastOne: false modal: true padding: 0 width: 420 parent: buildsDialog.parent x: (buildsDialog.parent.width - width) / 2 y: (buildsDialog.parent.height - height) / 2 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: Rectangle { color: "#1e1e1e" radius: 10 border.color: "#cc6666" border.width: 1 } header: Item { implicitHeight: 52 Text { anchors.centerIn: parent text: Loc.t.build.title.removeConfirm 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 Text { x: 20 width: parent.width - 40 wrapMode: Text.Wrap color: "#ffffff" font.pixelSize: 13 text: Loc.t.build.hint.removeIrreversible .arg(removeConfirm.buildName) } Text { x: 20 width: parent.width - 40 visible: removeConfirm.hasArchive wrapMode: Text.Wrap color: "#cc6666" font.pixelSize: 12 text: Loc.t.build.warning.archiveGoesToo } Text { x: 20 width: parent.width - 40 visible: removeConfirm.active && !removeConfirm.lastOne wrapMode: Text.Wrap color: "#cc6666" font.pixelSize: 12 text: Loc.t.build.warning.removingActive } Text { x: 20 width: parent.width - 40 visible: removeConfirm.lastOne wrapMode: Text.Wrap color: "#888888" font.pixelSize: 12 text: Loc.t.build.hint.removingLast } } footer: Item { implicitHeight: 60 Rectangle { anchors.top: parent.top width: parent.width height: 1 color: "#333333" } Row { anchors.centerIn: parent spacing: 12 Button { text: Loc.t.common.button.cancel width: 130; 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: removeConfirm.close() } Button { text: Loc.t.common.button.remove width: 130; height: 36 contentItem: Text { text: parent.text color: "#ffffff" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { color: parent.pressed ? "#8a3a3a" : "#663333" radius: 6 border.color: "#cc6666" border.width: 1 } onClicked: { const index = removeConfirm.buildIndex removeConfirm.close() buildsDialog.performRemove(index) } } } } onClosed: removeConfirm.buildIndex = -1 } VersionPickerDialog { id: versionPicker backend: buildsDialog.backend parent: buildsDialog.parent x: (buildsDialog.parent.width - width) / 2 y: (buildsDialog.parent.height - height) / 2 onVersionChosen: (versionId) => bdMinecraft.selectedId = versionId } // Установка версии или модлоадера меняет комплектность сборки — строка // состояния должна это заметить, не дожидаясь переоткрытия окна. 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: Loc.t.common.title.customBuilds 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: Loc.t.build.label.active 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.askRemove(buildRow.index) } } } 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: Loc.t.build.button.new 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: Loc.t.build.label.name placeholder: Loc.t.build.placeholder.name onEditingFinished: buildsDialog.commit({"name": bdName.text.trim()}) } LabelledField { id: bdServer width: parent.width label: Loc.t.build.label.serverAddress placeholder: Loc.t.build.placeholder.serverAddress onEditingFinished: buildsDialog.commit({"serverUrl": bdServer.text.trim()}) } Text { text: Loc.t.common.label.minecraftVersion color: "#aaaaaa" font.pixelSize: 11 } // Само поле только показывает выбор: версий около тысячи, и // разбираться в них удобнее в отдельном окне с категориями. Rectangle { id: bdMinecraft property string selectedId: "" width: parent.width height: 36 radius: 6 color: "#2a2a2a" border.color: versionFieldArea.containsMouse ? "#91B315" : "#444444" border.width: 1 Text { anchors.fill: parent leftPadding: 10 rightPadding: 110 verticalAlignment: Text.AlignVCenter elide: Text.ElideRight color: bdMinecraft.selectedId === "" ? "#666666" : "#ffffff" text: bdMinecraft.selectedId === "" ? Loc.t.build.button.pickVersion : bdMinecraft.selectedId } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: Loc.t.common.button.choose color: "#91B315" font.pixelSize: 12 } MouseArea { id: versionFieldArea anchors.fill: parent hoverEnabled: true onClicked: versionPicker.openFor(bdMinecraft.selectedId) } onSelectedIdChanged: buildsDialog.commit({"minecraftVersion": bdMinecraft.selectedId}) } Text { text: Loc.t.build.label.modLoader color: "#aaaaaa" font.pixelSize: 11 } // Лоадеры несовместимы между собой: игра запускается ровно с // одним профилем versions/. Поэтому отметка одного снимает // остальные, а «ничего не отмечено» — это чистая ваниль. 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: Loc.t.loader.name.forge gameVersion: bdMinecraft.selectedId onUserChecked: loaderPanel.keepOnly(bdForge) onChanged: loaderPanel.commitSelection() } LoaderRow { id: bdFabric width: parent.width backend: buildsDialog.backend loaderKey: "fabric" title: Loc.t.loader.name.fabric gameVersion: bdMinecraft.selectedId onUserChecked: loaderPanel.keepOnly(bdFabric) onChanged: loaderPanel.commitSelection() } LoaderRow { id: bdNeoForge width: parent.width backend: buildsDialog.backend loaderKey: "neoforge" title: Loc.t.loader.name.neoforge gameVersion: bdMinecraft.selectedId onUserChecked: loaderPanel.keepOnly(bdNeoForge) onChanged: loaderPanel.commitSelection() } LoaderRow { id: bdQuilt width: parent.width backend: buildsDialog.backend loaderKey: "quilt" title: Loc.t.loader.name.quilt 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: Loc.t.common.button.install 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: Loc.t.build.button.makeActive 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: Loc.t.common.button.close 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() } } } }