Qt: Add automatic updater

This commit is contained in:
Connor McLaughlin 2020-08-06 22:08:22 +10:00
parent 070b16e611
commit c09bfc4d2b
13 changed files with 705 additions and 3 deletions

View File

@ -12,6 +12,9 @@ set(SRCS
audiosettingswidget.cpp audiosettingswidget.cpp
audiosettingswidget.h audiosettingswidget.h
audiosettingswidget.ui audiosettingswidget.ui
autoupdaterdialog.cpp
autoupdaterdialog.h
autoupdaterdialog.ui
consolesettingswidget.cpp consolesettingswidget.cpp
consolesettingswidget.h consolesettingswidget.h
consolesettingswidget.ui consolesettingswidget.ui

View File

@ -0,0 +1,444 @@
#include "autoupdaterdialog.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/minizip_helpers.h"
#include "common/string_util.h"
#include "qthostinterface.h"
#include "qtutils.h"
#include "scmversion/scmversion.h"
#include "unzip.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtCore/QProcess>
#include <QtCore/QString>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#include <QtWidgets/QDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QProgressDialog>
Log_SetChannel(AutoUpdaterDialog);
// Logic to detect whether we can use the auto updater.
// Currently Windows-only, and requires that the channel be defined by the buildbot.
#ifdef WIN32
#if defined(__has_include) && __has_include("scmversion/tag.h")
#include "scmversion/tag.h"
#ifdef SCM_RELEASE_TAG
#define AUTO_UPDATER_SUPPORTED
#endif
#endif
#endif
#ifdef AUTO_UPDATER_SUPPORTED
static constexpr char LATEST_TAG_URL[] = "https://api.github.com/repos/stenzek/duckstation/tags";
static constexpr char LATEST_RELEASE_URL[] =
"https://api.github.com/repos/stenzek/duckstation/releases/tags/" SCM_RELEASE_TAG;
static constexpr char UPDATE_ASSET_FILENAME[] = SCM_RELEASE_ASSET;
#else
static constexpr char LATEST_TAG_URL[] = "";
static constexpr char LATEST_RELEASE_URL[] = "";
static constexpr char UPDATE_ASSET_FILENAME[] = "";
#endif
AutoUpdaterDialog::AutoUpdaterDialog(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
: QDialog(parent), m_host_interface(host_interface)
{
m_network_access_mgr = new QNetworkAccessManager(this);
m_ui.setupUi(this);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// m_ui.description->setTextInteractionFlags(Qt::TextBrowserInteraction);
// m_ui.description->setOpenExternalLinks(true);
connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterDialog::downloadUpdateClicked);
connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterDialog::skipThisUpdateClicked);
connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterDialog::remindMeLaterClicked);
}
AutoUpdaterDialog::~AutoUpdaterDialog() = default;
bool AutoUpdaterDialog::isSupported()
{
#ifdef AUTO_UPDATER_SUPPORTED
return true;
#else
return false;
#endif
}
void AutoUpdaterDialog::reportError(const char* msg, ...)
{
std::va_list ap;
va_start(ap, msg);
std::string full_msg = StringUtil::StdStringFromFormatV(msg, ap);
va_end(ap);
QMessageBox::critical(this, tr("Updater Error"), QString::fromStdString(full_msg));
}
void AutoUpdaterDialog::queueUpdateCheck(bool display_message)
{
connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, &AutoUpdaterDialog::getLatestTagComplete);
QUrl url(QUrl::fromEncoded(QByteArray(LATEST_TAG_URL, sizeof(LATEST_TAG_URL) - 1)));
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
m_network_access_mgr->get(request);
m_display_messages = display_message;
}
void AutoUpdaterDialog::queueGetLatestRelease()
{
connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, &AutoUpdaterDialog::getLatestReleaseComplete);
QUrl url(QUrl::fromEncoded(QByteArray(LATEST_RELEASE_URL, sizeof(LATEST_RELEASE_URL) - 1)));
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
m_network_access_mgr->get(request);
}
void AutoUpdaterDialog::getLatestTagComplete(QNetworkReply* reply)
{
// this might fail due to a lack of internet connection - in which case, don't spam the user with messages every time.
m_network_access_mgr->disconnect(this);
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError)
{
const QByteArray reply_json(reply->readAll());
QJsonParseError parse_error;
QJsonDocument doc(QJsonDocument::fromJson(reply_json, &parse_error));
if (doc.isArray())
{
const QJsonArray doc_array(doc.array());
for (const QJsonValue& val : doc_array)
{
if (!val.isObject())
continue;
if (val["name"].toString() != QStringLiteral(SCM_RELEASE_TAG))
continue;
m_latest_sha = val["commit"].toObject()["sha"].toString();
if (m_latest_sha.isEmpty())
continue;
if (updateNeeded())
{
queueGetLatestRelease();
return;
}
else
{
if (m_display_messages)
QMessageBox::information(this, tr("Automatic Updater"),
tr("No updates are currently available. Please try again later."));
emit updateCheckCompleted();
return;
}
}
if (m_display_messages)
reportError("latest release not found in JSON");
}
else
{
if (m_display_messages)
reportError("JSON is not an array");
}
}
else
{
if (m_display_messages)
reportError("Failed to download latest tag info: %d", static_cast<int>(reply->error()));
}
emit updateCheckCompleted();
}
void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply)
{
m_network_access_mgr->disconnect(this);
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError)
{
const QByteArray reply_json(reply->readAll());
QJsonParseError parse_error;
QJsonDocument doc(QJsonDocument::fromJson(reply_json, &parse_error));
if (doc.isObject())
{
const QJsonObject doc_object(doc.object());
// search for the correct file
const QJsonArray assets(doc_object["assets"].toArray());
const QString asset_filename(UPDATE_ASSET_FILENAME);
for (const QJsonValue& asset : assets)
{
const QJsonObject asset_obj(asset.toObject());
if (asset_obj["name"] == asset_filename)
{
m_download_url = asset_obj["browser_download_url"].toString();
if (!m_download_url.isEmpty())
{
m_ui.currentVersion->setText(tr("Current Version: %1 (%2)").arg(g_scm_hash_str).arg(__TIMESTAMP__));
m_ui.newVersion->setText(
tr("New Version: %1 (%2)").arg(m_latest_sha).arg(doc_object["published_at"].toString()));
m_ui.updateNotes->setText(doc_object["body"].toString());
exec();
emit updateCheckCompleted();
return;
}
break;
}
}
reportError("Asset/asset download not found");
}
else
{
reportError("JSON is not an object");
}
}
else
{
reportError("Failed to download latest release info: %d", static_cast<int>(reply->error()));
}
emit updateCheckCompleted();
}
void AutoUpdaterDialog::downloadUpdateClicked()
{
QUrl url(m_download_url);
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QNetworkReply* reply = m_network_access_mgr->get(request);
QProgressDialog progress(tr("Downloading %1...").arg(m_download_url), tr("Cancel"), 0, 1);
progress.setWindowTitle(tr("Automatic Updater"));
progress.setWindowIcon(windowIcon());
progress.setAutoClose(false);
connect(reply, &QNetworkReply::downloadProgress, [&progress](quint64 received, quint64 total) {
progress.setRange(0, static_cast<int>(total));
progress.setValue(static_cast<int>(received));
});
connect(m_network_access_mgr, &QNetworkAccessManager::finished, [this, &progress](QNetworkReply* reply) {
m_network_access_mgr->disconnect();
if (reply->error() != QNetworkReply::NoError)
{
reportError("Download failed: %s", reply->errorString().toUtf8().constData());
progress.done(-1);
return;
}
const QByteArray data = reply->readAll();
if (data.isEmpty())
{
reportError("Download failed: Update is empty");
progress.done(-1);
return;
}
if (processUpdate(data))
progress.done(1);
else
progress.done(-1);
});
const int result = progress.exec();
if (result == 0)
{
// cancelled
reply->abort();
}
else if (result == 1)
{
// updater started
m_host_interface->requestExit();
done(0);
}
reply->deleteLater();
}
bool AutoUpdaterDialog::updateNeeded() const
{
QString last_checked_sha =
QString::fromStdString(m_host_interface->GetStringSettingValue("AutoUpdater", "LastVersion"));
Log_InfoPrintf("Current SHA: %s", g_scm_hash_str);
Log_InfoPrintf("Latest SHA: %s", m_latest_sha.toUtf8().constData());
Log_InfoPrintf("Last Checked SHA: %s", last_checked_sha.toUtf8().constData());
if (m_latest_sha == g_scm_hash_str || m_latest_sha == last_checked_sha)
{
Log_InfoPrintf("No update needed.");
return false;
}
Log_InfoPrintf("Update needed.");
return true;
}
void AutoUpdaterDialog::skipThisUpdateClicked()
{
m_host_interface->SetStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData());
done(0);
}
void AutoUpdaterDialog::remindMeLaterClicked()
{
done(0);
}
#ifdef WIN32
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
{
const QString update_directory = QCoreApplication::applicationDirPath();
const QString update_zip_path = update_directory + QStringLiteral("\\update.zip");
const QString updater_path = update_directory + QStringLiteral("\\updater.exe");
Q_ASSERT(!update_zip_path.isEmpty() && !updater_path.isEmpty() && !update_directory.isEmpty());
if ((QFile::exists(update_zip_path) && !QFile::remove(update_zip_path)) ||
(QFile::exists(updater_path) && !QFile::remove(updater_path)))
{
reportError("Removing existing update zip/updater failed");
return false;
}
{
QFile update_zip_file(update_zip_path);
if (!update_zip_file.open(QIODevice::WriteOnly) || update_zip_file.write(update_data) != update_data.size())
{
reportError("Writing update zip to '%s' failed", update_zip_path.toUtf8().constData());
return false;
}
update_zip_file.close();
}
if (!extractUpdater(update_zip_path, updater_path))
{
reportError("Extracting updater failed");
return false;
}
if (!doUpdate(update_zip_path, updater_path, update_directory))
{
reportError("Launching updater failed");
return false;
}
return true;
}
bool AutoUpdaterDialog::extractUpdater(const QString& zip_path, const QString& destination_path)
{
unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.toUtf8().constData());
if (!zf)
{
reportError("Failed to open update zip");
return false;
}
if (unzLocateFile(zf, "updater.exe", 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)
{
reportError("Failed to locate updater.exe");
unzClose(zf);
return false;
}
QFile updater_exe(destination_path);
if (!updater_exe.open(QIODevice::WriteOnly))
{
reportError("Failed to open updater.exe for writing");
unzClose(zf);
return false;
}
static constexpr size_t CHUNK_SIZE = 4096;
char chunk[CHUNK_SIZE];
for (;;)
{
int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);
if (size < 0)
{
reportError("Failed to decompress updater exe");
unzClose(zf);
updater_exe.close();
updater_exe.remove();
return false;
}
else if (size == 0)
{
break;
}
if (updater_exe.write(chunk, size) != size)
{
reportError("Failed to write updater exe");
unzClose(zf);
updater_exe.close();
updater_exe.remove();
return false;
}
}
unzClose(zf);
updater_exe.close();
return true;
}
bool AutoUpdaterDialog::doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path)
{
const QString program_path = QCoreApplication::applicationFilePath();
if (program_path.isEmpty())
{
reportError("Failed to get current application path");
return false;
}
QStringList arguments;
arguments << QStringLiteral("%1").arg(QCoreApplication::applicationPid());
arguments << destination_path;
arguments << zip_path;
arguments << program_path;
// this will leak, but not sure how else to handle it...
QProcess* updater_process = new QProcess();
updater_process->setProgram(updater_path);
updater_process->setArguments(arguments);
updater_process->start(QIODevice::NotOpen);
if (!updater_process->waitForStarted())
{
reportError("Failed to start updater");
return false;
}
return true;
}
#else
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
{
return false;
}
#endif

