diff --git a/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt b/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt index 5857101ea..13bb3702c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt @@ -63,7 +63,7 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat if (compBusy != null) return compBusy = label Thread({ - val message = runCatching(work).getOrElse { e -> "$label failed: ${e.message ?: e.javaClass.simpleName}" } + val message = runCatching(work).getOrElse { e -> activity.getString(R.string.main_action_failed, label, e.message ?: e.javaClass.simpleName) } ui.post { compBusy = null android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show() @@ -79,7 +79,7 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat val cat = runCatching { ComponentsManager.catalog(activity, true) }.getOrNull() ui.post { compChecking = false - if (cat == null || cat.items.isEmpty()) android.widget.Toast.makeText(activity, "The Nightlies could not be reached", android.widget.Toast.LENGTH_LONG).show() + if (cat == null || cat.items.isEmpty()) android.widget.Toast.makeText(activity, activity.getString(R.string.comp_nightlies_unreachable), android.widget.Toast.LENGTH_LONG).show() else { compCatalog = cat.items; compCatalogAt = cat.fetchedAt } } }, "components-catalog").start() @@ -91,8 +91,8 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat Thread({ val message = runCatching { val pkg = ComponentsManager.download(activity, item) { pc -> ui.post { if (compDownloads.containsKey(item.file)) compDownloads = compDownloads + (item.file to pc) } } - "Stored ${pkg.version}" - }.getOrElse { e -> "Download failed: ${e.message ?: e.javaClass.simpleName}" } + activity.getString(R.string.comp_stored, pkg.version) + }.getOrElse { e -> activity.getString(R.string.comp_download_failed, e.message ?: e.javaClass.simpleName) } ui.post { compDownloads = compDownloads - item.file android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt b/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt index 3ac740ebc..34460cc90 100644 --- a/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt @@ -41,11 +41,11 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val fun installDecky(release: DeckyManager.Release) { if (deckyStage != null || SessionState.running) return - deckyStage = "Starting…"; deckyPercent = -1 + deckyStage = activity.getString(R.string.store_starting); deckyPercent = -1 Thread({ val problem = runCatching { DeckyManager.install(activity, release) { label, value -> ui.post { deckyStage = label; deckyPercent = value } } - }.getOrElse { error -> "Decky install failed: ${error.message ?: error.javaClass.simpleName}" } + }.getOrElse { error -> activity.getString(R.string.decky_install_failed, error.message ?: error.javaClass.simpleName) } ui.post { deckyStage = null; deckyPercent = -1; deckyInstalled = DeckyManager.installed(activity) if (problem != null) android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show() @@ -55,16 +55,16 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val fun importPluginZip(archive: File) { if (deckyStage != null || SessionState.running || deckyInstalled == null) return - deckyStage = "Starting plugin import…"; deckyPercent = -1 + deckyStage = activity.getString(R.string.decky_plugin_starting); deckyPercent = -1 Thread({ val problem = runCatching { DeckyManager.installPluginZip(activity, archive) { label, value -> ui.post { deckyStage = label; deckyPercent = value } } - }.getOrElse { error -> "Plugin import failed: ${error.message ?: error.javaClass.simpleName}" } + }.getOrElse { error -> activity.getString(R.string.decky_plugin_failed, error.message ?: error.javaClass.simpleName) } ui.post { deckyStage = null; deckyPercent = -1 - val message = problem ?: "Plugin installed. Restart Steam to load it." + val message = problem ?: activity.getString(R.string.decky_plugin_installed) android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show() } }, "import-decky-plugin").start() diff --git a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt index ff5ce18fe..3fe26735f 100644 --- a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt +++ b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt @@ -37,7 +37,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl /** The latest Banners-Turnip release as each driver menu offers it (see [refreshReleaseRows]). */ var linuxDownloads by mutableStateOf>(emptyList()) var androidDownloads by mutableStateOf>(emptyList()) - var releaseStatus by mutableStateOf("Not checked yet - tap refresh to look for new drivers") + // Empty until the first refresh: the activity has no context yet while its fields are made. + var releaseStatus by mutableStateOf("") var releaseChecking by mutableStateOf(false) var canRestoreBundled by mutableStateOf(false) /** Asset name -> download percent, while it downloads. */ @@ -60,7 +61,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl gpuName = gpu.name, gpuFamily = gpu.family.label, soc = gpu.soc, supportText = gpu.supportText, supported = gpu.support == GpuInfo.Support.TESTED, unsupported = gpu.support == GpuInfo.Support.UNSUPPORTED, auto = mode == SessionPrefs.GPU_DRIVERS_AUTO, pairs = pairRows, busy = pairBusy, percent = pairPercent, - autoStatus = autoStatus, releaseStatus = releaseStatus, checking = releaseChecking, + autoStatus = autoStatus, releaseStatus = releaseStatus.ifEmpty { activity.getString(R.string.drivers_not_checked) }, checking = releaseChecking, linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected, linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled, activeBundle = activeBundle, @@ -73,10 +74,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl fun origin(id: String) = if (TurnipReleases.isDownloaded(activity, id)) DriverRow.DOWNLOADED else DriverRow.IMPORTED val bundles = DriverBundle.all(activity) fun bundleRow(id: String, linux: Boolean) = bundles.firstOrNull { (if (linux) it.linuxId else it.androidId) == id }?.let { - DriverRow(id, it.label, "Android + Linux: sets the ${if (linux) "display" else "runtime"} driver with it", true, DriverRow.BUNDLE) + DriverRow(id, it.label, activity.getString(if (linux) R.string.drivers_bundle_sets_display else R.string.drivers_bundle_sets_runtime), true, DriverRow.BUNDLE) } linuxRows = LinuxVulkanDriver.optionValues(activity).map { id -> - if (id.isEmpty()) DriverRow("", "Runtime default", "the Turnip built into the runtime", false) + if (id.isEmpty()) DriverRow("", activity.getString(R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false) else bundleRow(id, linux = true) ?: DriverRow( id, lm.getDriverName(id), listOfNotNull( @@ -92,8 +93,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl val auto = td.autoId() androidRows = buildList { add(DriverRow( - TurnipDriver.AUTO, "Auto - picked by GPU", - if (auto == "system") "system Vulkan: no bundled build for this GPU" else "${td.displayName(auto)} (bundled)", + TurnipDriver.AUTO, activity.getString(R.string.gpu_auto_by_gpu), + if (auto == "system") activity.getString(R.string.drivers_system_vulkan) else activity.getString(R.string.drivers_bundled, td.displayName(auto)), false, )) for (id in td.visibleBundled()) add(DriverRow(id, td.displayName(id), td.driverVersion(id), true, DriverRow.BUNDLED)) @@ -109,7 +110,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl /** One line for the settings row that opens this: "Auto · WinNative · Balanced". */ fun summary(): String { val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle - return (if (mode == SessionPrefs.GPU_DRIVERS_AUTO) "Auto" else "Manual") + (active?.let { " · $it" } ?: "") + val label = activity.getString(if (mode == SessionPrefs.GPU_DRIVERS_AUTO) R.string.common_auto else R.string.drivers_manual) + return active?.let { activity.getString(R.string.drivers_summary, label, it) } ?: label } /** The pairs the last check found, with what is installed and in use; this GPU's first. */ @@ -125,7 +127,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl val mb = p.assets.sumOf { it.size } / 1_048_576.0 PairRow( key = p.key, name = p.name, version = p.version, - detail = if (!p.complete) "Only one half is published right now" else "%.0f MB for both".format(mb), + detail = if (!p.complete) activity.getString(R.string.drivers_half_published) else activity.getString(R.string.drivers_pair_size, mb), recommended = p.key == recommended, suits = p.suits(gpu), complete = p.complete, installed = displayId != null && linuxId != null, active = displayId != null && linuxId != null && @@ -153,11 +155,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl if (!force && autoCheckedThisProcess) return autoCheckedThisProcess = true if (DriverPairs.recommendedKey(gpu, emptyList()) == null) { - autoStatus = "No drivers to set: ${gpu.supportText.lowercase()}" + autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText.lowercase()) return } releaseChecking = true - autoStatus = "Checking for the latest drivers…" + autoStatus = activity.getString(R.string.drivers_checking_latest) Thread({ val cached = TurnipReleases.cached(activity) val stale = cached == null || System.currentTimeMillis() - cached.checkedAt > 24 * 3_600_000L || @@ -172,9 +174,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl val pair = pairs.firstOrNull { it.key == key } val row = pairRows.firstOrNull { it.key == key } when { - row?.active == true -> autoStatus = "Up to date" + (problem?.let { " (couldn't check: $it)" } ?: "") + row?.active == true -> autoStatus = problem?.let { activity.getString(R.string.drivers_up_to_date_check_failed, it) } + ?: activity.getString(R.string.upd_current_status) pair == null || !pair.complete -> - autoStatus = problem?.let { "Couldn't check: $it" } ?: "The recommended drivers aren't published right now" + autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished) else -> installPair(pair, auto = true) } } @@ -197,7 +200,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl if (!pair.complete) return pairBusy = pair.key pairPercent = 0 - if (auto) autoStatus = "Downloading ${pair.name} ${pair.version}…" + if (auto) autoStatus = activity.getString(R.string.drivers_downloading_pair, pair.name, pair.version) Thread({ val lm = LinuxVulkanDriverManager(activity) val td = TurnipDriver(activity) @@ -217,7 +220,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl val displayChanged = SessionPrefs.androidDriver(activity) != displayId SessionPrefs.setAndroidDriver(activity, displayId) SessionPrefs.setLinuxDriver(activity, linuxId) - val restart = if (displayChanged && CompositorHost.isStarted) " The display driver applies after DroidDeck restarts." else "" + val restart = displayChanged && CompositorHost.isStarted if (auto) { val previous = SessionPrefs.gpuAutoInstalled(activity) for (id in previous - setOf(displayId, linuxId)) { @@ -230,14 +233,18 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl TurnipReleases.forget(activity, id) } SessionPrefs.setGpuAutoInstalled(activity, setOf(displayId, linuxId)) - autoStatus = (if (downloaded) "Updated to" else "Switched to") + " ${pair.name} ${pair.version}.$restart" + autoStatus = activity.getString( + if (downloaded) { if (restart) R.string.drivers_updated_to_restart else R.string.drivers_updated_to } + else { if (restart) R.string.drivers_switched_to_restart else R.string.drivers_switched_to }, + pair.name, pair.version, + ) } else { - android.widget.Toast.makeText(activity, "Using ${pair.name} ${pair.version}.$restart", android.widget.Toast.LENGTH_LONG).show() + android.widget.Toast.makeText(activity, activity.getString(if (restart) R.string.drivers_using_restart else R.string.drivers_using, pair.name, pair.version), android.widget.Toast.LENGTH_LONG).show() } }.onFailure { e -> Log.w(TAG, "driver pair ${pair.key}", e) - val why = if (e is IllegalArgumentException) e.message else "Download failed: ${e.message}" - if (auto) autoStatus = why ?: "Download failed" + val why = if (e is IllegalArgumentException) e.message else activity.getString(R.string.comp_download_failed, e.message) + if (auto) autoStatus = why ?: activity.getString(R.string.user_apps_download_failed) else android.widget.Toast.makeText(activity, why, android.widget.Toast.LENGTH_LONG).show() } refreshDrivers() @@ -298,7 +305,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl try { LinuxVulkanDriverManager(activity).installDriver(uri, name) } catch (runtime: IllegalArgumentException) { - throw IllegalArgumentException("Not a driver zip: neither an AdrenoTools driver, a -Linux Turnip nor an Android + Linux bundle") + throw IllegalArgumentException(activity.getString(R.string.drivers_not_a_driver)) } } } @@ -307,10 +314,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl e.message } catch (e: Exception) { Log.w(TAG, "driver import", e) - "Import failed: ${e.message}" + activity.getString(R.string.drivers_import_failed, e.message) } ui.post { - val done = bundle?.let { useBundle(it) } ?: "Imported ${name ?: "driver"}" + val done = bundle?.let { useBundle(it) } + ?: name?.let { activity.getString(R.string.comp_imported, it) } ?: activity.getString(R.string.drivers_imported_unnamed) android.widget.Toast.makeText( activity, problem ?: done, if (problem != null || bundle != null) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT, @@ -340,8 +348,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl val displayChanged = SessionPrefs.androidDriver(activity) != bundle.androidId DriverBundle.select(activity, bundle) if (mode == SessionPrefs.GPU_DRIVERS_AUTO) setMode(false) - return "Using ${bundle.label} as both the runtime and display driver." + - if (displayChanged && CompositorHost.isStarted) " The display driver applies after DroidDeck restarts." else "" + return activity.getString( + if (displayChanged && CompositorHost.isStarted) R.string.drivers_using_bundle_restart else R.string.drivers_using_bundle, + bundle.label, + ) } /** @@ -354,7 +364,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl if (bundle != null) { DriverBundle.remove(activity, bundle) TurnipReleases.forget(activity, bundle.id) - android.widget.Toast.makeText(activity, "Deleted ${bundle.label} (both drivers)", android.widget.Toast.LENGTH_SHORT).show() + android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_deleted_bundle, bundle.label), android.widget.Toast.LENGTH_SHORT).show() refreshDrivers() return } @@ -367,7 +377,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl if (SessionPrefs.androidDriver(activity) == id) SessionPrefs.setAndroidDriver(activity, TurnipDriver.AUTO) } TurnipReleases.forget(activity, id) - android.widget.Toast.makeText(activity, "Deleted ${id}", android.widget.Toast.LENGTH_SHORT).show() + android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_deleted, id), android.widget.Toast.LENGTH_SHORT).show() refreshDrivers() } @@ -378,26 +388,28 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl .filter { it.bundle || it.linux == linux } .filter { a -> installedIdOf(a) == null } .map { a -> - val mb = "%.1f MB".format(a.size / 1_048_576.0) + val mb = activity.getString(R.string.common_size_mb, a.size / 1_048_576.0) com.droiddeck.launcher.ui.DownloadRow(a.name, "${a.source} ${a.tag}", "${a.label} · $mb", releaseProgress[a.name]) } linuxDownloads = rows(linux = true) androidDownloads = rows(linux = false) if (!releaseChecking) releaseStatus = when (check) { - null -> "Not checked yet - tap refresh to look for new drivers" - else -> "Latest: " + check.latest.joinToString(" · ") { "${it.first} ${it.second}" } + - (if (check.failed.isEmpty()) "" else " · ${check.failed.joinToString()} unreachable") + - " · checked ${ago(check.checkedAt)}" + null -> activity.getString(R.string.drivers_not_checked) + else -> { + val latest = check.latest.joinToString(" · ") { "${it.first} ${it.second}" } + if (check.failed.isEmpty()) activity.getString(R.string.drivers_release_status, latest, ago(check.checkedAt)) + else activity.getString(R.string.drivers_release_status_failed, latest, check.failed.joinToString(), ago(check.checkedAt)) + } } } private fun ago(t: Long): String { val m = ((System.currentTimeMillis() - t) / 60_000).coerceAtLeast(0) return when { - m < 1 -> "just now" - m < 60 -> "$m min ago" - m < 48 * 60 -> "${m / 60} h ago" - else -> "${m / (24 * 60)} days ago" + m < 1 -> activity.getString(R.string.upd_just_now) + m < 60 -> activity.resources.getQuantityString(R.plurals.drivers_ago_minutes, m.toInt(), m) + m < 48 * 60 -> activity.resources.getQuantityString(R.plurals.drivers_ago_hours, (m / 60).toInt(), m / 60) + else -> activity.resources.getQuantityString(R.plurals.drivers_ago_days, (m / (24 * 60)).toInt(), m / (24 * 60)) } } @@ -405,16 +417,16 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl fun checkLatestTurnip() { if (releaseChecking) return releaseChecking = true - releaseStatus = "Checking Banners-Turnip, WinNative and DroidDeck…" + releaseStatus = activity.getString(R.string.drivers_checking_sources) Thread({ val problem = try { TurnipReleases.refresh(activity); null } catch (e: Exception) { - Log.w(TAG, "latest Turnip check", e); e.message ?: "check failed" + Log.w(TAG, "latest Turnip check", e); e.message ?: activity.getString(R.string.drivers_check_failed) } ui.post { releaseChecking = false refreshReleaseRows() refreshPairs() - if (problem != null) releaseStatus = "Couldn't check: $problem" + if (problem != null) releaseStatus = activity.getString(R.string.upd_couldnt_check, problem) } }, "turnip-release-check").start() } @@ -433,13 +445,16 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl e.message } catch (e: Exception) { Log.w(TAG, "release driver download", e) - "Download failed: ${e.message}" + activity.getString(R.string.comp_download_failed, e.message) } ui.post { releaseProgress.remove(assetName) - val pick = if (asset.bundle) "pick it in either list to set both drivers" else "pick it in the menu" + val installed = activity.getString( + if (asset.bundle) R.string.drivers_installed_pick_either else R.string.drivers_installed_pick, + asset.name.removeSuffix(".zip"), + ) android.widget.Toast.makeText( - activity, problem ?: "Installed ${asset.name.removeSuffix(".zip")} - $pick", + activity, problem ?: installed, android.widget.Toast.LENGTH_LONG, ).show() refreshDrivers() diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index 2822388cd..6051bca9a 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -109,7 +109,7 @@ class MainActivity : ComponentActivity() { private var stage by mutableStateOf("") private var percent by mutableIntStateOf(-1) private var failed by mutableStateOf(false) - private var frameGenLabel by mutableStateOf("Off") + private var frameGenLabel by mutableStateOf("") private var lossless by mutableStateOf(Lossless.State.NONE) private var showRemove by mutableStateOf(false) private var showNonAdreno by mutableStateOf(null) @@ -256,29 +256,32 @@ class MainActivity : ComponentActivity() { /** Import a save zip into [game]: pick it in the app's file picker, then back up and unzip off the main thread. */ private fun importSaves(name: String, game: () -> GameSaves.Game) { if (SessionState.running) { - android.widget.Toast.makeText(this, "Close the Steam session first, so the game can't save over the import", android.widget.Toast.LENGTH_LONG).show() + android.widget.Toast.makeText(this, R.string.main_saves_close_session, android.widget.Toast.LENGTH_LONG).show() return } onSavePicked = { zip -> - saveAction("Importing into $name") { + saveAction(getString(R.string.main_saves_importing, name)) { val (written, backup) = GameSaves.import(game(), zip) val kind = GameSaves.layoutOf(zip)?.label ?: "zip" - "Imported $written files from the $kind into $name" + (backup?.let { ". Old saves backed up to Download/DroidDeck/Saves/backups" } ?: "") + resources.getQuantityString( + if (backup != null) R.plurals.main_saves_imported_backup else R.plurals.main_saves_imported, + written, written, kind, name, + ) } } - pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), "Choose a save zip for $name", GameSaves.savesDir().parentFile?.parentFile?.path)) + pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_saves_pick_zip, name), GameSaves.savesDir().parentFile?.parentFile?.path)) } /** Export [game]'s saves in [layout] to a folder picked in the app's file picker. */ private fun exportSaves(name: String, layout: GameSaves.Layout, game: () -> GameSaves.Game) { onSavePicked = { dir -> - saveAction("Exporting $name") { + saveAction(getString(R.string.main_saves_exporting, name)) { val (zip, count) = GameSaves.export(game(), layout, dir) - "Exported $count files as a ${layout.label}: ${zip.path.removePrefix("/storage/emulated/0/")}" + resources.getQuantityString(R.plurals.main_saves_exported, count, count, layout.label, zip.path.removePrefix("/storage/emulated/0/")) } } GameSaves.savesDir().mkdirs() - pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, "Choose where to save $name (${layout.label})", GameSaves.savesDir().path)) + pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_saves_pick_folder, name, layout.label), GameSaves.savesDir().path)) } private val pickAddedGamesDir = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r -> if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data)?.let { path -> @@ -361,15 +364,15 @@ class MainActivity : ComponentActivity() { if (intent?.component?.className == SessionActivity::class.java.name) { when { busy || LinuxRuntimeInstaller.isBusy() -> { - android.widget.Toast.makeText(this, "Wait for the runtime operation to finish", android.widget.Toast.LENGTH_SHORT).show() + android.widget.Toast.makeText(this, R.string.main_wait_runtime, android.widget.Toast.LENGTH_SHORT).show() return } protons.protonBusyId != null || ProtonExtras.installInProgress -> { - android.widget.Toast.makeText(this, "Wait for the compatibility tool install to finish", android.widget.Toast.LENGTH_SHORT).show() + android.widget.Toast.makeText(this, R.string.main_wait_proton_install, android.widget.Toast.LENGTH_SHORT).show() return } pkgStage != null -> { - android.widget.Toast.makeText(this, "Wait for the desktop app install to finish", android.widget.Toast.LENGTH_SHORT).show() + android.widget.Toast.makeText(this, R.string.main_wait_desktop_install, android.widget.Toast.LENGTH_SHORT).show() return } } @@ -440,7 +443,7 @@ class MainActivity : ComponentActivity() { private fun launchGame(game: Library.SteamGame): Boolean { if (protons.protonBusyId != null || ProtonExtras.installInProgress || pkgStage != null) { - android.widget.Toast.makeText(this, "Wait for the install to finish before launching a game", android.widget.Toast.LENGTH_SHORT).show() + android.widget.Toast.makeText(this, R.string.main_wait_install_game, android.widget.Toast.LENGTH_SHORT).show() return false } if (SessionState.running) { @@ -487,6 +490,7 @@ class MainActivity : ComponentActivity() { displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager refreshPhantomStatus() theme = SessionPrefs.theme(this) + frameGenLabel = getString(R.string.frame_gen_off) // Last start's game list, so the Steam wall is up on the first frame; refresh() replaces it. steamGames = if (shortcutPicker) emptyList() else com.droiddeck.launcher.frontend.LibraryCache.load(this) backActionsInverted = SessionPrefs.backActionsInverted(this) @@ -632,7 +636,7 @@ class MainActivity : ComponentActivity() { Thread({ val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull() ui.post { - if (zip == null) android.widget.Toast.makeText(this, "No session logs yet: run a session first.", android.widget.Toast.LENGTH_LONG).show() + if (zip == null) android.widget.Toast.makeText(this, R.string.main_no_logs_yet, android.widget.Toast.LENGTH_LONG).show() else startActivity(SessionLogShare.shareIntent(this, zip)) } }, "share-logs").start() @@ -642,7 +646,7 @@ class MainActivity : ComponentActivity() { val cleared = runCatching { SessionArtifacts.clearAll(this) }.getOrDefault(0) ui.post { android.widget.Toast.makeText(this, - if (cleared == 0) "No session logs to clear." else "Cleared $cleared session log folder${if (cleared == 1) "" else "s"}.", + if (cleared == 0) getString(R.string.main_no_logs_to_clear) else resources.getQuantityString(R.plurals.main_logs_cleared, cleared, cleared), android.widget.Toast.LENGTH_SHORT).show() } }, "clear-logs").start() @@ -688,7 +692,7 @@ class MainActivity : ComponentActivity() { val result = runCatching { kotlinx.coroutines.runBlocking { WirelessAdbFix.pair(this@MainActivity, host, port, code) } } - val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging pairing failed" } + val error = result.exceptionOrNull()?.let { it.localizedMessage ?: getString(R.string.main_wadb_pair_failed) } ui.post { complete(error) } }, "wireless-adb-pair").start() }, @@ -701,7 +705,7 @@ class MainActivity : ComponentActivity() { onWirelessAdbApply = { host, port, enabled, complete -> Thread({ val result = runCatching { WirelessAdbFix.setChildProcessLimit(this@MainActivity, host, port, enabled) } - val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging command failed" } + val error = result.exceptionOrNull()?.let { it.localizedMessage ?: getString(R.string.main_wadb_command_failed) } ui.post { if (error == null) refreshPhantomStatus() complete(error) @@ -711,7 +715,7 @@ class MainActivity : ComponentActivity() { onSetPhantomProcessLimit = { enabled, complete -> Thread({ val result = runCatching { WirelessAdbFix.setUsingSavedPairing(this@MainActivity, enabled) } - val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging is unavailable" } + val error = result.exceptionOrNull()?.let { it.localizedMessage ?: getString(R.string.main_wadb_unavailable) } ui.post { if (error == null) refreshPhantomStatus() complete(error) @@ -722,7 +726,7 @@ class MainActivity : ComponentActivity() { (getSystemService(CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( ClipData.newPlainText("DroidDeck child-process setting", PhantomProcessLimit.adbCommand(enabled)), ) - android.widget.Toast.makeText(this, "ADB command copied", android.widget.Toast.LENGTH_SHORT).show() + android.widget.Toast.makeText(this, R.string.main_adb_copied, android.widget.Toast.LENGTH_SHORT).show() }, onDismissPhantomGate = { showPhantomGate = false }, onStartWirelessAdbPairing = { WirelessAdbPairingService.start(this) }, @@ -756,24 +760,24 @@ class MainActivity : ComponentActivity() { path = romsDir, onChoose = { showRoms = false - pickRomsDir.launch(InAppFilePicker.buildDirIntent(this, "Choose the ROMs folder", romsDir)) + pickRomsDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_pick_roms), romsDir)) }, onClear = { SessionPrefs.setRomsDir(this, ""); romsDir = null; showRoms = false }, onDismiss = { showRoms = false }, ) showNonAdreno?.let { release -> ConfirmDialog( - title = "Not an Adreno GPU", - text = "Turnip supports Adreno GPUs. On ${com.droiddeck.launcher.core.DeviceSupport.gpuName()}, Steam may show a black screen. Download: ${"%.0f".format(release.size / 1e6)} MB.", - confirm = "Install anyway", + title = androidx.compose.ui.res.stringResource(R.string.main_non_adreno_title), + text = androidx.compose.ui.res.stringResource(R.string.main_non_adreno_text, com.droiddeck.launcher.core.DeviceSupport.gpuName(), release.size / 1e6), + confirm = androidx.compose.ui.res.stringResource(R.string.main_install_anyway), onConfirm = { showNonAdreno = null; install(release) }, onDismiss = { showNonAdreno = null }, ) } if (showRemove) ConfirmDialog( - title = "Remove Linux runtime", - text = "This deletes the runtime, the Steam client inside it, and every game installed there.", - confirm = "Remove", + title = androidx.compose.ui.res.stringResource(R.string.main_remove_runtime_title), + text = androidx.compose.ui.res.stringResource(R.string.main_remove_runtime_text), + confirm = androidx.compose.ui.res.stringResource(R.string.store_remove), onConfirm = { showRemove = false; removeRuntime() }, onDismiss = { showRemove = false }, ) @@ -883,7 +887,7 @@ class MainActivity : ComponentActivity() { if (!SessionState.running) Thread({ val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList()) if (applied.isNotEmpty()) ui.post { - android.widget.Toast.makeText(this, "Applied: " + applied.joinToString(", "), android.widget.Toast.LENGTH_LONG).show() + android.widget.Toast.makeText(this, getString(R.string.main_applied, applied.joinToString(", ")), android.widget.Toast.LENGTH_LONG).show() } ui.post { if (showComponents) components.refreshComponents() } }, "components-queue").start() @@ -942,9 +946,13 @@ class MainActivity : ComponentActivity() { try { HomeApp.launch(this, app, displayId) } catch (_: Exception) { - val target = if (displayId == null || displayId == Display.DEFAULT_DISPLAY) "the primary screen" - else secondScreenDisplays.firstOrNull { it.id == displayId }?.label ?: "display $displayId" - android.widget.Toast.makeText(this, "Could not open ${app.label} on $target", android.widget.Toast.LENGTH_SHORT).show() + val label = secondScreenDisplays.firstOrNull { it.id == displayId }?.label + val message = when { + displayId == null || displayId == Display.DEFAULT_DISPLAY -> getString(R.string.android_app_open_failed_primary, app.label) + label != null -> getString(R.string.android_app_open_failed_on, app.label, label) + else -> getString(R.string.android_app_open_failed_display, app.label, displayId) + } + android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).show() } } @@ -975,7 +983,7 @@ class MainActivity : ComponentActivity() { if (busy || LinuxRuntimeInstaller.isBusy() || saveBusy != null) return saveBusy = label Thread({ - val message = runCatching(work).getOrElse { e -> "$label failed: ${e.message ?: e.javaClass.simpleName}" } + val message = runCatching(work).getOrElse { e -> getString(R.string.main_action_failed, label, e.message ?: e.javaClass.simpleName) } ui.post { saveBusy = null android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show() @@ -1021,10 +1029,10 @@ class MainActivity : ComponentActivity() { private fun importComponent(uri: Uri) { val name = displayNameOf(uri) ?: "imported.wcp" - components.componentAction("Importing") { + components.componentAction(getString(R.string.comp_busy_importing)) { val tmp = File(cacheDir, "component-import.wcp") - contentResolver.openInputStream(uri)?.use { input -> tmp.outputStream().use { input.copyTo(it) } } ?: error("cannot read the file") - try { "Imported ${ComponentsManager.importPackage(this, tmp, name).version}" } finally { tmp.delete() } + contentResolver.openInputStream(uri)?.use { input -> tmp.outputStream().use { input.copyTo(it) } } ?: error(getString(R.string.comp_cannot_read)) + try { getString(R.string.comp_imported, ComponentsManager.importPackage(this, tmp, name).version) } finally { tmp.delete() } } } @@ -1042,14 +1050,14 @@ class MainActivity : ComponentActivity() { requestInitialFocus = focusComponentsContent, onProton = { components.chooseProton(it) }, onComp = { components.compComp = it }, - onSwap = { file -> components.compProton?.let { pid -> components.componentAction("Swapping") { ComponentsManager.swap(this, pid, file) } } }, - onRestore = { version -> components.compProton?.let { pid -> components.componentAction("Restoring") { ComponentsManager.restore(this, pid, components.compComp, version) } } }, - onCancelQueued = { components.compProton?.let { pid -> components.componentAction("Cancelling") { ComponentsManager.cancelQueued(this, pid, components.compComp); "The waiting swap was cancelled." } } }, - onDeletePackage = { file -> components.componentAction("Deleting") { ComponentsManager.deletePackage(this, file) } }, - onDeleteOriginal = { version -> components.compProton?.let { pid -> components.componentAction("Deleting") { ComponentsManager.deleteOriginal(this, pid, components.compComp, version) } } }, + onSwap = { file -> components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_swapping)) { ComponentsManager.swap(this, pid, file) } } }, + onRestore = { version -> components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_restoring)) { ComponentsManager.restore(this, pid, components.compComp, version) } } }, + onCancelQueued = { components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_cancelling)) { ComponentsManager.cancelQueued(this, pid, components.compComp); getString(R.string.comp_swap_cancelled) } } }, + onDeletePackage = { file -> components.componentAction(getString(R.string.comp_busy_deleting)) { ComponentsManager.deletePackage(this, file) } }, + onDeleteOriginal = { version -> components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_deleting)) { ComponentsManager.deleteOriginal(this, pid, components.compComp, version) } } }, onDownload = { components.downloadComponent(it) }, onRefresh = { components.refreshComponentCatalog() }, - onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, "Choose a component package (-linux .wcp)")) }, + onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, getString(R.string.comp_pick_wcp))) }, onBack = { showComponents = false }, gpu = drivers.state(), gpuActions = com.droiddeck.launcher.ui.GpuDriversActions( @@ -1062,9 +1070,9 @@ class MainActivity : ComponentActivity() { onRemoveLinux = { id -> drivers.deleteDriver(id, linux = true) }, onRemoveAndroid = { id -> drivers.deleteDriver(id, linux = false) }, onDownloadDriver = { name -> drivers.downloadReleaseDriver(name) }, - onImportLinux = { pickLinuxDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a Linux runtime driver (-Linux zip or Android + Linux bundle)")) }, - onImportAndroid = { pickAndroidDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a display driver (AdrenoTools zip or Android + Linux bundle)")) }, - onImportZip = { pickAnyDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a driver zip or an Android + Linux bundle")) }, + onImportLinux = { pickLinuxDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_linux))) }, + onImportAndroid = { pickAndroidDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_android))) }, + onImportZip = { pickAnyDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_any))) }, onRestoreBundled = { TurnipDriver(this).restoreBundled(); drivers.refreshDrivers() }, ), ) @@ -1100,14 +1108,14 @@ class MainActivity : ComponentActivity() { private fun installPackage(id: String) { val entry = catalog?.firstOrNull { it.id == id } ?: return if (busy || LinuxRuntimeInstaller.isBusy() || pkgStage != null || SessionState.running) return - pkgId = id; pkgStage = "Starting…"; pkgPercent = -1 + pkgId = id; pkgStage = getString(R.string.store_starting); pkgPercent = -1 Thread({ val problem = DesktopCatalog.install(this, entry) { stage, percent -> ui.post { pkgStage = stage; pkgPercent = percent } } ui.post { pkgStage = null; pkgId = null - if (problem != null) android.widget.Toast.makeText(this, "${entry.name}: $problem", android.widget.Toast.LENGTH_LONG).show() + if (problem != null) android.widget.Toast.makeText(this, getString(R.string.main_pkg_problem, entry.name, problem), android.widget.Toast.LENGTH_LONG).show() refreshPackages() refresh() } @@ -1117,7 +1125,7 @@ class MainActivity : ComponentActivity() { private fun removePackage(id: String) { val entry = catalog?.firstOrNull { it.id == id } ?: return if (busy || LinuxRuntimeInstaller.isBusy() || pkgStage != null || SessionState.running) return - pkgId = id; pkgStage = if (entry.kind == "appimage") "Removing ${entry.name}…" else "Forgetting ${entry.name}…"; pkgPercent = -1 + pkgId = id; pkgStage = getString(if (entry.kind == "appimage") R.string.main_removing_named else R.string.main_forgetting_named, entry.name); pkgPercent = -1 Thread({ DesktopCatalog.remove(this, entry) ui.post { @@ -1220,7 +1228,7 @@ class MainActivity : ComponentActivity() { onRenderer = { r -> SessionPrefs.setDesktopRenderer(this, r); renderer = r }, onGameStorage = { path, label -> setGameStorage(path, label) }, onPickGameStorageFolder = { - pickGameStorage.launch(InAppFilePicker.buildDirIntent(this, "Choose the game storage folder", gameStorage.ifEmpty { null })) + pickGameStorage.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_pick_game_storage), gameStorage.ifEmpty { null })) }, onStorageDiagnostics = { on -> SessionPrefs.setStorageDiagnosticsEnabled(this, on) @@ -1246,13 +1254,13 @@ class MainActivity : ComponentActivity() { SessionPrefs.setRunSteamAtStartup(this, on) runSteamAtStartup = on }, - onPickAddedGamesDir = { pickAddedGamesDir.launch(InAppFilePicker.buildDirIntent(this, "Choose a folder of your own games", addedGamesDirs.lastOrNull())) }, + onPickAddedGamesDir = { pickAddedGamesDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_pick_added_games), addedGamesDirs.lastOrNull())) }, onAddedGamesArt = { on -> SessionPrefs.setAddedGamesArt(this, on); addedGamesArt = on; if (on) refreshAddedGames() }, onForgetAddedGamesDir = { dir -> SessionPrefs.setAddedGamesDirs(this, addedGamesDirs - dir); addedGamesDirs = SessionPrefs.addedGamesDirs(this); refreshAddedGames(); refresh() }, onAddedGameExe = { folder, path -> SessionPrefs.setAddedGameExe(this, folder, path); refreshAddedGames(); refresh() }, onPickAddedGameExe = { folder -> pendingAddedGame = folder - pickAddedGameExe.launch(InAppFilePicker.buildIntent(this, listOf("exe"), "Choose the game's .exe", folder)) + pickAddedGameExe.launch(InAppFilePicker.buildIntent(this, listOf("exe"), getString(R.string.main_pick_game_exe), folder)) }, onDeckyInstall = { release -> decky.installDecky(release) }, onDeckyCheck = { decky.refreshDecky() }, @@ -1263,7 +1271,7 @@ class MainActivity : ComponentActivity() { decky.deckySupervisor = false }, onPickDeckyPluginZip = { - pickDeckyPluginZip.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a Decky plugin ZIP")) + pickDeckyPluginZip.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.main_pick_decky_zip))) }, onDismiss = { settingsMode = null }, ), @@ -1405,7 +1413,7 @@ class MainActivity : ComponentActivity() { 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() + android.widget.Toast.makeText(this, getString(R.string.main_storage_not_usable, problem), android.widget.Toast.LENGTH_LONG).show() return } } @@ -1447,9 +1455,9 @@ class MainActivity : ComponentActivity() { romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() } logsEnabled = SessionPrefs.logsEnabled(this) runningLabel = if (SessionState.running) when (SessionState.mode) { - SessionService.MODE_DESKTOP -> "Desktop" + SessionService.MODE_DESKTOP -> getString(R.string.rail_desktop) SessionService.MODE_RUN -> Library.nameForProgram(SessionState.program) - ?: SessionState.program?.substringAfterLast('/')?.substringBefore('.') ?: "Program" + ?: SessionState.program?.substringAfterLast('/')?.substringBefore('.') ?: getString(R.string.main_running_program) else -> "Steam" } else null // The libraries, off the main thread: manifests and a folder scan. @@ -1532,10 +1540,10 @@ class MainActivity : ComponentActivity() { } catch (error: Exception) { if (displayId != null) { runCatching { startActivity(intent) } - android.widget.Toast.makeText(this, "Could not open Settings on the bottom screen; opened it on the main screen.", android.widget.Toast.LENGTH_LONG).show() + android.widget.Toast.makeText(this, R.string.main_settings_main_screen, android.widget.Toast.LENGTH_LONG).show() } else { startActivity(Intent(android.provider.Settings.ACTION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - android.widget.Toast.makeText(this, "Open Developer options in Settings", android.widget.Toast.LENGTH_LONG).show() + android.widget.Toast.makeText(this, R.string.main_open_dev_options, android.widget.Toast.LENGTH_LONG).show() } } } @@ -1568,7 +1576,7 @@ class MainActivity : ComponentActivity() { private fun followRuntimeOperation(run: (LinuxRuntimeInstaller.ProgressListener) -> Boolean?) { busy = true failed = false - stage = if (LinuxRuntimeInstaller.isRemoving()) "Removing Linux runtime" else "Starting…" + stage = getString(if (LinuxRuntimeInstaller.isRemoving()) R.string.main_removing_runtime else R.string.store_starting) percent = -1 Thread({ val ok = run(LinuxRuntimeInstaller.ProgressListener { s, p -> diff --git a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt index ee055bb32..8bf520818 100644 --- a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt @@ -24,7 +24,7 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va if (protonBusyId != null || SessionState.running) return ProtonExtras.unqueue(activity, tool) protonBusyId = id - protonStage = "Starting…" + protonStage = activity.getString(R.string.store_starting) protonPercent = -1 Thread({ val problem = ProtonExtras.install(activity, tool) { label, value -> @@ -44,7 +44,7 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return if (protonBusyId != null || SessionState.running) return protonBusyId = id - protonStage = "Removing ${tool.name}…" + protonStage = activity.getString(R.string.main_removing_named, tool.name) protonPercent = -1 Thread({ ProtonExtras.remove(activity, tool) diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 3811eddfb..b494930f0 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -287,7 +287,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.onCreate(savedInstanceState) if (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isRemoving()) { runtimeRemovalBlocked = true - android.widget.Toast.makeText(this, "Wait for runtime removal to finish", android.widget.Toast.LENGTH_SHORT).show() + // Best effort: the activity finishes either way, even where its resources cannot be read. + runCatching { android.widget.Toast.makeText(this, R.string.session_wait_removal, android.widget.Toast.LENGTH_SHORT).show() } finish() return } @@ -471,7 +472,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionDrawer(drawerOpen, drawerPage, drawerControllerActive, onPageChange = { drawerPage = it }, a = DrawerActions( steam = SessionState.mode == SessionService.MODE_STEAM, title = if (SessionState.mode == SessionService.MODE_RUN) - com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "Game" else null, + com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_title_game) else null, isHomeApp = isHomeApp, androidApps = androidApps, hudOn = hudOn, @@ -481,7 +482,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { frameGen = frameGen, lossless = lossless, oscMode = oscMode, suspendPolicy = suspendPolicy, touchMode = touchMode, - touchAuto = if (usingTouchpad()) "touchpad" else "direct", + touchAuto = getString(if (usingTouchpad()) R.string.session_touch_auto_touchpad else R.string.session_touch_auto_direct), shapeMode = shapeMode, fexPreset = fexPreset, secondScreenMode = secondScreenMode, secondScreenDisplays = secondScreenDisplays, @@ -543,9 +544,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { try { HomeApp.launch(this@SessionActivity, app, displayId) } catch (_: Exception) { - val target = if (displayId == null || displayId == Display.DEFAULT_DISPLAY) "the primary screen" - else secondScreenDisplays.firstOrNull { it.id == displayId }?.label ?: "display $displayId" - Toast.makeText(this@SessionActivity, "Could not open ${app.label} on $target", Toast.LENGTH_SHORT).show() + val label = secondScreenDisplays.firstOrNull { it.id == displayId }?.label + val message = when { + displayId == null || displayId == Display.DEFAULT_DISPLAY -> getString(R.string.android_app_open_failed_primary, app.label) + label != null -> getString(R.string.android_app_open_failed_on, app.label, label) + else -> getString(R.string.android_app_open_failed_display, app.label, displayId) + } + Toast.makeText(this@SessionActivity, message, Toast.LENGTH_SHORT).show() } }, pipSupported = com.droiddeck.launcher.session.SessionPipController.supported(this@SessionActivity), @@ -663,7 +668,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val snap = runCatching { ComponentsManager.snapshot(this) }.getOrNull() uiHandler.post { drawerComponents = snap - if (applied.isNotEmpty()) android.widget.Toast.makeText(this, "Applied: " + applied.joinToString(", "), android.widget.Toast.LENGTH_LONG).show() + if (applied.isNotEmpty()) android.widget.Toast.makeText(this, getString(R.string.main_applied, applied.joinToString(", ")), android.widget.Toast.LENGTH_LONG).show() } }, "drawer-components").start() } @@ -678,10 +683,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val current = active ?: "orig:$build" if (queued != null && value == current) { // Picking what is already in place again cancels the swap waiting for the game. - ComponentsManager.cancelQueued(this, protonId, comp); "The waiting swap was cancelled." + ComponentsManager.cancelQueued(this, protonId, comp); getString(R.string.comp_swap_cancelled) } else if (value.startsWith("orig:")) ComponentsManager.restore(this, protonId, comp, value.removePrefix("orig:")) else ComponentsManager.swap(this, protonId, value) - }.getOrElse { e -> "Swap failed: ${e.message ?: e.javaClass.simpleName}" } + }.getOrElse { e -> getString(R.string.session_swap_failed, e.message ?: e.javaClass.simpleName) } uiHandler.post { android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show() refreshDrawerComponents() @@ -695,20 +700,22 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { else intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM private fun pausedTitle(): String = when (SessionState.mode) { - SessionService.MODE_DESKTOP -> "The desktop is paused" - SessionService.MODE_RUN -> (com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "The program") + " is paused" - else -> "Steam is paused" + SessionService.MODE_DESKTOP -> getString(R.string.session_desktop_paused) + SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) + ?.let { getString(R.string.session_named_paused, it) } ?: getString(R.string.session_program_paused) + else -> getString(R.string.load_steam_paused) } private fun loadingTitle(): String = when (loadingMode()) { SessionService.MODE_DESKTOP -> - if (!SessionState.running && intent.getStringExtra(SessionService.EXTRA_STEAM_UI) != null) "Starting Steam on the desktop" - else "Starting the desktop" + if (!SessionState.running && intent.getStringExtra(SessionService.EXTRA_STEAM_UI) != null) getString(R.string.session_starting_steam_desktop) + else getString(R.string.session_starting_desktop) SessionService.MODE_RUN -> { val program = if (SessionState.running) SessionState.program else intent.getStringExtra(SessionService.EXTRA_PROGRAM) - "Starting " + (com.droiddeck.launcher.frontend.Library.nameForProgram(program) ?: "the program") + com.droiddeck.launcher.frontend.Library.nameForProgram(program)?.let { getString(R.string.session_starting_named, it) } + ?: getString(R.string.session_starting_program) } - else -> "Starting Steam" + else -> getString(R.string.load_starting_steam) } private fun shareCurrentSessionLogs() { @@ -716,7 +723,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { // ended screen's Share logs is for that one. val folder = SessionPaths.current() ?: SessionPaths.lastEnded() if (folder == null || !folder.isDirectory) { - Toast.makeText(this, "No logs for this session.", Toast.LENGTH_LONG).show() + Toast.makeText(this, R.string.session_no_logs, Toast.LENGTH_LONG).show() return } Thread({ @@ -725,12 +732,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { .getOrNull() uiHandler.post { if (zip == null) { - Toast.makeText(this, "Could not create the session log archive.", Toast.LENGTH_LONG).show() + Toast.makeText(this, R.string.session_logs_archive_failed, Toast.LENGTH_LONG).show() } else { runCatching { startActivity(SessionLogShare.shareIntent(this, zip)) } .onFailure { Log.w(TAG, "could not share current session logs", it) - Toast.makeText(this, "Could not share the session logs.", Toast.LENGTH_LONG).show() + Toast.makeText(this, R.string.session_logs_share_failed, Toast.LENGTH_LONG).show() } } } @@ -790,8 +797,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { "${SessionState.installing}.installing", mapOf("component" to SessionState.installing), ) - loading.step = if (runtime) "downloading the Linux runtime" else if (desktop) "downloading the desktop" - else "downloading Proton Experimental (ARM64)" + loading.step = getString((if (runtime) RUNTIME_LINES else if (desktop) DESKTOP_LINES else PROTON_LINES).downloading) loading.percent = -1 Thread({ var failedComponent: String? = null @@ -832,7 +838,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return@post } loading.percent = -1 - loading.step = "Starting the session…" + loading.step = getString(R.string.session_starting_session) // The surface may have come and gone while the download ran; start on the live one. if (surfaceView.holder.surface?.isValid == true) surfaceCreated(surfaceView.holder) } @@ -848,14 +854,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "session-failure-artifacts").start() } + /** The loading screen's lines for one package: downloading it (with and without its size), checking it, unpacking it. */ + private class PackageLines(val downloading: Int, val downloadingMb: Int, val checking: Int, val unpacking: Int) + /** Reports one package's download on the loading screen: " · 332 of 791 MB", checking, unpacking. */ - private fun progressFor(what: String, mb: Long) = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener { stage, p -> + private fun progressFor(what: PackageLines, mb: Long) = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener { stage, p -> uiHandler.post { loading.percent = p loading.step = when { - stage.startsWith("Downloading") -> if (p >= 0 && mb > 0) "downloading $what · ${p * mb / 100} of $mb MB" else "downloading $what" - stage.startsWith("Verifying") -> "checking $what" - else -> "unpacking $what" + stage.startsWith("Downloading") -> if (p >= 0 && mb > 0) getString(what.downloadingMb, p * mb / 100, mb) else getString(what.downloading) + stage.startsWith("Verifying") -> getString(what.checking) + else -> getString(what.unpacking) } } } @@ -863,29 +872,29 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { /** Null when the runtime is in, else the loading screen's closing line. */ private fun installRuntime(): String? { val release = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.fetchRelease() - ?: return "Could not reach the runtime catalog. Check the connection and press Play again." + ?: return getString(R.string.session_runtime_catalog_unreachable) val ok = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.install(this, release, - progressFor("the Linux runtime", release.size / 1_000_000)) - return if (ok) null else "The Linux runtime did not install. Check the runtime in Setup and press Play again." + progressFor(RUNTIME_LINES, release.size / 1_000_000)) + return if (ok) null else getString(R.string.session_runtime_install_failed) } /** Null when the desktop package is in, else the loading screen's closing line. */ private fun installDesktop(): String? { - uiHandler.post { loading.percent = -1; loading.step = "downloading the desktop" } + uiHandler.post { loading.percent = -1; loading.step = getString(R.string.session_desktop_downloading) } val entry = com.droiddeck.launcher.runtime.DesktopCatalog.fetch()?.firstOrNull { it.id == "desktop" } - ?: return "Could not reach the desktop catalog. Check the connection and press Desktop again." + ?: return getString(R.string.session_desktop_catalog_unreachable) val problem = com.droiddeck.launcher.runtime.DesktopCatalog.install(this, entry, - progressFor("the desktop", entry.size / 1_000_000)) - return problem?.let { "The desktop did not install ($it). Check the connection and press Desktop again." } + progressFor(DESKTOP_LINES, entry.size / 1_000_000)) + return problem?.let { getString(R.string.session_desktop_install_failed, it) } } /** Null when the ARM64 Proton is in, else why not; the session goes on either way. */ private fun installProton(): String? { - uiHandler.post { loading.percent = -1; loading.step = "downloading Proton Experimental (ARM64)" } + uiHandler.post { loading.percent = -1; loading.step = getString(R.string.session_proton_downloading) } val catalog = com.droiddeck.launcher.runtime.DesktopCatalog val entry = catalog.fetch(catalog.STEAM_SEED_URL)?.firstOrNull { it.id == catalog.PROTON_SEED_ID } ?: return "catalog unreachable" - return catalog.install(this, entry, progressFor("Proton Experimental (ARM64)", entry.size / 1_000_000)) + return catalog.install(this, entry, progressFor(PROTON_LINES, entry.size / 1_000_000)) } override fun surfaceCreated(holder: SurfaceHolder) { @@ -1168,14 +1177,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return } // It stays until the user leaves: a failure read against a timer is a failure not read. - val what = when (SessionState.mode) { - SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "The program" - SessionService.MODE_DESKTOP -> "The desktop" - else -> "Steam" + val stopped = when (SessionState.mode) { + SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) + ?.let { getString(R.string.session_ended_named, it) } ?: getString(R.string.session_ended_program) + SessionService.MODE_DESKTOP -> getString(R.string.session_ended_desktop) + else -> getString(R.string.session_ended_named, "Steam") } loading.showEnded( - hint?.text ?: "$what stopped unexpectedly. Share the logs with a bug report, or try again.", - "Exit status $status · ${SessionState.logFile?.path ?: "no log"}", + hint?.text ?: stopped, + getString(R.string.session_ended_detail, status, SessionState.logFile?.path ?: getString(R.string.session_ended_no_log)), ) focusEndedScreen() } @@ -1225,8 +1235,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val what = com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_end_program) EndHint(getString(R.string.session_end_fex_missing, what), evenOnSuccess = true) } else if (enosys >= 8) { - EndHint("The log shows $enosys \"Function not implemented\" errors: proot's seccomp acceleration is failing a helper on this device. " + - "Try Performance \u2192 \"Run proot without seccomp\" (or \"Skip Steam's xalia helper\") and start again.") + EndHint(resources.getQuantityString(R.plurals.session_hint_enosys, enosys, enosys, + getString(R.string.perf_title), getString(R.string.perf_seccomp), getString(R.string.perf_xalia))) } else null } catch (e: Exception) { null @@ -2026,5 +2036,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { /** Compositor scale modes (Container.FULLSCREEN_* values): 1 = fit with bars, centred. */ private const val SCALE_FIT = 1 private const val ALIGN_CENTER = 0 + private val RUNTIME_LINES = PackageLines( + R.string.session_runtime_downloading, R.string.session_runtime_downloading_mb, + R.string.session_runtime_checking, R.string.session_runtime_unpacking, + ) + private val DESKTOP_LINES = PackageLines( + R.string.session_desktop_downloading, R.string.session_desktop_downloading_mb, + R.string.session_desktop_checking, R.string.session_desktop_unpacking, + ) + private val PROTON_LINES = PackageLines( + R.string.session_proton_downloading, R.string.session_proton_downloading_mb, + R.string.session_proton_checking, R.string.session_proton_unpacking, + ) } } diff --git a/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt b/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt index 6a3e05d01..f0b975fc9 100644 --- a/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt @@ -46,7 +46,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl onAllowInstalls = { askPermission = false runCatching { activity.startActivity(SelfInstaller.permissionIntent(activity)) } - .onFailure { error = "Couldn't open Android's \"Install unknown apps\" setting"; pending = null } + .onFailure { error = activity.getString(R.string.upd_unknown_apps_failed); pending = null } }, onDismissPermission = { askPermission = false; pending = null }, ) @@ -77,7 +77,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl catalog = it follow = AppUpdates.follow(activity, it) error = null - }.onFailure { if (force) error = "Couldn't check: ${it.message ?: it.javaClass.simpleName}" } + }.onFailure { if (force) error = activity.getString(R.string.upd_couldnt_check, it.message ?: it.javaClass.simpleName) } } }, "app-updates").start() } @@ -90,20 +90,20 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl private fun install(r: AppUpdates.Release) { if (stage != null) return - val apk = r.apk ?: run { error = "This build has no download for this copy of DroidDeck"; return } + val apk = r.apk ?: run { error = activity.getString(R.string.upd_no_download); return } AppUpdates.installBlock(r)?.let { error = it; return } - if (SessionState.running) { error = "Stop the running session first, then update"; return } + if (SessionState.running) { error = activity.getString(R.string.upd_stop_first); return } if (!SelfInstaller.canInstall(activity)) { pending = r; askPermission = true; return } error = null - stage = "Downloading" + stage = activity.getString(R.string.upd_stage_downloading) percent = 0 Thread({ val result = runCatching { val file = SelfInstaller.download(activity, apk) { pct -> ui.post { percent = pct } } - ui.post { stage = "Installing"; percent = -1 } + ui.post { stage = activity.getString(R.string.upd_stage_installing); percent = -1 } SelfInstaller.install(activity, file) } - result.onFailure { e -> ui.post { stage = null; error = e.message ?: "The update failed" } } + result.onFailure { e -> ui.post { stage = null; error = e.message ?: activity.getString(R.string.upd_failed) } } }, "app-update-install").start() } @@ -114,8 +114,8 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl PackageInstaller.STATUS_PENDING_USER_ACTION -> { val confirm = if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) else @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_INTENT) - if (confirm == null) { stage = null; error = "Android didn't offer to install the update"; return } - stage = "Waiting for you to confirm" + if (confirm == null) { stage = null; error = activity.getString(R.string.upd_not_offered); return } + stage = activity.getString(R.string.upd_stage_confirm) activity.startActivity(confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } PackageInstaller.STATUS_SUCCESS -> stage = null diff --git a/app/src/main/res/values/strings_g1.xml b/app/src/main/res/values/strings_g1.xml new file mode 100644 index 000000000..50c77a29b --- /dev/null +++ b/app/src/main/res/values/strings_g1.xml @@ -0,0 +1,213 @@ + + + + Wait for the runtime operation to finish + Wait for the compatibility tool install to finish + Wait for the desktop app install to finish + Wait for the install to finish before launching a game + Close the Steam session first, so the game can\'t save over the import + + Importing into %1$s + + + Imported %1$d file from the %2$s into %3$s + Imported %1$d files from the %2$s into %3$s + + + Imported %1$d file from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups + Imported %1$d files from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups + + Choose a save zip for %1$s + + Exporting %1$s + + + Exported %1$d file as a %2$s: %3$s + Exported %1$d files as a %2$s: %3$s + + + Choose where to save %1$s (%2$s) + + %1$s failed: %2$s + No session logs yet: run a session first. + No session logs to clear. + + Cleared %1$d session log folder. + Cleared %1$d session log folders. + + Wireless debugging pairing failed + Wireless debugging command failed + Wireless debugging is unavailable + ADB command copied + Choose the ROMs folder + Not an Adreno GPU + + Turnip supports Adreno GPUs. On %1$s, Steam may show a black screen. Download: %2$.0f MB. + Install anyway + Remove Linux runtime + This deletes the runtime, the Steam client inside it, and every game installed there. + + Applied: %1$s + Could not open %1$s on the primary screen + + Could not open %1$s on %2$s + + Could not open %1$s on display %2$d + + Importing + Swapping + Restoring + Cancelling + Deleting + cannot read the file + + Imported %1$s + The waiting swap was cancelled. + Choose a component package (-linux .wcp) + The Nightlies could not be reached + + Stored %1$s + Download failed: %1$s + Choose a Linux runtime driver (-Linux zip or Android + Linux bundle) + Choose a display driver (AdrenoTools zip or Android + Linux bundle) + Choose a driver zip or an Android + Linux bundle + + %1$s: %2$s + Removing %1$s… + Forgetting %1$s… + Choose the game storage folder + Choose a folder of your own games + Choose the game\'s .exe + Choose a Decky plugin ZIP + Not usable: %1$s + + Program + Removing Linux runtime + Could not open Settings on the bottom screen; opened it on the main screen. + Open Developer options in Settings + + + Couldn\'t open Android\'s \"Install unknown apps\" setting + Couldn\'t check: %1$s + This build has no download for this copy of DroidDeck + Stop the running session first, then update + + Downloading + Installing + Waiting for you to confirm + The update failed + Android didn\'t offer to install the update + + + Decky install failed: %1$s + Starting plugin import… + Plugin import failed: %1$s + Plugin installed. Restart Steam to load it. + + + Not checked yet - tap refresh to look for new drivers + Android + Linux: sets the display driver with it + Android + Linux: sets the runtime driver with it + the Turnip built into the runtime + system Vulkan: no bundled build for this GPU + %1$s (bundled) + Manual + + %1$s · %2$s + Only one half is published right now + %1$.0f MB for both + + No drivers to set: %1$s + Checking for the latest drivers… + Up to date (couldn\'t check: %1$s) + The recommended drivers aren\'t published right now + + Downloading %1$s %2$s… + Updated to %1$s %2$s. + Updated to %1$s %2$s. The display driver applies after DroidDeck restarts. + Switched to %1$s %2$s. + Switched to %1$s %2$s. The display driver applies after DroidDeck restarts. + Using %1$s %2$s. + Using %1$s %2$s. The display driver applies after DroidDeck restarts. + Not a driver zip: neither an AdrenoTools driver, a -Linux Turnip nor an Android + Linux bundle + Import failed: %1$s + Imported driver + Using %1$s as both the runtime and display driver. + Using %1$s as both the runtime and display driver. The display driver applies after DroidDeck restarts. + Deleted %1$s (both drivers) + Deleted %1$s + + Latest: %1$s · checked %2$s + + Latest: %1$s · %2$s unreachable · checked %3$s + + %1$d min ago + %1$d min ago + + + %1$d h ago + %1$d h ago + + + %1$d day ago + %1$d days ago + + Checking Banners-Turnip, WinNative and DroidDeck… + check failed + + Installed %1$s - pick it in either list to set both drivers + Installed %1$s - pick it in the menu + + + Wait for runtime removal to finish + + Game + + touchpad + + direct + Swap failed: %1$s + The desktop is paused + + %1$s is paused + The program is paused + Starting Steam on the desktop + Starting the desktop + + Starting %1$s + Starting the program + No logs for this session. + Could not create the session log archive. + Could not share the session logs. + Starting the session… + + downloading the Linux runtime + downloading the Linux runtime · %1$d of %2$d MB + checking the Linux runtime + unpacking the Linux runtime + downloading the desktop + downloading the desktop · %1$d of %2$d MB + checking the desktop + unpacking the desktop + downloading Proton Experimental (ARM64) + downloading Proton Experimental (ARM64) · %1$d of %2$d MB + checking Proton Experimental (ARM64) + unpacking Proton Experimental (ARM64) + Could not reach the runtime catalog. Check the connection and press Play again. + The Linux runtime did not install. Check the runtime in Setup and press Play again. + Could not reach the desktop catalog. Check the connection and press Desktop again. + + The desktop did not install (%1$s). Check the connection and press Desktop again. + + %1$s stopped unexpectedly. Share the logs with a bug report, or try again. + The program stopped unexpectedly. Share the logs with a bug report, or try again. + The desktop stopped unexpectedly. Share the logs with a bug report, or try again. + + Exit status %1$d · %2$s + no log + + + The log shows %1$d \"Function not implemented\" error: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again. + The log shows %1$d \"Function not implemented\" errors: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again. + +