version tracking and auto installing
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user