View File

@ -0,0 +1,55 @@
#pragma once
#include "ui_autoupdaterdialog.h"
#include <QtWidgets/QDialog>
class QNetworkAccessManager;
class QNetworkReply;
class QtHostInterface;
class AutoUpdaterDialog final : public QDialog
{
Q_OBJECT
public:
explicit AutoUpdaterDialog(QtHostInterface* host_interface, QWidget* parent = nullptr);
~AutoUpdaterDialog();
static bool isSupported();
Q_SIGNALS:
void updateCheckCompleted();
public Q_SLOTS:
void queueUpdateCheck(bool display_message);
void queueGetLatestRelease();
private Q_SLOTS:
void getLatestTagComplete(QNetworkReply* reply);
void getLatestReleaseComplete(QNetworkReply* reply);
void downloadUpdateClicked();
void skipThisUpdateClicked();
void remindMeLaterClicked();
private:
void reportError(const char* msg, ...);
bool updateNeeded() const;
#ifdef WIN32
bool processUpdate(const QByteArray& update_data);
bool extractUpdater(const QString& zip_path, const QString& destination_path);
bool doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path);
#else
bool processUpdate(const QByteArray& update_data);
#endif
Ui::AutoUpdaterDialog m_ui;
QtHostInterface* m_host_interface;
QNetworkAccessManager* m_network_access_mgr = nullptr;
QString m_latest_sha;
QString m_download_url;
bool m_display_messages = false;
};

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AutoUpdaterDialog</class>
<widget class="QDialog" name="AutoUpdaterDialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>651</width>
<height>474</height>
</rect>
</property>
<property name="windowTitle">
<string>Automatic Updater</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Update Available</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="currentVersion">
<property name="text">
<string>Current Version: </string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="newVersion">
<property name="text">
<string>New Version: </string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Update Notes:</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="updateNotes">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="downloadAndInstall">
<property name="text">
<string>Download and Install...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="skipThisUpdate">
<property name="text">
<string>Skip This Update</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remindMeLater">
<property name="text">
<string>Remind Me Later</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -38,6 +38,7 @@
<ClCompile Include="aboutdialog.cpp" /> <ClCompile Include="aboutdialog.cpp" />
<ClCompile Include="advancedsettingswidget.cpp" /> <ClCompile Include="advancedsettingswidget.cpp" />
<ClCompile Include="audiosettingswidget.cpp" /> <ClCompile Include="audiosettingswidget.cpp" />
<ClCompile Include="autoupdaterdialog.cpp" />
<ClCompile Include="consolesettingswidget.cpp" /> <ClCompile Include="consolesettingswidget.cpp" />
<ClCompile Include="gamelistmodel.cpp" /> <ClCompile Include="gamelistmodel.cpp" />
<ClCompile Include="gamelistsearchdirectoriesmodel.cpp" /> <ClCompile Include="gamelistsearchdirectoriesmodel.cpp" />
@ -74,6 +75,7 @@
<QtMoc Include="inputbindingdialog.h" /> <QtMoc Include="inputbindingdialog.h" />
<QtMoc Include="gamelistmodel.h" /> <QtMoc Include="gamelistmodel.h" />
<QtMoc Include="gamelistsearchdirectoriesmodel.h" /> <QtMoc Include="gamelistsearchdirectoriesmodel.h" />
<QtMoc Include="autoupdaterdialog.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
<ClInclude Include="settingwidgetbinder.h" /> <ClInclude Include="settingwidgetbinder.h" />
<QtMoc Include="consolesettingswidget.h" /> <QtMoc Include="consolesettingswidget.h" />
@ -145,6 +147,7 @@
<ItemGroup> <ItemGroup>
<ClCompile Include="$(IntDir)moc_aboutdialog.cpp" /> <ClCompile Include="$(IntDir)moc_aboutdialog.cpp" />
<ClCompile Include="$(IntDir)moc_audiosettingswidget.cpp" /> <ClCompile Include="$(IntDir)moc_audiosettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_autoupdaterdialog.cpp" />
<ClCompile Include="$(IntDir)moc_advancedsettingswidget.cpp" /> <ClCompile Include="$(IntDir)moc_advancedsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_consolesettingswidget.cpp" /> <ClCompile Include="$(IntDir)moc_consolesettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_controllersettingswidget.cpp" /> <ClCompile Include="$(IntDir)moc_controllersettingswidget.cpp" />
@ -195,6 +198,11 @@
<None Include="translations\duckstation-qt_pt-br.ts" /> <None Include="translations\duckstation-qt_pt-br.ts" />
<None Include="translations\duckstation-qt_pt-pt.ts" /> <None Include="translations\duckstation-qt_pt-pt.ts" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<QtUi Include="autoupdaterdialog.ui">
<FileType>Document</FileType>
</QtUi>
</ItemGroup>
<Target Name="CopyCommonDataFiles" AfterTargets="Build" Inputs="@(CommonDataFiles)" Outputs="@(CommonDataFiles -> '$(BinaryOutputDir)%(RecursiveDir)%(Filename)%(Extension)')"> <Target Name="CopyCommonDataFiles" AfterTargets="Build" Inputs="@(CommonDataFiles)" Outputs="@(CommonDataFiles -> '$(BinaryOutputDir)%(RecursiveDir)%(Filename)%(Extension)')">
<Message Text="Copying common data files" Importance="High" /> <Message Text="Copying common data files" Importance="High" />
<Copy SourceFiles="@(CommonDataFiles)" DestinationFolder="$(BinaryOutputDir)\%(RecursiveDir)" SkipUnchangedFiles="true" /> <Copy SourceFiles="@(CommonDataFiles)" DestinationFolder="$(BinaryOutputDir)\%(RecursiveDir)" SkipUnchangedFiles="true" />

