aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-02 13:43:45 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-02 13:43:45 -0500
commit8408fc77c1e89637534db62965068572a400a2c6 (patch)
tree92938dcb4c3c8b06e6e3dbc42971bb22c341b62c
parenta2e79c0928c34e753953ccf34af63225046b8db9 (diff)
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) <noreply@anthropic.com>
-rw-r--r--CMakeLists.txt4
-rwxr-xr-xdocker/build-inner.sh5
-rw-r--r--src/src/mainwindow.cpp101
-rw-r--r--src/src/mainwindow.h5
-rw-r--r--src/src/organizercore.cpp29
-rw-r--r--src/src/organizercore.h2
-rw-r--r--src/src/settingsdialog.cpp14
-rw-r--r--src/src/settingsdialog.h5
8 files changed, 128 insertions, 37 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 71de209..49b6ddc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,8 +5,8 @@ project(ModOrganizer2 VERSION 2.5.3 LANGUAGES C CXX)
# Fluorine Manager version (distinct from the MO2 engine version above).
# ---------------------------------------------------------------------------
set(FLUORINE_VERSION_MAJOR 0)
-set(FLUORINE_VERSION_MINOR 1)
-set(FLUORINE_VERSION_PATCH 4)
+set(FLUORINE_VERSION_MINOR 2)
+set(FLUORINE_VERSION_PATCH 0)
set(FLUORINE_BUILD_CHANNEL "stable" CACHE STRING
"Build channel: 'stable' for tagged releases, 'beta' for rolling CI builds")
set(FLUORINE_BUILD_TIMESTAMP "" CACHE STRING
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index 7adab23..24e965a 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -454,6 +454,11 @@ if [ "${HERE_REAL}" != "${DST_REAL}" ]; then
cp -af "${MANIFEST}" "${BIN_DST}/fluorine-manifest.txt"
echo "${CURRENT_VER}" > "${MARKER}"
echo "Sync complete." >&2
+
+ # Clean up the in-app updater's staging directory once the new bundle
+ # is live. Untouched if the user didn't go through the in-app updater.
+ STAGING_DIR="${FLUORINE_DATA}/update-staging"
+ [ -d "${STAGING_DIR}" ] && rm -rf "${STAGING_DIR}" 2>/dev/null || true
fi
fi
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 <http://www.gnu.org/licenses/>.
#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);
+ 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;
- });
+ int cancelFlag = 0;
+ connect(progress, &QProgressDialog::canceled, this, [&cancelFlag] {
+ cancelFlag = 1;
+ });
- // Run download synchronously using an event loop so the dialog stays responsive.
- QFutureWatcher<QString> watcher;
- QEventLoop loop;
- connect(&watcher, &QFutureWatcher<QString>::finished, &loop, &QEventLoop::quit);
- watcher.setFuture(QtConcurrent::run([&cancelFlag]() -> QString {
- return downloadSlr(nullptr, nullptr, &cancelFlag);
- }));
- progress->show();
- loop.exec();
- progress->close();
- progress->deleteLater();
+ QFutureWatcher<QString> watcher;
+ QEventLoop loop;
+ connect(&watcher, &QFutureWatcher<QString>::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;
+ 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 <chrono>
#include <cstdio>
#include <filesystem>
+#include <thread>
#include <QApplication>
#include <QCoreApplication>
#include <QDialog>
@@ -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<std::unique_ptr<SettingsTab>> m_tabs;
ExitFlags m_exit;
PluginContainer* m_pluginContainer;
+ int m_pendingTabOverride = -1;
};
#endif // SETTINGSDIALOG_H