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 } ] 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 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 : (seasons.backend.seasonalCatalogError === "" ? Loc.t.seasonal.status.catalogEmptyYet : 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 const entry = seasons.selectedEntry var line = Loc.t.seasonal.status.revision.arg(entry.revision) const size = seasons.formatMb(entry.sizeBytes) if (size !== "") line += Loc.t.seasonal.status.sizeSuffix.arg(size) if (entry.serverUrl !== "") line += " · " + entry.serverUrl if (entry.updateAvailable) line += Loc.t.seasonal.status.installedRevisionSuffix.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 ? Loc.t.common.status.updating : Loc.t.common.button.refreshList 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 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() } } } }