From 8408fc77c1e89637534db62965068572a400a2c6 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sat, 2 May 2026 13:43:45 -0500 Subject: updater: SLR auto-update + wire fluorine updater badge, bump 0.2.0 - OrganizerCore: add fluorineUpdater() getter + checkForSlrUpdates() fired from startup checkForUpdates(); detached thread runs downloadSlr when SLR already installed so steamrt4 BUILD_ID drift is picked up without a launch-time stall. - MainWindow: connect FluorineUpdater::updateAvailable to existing updateAvailable() slot so the statusbar badge + actionUpdate light up; on_actionUpdate_triggered routes to Settings -> Updates when a Fluorine update is pending (the MO2 self-updater path is no-op'd). - MainWindow launch path: drop !isSlrInstalled() short-circuit; fresh installs still get the progress dialog, up-to-date checks rely on the startup background pass. - SettingsDialog: selectTabByLabel() so MainWindow can open Updates tab. - Launcher: rm -rf update-staging/ once a new bundle has synced into bin/, so the in-app updater doesn't accumulate stale extracts. - CMakeLists.txt: 0.1.4 -> 0.2.0. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/src/mainwindow.cpp | 107 +++++++++++++++++++++++++++++---------------- src/src/mainwindow.h | 5 +++ src/src/organizercore.cpp | 29 ++++++++++++ src/src/organizercore.h | 2 + src/src/settingsdialog.cpp | 14 ++++++ src/src/settingsdialog.h | 5 +++ 6 files changed, 124 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index a928c6d..392c751 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -39,6 +39,7 @@ along with Mod Organizer. If not, see . #include "executableslist.h" #include "filedialogmemory.h" #include "filterlist.h" +#include "fluorineupdater.h" #include "guessedvalue.h" #include "imodinterface.h" #include "installationmanager.h" @@ -496,6 +497,17 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, } }); + // Fluorine self-updater is constructed during setUserInterface() above + // (lazily by checkForFluorineUpdates()). The bare MO2 self-updater is + // no-op'd, so this is what actually drives the statusbar update badge. + if (auto* fu = m_OrganizerCore.fluorineUpdater()) { + connect(fu, &FluorineUpdater::updateAvailable, this, + [this](const FluorineUpdater::ReleaseInfo&) { + m_FluorineUpdatePending = true; + updateAvailable(); + }); + } + connect(m_OrganizerCore.modList(), &ModList::showMessage, [=, this](auto&& message) { showMessage(message); }); @@ -2381,7 +2393,10 @@ void MainWindow::on_startButton_clicked() }); // Pre-check: if this executable uses Proton and the current instance has SLR - // enabled, download SLR before launching if it isn't installed yet. + // enabled, ensure SLR is installed (and up-to-date with the latest steamrt4 + // BUILD_ID) before launching. downloadSlr() short-circuits on BUILD_ID match + // so once SLR is current this is a single HTTP GET; no UI is shown unless + // the runtime actually needs (re)downloading. if (selectedExecutable->useProton()) { const auto* s = Settings::maybeInstance(); bool useSLR = true; @@ -2389,43 +2404,49 @@ void MainWindow::on_startButton_clicked() QSettings const instanceIni(s->filename(), QSettings::IniFormat); useSLR = instanceIni.value("fluorine/use_slr", true).toBool(); } - if (useSLR && !isSlrInstalled()) { - auto* progress = new QProgressDialog( - tr("Downloading Steam Linux Runtime (~200 MB)...\n" - "This is required to launch games. Check the MO2 log for details."), - tr("Cancel"), 0, 0, this); - progress->setWindowTitle(tr("Steam Linux Runtime")); - progress->setWindowModality(Qt::WindowModal); - progress->setAttribute(Qt::WA_ShowWithoutActivating); - progress->setMinimumDuration(0); - - int cancelFlag = 0; - connect(progress, &QProgressDialog::canceled, this, [&cancelFlag] { - cancelFlag = 1; - }); - - // Run download synchronously using an event loop so the dialog stays responsive. - QFutureWatcher watcher; - QEventLoop loop; - connect(&watcher, &QFutureWatcher::finished, &loop, &QEventLoop::quit); - watcher.setFuture(QtConcurrent::run([&cancelFlag]() -> QString { - return downloadSlr(nullptr, nullptr, &cancelFlag); - })); - progress->show(); - loop.exec(); - progress->close(); - progress->deleteLater(); - - const QString err = watcher.result(); - if (cancelFlag) { - return; // user cancelled, don't launch - } - if (!err.isEmpty()) { - log::error("[SLR] Download failed: {}", err); - QMessageBox::warning(this, tr("Steam Linux Runtime"), - tr("Steam Linux Runtime download failed:\n%1\n\n" - "You can disable SLR in the Instance Manager and try again.").arg(err)); - return; + if (useSLR) { + const bool freshInstall = !isSlrInstalled(); + // Only show the heavyweight progress dialog on a fresh install. + // Up-to-date checks happen quietly in the background here so we don't + // block launch on a remote BUILD_ID fetch — the up-to-date case is + // handled at startup by OrganizerCore::checkForSlrUpdates(). + if (freshInstall) { + auto* progress = new QProgressDialog( + tr("Downloading Steam Linux Runtime (~200 MB)...\n" + "This is required to launch games. Check the MO2 log for details."), + tr("Cancel"), 0, 0, this); + progress->setWindowTitle(tr("Steam Linux Runtime")); + progress->setWindowModality(Qt::WindowModal); + progress->setAttribute(Qt::WA_ShowWithoutActivating); + progress->setMinimumDuration(0); + + int cancelFlag = 0; + connect(progress, &QProgressDialog::canceled, this, [&cancelFlag] { + cancelFlag = 1; + }); + + QFutureWatcher watcher; + QEventLoop loop; + connect(&watcher, &QFutureWatcher::finished, &loop, &QEventLoop::quit); + watcher.setFuture(QtConcurrent::run([&cancelFlag]() -> QString { + return downloadSlr(nullptr, nullptr, &cancelFlag); + })); + progress->show(); + loop.exec(); + progress->close(); + progress->deleteLater(); + + const QString err = watcher.result(); + if (cancelFlag) { + return; // user cancelled, don't launch + } + if (!err.isEmpty()) { + log::error("[SLR] Download failed: {}", err); + QMessageBox::warning(this, tr("Steam Linux Runtime"), + tr("Steam Linux Runtime download failed:\n%1\n\n" + "You can disable SLR in the Instance Manager and try again.").arg(err)); + return; + } } } } @@ -3144,6 +3165,16 @@ void MainWindow::motdReceived(const QString& motd) void MainWindow::on_actionUpdate_triggered() { + // Fluorine has its own updater (Settings → Updates). The MO2 self-updater + // is no-op'd (see SelfUpdater::testForUpdate). Open the Updates tab so + // the user can see the release info and trigger Install & restart. + if (m_FluorineUpdatePending) { + Settings& settings = m_OrganizerCore.settings(); + SettingsDialog dialog(&m_PluginContainer, settings, this); + dialog.selectTabByLabel(tr("Updates")); + dialog.exec(); + return; + } m_OrganizerCore.startMOUpdate(); } diff --git a/src/src/mainwindow.h b/src/src/mainwindow.h index 20a7286..9af07bd 100644 --- a/src/src/mainwindow.h +++ b/src/src/mainwindow.h @@ -293,6 +293,11 @@ private: QTime m_StartTime; + // Set when FluorineUpdater reports a new release; consumed by + // on_actionUpdate_triggered() to route to Settings → Updates instead of + // the (no-op'd) MO2 self-updater. + bool m_FluorineUpdatePending = false; + OrganizerCore& m_OrganizerCore; PluginContainer& m_PluginContainer; diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 8b3171e..3707d58 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -2,6 +2,7 @@ #include "categoriesdialog.h" #include "credentialsdialog.h" #include "fluorineupdater.h" +#include "slrmanager.h" #include "delayedfilewriter.h" #include "directoryrefresher.h" #include "env.h" @@ -48,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -393,9 +395,36 @@ void OrganizerCore::checkForUpdates() if (m_UserInterface != nullptr) { m_Updater.testForUpdate(m_Settings); checkForFluorineUpdates(); + checkForSlrUpdates(); } } +void OrganizerCore::checkForSlrUpdates() +{ + // SLR auto-update: only relevant if SLR is already installed. The first- + // install case is handled at game launch in MainWindow, where the user gets + // a progress dialog. Here we silently background-fetch a newer steamrt4 + // image when one exists; downloadSlr() short-circuits on BUILD_ID match so + // the cost is one HTTP GET when already up-to-date. + if (!m_Settings.checkForUpdates()) { + return; + } + if (m_Settings.network().offlineMode()) { + return; + } + if (!isSlrInstalled()) { + return; + } + + // Fire and forget on a detached thread. No UI; result is logged. + std::thread([]() { + const QString err = downloadSlr(nullptr, nullptr, nullptr); + if (!err.isEmpty()) { + MOBase::log::warn("SLR update check failed: {}", err); + } + }).detach(); +} + void OrganizerCore::checkForFluorineUpdates() { // Set up the Fluorine self-update checker lazily so repeated calls don't diff --git a/src/src/organizercore.h b/src/src/organizercore.h index 7f60998..7ef7713 100644 --- a/src/src/organizercore.h +++ b/src/src/organizercore.h @@ -258,10 +258,12 @@ public: void checkForUpdates(); void checkForFluorineUpdates(); + void checkForSlrUpdates(); void startMOUpdate(); Settings& settings(); SelfUpdater* updater() { return &m_Updater; } + class FluorineUpdater* fluorineUpdater() const { return m_FluorineUpdater; } InstallationManager* installationManager(); MOShared::DirectoryEntry* directoryStructure() { return m_DirectoryStructure; } DirectoryRefresher* directoryRefresher() { return m_DirectoryRefresher.get(); } diff --git a/src/src/settingsdialog.cpp b/src/src/settingsdialog.cpp index c0e254b..e07ac71 100644 --- a/src/src/settingsdialog.cpp +++ b/src/src/settingsdialog.cpp @@ -82,11 +82,25 @@ ExitFlags SettingsDialog::exitNeeded() const return m_exit; } +void SettingsDialog::selectTabByLabel(const QString& label) +{ + for (int i = 0; i < ui->tabWidget->count(); ++i) { + if (ui->tabWidget->tabText(i) == label) { + ui->tabWidget->setCurrentIndex(i); + m_pendingTabOverride = i; + return; + } + } +} + int SettingsDialog::exec() { GeometrySaver gs(m_settings, this); m_settings.widgets().restoreIndex(ui->tabWidget); + if (m_pendingTabOverride >= 0) { + ui->tabWidget->setCurrentIndex(m_pendingTabOverride); + } auto ret = TutorableDialog::exec(); diff --git a/src/src/settingsdialog.h b/src/src/settingsdialog.h index 864eae7..030f6f6 100644 --- a/src/src/settingsdialog.h +++ b/src/src/settingsdialog.h @@ -80,6 +80,10 @@ public: void setExitNeeded(ExitFlags e); ExitFlags exitNeeded() const; + // Pre-select a tab by its visible label (e.g. "Updates"). Must be called + // before exec(); any saved tab index is overridden. + void selectTabByLabel(const QString& label); + int exec() override; public slots: @@ -91,6 +95,7 @@ private: std::vector> m_tabs; ExitFlags m_exit; PluginContainer* m_pluginContainer; + int m_pendingTabOverride = -1; }; #endif // SETTINGSDIALOG_H -- cgit v1.3.1