Skip to content

feat: allow disabling of cloud saves per game#596

Open
xXJSONDeruloXx wants to merge 11 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:feat/local-save-only
Open

feat: allow disabling of cloud saves per game#596
xXJSONDeruloXx wants to merge 11 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:feat/local-save-only

Conversation

@xXJSONDeruloXx
Copy link
Contributor

@xXJSONDeruloXx xXJSONDeruloXx commented Feb 21, 2026

image image

Summary by cubic

Adds a per-game “Local saves only” setting to disable cloud save sync for Steam, GOG, and Epic. Shows a Steam client sync warning when launching the Steam client with local-only enabled.

  • New Features

    • Per-game toggle in General settings.
    • Skips pre/post cloud sync for Steam/GOG/Epic when enabled.
    • “Force Cloud Sync” remains available with local-only on.
    • Steam client sync warning dialog; play anyway or cancel.
    • Localized strings for the setting and warning.
  • Bug Fixes

    • Centralized exit cloud sync into handleExitCloudSync; now scoped by game source and offline mode, and reads local-only from container state to avoid side effects.
    • Prevent stale menu state by reading local-only when building Epic/Steam menus.
    • Escaped apostrophe in the Italian Steam warning string.

Written for commit eac5d74. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • New Steam client sync warning dialog shown before launch when "Local Saves Only" is enabled, letting users confirm or cancel launching without cloud sync.
  • UI

    • "Local Saves Only" toggle added to General settings.
    • "Force Cloud Sync" menu option is disabled when local-saves-only is active.
    • Menu entries now reflect enabled/disabled states.
  • Behavior

    • Cloud-sync steps skipped on launch and exit when "Local Saves Only" is enabled.
  • Strings

    • Added localized texts for the new setting and Steam sync warning.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a local-saves-only flag threaded through container data, settings, menus, and launch/exit flows; introduces a Steam-client local-saves warning dialog (DialogType.STEAM_CLIENT_SYNC_WARNING) and an ignoreSteamClientLocalSavesWarning parameter to preLaunchApp; cloud-sync steps are conditionally skipped when local-saves-only is active.

Changes

Cohort / File(s) Summary
Container model & utils
com/winlator/container/ContainerData.kt, app/src/main/java/app/gamenative/utils/ContainerUtils.kt
Add localSavesOnly: Boolean to container data, persist/restore it in Saver, and expose isLocalSavesOnly(context, appId) plus extras read/write plumbing.
Settings UI & localization
app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt, app/src/main/res/values/strings.xml, app/src/main/res/values-*/strings.xml
Add localSavesOnly SettingsSwitch in General tab and new string resources for the local-saves switch and Steam client sync warning across locales.
Pre-launch flow & dialog
app/src/main/java/app/gamenative/ui/PluviaMain.kt, app/src/main/java/app/gamenative/ui/enums/DialogType.kt
Extend preLaunchApp with ignoreSteamClientLocalSavesWarning: Boolean = false; introduce DialogType.STEAM_CLIENT_SYNC_WARNING; intercept pre-launch to show the Steam warning when appropriate.
Exit/cloud-sync gating
app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
Gate shutdown/exit cloud-sync steps for GOG, Epic, and Steam on isLocalSavesOnly, skipping background sync and adding skip logs when true.
Menu option enabled state
app/src/main/java/app/gamenative/ui/data/AppMenuOption.kt, app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
Add enabled: Boolean = true to AppMenuOption and bind menuOption.enabled to DropdownMenuItem enabled state.
Provider-specific menu behavior
app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Compute container isLocalSavesOnly and disable the ForceCloudSync menu option when local-saves-only is true.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as UI/Settings
participant Container as ContainerUtils
participant Launcher as PluviaMain
participant Dialog as DialogController
participant Provider as CloudProvider (GOG/Epic/Steam)

UI->>Container: toggle localSavesOnly
Container-->>UI: persist flag
UI->>Launcher: preLaunchApp(appId, ...)
Launcher->>Container: isLocalSavesOnly(context, appId)?
alt localSavesOnly == true and Steam client will be launched
    Launcher->>Dialog: show STEAM_CLIENT_SYNC_WARNING
    Dialog-->>Launcher: user action (confirm/dismiss)
    alt user confirmed
        Launcher->>Provider: proceed without cloud sync (log skip)
        Launcher->>Provider: continue launch
    else user dismissed
        Launcher-->>UI: abort pre-launch
    end
else localSavesOnly == false
    Launcher->>Provider: perform cloud sync
    Provider-->>Launcher: sync result
    Launcher->>Provider: continue launch
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇
I hopped through code and left a switch so neat,
"Local saves only" tucked where settings meet.
A Steam warning knocks — confirm or stay,
Saves snug as carrots, safe for another day.
The rabbit smiles and hops off, light on tiny feet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: allow disabling of cloud saves per game' accurately and concisely describes the main change: adding a per-game feature to disable cloud save synchronization across multiple platforms.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 10 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt:809">
P2: isLocalSavesOnly is computed before the early return, and it calls getOrCreateContainer. This can create container data or do I/O even for games that are not installed or are downloading. Move the call below the guard to avoid side effects for those cases.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/ContainerUtils.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/ContainerUtils.kt:1030">
P2: isLocalSavesOnly creates containers as a side effect; this can trigger heavy container creation when UI code only needs to read a flag (e.g., in menu rendering), causing unexpected I/O and container creation for apps without containers.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (1)

