pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls 2.15 import QtQuick.Layouts 2.15 // Выбор версии Minecraft отдельным окном: слева категории, справа сами версии // с поиском сверху. // // Раньше это был выпадающий список на всю тысячу версий: найти в нём бету 1.7 // можно было только поиском по точному номеру. Категории делят список на // обозримые части, а поиск работает внутри выбранной. Dialog { id: picker required property var backend // Версия, с которой окно открылось: по «Отмене» выбор к ней и возвращается. property string selectedId: "" property string category: "release" property string filterText: "" property bool installedOnly: false // Выбранная версия подтверждена кнопкой или двойным щелчком. Имя не // accepted(): такой сигнал у Dialog уже есть, и переопределить его нельзя. signal versionChosen(string versionId) modal: true padding: 0 width: 720 height: 480 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: Rectangle { color: "#1e1e1e" radius: 10 border.color: "#91B315" border.width: 1 } readonly property var categories: [ { key: "release", title: qsTr("Релизы") }, { key: "snapshot", title: qsTr("Снапшоты") }, { key: "old_beta", title: qsTr("Беты") }, { key: "old_alpha", title: qsTr("Альфы") }, { key: "other", title: qsTr("Прочие") } ] // Каталог приходит из C++ уже отсортированным (новые сверху), поэтому здесь // только отбор — порядок не трогаем. readonly property var visibleEntries: { const query = picker.filterText.trim().toLowerCase() const source = picker.backend.versionCatalog const out = [] for (var i = 0; i < source.length; ++i) { const entry = source[i] if (entry.category !== picker.category) continue if (picker.installedOnly && !entry.installed) continue if (query !== "" && entry.search.indexOf(query) === -1) continue out.push(entry) } return out } function openFor(versionId) { picker.selectedId = versionId picker.filterText = "" searchField.text = "" // Открываемся на категории текущей версии, а не всегда на релизах. picker.category = picker.categoryOf(versionId) picker.backend.refreshVersionCatalog() picker.open() picker.revealSelected() } function categoryOf(versionId) { const source = picker.backend.versionCatalog for (var i = 0; i < source.length; ++i) { if (source[i].id === versionId) return source[i].category } return "release" } function indexOfSelected() { const list = picker.visibleEntries for (var i = 0; i < list.length; ++i) { if (list[i].id === picker.selectedId) return i } return -1 } function revealSelected() { const index = picker.indexOfSelected() versionList.currentIndex = index if (index >= 0) versionList.positionViewAtIndex(index, ListView.Center) } function acceptSelection() { if (picker.selectedId === "") return picker.versionChosen(picker.selectedId) picker.close() } function catalogHas(versionId) { const source = picker.backend.versionCatalog for (var i = 0; i < source.length; ++i) { if (source[i].id === versionId) return true } return false } // Спрашиваем до удаления: версию придётся качать заново, а профили // модлоадеров поверх неё — переустанавливать. Id запоминаем здесь: // к моменту ответа строка под курсором может быть уже другой. function askRemove(versionId) { const info = picker.backend.versionRemovalInfo(versionId) if (!info || info.installed !== true) return removeConfirm.versionId = versionId removeConfirm.sizeMb = info.sizeMb || 0 removeConfirm.dependents = info.dependents || [] removeConfirm.builds = info.builds || [] removeConfirm.open() } function performRemove(versionId) { picker.backend.removeVersion(versionId) // Профиль модлоадера был в каталоге только потому, что установлен: // после удаления строка исчезает совсем, и выбор надо снять. if (picker.selectedId === versionId && !picker.catalogHas(versionId)) picker.selectedId = "" picker.revealSelected() } Dialog { id: removeConfirm property string versionId: "" property int sizeMb: 0 property var dependents: [] property var builds: [] modal: true padding: 0 width: 420 parent: picker.parent x: (picker.parent.width - width) / 2 y: (picker.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: qsTr("Удалить версию?") 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: removeConfirm.sizeMb > 0 ? qsTr("Файлы версии «%1» будут удалены из versions/ — освободится около %2 МБ. Скачать её заново можно в любой момент.") .arg(removeConfirm.versionId).arg(removeConfirm.sizeMb) : qsTr("Файлы версии «%1» будут удалены из versions/. Скачать её заново можно в любой момент.") .arg(removeConfirm.versionId) } Text { x: 20 width: parent.width - 40 visible: removeConfirm.dependents.length > 0 wrapMode: Text.Wrap color: "#cc6666" font.pixelSize: 12 text: qsTr("На эту версию опираются установленные профили модлоадеров: %1. Без неё они не запустятся — их придётся переустановить.") .arg(removeConfirm.dependents.join(", ")) } Text { x: 20 width: parent.width - 40 visible: removeConfirm.builds.length > 0 wrapMode: Text.Wrap color: "#cc6666" font.pixelSize: 12 text: qsTr("Версию используют сборки: %1. Файлы понадобится скачать заново кнопкой «Установить».") .arg(removeConfirm.builds.join(", ")) } Text { x: 20 width: parent.width - 40 wrapMode: Text.Wrap color: "#888888" font.pixelSize: 12 text: qsTr("Библиотеки и ресурсы в libraries/ и assets/ общие для всех версий и остаются на месте.") } } 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: 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: qsTr("Удалить") 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 id = removeConfirm.versionId removeConfirm.close() picker.performRemove(id) } } } } onClosed: removeConfirm.versionId = "" } header: Item { implicitHeight: 52 Text { anchors.centerIn: parent text: qsTr("Версия Minecraft") color: "#ffffff" font.pixelSize: 17 font.bold: true } Rectangle { anchors.bottom: parent.bottom width: parent.width height: 1 color: "#333333" } } contentItem: RowLayout { spacing: 0 // ── Категории ─────────────────────────────────────────────────── Rectangle { Layout.preferredWidth: 150 Layout.fillHeight: true color: "transparent" Column { anchors.fill: parent anchors.margins: 8 spacing: 2 Repeater { model: picker.categories delegate: Rectangle { id: categoryRow required property var modelData width: parent.width height: 34 radius: 5 color: picker.category === categoryRow.modelData.key ? "#91B315" : categoryArea.containsMouse ? "#2d2d2d" : "transparent" Text { anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter text: categoryRow.modelData.title color: "#ffffff" font.pixelSize: 13 } MouseArea { id: categoryArea anchors.fill: parent hoverEnabled: true onClicked: { picker.category = categoryRow.modelData.key picker.revealSelected() } } } } } } Rectangle { Layout.preferredWidth: 1 Layout.fillHeight: true color: "#333333" } // ── Версии ────────────────────────────────────────────────────── Item { Layout.fillWidth: true Layout.fillHeight: true TextField { id: searchField anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 8 height: 32 placeholderText: qsTr("Поиск версии…") color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#232323" radius: 5 border.color: searchField.activeFocus ? "#91B315" : "#444444" border.width: 1 } onTextChanged: { picker.filterText = text versionList.currentIndex = picker.indexOfSelected() } Keys.onEscapePressed: picker.close() Keys.onReturnPressed: picker.acceptSelection() Keys.onEnterPressed: picker.acceptSelection() Keys.onDownPressed: versionList.step(1) Keys.onUpPressed: versionList.step(-1) } CheckBox { id: installedOnlyBox anchors.left: parent.left anchors.leftMargin: 8 anchors.bottom: parent.bottom anchors.bottomMargin: 4 height: 28 checked: picker.installedOnly onToggled: { picker.installedOnly = checked picker.revealSelected() } indicator: Rectangle { implicitWidth: 16 implicitHeight: 16 x: 0 y: (installedOnlyBox.height - height) / 2 radius: 4 color: installedOnlyBox.checked ? "#91B315" : "#2a2a2a" border.color: installedOnlyBox.checked ? "#91B315" : "#444444" border.width: 1 Text { anchors.centerIn: parent visible: installedOnlyBox.checked text: "✓" color: "#1e1e1e" font.pixelSize: 12 font.bold: true } } contentItem: Text { leftPadding: installedOnlyBox.indicator.width + 8 text: qsTr("Только установленные") color: "#aaaaaa" font.pixelSize: 11 verticalAlignment: Text.AlignVCenter } } ListView { id: versionList anchors.top: searchField.bottom anchors.topMargin: 6 anchors.left: parent.left anchors.right: parent.right anchors.bottom: installedOnlyBox.top anchors.leftMargin: 8 anchors.rightMargin: 8 clip: true spacing: 1 model: picker.visibleEntries ScrollIndicator.vertical: ScrollIndicator {} function step(delta) { const next = versionList.currentIndex + delta if (next < 0 || next >= versionList.count) return versionList.currentIndex = next versionList.positionViewAtIndex(next, ListView.Contain) picker.selectedId = picker.visibleEntries[next].id } delegate: Rectangle { id: versionRow required property var modelData required property int index width: versionList.width height: 32 radius: 4 color: picker.selectedId === versionRow.modelData.id ? "#91B315" : versionArea.containsMouse ? "#2d2d2d" : "transparent" Text { anchors.left: parent.left anchors.leftMargin: 10 anchors.right: rightMarks.left anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter text: versionRow.modelData.label color: "#ffffff" elide: Text.ElideRight font.pixelSize: 13 } Row { id: rightMarks anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter spacing: 8 Text { visible: versionRow.modelData.installed text: "✓" color: picker.selectedId === versionRow.modelData.id ? "#1e1e1e" : "#91B315" font.pixelSize: 13 font.bold: true anchors.verticalCenter: parent.verticalCenter } // Версия весит от десятков мегабайт, а галочка рядом — // единственное место, где видно, что именно скачано. Image { visible: versionRow.modelData.installed source: "images/Trash.svg" width: 14 height: 14 fillMode: Image.PreserveAspectFit anchors.verticalCenter: parent.verticalCenter opacity: trashArea.containsMouse ? 1.0 : 0.55 MouseArea { id: trashArea anchors.fill: parent anchors.margins: -5 hoverEnabled: true onClicked: picker.askRemove(versionRow.modelData.id) } } } MouseArea { id: versionArea anchors.fill: parent anchors.rightMargin: 40 hoverEnabled: true onClicked: { picker.selectedId = versionRow.modelData.id versionList.currentIndex = versionRow.index } onDoubleClicked: { picker.selectedId = versionRow.modelData.id picker.acceptSelection() } } } } // Пустая категория, ничего не нашлось или манифест ещё не пришёл. Text { anchors.centerIn: versionList width: versionList.width - 20 visible: picker.visibleEntries.length === 0 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap color: "#888888" font.pixelSize: 12 font.italic: true text: picker.backend.catalogLoading ? qsTr("Загрузка списка версий…") : picker.backend.versionCatalog.length === 0 ? qsTr("Список версий недоступен — проверьте соединение") : qsTr("Ничего не найдено") } } } footer: Item { implicitHeight: 60 Rectangle { anchors.top: parent.top width: parent.width height: 1 color: "#333333" } Text { anchors.left: parent.left anchors.leftMargin: 20 anchors.verticalCenter: parent.verticalCenter width: parent.width - 300 elide: Text.ElideRight color: picker.selectedId === "" ? "#666666" : "#91B315" font.pixelSize: 13 text: picker.selectedId === "" ? qsTr("Версия не выбрана") : picker.selectedId } Row { anchors.right: parent.right anchors.rightMargin: 20 anchors.verticalCenter: parent.verticalCenter spacing: 12 Button { text: qsTr("Отмена") 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: picker.close() } Button { text: qsTr("Выбрать") width: 110; height: 36 enabled: picker.selectedId !== "" 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: picker.acceptSelection() } } } onOpened: searchField.forceActiveFocus() }