Skip to content

Commit f96ac80

Browse files
feat: local saves only — conflict dialog on force sync and cache invalidation on toggle-off
1 parent 24508c2 commit f96ac80

19 files changed

Lines changed: 230 additions & 58 deletions

File tree

app/src/main/java/app/gamenative/service/SteamService.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ import app.gamenative.utils.generateSteamApp
5959
import app.gamenative.workshop.WorkshopManager
6060
import com.winlator.container.Container
6161
import com.winlator.xenvironment.ImageFs
62+
import dagger.hilt.EntryPoint
63+
import dagger.hilt.InstallIn
6264
import dagger.hilt.android.AndroidEntryPoint
65+
import dagger.hilt.android.EntryPointAccessors
66+
import dagger.hilt.components.SingletonComponent
6367
import `in`.dragonbra.javasteam.depotdownloader.DepotDownloader
6468
import `in`.dragonbra.javasteam.depotdownloader.IDownloadListener
6569
import `in`.dragonbra.javasteam.depotdownloader.data.AppItem
@@ -2634,6 +2638,24 @@ class SteamService : Service(), IChallengeUrlChanged {
26342638
else -> false
26352639
}
26362640

2641+
@EntryPoint
2642+
@InstallIn(SingletonComponent::class)
2643+
interface CloudSyncCacheEntryPoint {
2644+
fun fileChangeListsDao(): FileChangeListsDao
2645+
}
2646+
2647+
fun clearCloudSyncCache(context: Context, appId: Int) {
2648+
val dao = instance?.fileChangeListsDao
2649+
?: EntryPointAccessors.fromApplication(
2650+
context.applicationContext,
2651+
CloudSyncCacheEntryPoint::class.java,
2652+
).fileChangeListsDao()
2653+
runBlocking {
2654+
dao.deleteByAppId(appId)
2655+
}
2656+
Timber.i("Cleared cloud sync cache for appId=$appId")
2657+
}
2658+
26372659
fun clearDatabase(clearCloudSyncState: Boolean = false) {
26382660
with(instance!!) {
26392661
scope.launch {

app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,13 @@ fun GeneralTabContent(
320320
state = config.forceDlc,
321321
onCheckedChange = { state.config.value = config.copy(forceDlc = it) },
322322
)
323-
// SettingsSwitch(
324-
// colors = settingsTileColorsAlt(),
325-
// title = { Text(text = stringResource(R.string.local_saves_only)) },
326-
// subtitle = { Text(text = stringResource(R.string.local_saves_only_description)) },
327-
// state = config.localSavesOnly,
328-
// onCheckedChange = { state.config.value = config.copy(localSavesOnly = it) },
329-
// )
323+
SettingsSwitch(
324+
colors = settingsTileColorsAlt(),
325+
title = { Text(text = stringResource(R.string.local_saves_only)) },
326+
subtitle = { Text(text = stringResource(R.string.local_saves_only_description)) },
327+
state = config.localSavesOnly,
328+
onCheckedChange = { state.config.value = config.copy(localSavesOnly = it) },
329+
)
330330
SettingsSwitch(
331331
colors = settingsTileColorsAlt(),
332332
title = { Text(text = stringResource(R.string.use_legacy_drm)) },

app/src/main/java/app/gamenative/ui/enums/DialogType.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum class DialogType(val icon: ImageVector? = null) {
2828
DELETE_APP,
2929
INSTALL_IMAGEFS,
3030
UPDATE_VERIFY_CONFIRM,
31+
CLOUD_SYNC_CONFLICT,
3132
RESET_CONTAINER_CONFIRM,
3233

3334
GAME_FEEDBACK,

app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt

Lines changed: 127 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import app.gamenative.R
4040
import app.gamenative.data.LibraryItem
4141
import app.gamenative.enums.Marker
4242
import app.gamenative.enums.PathType
43+
import app.gamenative.enums.SaveLocation
4344
import app.gamenative.enums.SyncResult
4445
import app.gamenative.events.AndroidEvent
4546
import app.gamenative.service.DownloadService
@@ -60,6 +61,7 @@ import app.gamenative.workshop.WorkshopManager
6061
import app.gamenative.NetworkMonitor
6162
import com.google.android.play.core.splitcompat.SplitCompat
6263
import com.posthog.PostHog
64+
import com.winlator.container.Container
6365
import com.winlator.container.ContainerData
6466
import com.winlator.container.ContainerManager
6567
import com.winlator.fexcore.FEXCoreManager
@@ -213,6 +215,78 @@ class SteamAppScreen : BaseAppScreen() {
213215
fun getPendingUpdateVerifyOperation(gameId: Int): AppOptionMenuType? {
214216
return pendingUpdateVerifyOperations[gameId]
215217
}
218+
219+
fun performForceCloudSync(
220+
context: Context,
221+
appId: String,
222+
gameId: Int,
223+
preferredSave: SaveLocation = SaveLocation.None,
224+
container: Container? = null,
225+
) {
226+
CoroutineScope(Dispatchers.IO).launch {
227+
val steamId = SteamService.userSteamId
228+
if (steamId == null) {
229+
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
230+
return@launch
231+
}
232+
233+
val resolvedContainer = container ?: ContainerUtils.getOrCreateContainer(context, appId)
234+
val containerManager = ContainerManager(context)
235+
containerManager.activateContainer(resolvedContainer)
236+
237+
val prefixToPath: (String) -> String = { prefix ->
238+
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
239+
}
240+
val syncResult = SteamService.forceSyncUserFiles(
241+
appId = gameId,
242+
prefixToPath = prefixToPath,
243+
preferredSave = preferredSave,
244+
).await()
245+
246+
when (syncResult.syncResult) {
247+
SyncResult.Success -> {
248+
SnackbarManager.show(context.getString(R.string.steam_cloud_sync_success))
249+
}
250+
251+
SyncResult.UpToDate -> {
252+
SnackbarManager.show(context.getString(R.string.steam_cloud_sync_up_to_date))
253+
}
254+
255+
SyncResult.Conflict -> {
256+
val fmt = java.text.DateFormat.getDateTimeInstance(
257+
java.text.DateFormat.MEDIUM,
258+
java.text.DateFormat.SHORT,
259+
)
260+
withContext(Dispatchers.Main) {
261+
showInstallDialog(
262+
gameId,
263+
MessageDialogState(
264+
visible = true,
265+
type = DialogType.CLOUD_SYNC_CONFLICT,
266+
title = context.getString(R.string.main_save_conflict_title),
267+
message = context.getString(
268+
R.string.main_save_conflict_message,
269+
fmt.format(java.util.Date(syncResult.localTimestamp)),
270+
fmt.format(java.util.Date(syncResult.remoteTimestamp)),
271+
),
272+
confirmBtnText = context.getString(R.string.main_keep_remote),
273+
dismissBtnText = context.getString(R.string.main_keep_local),
274+
),
275+
)
276+
}
277+
}
278+
279+
else -> {
280+
SnackbarManager.show(
281+
context.getString(
282+
R.string.steam_cloud_sync_failed,
283+
syncResult.syncResult,
284+
),
285+
)
286+
}
287+
}
288+
}
289+
}
216290
}
217291

218292
@Composable
@@ -709,15 +783,20 @@ class SteamAppScreen : BaseAppScreen() {
709783
AppMenuOption(
710784
AppOptionMenuType.VerifyFiles,
711785
onClick = {
712-
// Show confirmation dialog before verifying
713786
setPendingUpdateVerifyOperation(gameId, AppOptionMenuType.VerifyFiles)
787+
val container = ContainerUtils.getOrCreateContainer(context, appId)
788+
val verifyMessage = if (container.isLocalSavesOnly) {
789+
context.getString(R.string.steam_verify_files_message_local_saves)
790+
} else {
791+
context.getString(R.string.steam_verify_files_message)
792+
}
714793
showInstallDialog(
715794
gameId,
716795
MessageDialogState(
717796
visible = true,
718797
type = DialogType.UPDATE_VERIFY_CONFIRM,
719798
title = context.getString(R.string.steam_verify_files_title),
720-
message = context.getString(R.string.steam_verify_files_message),
799+
message = verifyMessage,
721800
confirmBtnText = context.getString(R.string.steam_continue),
722801
dismissBtnText = context.getString(R.string.cancel),
723802
),
@@ -755,43 +834,21 @@ class SteamAppScreen : BaseAppScreen() {
755834
event = "cloud_sync_forced",
756835
properties = mapOf("game_name" to appInfo.name),
757836
)
758-
CoroutineScope(Dispatchers.IO).launch {
759-
val steamId = SteamService.userSteamId
760-
if (steamId == null) {
761-
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
762-
return@launch
763-
}
764-
765-
val containerManager = ContainerManager(context)
766-
val container = ContainerUtils.getOrCreateContainer(context, appId)
767-
containerManager.activateContainer(container)
768-
769-
val prefixToPath: (String) -> String = { prefix ->
770-
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
771-
}
772-
val syncResult = SteamService.forceSyncUserFiles(
773-
appId = gameId,
774-
prefixToPath = prefixToPath,
775-
).await()
776-
777-
when (syncResult.syncResult) {
778-
SyncResult.Success -> {
779-
SnackbarManager.show(context.getString(R.string.steam_cloud_sync_success))
780-
}
781-
782-
SyncResult.UpToDate -> {
783-
SnackbarManager.show(context.getString(R.string.steam_cloud_sync_up_to_date))
784-
}
785-
786-
else -> {
787-
SnackbarManager.show(
788-
context.getString(
789-
R.string.steam_cloud_sync_failed,
790-
syncResult.syncResult,
791-
),
792-
)
793-
}
794-
}
837+
val container = ContainerUtils.getOrCreateContainer(context, appId)
838+
if (container.isLocalSavesOnly) {
839+
showInstallDialog(
840+
gameId,
841+
MessageDialogState(
842+
visible = true,
843+
type = DialogType.CLOUD_SYNC_CONFLICT,
844+
title = context.getString(R.string.cloud_sync_local_saves_only_title),
845+
message = context.getString(R.string.cloud_sync_local_saves_only_message),
846+
confirmBtnText = context.getString(R.string.main_keep_remote),
847+
dismissBtnText = context.getString(R.string.main_keep_local),
848+
),
849+
)
850+
} else {
851+
performForceCloudSync(context, appId, gameId, container = container)
795852
}
796853
},
797854
),
@@ -1058,18 +1115,21 @@ class SteamAppScreen : BaseAppScreen() {
10581115

10591116
if (operation == AppOptionMenuType.VerifyFiles) {
10601117
MarkerUtils.clearInstalledPrerequisiteMarkers(getAppDirPath(gameId))
1061-
val steamId = SteamService.userSteamId
1062-
if (steamId != null) {
1063-
val prefixToPath: (String) -> String = { prefix ->
1064-
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
1118+
// skip cloud sync if local saves only — verify is about game files, not saves
1119+
if (!container.isLocalSavesOnly) {
1120+
val steamId = SteamService.userSteamId
1121+
if (steamId != null) {
1122+
val prefixToPath: (String) -> String = { prefix ->
1123+
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
1124+
}
1125+
SteamService.forceSyncUserFiles(
1126+
appId = gameId,
1127+
prefixToPath = prefixToPath,
1128+
overrideLocalChangeNumber = -1,
1129+
).await()
1130+
} else {
1131+
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
10651132
}
1066-
SteamService.forceSyncUserFiles(
1067-
appId = gameId,
1068-
prefixToPath = prefixToPath,
1069-
overrideLocalChangeNumber = -1,
1070-
).await()
1071-
} else {
1072-
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
10731133
}
10741134
}
10751135

@@ -1120,14 +1180,30 @@ class SteamAppScreen : BaseAppScreen() {
11201180
}
11211181
}
11221182

1183+
DialogType.CLOUD_SYNC_CONFLICT -> {
1184+
{
1185+
hideInstallDialog(gameId)
1186+
performForceCloudSync(context, libraryItem.appId, gameId, SaveLocation.Remote)
1187+
}
1188+
}
1189+
11231190
else -> null
11241191
}
1192+
val effectiveDismissClick: (() -> Unit)? = when (installDialogState.type) {
1193+
DialogType.CLOUD_SYNC_CONFLICT -> {
1194+
{
1195+
hideInstallDialog(gameId)
1196+
performForceCloudSync(context, libraryItem.appId, gameId, SaveLocation.Local)
1197+
}
1198+
}
1199+
else -> onDismissClick
1200+
}
11251201

11261202
MessageDialog(
11271203
visible = installDialogState.visible,
11281204
onDismissRequest = onDismissRequest,
11291205
onConfirmClick = onConfirmClick,
1130-
onDismissClick = onDismissClick,
1206+
onDismissClick = effectiveDismissClick,
11311207
confirmBtnText = installDialogState.confirmBtnText,
11321208
dismissBtnText = installDialogState.dismissBtnText,
11331209
title = installDialogState.title,

app/src/main/java/app/gamenative/utils/ContainerUtils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,11 @@ object ContainerUtils {
471471
container.setExternalDisplayMode(containerData.externalDisplayMode)
472472
container.setExternalDisplaySwap(containerData.externalDisplaySwap)
473473
container.setForceDlc(containerData.forceDlc)
474+
// clear stale cloud cache so next sync detects divergence and shows conflict dialog
475+
if (saveToDisk && container.isLocalSavesOnly && !containerData.localSavesOnly) {
476+
val gameId = extractGameIdFromContainerId(container.id)
477+
SteamService.clearCloudSyncCache(context, gameId)
478+
}
474479
container.setLocalSavesOnly(containerData.localSavesOnly)
475480
container.setSteamOfflineMode(containerData.steamOfflineMode)
476481
container.setUseLegacyDRM(containerData.useLegacyDRM)

app/src/main/res/values-da/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@
479479
<!-- Container Configuration: Steam Integration -->
480480
<string name="force_dlc">Gennemtving DLC</string>
481481
<string name="force_dlc_description">Kun aktiver hvis DLC\'er ikke opdages, eller gemfiler med DLC ikke virker</string>
482+
<string name="local_saves_only">Kun lokale gemte data</string>
483+
<string name="local_saves_only_description">Deaktiver cloud-synkronisering af gemte data for dette spil og behold kun gemte data på enheden</string>
482484
<string name="launch_steam_client_beta">Start Steam-klient (Beta)</string>
483485
<string name="steam_offline_mode">Steam Offline-tilstand</string>
484486
<string name="steam_offline_mode_description">Start Steam-spil i offline-tilstand</string>
@@ -1074,6 +1076,7 @@
10741076
<string name="steam_reset_container_message">Dette vil nulstille din container til standardkonfigurationen.</string>
10751077
<string name="steam_verify_files_title">Verificér filer</string>
10761078
<string name="steam_verify_files_message">Sørg venligst for, at dine gemfiler er uploadet til skyen eller sikkerhedskopieret før verificering, da de ellers kan blive overskrevet.</string>
1079+
<string name="steam_verify_files_message_local_saves">Cloud-synkronisering springes over under verificering, fordi kun lokale gemte data er aktiveret.</string>
10771080
<string name="steam_update_title">Opdatering</string>
10781081
<string name="steam_update_message">Sørg venligst for, at dine gemfiler er uploadet til skyen eller sikkerhedskopieret før opdatering, da de ellers kan blive overskrevet.</string>
10791082
<string name="steam_cloud_sync_success">Sky-synkronisering fuldført</string>
@@ -1231,4 +1234,6 @@
12311234
<string name="workshop_processing">Behandler mods…</string>
12321235
<string name="workshop_failed_count">%1$d workshop-mod(s) kunne ikke opdateres</string>
12331236
<string name="workshop_download_failed">Download mislykkedes</string>
1237+
<string name="cloud_sync_local_saves_only_title">Kun lokale gemte data</string>
1238+
<string name="cloud_sync_local_saves_only_message">"Kun lokale gemte data" er aktiveret for dette spil. Cloud-gemte data kan afvige fra lokale gemte data. Hvilke vil du beholde?</string>
12341239
</resources>

0 commit comments

Comments
 (0)