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()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|