Files
minecraft-launcher/SeasonalBuildsDialog.qml
T

340 lines
14 KiB
QML
Raw Normal View History

2026-08-31 00:59:14 +03:00
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls 2.15
// Готовые сезонные сборки с сервера лаунчера: таблица со всем, что нужно
// решить, ставить её или нет, и одна кнопка, которая делает всё остальное —
// заводит сборку, ставит версию игры, модлоадер, Java и раскладывает файлы.
//
// Таблица собрана из Row фиксированных колонок, а не из TableView: в проекте
// нет ни одной модели QAbstractItemModel, а строки приходят готовыми
// QVariantMap — заводить ради семи колонок отдельную модель незачем.
Dialog {
id: seasons
required property var backend
// Выбранная строка. Хранится по id, а не по индексу: список обновляется
// под руками, и индекс после обновления указывал бы на другую сборку.
property string selectedId: ""
// Ширины колонок в одном месте: их повторяют и шапка, и делегат.
readonly property var columns: [
{ key: "name", title: Loc.t.common.header.name, width: 250, align: Text.AlignLeft },
{ key: "minecraftVersion", title: Loc.t.common.header.version, width: 90, align: Text.AlignLeft },
{ key: "loaderLabel", title: Loc.t.common.header.loader, width: 150, align: Text.AlignLeft },
{ key: "modCount", title: Loc.t.seasonal.header.mods, width: 70, align: Text.AlignRight },
{ key: "seasonStart", title: Loc.t.seasonal.header.start, width: 110, align: Text.AlignLeft },
{ key: "seasonEnd", title: Loc.t.seasonal.header.end, width: 110, align: Text.AlignLeft },
{ key: "status", title: Loc.t.common.header.status, width: 140, align: Text.AlignLeft }
2026-08-31 00:59:14 +03:00
]
modal: true
padding: 0
width: 960
height: 560
// Пока идёт установка, окно закрывается только кнопкой: случайный щелчок
// мимо не должен спрятать единственную видимую отмену.
closePolicy: seasons.backend.seasonalInstalling
? Popup.NoAutoClose
: (Popup.CloseOnEscape | Popup.CloseOnPressOutside)
background: Rectangle {
color: "#1e1e1e"
radius: 10
border.color: "#91B315"
border.width: 1
}
// Строки приходят из C++ уже отсортированными и сведёнными с локальными
// записями — здесь только показ.
readonly property var entries: seasons.backend.seasonalCatalog
readonly property var selectedEntry: {
const list = seasons.entries
for (var i = 0; i < list.length; ++i) {
if (list[i].id === seasons.selectedId)
return list[i]
}
return null
}
function openCatalog() {
seasons.open()
}
function formatMb(bytes) {
if (!bytes || bytes <= 0)
return ""
return (bytes / (1024 * 1024)).toFixed(1)
}
function cellText(entry, key) {
if (key === "loaderLabel") {
return entry.loaderVersion === ""
? entry.loaderTitle
: entry.loaderTitle + " " + entry.loaderVersion
}
return String(entry[key])
}
// Свежий кэш отвечает без сети, поэтому дёргаем при каждом открытии.
onAboutToShow: seasons.backend.refreshSeasonalCatalog(false)
header: Item {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: Loc.t.common.title.seasonalBuilds
2026-08-31 00:59:14 +03:00
color: "#ffffff"
font.pixelSize: 17
font.bold: true
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: "#333333"
}
}
contentItem: Item {
// ── Шапка таблицы ───────────────────────────────────────────────
Row {
id: tableHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: 18
height: 30
Repeater {
model: seasons.columns
delegate: Text {
required property var modelData
width: modelData.width
height: tableHeader.height
text: modelData.title
color: "#aaaaaa"
font.pixelSize: 12
elide: Text.ElideRight
horizontalAlignment: modelData.align
verticalAlignment: Text.AlignVCenter
rightPadding: 10
}
}
}
Rectangle {
id: headerLine
anchors.top: tableHeader.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 12
anchors.rightMargin: 12
height: 1
color: "#333333"
}
// ── Сообщение вместо таблицы ────────────────────────────────────
// Пустой список и ошибка выглядят одинаково пустыми, поэтому причина
// показывается прямо на месте строк.
Text {
anchors.centerIn: parent
width: parent.width - 80
visible: seasons.entries.length === 0
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: seasons.backend.seasonalCatalogError === "" ? "#888888" : "#cc6666"
font.pixelSize: 13
text: seasons.backend.seasonalCatalogLoading
? Loc.t.seasonal.progress.loadingCatalog
2026-08-31 00:59:14 +03:00
: (seasons.backend.seasonalCatalogError === ""
? Loc.t.seasonal.status.catalogEmptyYet
2026-08-31 00:59:14 +03:00
: seasons.backend.seasonalCatalogError)
}
// ── Строки ──────────────────────────────────────────────────────
ListView {
id: buildList
anchors.top: headerLine.bottom
anchors.topMargin: 4
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 12
anchors.rightMargin: 12
clip: true
spacing: 1
model: seasons.entries
ScrollIndicator.vertical: ScrollIndicator {}
delegate: Rectangle {
id: buildRow
required property var modelData
width: buildList.width
height: 36
radius: 4
color: seasons.selectedId === buildRow.modelData.id ? "#91B315"
: rowArea.containsMouse ? "#2d2d2d" : "transparent"
readonly property bool highlighted: seasons.selectedId === buildRow.modelData.id
Row {
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
Repeater {
model: seasons.columns
delegate: Text {
required property var modelData
width: modelData.width
text: seasons.cellText(buildRow.modelData, modelData.key)
// Подсвеченная строка залита зелёным — тёмный текст
// на ней читается, светлый нет.
color: buildRow.highlighted
? "#1e1e1e"
: (modelData.key === "status"
&& buildRow.modelData.updateAvailable
? "#91B315" : "#ffffff")
font.pixelSize: 13
elide: Text.ElideRight
horizontalAlignment: modelData.align
rightPadding: 10
}
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
onClicked: seasons.selectedId = buildRow.modelData.id
onDoubleClicked: {
seasons.selectedId = buildRow.modelData.id
seasons.installSelected()
}
}
}
}
}
function installSelected() {
const entry = seasons.selectedEntry
if (!entry || seasons.backend.busy)
return
seasons.backend.installSeasonalBuild(entry.id)
}
footer: Item {
implicitHeight: 76
Rectangle {
anchors.top: parent.top
width: parent.width
height: 1
color: "#333333"
}
// Описание и размер выбранной сборки: они длинные и в таблицу не
// помещаются, а решение принимается по ним.
Column {
anchors.left: parent.left
anchors.leftMargin: 18
anchors.right: footerButtons.left
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
elide: Text.ElideRight
color: "#ffffff"
font.pixelSize: 12
text: seasons.selectedEntry ? seasons.selectedEntry.description : ""
}
Text {
width: parent.width
elide: Text.ElideRight
color: "#888888"
font.pixelSize: 11
text: {
if (!seasons.selectedEntry)
return Loc.t.seasonal.hint.pickBuild
2026-08-31 00:59:14 +03:00
const entry = seasons.selectedEntry
var line = Loc.t.seasonal.status.revision.arg(entry.revision)
2026-08-31 00:59:14 +03:00
const size = seasons.formatMb(entry.sizeBytes)
if (size !== "")
line += Loc.t.seasonal.status.sizeSuffix.arg(size)
2026-08-31 00:59:14 +03:00
if (entry.serverUrl !== "")
line += " · " + entry.serverUrl
if (entry.updateAvailable)
line += Loc.t.seasonal.status.installedRevisionSuffix.arg(entry.installedRevision)
2026-08-31 00:59:14 +03:00
return line
}
}
}
Row {
id: footerButtons
anchors.right: parent.right
anchors.rightMargin: 18
anchors.verticalCenter: parent.verticalCenter
spacing: 12
Button {
id: refreshButton
width: 150
height: 36
enabled: !seasons.backend.seasonalCatalogLoading
contentItem: Text {
text: seasons.backend.seasonalCatalogLoading ? Loc.t.common.status.updating
: Loc.t.common.button.refreshList
2026-08-31 00:59:14 +03:00
color: refreshButton.enabled ? "#ffffff" : "#888888"
font.pixelSize: 13
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: refreshButton.pressed ? "#444444" : "#333333"
radius: 6
}
onClicked: seasons.backend.refreshSeasonalCatalog(true)
}
Button {
id: installButton
width: 150
height: 36
// Установка занимает и панель загрузки, и .minecraft целиком:
// пока лаунчер занят чем угодно, вторую начинать нельзя.
enabled: seasons.selectedEntry !== null && !seasons.backend.busy
contentItem: Text {
text: seasons.selectedEntry && seasons.selectedEntry.updateAvailable
? Loc.t.common.button.update
: Loc.t.common.button.install
2026-08-31 00:59:14 +03:00
color: installButton.enabled ? "#ffffff" : "#888888"
font.pixelSize: 13
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: !installButton.enabled ? "#2a2a2a"
: installButton.pressed ? "#6a8510" : "#91B315"
radius: 6
}
onClicked: seasons.installSelected()
}
}
}
}