View File

@ -46,6 +46,8 @@
<ClCompile Include="$(IntDir)moc_gamelistmodel.cpp" /> <ClCompile Include="$(IntDir)moc_gamelistmodel.cpp" />
<ClCompile Include="gamelistsearchdirectoriesmodel.cpp" /> <ClCompile Include="gamelistsearchdirectoriesmodel.cpp" />
<ClCompile Include="$(IntDir)moc_gamelistsearchdirectoriesmodel.cpp" /> <ClCompile Include="$(IntDir)moc_gamelistsearchdirectoriesmodel.cpp" />
<ClCompile Include="autoupdaterdialog.cpp" />
<ClCompile Include="$(IntDir)moc_autoupdaterdialog.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="qtutils.h" /> <ClInclude Include="qtutils.h" />
@ -85,6 +87,7 @@
<QtMoc Include="inputbindingdialog.h" /> <QtMoc Include="inputbindingdialog.h" />
<QtMoc Include="gamelistmodel.h" /> <QtMoc Include="gamelistmodel.h" />
<QtMoc Include="gamelistsearchdirectoriesmodel.h" /> <QtMoc Include="gamelistsearchdirectoriesmodel.h" />
<QtMoc Include="autoupdaterdialog.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<QtUi Include="consolesettingswidget.ui" /> <QtUi Include="consolesettingswidget.ui" />
@ -98,6 +101,7 @@
<QtUi Include="gamepropertiesdialog.ui" /> <QtUi Include="gamepropertiesdialog.ui" />
<QtUi Include="aboutdialog.ui" /> <QtUi Include="aboutdialog.ui" />
<QtUi Include="inputbindingdialog.ui" /> <QtUi Include="inputbindingdialog.ui" />
<QtUi Include="autoupdaterdialog.ui" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Natvis Include="qt5.natvis" /> <Natvis Include="qt5.natvis" />

