version tracking and auto installing

This commit is contained in:
2026-08-24 21:09:52 +03:00
parent 671fbe169d
commit 5bc59f8018
10 changed files with 1937 additions and 24 deletions
+391 -21
View File
@@ -48,6 +48,10 @@ Window {
}
}
function formatMb(bytes) {
return (bytes / 1048576).toFixed(1)
}
// ── Background ─────────────────────────────────────────────────────────
Image {
id: image
@@ -63,7 +67,7 @@ Window {
visible: toastLabel.text !== ""
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
anchors.bottomMargin: downloadPanel.visible ? 110 : 40
width: Math.min(window.width - 80, toastLabel.implicitWidth + 32)
height: toastLabel.implicitHeight + 20
radius: 8
@@ -86,6 +90,94 @@ Window {
}
}
// ── Download panel ─────────────────────────────────────────────────────
// Левый нижний угол: не задевает кнопку запуска, плашку по центру и
// кнопки папки с настройками.
Rectangle {
id: downloadPanel
visible: backend.downloading
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.leftMargin: 24
anchors.bottomMargin: 24
width: 320
height: 72
radius: 8
color: "#1e1e1e"
opacity: 0.94
border.color: "#91B315"
border.width: 1
Text {
x: 12; y: 8
width: parent.width - 70
elide: Text.ElideRight
color: "#ffffff"
font.pixelSize: 12
font.bold: true
text: qsTr("Загрузка %1").arg(backend.downloadVersion)
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
y: 8
color: "#91B315"
font.pixelSize: 12
// Пока не известен общий объём, доля равна -1.
text: backend.downloadProgress < 0
? "…" : Math.round(backend.downloadProgress * 100) + "%"
}
Rectangle {
id: dlTrack
x: 12; y: 30
width: parent.width - 24
height: 6
radius: 3
color: "#2a2a2a"
Rectangle {
width: backend.downloadProgress > 0 ? dlTrack.width * backend.downloadProgress : 0
height: parent.height
radius: 3
color: "#91B315"
Behavior on width { NumberAnimation { duration: 120 } }
}
}
Text {
x: 12; y: 44
width: parent.width - 40
elide: Text.ElideMiddle
color: "#888888"
font.pixelSize: 10
text: backend.downloadBytesTotal > 0
? qsTr("%1 — %2 / %3 МБ").arg(backend.downloadStatus)
.arg(window.formatMb(backend.downloadBytesDone))
.arg(window.formatMb(backend.downloadBytesTotal))
: backend.downloadStatus
}
Text {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 10
anchors.bottomMargin: 6
text: "✕"
color: cancelDownloadArea.containsMouse ? "#cc6666" : "#666666"
font.pixelSize: 12
MouseArea {
id: cancelDownloadArea
anchors.fill: parent
anchors.margins: -6
hoverEnabled: true
onClicked: backend.cancelDownload()
}
}
}
// ── Play button ────────────────────────────────────────────────────────
Button {
id: button
@@ -753,21 +845,26 @@ Window {
}
}
// Папка внутри .minecraft/versions, которую и будем запускать.
DarkCombo {
id: verInstalled
// Установленные версии и весь каталог Mojang одним списком.
VersionCatalogCombo {
id: verVersion
x: 20
width: parent.width - 40
model: backend.installedVersions
catalog: backend.versionCatalog
onAboutToOpen: backend.refreshVersionCatalog()
}
Text {
x: 20
width: parent.width - 40
text: backend.installedVersions.length === 0
? qsTr("В .minecraft/versions нет установленных версий")
: qsTr("Установленная версия из .minecraft")
color: backend.installedVersions.length === 0 ? "#cc6666" : "#888888"
text: verVersion.selectedId === ""
? qsTr("Выберите версию из списка")
: backend.isVersionInstalled(verVersion.selectedId)
? qsTr("Версия уже установлена в .minecraft")
: qsTr("Версия будет загружена в .minecraft после сохранения")
color: verVersion.selectedId !== ""
&& !backend.isVersionInstalled(verVersion.selectedId)
? "#91B315" : "#888888"
font.pixelSize: 11
wrapMode: Text.Wrap
}
@@ -819,18 +916,22 @@ Window {
}
}
onAboutToShow: verInstalled.currentIndex = 0
// Манифест прогреваем заранее — тогда список открывается мгновенно.
onAboutToShow: {
verVersion.selectedId = ""
backend.refreshVersionCatalog()
}
onAccepted: {
const name = verName.text.trim()
if (name !== "") {
backend.addVersion(name, verServer.text.trim(), verInstalled.currentText)
backend.addVersion(name, verServer.text.trim(), verVersion.selectedId)
versionBox.currentIndex = backend.versionNames.length - 1
}
verName.text = ""; verServer.text = ""
verName.text = ""; verServer.text = ""; verVersion.selectedId = ""
}
onRejected: {
verName.text = ""; verServer.text = ""
verName.text = ""; verServer.text = ""; verVersion.selectedId = ""
}
}
@@ -1008,7 +1109,7 @@ Window {
Dialog {
id: editVersionDialog
modal: true
width: 320
width: 380
x: (window.width - width) / 2
y: (window.height - height) / 2
padding: 0
@@ -1020,7 +1121,8 @@ Window {
editIndex = index
evName.text = data.name || ""
evServer.text = data.serverUrl || ""
evInstalled.currentIndex = backend.installedVersions.indexOf(data.versionId || "")
evVersion.selectedId = data.versionId || ""
backend.refreshVersionCatalog()
const problems = backend.checkInstallation(index)
evStatus.text = problems.length === 0
@@ -1090,11 +1192,12 @@ Window {
}
}
DarkCombo {
id: evInstalled
VersionCatalogCombo {
id: evVersion
x: 20
width: parent.width - 40
model: backend.installedVersions
catalog: backend.versionCatalog
onAboutToOpen: backend.refreshVersionCatalog()
}
// Показываем, чего не хватает в .minecraft именно для этой версии.
@@ -1120,6 +1223,25 @@ Window {
anchors.centerIn: parent
spacing: 12
// Починка: докачать то, чего не хватает выбранной версии.
Button {
text: "Скачать"
width: 110; height: 36
enabled: evVersion.selectedId !== "" && !backend.downloading
opacity: enabled ? 1.0 : 0.45
contentItem: Text {
text: parent.text
color: "#ffffff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.pressed ? "#444444" : "#333333"
radius: 6
}
onClicked: backend.installVersion(evVersion.selectedId)
}
Button {
text: "Отмена"
width: 110; height: 36
@@ -1157,13 +1279,13 @@ Window {
onAccepted: {
const name = evName.text.trim()
if (editIndex >= 0 && name !== "")
backend.updateVersion(editIndex, name, evServer.text.trim(), evInstalled.currentText)
backend.updateVersion(editIndex, name, evServer.text.trim(), evVersion.selectedId)
editIndex = -1
evName.text = ""; evServer.text = ""
evName.text = ""; evServer.text = ""; evVersion.selectedId = ""
}
onRejected: {
editIndex = -1
evName.text = ""; evServer.text = ""
evName.text = ""; evServer.text = ""; evVersion.selectedId = ""
}
}
@@ -1574,6 +1696,254 @@ Window {
}
}
// Выпадающий список версий: сверху установленные, ниже через разделитель
// весь каталог Mojang, сверху поле поиска.
//
// Это Item с Popup, а не ComboBox: editable-ComboBox привязывает поле ввода
// к currentText и запускает своё автодополнение, а при активном фильтре
// currentIndex перестаёт указывать в исходную модель.
component VersionCatalogCombo: 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()
}
}
}
}
}
}
}
}
// Подпись + поле ввода одной колонкой — используется в настройках.
component LabelledField: Column {
id: labelled