diff --git a/README.md b/README.md index b7ec226ff..b247ab194 100644 --- a/README.md +++ b/README.md @@ -174,9 +174,11 @@ under proot; Firefox's are turned off in the session's environment, without whic ### Game storage: internal, an SD card, or a folder -The client's own library is internal storage and always there. **Game storage** in the Steam cog -registers one more: an SD card (through the app's own folder on it, the one place a card lets -this app write) or any folder chosen in the File Manager. The session binds it at +The client's own library is internal storage and always there. An **SD card in the phone is +registered as a second library by itself** (through the app's own folder on it, the one place a +card lets this app write), so the choice is made where it is anywhere else - inside Steam, on +every Install. **Game storage** in the Steam cog is only for turning that off or pointing it at a +folder chosen in the File Manager instead. The session binds it at `/mnt/bannerlator-sd` and `bannerlator-steam-library` registers it with the client under the name the app gives it; the client then asks where to install every game, lists both on its Storage page and moves games between them itself. Set to internal only, the entry is removed again. A diff --git a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt index 59a2bb534..67212fa4b 100644 --- a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt @@ -386,7 +386,7 @@ class MainActivity : ComponentActivity() { /** A second Steam library, proven writable first; "" = internal only. */ private fun setGameStorage(path: String, label: String) { - if (path.isNotEmpty()) { + if (path.isNotEmpty() && path != SessionPrefs.GAME_STORAGE_OFF) { val problem = GameStorage.prepare(path) if (problem != null) { android.widget.Toast.makeText(this, "Not usable: $problem", android.widget.Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/com/steamdeck/launcher/core/DeviceReport.kt b/app/src/main/java/com/steamdeck/launcher/core/DeviceReport.kt index fd757ac61..658192542 100644 --- a/app/src/main/java/com/steamdeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/steamdeck/launcher/core/DeviceReport.kt @@ -147,7 +147,11 @@ object DeviceReport { k("On-screen controls", SessionPrefs.oscMode(context)) k("Touch mode", SessionPrefs.touchMode(context)) k("Performance HUD", SessionPrefs.hudEnabled(context)) - k("Game storage", SessionPrefs.gameStorage(context).ifEmpty { "internal only" }) + k("Game storage", when (val g = SessionPrefs.gameStorage(context)) { + "" -> "automatic (" + (com.steamdeck.launcher.session.GameStorage.effective(context)?.path ?: "no card present") + ")" + SessionPrefs.GAME_STORAGE_OFF -> "internal only" + else -> g + }) h("Android process limits") // Android 12 kills "phantom" processes - the children an app forks itself rather than diff --git a/app/src/main/java/com/steamdeck/launcher/session/GameStorage.kt b/app/src/main/java/com/steamdeck/launcher/session/GameStorage.kt index 488ef20ff..1bde1f841 100644 --- a/app/src/main/java/com/steamdeck/launcher/session/GameStorage.kt +++ b/app/src/main/java/com/steamdeck/launcher/session/GameStorage.kt @@ -53,6 +53,16 @@ object GameStorage { } } + /** The root the session binds, from the setting: automatic = the first card in the phone. */ + fun effective(context: Context): Option? { + val pref = SessionPrefs.gameStorage(context) + return when { + pref == SessionPrefs.GAME_STORAGE_OFF -> null + pref.isEmpty() -> options(context).firstOrNull()?.let { Option(it.label.substringBefore(" ·"), it.path) } + else -> Option(SessionPrefs.gameStorageLabel(context), pref) + } + } + /** The label the client shows for a chosen folder: its last name, or the volume's. */ fun labelFor(context: Context, path: String): String = options(context).firstOrNull { it.path == path }?.label?.substringBefore(" ·") diff --git a/app/src/main/java/com/steamdeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/steamdeck/launcher/session/SessionPrefs.kt index 98a904487..260211b8c 100644 --- a/app/src/main/java/com/steamdeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/steamdeck/launcher/session/SessionPrefs.kt @@ -229,10 +229,14 @@ object SessionPrefs { /** * A second Steam library on this device: the folder bound at /mnt/bannerlator-sd and * registered with the client, which then asks where to install every game and shows both - * on its Storage page. "" = internal only (the client's own library, the default). + * on its Storage page. "" = automatic: the SD card when one is in the phone (the default, + * so the choice is made inside the client like anywhere else); GAME_STORAGE_OFF = internal + * only; otherwise the folder chosen. */ fun gameStorage(context: Context): String = prefs(context).getString("gameStorage", "") ?: "" + const val GAME_STORAGE_OFF = "off" + fun gameStorageLabel(context: Context): String = prefs(context).getString("gameStorageLabel", "SD Card") ?: "SD Card" fun setGameStorage(context: Context, path: String, label: String) { diff --git a/app/src/main/java/com/steamdeck/launcher/session/SessionService.kt b/app/src/main/java/com/steamdeck/launcher/session/SessionService.kt index cd8d75c53..934acf390 100644 --- a/app/src/main/java/com/steamdeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/steamdeck/launcher/session/SessionService.kt @@ -324,17 +324,19 @@ class SessionService : Service() { // A second Steam library: the storage chosen in the Steam cog, at the path the runtime's // bannerlator-steam-library registers with the client. Nothing bound = the script removes // the entry, so the client never offers a place that is not there. - val library = SessionPrefs.gameStorage(this).takeIf { it.isNotEmpty() } + val library = GameStorage.effective(this) if (library != null) { - val problem = GameStorage.prepare(library) + val problem = GameStorage.prepare(library.path) if (problem == null) { File(LinuxRuntime.rootDir(this), "mnt/bannerlator-sd").mkdirs() - binds.add("$library:/mnt/bannerlator-sd") - guest.add("BL_LIBRARY_LABEL=" + SessionPrefs.gameStorageLabel(this).replace('"', ' ')) - Log.i(TAG, "game storage: $library -> /mnt/bannerlator-sd (\"${SessionPrefs.gameStorageLabel(this)}\")") + binds.add("${library.path}:/mnt/bannerlator-sd") + guest.add("BL_LIBRARY_LABEL=" + library.label.replace('"', ' ')) + Log.i(TAG, "game storage: ${library.path} -> /mnt/bannerlator-sd (\"${library.label}\")") } else { Log.w(TAG, "game storage: $problem; internal only this session") } + } else { + Log.i(TAG, "game storage: internal only") } val roms = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }?.let { File(it) } if (roms != null && roms.isDirectory && roms.canRead()) { diff --git a/app/src/main/java/com/steamdeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/steamdeck/launcher/ui/ModeSettingsDialog.kt index 75d98eefe..68deb97e3 100644 --- a/app/src/main/java/com/steamdeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/steamdeck/launcher/ui/ModeSettingsDialog.kt @@ -248,16 +248,21 @@ fun ModeSettingsDialog(s: ModeSettings, a: ModeSettingsActions) { "storage" -> if (s.gameStorage != null) { Section( "Game storage", - "Where Steam may install games. Internal is the client's own library and always there. A second " + - "place is registered with the client at the next session start: it then asks where to install " + - "every game, lists both on its Storage page, and can move a game between them itself.", + "The choice itself is made inside Steam: with a second place registered, every Install asks which " + + "drive, Settings › Storage lists both, and Steam moves games between them. Internal is the " + + "client's own library and always there. Applies at the next session start.", ) Spacer(Modifier.height(6.dp)) - Choice("Internal only — the default", s.gameStorage.isEmpty()) { a.onGameStorage("", "") } + Choice( + "Automatic — the SD card when one is in the phone" + + (if (s.storageOptions.isEmpty()) " (none right now)" else ""), + s.gameStorage.isEmpty(), + ) { a.onGameStorage("", "") } + Choice("Internal only", s.gameStorage == "off") { a.onGameStorage("off", "") } for ((label, path) in s.storageOptions) { Choice(label, s.gameStorage == path) { a.onGameStorage(path, label.substringBefore(" ·")) } } - val custom = s.gameStorage.isNotEmpty() && s.storageOptions.none { it.second == s.gameStorage } + val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.second == s.gameStorage } if (custom) Choice("Folder: ${s.gameStorage}", true) {} OutlinedButton(onClick = a.onPickGameStorageFolder, modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) { Text("Choose a folder…")