View File

@ -1,4 +1,5 @@
#include "generalsettingswidget.h" #include "generalsettingswidget.h"
#include "autoupdaterdialog.h"
#include "settingsdialog.h" #include "settingsdialog.h"
#include "settingwidgetbinder.h" #include "settingwidgetbinder.h"
@ -80,16 +81,27 @@ GeneralSettingsWidget::GeneralSettingsWidget(QtHostInterface* host_interface, QW
tr("Shows the current emulation speed of the system in the top-right corner of the display as a percentage.")); tr("Shows the current emulation speed of the system in the top-right corner of the display as a percentage."));
// Since this one is compile-time selected, we don't put it in the .ui file. // Since this one is compile-time selected, we don't put it in the .ui file.
const int last_row_count = m_ui.formLayout_4->rowCount();
#ifdef WITH_DISCORD_PRESENCE #ifdef WITH_DISCORD_PRESENCE
{ {
QCheckBox* enableDiscordPresence = new QCheckBox(tr("Enable Discord Presence"), m_ui.groupBox_4); QCheckBox* enableDiscordPresence = new QCheckBox(tr("Enable Discord Presence"), m_ui.groupBox_4);
SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, enableDiscordPresence, "Main", SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, enableDiscordPresence, "Main",
"EnableDiscordPresence"); "EnableDiscordPresence");
m_ui.formLayout_4->addWidget(enableDiscordPresence, m_ui.formLayout_4->rowCount(), 0); m_ui.formLayout_4->addWidget(enableDiscordPresence, last_row_count, 0);
dialog->registerWidgetHelp(enableDiscordPresence, tr("Enable Discord Presence"), tr("Unchecked"), dialog->registerWidgetHelp(enableDiscordPresence, tr("Enable Discord Presence"), tr("Unchecked"),
tr("Shows the game you are currently playing as part of your profile in Discord.")); tr("Shows the game you are currently playing as part of your profile in Discord."));
} }
#endif #endif
if (AutoUpdaterDialog::isSupported())
{
QCheckBox* enableDiscordPresence = new QCheckBox(tr("Enable Automatic Update Check"), m_ui.groupBox_4);
SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, enableDiscordPresence, "AutoUpdater",
"CheckAtStartup");
m_ui.formLayout_4->addWidget(enableDiscordPresence, last_row_count, 1);
dialog->registerWidgetHelp(enableDiscordPresence, tr("Enable Automatic Update Check"), tr("Checked"),
tr("Automatically checks for updates to the program on startup. Updates can be deferred "
"until later or skipped entirely."));
}
} }
GeneralSettingsWidget::~GeneralSettingsWidget() = default; GeneralSettingsWidget::~GeneralSettingsWidget() = default;

