builds fixes
This commit is contained in:
+214
-11
@@ -48,6 +48,30 @@ Dialog {
|
||||
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
|
||||
@@ -76,6 +100,157 @@ Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
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: 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: qsTr("Сборка «%1» будет удалена без возможности восстановления.")
|
||||
.arg(removeConfirm.buildName)
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 20
|
||||
width: parent.width - 40
|
||||
visible: removeConfirm.hasArchive
|
||||
wrapMode: Text.Wrap
|
||||
color: "#cc6666"
|
||||
font.pixelSize: 12
|
||||
text: qsTr("Вместе с ней удалится её архив: моды, конфиги и миры этой сборки восстановить будет нельзя.")
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 20
|
||||
width: parent.width - 40
|
||||
visible: removeConfirm.active && !removeConfirm.lastOne
|
||||
wrapMode: Text.Wrap
|
||||
color: "#cc6666"
|
||||
font.pixelSize: 12
|
||||
text: qsTr("Сборка сейчас активна: содержимое .minecraft принадлежит ей и будет очищено, а на его место развернётся следующая сборка.")
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 20
|
||||
width: parent.width - 40
|
||||
visible: removeConfirm.lastOne
|
||||
wrapMode: Text.Wrap
|
||||
color: "#888888"
|
||||
font.pixelSize: 12
|
||||
text: qsTr("Это последняя сборка. Содержимое .minecraft останется на месте.")
|
||||
}
|
||||
}
|
||||
|
||||
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 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 {
|
||||
@@ -176,14 +351,7 @@ Dialog {
|
||||
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))
|
||||
}
|
||||
onClicked: buildsDialog.askRemove(buildRow.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,11 +444,46 @@ Dialog {
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
VersionCatalogCombo {
|
||||
// Само поле только показывает выбор: версий около тысячи, и
|
||||
// разбираться в них удобнее в отдельном окне с категориями.
|
||||
Rectangle {
|
||||
id: bdMinecraft
|
||||
property string selectedId: ""
|
||||
|
||||
width: parent.width
|
||||
catalog: buildsDialog.backend.versionCatalog
|
||||
onAboutToOpen: buildsDialog.backend.refreshVersionCatalog()
|
||||
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 === "" ? qsTr("Выберите версию")
|
||||
: bdMinecraft.selectedId
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Выбрать…")
|
||||
color: "#91B315"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: versionFieldArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: versionPicker.openFor(bdMinecraft.selectedId)
|
||||
}
|
||||
|
||||
onSelectedIdChanged: buildsDialog.commit({"minecraftVersion":
|
||||
bdMinecraft.selectedId})
|
||||
}
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ qt_add_qml_module(appMinecraft_launcher
|
||||
QML_FILES
|
||||
Main.qml
|
||||
DarkCombo.qml
|
||||
VersionCatalogCombo.qml
|
||||
VersionPickerDialog.qml
|
||||
LabelledField.qml
|
||||
BuildsDialog.qml
|
||||
LoaderRow.qml
|
||||
|
||||
+25
-34
@@ -42,35 +42,35 @@ Column {
|
||||
return -1
|
||||
}
|
||||
|
||||
// Самая свежая стабильная сборка, а если стабильных нет — просто самая
|
||||
// свежая. Список отсортирован новыми вперёд, поэтому хватает первого
|
||||
// совпадения.
|
||||
function preferredIndex() {
|
||||
for (var i = 0; i < loaderRow.entries.length; ++i) {
|
||||
if (loaderRow.entries[i].recommended)
|
||||
return i
|
||||
}
|
||||
for (var j = 0; j < loaderRow.entries.length; ++j) {
|
||||
if (loaderRow.entries[j].stable)
|
||||
return j
|
||||
}
|
||||
return loaderRow.entries.length > 0 ? 0 : -1
|
||||
// Единственное место, где меняются entries и selectedVersion. Через него
|
||||
// проходят все три пути — кэш, ответ сети и заполнение из сборки, — потому
|
||||
// что выбранная версия обязана существовать в списке под текущую версию
|
||||
// игры. Раньше проверял только путь из сети, и версия лоадера от прошлой
|
||||
// версии игры доживала до сохранения сборки.
|
||||
function applyEntries(list) {
|
||||
loaderRow.entries = list
|
||||
|
||||
// Список отсортирован новыми вперёд, поэтому максимально доступная
|
||||
// версия для этой версии игры — всегда первая строка.
|
||||
if (loaderRow.versionIndex(loaderRow.selectedVersion) < 0) {
|
||||
const fallback = list.length > 0 ? list[0].version : ""
|
||||
const changedValue = loaderRow.selectedVersion !== fallback
|
||||
loaderRow.selectedVersion = fallback
|
||||
if (changedValue && !loaderRow.applying)
|
||||
loaderRow.changed()
|
||||
}
|
||||
|
||||
function syncCombo() {
|
||||
const index = loaderRow.versionIndex(loaderRow.selectedVersion)
|
||||
combo.currentIndex = index
|
||||
combo.currentIndex = loaderRow.versionIndex(loaderRow.selectedVersion)
|
||||
}
|
||||
|
||||
function reload() {
|
||||
if (!box.checked || loaderRow.gameVersion === "") {
|
||||
loaderRow.entries = []
|
||||
loaderRow.applyEntries([])
|
||||
return
|
||||
}
|
||||
// Сначала из кэша — список появляется мгновенно, — потом обновление сети.
|
||||
loaderRow.entries = loaderRow.backend.loaderVersions(loaderRow.loaderKey,
|
||||
loaderRow.gameVersion)
|
||||
loaderRow.syncCombo()
|
||||
loaderRow.applyEntries(loaderRow.backend.loaderVersions(loaderRow.loaderKey,
|
||||
loaderRow.gameVersion))
|
||||
loaderRow.backend.refreshLoaderVersions(loaderRow.loaderKey, loaderRow.gameVersion)
|
||||
}
|
||||
|
||||
@@ -78,17 +78,17 @@ Column {
|
||||
function applyBuild(loader, loaderVersion) {
|
||||
loaderRow.applying = true
|
||||
box.checked = loader === loaderRow.loaderKey
|
||||
// Версию из сборки берём как пожелание: если под выбранную версию игры
|
||||
// её нет, applyEntries() заменит её на максимально доступную.
|
||||
loaderRow.selectedVersion = box.checked ? loaderVersion : ""
|
||||
loaderRow.reload()
|
||||
loaderRow.applying = false
|
||||
}
|
||||
|
||||
onGameVersionChanged: {
|
||||
// Сборка лоадера привязана к версии игры — при её смене выбор
|
||||
// сбрасывается, и список перезапрашивается под новую версию.
|
||||
loaderRow.entries = []
|
||||
// Сборка лоадера привязана к версии игры: под новой версией прежний
|
||||
// выбор недействителен, и список перезапрашивается заново.
|
||||
loaderRow.selectedVersion = ""
|
||||
combo.currentIndex = -1
|
||||
loaderRow.reload()
|
||||
}
|
||||
|
||||
@@ -97,16 +97,7 @@ Column {
|
||||
function onLoaderVersionsChanged(key, game) {
|
||||
if (key !== loaderRow.loaderKey || game !== loaderRow.gameVersion || !box.checked)
|
||||
return
|
||||
loaderRow.entries = loaderRow.backend.loaderVersions(key, game)
|
||||
if (loaderRow.selectedVersion === "" || loaderRow.versionIndex(loaderRow.selectedVersion) < 0) {
|
||||
const preferred = loaderRow.preferredIndex()
|
||||
if (preferred >= 0) {
|
||||
loaderRow.selectedVersion = loaderRow.entries[preferred].version
|
||||
if (!loaderRow.applying)
|
||||
loaderRow.changed()
|
||||
}
|
||||
}
|
||||
loaderRow.syncCombo()
|
||||
loaderRow.applyEntries(loaderRow.backend.loaderVersions(key, game))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,9 +161,17 @@ Window {
|
||||
anchors.bottomMargin: -218
|
||||
rotation: 0
|
||||
editable: false
|
||||
hoverEnabled: true
|
||||
|
||||
model: backend.profileNames
|
||||
|
||||
// Пока список закрыт: открытый popup накрывает кнопку, и подсказка
|
||||
// висела бы поверх собственного меню.
|
||||
ToolTip.visible: profileBox.hovered && !profileBox.down
|
||||
ToolTip.text: backend.profileNames.length === 0
|
||||
? qsTr("Профили игрока — добавьте первый профиль")
|
||||
: qsTr("Профиль игрока: %1").arg(profileBox.displayText)
|
||||
|
||||
contentItem: Text {
|
||||
id: profileText
|
||||
text: profileBox.displayText
|
||||
@@ -427,8 +435,12 @@ Window {
|
||||
|
||||
background: Image {
|
||||
anchors.fill: parent
|
||||
source: settingsButton.pressed ? "images/Options/Options_Pressed.svg"
|
||||
: settingsButton.hovered ? "images/Options/Options_active.svg"
|
||||
// Ассеты Options названы наоборот относительно Folder: зелёный
|
||||
// #637a10 лежит в Options_Pressed.svg, а Options_active.svg —
|
||||
// тёмный, как Idle. Берём их по цвету, а не по имени, чтобы
|
||||
// наведение выглядело так же, как у кнопки папки модов.
|
||||
source: settingsButton.pressed ? "images/Options/Options_active.svg"
|
||||
: settingsButton.hovered ? "images/Options/Options_Pressed.svg"
|
||||
: "images/Options/Options_Idle.svg"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
// Выпадающий список версий: сверху установленные, ниже через разделитель
|
||||
// весь каталог Mojang, сверху поле поиска.
|
||||
//
|
||||
// Это Item с Popup, а не ComboBox: editable-ComboBox привязывает поле ввода
|
||||
// к currentText и запускает своё автодополнение, а при активном фильтре
|
||||
// currentIndex перестаёт указывать в исходную модель.
|
||||
Item {
|
||||
id: vcombo
|
||||
|
||||
property var catalog: []
|
||||
property string selectedId: ""
|
||||
property string placeholder: qsTr("Выберите версию")
|
||||
property string filterText: ""
|
||||
signal aboutToOpen()
|
||||
|
||||
implicitHeight: 36
|
||||
height: implicitHeight
|
||||
|
||||
// Фильтр по номеру версии. ~900 строк за нажатие — доли миллисекунды,
|
||||
// а делегаты ListView создаёт только для видимых строк.
|
||||
function filterEntries(needle) {
|
||||
const query = needle.trim().toLowerCase()
|
||||
if (query === "")
|
||||
return vcombo.catalog
|
||||
const out = []
|
||||
for (var i = 0; i < vcombo.catalog.length; ++i) {
|
||||
const entry = vcombo.catalog[i]
|
||||
if (entry.selectable === false)
|
||||
continue
|
||||
if (entry.search.indexOf(query) !== -1)
|
||||
out.push(entry)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
readonly property var visibleEntries: vcombo.filterEntries(vcombo.filterText)
|
||||
|
||||
function openPopup() {
|
||||
vcombo.aboutToOpen()
|
||||
vcombo.filterText = ""
|
||||
vcomboFilter.text = ""
|
||||
vcomboPopup.open()
|
||||
vcomboFilter.forceActiveFocus()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: vcomboField
|
||||
anchors.fill: parent
|
||||
color: "#2a2a2a"
|
||||
radius: 6
|
||||
border.color: vcomboPopup.opened ? "#91B315" : "#444444"
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
leftPadding: 10
|
||||
rightPadding: 32
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
color: vcombo.selectedId === "" ? "#666666" : "#ffffff"
|
||||
text: vcombo.selectedId === "" ? vcombo.placeholder : vcombo.selectedId
|
||||
}
|
||||
|
||||
// Та же стрелка, что у остальных списков окна.
|
||||
Image {
|
||||
width: 10; height: 10
|
||||
x: vcomboField.width - width - 12
|
||||
y: (vcomboField.height - height) / 2
|
||||
source: vcomboPopup.opened ? "images/Profile_Box/Asset_23.svg"
|
||||
: "images/Profile_Box/Asset_24.svg"
|
||||
rotation: 180
|
||||
sourceSize.width: 10; sourceSize.height: 10
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: vcombo.openPopup()
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: vcomboPopup
|
||||
y: vcombo.height + 2
|
||||
width: vcombo.width
|
||||
padding: 1
|
||||
height: 44 + Math.min(vcomboList.contentHeight, 240)
|
||||
|
||||
background: Rectangle {
|
||||
color: "#2a2a2a"
|
||||
radius: 6
|
||||
border.color: "#444444"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
TextField {
|
||||
id: vcomboFilter
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 4
|
||||
height: 30
|
||||
placeholderText: qsTr("Поиск версии…")
|
||||
color: "#ffffff"
|
||||
placeholderTextColor: "#666666"
|
||||
background: Rectangle {
|
||||
color: "#232323"
|
||||
radius: 5
|
||||
border.color: vcomboFilter.activeFocus ? "#91B315" : "#444444"
|
||||
border.width: 1
|
||||
}
|
||||
onTextChanged: {
|
||||
vcombo.filterText = text
|
||||
vcomboList.currentIndex = vcomboList.firstSelectable(0, 1)
|
||||
}
|
||||
Keys.onEscapePressed: vcomboPopup.close()
|
||||
Keys.onDownPressed: vcomboList.step(1)
|
||||
Keys.onUpPressed: vcomboList.step(-1)
|
||||
Keys.onReturnPressed: vcomboList.acceptCurrent()
|
||||
Keys.onEnterPressed: vcomboList.acceptCurrent()
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: vcomboList
|
||||
anchors.top: vcomboFilter.bottom
|
||||
anchors.topMargin: 4
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
clip: true
|
||||
model: vcombo.visibleEntries
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
|
||||
// Разделитель и служебные строки клавишами пропускаем.
|
||||
function firstSelectable(from, delta) {
|
||||
for (var i = from; i >= 0 && i < count; i += delta) {
|
||||
if (model[i].selectable !== false)
|
||||
return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
function step(delta) {
|
||||
const next = firstSelectable(currentIndex + delta, delta)
|
||||
if (next >= 0) {
|
||||
currentIndex = next
|
||||
positionViewAtIndex(next, ListView.Contain)
|
||||
}
|
||||
}
|
||||
function acceptCurrent() {
|
||||
if (currentIndex < 0 || currentIndex >= count)
|
||||
return
|
||||
const entry = model[currentIndex]
|
||||
if (entry.selectable === false)
|
||||
return
|
||||
vcombo.selectedId = entry.id
|
||||
vcomboPopup.close()
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: vcomboRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: vcomboList.width
|
||||
height: modelData.kind === "separator" ? 26
|
||||
: modelData.kind === "status" ? 30 : 34
|
||||
|
||||
// Разделитель между установленными и каталогом.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: vcomboRow.modelData.kind === "separator"
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 8
|
||||
height: 1
|
||||
color: "#3a3a3a"
|
||||
}
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
color: "#2a2a2a"
|
||||
width: separatorLabel.implicitWidth + 12
|
||||
height: 18
|
||||
Text {
|
||||
id: separatorLabel
|
||||
anchors.centerIn: parent
|
||||
text: vcomboRow.modelData.label
|
||||
color: "#888888"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// «Загрузка списка…» или сообщение о недоступности.
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: vcomboRow.modelData.kind === "status"
|
||||
text: vcomboRow.modelData.label
|
||||
color: "#888888"
|
||||
font.pixelSize: 11
|
||||
font.italic: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: vcomboRow.modelData.selectable !== false
|
||||
color: (vcomboRowArea.containsMouse || vcomboList.currentIndex === vcomboRow.index)
|
||||
? "#3a3a3a" : "#2a2a2a"
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 100
|
||||
elide: Text.ElideRight
|
||||
text: vcomboRow.modelData.label
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: vcomboRow.modelData.installed ? qsTr("установлена")
|
||||
: vcomboRow.modelData.type
|
||||
color: vcomboRow.modelData.installed ? "#91B315" : "#666666"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: vcomboRowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
vcombo.selectedId = vcomboRow.modelData.id
|
||||
vcomboPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
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()
|
||||
}
|
||||
|
||||
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: installedMark.left
|
||||
anchors.rightMargin: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: versionRow.modelData.label
|
||||
color: "#ffffff"
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
Text {
|
||||
id: installedMark
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: versionRow.modelData.installed
|
||||
text: "✓"
|
||||
color: picker.selectedId === versionRow.modelData.id ? "#1e1e1e" : "#91B315"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: versionArea
|
||||
anchors.fill: parent
|
||||
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()
|
||||
}
|
||||
@@ -352,6 +352,28 @@ void BuildSwitcher::fail(const QString &message, bool gameDirIntact)
|
||||
emit failed(toId, message, gameDirIntact);
|
||||
}
|
||||
|
||||
// ── Удаление сборки ────────────────────────────────────────────────────────
|
||||
|
||||
bool BuildSwitcher::forgetBuild(int buildId)
|
||||
{
|
||||
if (buildId < 0)
|
||||
return true;
|
||||
|
||||
// Запись убираем в любом случае: даже если файл не удалось стереть, ссылаться
|
||||
// на архив несуществующей сборки нельзя — иначе он «воскреснет» под новой
|
||||
// сборкой с тем же id.
|
||||
QJsonObject index = readIndex();
|
||||
QJsonObject builds = index.value(QStringLiteral("builds")).toObject();
|
||||
builds.remove(QString::number(buildId));
|
||||
index.insert(QStringLiteral("builds"), builds);
|
||||
if (index.value(QStringLiteral("active")).toInt(-1) == buildId)
|
||||
index.remove(QStringLiteral("active"));
|
||||
writeIndex(index);
|
||||
|
||||
QDir dir(buildDir(buildId));
|
||||
return !dir.exists() || dir.removeRecursively();
|
||||
}
|
||||
|
||||
// ── Незавершённое переключение ─────────────────────────────────────────────
|
||||
|
||||
QString BuildSwitcher::interruptedSwitchWarning() const
|
||||
|
||||
@@ -39,6 +39,13 @@ public:
|
||||
const QString &gameDir);
|
||||
void cancel();
|
||||
|
||||
// Сборку удалили: убираем её архив и запись о нём. Возвращает false, если
|
||||
// папку архива не удалось удалить целиком.
|
||||
bool forgetBuild(int buildId);
|
||||
|
||||
// Есть ли у сборки сохранённый архив (для текста предупреждения об удалении).
|
||||
bool hasArchive(int buildId) const { return !archivePathOf(buildId).isEmpty(); }
|
||||
|
||||
// Незавершённое переключение с прошлого запуска. Возвращает пустую строку,
|
||||
// если всё в порядке, иначе — текст для пользователя.
|
||||
QString interruptedSwitchWarning() const;
|
||||
|
||||
+89
-61
@@ -371,14 +371,71 @@ void LauncherBackend::removeProfile(int index)
|
||||
emit profilesChanged();
|
||||
}
|
||||
|
||||
QVariantMap LauncherBackend::customBuildRemovalInfo(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_builds.size())
|
||||
return {};
|
||||
const CustomBuild &build = m_builds[index];
|
||||
const bool active = m_settings.value(QStringLiteral("activeBuildId")).toInt() == build.id
|
||||
|| m_builds.size() == 1;
|
||||
return {{QStringLiteral("name"), build.name},
|
||||
{QStringLiteral("hasArchive"), m_switcher->hasArchive(build.id)},
|
||||
{QStringLiteral("active"), active},
|
||||
{QStringLiteral("lastOne"), m_builds.size() == 1}};
|
||||
}
|
||||
|
||||
void LauncherBackend::removeCustomBuild(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_builds.size())
|
||||
return;
|
||||
m_builds.removeAt(index);
|
||||
|
||||
if (m_switcher->isRunning() || m_busy || m_installer->isRunning()
|
||||
|| m_loaderInstaller->isRunning()) {
|
||||
emit launchError(tr("Дождитесь окончания текущей операции"));
|
||||
return;
|
||||
}
|
||||
if (m_launcher->isRunning()) {
|
||||
emit launchError(tr("Сначала закройте игру"));
|
||||
return;
|
||||
}
|
||||
|
||||
const CustomBuild removed = m_builds.takeAt(index);
|
||||
const bool wasActive = m_settings.value(QStringLiteral("activeBuildId")).toInt() == removed.id
|
||||
|| m_builds.isEmpty();
|
||||
saveCustomBuilds();
|
||||
|
||||
// Раньше удалялась только запись, а архив сборки оставался лежать в
|
||||
// galeonLauncher/builds вместе со ссылкой на него в index.json.
|
||||
if (!m_switcher->forgetBuild(removed.id)) {
|
||||
emit launchError(tr("Сборка «%1» удалена, но её архив в %2 стереть не удалось")
|
||||
.arg(removed.name,
|
||||
QDir::toNativeSeparators(LauncherPaths::buildStorageDir())));
|
||||
}
|
||||
|
||||
emit customBuildsChanged();
|
||||
emit activeBuildChanged();
|
||||
|
||||
if (!wasActive)
|
||||
return;
|
||||
|
||||
// Удалили активную сборку: в .minecraft лежит её содержимое, а активной
|
||||
// становится другая. Оставить как есть нельзя — при следующем переключении
|
||||
// чужое содержимое уехало бы в архив новой сборки поверх её собственного.
|
||||
m_settings.remove(QStringLiteral("activeBuildId"));
|
||||
saveSettings();
|
||||
|
||||
if (m_builds.isEmpty()) {
|
||||
emit activeBuildChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
const CustomBuild &next = m_builds.first();
|
||||
emit launchProgress(tr("Активна сборка «%1»…").arg(next.name));
|
||||
// from = -1: архивировать нечего, содержимое удалённой сборки уходит вместе
|
||||
// с ней. Дальше обычная очистка и распаковка новой активной сборки.
|
||||
m_switcher->switchTo(-1, QString(), next.id, next.name, gameDir());
|
||||
emit switchChanged();
|
||||
emit busyChanged();
|
||||
}
|
||||
|
||||
int LauncherBackend::indexOfBuildId(int buildId) const
|
||||
@@ -525,75 +582,46 @@ void LauncherBackend::rebuildCatalog() const
|
||||
{
|
||||
m_catalog.clear();
|
||||
|
||||
// Плоский список в порядке манифеста — новые версии сверху. Раскладку по
|
||||
// категориям и отбор делает окно выбора версии: ему нужен один список,
|
||||
// который можно фильтровать, а не готовая структура с разделителями.
|
||||
const QList<RemoteVersionEntry> remote = m_manifest->versions();
|
||||
QHash<QString, int> remoteIndex;
|
||||
remoteIndex.reserve(remote.size());
|
||||
for (int i = 0; i < remote.size(); ++i)
|
||||
remoteIndex.insert(remote.at(i).id, i);
|
||||
const QStringList installed = VersionLoader::installedVersions(gameDir());
|
||||
const QSet<QString> installedSet(installed.cbegin(), installed.cend());
|
||||
|
||||
const auto makeVersionRow = [](const QString &id, const QString &type, bool installed) {
|
||||
return QVariantMap{{QStringLiteral("kind"),
|
||||
installed ? QStringLiteral("installed") : QStringLiteral("remote")},
|
||||
{QStringLiteral("id"), id},
|
||||
const auto category = [](const QString &type) {
|
||||
if (type == QLatin1String("release") || type == QLatin1String("snapshot")
|
||||
|| type == QLatin1String("old_beta") || type == QLatin1String("old_alpha"))
|
||||
return type;
|
||||
return QStringLiteral("other");
|
||||
};
|
||||
|
||||
const auto makeVersionRow = [&category](const QString &id, const QString &type, bool isInstalled) {
|
||||
return QVariantMap{{QStringLiteral("id"), id},
|
||||
{QStringLiteral("type"), type},
|
||||
{QStringLiteral("category"), category(type)},
|
||||
{QStringLiteral("label"), id},
|
||||
{QStringLiteral("installed"), installed},
|
||||
{QStringLiteral("selectable"), true},
|
||||
{QStringLiteral("installed"), isInstalled},
|
||||
{QStringLiteral("search"), (id + u' ' + type).toLower()}};
|
||||
};
|
||||
|
||||
// Установленные: сначала знакомые манифесту, свежие сверху, затем сборки
|
||||
// вроде fabric-loader-…, о которых манифест ничего не знает.
|
||||
QStringList installed = VersionLoader::installedVersions(gameDir());
|
||||
std::stable_sort(installed.begin(), installed.end(),
|
||||
[&remoteIndex, &remote](const QString &a, const QString &b) {
|
||||
const auto ia = remoteIndex.constFind(a);
|
||||
const auto ib = remoteIndex.constFind(b);
|
||||
const bool knownA = ia != remoteIndex.constEnd();
|
||||
const bool knownB = ib != remoteIndex.constEnd();
|
||||
if (knownA != knownB)
|
||||
return knownA;
|
||||
if (knownA)
|
||||
return ia.value() < ib.value(); // манифест уже отсортирован
|
||||
return a < b;
|
||||
});
|
||||
|
||||
QSet<QString> installedSet;
|
||||
for (const QString &id : std::as_const(installed)) {
|
||||
const auto it = remoteIndex.constFind(id);
|
||||
const QString type = it != remoteIndex.constEnd() ? remote.at(it.value()).type : QString();
|
||||
installedSet.insert(id);
|
||||
m_catalog.append(makeVersionRow(id, type, true));
|
||||
}
|
||||
|
||||
m_catalog.append(QVariantMap{{QStringLiteral("kind"), QStringLiteral("separator")},
|
||||
{QStringLiteral("id"), QString()},
|
||||
{QStringLiteral("type"), QString()},
|
||||
{QStringLiteral("label"), tr("Все версии Minecraft")},
|
||||
{QStringLiteral("installed"), false},
|
||||
{QStringLiteral("selectable"), false},
|
||||
{QStringLiteral("search"), QString()}});
|
||||
|
||||
if (remote.isEmpty()) {
|
||||
m_catalog.append(
|
||||
QVariantMap{{QStringLiteral("kind"), QStringLiteral("status")},
|
||||
{QStringLiteral("id"), QString()},
|
||||
{QStringLiteral("type"), QString()},
|
||||
{QStringLiteral("label"),
|
||||
m_manifest->isRefreshing()
|
||||
? tr("Загрузка списка версий…")
|
||||
: tr("Список версий недоступен — проверьте соединение")},
|
||||
{QStringLiteral("installed"), false},
|
||||
{QStringLiteral("selectable"), false},
|
||||
{QStringLiteral("search"), QString()}});
|
||||
return;
|
||||
}
|
||||
|
||||
m_catalog.reserve(remote.size() + installed.size());
|
||||
QSet<QString> known;
|
||||
known.reserve(remote.size());
|
||||
for (const RemoteVersionEntry &entry : remote) {
|
||||
if (installedSet.contains(entry.id))
|
||||
continue;
|
||||
m_catalog.append(makeVersionRow(entry.id, entry.type, false));
|
||||
known.insert(entry.id);
|
||||
m_catalog.append(makeVersionRow(entry.id, entry.type, installedSet.contains(entry.id)));
|
||||
}
|
||||
|
||||
// Сборки модлоадеров манифест не знает — они попадают в «Прочие».
|
||||
QStringList unknown;
|
||||
for (const QString &id : installed) {
|
||||
if (!known.contains(id))
|
||||
unknown << id;
|
||||
}
|
||||
std::sort(unknown.begin(), unknown.end());
|
||||
for (const QString &id : std::as_const(unknown))
|
||||
m_catalog.append(makeVersionRow(id, QString(), true));
|
||||
}
|
||||
|
||||
// ── Загрузка версий ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -103,6 +103,8 @@ public:
|
||||
Q_INVOKABLE QVariantMap profileAt(int index) const;
|
||||
Q_INVOKABLE QVariantMap customBuildAt(int index) const;
|
||||
Q_INVOKABLE void removeProfile(int index);
|
||||
// Что именно потеряется при удалении — для текста предупреждения.
|
||||
Q_INVOKABLE QVariantMap customBuildRemovalInfo(int index) const;
|
||||
Q_INVOKABLE void removeCustomBuild(int index);
|
||||
|
||||
// Докачать то, чего не хватает выбранной сборке. Отдельной кнопкой, потому
|
||||
|
||||
Reference in New Issue
Block a user