diff --git a/.ci/linux/build.sh b/.ci/linux/build.sh index f87b12e762..cdaf512406 100755 --- a/.ci/linux/build.sh +++ b/.ci/linux/build.sh @@ -34,10 +34,9 @@ else export EXTRA_CMAKE_FLAGS=(-DYUZU_USE_PRECOMPILED_HEADERS=OFF) fi -# TODO(crueter): update checker -# if [ "$GITHUB_REF_TYPE" == "tag" ]; then -# export EXTRA_CMAKE_FLAGS=($EXTRA_CMAKE_FLAGS -DENABLE_QT_UPDATE_CHECKER=ON) -# fi +if [ "$RELEASE" == "1" ]; then + export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DENABLE_QT_UPDATE_CHECKER=ON) +fi mkdir -p build && cd build cmake .. -G Ninja \ diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 29d66c42d7..05e53bd5ae 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -93,7 +93,7 @@ jobs: fetch-tags: true - name: Build - run: TARGET=appimage ./.ci/linux/build.sh v3 8 + run: TARGET=appimage RELEASE=1 ./.ci/linux/build.sh v3 8 - name: Package AppImage run: ./.ci/linux/package.sh v3 &> /dev/null diff --git a/CMakeLists.txt b/CMakeLists.txt index e094d99dee..d9958fcb94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,9 +34,11 @@ cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "NOT ANDROID" option(ENABLE_OPENGL "Enable OpenGL" ON) mark_as_advanced(FORCE ENABLE_OPENGL) -option(ENABLE_QT "Enable the Qt frontend" ON) +option(ENABLE_QT "Enable the Qt frontend" ON) option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF) +option(ENABLE_QT_UPDATE_CHECKER "Enable update checker for the Qt frontend" OFF) + CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF) option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) @@ -353,6 +355,9 @@ endif() if (ENABLE_WEB_SERVICE) find_package(cpp-jwt 1.4 CONFIG) +endif() + +if (ENABLE_WEB_SERVICE OR ENABLE_QT_UPDATE_CHECKER) find_package(httplib 0.12 MODULE COMPONENTS OpenSSL) endif() diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index d89eb1804a..fd3f54ae4f 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -119,7 +119,7 @@ endif() add_subdirectory(sirit) # httplib -if (ENABLE_WEB_SERVICE AND NOT TARGET httplib::httplib) +if ((ENABLE_WEB_SERVICE OR ENABLE_QT_UPDATE_CHECKER) AND NOT TARGET httplib::httplib) set(HTTPLIB_REQUIRE_OPENSSL ON) add_subdirectory(cpp-httplib) endif() diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 12f2e13210..440ef3c46d 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -266,6 +266,12 @@ file(GLOB COMPAT_LIST file(GLOB_RECURSE ICONS ${PROJECT_SOURCE_DIR}/dist/icons/*) file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/qt_themes/*) +if (ENABLE_QT_UPDATE_CHECKER) + target_link_libraries(yuzu PRIVATE httplib::httplib) + target_sources(yuzu PRIVATE update_checker.cpp) + target_compile_definitions(yuzu PUBLIC ENABLE_QT_UPDATE_CHECKER) +endif() + if (ENABLE_QT_TRANSLATION) set(YUZU_QT_LANGUAGES "${PROJECT_SOURCE_DIR}/dist/languages" CACHE PATH "Path to the translation bundle for the Qt frontend") option(GENERATE_QT_TRANSLATION "Generate en.ts as the translation source file" OFF) diff --git a/src/yuzu/applets/qt_amiibo_settings.cpp b/src/yuzu/applets/qt_amiibo_settings.cpp index 9da63c2dc8..ca9c5cfbdc 100644 --- a/src/yuzu/applets/qt_amiibo_settings.cpp +++ b/src/yuzu/applets/qt_amiibo_settings.cpp @@ -13,6 +13,7 @@ #include "input_common/drivers/virtual_amiibo.h" #include "input_common/main.h" #include "ui_qt_amiibo_settings.h" +#include "web_service/web_result.h" #ifdef ENABLE_WEB_SERVICE #include "web_service/web_backend.h" #endif diff --git a/src/yuzu/configuration/shared_translation.cpp b/src/yuzu/configuration/shared_translation.cpp index da87850fae..53ab0aa797 100644 --- a/src/yuzu/configuration/shared_translation.cpp +++ b/src/yuzu/configuration/shared_translation.cpp @@ -296,6 +296,10 @@ std::unique_ptr InitializeTranslations(QWidget* parent) { INSERT(UISettings, controller_applet_disabled, tr("Disable controller applet"), tr("Forcibly disables the use of the controller applet by guests.\nWhen a guest " "attempts to open the controller applet, it is immediately closed.")); + INSERT(UISettings, + check_for_updates, + tr("Check for updates"), + tr("Whether or not to check for updates upon startup.")); // Linux INSERT(Settings, enable_gamemode, tr("Enable Gamemode"), QString()); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 928e42df59..c497b7c40e 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -52,6 +52,10 @@ #include "yuzu/multiplayer/state.h" #include "yuzu/util/controller_navigation.h" +#ifdef ENABLE_QT_UPDATE_CHECKER +#include "yuzu/update_checker.h" +#endif + #ifdef YUZU_ROOM #include "dedicated_room/yuzu_room.h" #endif @@ -313,6 +317,7 @@ GMainWindow::GMainWindow(bool has_broken_vulkan) provider{std::make_unique()} { Common::FS::CreateEdenPaths(); this->config = std::make_unique(); + #ifdef __unix__ SetupSigInterrupts(); SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); @@ -410,6 +415,27 @@ GMainWindow::GMainWindow(bool has_broken_vulkan) show(); +#ifdef ENABLE_QT_UPDATE_CHECKER + if (UISettings::values.check_for_updates) { + update_future = QtConcurrent::run([]() -> QString { + const bool is_prerelease = + ((strstr(Common::g_build_fullname, "pre-alpha") != NULL) || + (strstr(Common::g_build_fullname, "alpha") != NULL) || + (strstr(Common::g_build_fullname, "beta") != NULL) || + (strstr(Common::g_build_fullname, "rc") != NULL)); + const std::optional latest_release_tag = + UpdateChecker::GetLatestRelease(is_prerelease); + if (latest_release_tag && latest_release_tag.value() != Common::g_build_fullname) { + return QString::fromStdString(latest_release_tag.value()); + } + return QString{}; + }); + QObject::connect(&update_watcher, &QFutureWatcher::finished, this, + &GMainWindow::OnEmulatorUpdateAvailable); + update_watcher.setFuture(update_future); + } +#endif + system->SetContentProvider(std::make_unique()); system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, provider.get()); @@ -4767,6 +4793,28 @@ void GMainWindow::MigrateConfigFiles() { } } +#ifdef ENABLE_QT_UPDATE_CHECKER +void GMainWindow::OnEmulatorUpdateAvailable() { + QString version_string = update_future.result(); + if (version_string.isEmpty()) + 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("Update %1 for Eden is available.\nWould you like to download it?") + .arg(version_string)); + update_prompt.exec(); + if (update_prompt.button(QMessageBox::Yes) == update_prompt.clickedButton()) { + QDesktopServices::openUrl( + QUrl(QString::fromStdString("https://github.com/eden-emulator/Releases/releases/tag/") + + version_string)); + } +} +#endif + void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_view title_version, std::string_view gpu_vendor) { const auto branch_name = std::string(Common::g_scm_branch); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 099bee52fb..f6a731e050 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -13,7 +13,6 @@ #include #include -#include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "configuration/qt_config.h" #include "frontend_common/content_manager.h" @@ -21,14 +20,19 @@ #include "user_data_migration.h" #include "yuzu/compatibility_list.h" #include "yuzu/hotkeys.h" -#include "yuzu/util/controller_navigation.h" #ifdef __unix__ +#include #include #include #include #endif +#ifdef ENABLE_QT_UPDATE_CHECKER +#include +#include +#endif + class QtConfig; class ClickableLabel; class EmuThread; @@ -414,6 +418,10 @@ private slots: void OnEmulationStopped(); void OnEmulationStopTimeExpired(); +#ifdef ENABLE_QT_UPDATE_CHECKER + void OnEmulatorUpdateAvailable(); +#endif + private: QString GetGameListErrorRemoving(InstalledEntryType type) const; void RemoveBaseContent(u64 program_id, InstalledEntryType type); @@ -483,6 +491,11 @@ private: std::unique_ptr play_time_manager; std::shared_ptr input_subsystem; +#ifdef ENABLE_QT_UPDATE_CHECKER + QFuture update_future; + QFutureWatcher update_watcher; +#endif + MultiplayerState* multiplayer_state = nullptr; GRenderWindow* render_window; diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 0cd68feffe..be9286cd45 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -141,6 +141,7 @@ struct Values { true, true}; Setting disable_web_applet{linkage, true, "disable_web_applet", Category::Ui}; + Setting check_for_updates{linkage, true, "check_for_updates", Category::UiGeneral}; // Discord RPC Setting enable_discord_presence{linkage, false, "enable_discord_presence", Category::Ui}; diff --git a/src/yuzu/update_checker.cpp b/src/yuzu/update_checker.cpp new file mode 100644 index 0000000000..8291987d73 --- /dev/null +++ b/src/yuzu/update_checker.cpp @@ -0,0 +1,109 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +// SPDX-FileCopyrightText: eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "update_checker.h" +#include "common/logging/log.h" +#include +#include +#include +#include +#include + +std::optional UpdateChecker::GetResponse(std::string url, std::string path) +{ + constexpr std::size_t timeout_seconds = 15; + + std::unique_ptr client = std::make_unique(url); + client->set_connection_timeout(timeout_seconds); + client->set_read_timeout(timeout_seconds); + client->set_write_timeout(timeout_seconds); + + if (client == nullptr) { + LOG_ERROR(Frontend, "Invalid URL {}{}", url, path); + return {}; + } + + httplib::Request request{ + .method = "GET", + .path = path, + }; + + client->set_follow_location(true); + const auto result = client->send(request); + 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); + return {}; + } + if (!response.headers.contains("content-type")) { + LOG_ERROR(Frontend, "GET to {}{} returned no content", url, path); + return {}; + } + + return response.body; +} + +std::optional UpdateChecker::GetLatestRelease(bool include_prereleases) +{ + constexpr auto update_check_url = "http://api.github.com"; + std::string update_check_path = "/repos/eden-emulator/Releases"; + try { + if (include_prereleases) { // This can return either a prerelease or a stable release, + // whichever is more recent. + const auto update_check_tags_path = update_check_path + "/tags"; + const auto update_check_releases_path = update_check_path + "/releases"; + + const auto tags_response = GetResponse(update_check_url, update_check_tags_path); + const auto releases_response = GetResponse(update_check_url, update_check_releases_path); + + if (!tags_response || !releases_response) + return {}; + + const std::string latest_tag + = nlohmann::json::parse(tags_response.value()).at(0).at("name"); + const bool latest_tag_has_release = releases_response.value().find( + fmt::format("\"{}\"", latest_tag)) + != std::string::npos; + + // If there is a newer tag, but that tag has no associated release, don't prompt the + // user to update. + if (!latest_tag_has_release) + return {}; + + return latest_tag; + } else { // This is a stable release, only check for other stable releases. + update_check_path += "/releases/latest"; + const auto response = GetResponse(update_check_url, update_check_path); + + if (!response) + return {}; + + const std::string latest_tag = nlohmann::json::parse(response.value()).at("tag_name"); + return latest_tag; + } + + } catch (nlohmann::detail::out_of_range&) { + LOG_ERROR(Frontend, + "Parsing JSON response from {}{} failed during update check: " + "nlohmann::detail::out_of_range", + update_check_url, + update_check_path); + return {}; + } catch (nlohmann::detail::type_error&) { + LOG_ERROR(Frontend, + "Parsing JSON response from {}{} failed during update check: " + "nlohmann::detail::type_error", + update_check_url, + update_check_path); + return {}; + } +} diff --git a/src/yuzu/update_checker.h b/src/yuzu/update_checker.h new file mode 100644 index 0000000000..cd2d15d834 --- /dev/null +++ b/src/yuzu/update_checker.h @@ -0,0 +1,13 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +namespace UpdateChecker { +std::optional GetResponse(std::string url, std::string path); +std::optional GetLatestRelease(bool); +} // namespace UpdateChecker