View File

@ -46,6 +46,10 @@ int main(int argc, char* argv[])
host_interface->bootSystem(*boot_params); host_interface->bootSystem(*boot_params);
boot_params.reset(); boot_params.reset();
} }
else
{
window->startupUpdateCheck();
}
int result = app.exec(); int result = app.exec();

View File

@ -1,5 +1,6 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "aboutdialog.h" #include "aboutdialog.h"
#include "autoupdaterdialog.h"
#include "common/assert.h" #include "common/assert.h"
#include "core/game_list.h" #include "core/game_list.h"
#include "core/host_display.h" #include "core/host_display.h"
@ -609,6 +610,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered); connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered);
connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered); connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered);
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
connect(m_ui.actionCheckForUpdates, &QAction::triggered, [this]() { checkForUpdates(true); });
connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError, connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError,
Qt::BlockingQueuedConnection); Qt::BlockingQueuedConnection);
@ -820,3 +822,38 @@ void MainWindow::changeEvent(QEvent* event)
QMainWindow::changeEvent(event); QMainWindow::changeEvent(event);
} }
void MainWindow::startupUpdateCheck()
{
if (!m_host_interface->GetBoolSettingValue("AutoUpdater", "CheckAtStartup", true))
return;
checkForUpdates(false);
}
void MainWindow::checkForUpdates(bool display_message)
{
if (!AutoUpdaterDialog::isSupported())
{
if (display_message)
QMessageBox::critical(this, tr("Updater Error"), tr("Updates are not supported on this build."));
return;
}
if (m_auto_updater_dialog)
return;
m_auto_updater_dialog = new AutoUpdaterDialog(m_host_interface, this);
connect(m_auto_updater_dialog, &AutoUpdaterDialog::updateCheckCompleted, this, &MainWindow::onUpdateCheckComplete);
m_auto_updater_dialog->queueUpdateCheck(display_message);
}
void MainWindow::onUpdateCheckComplete()
{
if (!m_auto_updater_dialog)
return;
m_auto_updater_dialog->deleteLater();
m_auto_updater_dialog = nullptr;
}

