diff --git a/src/launcher/CMakeLists.txt b/src/launcher/CMakeLists.txt index e30ef61a7..12d116051 100644 --- a/src/launcher/CMakeLists.txt +++ b/src/launcher/CMakeLists.txt @@ -20,7 +20,7 @@ add_executable(launcher WIN32 ) -target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets) +target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets kyty_git_version) if(WIN32) target_link_libraries(launcher setupapi) endif() diff --git a/src/launcher/forms/main_dialog.ui b/src/launcher/forms/main_dialog.ui index 48e5265ce..94f50252b 100644 --- a/src/launcher/forms/main_dialog.ui +++ b/src/launcher/forms/main_dialog.ui @@ -72,9 +72,43 @@ - + + + + + TextLabel + + + + + + + <a href="check">Check for updates</a> + + + Qt::RichText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + - TextLabel + Check for updates on startup diff --git a/src/launcher/include/updateChecker.h b/src/launcher/include/updateChecker.h new file mode 100644 index 000000000..86d738a0b --- /dev/null +++ b/src/launcher/include/updateChecker.h @@ -0,0 +1,34 @@ +#ifndef UPDATE_CHECKER_H +#define UPDATE_CHECKER_H + +#include +#include + +class QByteArray; +class QWidget; + +class UpdateChecker final: public QObject { + Q_OBJECT + +public: + explicit UpdateChecker(QWidget* parent); + + [[nodiscard]] static bool IsSupported(); + void Check(bool manual); + +signals: + void CheckingChanged(bool checking); + +private: + struct UpdateInfo; + + static UpdateInfo ParseUpdateInfo(const QByteArray& data); + void FetchUpdateInfo(const char* url, bool fallback, bool manual); + void ShowUpdateResult(const UpdateInfo& info, bool manual); + + QWidget* m_parent = nullptr; + QNetworkAccessManager m_network; + bool m_checking_updates = false; +}; + +#endif // UPDATE_CHECKER_H diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 5bca29a45..e8e151cac 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -5,9 +5,11 @@ #include "configurationItem.h" #include "configurationListWidget.h" #include "patchesDialog.h" +#include "updateChecker.h" #include #include +#include #include #include #include @@ -54,6 +56,7 @@ constexpr DWORD CMD_Y_CHARS = 1000; #endif constexpr char SETTINGS_MAIN_DIALOG[] = "MainDialog"; constexpr char SETTINGS_MAIN_LAST_GEOMETRY[] = "geometry"; +constexpr char SETTINGS_CHECK_UPDATES[] = "check_updates_on_startup"; class DetachableProcess: public QProcess { Q_OBJECT; @@ -89,9 +92,11 @@ public: private: static QByteArray g_last_geometry; + static bool g_check_updates_on_startup; - Ui::MainDialog* m_ui = {nullptr}; - MainDialog* m_main_dialog = nullptr; + Ui::MainDialog* m_ui = {nullptr}; + MainDialog* m_main_dialog = nullptr; + UpdateChecker* m_update_checker = nullptr; QString m_interpreter; /*DetachableProcess*/ QProcess m_process; @@ -100,6 +105,7 @@ private: }; QByteArray MainDialogPrivate::g_last_geometry; +bool MainDialogPrivate::g_check_updates_on_startup = true; MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPrivate(this)) { m_p->Setup(this); @@ -114,7 +120,11 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { m_ui->setupUi(main_dialog); m_main_dialog = main_dialog; + m_update_checker = new UpdateChecker(main_dialog); m_ui->widget->SetMainDialog(main_dialog); + m_ui->check_updates_on_startup->setChecked(g_check_updates_on_startup); + m_ui->check_updates_link->setVisible(UpdateChecker::IsSupported()); + m_ui->check_updates_on_startup->setVisible(UpdateChecker::IsSupported()); main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/); @@ -122,6 +132,14 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { Qt::QueuedConnection); connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update); connect(m_ui->widget, &ConfigurationListWidget::Run, this, &MainDialogPrivate::Run); + connect(m_ui->check_updates_link, &QLabel::linkActivated, this, + [this](const QString&) { m_update_checker->Check(true); }); + connect(m_update_checker, &UpdateChecker::CheckingChanged, m_ui->check_updates_link, + &QLabel::setDisabled); + connect(m_ui->check_updates_on_startup, &QCheckBox::toggled, this, [this](bool checked) { + g_check_updates_on_startup = checked; + m_ui->widget->WriteSettings(); + }); connect(main_dialog, &MainDialog::Resize, [this]() { g_last_geometry = m_main_dialog->saveGeometry(); m_ui->widget->WriteSettings(); @@ -189,6 +207,9 @@ void MainDialogPrivate::FindInterpreter() { m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile()); Update(); + if (m_ui->check_updates_on_startup->isChecked()) { + m_update_checker->Check(false); + } } static QString BoolArg(bool value) { @@ -448,6 +469,7 @@ void MainDialogPrivate::WriteSettings(QSettings& s) { if (!g_last_geometry.isEmpty()) { s.setValue(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry); } + s.setValue(SETTINGS_CHECK_UPDATES, g_check_updates_on_startup); s.endGroup(); } @@ -456,6 +478,7 @@ void MainDialogPrivate::ReadSettings(QSettings& s) { s.beginGroup(SETTINGS_MAIN_DIALOG); g_last_geometry = s.value(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry).toByteArray(); + g_check_updates_on_startup = s.value(SETTINGS_CHECK_UPDATES, true).toBool(); s.endGroup(); } diff --git a/src/launcher/src/updateChecker.cpp b/src/launcher/src/updateChecker.cpp new file mode 100644 index 000000000..9dc5e018a --- /dev/null +++ b/src/launcher/src/updateChecker.cpp @@ -0,0 +1,125 @@ +#include "updateChecker.h" + +#include "kytyGitVersion.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr char DEFAULT_FEED_URL[] = "https://kytyps5.github.io/data/updates.json"; +constexpr char FALLBACK_FEED_URL[] = + "https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest"; + +} // namespace + +struct UpdateChecker::UpdateInfo { + QString tag; + QUrl page_url; + QString error; +}; + +UpdateChecker::UpdateChecker(QWidget* parent): QObject(parent), m_parent(parent), m_network(this) {} + +bool UpdateChecker::IsSupported() { +#if defined(KYTY_OFFICIAL_BUILD) && defined(NDEBUG) + return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty")); +#else + return false; +#endif +} + +UpdateChecker::UpdateInfo UpdateChecker::ParseUpdateInfo(const QByteArray& data) { + const auto document = QJsonDocument::fromJson(data); + if (!document.isObject()) { + return {{}, {}, tr("Invalid update feed")}; + } + + const auto root = document.object(); + UpdateInfo info {root.value(QStringLiteral("tag")).toString(), + QUrl(root.value(QStringLiteral("html_url")).toString()), {}}; + if (info.tag.isEmpty()) { + info.tag = root.value(QStringLiteral("tag_name")).toString(); + } + if (info.tag.isEmpty() || !info.page_url.isValid() || + info.page_url.scheme() != QStringLiteral("https")) { + info.error = tr("Incomplete update feed"); + } + return info; +} + +void UpdateChecker::Check(bool manual) { + if (!IsSupported() || m_checking_updates) { + return; + } + m_checking_updates = true; + emit CheckingChanged(true); + FetchUpdateInfo(DEFAULT_FEED_URL, false, manual); +} + +void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual) { + QNetworkRequest request(QUrl(QString::fromLatin1(url))); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + request.setRawHeader("User-Agent", "Kyty-Launcher"); + request.setRawHeader("Accept", "application/vnd.github+json"); + request.setTransferTimeout(15000); + + auto* reply = m_network.get(request); + connect(reply, &QNetworkReply::finished, this, [this, reply, fallback, manual]() { + UpdateInfo info; + if (reply->error() == QNetworkReply::NoError) { + info = ParseUpdateInfo(reply->readAll()); + } else { + info.error = reply->errorString(); + } + reply->deleteLater(); + + if (!info.error.isEmpty() && !fallback) { + FetchUpdateInfo(FALLBACK_FEED_URL, true, manual); + return; + } + + m_checking_updates = false; + emit CheckingChanged(false); + ShowUpdateResult(info, manual); + }); +} + +void UpdateChecker::ShowUpdateResult(const UpdateInfo& info, bool manual) { + if (!info.error.isEmpty()) { + qWarning() << "Could not check for updates:" << info.error; + if (manual) { + QMessageBox::warning(m_parent, tr("Update Check"), + tr("Could not check for updates:\n%1").arg(info.error)); + } + return; + } + + const bool current = info.tag == QString::fromLatin1(KYTY_RELEASE_TAG); + if (current) { + if (manual) { + QMessageBox::information(m_parent, tr("Update Check"), + tr("You are using the latest version (%1).").arg(info.tag)); + } + return; + } + const auto message = + tr("An update is available.\n\nCurrent: %1\nLatest: %2\n\n" + "Open the release page?") + .arg(QString::fromLatin1(KYTY_RELEASE_TAG), info.tag); + if (QMessageBox::question(m_parent, tr("KytyPS5 Update"), message, + QMessageBox::Open | QMessageBox::Cancel, + QMessageBox::Open) == QMessageBox::Open) { + QDesktopServices::openUrl(info.page_url); + } +}