From 8d5f837e979cf4f1384885c8e24bf101be820df9 Mon Sep 17 00:00:00 2001 From: Leon Styhre <leon@leonstyhre.com> Date: Mon, 3 Jul 2023 17:46:56 +0200 Subject: [PATCH] Added an application updater which downloads and installs ES-DE updates --- es-app/CMakeLists.txt | 2 + es-app/src/ApplicationUpdater.cpp | 15 +- es-app/src/ApplicationUpdater.h | 1 + es-app/src/guis/GuiApplicationUpdater.cpp | 434 ++++++++++++++++++++++ es-app/src/guis/GuiApplicationUpdater.h | 80 ++++ es-app/src/views/ViewController.cpp | 22 +- 6 files changed, 542 insertions(+), 12 deletions(-) create mode 100644 es-app/src/guis/GuiApplicationUpdater.cpp create mode 100644 es-app/src/guis/GuiApplicationUpdater.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index eec608187..3900481b6 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -29,6 +29,7 @@ set(ES_HEADERS # GUIs ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiApplicationUpdater.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.h @@ -79,6 +80,7 @@ set(ES_SOURCES # GUIs ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiApplicationUpdater.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.cpp diff --git a/es-app/src/ApplicationUpdater.cpp b/es-app/src/ApplicationUpdater.cpp index a6a306d12..841f46cf7 100644 --- a/es-app/src/ApplicationUpdater.cpp +++ b/es-app/src/ApplicationUpdater.cpp @@ -396,6 +396,8 @@ void ApplicationUpdater::compareVersions() mPackage = package; } + mPackage.version = releaseType->version; + // Cut the message to 280 characters so we don't make the message box exceedingly large. mPackage.message = mPackage.message.substr(0, 280); @@ -410,13 +412,12 @@ void ApplicationUpdater::compareVersions() mResults.append("New ") .append(releaseType == &mStableRelease ? "release " : "prerelease ") - .append("available!\n") - .append(releaseType->version) - .append(" (") - .append(releaseType->date) - .append(")\n") - .append("can now be downloaded from\n") - .append("https://es-de.org/"); + .append("available: ") + .append(releaseType->version); + + if (mPackage.name != "LinuxAppImage" && mPackage.name != "LinuxSteamDeckAppImage") + mResults.append("\nIt can be downloaded from\n").append("https://es-de.org"); + // mResults.append("\nFor more information visit\n").append("https://es-de.org"); if (mPackage.message != "") mResults.append("\n").append(mPackage.message); diff --git a/es-app/src/ApplicationUpdater.h b/es-app/src/ApplicationUpdater.h index 0778cf883..3bb7d9542 100644 --- a/es-app/src/ApplicationUpdater.h +++ b/es-app/src/ApplicationUpdater.h @@ -34,6 +34,7 @@ public: struct Package { std::string name; + std::string version; std::string filename; std::string url; std::string md5; diff --git a/es-app/src/guis/GuiApplicationUpdater.cpp b/es-app/src/guis/GuiApplicationUpdater.cpp new file mode 100644 index 000000000..1f99bc925 --- /dev/null +++ b/es-app/src/guis/GuiApplicationUpdater.cpp @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiApplicationUpdater.cpp +// +// Installs application updates. +// + +#include "GuiApplicationUpdater.h" + +#include "EmulationStation.h" +#include "utils/PlatformUtil.h" + +#include <filesystem> + +GuiApplicationUpdater::GuiApplicationUpdater() + : mRenderer {Renderer::getInstance()} + , mBackground {":/graphics/frame.svg"} + , mGrid {glm::ivec2 {4, 11}} + , mAbortDownload {false} + , mDownloading {false} + , mReadyToInstall {false} + , mHasDownloaded {false} + , mInstalling {false} + , mHasInstalled {false} +{ + addChild(&mBackground); + addChild(&mGrid); + + mPackage = ApplicationUpdater::getInstance().getPackageInfo(); + assert(mPackage.url != ""); + + LOG(LogInfo) << "Starting Application Updater"; + + // Set up grid. + mTitle = std::make_shared<TextComponent>("APPLICATION UPDATER", Font::get(FONT_SIZE_LARGE), + mMenuColorTitle, ALIGN_CENTER); + mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true, glm::ivec2 {4, 1}, + GridFlags::BORDER_BOTTOM); + + mStatusHeader = std::make_shared<TextComponent>( + "INSTALLATION STEPS:", Font::get(FONT_SIZE_MINI), mMenuColorPrimary, ALIGN_LEFT); + mGrid.setEntry(mStatusHeader, glm::ivec2 {1, 1}, false, true, glm::ivec2 {2, 1}); + + mProcessStep1 = std::make_shared<TextComponent>( + "DOWNLOAD NEW RELEASE", Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary, ALIGN_LEFT); + mGrid.setEntry(mProcessStep1, glm::ivec2 {1, 2}, false, true, glm::ivec2 {2, 1}); + + mProcessStep2 = std::make_shared<TextComponent>("INSTALL PACKAGE", Font::get(FONT_SIZE_MEDIUM), + mMenuColorPrimary, ALIGN_LEFT); + mGrid.setEntry(mProcessStep2, glm::ivec2 {1, 3}, false, true, glm::ivec2 {2, 1}); + + mProcessStep3 = + std::make_shared<TextComponent>("QUIT AND MANUALLY RESTART ES-DE", + Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary, ALIGN_LEFT); + mGrid.setEntry(mProcessStep3, glm::ivec2 {1, 4}, false, true, glm::ivec2 {2, 1}); + + mStatusMessageHeader = std::make_shared<TextComponent>( + "STATUS MESSAGE:", Font::get(FONT_SIZE_MINI), mMenuColorPrimary, ALIGN_LEFT); + mGrid.setEntry(mStatusMessageHeader, glm::ivec2 {1, 6}, false, true, glm::ivec2 {2, 1}); + + mStatusMessage = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL), + mMenuColorPrimary, ALIGN_LEFT); + mGrid.setEntry(mStatusMessage, glm::ivec2 {1, 7}, false, true, glm::ivec2 {2, 1}); + + mChangelogMessage = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL), + mMenuColorPrimary, ALIGN_LEFT); + mGrid.setEntry(mChangelogMessage, glm::ivec2 {1, 8}, false, true, glm::ivec2 {2, 1}); + + // Buttons. + std::vector<std::shared_ptr<ButtonComponent>> buttons; + + mButton1 = std::make_shared<ButtonComponent>("DOWNLOAD", "download new release", [this]() { + if (!mDownloading) { + mMessage = ""; + mStatusMessage->setText(mMessage); + mDownloading = true; + if (mThread) { + mThread->join(); + mThread.reset(); + } + mThread = std::make_unique<std::thread>(&GuiApplicationUpdater::downloadPackage, this); + } + }); + + buttons.push_back(mButton1); + + mButton2 = std::make_shared<ButtonComponent>("CANCEL", "cancel", [this]() { + mAbortDownload = true; + if (mDownloading) { + mWindow->pushGui( + new GuiMsgBox(getHelpStyle(), "DOWNLOAD ABORTED\nNO PACKAGE SAVED TO DISK", "OK", + nullptr, "", nullptr, "", nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else if (mHasDownloaded && !mHasInstalled) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), "PACKAGE WAS DOWNLOADED AND\nCAN BE MANUALLY INSTALLED", "OK", + nullptr, "", nullptr, "", nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + delete this; + }); + + buttons.push_back(mButton2); + + mButtons = MenuComponent::makeButtonGrid(buttons); + mGrid.setEntry(mButtons, glm::ivec2 {0, 10}, true, false, glm::ivec2 {4, 1}, + GridFlags::BORDER_TOP); + + // Limit the width of the GUI on ultrawide monitors. The 1.778 aspect ratio value is + // the 16:9 reference. + const float aspectValue {1.778f / Renderer::getScreenAspectRatio()}; + const float width {glm::clamp(0.70f * aspectValue, 0.55f, + (mRenderer->getIsVerticalOrientation() ? 0.95f : 0.85f)) * + mRenderer->getScreenWidth()}; + setSize(width, + mTitle->getSize().y + + (FONT_SIZE_MEDIUM * 1.5f * (mRenderer->getIsVerticalOrientation() ? 8.0f : 7.0f)) + + mButtons->getSize().y); + + setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f, + (mRenderer->getScreenHeight() - mSize.y) / 2.0f); + + setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f, + std::round(mRenderer->getScreenHeight() * 0.13f)); + + mBusyAnim.setSize(glm::vec2 {mRenderer->getScreenWidth(), + mRenderer->getScreenHeight() * + (mRenderer->getIsVerticalOrientation() ? 0.80f : 1.0f)}); + mBusyAnim.setText("DOWNLOADING"); + mBusyAnim.onSizeChanged(); +} + +GuiApplicationUpdater::~GuiApplicationUpdater() +{ + mAbortDownload = true; + + if (mThread) + mThread->join(); +} + +bool GuiApplicationUpdater::downloadPackage() +{ + mStatus = ASYNC_IN_PROGRESS; + mRequest = std::unique_ptr<HttpReq>(std::make_unique<HttpReq>(mPackage.url)); + LOG(LogDebug) << "GuiApplicationUpdater::downloadPackage(): Starting download of \"" + << mPackage.filename << "\""; + + while (!mAbortDownload) { + HttpReq::Status reqStatus {mRequest->status()}; + if (reqStatus == HttpReq::REQ_SUCCESS) { + mStatus = ASYNC_DONE; + break; + } + else if (reqStatus != HttpReq::REQ_IN_PROGRESS) { + std::string errorMessage {"Network error (status: "}; + errorMessage.append(std::to_string(reqStatus)) + .append(") - ") + .append(mRequest->getErrorMsg()); + mRequest.reset(); + std::unique_lock<std::mutex> lock {mMutex}; + mMessage = errorMessage; + return true; + } + } + + if (mAbortDownload) { + if (mAbortDownload) { + LOG(LogInfo) << "Aborted package download"; + } + mRequest.reset(); + return false; + } + + std::string fileContents {mRequest->getContent()}; + mRequest.reset(); + + if (Utils::Math::md5Hash(fileContents) != mPackage.md5) { + const std::string errorMessage {"Downloaded file does not match expected MD5 checksum"}; + LOG(LogError) << errorMessage; + std::unique_lock<std::mutex> lock {mMutex}; + mMessage = "Error: " + errorMessage; + return true; + } + + const std::string packageTempFile { + Utils::FileSystem::getParent(Utils::FileSystem::getEsBinary()) + "/" + mPackage.filename + + "_" + mPackage.version}; + LOG(LogDebug) + << "GuiApplicationUpdater::downloadPackage(): Package downloaded, writing it to \"" + << packageTempFile << "\""; + + if (Utils::FileSystem::isRegularFile(packageTempFile)) { + LOG(LogInfo) << "Temporary package file already exists, deleting it"; + Utils::FileSystem::removeFile(packageTempFile); + if (Utils::FileSystem::exists(packageTempFile)) { + const std::string errorMessage { + "Couldn't delete temporary package file, permission problems?"}; + LOG(LogError) << errorMessage; + std::unique_lock<std::mutex> lock {mMutex}; + mMessage = "Error: " + errorMessage; + return true; + } + } + + std::ofstream writeFile; + writeFile.open(packageTempFile.c_str(), std::ofstream::binary); + + if (writeFile.fail()) { + const std::string errorMessage { + "Couldn't open package file for writing, permission problems?"}; + LOG(LogError) << errorMessage; + std::unique_lock<std::mutex> lock {mMutex}; + mMessage = "Error: " + errorMessage; + return true; + } + + writeFile.write(&fileContents[0], fileContents.length()); + writeFile.close(); + + fileContents.clear(); + + std::filesystem::permissions(packageTempFile, std::filesystem::perms::owner_all | + std::filesystem::perms::group_all | + std::filesystem::perms::others_read | + std::filesystem::perms::others_exec); + + if (std::filesystem::status(packageTempFile).permissions() != + (std::filesystem::perms::owner_all | std::filesystem::perms::group_all | + std::filesystem::perms::others_read | std::filesystem::perms::others_exec)) { + Utils::FileSystem::removeFile(packageTempFile); + const std::string errorMessage {"Couldn't set permissions on AppImage file"}; + LOG(LogError) << errorMessage; + std::unique_lock<std::mutex> lock {mMutex}; + mMessage = "Error: " + errorMessage; + return true; + } + + LOG(LogInfo) << "Successfully downloaded package file \"" << packageTempFile << "\""; + + std::unique_lock<std::mutex> lock {mMutex}; + mMessage = "Downloaded " + mPackage.filename + "_" + mPackage.version; + + mDownloading = false; + mReadyToInstall = true; + + return false; +} + +bool GuiApplicationUpdater::installAppImage() +{ + LOG(LogDebug) << "GuiApplicationUpdater::installAppImage(): Attempting to install new package"; + + mReadyToInstall = false; + mInstalling = true; + + const std::string packageTempFile { + Utils::FileSystem::getParent(Utils::FileSystem::getEsBinary()) + "/" + mPackage.filename + + "_" + mPackage.version}; + const std::string packageTargetFile {Utils::FileSystem::getEsBinary()}; + + if (packageTargetFile != + Utils::FileSystem::getParent(Utils::FileSystem::getEsBinary()) + "/" + mPackage.filename) { + LOG(LogWarning) << "Running AppImage seems to have a non-standard filename: \"" + << packageTargetFile << "\""; + } + + if (Utils::FileSystem::isSymlink(packageTargetFile)) { + LOG(LogInfo) + << "Target file is a symbolic link, this will be followed and the actual symlink file " + << "will not be touched "; + } + + // Extra precaution, make sure that the file was actually correctly written to disk. + std::ifstream readFile; + readFile.open(packageTempFile.c_str(), std::ofstream::binary); + + if (readFile.fail()) { + const std::string errorMessage {"Couldn't open AppImage update file for reading"}; + LOG(LogError) << errorMessage; + mMessage = "Error: " + errorMessage; + mHasDownloaded = false; + return true; + } + + readFile.seekg(0, std::ios::end); + const long fileLength {static_cast<long>(readFile.tellg())}; + readFile.seekg(0, std::ios::beg); + std::string fileData(fileLength, 0); + readFile.read(&fileData[0], fileLength); + readFile.close(); + + if (Utils::Math::md5Hash(fileData) != mPackage.md5) { + const std::string errorMessage {"Downloaded file does not match expected MD5 checksum"}; + LOG(LogError) << errorMessage; + mMessage = "Error: " + errorMessage; + mHasDownloaded = false; + return true; + } + + const std::string packageOldFile {packageTargetFile + "_OLD_" + PROGRAM_VERSION_STRING}; + + if (Utils::FileSystem::renameFile(packageTargetFile, packageOldFile, true)) { + const std::string errorMessage { + "Couldn't rename running AppImage file, permission problems?"}; + LOG(LogError) << errorMessage; + mMessage = "Error: " + errorMessage; + LOG(LogInfo) << "Attempting to rename \"" << packageOldFile + << "\" back to running AppImage"; + Utils::FileSystem::renameFile(packageOldFile, packageTargetFile, true); + mInstalling = false; + return true; + } + + LOG(LogInfo) << "Renamed running AppImage to \"" << packageOldFile << "\""; + + if (Utils::FileSystem::renameFile(packageTempFile, packageTargetFile, true)) { + const std::string errorMessage { + "Couldn't replace running AppImage file, permission problems?"}; + LOG(LogError) << errorMessage; + mMessage = "Error: " + errorMessage; + LOG(LogInfo) << "Attempting to rename \"" << packageOldFile + << "\" back to running AppImage"; + Utils::FileSystem::renameFile(packageOldFile, packageTargetFile, true); + mInstalling = false; + return true; + } + + LOG(LogInfo) << "Package was successfully installed as \"" << packageTargetFile << "\""; + + std::unique_lock<std::mutex> lock {mMutex}; + mMessage = "Package was successfully installed"; + mHasInstalled = true; + + return false; +} + +void GuiApplicationUpdater::update(int deltaTime) +{ + { + std::unique_lock<std::mutex> lock {mMutex}; + if (mMessage != "") { + mStatusMessage->setText(mMessage); + mDownloading = false; + } + } + + if (mDownloading) + mBusyAnim.update(deltaTime); + else if (mReadyToInstall) { + mProcessStep1->setText(ViewController::TICKMARK_CHAR + " " + mProcessStep1->getValue()); + mProcessStep1->setColor(mMenuColorGreen); + mButton1->setText("INSTALL", "install package"); + mButton1->setPressedFunc([this] { + if (!mInstalling) { + mMessage = ""; + mStatusMessage->setText(mMessage); + installAppImage(); + } + }); + mReadyToInstall = false; + mHasDownloaded = true; + } + else if (mHasInstalled) { + mProcessStep2->setText(ViewController::TICKMARK_CHAR + " " + mProcessStep2->getValue()); + mProcessStep2->setColor(mMenuColorGreen); + mChangelogMessage->setText("Find the detailed changelog at https://es-de.org"); + mButton1->setText("DONE", "quit application"); + mButton1->setPressedFunc([this] { + delete this; + Utils::Platform::quitES(); + }); + mButton2->setText("QUIT", "quit application"); + mButton2->setPressedFunc([this] { + delete this; + Utils::Platform::quitES(); + }); + mHasInstalled = false; + } +} + +void GuiApplicationUpdater::render(const glm::mat4& parentTrans) +{ + glm::mat4 trans {parentTrans * getTransform()}; + + renderChildren(trans); + + if (mDownloading) + mBusyAnim.render(parentTrans); +} + +void GuiApplicationUpdater::onSizeChanged() +{ + const float screenSize {mRenderer->getIsVerticalOrientation() ? mRenderer->getScreenWidth() : + mRenderer->getScreenHeight()}; + mGrid.setRowHeightPerc(0, (mTitle->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / + 2.0f); + mGrid.setRowHeightPerc(1, (mStatusHeader->getFont()->getLetterHeight() + screenSize * 0.2f) / + mSize.y / 4.0f); + mGrid.setRowHeightPerc(2, (mProcessStep1->getFont()->getLetterHeight() + screenSize * 0.2f) / + mSize.y / 4.0f); + mGrid.setRowHeightPerc(3, (mProcessStep2->getFont()->getLetterHeight() + screenSize * 0.2f) / + mSize.y / 4.0f); + mGrid.setRowHeightPerc(4, (mProcessStep3->getFont()->getLetterHeight() + screenSize * 0.2f) / + mSize.y / 4.0f); + mGrid.setRowHeightPerc( + 5, + (mStatusMessageHeader->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / 4.0f); + mGrid.setRowHeightPerc( + 6, + (mStatusMessageHeader->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / 4.0f); + mGrid.setRowHeightPerc(7, (mStatusMessage->getFont()->getLetterHeight() + screenSize * 0.15f) / + mSize.y / 4.0f); + mGrid.setRowHeightPerc( + 8, (mChangelogMessage->getFont()->getLetterHeight() + screenSize * 0.15f) / mSize.y / 4.0f); + mGrid.setRowHeightPerc(10, mButtons->getSize().y / mSize.y); + + mGrid.setColWidthPerc(0, 0.01f); + mGrid.setColWidthPerc(3, 0.01f); + + mGrid.setSize(mSize); + mBackground.fitTo(mSize); +} + +std::vector<HelpPrompt> GuiApplicationUpdater::getHelpPrompts() +{ + std::vector<HelpPrompt> prompts {mGrid.getHelpPrompts()}; + return prompts; +} \ No newline at end of file diff --git a/es-app/src/guis/GuiApplicationUpdater.h b/es-app/src/guis/GuiApplicationUpdater.h new file mode 100644 index 000000000..738ced36c --- /dev/null +++ b/es-app/src/guis/GuiApplicationUpdater.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiApplicationUpdater.h +// +// Installs application updates. +// + +#ifndef ES_APP_GUIS_GUI_APPLICATION_UPDATER_H +#define ES_APP_GUIS_GUI_APPLICATION_UPDATER_H + +#include "ApplicationUpdater.h" +#include "AsyncHandle.h" +#include "GuiComponent.h" +#include "HttpReq.h" +#include "components/BusyComponent.h" +#include "guis/GuiSettings.h" +#include "views/ViewController.h" + +#include <atomic> +#include <mutex> +#include <thread> + +class GuiApplicationUpdater : public GuiComponent +{ +public: + GuiApplicationUpdater(); + ~GuiApplicationUpdater(); + + bool downloadPackage(); + bool installAppImage(); + + void update(int deltaTime) override; + void render(const glm::mat4& parentTrans) override; + +private: + void onSizeChanged() override; + std::vector<HelpPrompt> getHelpPrompts() override; + HelpStyle getHelpStyle() override + { + if (ViewController::getInstance()->getState().viewing == ViewController::ViewMode::NOTHING) + return HelpStyle(); + else + return ViewController::getInstance()->getViewHelpStyle(); + } + + Renderer* mRenderer; + BusyComponent mBusyAnim; + + NinePatchComponent mBackground; + ComponentGrid mGrid; + std::shared_ptr<ComponentGrid> mButtons; + std::shared_ptr<ButtonComponent> mButton1; + std::shared_ptr<ButtonComponent> mButton2; + + std::shared_ptr<TextComponent> mTitle; + std::shared_ptr<TextComponent> mStatusHeader; + std::shared_ptr<TextComponent> mProcessStep1; + std::shared_ptr<TextComponent> mProcessStep2; + std::shared_ptr<TextComponent> mProcessStep3; + std::shared_ptr<TextComponent> mStatusMessageHeader; + std::shared_ptr<TextComponent> mStatusMessage; + std::shared_ptr<TextComponent> mChangelogMessage; + + std::unique_ptr<std::thread> mThread; + std::unique_ptr<HttpReq> mRequest; + AsyncHandleStatus mStatus; + std::mutex mMutex; + std::string mMessage; + + ApplicationUpdater::Package mPackage; + std::atomic<bool> mAbortDownload; + std::atomic<bool> mDownloading; + std::atomic<bool> mReadyToInstall; + bool mHasDownloaded; + bool mInstalling; + bool mHasInstalled; +}; + +#endif // ES_APP_GUIS_GUI_APPLICATION_UPDATER_H diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index dab874390..1dab9531a 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -27,6 +27,7 @@ #include "animations/Animation.h" #include "animations/LambdaAnimation.h" #include "animations/MoveCameraAnimation.h" +#include "guis/GuiApplicationUpdater.h" #include "guis/GuiMenu.h" #include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" @@ -306,11 +307,22 @@ void ViewController::updateAvailableDialog() << "\""; } - mWindow->pushGui(new GuiMsgBox(getHelpStyle(), results, "OK", nullptr, "", nullptr, "", nullptr, - true, true, - (mRenderer->getIsVerticalOrientation() ? - 0.70f : - 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + if (package.name == "LinuxAppImage" || package.name == "LinuxSteamDeckAppImage") { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), results, "UPDATE", + [this] { mWindow->pushGui(new GuiApplicationUpdater()); }, "CANCEL", [] { return; }, "", + nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else { + mWindow->pushGui(new GuiMsgBox(getHelpStyle(), results, "OK", nullptr, "", nullptr, "", + nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + } } void ViewController::goToStart(bool playTransition)