View File

@ -13,6 +13,7 @@ class QThread;
class GameListWidget; class GameListWidget;
class QtHostInterface; class QtHostInterface;
class QtDisplayWidget; class QtDisplayWidget;
class AutoUpdaterDialog;
class HostDisplay; class HostDisplay;
struct GameListEntry; struct GameListEntry;
@ -25,6 +26,9 @@ public:
explicit MainWindow(QtHostInterface* host_interface); explicit MainWindow(QtHostInterface* host_interface);
~MainWindow(); ~MainWindow();
/// Performs update check if enabled in settings.
void startupUpdateCheck();
private Q_SLOTS: private Q_SLOTS:
void reportError(const QString& message); void reportError(const QString& message);
void reportMessage(const QString& message); void reportMessage(const QString& message);
@ -61,6 +65,9 @@ private Q_SLOTS:
void onGameListEntryDoubleClicked(const GameListEntry* entry); void onGameListEntryDoubleClicked(const GameListEntry* entry);
void onGameListContextMenuRequested(const QPoint& point, const GameListEntry* entry); void onGameListContextMenuRequested(const QPoint& point, const GameListEntry* entry);
void checkForUpdates(bool display_message);
void onUpdateCheckComplete();
protected: protected:
void closeEvent(QCloseEvent* event) override; void closeEvent(QCloseEvent* event) override;
void changeEvent(QEvent* event) override; void changeEvent(QEvent* event) override;
@ -94,6 +101,7 @@ private:
QLabel* m_status_frame_time_widget = nullptr; QLabel* m_status_frame_time_widget = nullptr;
SettingsDialog* m_settings_dialog = nullptr; SettingsDialog* m_settings_dialog = nullptr;
AutoUpdaterDialog* m_auto_updater_dialog = nullptr;
bool m_emulation_running = false; bool m_emulation_running = false;
}; };