809-812: Same staleness/performance concern as EpicAppScreen; also runs before the early-return guard unnecessarily.

ContainerUtils.isLocalSavesOnly is evaluated on every recomposition with no remember, and it executes even when the function is about to return emptyList() (lines 811-813). Both issues can be addressed by moving the call after the early-return guard and memoising it.

♻️ Suggested fix
         val isDownloadInProgress = SteamService.getDownloadingAppInfoOf(gameId) != null
-        val isLocalSavesOnly = ContainerUtils.isLocalSavesOnly(context, appId)
 
         if (!isInstalled || isDownloadInProgress) {
             return emptyList()
         }
 
         val scope = rememberCoroutineScope()
+        val isLocalSavesOnly = remember(appId) {
+            ContainerUtils.isLocalSavesOnly(context, appId)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`
around lines 809 - 812, Move the ContainerUtils.isLocalSavesOnly call so it runs
after the early-return guard (the checks of isInstalled and
isDownloadInProgress) and memoize it with Compose's remember; e.g. replace the
immediate call to ContainerUtils.isLocalSavesOnly(context, appId) with a
remembered value computed after the if (!isInstalled || isDownloadInProgress)
return, using remember(context, appId) {
ContainerUtils.isLocalSavesOnly(context, appId) } so the expensive call doesn't
run on every recomposition or when the function returns early.
app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt (1)

563-573: Wrap isLocalSavesOnly in remember (or expose it as Compose State) to avoid stale enabled state and redundant recomputation.

ContainerUtils.isLocalSavesOnly(context, libraryItem.appId) is called on the main thread on every recomposition of getSourceSpecificMenuOptions with no memoisation. Two problems:

  1. Staleness – if isLocalSavesOnly reads from a non-Compose-observable source (e.g. SharedPreferences / a file), changes to the setting won't retrigger recomposition, so the ForceCloudSync button's enabled state stays stale until an unrelated recompose occurs.
  2. Performance – if the read involves any I/O, it blocks the main thread on every frame that triggers this composable.
♻️ Suggested fix
-        val isLocalSavesOnly = app.gamenative.utils.ContainerUtils.isLocalSavesOnly(context, libraryItem.appId)
+        val isLocalSavesOnly = remember(libraryItem.appId) {
+            app.gamenative.utils.ContainerUtils.isLocalSavesOnly(context, libraryItem.appId)
+        }

For full reactivity, consider making ContainerUtils.isLocalSavesOnly return a StateFlow<Boolean> and collecting it with collectAsState() here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt`
around lines 563 - 573, The call to ContainerUtils.isLocalSavesOnly(context,
libraryItem.appId) inside getSourceSpecificMenuOptions should be
memoized/observed so the ForceCloudSync AppMenuOption's enabled state is not
stale or recomputed every recomposition; replace the direct call with a
Compose-aware value (e.g. obtain a State<Boolean> by either wrapping the current
boolean in remember { mutableStateOf(...) } if the value is static for the
composition, or better: change ContainerUtils.isLocalSavesOnly to expose a
StateFlow<Boolean> and collect it here with collectAsState(), then use that
state value when setting enabled for AppOptionMenuType.ForceCloudSync so updates
and recompositions occur correctly and no blocking IO runs on every
recomposition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/utils/ContainerUtils.kt`:
- Around line 1029-1032: The current isLocalSavesOnly calls getOrCreateContainer
which may create a container as a side effect; change it to perform a
non‑creating lookup (e.g., use an existing read-only getter like
getContainer/getContainerIfExists or add one) and return false when no container
or key exists; specifically replace the getOrCreateContainer(context, appId)
call inside isLocalSavesOnly with a non-creating lookup, read
EXTRA_LOCAL_SAVES_ONLY only if the container exists, and default to "false".

---

Nitpick comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt`:
- Around line 563-573: The call to ContainerUtils.isLocalSavesOnly(context,
libraryItem.appId) inside getSourceSpecificMenuOptions should be
memoized/observed so the ForceCloudSync AppMenuOption's enabled state is not
stale or recomputed every recomposition; replace the direct call with a
Compose-aware value (e.g. obtain a State<Boolean> by either wrapping the current
boolean in remember { mutableStateOf(...) } if the value is static for the
composition, or better: change ContainerUtils.isLocalSavesOnly to expose a
StateFlow<Boolean> and collect it here with collectAsState(), then use that
state value when setting enabled for AppOptionMenuType.ForceCloudSync so updates
and recompositions occur correctly and no blocking IO runs on every
recomposition.

In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 809-812: Move the ContainerUtils.isLocalSavesOnly call so it runs
after the early-return guard (the checks of isInstalled and
isDownloadInProgress) and memoize it with Compose's remember; e.g. replace the
immediate call to ContainerUtils.isLocalSavesOnly(context, appId) with a
remembered value computed after the if (!isInstalled || isDownloadInProgress)
return, using remember(context, appId) {
ContainerUtils.isLocalSavesOnly(context, appId) } so the expensive call doesn't
run on every recomposition or when the function returns early.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt:814">
P2: Memoizing isLocalSavesOnly with remember(appId) prevents the menu from updating when the setting toggles; ForceCloudSync can stay stale until screen recreation.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt:565">
P2: `remember(libraryItem.appId)` caches `isLocalSavesOnly` so toggling local-only saves for the same game won’t update the menu enabled state until the composable is recreated.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt`:
- Around line 565-567: The cached isLocalSavesOnly value uses
remember(libraryItem.appId) which only invalidates when appId changes and
therefore becomes stale after settings change; replace the remembered call so
that you call ContainerUtils.isLocalSavesOnly(context, libraryItem.appId)
directly (remove the remember wrapper around the isLocalSavesOnly assignment in
EpicAppScreen.kt) so the check runs on every recomposition and reflects current
settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 814-815: The call to ContainerUtils.isLocalSavesOnly(context,
appId) performs main-thread disk I/O on every recomposition; wrap this call in
Compose's remember keyed by appId (e.g., val isLocalSavesOnly = remember(appId)
{ ContainerUtils.isLocalSavesOnly(context, appId) }) so the filesystem scan via
ContainerManager/loadContainers only runs once per appId instead of on every
recomposition.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/main/res/values/strings.xml (1)

496-497: Add translations for the new local-saves toggle strings in other locales.

local_saves_only and its description are only defined in the base locale here; other locale files in this PR only add the warning strings. Consider adding localized versions (or mark as non‑translatable if intentional) so non‑English UIs don’t fall back to English for the toggle/description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values/strings.xml` around lines 496 - 497, The two new
string resources local_saves_only and local_saves_only_description are only
defined in the base locale so other locales fall back to English; update each
translated strings.xml (the same locale files where you already added the
warning strings) to include localized versions of local_saves_only and
local_saves_only_description, or if these labels must remain identical across
locales mark them as translatable="false" on the <string> entries; ensure you
update the string names local_saves_only and local_saves_only_description
consistently across all locale resource files.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/res/values-it/strings.xml`:
- Around line 1014-1015: The string resource steam_client_sync_warning_message
contains an invalid escape (a backslash-based/unrecognized unicode escape)
causing the build to fail; open the string for steam_client_sync_warning_message
and remove the backslash escape(s) and replace any escaped apostrophe sequences
with a proper XML escape (e.g., use &apos; or &#39;) or a plain apostrophe,
ensuring the string is valid XML so resources compile.

In `@app/src/main/res/values-pt-rBR/strings.xml`:
- Around line 885-886: Fix the Portuguese grammar in the string named
steam_client_sync_warning_message by correcting the subject-verb agreement:
update the message to use a singular setting label or plural verb form (e.g., "O
modo somente salvamentos locais está ativado, mas o cliente Steam está
configurado para iniciar..." or "Somente salvamentos locais estão ativados, mas
o cliente Steam está configurado para iniciar...") so the subject
("salvamentos") and verb agree; keep the rest of the sentence unchanged.

---

Nitpick comments:
In `@app/src/main/res/values/strings.xml`:
- Around line 496-497: The two new string resources local_saves_only and
local_saves_only_description are only defined in the base locale so other
locales fall back to English; update each translated strings.xml (the same
locale files where you already added the warning strings) to include localized
versions of local_saves_only and local_saves_only_description, or if these
labels must remain identical across locales mark them as translatable="false" on
the <string> entries; ensure you update the string names local_saves_only and
local_saves_only_description consistently across all locale resource files.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/utils/ContainerUtils.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/ContainerUtils.kt:285">
P2: Legacy localSavesOnly extras can keep the flag permanently true because the new OR fallback still reads the extra, while applyToContainer no longer clears/updates it. This makes disabling local-only saves impossible for containers created under the old scheme.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@xXJSONDeruloXx xXJSONDeruloXx moved this to PR Needs Review in Open Source Feb 23, 2026
Comment on lines 375 to 378
SteamService.closeApp(gameId, isOffline.value) { prefix ->
PathType.from(prefix).toAbsPath(context, gameId, SteamService.userSteamId!!.accountID)
}.await()
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add a check for steam game source as well here, it'll do this for custom games for instance.

return
}

if (ContainerUtils.extractGameSourceFromContainerId(appId) == GameSource.GOG) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put ContainerUtils.extractGameSourceFromContainerId(appId) in a variable so we don't have to extract it multiple times

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using gamesource now!

return emptyList()
}

val isLocalSavesOnly = ContainerUtils.isLocalSavesOnly(context, appId)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used? if not we should remove it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nope, removed

}

private suspend fun handleExitCloudSync(context: Context, appId: String, gameId: Int) {
if (ContainerUtils.isLocalSavesOnly(context, appId)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should likely add || isOfflineMode here too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@utkarshdalal
Copy link
Owner

@xXJSONDeruloXx i've left some comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: PR Needs Review

Development

Successfully merging this pull request may close these issues.

2 participants