Initial Qt AppImage release catalog client
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
build/
|
||||
.cache/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,32 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(appimage-release-client VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Widgets Network)
|
||||
|
||||
qt_add_executable(appimage-release-client
|
||||
src/main.cpp
|
||||
src/catalogclient.cpp
|
||||
src/catalogclient.h
|
||||
src/mainwindow.cpp
|
||||
src/mainwindow.h
|
||||
)
|
||||
|
||||
target_link_libraries(appimage-release-client PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::Network
|
||||
)
|
||||
|
||||
target_compile_definitions(appimage-release-client PRIVATE
|
||||
APP_VERSION="${PROJECT_VERSION}"
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS appimage-release-client
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
# AppImage Release Catalog
|
||||
|
||||
Client desktop Qt 6/C++ per visualizzare il catalogo JSON delle release AppImage pubblicato su Brain.
|
||||
|
||||
## Funzioni
|
||||
|
||||
- legge `/api/index.json` e `/api/apps/<app>.json`;
|
||||
- mostra applicazioni, descrizione, release, architettura, dimensione e SHA-256;
|
||||
- carica l'icona dal catalogo;
|
||||
- apre gli URL dell'AppImage stabile, del manifest zsync e del progetto;
|
||||
- mantiene la GUI responsiva durante le richieste di rete;
|
||||
- URL del catalogo modificabile dalla barra superiore.
|
||||
|
||||
Il client è **sola lettura**: non pubblica file e non modifica il repository remoto.
|
||||
|
||||
## Requisiti
|
||||
|
||||
- C++17;
|
||||
- CMake 3.21 o superiore;
|
||||
- Qt 6.4 o superiore, moduli `Widgets` e `Network`.
|
||||
|
||||
Su Debian/Ubuntu, i pacchetti di sviluppo normalmente necessari sono `cmake`, `g++`, `qt6-base-dev` e `qt6-base-dev-tools`.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --parallel
|
||||
./build/appimage-release-client
|
||||
```
|
||||
|
||||
URL predefinito: `http://10.8.0.3:8092`.
|
||||
|
||||
Per la rete LAN è possibile usare `http://10.0.0.181:8092`.
|
||||
@@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=AppImage Release Catalog
|
||||
Comment=Visualizza le release AppImage dal catalogo Brain
|
||||
Exec=/opt/appimage-release-client/bin/appimage-release-client
|
||||
TryExec=/opt/appimage-release-client/bin/appimage-release-client
|
||||
Icon=appimage-release-client
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
StartupNotify=true
|
||||
StartupWMClass=appimage-release-client
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,125 @@
|
||||
#include "catalogclient.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include <QPixmap>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace {
|
||||
QUrl appendPath(const QUrl &base, const QString &path)
|
||||
{
|
||||
QString value = base.toString();
|
||||
while (value.endsWith('/'))
|
||||
value.chop(1);
|
||||
return QUrl(value + path);
|
||||
}
|
||||
}
|
||||
|
||||
CatalogClient::CatalogClient(QObject *parent)
|
||||
: QObject(parent), m_baseUrl(QStringLiteral("http://10.8.0.3:8092"))
|
||||
{
|
||||
}
|
||||
|
||||
void CatalogClient::setBaseUrl(const QUrl &baseUrl)
|
||||
{
|
||||
m_baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
QUrl CatalogClient::baseUrl() const
|
||||
{
|
||||
return m_baseUrl;
|
||||
}
|
||||
|
||||
void CatalogClient::setBusy(bool busy)
|
||||
{
|
||||
emit busyChanged(busy);
|
||||
}
|
||||
|
||||
void CatalogClient::getJson(const QUrl &url, const std::function<void(const QJsonObject &)> &callback)
|
||||
{
|
||||
if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty()) {
|
||||
emit requestFailed(QStringLiteral("URL non valida: %1").arg(url.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader,
|
||||
QStringLiteral("AppImage Release Client/%1").arg(APP_VERSION));
|
||||
auto *reply = m_network.get(request);
|
||||
++m_pendingRequests;
|
||||
setBusy(true);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, callback]() {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
emit requestFailed(QStringLiteral("Richiesta fallita: %1").arg(reply->errorString()));
|
||||
reply->deleteLater();
|
||||
--m_pendingRequests;
|
||||
if (m_pendingRequests == 0)
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
emit requestFailed(QStringLiteral("Risposta JSON non valida: %1").arg(parseError.errorString()));
|
||||
reply->deleteLater();
|
||||
--m_pendingRequests;
|
||||
if (m_pendingRequests == 0)
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(document.object());
|
||||
reply->deleteLater();
|
||||
--m_pendingRequests;
|
||||
if (m_pendingRequests == 0)
|
||||
setBusy(false);
|
||||
});
|
||||
}
|
||||
|
||||
void CatalogClient::fetchIndex()
|
||||
{
|
||||
getJson(appendPath(m_baseUrl, QStringLiteral("/api/index.json")),
|
||||
[this](const QJsonObject &root) {
|
||||
QVariantMap repository = root.value(QStringLiteral("repository")).toObject().toVariantMap();
|
||||
QVector<QVariantMap> applications;
|
||||
const QJsonArray array = root.value(QStringLiteral("applications")).toArray();
|
||||
applications.reserve(array.size());
|
||||
for (const QJsonValue &value : array) {
|
||||
if (value.isObject())
|
||||
applications.append(value.toObject().toVariantMap());
|
||||
}
|
||||
emit indexReady(repository, applications);
|
||||
});
|
||||
}
|
||||
|
||||
void CatalogClient::fetchDetails(const QUrl &url)
|
||||
{
|
||||
getJson(url, [this](const QJsonObject &details) {
|
||||
emit detailsReady(details.toVariantMap());
|
||||
});
|
||||
}
|
||||
|
||||
void CatalogClient::fetchIcon(const QUrl &url)
|
||||
{
|
||||
if (!url.isValid())
|
||||
return;
|
||||
|
||||
auto *reply = m_network.get(QNetworkRequest(url));
|
||||
++m_pendingRequests;
|
||||
setBusy(true);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QPixmap icon;
|
||||
if (icon.loadFromData(reply->readAll()))
|
||||
emit iconReady(icon);
|
||||
}
|
||||
reply->deleteLater();
|
||||
--m_pendingRequests;
|
||||
if (m_pendingRequests == 0)
|
||||
setBusy(false);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QVariantMap>
|
||||
#include <QVector>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QPixmap;
|
||||
|
||||
class CatalogClient final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CatalogClient(QObject *parent = nullptr);
|
||||
|
||||
void setBaseUrl(const QUrl &baseUrl);
|
||||
QUrl baseUrl() const;
|
||||
void fetchIndex();
|
||||
void fetchDetails(const QUrl &url);
|
||||
void fetchIcon(const QUrl &url);
|
||||
|
||||
signals:
|
||||
void indexReady(const QVariantMap &repository, const QVector<QVariantMap> &applications);
|
||||
void detailsReady(const QVariantMap &details);
|
||||
void iconReady(const QPixmap &icon);
|
||||
void requestFailed(const QString &message);
|
||||
void busyChanged(bool busy);
|
||||
|
||||
private:
|
||||
void getJson(const QUrl &url, const std::function<void(const QJsonObject &)> &callback);
|
||||
void setBusy(bool busy);
|
||||
|
||||
QNetworkAccessManager m_network;
|
||||
QUrl m_baseUrl;
|
||||
int m_pendingRequests = 0;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QStyleFactory>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName(QStringLiteral("AppImage Release Catalog"));
|
||||
QApplication::setApplicationVersion(QStringLiteral(APP_VERSION));
|
||||
QApplication::setOrganizationName(QStringLiteral("enne2"));
|
||||
|
||||
MainWindow window;
|
||||
window.show();
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include "catalogclient.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QTextBrowser>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
QVariantMap mapValue(const QVariantMap &map, const QString &key)
|
||||
{
|
||||
return map.value(key).toMap();
|
||||
}
|
||||
|
||||
QString urlValue(const QVariantMap &map, const QString &key)
|
||||
{
|
||||
return map.value(key).toString();
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), m_client(new CatalogClient(this))
|
||||
{
|
||||
buildUi();
|
||||
connect(m_client, &CatalogClient::indexReady, this, &MainWindow::showIndex);
|
||||
connect(m_client, &CatalogClient::detailsReady, this, &MainWindow::showDetails);
|
||||
connect(m_client, &CatalogClient::iconReady, this, &MainWindow::showIcon);
|
||||
connect(m_client, &CatalogClient::requestFailed, this, &MainWindow::showError);
|
||||
connect(m_client, &CatalogClient::busyChanged, this, &MainWindow::setBusy);
|
||||
|
||||
refreshCatalog();
|
||||
}
|
||||
|
||||
void MainWindow::buildUi()
|
||||
{
|
||||
setWindowTitle(QStringLiteral("AppImage Release Catalog"));
|
||||
resize(1120, 720);
|
||||
|
||||
auto *central = new QWidget(this);
|
||||
auto *rootLayout = new QVBoxLayout(central);
|
||||
rootLayout->setContentsMargins(12, 12, 12, 8);
|
||||
rootLayout->setSpacing(8);
|
||||
|
||||
auto *connectionLayout = new QHBoxLayout;
|
||||
connectionLayout->addWidget(new QLabel(QStringLiteral("Catalogo:"), central));
|
||||
m_baseUrlEdit = new QLineEdit(QStringLiteral("http://10.8.0.3:8092"), central);
|
||||
m_baseUrlEdit->setPlaceholderText(QStringLiteral("https://host:porta"));
|
||||
m_baseUrlEdit->setToolTip(QStringLiteral("URL base del repository AppImage"));
|
||||
connectionLayout->addWidget(m_baseUrlEdit, 1);
|
||||
m_refreshButton = new QPushButton(QStringLiteral("Aggiorna"), central);
|
||||
m_refreshButton->setDefault(true);
|
||||
connectionLayout->addWidget(m_refreshButton);
|
||||
rootLayout->addLayout(connectionLayout);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Horizontal, central);
|
||||
splitter->setChildrenCollapsible(false);
|
||||
|
||||
auto *leftPanel = new QWidget(splitter);
|
||||
auto *leftLayout = new QVBoxLayout(leftPanel);
|
||||
leftLayout->setContentsMargins(0, 0, 8, 0);
|
||||
leftLayout->addWidget(new QLabel(QStringLiteral("Applicazioni"), leftPanel));
|
||||
m_applicationList = new QListWidget(leftPanel);
|
||||
m_applicationList->setMinimumWidth(280);
|
||||
m_applicationList->setAlternatingRowColors(true);
|
||||
leftLayout->addWidget(m_applicationList, 1);
|
||||
|
||||
auto *rightPanel = new QWidget(splitter);
|
||||
auto *rightLayout = new QVBoxLayout(rightPanel);
|
||||
rightLayout->setContentsMargins(8, 0, 0, 0);
|
||||
|
||||
auto *headingLayout = new QHBoxLayout;
|
||||
m_iconLabel = new QLabel(rightPanel);
|
||||
m_iconLabel->setFixedSize(80, 80);
|
||||
m_iconLabel->setAlignment(Qt::AlignCenter);
|
||||
m_iconLabel->setStyleSheet(QStringLiteral("background: palette(alternate-base); border-radius: 8px;"));
|
||||
headingLayout->addWidget(m_iconLabel);
|
||||
auto *titleLayout = new QVBoxLayout;
|
||||
m_nameLabel = new QLabel(QStringLiteral("Seleziona un'applicazione"), rightPanel);
|
||||
m_nameLabel->setStyleSheet(QStringLiteral("font-size: 20px; font-weight: 600;"));
|
||||
m_nameLabel->setWordWrap(true);
|
||||
titleLayout->addWidget(m_nameLabel);
|
||||
m_summaryLabel = new QLabel(rightPanel);
|
||||
m_summaryLabel->setWordWrap(true);
|
||||
m_summaryLabel->setStyleSheet(QStringLiteral("color: palette(placeholder-text);"));
|
||||
titleLayout->addWidget(m_summaryLabel);
|
||||
headingLayout->addLayout(titleLayout, 1);
|
||||
rightLayout->addLayout(headingLayout);
|
||||
|
||||
m_descriptionLabel = new QLabel(rightPanel);
|
||||
m_descriptionLabel->setWordWrap(true);
|
||||
m_descriptionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
rightLayout->addWidget(m_descriptionLabel);
|
||||
|
||||
m_metadataLabel = new QLabel(rightPanel);
|
||||
m_metadataLabel->setWordWrap(true);
|
||||
m_metadataLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
rightLayout->addWidget(m_metadataLabel);
|
||||
|
||||
auto *releaseTitle = new QLabel(QStringLiteral("Release"), rightPanel);
|
||||
releaseTitle->setStyleSheet(QStringLiteral("font-weight: 600; margin-top: 8px;"));
|
||||
rightLayout->addWidget(releaseTitle);
|
||||
m_releaseTable = new QTableWidget(0, 7, rightPanel);
|
||||
m_releaseTable->setHorizontalHeaderLabels({QStringLiteral("Version"), QStringLiteral("Canale"),
|
||||
QStringLiteral("Arch"), QStringLiteral("Data"),
|
||||
QStringLiteral("Dimensione"), QStringLiteral("File"),
|
||||
QStringLiteral("SHA-256")});
|
||||
m_releaseTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_releaseTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_releaseTable->setAlternatingRowColors(true);
|
||||
m_releaseTable->horizontalHeader()->setStretchLastSection(true);
|
||||
m_releaseTable->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
|
||||
rightLayout->addWidget(m_releaseTable, 1);
|
||||
|
||||
auto *actionsLayout = new QHBoxLayout;
|
||||
m_downloadButton = new QPushButton(QStringLiteral("Apri download AppImage"), rightPanel);
|
||||
m_zsyncButton = new QPushButton(QStringLiteral("Apri zsync"), rightPanel);
|
||||
m_projectButton = new QPushButton(QStringLiteral("Apri progetto"), rightPanel);
|
||||
actionsLayout->addWidget(m_downloadButton);
|
||||
actionsLayout->addWidget(m_zsyncButton);
|
||||
actionsLayout->addWidget(m_projectButton);
|
||||
actionsLayout->addStretch();
|
||||
rightLayout->addLayout(actionsLayout);
|
||||
|
||||
splitter->addWidget(leftPanel);
|
||||
splitter->addWidget(rightPanel);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
rootLayout->addWidget(splitter, 1);
|
||||
|
||||
m_statusLabel = new QLabel(QStringLiteral("Pronto"), central);
|
||||
m_statusLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
rootLayout->addWidget(m_statusLabel);
|
||||
setCentralWidget(central);
|
||||
|
||||
connect(m_refreshButton, &QPushButton::clicked, this, &MainWindow::refreshCatalog);
|
||||
connect(m_baseUrlEdit, &QLineEdit::returnPressed, this, &MainWindow::refreshCatalog);
|
||||
connect(m_applicationList, &QListWidget::itemClicked, this, &MainWindow::selectApplication);
|
||||
connect(m_downloadButton, &QPushButton::clicked, this, [this] { openSelectedUrl(QStringLiteral("stableUrl")); });
|
||||
connect(m_zsyncButton, &QPushButton::clicked, this, [this] { openSelectedUrl(QStringLiteral("zsyncUrl")); });
|
||||
connect(m_projectButton, &QPushButton::clicked, this, [this] { openSelectedUrl(QStringLiteral("projectUrl")); });
|
||||
|
||||
clearDetails(QStringLiteral("Seleziona un'applicazione per visualizzare le release."));
|
||||
}
|
||||
|
||||
void MainWindow::refreshCatalog()
|
||||
{
|
||||
const QUrl url(m_baseUrlEdit->text().trimmed());
|
||||
if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty()) {
|
||||
showError(QStringLiteral("Inserisci un URL base valido, ad esempio http://10.8.0.3:8092"));
|
||||
return;
|
||||
}
|
||||
m_client->setBaseUrl(url);
|
||||
m_statusLabel->setText(QStringLiteral("Caricamento catalogo…"));
|
||||
m_client->fetchIndex();
|
||||
}
|
||||
|
||||
void MainWindow::selectApplication(QListWidgetItem *item)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
const int index = item->data(Qt::UserRole).toInt();
|
||||
if (index < 0 || index >= m_applications.size())
|
||||
return;
|
||||
|
||||
const QVariantMap app = m_applications.at(index);
|
||||
m_statusLabel->setText(QStringLiteral("Caricamento dettagli…"));
|
||||
clearDetails(QStringLiteral("Caricamento dettagli dell'applicazione…"));
|
||||
m_client->fetchDetails(QUrl(urlValue(app, QStringLiteral("detailsUrl"))));
|
||||
}
|
||||
|
||||
void MainWindow::showIndex(const QVariantMap &repository, const QVector<QVariantMap> &applications)
|
||||
{
|
||||
m_applications = applications;
|
||||
m_applicationList->clear();
|
||||
|
||||
const QString repositoryName = stringValue(repository, QStringLiteral("name"));
|
||||
const QString updatedAt = stringValue(repository, QStringLiteral("updatedAt"));
|
||||
m_statusLabel->setText(QStringLiteral("%1 — %2 applicazioni — aggiornato %3")
|
||||
.arg(repositoryName.isEmpty() ? QStringLiteral("Catalogo") : repositoryName)
|
||||
.arg(applications.size())
|
||||
.arg(updatedAt.isEmpty() ? QStringLiteral("n/d") : updatedAt));
|
||||
|
||||
for (int i = 0; i < applications.size(); ++i) {
|
||||
const QVariantMap app = applications.at(i);
|
||||
auto *item = new QListWidgetItem(stringValue(app, QStringLiteral("name")), m_applicationList);
|
||||
item->setData(Qt::UserRole, i);
|
||||
item->setToolTip(stringValue(app, QStringLiteral("summary")));
|
||||
}
|
||||
|
||||
if (!applications.isEmpty()) {
|
||||
m_applicationList->setCurrentRow(0);
|
||||
selectApplication(m_applicationList->item(0));
|
||||
} else {
|
||||
clearDetails(QStringLiteral("Il catalogo non contiene applicazioni."));
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::showDetails(const QVariantMap &details)
|
||||
{
|
||||
m_selectedDetails = details;
|
||||
m_nameLabel->setText(stringValue(details, QStringLiteral("name")));
|
||||
m_summaryLabel->setText(stringValue(details, QStringLiteral("summary")));
|
||||
m_descriptionLabel->setText(stringValue(details, QStringLiteral("description")));
|
||||
|
||||
const QVariantMap developer = mapValue(details, QStringLiteral("developer"));
|
||||
const QStringList categories = details.value(QStringLiteral("categories")).toStringList();
|
||||
const QStringList licenses = details.value(QStringLiteral("licenses")).toStringList();
|
||||
m_metadataLabel->setText(QStringLiteral("Sviluppatore: %1 • Categorie: %2 • Licenze: %3")
|
||||
.arg(stringValue(developer, QStringLiteral("name")))
|
||||
.arg(categories.join(QStringLiteral(", ")))
|
||||
.arg(licenses.join(QStringLiteral(", "))));
|
||||
|
||||
m_releaseTable->setRowCount(0);
|
||||
const QVariantList releases = details.value(QStringLiteral("releases")).toList();
|
||||
if (!releases.isEmpty()) {
|
||||
m_selectedRelease = releases.first().toMap();
|
||||
for (const QVariant &value : releases)
|
||||
populateRelease(value.toMap());
|
||||
} else {
|
||||
m_selectedRelease.clear();
|
||||
}
|
||||
|
||||
m_downloadButton->setEnabled(!m_selectedRelease.isEmpty());
|
||||
m_zsyncButton->setEnabled(!m_selectedRelease.isEmpty());
|
||||
m_projectButton->setEnabled(!urlValue(details, QStringLiteral("projectUrl")).isEmpty());
|
||||
|
||||
const QString iconUrl = urlValue(details, QStringLiteral("icon"));
|
||||
if (iconUrl.isEmpty())
|
||||
showIcon(QPixmap());
|
||||
else
|
||||
m_client->fetchIcon(QUrl(iconUrl));
|
||||
m_statusLabel->setText(QStringLiteral("Dettagli caricati"));
|
||||
}
|
||||
|
||||
void MainWindow::populateRelease(const QVariantMap &release)
|
||||
{
|
||||
const int row = m_releaseTable->rowCount();
|
||||
m_releaseTable->insertRow(row);
|
||||
const QStringList values = {
|
||||
stringValue(release, QStringLiteral("version")),
|
||||
stringValue(release, QStringLiteral("channel")),
|
||||
stringValue(release, QStringLiteral("arch")),
|
||||
stringValue(release, QStringLiteral("date")),
|
||||
formatBytes(release.value(QStringLiteral("size")).toLongLong()),
|
||||
stringValue(release, QStringLiteral("file")),
|
||||
stringValue(release, QStringLiteral("sha256"))
|
||||
};
|
||||
for (int column = 0; column < values.size(); ++column)
|
||||
m_releaseTable->setItem(row, column, new QTableWidgetItem(values.at(column)));
|
||||
}
|
||||
|
||||
void MainWindow::showIcon(const QPixmap &icon)
|
||||
{
|
||||
if (icon.isNull()) {
|
||||
m_iconLabel->setPixmap(QPixmap());
|
||||
m_iconLabel->setText(QStringLiteral("APP"));
|
||||
return;
|
||||
}
|
||||
m_iconLabel->setText(QString());
|
||||
m_iconLabel->setPixmap(icon.scaled(m_iconLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
void MainWindow::openSelectedUrl(const QString &field)
|
||||
{
|
||||
QString value;
|
||||
if (field == QStringLiteral("projectUrl"))
|
||||
value = urlValue(m_selectedDetails, field);
|
||||
else
|
||||
value = urlValue(m_selectedRelease, field);
|
||||
if (!value.isEmpty())
|
||||
QDesktopServices::openUrl(QUrl(value));
|
||||
}
|
||||
|
||||
void MainWindow::clearDetails(const QString &message)
|
||||
{
|
||||
m_nameLabel->setText(QStringLiteral("AppImage Release Catalog"));
|
||||
m_summaryLabel->clear();
|
||||
m_descriptionLabel->setText(message);
|
||||
m_metadataLabel->clear();
|
||||
m_releaseTable->setRowCount(0);
|
||||
m_selectedRelease.clear();
|
||||
m_downloadButton->setEnabled(false);
|
||||
m_zsyncButton->setEnabled(false);
|
||||
m_projectButton->setEnabled(false);
|
||||
showIcon(QPixmap());
|
||||
}
|
||||
|
||||
void MainWindow::showError(const QString &message)
|
||||
{
|
||||
m_statusLabel->setText(message);
|
||||
}
|
||||
|
||||
void MainWindow::setBusy(bool busy)
|
||||
{
|
||||
m_refreshButton->setEnabled(!busy);
|
||||
if (busy)
|
||||
m_refreshButton->setText(QStringLiteral("Caricamento…"));
|
||||
else
|
||||
m_refreshButton->setText(QStringLiteral("Aggiorna"));
|
||||
}
|
||||
|
||||
QString MainWindow::stringValue(const QVariantMap &map, const QString &key)
|
||||
{
|
||||
return map.value(key).toString();
|
||||
}
|
||||
|
||||
QString MainWindow::formatBytes(qint64 bytes)
|
||||
{
|
||||
if (bytes < 0)
|
||||
return QStringLiteral("n/d");
|
||||
if (bytes >= 1024LL * 1024 * 1024)
|
||||
return QStringLiteral("%1 GiB").arg(bytes / (1024.0 * 1024 * 1024), 0, 'f', 2);
|
||||
if (bytes >= 1024LL * 1024)
|
||||
return QStringLiteral("%1 MiB").arg(bytes / (1024.0 * 1024), 0, 'f', 1);
|
||||
if (bytes >= 1024)
|
||||
return QStringLiteral("%1 KiB").arg(bytes / 1024.0, 0, 'f', 1);
|
||||
return QStringLiteral("%1 B").arg(bytes);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QVariantMap>
|
||||
#include <QVector>
|
||||
|
||||
class CatalogClient;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPixmap;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
class QTableWidget;
|
||||
|
||||
class MainWindow final : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void refreshCatalog();
|
||||
void selectApplication(QListWidgetItem *item);
|
||||
void showIndex(const QVariantMap &repository, const QVector<QVariantMap> &applications);
|
||||
void showDetails(const QVariantMap &details);
|
||||
void showIcon(const QPixmap &icon);
|
||||
void showError(const QString &message);
|
||||
void setBusy(bool busy);
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void clearDetails(const QString &message);
|
||||
void populateRelease(const QVariantMap &release);
|
||||
void openSelectedUrl(const QString &field);
|
||||
static QString formatBytes(qint64 bytes);
|
||||
static QString stringValue(const QVariantMap &map, const QString &key);
|
||||
|
||||
CatalogClient *m_client = nullptr;
|
||||
QLineEdit *m_baseUrlEdit = nullptr;
|
||||
QPushButton *m_refreshButton = nullptr;
|
||||
QListWidget *m_applicationList = nullptr;
|
||||
QLabel *m_iconLabel = nullptr;
|
||||
QLabel *m_nameLabel = nullptr;
|
||||
QLabel *m_summaryLabel = nullptr;
|
||||
QLabel *m_descriptionLabel = nullptr;
|
||||
QLabel *m_metadataLabel = nullptr;
|
||||
QTableWidget *m_releaseTable = nullptr;
|
||||
QPushButton *m_downloadButton = nullptr;
|
||||
QPushButton *m_zsyncButton = nullptr;
|
||||
QPushButton *m_projectButton = nullptr;
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
|
||||
QVector<QVariantMap> m_applications;
|
||||
QVariantMap m_selectedDetails;
|
||||
QVariantMap m_selectedRelease;
|
||||
};
|
||||
Reference in New Issue
Block a user