View File

@ -122,6 +122,8 @@
<addaction name="actionIssueTracker"/> <addaction name="actionIssueTracker"/>
<addaction name="actionDiscordServer"/> <addaction name="actionDiscordServer"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionCheckForUpdates"/>
<addaction name="separator"/>
<addaction name="actionAbout"/> <addaction name="actionAbout"/>
</widget> </widget>
<widget class="QMenu" name="menuDebug"> <widget class="QMenu" name="menuDebug">
@ -346,6 +348,11 @@
<string>&amp;Discord Server...</string> <string>&amp;Discord Server...</string>
</property> </property>
</action> </action>
<action name="actionCheckForUpdates">
<property name="text">
<string>Check for &amp;Updates...</string>
</property>
</action>
<action name="actionAbout"> <action name="actionAbout">
<property name="text"> <property name="text">
<string>&amp;About...</string> <string>&amp;About...</string>

View File

@ -2,6 +2,7 @@
#include "common/assert.h" #include "common/assert.h"
#include "common/audio_stream.h" #include "common/audio_stream.h"
#include "common/byte_stream.h" #include "common/byte_stream.h"
#include "common/file_system.h"
#include "common/log.h" #include "common/log.h"
#include "common/string_util.h" #include "common/string_util.h"
#include "core/controller.h" #include "core/controller.h"
@ -720,7 +721,7 @@ void QtHostInterface::saveInputProfile(const QString& profile_name)
QString QtHostInterface::getUserDirectoryRelativePath(const QString& arg) const QString QtHostInterface::getUserDirectoryRelativePath(const QString& arg) const
{ {
QString result = QString::fromStdString(m_user_directory); QString result = QString::fromStdString(m_user_directory);
result += '/'; result += FS_OSPATH_SEPERATOR_CHARACTER;
result += arg; result += arg;
return result; return result;
} }
@ -728,11 +729,16 @@ QString QtHostInterface::getUserDirectoryRelativePath(const QString& arg) const
QString QtHostInterface::getProgramDirectoryRelativePath(const QString& arg) const QString QtHostInterface::getProgramDirectoryRelativePath(const QString& arg) const
{ {
QString result = QString::fromStdString(m_program_directory); QString result = QString::fromStdString(m_program_directory);
result += '/'; result += FS_OSPATH_SEPERATOR_CHARACTER;
result += arg; result += arg;
return result; return result;
} }
QString QtHostInterface::getProgramDirectory() const
{
return QString::fromStdString(m_program_directory);
}
void QtHostInterface::powerOffSystem() void QtHostInterface::powerOffSystem()
{ {
if (!isOnWorkerThread()) if (!isOnWorkerThread())

View File

@ -71,6 +71,7 @@ public:
ALWAYS_INLINE const HotkeyInfoList& getHotkeyInfoList() const { return GetHotkeyInfoList(); } ALWAYS_INLINE const HotkeyInfoList& getHotkeyInfoList() const { return GetHotkeyInfoList(); }
ALWAYS_INLINE ControllerInterface* getControllerInterface() const { return GetControllerInterface(); } ALWAYS_INLINE ControllerInterface* getControllerInterface() const { return GetControllerInterface(); }
ALWAYS_INLINE bool inBatchMode() const { return InBatchMode(); } ALWAYS_INLINE bool inBatchMode() const { return InBatchMode(); }
ALWAYS_INLINE void requestExit() { RequestExit(); }
ALWAYS_INLINE bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; } ALWAYS_INLINE bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; }
@ -99,6 +100,9 @@ public:
/// Returns a list of supported languages and codes (suffixes for translation files). /// Returns a list of supported languages and codes (suffixes for translation files).
static std::vector<std::pair<QString, QString>> getAvailableLanguageList(); static std::vector<std::pair<QString, QString>> getAvailableLanguageList();
/// Returns program directory as a QString.
QString getProgramDirectory() const;
Q_SIGNALS: Q_SIGNALS:
void errorReported(const QString& message); void errorReported(const QString& message);
void messageReported(const QString& message); void messageReported(const QString& message);