340 lines
14 KiB
QML
340 lines
14 KiB
QML
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: qsTr("Название"), width: 250, align: Text.AlignLeft },
|
||
{ key: "minecraftVersion", title: qsTr("Версия"), width: 90, align: Text.AlignLeft },
|
||
{ key: "loaderLabel", title: qsTr("Загрузчик"), width: 150, align: Text.AlignLeft },
|
||
{ key: "modCount", title: qsTr("Модов"), width: 70, align: Text.AlignRight },
|
||
{ key: "seasonStart", title: qsTr("Начало"), width: 110, align: Text.AlignLeft },
|
||
{ key: "seasonEnd", title: qsTr("Конец"), width: 110, align: Text.AlignLeft },
|
||
{ key: "status", title: qsTr("Статус"), width: 140, align: Text.AlignLeft }
|
||
]
|
||
|
||
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: qsTr("Сезонные сборки")
|
||
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
|
||
? qsTr("Загрузка списка сборок…")
|
||
: (seasons.backend.seasonalCatalogError === ""
|
||
? qsTr("Сборок пока нет")
|
||
: 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 qsTr("Выберите сборку в списке")
|
||
const entry = seasons.selectedEntry
|
||
var line = qsTr("Ревизия %1").arg(entry.revision)
|
||
const size = seasons.formatMb(entry.sizeBytes)
|
||
if (size !== "")
|
||
line += qsTr(" · %1 МБ").arg(size)
|
||
if (entry.serverUrl !== "")
|
||
line += " · " + entry.serverUrl
|
||
if (entry.updateAvailable)
|
||
line += qsTr(" · установлена ревизия %1").arg(entry.installedRevision)
|
||
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 ? qsTr("Обновление…")
|
||
: qsTr("Обновить список")
|
||
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
|
||
? qsTr("Обновить")
|
||
: qsTr("Установить")
|
||
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()
|
||
}
|
||
}
|
||
}
|
||
}
|