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);
+ }
+}