mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-05-17 12:27:03 +02:00
[frontend] Built-in auto updater (#3845)
Checks latest release and opens a dialog containing the changelog, and allow the user to select a specific build to download. After downloading, it prompts the user to open it. On Windows, this just opens up the zip in File Explorer. In the future setup files will be available. On macOS this opens up the DMG in Finder so the user can drag it to the Applications folder. Android retains the auto-update functionality from before, but updated to the new scheme. Body/View on Forgejo are not implemented, that should be in a future PR. Additionally, moved some common httplib incantations to `Common::Net`. This will serve as the common network accessor and JSON parser from here on out. TODO: - [x] android :( - [x] Search for builds based on keywords, with weights towards certain builds (e.g. macOS will search for dmg then tar.gz, windows msvc then mingw/exe then zip, etc.) - [x] remove linux leftovers - [x] don't allow asset selection on platforms w/o assets - [x] nightly changelog should be in the real FUTURE IMPLEMENTATION: - [ ] Body/View on Forgejo for Android - [ ] Setup files for Windows (Eden/nightly are separate) -- maybe portable/setup selector? - [ ] Something else I'm forgetting Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3845
This commit is contained in:
parent
77decca678
commit
676b1aabfc
23 changed files with 856 additions and 375 deletions
|
|
@ -245,6 +245,8 @@ add_executable(yuzu
|
|||
|
||||
render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui
|
||||
libqt_common.h libqt_common.cpp
|
||||
updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui
|
||||
|
||||
)
|
||||
|
||||
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
|
||||
|
|
|
|||
|
|
@ -330,7 +330,6 @@ void ConfigurePerGameAddons::LoadConfiguration() {
|
|||
if (is_external_update) {
|
||||
first_item->setData(static_cast<quint32>(patch.numeric_version), NUMERIC_VERSION);
|
||||
} else if (is_mod) {
|
||||
// qDebug() << patch.location;
|
||||
first_item->setData(QString::fromStdString(patch.location), PATCH_LOCATION);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "frontend_common/settings_generator.h"
|
||||
#include "qt_common/qt_string_lookup.h"
|
||||
#include "render/performance_overlay.h"
|
||||
#include "updater/update_dialog.h"
|
||||
#if defined(QT_STATICPLUGIN) && !defined(__APPLE__)
|
||||
#undef VMA_IMPLEMENTATION
|
||||
#endif
|
||||
|
|
@ -539,7 +540,7 @@ MainWindow::MainWindow(bool has_broken_vulkan)
|
|||
#ifdef ENABLE_UPDATE_CHECKER
|
||||
if (UISettings::values.check_for_updates) {
|
||||
update_future = QtConcurrent::run(
|
||||
[]() -> std::optional<UpdateChecker::Update> { return UpdateChecker::GetUpdate(); });
|
||||
[]() -> std::optional<Common::Net::Release> { return UpdateChecker::GetUpdate(); });
|
||||
update_watcher.connect(&update_watcher, &QFutureWatcher<QString>::finished, this,
|
||||
&MainWindow::OnEmulatorUpdateAvailable);
|
||||
update_watcher.setFuture(update_future);
|
||||
|
|
@ -4218,23 +4219,12 @@ void MainWindow::OnCaptureScreenshot() {
|
|||
|
||||
#ifdef ENABLE_UPDATE_CHECKER
|
||||
void MainWindow::OnEmulatorUpdateAvailable() {
|
||||
std::optional<UpdateChecker::Update> version = update_future.result();
|
||||
std::optional<Common::Net::Release> version = update_future.result();
|
||||
if (!version)
|
||||
return;
|
||||
|
||||
QMessageBox update_prompt(this);
|
||||
update_prompt.setWindowTitle(tr("Update Available"));
|
||||
update_prompt.setIcon(QMessageBox::Information);
|
||||
update_prompt.addButton(QMessageBox::Yes);
|
||||
update_prompt.addButton(QMessageBox::Ignore);
|
||||
update_prompt.setText(tr("Download %1?").arg(QString::fromStdString(version->name)));
|
||||
update_prompt.exec();
|
||||
if (update_prompt.button(QMessageBox::Yes) == update_prompt.clickedButton()) {
|
||||
auto const full_url =
|
||||
fmt::format("{}/{}/releases/tag/", std::string{Common::g_build_auto_update_website},
|
||||
std::string{Common::g_build_auto_update_repo});
|
||||
QDesktopServices::openUrl(QUrl(QString::fromStdString(full_url + version->tag)));
|
||||
}
|
||||
UpdateDialog dialog(version.value(), this);
|
||||
dialog.exec();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -493,8 +493,8 @@ private:
|
|||
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
|
||||
|
||||
#ifdef ENABLE_UPDATE_CHECKER
|
||||
QFuture<std::optional<UpdateChecker::Update>> update_future;
|
||||
QFutureWatcher<std::optional<UpdateChecker::Update>> update_watcher;
|
||||
QFuture<std::optional<Common::Net::Release>> update_future;
|
||||
QFutureWatcher<std::optional<Common::Net::Release>> update_watcher;
|
||||
#endif
|
||||
|
||||
MultiplayerState* multiplayer_state = nullptr;
|
||||
|
|
|
|||
178
src/yuzu/updater/update_dialog.cpp
Normal file
178
src/yuzu/updater/update_dialog.cpp
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QRadioButton>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <qdesktopservices.h>
|
||||
#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 <openssl/cert.h>
|
||||
#endif
|
||||
|
||||
#include <QDesktopServices>
|
||||
|
||||
#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("<a href=\"%1\">View on Forgejo</a>").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<httplib::Client> client = std::make_unique<httplib::Client>(m_asset.url);
|
||||
client->set_connection_timeout(timeout_seconds);
|
||||
client->set_read_timeout(timeout_seconds);
|
||||
client->set_write_timeout(timeout_seconds);
|
||||
|
||||
#ifdef YUZU_BUNDLED_OPENSSL
|
||||
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<int>((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));
|
||||
}
|
||||
}
|
||||
28
src/yuzu/updater/update_dialog.h
Normal file
28
src/yuzu/updater/update_dialog.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include "common/net/net.h"
|
||||
|
||||
class QRadioButton;
|
||||
namespace Ui {
|
||||
class UpdateDialog;
|
||||
}
|
||||
|
||||
class UpdateDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UpdateDialog(const Common::Net::Release &release, QWidget* parent = nullptr);
|
||||
~UpdateDialog();
|
||||
|
||||
private slots:
|
||||
void Download();
|
||||
|
||||
private:
|
||||
Ui::UpdateDialog* ui;
|
||||
QList<QRadioButton *> m_buttons;
|
||||
Common::Net::Asset m_asset;
|
||||
};
|
||||
112
src/yuzu/updater/update_dialog.ui
Normal file
112
src/yuzu/updater/update_dialog.ui
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>UpdateDialog</class>
|
||||
<widget class="QDialog" name="UpdateDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>655</width>
|
||||
<height>551</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Update Available</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="url">
|
||||
<property name="text">
|
||||
<string><a href="%1">View on Forgejo</a></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Would you like to install this update?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::No|QDialogButtonBox::StandardButton::Yes</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Available Versions</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="radioButtons"/>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="version">
|
||||
<property name="text">
|
||||
<string>%1 is available for download.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QTextBrowser" name="body">
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>UpdateDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>UpdateDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
Loading…
Add table
Add a link
Reference in a new issue