builds fixes
This commit is contained in:
+214
-11
@@ -48,6 +48,30 @@ Dialog {
|
|||||||
buildsDialog.refreshStatus()
|
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) {
|
function commit(fields) {
|
||||||
if (buildsDialog.loading || buildsDialog.editIndex < 0)
|
if (buildsDialog.loading || buildsDialog.editIndex < 0)
|
||||||
return
|
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 {
|
Connections {
|
||||||
@@ -176,14 +351,7 @@ Dialog {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: -6
|
anchors.margins: -6
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: buildsDialog.askRemove(buildRow.index)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,11 +444,46 @@ Dialog {
|
|||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionCatalogCombo {
|
// Само поле только показывает выбор: версий около тысячи, и
|
||||||
|
// разбираться в них удобнее в отдельном окне с категориями.
|
||||||
|
Rectangle {
|
||||||
id: bdMinecraft
|
id: bdMinecraft
|
||||||
|
property string selectedId: ""
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
catalog: buildsDialog.backend.versionCatalog
|
height: 36
|
||||||
onAboutToOpen: buildsDialog.backend.refreshVersionCatalog()
|
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":
|
onSelectedIdChanged: buildsDialog.commit({"minecraftVersion":
|
||||||
bdMinecraft.selectedId})
|
bdMinecraft.selectedId})
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ qt_add_qml_module(appMinecraft_launcher
|
|||||||
QML_FILES
|
QML_FILES
|
||||||
Main.qml
|
Main.qml
|
||||||
DarkCombo.qml
|
DarkCombo.qml
|
||||||
VersionCatalogCombo.qml
|
VersionPickerDialog.qml
|
||||||
LabelledField.qml
|
LabelledField.qml
|
||||||
BuildsDialog.qml
|
BuildsDialog.qml
|
||||||
LoaderRow.qml
|
LoaderRow.qml
|
||||||
|
|||||||
+25
-34
@@ -42,35 +42,35 @@ Column {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Самая свежая стабильная сборка, а если стабильных нет — просто самая
|
// Единственное место, где меняются entries и selectedVersion. Через него
|
||||||
// свежая. Список отсортирован новыми вперёд, поэтому хватает первого
|
// проходят все три пути — кэш, ответ сети и заполнение из сборки, — потому
|
||||||
// совпадения.
|
// что выбранная версия обязана существовать в списке под текущую версию
|
||||||
function preferredIndex() {
|
// игры. Раньше проверял только путь из сети, и версия лоадера от прошлой
|
||||||
for (var i = 0; i < loaderRow.entries.length; ++i) {
|
// версии игры доживала до сохранения сборки.
|
||||||
if (loaderRow.entries[i].recommended)
|
function applyEntries(list) {
|
||||||
return i
|
loaderRow.entries = list
|
||||||
}
|
|
||||||
for (var j = 0; j < loaderRow.entries.length; ++j) {
|
// Список отсортирован новыми вперёд, поэтому максимально доступная
|
||||||
if (loaderRow.entries[j].stable)
|
// версия для этой версии игры — всегда первая строка.
|
||||||
return j
|
if (loaderRow.versionIndex(loaderRow.selectedVersion) < 0) {
|
||||||
}
|
const fallback = list.length > 0 ? list[0].version : ""
|
||||||
return loaderRow.entries.length > 0 ? 0 : -1
|
const changedValue = loaderRow.selectedVersion !== fallback
|
||||||
|
loaderRow.selectedVersion = fallback
|
||||||
|
if (changedValue && !loaderRow.applying)
|
||||||
|
loaderRow.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncCombo() {
|
combo.currentIndex = loaderRow.versionIndex(loaderRow.selectedVersion)
|
||||||
const index = loaderRow.versionIndex(loaderRow.selectedVersion)
|
|
||||||
combo.currentIndex = index
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
if (!box.checked || loaderRow.gameVersion === "") {
|
if (!box.checked || loaderRow.gameVersion === "") {
|
||||||
loaderRow.entries = []
|
loaderRow.applyEntries([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Сначала из кэша — список появляется мгновенно, — потом обновление сети.
|
// Сначала из кэша — список появляется мгновенно, — потом обновление сети.
|
||||||
loaderRow.entries = loaderRow.backend.loaderVersions(loaderRow.loaderKey,
|
loaderRow.applyEntries(loaderRow.backend.loaderVersions(loaderRow.loaderKey,
|
||||||
loaderRow.gameVersion)
|
loaderRow.gameVersion))
|
||||||
loaderRow.syncCombo()
|
|
||||||
loaderRow.backend.refreshLoaderVersions(loaderRow.loaderKey, loaderRow.gameVersion)
|
loaderRow.backend.refreshLoaderVersions(loaderRow.loaderKey, loaderRow.gameVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,17 +78,17 @@ Column {
|
|||||||
function applyBuild(loader, loaderVersion) {
|
function applyBuild(loader, loaderVersion) {
|
||||||
loaderRow.applying = true
|
loaderRow.applying = true
|
||||||
box.checked = loader === loaderRow.loaderKey
|
box.checked = loader === loaderRow.loaderKey
|
||||||
|
// Версию из сборки берём как пожелание: если под выбранную версию игры
|
||||||
|
// её нет, applyEntries() заменит её на максимально доступную.
|
||||||
loaderRow.selectedVersion = box.checked ? loaderVersion : ""
|
loaderRow.selectedVersion = box.checked ? loaderVersion : ""
|
||||||
loaderRow.reload()
|
loaderRow.reload()
|
||||||
loaderRow.applying = false
|
loaderRow.applying = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onGameVersionChanged: {
|
onGameVersionChanged: {
|
||||||
// Сборка лоадера привязана к версии игры — при её смене выбор
|
// Сборка лоадера привязана к версии игры: под новой версией прежний
|
||||||
// сбрасывается, и список перезапрашивается под новую версию.
|
// выбор недействителен, и список перезапрашивается заново.
|
||||||
loaderRow.entries = []
|
|
||||||
loaderRow.selectedVersion = ""
|
loaderRow.selectedVersion = ""
|
||||||
combo.currentIndex = -1
|
|
||||||
loaderRow.reload()
|
loaderRow.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,16 +97,7 @@ Column {
|
|||||||
function onLoaderVersionsChanged(key, game) {
|
function onLoaderVersionsChanged(key, game) {
|
||||||
if (key !== loaderRow.loaderKey || game !== loaderRow.gameVersion || !box.checked)
|
if (key !== loaderRow.loaderKey || game !== loaderRow.gameVersion || !box.checked)
|
||||||
return
|
return
|
||||||
loaderRow.entries = loaderRow.backend.loaderVersions(key, game)
|
loaderRow.applyEntries(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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,9 +161,17 @@ Window {
|
|||||||
anchors.bottomMargin: -218
|
anchors.bottomMargin: -218
|
||||||
rotation: 0
|
rotation: 0
|
||||||
editable: false
|
editable: false
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
model: backend.profileNames
|
model: backend.profileNames
|
||||||
|
|
||||||
|
// Пока список закрыт: открытый popup накрывает кнопку, и подсказка
|
||||||
|
// висела бы поверх собственного меню.
|
||||||
|
ToolTip.visible: profileBox.hovered && !profileBox.down
|
||||||
|
ToolTip.text: backend.profileNames.length === 0
|
||||||
|
? qsTr("Профили игрока — добавьте первый профиль")
|
||||||
|
: qsTr("Профиль игрока: %1").arg(profileBox.displayText)
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
id: profileText
|
id: profileText
|
||||||
text: profileBox.displayText
|
text: profileBox.displayText
|
||||||
@@ -427,8 +435,12 @@ Window {
|
|||||||
|
|
||||||
background: Image {
|
background: Image {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: settingsButton.pressed ? "images/Options/Options_Pressed.svg"
|
// Ассеты Options названы наоборот относительно Folder: зелёный
|
||||||
: settingsButton.hovered ? "images/Options/Options_active.svg"
|
// #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"
|
: "images/Options/Options_Idle.svg"
|
||||||
fillMode: Image.PreserveAspectFit
|
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);
|
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
|
QString BuildSwitcher::interruptedSwitchWarning() const
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ public:
|
|||||||
const QString &gameDir);
|
const QString &gameDir);
|
||||||
void cancel();
|
void cancel();
|
||||||
|
|
||||||
|
// Сборку удалили: убираем её архив и запись о нём. Возвращает false, если
|
||||||
|
// папку архива не удалось удалить целиком.
|
||||||
|
bool forgetBuild(int buildId);
|
||||||
|
|
||||||
|
// Есть ли у сборки сохранённый архив (для текста предупреждения об удалении).
|
||||||
|
bool hasArchive(int buildId) const { return !archivePathOf(buildId).isEmpty(); }
|
||||||
|
|
||||||
// Незавершённое переключение с прошлого запуска. Возвращает пустую строку,
|
// Незавершённое переключение с прошлого запуска. Возвращает пустую строку,
|
||||||
// если всё в порядке, иначе — текст для пользователя.
|
// если всё в порядке, иначе — текст для пользователя.
|
||||||
QString interruptedSwitchWarning() const;
|
QString interruptedSwitchWarning() const;
|
||||||
|
|||||||
+89
-61
@@ -371,14 +371,71 @@ void LauncherBackend::removeProfile(int index)
|
|||||||
emit profilesChanged();
|
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)
|
void LauncherBackend::removeCustomBuild(int index)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= m_builds.size())
|
if (index < 0 || index >= m_builds.size())
|
||||||
return;
|
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();
|
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 customBuildsChanged();
|
||||||
emit activeBuildChanged();
|
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
|
int LauncherBackend::indexOfBuildId(int buildId) const
|
||||||
@@ -525,75 +582,46 @@ void LauncherBackend::rebuildCatalog() const
|
|||||||
{
|
{
|
||||||
m_catalog.clear();
|
m_catalog.clear();
|
||||||
|
|
||||||
|
// Плоский список в порядке манифеста — новые версии сверху. Раскладку по
|
||||||
|
// категориям и отбор делает окно выбора версии: ему нужен один список,
|
||||||
|
// который можно фильтровать, а не готовая структура с разделителями.
|
||||||
const QList<RemoteVersionEntry> remote = m_manifest->versions();
|
const QList<RemoteVersionEntry> remote = m_manifest->versions();
|
||||||
QHash<QString, int> remoteIndex;
|
const QStringList installed = VersionLoader::installedVersions(gameDir());
|
||||||
remoteIndex.reserve(remote.size());
|
const QSet<QString> installedSet(installed.cbegin(), installed.cend());
|
||||||
for (int i = 0; i < remote.size(); ++i)
|
|
||||||
remoteIndex.insert(remote.at(i).id, i);
|
|
||||||
|
|
||||||
const auto makeVersionRow = [](const QString &id, const QString &type, bool installed) {
|
const auto category = [](const QString &type) {
|
||||||
return QVariantMap{{QStringLiteral("kind"),
|
if (type == QLatin1String("release") || type == QLatin1String("snapshot")
|
||||||
installed ? QStringLiteral("installed") : QStringLiteral("remote")},
|
|| type == QLatin1String("old_beta") || type == QLatin1String("old_alpha"))
|
||||||
{QStringLiteral("id"), id},
|
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("type"), type},
|
||||||
|
{QStringLiteral("category"), category(type)},
|
||||||
{QStringLiteral("label"), id},
|
{QStringLiteral("label"), id},
|
||||||
{QStringLiteral("installed"), installed},
|
{QStringLiteral("installed"), isInstalled},
|
||||||
{QStringLiteral("selectable"), true},
|
|
||||||
{QStringLiteral("search"), (id + u' ' + type).toLower()}};
|
{QStringLiteral("search"), (id + u' ' + type).toLower()}};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Установленные: сначала знакомые манифесту, свежие сверху, затем сборки
|
m_catalog.reserve(remote.size() + installed.size());
|
||||||
// вроде fabric-loader-…, о которых манифест ничего не знает.
|
QSet<QString> known;
|
||||||
QStringList installed = VersionLoader::installedVersions(gameDir());
|
known.reserve(remote.size());
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const RemoteVersionEntry &entry : remote) {
|
for (const RemoteVersionEntry &entry : remote) {
|
||||||
if (installedSet.contains(entry.id))
|
known.insert(entry.id);
|
||||||
continue;
|
m_catalog.append(makeVersionRow(entry.id, entry.type, installedSet.contains(entry.id)));
|
||||||
m_catalog.append(makeVersionRow(entry.id, entry.type, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сборки модлоадеров манифест не знает — они попадают в «Прочие».
|
||||||
|
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 profileAt(int index) const;
|
||||||
Q_INVOKABLE QVariantMap customBuildAt(int index) const;
|
Q_INVOKABLE QVariantMap customBuildAt(int index) const;
|
||||||
Q_INVOKABLE void removeProfile(int index);
|
Q_INVOKABLE void removeProfile(int index);
|
||||||
|
// Что именно потеряется при удалении — для текста предупреждения.
|
||||||
|
Q_INVOKABLE QVariantMap customBuildRemovalInfo(int index) const;
|
||||||
Q_INVOKABLE void removeCustomBuild(int index);
|
Q_INVOKABLE void removeCustomBuild(int index);
|
||||||
|
|
||||||
// Докачать то, чего не хватает выбранной сборке. Отдельной кнопкой, потому
|
// Докачать то, чего не хватает выбранной сборке. Отдельной кнопкой, потому
|
||||||
|
|||||||
Reference in New Issue
Block a user