// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include "common/logging.h" #include "qt_common/abstract/frontend.h" #include "qt_common/abstract/progress.h" #include "ui_update_dialog.h" #include "update_dialog.h" #include "common/httplib.h" #ifdef YUZU_BUNDLED_OPENSSL #include #endif #include #undef GetSaveFileName UpdateDialog::UpdateDialog(const Common::Net::Release& release, QWidget* parent) : QDialog(parent), ui(new Ui::UpdateDialog) { ui->setupUi(this); ui->version->setText( tr("%1 is available for download.").arg(QString::fromStdString(release.title))); ui->url->setText( tr("View on Forgejo").arg(QString::fromStdString(release.html_url))); std::string text{release.body}; if (auto pos = text.find("# Packages"); pos != std::string::npos) { text = text.substr(0, pos); } ui->body->setMarkdown(QString::fromStdString(text)); // TODO(crueter): Find a way to set default const auto assets = release.GetPlatformAssets(); if (assets.empty()) { ui->groupBox->setHidden(true); connect(this, &QDialog::accepted, this, [release]() { QDesktopServices::openUrl(QUrl{QString::fromStdString(release.html_url)}); }); } else if (assets.size() == 1) { m_asset = assets[0]; connect(this, &QDialog::accepted, this, &UpdateDialog::Download); } else { u32 i = 0; for (const Common::Net::Asset& a : assets) { QRadioButton* r = new QRadioButton(tr(a.name.c_str()), this); connect(r, &QRadioButton::toggled, this, [a, this](bool checked) { if (checked) m_asset = a; }); if (i == 0) r->setChecked(true); ++i; ui->radioButtons->addWidget(r); } connect(this, &QDialog::accepted, this, &UpdateDialog::Download); } } UpdateDialog::~UpdateDialog() { delete ui; } void UpdateDialog::Download() { const auto filename = QtCommon::Frontend::GetSaveFileName( tr("New Version Location"), qApp->applicationDirPath() % QStringLiteral("/") % QString::fromStdString(m_asset.filename), tr("All Files (*.*)")); if (filename.isEmpty()) return; QSaveFile file(filename); if (!file.open(QIODevice::Truncate | QIODevice::WriteOnly)) { LOG_WARNING(Frontend, "Could not open file {}", filename.toStdString()); QtCommon::Frontend::Critical(tr("Failed to save file"), tr("Could not open file %1 for writing.").arg(filename)); return; } // TODO(crueter): Move to net.cpp constexpr std::size_t timeout_seconds = 15; std::unique_ptr client = std::make_unique(m_asset.url); client->set_connection_timeout(timeout_seconds); client->set_read_timeout(timeout_seconds); client->set_write_timeout(timeout_seconds); #if defined(YUZU_BUNDLED_OPENSSL) && !defined(__OPENORBIS__) client->load_ca_cert_store(kCert, sizeof(kCert)); #endif if (client == nullptr) { LOG_ERROR(Frontend, "Invalid URL {}{}", m_asset.url, m_asset.path); return; } auto progress = QtCommon::Frontend::newProgressDialog(tr("Downloading..."), tr("Cancel"), 0, 100); progress->show(); QGuiApplication::processEvents(); // Progress dialog. auto progress_callback = [&](size_t processed_size, size_t total_size) { QGuiApplication::processEvents(); progress->setValue(static_cast((processed_size * 100) / total_size)); return !progress->wasCanceled(); }; // Write file in chunks. auto content_receiver = [&file, filename](const char* t_data, size_t data_length) -> bool { if (file.write(t_data, data_length) == -1) { LOG_WARNING(Frontend, "Could not write {} bytes to file {}", data_length, filename.toStdString()); QtCommon::Frontend::Critical(tr("Failed to save file"), tr("Could not write to file %1.").arg(filename)); return false; } return true; }; // Now send off request auto result = client->Get(m_asset.path, content_receiver, progress_callback); progress->close(); // commit to file if (!file.commit()) { LOG_WARNING(Frontend, "Could not commit to file {}", filename.toStdString()); QtCommon::Frontend::Critical(tr("Failed to save file"), tr("Could not commit to file %1.").arg(filename)); } if (!result) { LOG_ERROR(Frontend, "GET to {}{} returned null", m_asset.url, m_asset.path); return; } const auto& response = result.value(); if (response.status >= 400) { LOG_ERROR(Frontend, "GET to {}{} returned error status code: {}", m_asset.url, m_asset.path, response.status); QtCommon::Frontend::Critical(tr("Failed to download file"), tr("Could not download from %1%2\nError code: %3") .arg(QString::fromStdString(m_asset.url), QString::fromStdString(m_asset.path), QString::number(response.status))); return; } if (!response.headers.contains("content-type")) { LOG_ERROR(Frontend, "GET to {}{} returned no content", m_asset.url, m_asset.path); return; } // Download is complete. User may choose to open in the file manager. auto button = QtCommon::Frontend::Question(tr("Download Complete"), tr("Successfully downloaded %1. Would you like to open it?") .arg(QString::fromStdString(m_asset.filename)), QtCommon::Frontend::Yes | QtCommon::Frontend::No); if (button == QtCommon::Frontend::Yes) { QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); } }