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/. Поэтому отметка одного снимает // остальные, а «ничего не отмечено» — это чистая ваниль. 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() } } } }