seasonal builds
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user