Files
emulator/src/citron/updater/updater_dialog.cpp
2025-10-27 03:08:55 +00:00

405 lines
16 KiB
C++

// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/updater/updater_dialog.h"
#include "ui_updater_dialog.h"
#include <QApplication>
#include <QCloseEvent>
#include <QDateTime>
#include <QDesktopServices>
#include <QMessageBox>
#include <QProcess>
#include <QRegularExpression>
#include <QTimer>
#include <QUrl>
namespace Updater {
// Helper function to format the date and time nicely.
QString FormatDateTimeString(const std::string& iso_string) {
if (iso_string.empty() || iso_string == "Unknown") {
return QStringLiteral("Unknown");
}
QDateTime date_time = QDateTime::fromString(QString::fromStdString(iso_string), Qt::ISODate);
if (!date_time.isValid()) {
return QString::fromStdString(iso_string);
}
return date_time.toLocalTime().toString(QStringLiteral("MMMM d, yyyy 'at' hh:mm AP"));
}
// Helper function to reformat the changelog with the correct commit link.
QString FormatChangelog(const std::string& raw_changelog) {
QString changelog = QString::fromStdString(raw_changelog);
const QString new_url = QStringLiteral("https://git.citron-emu.org/citron/emulator/-/commits/main");
QRegularExpression regex(QStringLiteral("\\[\\`([0-9a-fA-F]{7,40})\\`\\]\\(.*?\\)"));
QString replacement = QStringLiteral("[`\\1`](%1)").arg(new_url);
changelog.replace(regex, replacement);
return changelog;
}
UpdaterDialog::UpdaterDialog(QWidget* parent)
: QDialog(parent), ui(std::make_unique<Ui::UpdaterDialog>()),
updater_service(std::make_unique<Updater::UpdaterService>(this)),
current_state(State::Checking), total_download_size(0), downloaded_bytes(0),
progress_timer(new QTimer(this)) {
ui->setupUi(this);
// Disable the default link handling behavior of the QTextBrowser.
ui->changelogText->setOpenLinks(false);
// Manually handle link clicks to ensure they always open in an external browser.
connect(ui->changelogText, &QTextBrowser::anchorClicked, this, [](const QUrl& link) {
QDesktopServices::openUrl(link);
});
// Set up connections
connect(updater_service.get(), &Updater::UpdaterService::UpdateCheckCompleted, this,
&UpdaterDialog::OnUpdateCheckCompleted);
connect(updater_service.get(), &Updater::UpdaterService::UpdateDownloadProgress, this,
&UpdaterDialog::OnUpdateDownloadProgress);
connect(updater_service.get(), &Updater::UpdaterService::UpdateInstallProgress, this,
&UpdaterDialog::OnUpdateInstallProgress);
connect(updater_service.get(), &Updater::UpdaterService::UpdateCompleted, this,
&UpdaterDialog::OnUpdateCompleted);
connect(updater_service.get(), &Updater::UpdaterService::UpdateError, this,
&UpdaterDialog::OnUpdateError);
// Set up UI connections
connect(ui->downloadButton, &QPushButton::clicked, this, &UpdaterDialog::OnDownloadButtonClicked);
connect(ui->cancelButton, &QPushButton::clicked, this, &UpdaterDialog::OnCancelButtonClicked);
connect(ui->closeButton, &QPushButton::clicked, this, &UpdaterDialog::OnCloseButtonClicked);
connect(ui->restartButton, &QPushButton::clicked, this, &UpdaterDialog::OnRestartButtonClicked);
SetupUI();
// Set up progress timer for smooth updates
progress_timer->setInterval(100); // Update every 100ms
connect(progress_timer, &QTimer::timeout, this, [this]() {
if (current_state == State::Downloading) {
ui->downloadInfoLabel->setText(
QStringLiteral("Downloaded: %1 / %2")
.arg(FormatBytes(downloaded_bytes))
.arg(FormatBytes(total_download_size)));
}
});
}
UpdaterDialog::~UpdaterDialog() = default;
void UpdaterDialog::CheckForUpdates(const std::string& update_url) {
ShowCheckingState();
updater_service->CheckForUpdates(update_url);
}
void UpdaterDialog::OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info) {
if (has_update) {
current_update_info = update_info;
ShowUpdateAvailableState();
} else {
ShowNoUpdateState(update_info);
}
}
void UpdaterDialog::OnUpdateDownloadProgress(int percentage, qint64 bytes_received,
qint64 bytes_total) {
downloaded_bytes = bytes_received;
total_download_size = bytes_total;
ui->progressBar->setValue(percentage);
ui->progressLabel->setText(QStringLiteral("Downloading update... %1%").arg(percentage));
if (!progress_timer->isActive()) {
progress_timer->start();
}
}
void UpdaterDialog::OnUpdateInstallProgress(int percentage, const QString& current_file) {
progress_timer->stop();
ui->progressBar->setValue(percentage);
ui->progressLabel->setText(QStringLiteral("Installing update... %1%").arg(percentage));
ui->downloadInfoLabel->setText(current_file);
}
void UpdaterDialog::OnUpdateCompleted(Updater::UpdaterService::UpdateResult result,
const QString& message) {
progress_timer->stop();
switch (result) {
case Updater::UpdaterService::UpdateResult::Success:
ShowCompletedState();
break;
case Updater::UpdaterService::UpdateResult::Cancelled:
close();
break;
default:
ShowErrorState();
ui->statusLabel->setText(GetUpdateMessage(result) + QStringLiteral("\n\n") + message);
break;
}
}
void UpdaterDialog::OnUpdateError(const QString& error_message) {
progress_timer->stop();
ShowErrorState();
ui->statusLabel->setText(QStringLiteral("Update failed: ") + error_message);
}
void UpdaterDialog::OnDownloadButtonClicked() {
std::string download_url;
#ifdef __linux__
if (ui->appImageSelector->isVisible() && !current_update_info.download_options.empty()) {
int current_index = ui->appImageSelector->currentIndex();
if (current_index >= 0 && static_cast<size_t>(current_index) < current_update_info.download_options.size()) {
download_url = current_update_info.download_options[current_index].url;
}
}
#endif
if (download_url.empty() && !current_update_info.download_options.empty()) {
download_url = current_update_info.download_options[0].url;
}
if (!download_url.empty()) {
ShowDownloadingState();
updater_service->DownloadAndInstallUpdate(download_url);
} else {
OnUpdateError(QStringLiteral("No download URL could be found for the update."));
}
}
void UpdaterDialog::OnCancelButtonClicked() {
if (updater_service->IsUpdateInProgress()) {
updater_service->CancelUpdate();
} else {
close();
}
}
void UpdaterDialog::OnCloseButtonClicked() {
close();
}
void UpdaterDialog::OnRestartButtonClicked() {
int ret = QMessageBox::question(this, QStringLiteral("Restart Citron"),
QStringLiteral("Are you sure you want to restart Citron now?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (ret == QMessageBox::Yes) {
QString program;
QByteArray appimage_path = qgetenv("APPIMAGE");
if (!appimage_path.isEmpty()) {
// We are running from an AppImage. The program to restart is the AppImage file itself.
program = QString::fromUtf8(appimage_path);
} else {
// Not an AppImage (e.g., Windows or a non-AppImage Linux build), use the default method.
program = QApplication::applicationFilePath();
}
QStringList arguments = QApplication::arguments();
arguments.removeFirst();
QProcess::startDetached(program, arguments);
QApplication::quit();
}
}
void UpdaterDialog::SetupUI() {
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setMinimumSize(size());
ui->currentVersionValue->setText(QString::fromStdString(updater_service->GetCurrentVersion()));
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
ShowCheckingState();
}
void UpdaterDialog::ShowCheckingState() {
current_state = State::Checking;
ui->titleLabel->setText(QStringLiteral("Checking for updates..."));
ui->statusLabel->setText(QStringLiteral("Please wait while we check for available updates..."));
ui->updateInfoGroup->setVisible(false);
ui->changelogGroup->setVisible(false);
ui->progressGroup->setVisible(false);
ui->downloadButton->setVisible(false);
ui->cancelButton->setVisible(true);
ui->closeButton->setVisible(false);
ui->restartButton->setVisible(false);
ui->cancelButton->setText(QStringLiteral("Cancel"));
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
}
void UpdaterDialog::ShowNoUpdateState(const Updater::UpdateInfo& update_info) {
current_state = State::NoUpdate;
ui->titleLabel->setText(QStringLiteral("No updates available"));
ui->statusLabel->setText(QStringLiteral("You are running the latest version of Citron."));
ui->updateInfoGroup->setVisible(true);
ui->latestVersionValue->setText(QString::fromStdString(update_info.version));
ui->releaseDateValue->setText(FormatDateTimeString(update_info.release_date));
ui->changelogGroup->setVisible(false);
ui->progressGroup->setVisible(false);
ui->downloadButton->setVisible(false);
ui->cancelButton->setVisible(false);
ui->closeButton->setVisible(true);
ui->restartButton->setVisible(false);
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
}
void UpdaterDialog::ShowUpdateAvailableState() {
current_state = State::UpdateAvailable;
ui->titleLabel->setText(QStringLiteral("Update available"));
ui->statusLabel->setText(QStringLiteral("A new version of Citron is available for download."));
ui->latestVersionValue->setText(QString::fromStdString(current_update_info.version));
ui->releaseDateValue->setText(FormatDateTimeString(current_update_info.release_date));
if (!current_update_info.changelog.empty()) {
ui->changelogText->setMarkdown(FormatChangelog(current_update_info.changelog));
} else {
ui->changelogText->setText(tr("No changelog information was provided for this update."));
}
ui->changelogGroup->setVisible(true);
#ifdef __linux__
if (current_update_info.download_options.size() > 1) {
ui->appImageSelector->clear();
for (const auto& option : current_update_info.download_options) {
ui->appImageSelector->addItem(QString::fromStdString(option.name));
}
ui->appImageSelectorLabel->setVisible(true);
ui->appImageSelector->setVisible(true);
} else {
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
}
#else
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
#endif
ui->updateInfoGroup->setVisible(true);
ui->progressGroup->setVisible(false);
ui->downloadButton->setVisible(true);
ui->cancelButton->setVisible(true);
ui->closeButton->setVisible(false);
ui->restartButton->setVisible(false);
ui->cancelButton->setText(QStringLiteral("Later"));
}
void UpdaterDialog::ShowDownloadingState() {
current_state = State::Downloading;
ui->titleLabel->setText(QStringLiteral("Downloading update..."));
ui->statusLabel->setText(
QStringLiteral("Please wait while the update is being downloaded and installed."));
ui->updateInfoGroup->setVisible(false);
ui->changelogGroup->setVisible(false);
ui->progressGroup->setVisible(true);
ui->progressLabel->setText(QStringLiteral("Preparing download..."));
ui->progressBar->setValue(0);
ui->downloadInfoLabel->setText(QStringLiteral(""));
ui->downloadButton->setVisible(false);
ui->cancelButton->setVisible(true);
ui->closeButton->setVisible(false);
ui->restartButton->setVisible(false);
ui->cancelButton->setText(QStringLiteral("Cancel"));
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
progress_timer->start();
}
void UpdaterDialog::ShowInstallingState() {
current_state = State::Installing;
ui->titleLabel->setText(QStringLiteral("Installing update..."));
ui->statusLabel->setText(QStringLiteral(
"Please wait while the update is being installed. Do not close the application."));
ui->progressLabel->setText(QStringLiteral("Installing..."));
ui->downloadInfoLabel->setText(QStringLiteral(""));
ui->cancelButton->setVisible(false);
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
}
void UpdaterDialog::ShowCompletedState() {
current_state = State::Completed;
ui->titleLabel->setText(QStringLiteral("Update ready!"));
ui->statusLabel->setText(QStringLiteral("The update has been downloaded and prepared "
"successfully. The update will be applied when you "
"restart Citron."));
ui->progressGroup->setVisible(false);
ui->downloadButton->setVisible(false);
ui->cancelButton->setVisible(false);
ui->closeButton->setVisible(true);
ui->restartButton->setVisible(true);
ui->progressBar->setValue(100);
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
}
void UpdaterDialog::ShowErrorState() {
current_state = State::Error;
ui->titleLabel->setText(QStringLiteral("Update failed"));
ui->updateInfoGroup->setVisible(false);
ui->changelogGroup->setVisible(false);
ui->progressGroup->setVisible(false);
ui->downloadButton->setVisible(false);
ui->cancelButton->setVisible(false);
ui->closeButton->setVisible(true);
ui->restartButton->setVisible(false);
ui->appImageSelectorLabel->setVisible(false);
ui->appImageSelector->setVisible(false);
}
QString UpdaterDialog::FormatBytes(qint64 bytes) const {
const QStringList units = {QStringLiteral("B"), QStringLiteral("KB"), QStringLiteral("MB"),
QStringLiteral("GB")};
double size = bytes;
int unit = 0;
while (size >= 1024.0 && unit < units.size() - 1) {
size /= 1024.0;
unit++;
}
return QStringLiteral("%1 %2")
.arg(QString::number(size, 'f', unit == 0 ? 0 : 1))
.arg(units[unit]);
}
QString UpdaterDialog::GetUpdateMessage(Updater::UpdaterService::UpdateResult result) const {
switch (result) {
case Updater::UpdaterService::UpdateResult::Success:
return QStringLiteral("Update completed successfully!");
case Updater::UpdaterService::UpdateResult::Failed:
return QStringLiteral("Update failed due to an unknown error.");
case Updater::UpdaterService::UpdateResult::Cancelled:
return QStringLiteral("Update was cancelled.");
case Updater::UpdaterService::UpdateResult::NetworkError:
return QStringLiteral("Update failed due to a network error.");
case Updater::UpdaterService::UpdateResult::ExtractionError:
return QStringLiteral("Failed to extract the update archive.");
case Updater::UpdaterService::UpdateResult::PermissionError:
return QStringLiteral("Update failed due to insufficient permissions.");
case Updater::UpdaterService::UpdateResult::InvalidArchive:
return QStringLiteral("The downloaded update archive is invalid.");
case Updater::UpdaterService::UpdateResult::NoUpdateAvailable:
return QStringLiteral("No update is available.");
default:
return QStringLiteral("Unknown error occurred.");
}
}
} // namespace Updater
#include "updater_dialog.moc"