mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-23 07:49:00 +02:00
WIP: [frontend] Built-in auto updater
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. Untested but should auto-mount the DMG on macOS and open up the zip in Windows? Needs work on Android, but I don't feel like doing it Signed-off-by: crueter <crueter@eden-emu.dev>
This commit is contained in:
parent
3d0eb4b5d7
commit
7273540d43
17 changed files with 718 additions and 327 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
|
||||
update_dialog.h update_dialog.cpp 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
|
||||
// Qt on macOS doesn't define VMA shit
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <qttranslation.h>
|
||||
#include "common/settings.h"
|
||||
#include "common/settings_enums.h"
|
||||
#include "frontend_common/settings_generator.h"
|
||||
#include "qt_common/qt_string_lookup.h"
|
||||
#include "render/performance_overlay.h"
|
||||
#include "update_dialog.h"
|
||||
#if defined(QT_STATICPLUGIN) && !defined(__APPLE__)
|
||||
#undef VMA_IMPLEMENTATION
|
||||
#endif
|
||||
|
|
@ -539,7 +541,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 +4220,23 @@ 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();
|
||||
|
||||
// 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->title)));
|
||||
// update_prompt.exec();
|
||||
// if (update_prompt.button(QMessageBox::Yes) == update_prompt.clickedButton()) {
|
||||
// QDesktopServices::openUrl(QUrl(QString::fromStdString(version->html_url)));
|
||||
// }
|
||||
}
|
||||
#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;
|
||||
|
|
|
|||
171
src/yuzu/update_dialog.cpp
Normal file
171
src/yuzu/update_dialog.cpp
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
#include <QRadioButton>
|
||||
#include <QStandardPaths>
|
||||
#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 <QSaveFile>
|
||||
|
||||
#include "common/httplib.h"
|
||||
|
||||
#ifdef YUZU_BUNDLED_OPENSSL
|
||||
#include <openssl/cert.h>
|
||||
|
||||
#include <QDesktopServices>
|
||||
#endif
|
||||
|
||||
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
|
||||
u32 i = 0;
|
||||
for (const Common::Net::Asset& a : release.GetAssets()) {
|
||||
QRadioButton* r = new QRadioButton(tr(a.name.c_str()), this);
|
||||
if (i == 0) r->setChecked(true);
|
||||
++i;
|
||||
|
||||
r->setProperty("url", QString::fromStdString(a.url));
|
||||
r->setProperty("path", QString::fromStdString(a.path));
|
||||
r->setProperty("filename", QString::fromStdString(a.filename));
|
||||
|
||||
ui->radioButtons->addWidget(r);
|
||||
m_buttons.append(r);
|
||||
}
|
||||
|
||||
connect(this, &QDialog::accepted, this, &UpdateDialog::Download);
|
||||
}
|
||||
|
||||
UpdateDialog::~UpdateDialog() {
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void UpdateDialog::Download() {
|
||||
std::string url, path, asset_filename;
|
||||
for (QRadioButton* r : std::as_const(m_buttons)) {
|
||||
if (r->isChecked()) {
|
||||
url = r->property("url").toString().toStdString();
|
||||
path = r->property("path").toString().toStdString();
|
||||
asset_filename = r->property("filename").toString().toStdString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (url.empty())
|
||||
return;
|
||||
|
||||
const auto filename = QtCommon::Frontend::GetSaveFileName(
|
||||
tr("New Version Location"),
|
||||
qApp->applicationDirPath() % QStringLiteral("/") % QString::fromStdString(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>(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 {}{}", url, 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(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", url, path);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& response = result.value();
|
||||
if (response.status >= 400) {
|
||||
LOG_ERROR(Frontend, "GET to {}{} returned error status code: {}", url, path,
|
||||
response.status);
|
||||
QtCommon::Frontend::Critical(
|
||||
tr("Failed to download file"),
|
||||
tr("Could not download from %1%2\nError code: %3")
|
||||
.arg(QString::fromStdString(url), QString::fromStdString(path), QString::number(response.status)));
|
||||
return;
|
||||
}
|
||||
if (!response.headers.contains("content-type")) {
|
||||
LOG_ERROR(Frontend, "GET to {}{} returned no content", url, path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download is complete. User may choose to open in the file manager.
|
||||
// TODO(crueter): Auto-extract for zip, auto-open for DMG
|
||||
// e.g. download to tmp directory?
|
||||
|
||||
auto button =
|
||||
QtCommon::Frontend::Question(tr("Download Complete"),
|
||||
tr("Successfully downloaded %1. Would you like to open it?")
|
||||
.arg(QString::fromStdString(asset_filename)),
|
||||
QtCommon::Frontend::Yes | QtCommon::Frontend::No);
|
||||
|
||||
if (button == QtCommon::Frontend::Yes) {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(filename));
|
||||
}
|
||||
}
|
||||
23
src/yuzu/update_dialog.h
Normal file
23
src/yuzu/update_dialog.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#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;
|
||||
};
|
||||
106
src/yuzu/update_dialog.ui
Normal file
106
src/yuzu/update_dialog.ui
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?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="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="1" column="0" colspan="2">
|
||||
<widget class="QTextEdit" name="body">
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</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>
|
||||
</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