From 0fa1f2f8689facd37216d7906fc00b48db4eaac0 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 1 Oct 2026 22:10:22 -0400 Subject: [PATCH] ui: move settings, components, drawer and setup copy to string resources, tighten wording --- .../droiddeck/launcher/ui/ComponentsPage.kt | 110 +++--- .../com/droiddeck/launcher/ui/DriverPage.kt | 28 +- .../droiddeck/launcher/ui/FrontEndSetup.kt | 129 +++---- .../launcher/ui/ModeSettingsDialog.kt | 224 ++++++----- .../launcher/ui/PerformanceDialog.kt | 74 ++-- .../droiddeck/launcher/ui/SessionOverlay.kt | 129 ++++--- app/src/main/res/values/strings.xml | 362 +++++++++++++++++- 7 files changed, 714 insertions(+), 342 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt index 67fc47be7..0d19c376e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt @@ -1,5 +1,8 @@ package com.droiddeck.launcher.ui +import androidx.compose.ui.platform.LocalContext +import com.droiddeck.launcher.R +import androidx.compose.ui.res.stringResource import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -114,16 +117,18 @@ fun ComponentsPage( var confirmTitle by remember { mutableStateOf("") } var about by remember { mutableStateOf(false) } var protonMenu by remember { mutableStateOf(false) } - fun ask(title: String, body: String, action: () -> Unit) { confirmTitle = title; confirm = body to action } + var confirmVerb by remember { mutableStateOf(R.string.comp_swap) } + val ctx = LocalContext.current + fun ask(title: String, body: String, verb: Int, action: () -> Unit) { confirmTitle = title; confirmVerb = verb; confirm = body to action } // The GPU drivers tab's Advanced pages: one driver list on its own, full page. var driverPage by remember { mutableStateOf(null) } val gpuTab = comp == GPU_TAB when (driverPage) { "rt" -> { DriverPage( - title = "Runtime driver", hint = "Steam, its games and the desktop draw with it. Applies next session.", + title = stringResource(R.string.comp_runtime_driver), hint = stringResource(R.string.comp_runtime_driver_hint), rows = gpu.linuxRows, selected = gpu.linuxSelected, downloads = gpu.linuxDownloads, - status = gpu.releaseStatus, checking = gpu.checking, importLabel = "Import Turnip zip…", canRestore = false, + status = gpu.releaseStatus, checking = gpu.checking, importLabel = stringResource(R.string.comp_import_turnip), canRestore = false, onSelect = gpuActions.onSelectLinux, onDelete = gpuActions.onRemoveLinux, onRefresh = gpuActions.onRefresh, onDownload = gpuActions.onDownloadDriver, onImport = gpuActions.onImportLinux, onRestore = {}, onBack = { driverPage = null }, ) @@ -131,9 +136,9 @@ fun ComponentsPage( } "panel" -> { DriverPage( - title = "Display driver", hint = "Puts frames on the screen in both modes. Restart the app to apply.", + title = stringResource(R.string.comp_display_driver), hint = stringResource(R.string.comp_display_driver_hint), rows = gpu.androidRows, selected = gpu.androidSelected, downloads = gpu.androidDownloads, - status = gpu.releaseStatus, checking = gpu.checking, importLabel = "Import an AdrenoTools zip…", + status = gpu.releaseStatus, checking = gpu.checking, importLabel = stringResource(R.string.comp_import_adrenotools), canRestore = gpu.canRestoreBundled, onSelect = gpuActions.onSelectAndroid, onDelete = gpuActions.onRemoveAndroid, onRefresh = gpuActions.onRefresh, onDownload = gpuActions.onDownloadDriver, onImport = gpuActions.onImportAndroid, onRestore = gpuActions.onRestoreBundled, @@ -146,14 +151,14 @@ fun ComponentsPage( val views = snapshot?.protons ?: emptyList() val view = views.firstOrNull { it.proton.id == protonId } ?: views.firstOrNull() val label = ComponentsManager.LABEL[comp] ?: comp - val checked = if (catalogAt > 0) DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(catalogAt * 1000)) else "never" - val nightlies = if (gpuTab) gpu.releaseStatus else if (busy != null) "$busy…" else "Nightlies checked $checked" + val checked = if (catalogAt > 0) DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(catalogAt * 1000)) else stringResource(R.string.comp_never) + val nightlies = if (gpuTab) gpu.releaseStatus else if (busy != null) "$busy…" else stringResource(R.string.comp_nightlies_checked, checked) Column(Modifier.fillMaxSize().padding(horizontal = if (narrow) 16.dp else 22.dp, vertical = if (narrow) 12.dp else 18.dp)) { // ---- title, and the one thing that goes online ----------------------------------------- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) { Text( - "Components", fontSize = if (narrow) 22.sp else 26.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, + stringResource(R.string.comp_title), fontSize = if (narrow) 22.sp else 26.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1, ) // The status gives way to the title, not the other way round. @@ -161,9 +166,9 @@ fun ComponentsPage( nightlies, fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = androidx.compose.ui.text.style.TextAlign.End, modifier = Modifier.weight(1f), ) else Spacer(Modifier.weight(1f)) - if (!gpuTab) ToolIcon(Icons.Outlined.Info, "About Components") { about = true } - if (gpuTab) ToolIcon(Icons.Outlined.Refresh, "Check for new drivers", busy = gpu.checking, enabled = !gpu.checking && gpu.busy == null, onClick = gpuActions.onRefresh) - else ToolIcon(Icons.Outlined.Refresh, "Check the Nightlies for new packages", busy = checking, enabled = !checking, onClick = onRefresh) + if (!gpuTab) ToolIcon(Icons.Outlined.Info, stringResource(R.string.comp_about)) { about = true } + if (gpuTab) ToolIcon(Icons.Outlined.Refresh, stringResource(R.string.comp_check_drivers), busy = gpu.checking, enabled = !gpu.checking && gpu.busy == null, onClick = gpuActions.onRefresh) + else ToolIcon(Icons.Outlined.Refresh, stringResource(R.string.comp_check_nightlies), busy = checking, enabled = !checking, onClick = onRefresh) } if (narrow) Text(nightlies, fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis) @@ -175,26 +180,27 @@ fun ComponentsPage( ) { if (view != null && !gpuTab) Box { ValueChip(view.proton.name, protonMenu, modifier = Modifier.widthIn(max = 300.dp).heightIn(min = 44.dp)) { protonMenu = !protonMenu } - AnchoredMenu(protonMenu, onDismiss = { protonMenu = false }, title = "Proton", - note = "Valve's own Proton is replaced when Steam updates it; its originals are kept per build.") { first -> + AnchoredMenu(protonMenu, onDismiss = { protonMenu = false }, title = stringResource(R.string.comp_proton), + note = stringResource(R.string.comp_proton_note)) { first -> views.forEachIndexed { i, v -> val swaps = v.components.values.count { it.activeFile != null } MenuItem( v.proton.name, checked = v.proton.id == view.proton.id, - detail = v.proton.version + (if (swaps > 0) " · $swaps swapped" else " · all original") + (if (v.inUseByGame) " · game running" else ""), + detail = (if (swaps > 0) stringResource(R.string.comp_swapped, v.proton.version, swaps) else stringResource(R.string.comp_all_original, v.proton.version)) + .let { if (v.inUseByGame) stringResource(R.string.comp_game_running_suffix, it) else it }, focusRequester = if (i == 0) first else null, ) { onProton(v.proton.id); protonMenu = false } } } } val tabs = listOf(GPU_TAB) + comps - TabStrip(tabs.map { ComponentsManager.LABEL[it] ?: "GPU drivers" }, tabs.indexOf(comp).coerceAtLeast(0), { onComp(tabs[it]) }) + TabStrip(tabs.map { ComponentsManager.LABEL[it] ?: stringResource(R.string.comp_gpu_drivers) }, tabs.indexOf(comp).coerceAtLeast(0), { onComp(tabs[it]) }) } // ---- what is in use ----------------------------------------------------------------------- if (view != null && !gpuTab) { val st = view.components.getValue(comp) - val fixedAt = if (view.reappliedAt > 0) " · re-applied at launch " + DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(view.reappliedAt * 1000)) else "" + val reappliedAt = if (view.reappliedAt > 0) DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(view.reappliedAt * 1000)) else null Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth().padding(top = 12.dp).clip(RoundedCornerShape(12.dp)).background(colors.surface) @@ -202,12 +208,13 @@ fun ComponentsPage( ) { Box(Modifier.size(8.dp).background(if (st.queued != null) Color(0xFFFFB86B) else pal.good, CircleShape)) Text( - (if (st.queued != null) "Next: ${st.queued} · after the game closes" else "$label in use: ${st.inUse}") + fixedAt, + (if (st.queued != null) stringResource(R.string.comp_next_queued, st.queued) else stringResource(R.string.comp_in_use, label, st.inUse)) + .let { if (reappliedAt != null) stringResource(R.string.comp_reapplied, it, reappliedAt) else it }, fontSize = 14.sp, color = colors.onBackground, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), ) - if (st.queued != null) FocusText("Cancel", pal.signal, onClick = onCancelQueued) + if (st.queued != null) FocusText(stringResource(R.string.common_cancel), pal.signal, onClick = onCancelQueued) else if (!narrow) Text( - if (view.inUseByGame) "Waits until the game closes" else "Changes apply the next time a game starts", + if (view.inUseByGame) stringResource(R.string.comp_waits) else stringResource(R.string.comp_applies_next_game), fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1, ) } @@ -218,16 +225,16 @@ fun ComponentsPage( gpuTab -> Column(Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 16.dp)) { GpuDriversPanel(gpu, gpuActions) { driverPage = it } Row(modifier = Modifier.padding(top = 14.dp)) { - SmallButton("Import .zip", onClick = gpuActions.onImportZip) + SmallButton(stringResource(R.string.comp_import_zip), onClick = gpuActions.onImportZip) } } snapshot == null -> Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(16.dp)) { CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(12.dp)) - Text("Reading the Protons…", fontSize = 14.sp, color = colors.onSurfaceVariant) + Text(stringResource(R.string.comp_reading), fontSize = 14.sp, color = colors.onSurfaceVariant) } view == null -> Text( - "No Proton is installed in the Linux runtime yet. Start the Steam client once so it downloads its ARM64 Proton, or add GE-Proton / CachyOS from Setup.", + stringResource(R.string.comp_no_proton), fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 16.dp), ) else -> { @@ -240,12 +247,12 @@ fun ComponentsPage( val rows = originals.map { o -> val isCurrent = o.protonVersion == build InstalledItem( - "Original · ${o.protonVersion}", - o.label.substringAfterLast(" · ", "").ifEmpty { o.label } + if (isCurrent) " · this build" else " · earlier build", + stringResource(R.string.comp_original, o.protonVersion), + o.label.substringAfterLast(" · ", "").ifEmpty { o.label }.let { if (isCurrent) stringResource(R.string.comp_this_build, it) else stringResource(R.string.comp_earlier_build, it) }, "ORIGINAL", activeIsOriginal && isCurrent, !isCurrent, null, o.protonVersion, ) } + stored.map { s -> - InstalledItem(s.version, String.format("%.1f MB", s.size / 1048576.0), "STORED", s.file == st.activeFile, true, s.file, null) + InstalledItem(s.version, stringResource(R.string.common_size_mb, s.size / 1048576.0), "STORED", s.file == st.activeFile, true, s.file, null) } val storedNames = snapshot.packages.map { it.file }.toSet() val available = catalog.filter { it.comp == comp && ComponentsManager.safeName(it.file) !in storedNames } @@ -255,9 +262,9 @@ fun ComponentsPage( } val installed: @Composable () -> Unit = { - SettingsGroup("Installed · ${rows.size}") { + SettingsGroup(stringResource(R.string.comp_installed_count, rows.size)) { if (rows.isEmpty()) Text( - "Nothing stored for $label yet. This Proton's own files are saved as its original when the page opens.", + stringResource(R.string.comp_nothing_stored, label), fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), ) rows.forEachIndexed { i, item -> @@ -265,22 +272,22 @@ fun ComponentsPage( item, modifier = if (i == 0) Modifier.focusRequester(first) else Modifier, onSelect = { if (item.selected) return@InstalledLine - val waits = if (view.inUseByGame) " A game is running on this Proton, so it waits until the game closes." else " It applies the next time a game starts." - if (item.file != null) ask("Swap $label?", "Put ${item.name} into ${p.name}? Its shipped files stay saved as an original bundle.$waits") { onSwap(item.file) } - else ask("Restore $label?", "Put the ${item.protonVersion} original back into ${p.name}?$waits") { onRestore(item.protonVersion!!) } + val waits = if (view.inUseByGame) ctx.getString(R.string.comp_waits_long) else ctx.getString(R.string.comp_applies_long) + if (item.file != null) ask(ctx.getString(R.string.comp_swap_title, label), ctx.getString(R.string.comp_swap_body, item.name, p.name, waits), R.string.comp_swap) { onSwap(item.file) } + else ask(ctx.getString(R.string.comp_restore_title, label), ctx.getString(R.string.comp_restore_body, item.protonVersion, p.name, waits), R.string.common_restore) { onRestore(item.protonVersion!!) } }, onDelete = { - if (item.file != null) ask("Delete ${item.name}?", "Its package file is removed from the app. You can download it again any time.") { onDeletePackage(item.file) } - else ask("Delete this original?", "The ${item.protonVersion} original of $label belongs to an earlier build of ${p.name}. The installed build's original is always kept.") { onDeleteOriginal(item.protonVersion!!) } + if (item.file != null) ask(ctx.getString(R.string.common_delete_named, item.name) + "?", ctx.getString(R.string.comp_delete_package_body), R.string.common_delete) { onDeletePackage(item.file) } + else ask(ctx.getString(R.string.comp_delete_original_title), ctx.getString(R.string.comp_delete_original_body, item.protonVersion, p.name), R.string.common_delete) { onDeleteOriginal(item.protonVersion!!) } }, ) } } } val nightly: @Composable () -> Unit = { - SettingsGroup("On the Nightlies · ${available.size}") { + SettingsGroup(stringResource(R.string.comp_nightlies_count, available.size)) { if (available.isEmpty()) Text( - if (catalogAt == 0L) "Refresh to list the packages on the Nightlies." else "Nothing new since the last check.", + if (catalogAt == 0L) stringResource(R.string.comp_refresh_to_list) else stringResource(R.string.comp_nothing_new), fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), ) available.forEach { d -> AvailableLine(d, downloads[d.file], enabled = busy == null) { onDownload(d) } } @@ -293,9 +300,9 @@ fun ComponentsPage( Column(Modifier.weight(1f)) { nightly() } } Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 14.dp)) { - SmallButton("Import .wcp", onClick = onImport) - if (!activeIsOriginal && originals.any { it.protonVersion == build }) FocusText("Restore this Proton's original", pal.signal) { - ask("Restore $label?", "Put ${p.name}'s original $label back?") { onRestore(build) } + SmallButton(stringResource(R.string.comp_import_wcp), onClick = onImport) + if (!activeIsOriginal && originals.any { it.protonVersion == build }) FocusText(stringResource(R.string.comp_restore_original), pal.signal) { + ask(ctx.getString(R.string.comp_restore_title, label), ctx.getString(R.string.comp_restore_original_body, p.name, label), R.string.common_restore) { onRestore(build) } } } } @@ -305,17 +312,14 @@ fun ComponentsPage( if (about) AlertDialog( onDismissRequest = { about = false }, - title = { Text("Components") }, + title = { Text(stringResource(R.string.comp_title)) }, text = { Text( - "FEX, DXVK and VKD3D-Proton for each Proton the Steam client runs games with.\n\n" + - "Tap an installed row to swap it in; it applies the next time a game starts. If a game is running on that Proton, the swap waits until it closes.\n\n" + - "Your choices are also checked right before every game launch: if anything changed a Proton's files (Steam's start-up tests, for one), they are put back first, and the page says \"re-applied at launch\".\n\n" + - "Each Proton build's own files are kept as its original, so a Steam update never loses them. Packages come from the Nightlies \"-Linux\" releases (refresh) or Import .wcp.", + stringResource(R.string.comp_about_text), fontSize = 14.sp, ) }, - confirmButton = { FocusText("OK", pal.signal) { about = false } }, + confirmButton = { FocusText(stringResource(R.string.common_ok), pal.signal) { about = false } }, ) confirm?.let { (body, action) -> AlertDialog( @@ -324,13 +328,13 @@ fun ComponentsPage( text = { Text(body, fontSize = 14.sp) }, // Opens on Cancel, so a stray A press on a controller never swaps or deletes anything. confirmButton = { - val del = confirmTitle.startsWith("Delete") - FocusText(if (del) "Delete" else if (confirmTitle.startsWith("Restore")) "Restore" else "Swap", if (del) colors.error else pal.signal) { confirm = null; action() } + val del = confirmVerb == R.string.common_delete + FocusText(stringResource(confirmVerb), if (del) colors.error else pal.signal) { confirm = null; action() } }, dismissButton = { val cancelFocus = remember { FocusRequester() } LaunchedEffect(Unit) { runCatching { cancelFocus.requestFocus() } } - FocusText("Cancel", colors.onBackground, modifier = Modifier.focusRequester(cancelFocus)) { confirm = null } + FocusText(stringResource(R.string.common_cancel), colors.onBackground, modifier = Modifier.focusRequester(cancelFocus)) { confirm = null } }, ) } @@ -392,7 +396,13 @@ private fun Tag(text: String) { else -> colors.onSurfaceVariant to Color.White.copy(alpha = 0.07f) } Text( - text.lowercase().replaceFirstChar { it.uppercase() }, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = fg, maxLines = 1, + when (text) { + "ORIGINAL" -> stringResource(R.string.comp_tag_original) + "IN USE" -> stringResource(R.string.comp_tag_in_use) + "NEW" -> stringResource(R.string.comp_tag_new) + "STORED" -> stringResource(R.string.comp_tag_stored) + else -> text.lowercase().replaceFirstChar { it.uppercase() } + }, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = fg, maxLines = 1, modifier = Modifier.clip(RoundedCornerShape(6.dp)).background(bg).padding(horizontal = 7.dp, vertical = 1.dp), ) } @@ -443,7 +453,7 @@ private fun InstalledLine(item: InstalledItem, modifier: Modifier = Modifier, on .glideBorder(delHot, ROW_SHAPE, colors.error) .hoverable(delSrc).clickable(interactionSource = delSrc, indication = null, onClick = onDelete) .controllerConfirm(onClick = onDelete), - ) { Icon(Icons.Outlined.Delete, contentDescription = "Delete ${item.name}", tint = if (delHot) colors.error else colors.onSurfaceVariant, modifier = Modifier.size(19.dp)) } + ) { Icon(Icons.Outlined.Delete, contentDescription = stringResource(R.string.common_delete_named, item.name), tint = if (delHot) colors.error else colors.onSurfaceVariant, modifier = Modifier.size(19.dp)) } } } Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) @@ -460,11 +470,11 @@ private fun AvailableLine(d: CatalogItem, progress: Int?, enabled: Boolean, onDo Column(Modifier.weight(1f)) { Text(d.file.removeSuffix(".wcp"), fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis) Text( - "${d.release} · " + String.format("%.1f MB", d.size / 1048576.0), fontSize = 13.sp, color = colors.onSurfaceVariant, + "${d.release} · " + stringResource(R.string.common_size_mb, d.size / 1048576.0), fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 2.dp), ) } - if (progress == null) SmallButton("Download", enabled = enabled, accent = true, onClick = onDownload) + if (progress == null) SmallButton(stringResource(R.string.common_download), enabled = enabled, accent = true, onClick = onDownload) else Column(horizontalAlignment = Alignment.End, modifier = Modifier.width(96.dp)) { Text(if (progress < 0) "…" else "$progress%", fontSize = 13.sp, color = colors.onSurfaceVariant) Spacer(Modifier.height(4.dp)) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt index 7b6722ccd..1ee6cbc2a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt @@ -1,5 +1,7 @@ package com.droiddeck.launcher.ui +import com.droiddeck.launcher.R +import androidx.compose.ui.res.stringResource import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.hoverable @@ -98,11 +100,11 @@ fun DriverPage( .clickable(interactionSource = refreshSrc, indication = null, enabled = !checking, onClick = onRefresh), ) { if (checking) CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(18.dp)) - else Icon(Icons.Outlined.Refresh, contentDescription = "Check for new drivers", tint = colors.onBackground, modifier = Modifier.size(20.dp)) + else Icon(Icons.Outlined.Refresh, contentDescription = stringResource(R.string.comp_check_drivers), tint = colors.onBackground, modifier = Modifier.size(20.dp)) } }, ) { - SettingsGroup("Installed") { + SettingsGroup(stringResource(R.string.driver_installed)) { val focusRow = rows.firstOrNull { it.id == selected } ?: rows.firstOrNull() rows.forEachIndexed { i, row -> InstalledRow( @@ -113,14 +115,14 @@ fun DriverPage( ) } } - SettingsGroup("Available to download") { + SettingsGroup(stringResource(R.string.driver_available)) { if (downloads.isEmpty()) Text( - "Nothing new from the last check. Tap the refresh button to look for newer releases.", + stringResource(R.string.driver_nothing_new), fontSize = 12.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 14.dp, vertical = 14.dp), ) else for (d in downloads) { SettingsRow(d.label, d.detail) { val p = d.progress - if (p == null) SecondaryButton("Download") { onDownload(d.key) } + if (p == null) SecondaryButton(stringResource(R.string.common_download)) { onDownload(d.key) } else Column(horizontalAlignment = Alignment.End, modifier = Modifier.width(120.dp)) { Text("$p%", fontSize = 12.sp, color = colors.onSurfaceVariant) Spacer(Modifier.height(4.dp)) @@ -131,27 +133,27 @@ fun DriverPage( } Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 14.dp)) { SecondaryButton(importLabel, onClick = onImport) - if (canRestore) FocusText("Restore built-in drivers", pal.signal, onClick = onRestore) + if (canRestore) FocusText(stringResource(R.string.driver_restore_builtin), pal.signal, onClick = onRestore) } } confirm?.let { row -> AlertDialog( onDismissRequest = { confirm = null }, - title = { Text("Delete ${row.name}?") }, + title = { Text(stringResource(R.string.common_delete_named, row.name) + "?") }, text = { Text( - if (row.tag == DriverRow.BUNDLED) "It is built into the app, so this removes its unpacked files and hides it. \"Restore built-in drivers\" brings it back, and Auto still uses it on a GPU that needs it." - else if (row.tag == DriverRow.BUNDLE) "It is one half of an Android + Linux bundle, so its runtime and display drivers are both removed. A driver in use goes back to the default." - else "Its files are removed from the app." + if (row.id == selected) " It is the driver in use, so the default takes its place." else "", + if (row.tag == DriverRow.BUNDLED) stringResource(R.string.driver_delete_builtin) + else if (row.tag == DriverRow.BUNDLE) stringResource(R.string.driver_delete_bundle) + else if (row.id == selected) stringResource(R.string.driver_delete_in_use) else stringResource(R.string.driver_delete_plain), fontSize = 13.sp, ) }, // Opens on Cancel, so a stray A press on a controller never deletes anything. - confirmButton = { FocusText("Delete", colors.error) { confirm = null; onDelete(row.id) } }, + confirmButton = { FocusText(stringResource(R.string.common_delete), colors.error) { confirm = null; onDelete(row.id) } }, dismissButton = { val cancelFocus = remember { FocusRequester() } LaunchedEffect(Unit) { runCatching { cancelFocus.requestFocus() } } - FocusText("Cancel", colors.onBackground, modifier = Modifier.focusRequester(cancelFocus)) { confirm = null } + FocusText(stringResource(R.string.common_cancel), colors.onBackground, modifier = Modifier.focusRequester(cancelFocus)) { confirm = null } }, ) } @@ -214,7 +216,7 @@ private fun InstalledRow(row: DriverRow, selected: Boolean, onSelect: () -> Unit .focusRequester(delFocus) .focusProperties { left = mainFocus } .hoverable(delSrc).clickable(interactionSource = delSrc, indication = null, onClick = onDelete), - ) { Icon(Icons.Outlined.Delete, contentDescription = "Delete ${row.name}", tint = if (delHot) colors.error else colors.onSurfaceVariant, modifier = Modifier.size(20.dp)) } + ) { Icon(Icons.Outlined.Delete, contentDescription = stringResource(R.string.common_delete_named, row.name), tint = if (delHot) colors.error else colors.onSurfaceVariant, modifier = Modifier.size(20.dp)) } } } Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 433ceb758..af7a1470d 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -1,5 +1,6 @@ package com.droiddeck.launcher.ui +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.FocusRequester import android.os.Build @@ -106,10 +107,9 @@ private fun CheckRow(state: CheckState, title: String, detail: String?, divider: CheckState.BUSY -> Icons.Filled.Refresh }, contentDescription = when (state) { - CheckState.OK -> "Done" - CheckState.WARN -> "Needs attention" - CheckState.BUSY -> "Working" - }, + CheckState.OK -> stringResource(R.string.setup_check_done) + CheckState.WARN -> stringResource(R.string.setup_check_warn) + CheckState.BUSY -> stringResource(R.string.setup_check_busy) }, tint = tint, modifier = Modifier.size(16.dp), ) } @@ -130,6 +130,7 @@ internal fun SetupPanel( onRequestWirelessAdb: (Boolean) -> Unit, ) { val host = rememberMenuHost() + val ctx = LocalContext.current var processLimitBusy by remember { mutableStateOf(false) } var processLimitMessage by remember { mutableStateOf(null) } val setProcessLimit: (Boolean) -> Unit = { enabled -> @@ -140,16 +141,16 @@ internal fun SetupPanel( if (error == null) { a.onRefreshPhantomStatus() } else { - processLimitMessage = "Check the Wireless debugging IP address & Port, or pair again if Android removed this device." + processLimitMessage = ctx.getString(R.string.setup_limit_adb_failed) onRequestWirelessAdb(enabled) } } } val runtime = when { - s.busy -> "Working…" - !s.ready -> "Install" - s.available != null && s.available != s.installed -> "Update" - else -> "Manage" + s.busy -> stringResource(R.string.common_working) + !s.ready -> stringResource(R.string.setup_install) + s.available != null && s.available != s.installed -> stringResource(R.string.common_update) + else -> stringResource(R.string.setup_manage) } val colors = MaterialTheme.colorScheme val pal = LocalPalette.current @@ -164,7 +165,7 @@ internal fun SetupPanel( val readyCount = listOf(gpuOk, s.ready && !s.busy, !limitBlocks, signedIn).count { it } // Four tabs instead of one long scroll; LB and RB turn them from anywhere on the page. Build // and credits are on the Updates page. - val tabs = listOf("Overview", "Controller", "Session", "Launcher") + val tabs = listOf(stringResource(R.string.setup_tab_overview), stringResource(R.string.setup_tab_controller), stringResource(R.string.setup_tab_session), stringResource(R.string.setup_tab_launcher)) var tab by rememberSaveable { mutableStateOf(0) } val tabFocus = remember { List(tabs.size) { FocusRequester() } } var tabTurned by remember { mutableStateOf(false) } @@ -184,53 +185,53 @@ internal fun SetupPanel( onNext = { pick((tab + 1) % tabs.size) }, ), ) { - PageHeader("Setup") { - Chip(if (readyCount == checks) "● All set" else "$readyCount of $checks ready", ok = readyCount == checks) + PageHeader(stringResource(R.string.setup_title)) { + Chip(if (readyCount == checks) stringResource(R.string.setup_all_set) else stringResource(R.string.setup_n_ready, readyCount, checks), ok = readyCount == checks) } TabStrip(tabs, tab, pick, Modifier.padding(bottom = 4.dp), tabFocus) Column(modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) { when (tab) { 0 -> { - SectionTitle("System check", null) + SectionTitle(stringResource(R.string.setup_system_check), null) // What Steam needs, one row each: green when done, one button when not. The // process-limit controls only open under their row. Column(modifier = Modifier.fillMaxWidth().clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14)) { CheckRow( if (gpuOk) CheckState.OK else CheckState.WARN, when (gpu.support) { - com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> "Device supported" - com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> "Untested GPU" - else -> "GPU not supported" + com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> stringResource(R.string.setup_gpu_supported) + com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> stringResource(R.string.setup_gpu_untested) + else -> stringResource(R.string.setup_gpu_unsupported) }, when (gpu.support) { - com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> "${gpu.name} · $gpuName" - com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> "${gpu.name}: ${gpu.supportText.replaceFirstChar { it.lowercase() }}" - else -> "Steam draws with an Adreno driver; $gpuName may show a black screen" + com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> stringResource(R.string.setup_gpu_detail, gpu.name, gpuName) + com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> stringResource(R.string.setup_gpu_untested_detail, gpu.name, gpu.supportText.replaceFirstChar { it.lowercase() }) + else -> stringResource(R.string.setup_gpu_unsupported_detail, gpuName) }, ) CheckRow( when { s.busy -> CheckState.BUSY; !s.ready -> CheckState.WARN; else -> CheckState.OK }, - "Linux runtime", + stringResource(R.string.setup_runtime), when { - s.busy -> if (s.percent >= 0) "${s.stage} · ${s.percent}%" else s.stage - !s.ready -> "Not installed · about 3 GB, installed on the first Play" - s.available != null && s.available != s.installed -> "${s.installed ?: "Installed"} · update available" - else -> "${s.installed ?: "Installed"} · up to date" + s.busy -> if (s.percent >= 0) stringResource(R.string.setup_runtime_progress, s.stage, s.percent) else s.stage + !s.ready -> stringResource(R.string.setup_runtime_missing) + s.available != null && s.available != s.installed -> stringResource(R.string.setup_runtime_update, s.installed ?: stringResource(R.string.setup_installed)) + else -> stringResource(R.string.setup_runtime_current, s.installed ?: stringResource(R.string.setup_installed)) }, ) { SecondaryButton(runtime, enabled = !s.busy, compact = true, onClick = a.onRuntime) } CheckRow( if (limitBlocks) CheckState.WARN else CheckState.OK, - "Child-process limit", + stringResource(R.string.setup_limit), when (s.phantomProcessStatus) { - PhantomProcessStatus.ENABLED -> "On · Android may close Steam. Turning it off takes a minute" - PhantomProcessStatus.UNSET -> "Not set · the ROM default may close Steam. Turning it off takes a minute" - PhantomProcessStatus.UNREADABLE -> "Could not be checked · turning it off takes a minute" + PhantomProcessStatus.ENABLED -> stringResource(R.string.setup_limit_on) + PhantomProcessStatus.UNSET -> stringResource(R.string.setup_limit_unset) + PhantomProcessStatus.UNREADABLE -> stringResource(R.string.setup_limit_unknown) else -> PhantomProcessLimit.title(s.phantomProcessStatus) }, ) { - if (limitBlocks) PrimaryButton(if (showLimitDetails) "Hide" else "Fix it", compact = true) { showLimitDetails = !showLimitDetails } + if (limitBlocks) PrimaryButton(if (showLimitDetails) stringResource(R.string.common_hide) else stringResource(R.string.setup_fix_it), compact = true) { showLimitDetails = !showLimitDetails } else if (s.phantomProcessStatus != PhantomProcessStatus.NOT_APPLICABLE) { - SecondaryButton(if (showLimitDetails) "Hide" else "Details", compact = true) { showLimitDetails = !showLimitDetails } + SecondaryButton(if (showLimitDetails) stringResource(R.string.common_hide) else stringResource(R.string.setup_details), compact = true) { showLimitDetails = !showLimitDetails } } } AnimatedVisibility(showLimitDetails, enter = expandVertically(Motion.sp(1f)) + fadeIn(Motion.sp(1f)), exit = shrinkVertically(Motion.sp(1f)) + fadeOut(Motion.sp(1f))) { @@ -244,11 +245,11 @@ internal fun SetupPanel( ) Actions { if (limitBlocks) { - PrimaryButton("Developer options", compact = true, onClick = onOpenDeveloperOptions) - SecondaryButton("Use Wireless debugging", compact = true, enabled = !processLimitBusy) { setProcessLimit(false) } - SecondaryButton("Check again", compact = true, onClick = a.onRefreshPhantomStatus) + PrimaryButton(stringResource(R.string.setup_dev_options), compact = true, onClick = onOpenDeveloperOptions) + SecondaryButton(stringResource(R.string.setup_use_wireless), compact = true, enabled = !processLimitBusy) { setProcessLimit(false) } + SecondaryButton(stringResource(R.string.setup_check_again), compact = true, onClick = a.onRefreshPhantomStatus) } else if (s.phantomProcessStatus == PhantomProcessStatus.DISABLED) { - SecondaryButton("Turn limit on", compact = true, enabled = !processLimitBusy) { setProcessLimit(true) } + SecondaryButton(stringResource(R.string.setup_limit_turn_on), compact = true, enabled = !processLimitBusy) { setProcessLimit(true) } } } if (processLimitBusy) LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)) @@ -257,25 +258,25 @@ internal fun SetupPanel( } CheckRow( if (signedIn) CheckState.OK else CheckState.WARN, - "Steam account", - s.offlineAccount?.let { if (s.offline) "Signed in as $it · offline mode" else "Signed in as $it" } ?: "Press Play and sign in to Steam", + stringResource(R.string.setup_account), + s.offlineAccount?.let { if (s.offline) stringResource(R.string.setup_signed_in_offline, it) else stringResource(R.string.setup_signed_in, it) } ?: stringResource(R.string.setup_sign_in), divider = false, ) } - SectionTitle("Tools", null) + SectionTitle(stringResource(R.string.setup_tools), null) ToolGrid(s, a) } 1 -> { val controller = s.controller - if (controller != null && a.controller != null) SettingsGroup("Controller") { + if (controller != null && a.controller != null) SettingsGroup(stringResource(R.string.setup_controller)) { ControllerRows(host, s.oscMode, controller, a.controller) } - if (s.controller == null || a.controller == null) Note("Controller settings are unavailable.") + if (s.controller == null || a.controller == null) Note(stringResource(R.string.setup_controller_unavailable)) } 2 -> { - SettingsGroup("Session") { + SettingsGroup(stringResource(R.string.setup_session)) { ChoiceRow( - host, "back-actions", "Back", SessionPrefs.backActionsOrder(s.backActionsInverted), + host, "back-actions", stringResource(R.string.mode_back), SessionPrefs.backActionsOrder(s.backActionsInverted), listOf( false to SessionPrefs.BACK_MENU_THEN_QAM, true to SessionPrefs.BACK_QAM_THEN_MENU, @@ -287,22 +288,22 @@ internal fun SetupPanel( FrameGenMenu(s, a, host) } } - ToggleRow(host, "logs", "Session logs", "Saved after each session", s.logsEnabled) { a.onLogs() } - ActionRow("Latest session logs", "Send them with a bug report", "Share logs", a.onShareLogs) - ActionRow("Saved session logs", "The newest ${com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS} are kept", "Clear logs", a.onClearLogs) + ToggleRow(host, "logs", stringResource(R.string.setup_logs), stringResource(R.string.setup_logs_hint), s.logsEnabled) { a.onLogs() } + ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs) + ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs) ToggleRow( - host, "offline", "Offline mode", - s.offlineAccount?.let { "Signed in as $it" } ?: "Sign in to Steam first", + host, "offline", stringResource(R.string.setup_offline), + s.offlineAccount?.let { stringResource(R.string.setup_signed_in, it) } ?: stringResource(R.string.setup_sign_in_first), s.offline, enabled = s.offlineAccount != null, ) { a.onOffline() } } } 3 -> { - SettingsGroup("Launcher") { - SettingsRow("Theme", "Choose the launcher appearance") { + SettingsGroup(stringResource(R.string.setup_launcher)) { + SettingsRow(stringResource(R.string.setup_theme), stringResource(R.string.setup_theme_hint)) { Box { ValueChip(Themes.byId(s.theme).label, host.open == "theme") { host.open = if (host.open == "theme") null else "theme" } - AnchoredMenu(host.open == "theme", onDismiss = { if (host.open == "theme") host.open = null }, title = "Theme") { firstItemFocus -> + AnchoredMenu(host.open == "theme", onDismiss = { if (host.open == "theme") host.open = null }, title = stringResource(R.string.setup_theme)) { firstItemFocus -> Themes.all.forEachIndexed { index, theme -> MenuItem(theme.label, checked = s.theme == theme.id, focusRequester = if (index == 0) firstItemFocus else null) { a.onTheme(theme.id) @@ -313,24 +314,24 @@ internal fun SetupPanel( } } ToggleRow( - host, "home-screen", "Use as a Home screen", - if (s.homeScreenEnabled) "DroidDeck can be the phone's Home app" else "Off: DroidDeck is never offered as a Home app", + host, "home-screen", stringResource(R.string.setup_home), + if (s.homeScreenEnabled) stringResource(R.string.setup_home_on) else stringResource(R.string.setup_home_off), s.homeScreenEnabled, ) { a.onHomeScreen(it) } ToggleRow( - host, "launcher-fullscreen", "Fullscreen", - if (s.launcherFullscreen) "Hide the Android status and navigation bars" else "Show the Android status and navigation bars", + host, "launcher-fullscreen", stringResource(R.string.setup_fullscreen), + if (s.launcherFullscreen) stringResource(R.string.setup_fullscreen_on) else stringResource(R.string.setup_fullscreen_off), s.launcherFullscreen, ) { a.onLauncherFullscreen(it) } if (s.homeScreenEnabled) { - ActionRow("Default Home app", s.defaultHomeLabel ?: "Choose a Home app", "Choose", a.onHomeApp) + ActionRow(stringResource(R.string.setup_default_home), s.defaultHomeLabel ?: stringResource(R.string.setup_choose_home), stringResource(R.string.setup_choose), a.onHomeApp) } } - SettingsGroup("Linux apps (beta)") { + SettingsGroup(stringResource(R.string.setup_linux_apps)) { ToggleRow( - host, "store-enabled", "Flathub Store", - if (s.storeEnabled) "The Store is in the menu. Some apps may not start; logs are in Download/DroidDeck" - else "Off: install Linux apps and games from Flathub with Flatpak", + host, "store-enabled", stringResource(R.string.setup_store), + if (s.storeEnabled) stringResource(R.string.setup_store_on) + else stringResource(R.string.setup_store_off), s.storeEnabled, ) { a.onStoreEnabled(it) } } @@ -346,10 +347,10 @@ internal fun SetupPanel( private fun ToolGrid(s: FrontEndState, a: FrontEndActions) { val columns = if (LocalNarrowPane.current) 2 else 4 val tools = listOf( - ToolSpec(Icons.Outlined.Folder, "Files", "Browse and manage files", a.onFiles), - ToolSpec(Icons.Outlined.Extension, "Proton versions", "Install ARM64 Proton builds", a.onProtons), - ToolSpec(Icons.Outlined.Speed, "Performance", "CPU core assignment", a.onPerformance), - ToolSpec(Icons.Outlined.VideogameAsset, "ROMs folder", s.romsDir ?: "Choose where emulator games are stored", a.onRoms), + ToolSpec(Icons.Outlined.Folder, stringResource(R.string.setup_tool_files), stringResource(R.string.setup_tool_files_hint), a.onFiles), + ToolSpec(Icons.Outlined.Extension, stringResource(R.string.setup_tool_protons), stringResource(R.string.setup_tool_protons_hint), a.onProtons), + ToolSpec(Icons.Outlined.Speed, stringResource(R.string.setup_tool_performance), stringResource(R.string.setup_tool_performance_hint), a.onPerformance), + ToolSpec(Icons.Outlined.VideogameAsset, stringResource(R.string.setup_tool_roms), s.romsDir ?: stringResource(R.string.setup_tool_roms_hint), a.onRoms), ) Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) { for (row in tools.chunked(columns)) { @@ -403,7 +404,7 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost val columns = if (LocalNarrowPane.current) 2 else 3 val controller = a.controller val cards = buildList<@Composable (Modifier) -> Unit> { - add { m -> SettingCard("Components", "FEX, DXVK, VKD3D", "card:components", m) { a.onComponents(true) } } + add { m -> SettingCard(stringResource(R.string.setup_card_components), stringResource(R.string.setup_card_components_hint), "card:components", m) { a.onComponents(true) } } add { m -> Box(m) { SettingCard(stringResource(R.string.frame_gen_title), s.frameGenLabel, "card:fg", Modifier.fillMaxSize()) { @@ -412,7 +413,7 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost FrameGenMenu(s, a, host) } } - if (controller != null) add { m -> SettingCard("Controls", "Button mapping", "card:controls", m, controller.onMapping) } + if (controller != null) add { m -> SettingCard(stringResource(R.string.setup_card_controls), stringResource(R.string.setup_card_controls_hint), "card:controls", m, controller.onMapping) } } Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) { for (row in cards.chunked(columns)) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index 2a2f4a561..61f1e2fc0 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -1,5 +1,6 @@ package com.droiddeck.launcher.ui +import androidx.compose.ui.res.pluralStringResource import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -151,32 +152,32 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { } SettingsPage( host, - title = if (steam) "Steam session" else "Desktop session", + title = if (steam) stringResource(R.string.mode_steam_title) else stringResource(R.string.mode_desktop_title), onBack = a.onDismiss, scroll = pageScroll, ) { - SettingsGroup("Display") { + SettingsGroup(stringResource(R.string.mode_display)) { val default = SessionPrefs.defaultResolutionCap(s.mode) var editCustom by remember { mutableStateOf(false) } val custom = s.customResolution ChoiceRow( - host, "res", "Resolution", "Applies next session.", - listOf(720 to "Up to 720p", 900 to "Up to 900p", 1080 to "Up to 1080p", 0 to "The panel's own") - .map { (cap, label) -> cap to (if (cap == default) "$label - the default" else label) } + - (CUSTOM to (custom?.let { "Custom · ${it.first}×${it.second}" } ?: "Custom…")), - if (custom != null) CUSTOM else s.resolutionCap, note = "720p can improve menu responsiveness.", + host, "res", stringResource(R.string.mode_resolution), stringResource(R.string.common_applies_next_session), + listOf(720 to stringResource(R.string.mode_res_720), 900 to stringResource(R.string.mode_res_900), 1080 to stringResource(R.string.mode_res_1080), 0 to stringResource(R.string.mode_res_native)) + .map { (cap, label) -> cap to (if (cap == default) stringResource(R.string.mode_res_default, label) else label) } + + (CUSTOM to (custom?.let { stringResource(R.string.mode_res_custom_value, it.first, it.second) } ?: stringResource(R.string.mode_res_custom))), + if (custom != null) CUSTOM else s.resolutionCap, note = stringResource(R.string.mode_res_note), chipModifier = androidx.compose.ui.Modifier.focusRequester(firstChip), onPick = { v -> if (v == CUSTOM) editCustom = true else { a.onCustomResolution(null); a.onResolution(v) } }, ) ChoiceRow( - host, "shape", "Screen ratio", - if (custom != null) "Set by the custom resolution." else "Auto uses at least 16:9.", + host, "shape", stringResource(R.string.mode_ratio), + if (custom != null) stringResource(R.string.mode_ratio_custom) else stringResource(R.string.mode_ratio_auto), com.droiddeck.launcher.session.SessionPrefs.shapeChoices, s.shapeMode, enabled = custom == null, onPick = a.onShape, ) ChoiceRow( - host, "fps", "Frame limit", "Applies next session.", + host, "fps", stringResource(R.string.mode_fps), stringResource(R.string.common_applies_next_session), com.droiddeck.launcher.session.SessionPrefs.fpsLimitChoices, s.fpsLimit, - note = "Caps the whole session. The screen switches to a rate the cap divides evenly.", + note = stringResource(R.string.mode_fps_note), onPick = a.onFpsLimit, ) if (editCustom) CustomResolutionDialog( @@ -185,94 +186,93 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { onDismiss = { editCustom = false }, ) } - SettingsGroup("HDR") { + SettingsGroup(stringResource(R.string.mode_hdr)) { ToggleRow( - host, "hdr", "HDR10 output", - s.hdrReason?.let { "Not available: $it." } - ?: "Restart the app to apply.", + host, "hdr", stringResource(R.string.mode_hdr10), + s.hdrReason?.let { stringResource(R.string.mode_hdr_unavailable, it) } + ?: stringResource(R.string.mode_hdr_restart), checked = s.hdr && s.hdrReason == null, enabled = s.hdrReason == null, onChange = a.onHdr, ) } - SettingsGroup("Drivers") { - SettingsRow("GPU drivers", "Shared by Steam and the desktop, on the Components page.") { + SettingsGroup(stringResource(R.string.mode_drivers)) { + SettingsRow(stringResource(R.string.mode_gpu_drivers), stringResource(R.string.mode_gpu_drivers_hint)) { ValueChip(s.gpuDrivers, open = false) { a.onGpuDrivers() } } } - SettingsGroup(if (steam) "Touch & controls" else "Touch") { + SettingsGroup(if (steam) stringResource(R.string.mode_touch_controls) else stringResource(R.string.mode_touch)) { ChoiceRow( - host, "touch", "Touch", null, - listOf("auto" to "Auto", "touchpad" to "Touchpad", "direct" to "Direct"), s.touchMode, - note = "Auto uses touchpad on desktop and direct input in Steam. Touchpad: drag to move, tap to click.", + host, "touch", stringResource(R.string.mode_touch), null, + listOf("auto" to stringResource(R.string.common_auto), "touchpad" to stringResource(R.string.mode_touch_touchpad), "direct" to stringResource(R.string.mode_touch_direct)), s.touchMode, + note = stringResource(R.string.mode_touch_note), onPick = a.onTouch, ) if (steam && s.oscMode != null) ChoiceRow( - host, "osc", "On-screen controls", null, + host, "osc", stringResource(R.string.mode_osc), null, listOf( - SessionPrefs.OSC_AUTO to "Auto", - SessionPrefs.OSC_ALWAYS to "Always", - SessionPrefs.OSC_STEAM_QAM to "Steam + QAM", - SessionPrefs.OSC_NEVER to "Never", + SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto), + SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always), + SessionPrefs.OSC_STEAM_QAM to stringResource(R.string.mode_osc_qam), + SessionPrefs.OSC_NEVER to stringResource(R.string.common_never), ), s.oscMode, - note = "Auto shows all controls without a controller. Steam + QAM shows only those buttons.", onPick = a.onOsc, + note = stringResource(R.string.mode_osc_note), onPick = a.onOsc, ) if (steam && s.steamController != null) ChoiceRow( - host, "controller", "Controller", "What your controller is to Steam. Applies next session.", + host, "controller", stringResource(R.string.mode_controller), stringResource(R.string.mode_controller_hint), listOf( - SessionPrefs.CONTROLLER_DECK to "Steam Deck controller", - SessionPrefs.CONTROLLER_XBOX360 to "Xbox 360 controller", + SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck), + SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360), ), s.steamController, - note = "Steam Deck controller: Steam reads it as a Deck's own, with its Quick Access button and the device's gyro. " + - "Xbox 360 controller: the plain pad of earlier versions; Quick Access opens with Guide+A.", + note = stringResource(R.string.mode_controller_note), onPick = a.onSteamController, ) if (steam) ChoiceRow( - host, "back-actions", "Back", SessionPrefs.backActionsOrder(s.backActionsInverted), + host, "back-actions", stringResource(R.string.mode_back), SessionPrefs.backActionsOrder(s.backActionsInverted), listOf( false to SessionPrefs.BACK_MENU_THEN_QAM, true to SessionPrefs.BACK_QAM_THEN_MENU, ), s.backActionsInverted, onPick = a.onBackActionsInverted, ) } - SettingsGroup("Session") { + SettingsGroup(stringResource(R.string.mode_session)) { ChoiceRow( - host, "suspend", "Background behavior", - "How this session behaves when the app leaves the screen or the display turns off.", + host, "suspend", stringResource(R.string.mode_suspend), + stringResource(R.string.mode_suspend_hint), listOf( - SessionPrefs.SUSPEND_AUTO to "Auto", - SessionPrefs.SUSPEND_MANUAL to "Manual", - SessionPrefs.SUSPEND_NEVER to "Never", + SessionPrefs.SUSPEND_AUTO to stringResource(R.string.common_auto), + SessionPrefs.SUSPEND_MANUAL to stringResource(R.string.mode_suspend_manual), + SessionPrefs.SUSPEND_NEVER to stringResource(R.string.common_never), ), s.suspendPolicy, - note = "Auto pauses in the background and resumes when visible. Manual pauses there and waits for Resume. Never keeps the session running.", + note = stringResource(R.string.mode_suspend_note), onPick = a.onSuspendPolicy, ) } - if (steam) SettingsGroup("Startup") { + if (steam) SettingsGroup(stringResource(R.string.mode_startup)) { ToggleRow( - host, "steam-startup", "Run Steam when DroidDeck starts", - "Open the Steam session when you launch DroidDeck.", + host, "steam-startup", stringResource(R.string.mode_steam_startup), + stringResource(R.string.mode_steam_startup_hint), s.runSteamAtStartup, onChange = a.onRunSteamAtStartup, ) } - if (steam) SettingsGroup("Decky") { + if (steam) SettingsGroup(stringResource(R.string.mode_decky)) { val updateAvailable = s.deckyInstalled != null && s.deckyLatestRelease != null && s.deckyInstalled != s.deckyLatestRelease.tag val status = when { s.deckyStage != null -> s.deckyStage - s.deckyChecking -> "Checking compatible releases…" - s.deckyInstalled == null && s.deckyLatestRelease != null -> "Ready to install ${s.deckyLatestRelease.tag}." - s.deckyInstalled == null -> "No compatible build found. Check again later." - s.deckyLatestRelease == null -> "Installed · ${s.deckyInstalled}" - updateAvailable -> "Update available · ${s.deckyLatestRelease.tag}" - else -> "Up to date · ${s.deckyInstalled}" + s.deckyChecking -> stringResource(R.string.mode_decky_checking) + s.deckyInstalled == null && s.deckyLatestRelease != null -> stringResource(R.string.mode_decky_ready, s.deckyLatestRelease.tag) + s.deckyInstalled == null -> stringResource(R.string.mode_decky_none) + s.deckyLatestRelease == null -> stringResource(R.string.mode_decky_installed, s.deckyInstalled) + updateAvailable -> stringResource(R.string.mode_decky_update, s.deckyLatestRelease.tag) + else -> stringResource(R.string.mode_decky_current, s.deckyInstalled) } - SettingsRow("Loader", status) { + SettingsRow(stringResource(R.string.mode_decky_loader), status) { val action = when { - s.deckyStage != null -> "Working…" - s.deckyChecking -> "Checking…" - s.deckyInstalled == null && s.deckyLatestRelease != null -> "Install latest" - s.deckyInstalled != null && updateAvailable -> "Update" - else -> "Check" + s.deckyStage != null -> stringResource(R.string.common_working) + s.deckyChecking -> stringResource(R.string.common_checking) + s.deckyInstalled == null && s.deckyLatestRelease != null -> stringResource(R.string.mode_decky_install_latest) + s.deckyInstalled != null && updateAvailable -> stringResource(R.string.common_update) + else -> stringResource(R.string.common_check) } Row(horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(6.dp)) { SecondaryButton( @@ -283,15 +283,15 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { else s.deckyLatestRelease?.let(a.onDeckyInstall) } if (s.deckyInstalled != null) SecondaryButton( - "Uninstall", + stringResource(R.string.common_uninstall), enabled = s.deckyStage == null && !s.deckySessionRunning, ) { confirmDeckyRemoval = true } } } if (s.deckyInstalled != null) SettingsRow( - "Decky", - if (s.deckyEnabled) "Starts with Steam. Other apps on this device may be able to control Steam while enabled." - else "Off. Decky stays off and Steam's remote debugging port stays closed.", + stringResource(R.string.mode_decky), + if (s.deckyEnabled) stringResource(R.string.mode_decky_on) + else stringResource(R.string.mode_decky_off), ) { Switch( checked = s.deckyEnabled, @@ -306,48 +306,48 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { ) } } - if (steam && s.steamChannel != null) SettingsGroup("Client") { + if (steam && s.steamChannel != null) SettingsGroup(stringResource(R.string.mode_client)) { ToggleRow( - host, "steamdeck", "Steam Deck mode", - "Enables Steam's Deck interface and Quick Access performance overlay controls. Applies next session.", + host, "steamdeck", stringResource(R.string.mode_deck_mode), + stringResource(R.string.mode_deck_mode_hint), s.steamDeckMode, onChange = a.onSteamDeckMode, ) if (s.steamDeckMode) ToggleRow( - host, "mangoapp", "Performance overlay", - "The Quick Access Menu's frame-rate and usage overlay. Turn off if games crash or go black with Deck mode on. Applies next session.", + host, "mangoapp", stringResource(R.string.mode_mangoapp), + stringResource(R.string.mode_mangoapp_hint), s.mangoapp, onChange = a.onMangoapp, ) // Deck mode fixes the branch (SessionPrefs.steamChannel); the choice is for Deck mode off. - if (s.steamDeckMode) SettingsRow("Client branch", "Steam Deck public beta: Deck mode needs it, so Steam doesn't keep offering an update") {} + if (s.steamDeckMode) SettingsRow(stringResource(R.string.mode_branch), stringResource(R.string.mode_branch_deck)) {} else ChoiceRow( - host, "channel", "Client branch", "The Steam client build the session forces. Applies at the next session start; the client may update itself once.", - listOf("publicbeta" to "Public beta", "steamdeck_publicbeta" to "Steam Deck public beta"), s.steamChannel, - note = "Public beta is what every session ran on before. Steam Deck public beta is the one Armada bootstraps from, and the one Deck mode always uses.", + host, "channel", stringResource(R.string.mode_branch), stringResource(R.string.mode_branch_hint), + listOf("publicbeta" to stringResource(R.string.mode_branch_public), "steamdeck_publicbeta" to stringResource(R.string.mode_branch_deck_beta)), s.steamChannel, + note = stringResource(R.string.mode_branch_note), onPick = a.onSteamChannel, ) } - if (steam && s.addedGamesDirs != null) SettingsGroup("Added games") { + if (steam && s.addedGamesDirs != null) SettingsGroup(stringResource(R.string.mode_added_games)) { for (dir in s.addedGamesDirs) { val n = s.addedGames.count { it.folderPath.startsWith("$dir/") } ActionRow( - dir.substringAfterLast('/').ifEmpty { dir }, dir + " · " + (if (n == 0) "no game folders with a .exe found" else "$n game${if (n == 1) "" else "s"}") + ". " + stringResource(R.string.added_games_forget_hint), - "Forget", onClick = { a.onForgetAddedGamesDir(dir) }, + dir.substringAfterLast('/').ifEmpty { dir }, dir + " · " + (if (n == 0) stringResource(R.string.mode_added_none) else pluralStringResource(R.plurals.mode_added_count, n, n)) + ". " + stringResource(R.string.added_games_forget_hint), + stringResource(R.string.common_forget), onClick = { a.onForgetAddedGamesDir(dir) }, ) } ActionRow( - if (s.addedGamesDirs.isEmpty()) "Games folder" else "Another games folder", + if (s.addedGamesDirs.isEmpty()) stringResource(R.string.mode_games_folder) else stringResource(R.string.mode_games_folder_another), stringResource(R.string.added_games_import_hint), - "Add…", onClick = a.onPickAddedGamesDir, + stringResource(R.string.common_add_ellipsis), onClick = a.onPickAddedGamesDir, ) ToggleRow( - host, "addedArt", "Artwork from Steam", - "A game with no art of its own gets the store's capsule, header, hero and logo for the same title, looked up by folder name. Your own art wins: drop cover.jpg (or poster, boxart, folder, the folder's name), header.jpg, hero.jpg, logo.png or icon.png into the game's folder or its art subfolder.", + host, "addedArt", stringResource(R.string.mode_added_art), + stringResource(R.string.mode_added_art_hint), s.addedGamesArt, onChange = a.onAddedGamesArt, ) for (g in s.addedGames) ChoiceRow( - host, "added:" + g.folderPath, g.folderName, "Launches ${g.exeName}" + (if (s.addedGamesDirs.size > 1) " · in " + g.folderPath.substringBeforeLast('/').substringAfterLast('/') else ""), - g.candidates + ("__pick__" to "Choose another file…"), g.exePath, - note = "The .exe files found in the game's folder; the one named after the folder, else the largest, is picked unless you choose.", + host, "added:" + g.folderPath, g.folderName, if (s.addedGamesDirs.size > 1) stringResource(R.string.mode_added_launches_in, g.exeName, g.folderPath.substringBeforeLast('/').substringAfterLast('/')) else stringResource(R.string.mode_added_launches, g.exeName), + g.candidates + ("__pick__" to stringResource(R.string.mode_added_choose)), g.exePath, + note = stringResource(R.string.mode_added_exe_note), onPick = { path -> if (path == "__pick__") a.onPickAddedGameExe(g.folderPath) else a.onAddedGameExe(g.folderPath, path) }, ) } @@ -359,44 +359,44 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { ) GameEnvironmentRow() if (s.forceFullscreen != null) ToggleRow( - host, "fill", "Stretch games to fill the screen", - "Keeps games that resize their own window (FlatOut) full screen. Turn it off if a game shows up small in a corner (Quake 3). Applies next session.", + host, "fill", stringResource(R.string.mode_fill), + stringResource(R.string.mode_fill_hint), s.forceFullscreen, onChange = a.onForceFullscreen, ) if (s.stretch16x9 != null) ToggleRow( - host, "stretch169", "Stretch 16:9 games to a narrower screen", - "On a screen squarer than 16:9 (a foldable's inner panel), games render at 16:9 and are stretched to fill it: no bars, slightly wider-looking. Applies next session.", + host, "stretch169", stringResource(R.string.mode_stretch169), + stringResource(R.string.mode_stretch169_hint), s.stretch16x9, onChange = a.onStretch16x9, ) } - if (steam && s.directAudio != null && s.mic != null) SettingsGroup("Audio") { - ToggleRow(host, "da", "DirectAudio for games", "Bypasses PulseAudio for lower latency in games.", s.directAudio, onChange = a.onDirectAudio) + if (steam && s.directAudio != null && s.mic != null) SettingsGroup(stringResource(R.string.mode_audio)) { + ToggleRow(host, "da", stringResource(R.string.mode_directaudio), stringResource(R.string.mode_directaudio_hint), s.directAudio, onChange = a.onDirectAudio) ChoiceRow( - host, "clientAudio", "Steam client audio", "Classic is the AAudio sink from 0.1.5. DirectAudio goes through the relay. Applies next session.", - listOf("classic" to "Classic", "directaudio" to "DirectAudio"), if (s.clientDirectAudio) "directaudio" else "classic", + host, "clientAudio", stringResource(R.string.mode_client_audio), stringResource(R.string.mode_client_audio_hint), + listOf("classic" to stringResource(R.string.mode_client_audio_classic), "directaudio" to stringResource(R.string.mode_client_audio_direct)), if (s.clientDirectAudio) "directaudio" else "classic", onPick = { id -> a.onClientDirectAudio(id == "directaudio") }, ) - ToggleRow(host, "mic", "Microphone", "Uses the device microphone for voice chat.", s.mic, onChange = a.onMic) + ToggleRow(host, "mic", stringResource(R.string.mode_mic), stringResource(R.string.mode_mic_hint), s.mic, onChange = a.onMic) } - if (steam && s.gameStorage != null) SettingsGroup("Game storage") { + if (steam && s.gameStorage != null) SettingsGroup(stringResource(R.string.mode_storage)) { val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.second == s.gameStorage } val options = buildList { - add("" to ("Automatic - the SD card when one is in" + (if (s.storageOptions.isEmpty()) " (none right now)" else ""))) - add("off" to "Internal only") + add("" to (if (s.storageOptions.isEmpty()) stringResource(R.string.mode_storage_auto_none) else stringResource(R.string.mode_storage_auto))) + add("off" to stringResource(R.string.mode_storage_internal)) for ((label, path) in s.storageOptions) add(path to label) - if (custom) add(s.gameStorage to "Folder: ${s.gameStorage}") + if (custom) add(s.gameStorage to stringResource(R.string.mode_storage_folder, s.gameStorage)) } val open = host.open == "storage" SettingsRow( - "Second library", + stringResource(R.string.mode_second_library), stringResource(R.string.second_library_import_hint), highlighted = open, ) { androidx.compose.foundation.layout.Box { ValueChip(options.firstOrNull { it.first == s.gameStorage }?.second?.substringBefore(" -") ?: "-", open) { host.open = if (open) null else "storage" } AnchoredMenu( - open, onDismiss = { if (host.open == "storage") host.open = null }, title = "Second library", - note = "Games that stream assets from SD or shared storage may stutter. Keep them internal.", + open, onDismiss = { if (host.open == "storage") host.open = null }, title = stringResource(R.string.mode_second_library), + note = stringResource(R.string.mode_storage_note), ) { firstItemFocus -> options.forEachIndexed { index, (path, label) -> MenuItem(label, checked = path == s.gameStorage, focusRequester = if (index == 0) firstItemFocus else null) { @@ -404,30 +404,28 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { host.open = null } } - MenuItem("Choose a folder…", checked = false) { host.open = null; a.onPickGameStorageFolder() } + MenuItem(stringResource(R.string.mode_choose_folder), checked = false) { host.open = null; a.onPickGameStorageFolder() } } } } } - if (!steam && s.renderer != null) SettingsGroup("Renderer") { + if (!steam && s.renderer != null) SettingsGroup(stringResource(R.string.mode_renderer)) { ChoiceRow( - host, "renderer", "Desktop renderer", "Composites the desktop.", - listOf("vulkan" to "vulkan - GPU", "gles2" to "gles2 - GPU (experimental)", "pixman" to "pixman - software"), s.renderer, - note = "On Vulkan, programs on the desktop draw with the GPU in their own windows; if it cannot start, " + - "the desktop comes up on pixman. On pixman, games and emulators from the menu open full screen " + - "on the GPU instead (right-click one for a desktop window).", + host, "renderer", stringResource(R.string.mode_desktop_renderer), stringResource(R.string.mode_renderer_hint), + listOf("vulkan" to stringResource(R.string.mode_renderer_vulkan), "gles2" to stringResource(R.string.mode_renderer_gles2), "pixman" to stringResource(R.string.mode_renderer_pixman)), s.renderer, + note = stringResource(R.string.mode_renderer_note), onPick = a.onRenderer, ) } } if (confirmDeckyRemoval) AlertDialog( onDismissRequest = { confirmDeckyRemoval = false }, - title = { Text("Uninstall Decky Loader?") }, - text = { Text("Remove the loader and keep your plugins and settings.") }, + title = { Text(stringResource(R.string.mode_decky_remove_title)) }, + text = { Text(stringResource(R.string.mode_decky_remove_text)) }, confirmButton = { - TextButton(onClick = { confirmDeckyRemoval = false; a.onDeckyUninstall() }) { Text("Uninstall") } + TextButton(onClick = { confirmDeckyRemoval = false; a.onDeckyUninstall() }) { Text(stringResource(R.string.common_uninstall)) } }, - dismissButton = { TextButton(onClick = { confirmDeckyRemoval = false }) { Text("Cancel") } }, + dismissButton = { TextButton(onClick = { confirmDeckyRemoval = false }) { Text(stringResource(R.string.common_cancel)) } }, ) } @@ -443,11 +441,11 @@ private fun CustomResolutionDialog(initial: Pair?, onSave: (Pair?, onSave: (Pair w = v.filter(Char::isDigit).take(4) }, label = { Text("Width") }, + w, { v -> w = v.filter(Char::isDigit).take(4) }, label = { Text(stringResource(R.string.mode_width)) }, singleLine = true, keyboardOptions = numbers, modifier = Modifier.weight(1f), ) Text("×", fontSize = 18.sp, modifier = Modifier.padding(horizontal = 10.dp)) androidx.compose.material3.OutlinedTextField( - h, { v -> h = v.filter(Char::isDigit).take(4) }, label = { Text("Height") }, + h, { v -> h = v.filter(Char::isDigit).take(4) }, label = { Text(stringResource(R.string.mode_height)) }, singleLine = true, keyboardOptions = numbers, modifier = Modifier.weight(1f), ) } @@ -477,12 +475,12 @@ private fun CustomResolutionDialog(initial: Pair?, onSave: (Pair @@ -161,7 +163,7 @@ fun PerformancePage( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(12.dp)).background(colors.error.copy(alpha = 0.08f)) .border(1.dp, colors.error.copy(alpha = 0.4f), RoundedCornerShape(12.dp)).padding(12.dp), ) { - Text("Android is set to kill this session", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.error) + Text(stringResource(R.string.perf_phantom_warning), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.error) Text(phantomWarning, fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp)) } } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index 7133aeaee..63a2196d6 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -123,7 +123,7 @@ import com.droiddeck.launcher.input.SecondScreenDisplay import com.droiddeck.launcher.input.SecondScreenMode import kotlinx.coroutines.flow.collect -private val drawerPageTitles = listOf("Display", "Controls", "Components", "Settings") +private val drawerPageTitles = listOf(R.string.drawer_page_display, R.string.drawer_page_controls, R.string.drawer_page_components, R.string.drawer_page_settings) /** One icon per drawer page, in page order (QAM-style tabs). */ private val drawerPageIcons = listOf(Icons.Outlined.DesktopWindows, Icons.Outlined.SportsEsports, Icons.Outlined.Layers, Icons.Outlined.Settings) @@ -276,7 +276,7 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha .padding(if (short) 12.dp else 16.dp), ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().heightIn(min = if (short) 40.dp else 44.dp)) { - Text(a.title ?: if (a.steam) "Steam" else "Desktop", fontSize = if (short) 18.sp else 20.sp, fontWeight = FontWeight.Bold, + Text(a.title ?: if (a.steam) stringResource(R.string.drawer_steam) else stringResource(R.string.drawer_desktop), fontSize = if (short) 18.sp else 20.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f)) StopSessionButton(modifier = focus.track(page, "stop").onGloballyPositioned { stopCoords = it }) { host.open = null; confirmStop = true } } @@ -296,13 +296,13 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(top = if (short) 8.dp else 12.dp), ) { - QuickAction("Steam menu", Icons.Outlined.Menu, Modifier.weight(1f).then(focus.track(page, "steam")), compact = short) { + QuickAction(stringResource(R.string.drawer_steam_menu), Icons.Outlined.Menu, Modifier.weight(1f).then(focus.track(page, "steam")), compact = short) { host.open = null a.onSteamMenu.invoke() } QuickAction( - "Quick access", Icons.Outlined.MoreHoriz, - Modifier.weight(1f).then(focus.track(page, "qam")).semantics { contentDescription = "Open Quick Access Menu" }, + stringResource(R.string.drawer_quick_access), Icons.Outlined.MoreHoriz, + stringResource(R.string.drawer_open_qam).let { d -> Modifier.weight(1f).then(focus.track(page, "qam")).semantics { contentDescription = d } }, compact = short, interactionSource = qamInteraction, onConfirm = { host.open = null; a.onQam.invoke() }, @@ -314,7 +314,7 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha // Big Picture's own "Switch to Desktop" waits on SteamOS Manager for ever // here; this does the switch without the client. if (a.onSwitchToDesktop != null) { - QuickAction("Desktop", Icons.Outlined.DesktopWindows, Modifier.weight(1f).then(focus.track(page, "desktop")), compact = short) { + QuickAction(stringResource(R.string.drawer_desktop), Icons.Outlined.DesktopWindows, Modifier.weight(1f).then(focus.track(page, "desktop")), compact = short) { host.open = null a.onSwitchToDesktop.invoke() } @@ -326,11 +326,11 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = if (short) 6.dp else 12.dp), ) { - DrawerBumper("LB", "Previous page", modifier = focus.track(page, "prev")) { + DrawerBumper("LB", stringResource(R.string.drawer_prev_page), modifier = focus.track(page, "prev")) { host.open = null; onPageChange((page + DRAWER_PAGES - 1) % DRAWER_PAGES) } DrawerPageTabs(page = page, compact = short, modifier = Modifier.weight(1f)) { index -> host.open = null; onPageChange(index) } - DrawerBumper("RB", "Next page", modifier = focus.track(page, "next")) { + DrawerBumper("RB", stringResource(R.string.drawer_next_page), modifier = focus.track(page, "next")) { host.open = null; onPageChange((page + 1) % DRAWER_PAGES) } } @@ -338,11 +338,11 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(pageScroll[page]), ) { CompositionLocalProvider(LocalChipMinWidth provides 120.dp) { when (page) { - 0 -> SettingsGroup("Display") { - ToggleRow(host, "hud", "Performance HUD", null, a.hudOn, + 0 -> SettingsGroup(stringResource(R.string.drawer_page_display)) { + ToggleRow(host, "hud", stringResource(R.string.drawer_hud), null, a.hudOn, chipModifier = focus.track(page, "hud"), onChange = a.onHud) if (a.fillScreen != null) ToggleRow( - host, "fill", "Stretch games to fill", null, a.fillScreen, + host, "fill", stringResource(R.string.drawer_fill), null, a.fillScreen, chipModifier = focus.track(page, "fill"), onChange = a.onFillScreen, ) val fgOpen = host.open == "fg" @@ -356,49 +356,49 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha } DRAWER_PAGE_COMPONENTS -> ComponentsDrawerPage(host, a) { key -> focus.track(page, key) } 1 -> { - SettingsGroup("Controls") { - ChoiceRow(host, "touch", "Touch", null, - listOf(SessionPrefs.TOUCH_AUTO to "Auto (${a.touchAuto})", SessionPrefs.TOUCH_PAD to "Touchpad", SessionPrefs.TOUCH_DIRECT to "Direct"), + SettingsGroup(stringResource(R.string.drawer_page_controls)) { + ChoiceRow(host, "touch", stringResource(R.string.mode_touch), null, + listOf(SessionPrefs.TOUCH_AUTO to stringResource(R.string.drawer_touch_auto, a.touchAuto), SessionPrefs.TOUCH_PAD to stringResource(R.string.mode_touch_touchpad), SessionPrefs.TOUCH_DIRECT to stringResource(R.string.mode_touch_direct)), a.touchMode, chipModifier = focus.track(page, "touch"), onPick = a.onTouch) - ChoiceRow(host, "osc", "On-screen controls", null, - if (a.steam) listOf(SessionPrefs.OSC_AUTO to "Auto", SessionPrefs.OSC_ALWAYS to "Always", SessionPrefs.OSC_STEAM_QAM to "Steam + QAM", SessionPrefs.OSC_NEVER to "Never") - else listOf(SessionPrefs.OSC_AUTO to "Auto", SessionPrefs.OSC_ALWAYS to "Always", SessionPrefs.OSC_NEVER to "Never"), + ChoiceRow(host, "osc", stringResource(R.string.mode_osc), null, + if (a.steam) listOf(SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto), SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always), SessionPrefs.OSC_STEAM_QAM to stringResource(R.string.mode_osc_qam), SessionPrefs.OSC_NEVER to stringResource(R.string.common_never)) + else listOf(SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto), SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always), SessionPrefs.OSC_NEVER to stringResource(R.string.common_never)), a.oscMode, chipModifier = focus.track(page, "osc"), onPick = a.onOsc) - if (a.steam) ChoiceRow(host, "back-actions", "Back", null, + if (a.steam) ChoiceRow(host, "back-actions", stringResource(R.string.mode_back), null, listOf(false to SessionPrefs.BACK_MENU_THEN_QAM, true to SessionPrefs.BACK_QAM_THEN_MENU), a.backActionsInverted, chipModifier = focus.track(page, "back-actions"), onPick = a.onBackActionsInverted) } - SettingsGroup("Keyboard") { + SettingsGroup(stringResource(R.string.drawer_keyboard)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(8.dp)) { - DrawerOutlineButton("PC keyboard", modifier = Modifier.weight(1f).height(48.dp).then(focus.track(page, "hardware"))) { + DrawerOutlineButton(stringResource(R.string.drawer_pc_keyboard), modifier = Modifier.weight(1f).height(48.dp).then(focus.track(page, "hardware"))) { host.open = null; a.onHardwareKeyboard() } - DrawerOutlineButton("Android", modifier = Modifier.weight(1f).height(48.dp).then(focus.track(page, "android"))) { + DrawerOutlineButton(stringResource(R.string.drawer_android_keyboard), modifier = Modifier.weight(1f).height(48.dp).then(focus.track(page, "android"))) { host.open = null; a.onKeyboard() } } } - if (a.steam && a.secondScreenDisplays.isNotEmpty()) SettingsGroup("Second screen") { - ChoiceRow(host, "second-screen-mode", "Shows", null, + if (a.steam && a.secondScreenDisplays.isNotEmpty()) SettingsGroup(stringResource(R.string.drawer_second_screen)) { + ChoiceRow(host, "second-screen-mode", stringResource(R.string.drawer_second_screen_shows), null, (listOf(SecondScreenMode.NONE, SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.TERMINAL) + (if (com.droiddeck.launcher.session.SessionState.deckPad) listOf(SecondScreenMode.DECK_CONTROLS) else emptyList())) .map { it to it.label }, a.secondScreenMode, chipModifier = focus.track(page, "second-screen-mode"), onPick = a.onSecondScreenMode) - if (a.secondScreenDisplays.size > 1) ChoiceRow(host, "second-screen-display", "Display", null, + if (a.secondScreenDisplays.size > 1) ChoiceRow(host, "second-screen-display", stringResource(R.string.drawer_page_display), null, a.secondScreenDisplays.map { it.id to it.label }, a.selectedSecondScreenDisplay, chipModifier = focus.track(page, "second-screen-display"), onPick = a.onSecondScreenDisplay) } } else -> { - if (a.isHomeApp) SettingsGroup("Android apps") { - SettingsRow("Launch an app", null) { - DrawerOutlineButton(if (androidAppsExpanded) "Hide" else "Show", modifier = focus.track(page, "apps")) { + if (a.isHomeApp) SettingsGroup(stringResource(R.string.drawer_android_apps)) { + SettingsRow(stringResource(R.string.drawer_launch_app), null) { + DrawerOutlineButton(if (androidAppsExpanded) stringResource(R.string.common_hide) else stringResource(R.string.common_show), modifier = focus.track(page, "apps")) { androidAppsExpanded = !androidAppsExpanded } } if (androidAppsExpanded) { if (a.androidApps.isEmpty()) { - Text("No launchable apps", fontSize = 12.sp, color = colors.onSurfaceVariant, + Text(stringResource(R.string.drawer_no_apps), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp)) } else for (app in a.androidApps) { MenuItem( @@ -418,23 +418,23 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha } } } - SettingsGroup("Session behavior") { + SettingsGroup(stringResource(R.string.drawer_session_behavior)) { ChoiceRow( - host, "suspend", "Background behavior", - "Applies now and to future sessions in this mode.", + host, "suspend", stringResource(R.string.mode_suspend), + stringResource(R.string.drawer_suspend_hint), listOf( - SessionPrefs.SUSPEND_AUTO to "Auto", - SessionPrefs.SUSPEND_MANUAL to "Manual", - SessionPrefs.SUSPEND_NEVER to "Never", + SessionPrefs.SUSPEND_AUTO to stringResource(R.string.common_auto), + SessionPrefs.SUSPEND_MANUAL to stringResource(R.string.mode_suspend_manual), + SessionPrefs.SUSPEND_NEVER to stringResource(R.string.common_never), ), a.suspendPolicy, - note = "Auto pauses in the background and resumes when visible. Manual pauses there and waits for Resume. Never keeps the session running.", + note = stringResource(R.string.mode_suspend_note), chipModifier = focus.track(page, "suspend"), onPick = a.onSuspendPolicy, ) } - SettingsGroup("Next session") { - ChoiceRow(host, "shape", "Screen ratio", null, + SettingsGroup(stringResource(R.string.drawer_next_session)) { + ChoiceRow(host, "shape", stringResource(R.string.mode_ratio), null, SessionPrefs.shapeChoices, a.shapeMode, chipModifier = focus.track(page, "shape"), onPick = a.onShape) } @@ -444,16 +444,16 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha chipModifier = focus.track(page, "fex"), onPick = a.onFexPreset) GameEnvironmentRow(modifier = focus.track(page, "game-env")) } - SettingsGroup("Support") { - SettingsRow("Session logs", "Send this session's logs with a bug report") { - DrawerOutlineButton("Share logs", modifier = focus.track(page, "share-logs")) { + SettingsGroup(stringResource(R.string.drawer_support)) { + SettingsRow(stringResource(R.string.drawer_logs), stringResource(R.string.drawer_logs_hint)) { + DrawerOutlineButton(stringResource(R.string.drawer_share_logs), modifier = focus.track(page, "share-logs")) { host.open = null a.onShareLogs() } } } Spacer(Modifier.height(18.dp)) - if (!a.isHomeApp) DrawerOutlineButton("Background", modifier = focus.track(page, "background")) { + if (!a.isHomeApp) DrawerOutlineButton(stringResource(R.string.drawer_background), modifier = focus.track(page, "background")) { host.open = null; a.onBackground() } } @@ -518,16 +518,17 @@ private fun StopSessionButton(modifier: Modifier = Modifier, onClick: () -> Unit val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value val fill by animateColorAsState(if (hot) colors.error.copy(alpha = 0.18f) else Color.Transparent, Motion.tw(220), label = "dangerFill") val shape = RoundedCornerShape(20.dp) + val stopDescription = stringResource(R.string.stop_session) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), - modifier = modifier.heightIn(min = 44.dp).semantics { contentDescription = "Stop session" } + modifier = modifier.heightIn(min = 44.dp).semantics { contentDescription = stopDescription } .clip(shape).background(fill).glideBorder(hot, shape, colors.error.copy(alpha = 0.9f), colors.error.copy(alpha = 0.55f)) .hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, onClick = onClick) .controllerConfirm(onClick = onClick) .padding(start = 12.dp, end = 14.dp), ) { Icon(Icons.Outlined.PowerSettingsNew, contentDescription = null, tint = colors.error, modifier = Modifier.size(18.dp)) - Text("Stop", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.error) + Text(stringResource(R.string.drawer_stop), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.error) } } @@ -622,6 +623,7 @@ private fun DrawerPageTabs(page: Int, modifier: Modifier = Modifier, compact: Bo val scale by animateFloatAsState(if (selected) 1f else DRAWER_TAB_SIDE_SCALE, motion, label = "tabScale") val alpha by animateFloatAsState(if (selected) 1f else DRAWER_TAB_SIDE_ALPHA, motion, label = "tabAlpha") val glow by animateFloatAsState(if (selected) 1f else 0f, motion, label = "tabGlow") + val pageDescription = stringResource(R.string.drawer_page_desc, stringResource(drawerPageTitles[index])) val tint by animateColorAsState(if (selected) colors.onBackground else colors.onSurfaceVariant, Motion.tw(340, easing = FastOutSlowInEasing), label = "tabTint") val select = { onSelect(index) } Box( @@ -637,7 +639,7 @@ private fun DrawerPageTabs(page: Int, modifier: Modifier = Modifier, compact: Bo ), ) .glideBorder(focused, RoundedCornerShape(16.dp), colors.onBackground) - .semantics { contentDescription = "${drawerPageTitles[index]} page" } + .semantics { contentDescription = pageDescription } .hoverable(source) .clickable(interactionSource = source, indication = LocalIndication.current, onClick = select) .controllerConfirm(onClick = select), @@ -663,8 +665,8 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin val colors = MaterialTheme.colorScheme val snap = a.components if (snap == null) { - SettingsGroup("Components") { - Text("Reading the Protons…", fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(14.dp)) + SettingsGroup(stringResource(R.string.drawer_page_components)) { + Text(stringResource(R.string.comp_reading), fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(14.dp)) } return } @@ -672,7 +674,7 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin var pick by rememberSaveable { mutableStateOf(null) } val view = snap.protons.firstOrNull { it.proton.id == pick } ?: running.firstOrNull() ?: snap.protons.firstOrNull() // Focusable, so the pad can climb from the Proton box up to it and the page scrolls it into view. - SettingsGroup("Running now") { + SettingsGroup(stringResource(R.string.drawer_running_now)) { val src = remember { MutableInteractionSource() } val hot = src.collectIsFocusedAsState().value val pal = LocalPalette.current @@ -683,8 +685,8 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin .padding(horizontal = 14.dp, vertical = 11.dp), ) { if (running.isEmpty()) { - Text("No game is running on a Proton", fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground) - Text("Changes apply the next time a game starts.", fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp)) + Text(stringResource(R.string.drawer_no_game), fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground) + Text(stringResource(R.string.comp_applies_long), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp)) } else for (r in running) { Text(r.proton.name, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis) for (comp in ComponentsManager.COMPONENTS) { @@ -697,28 +699,29 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin } } if (view == null) { - SettingsGroup("Components") { - Text("No Proton is installed yet.", fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(14.dp)) + SettingsGroup(stringResource(R.string.drawer_page_components)) { + Text(stringResource(R.string.drawer_no_proton), fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(14.dp)) } return } val p = view.proton - SettingsGroup("Components") { + SettingsGroup(stringResource(R.string.drawer_page_components)) { DrawerStackedChoice( - host, "cmp-proton", "Proton", + host, "cmp-proton", stringResource(R.string.comp_proton), listOfNotNull( p.version, - "game running".takeIf { view.inUseByGame }, - view.reappliedAt.takeIf { it > 0 }?.let { "re-applied at launch " + java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT).format(java.util.Date(it * 1000)) }, + stringResource(R.string.drawer_game_running).takeIf { view.inUseByGame }, + view.reappliedAt.takeIf { it > 0 }?.let { stringResource(R.string.drawer_reapplied, java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT).format(java.util.Date(it * 1000))) }, ).joinToString(" · "), snap.protons.map { it.proton.id to it.proton.name }, p.id, chipModifier = track("cmp-proton"), onPick = { pick = it }, ) + val originalLabel = stringResource(R.string.drawer_original) val build = ComponentsManager.safeName(p.version) for (comp in ComponentsManager.COMPONENTS) { val st = view.components.getValue(comp) val originals = view.originals.filter { it.comp == comp } - val options = originals.map { "orig:${it.protonVersion}" to (if (it.protonVersion == build) "Original" else "Original · ${it.protonVersion}") } + + val options = originals.map { "orig:${it.protonVersion}" to (if (it.protonVersion == build) originalLabel else stringResource(R.string.comp_original, it.protonVersion)) } + snap.packages.filter { it.comp == comp }.map { it.file to it.version } if (options.isEmpty()) continue val current = st.activeFile ?: "orig:$build" @@ -726,16 +729,16 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin DrawerStackedChoice( host, "cmp-$comp", ComponentsManager.LABEL.getValue(comp), // The box already shows the choice: only say more when there is more to say. - st.queued?.let { "Next: $it · after the game closes" } ?: st.inUse.takeIf { it != shown }, + st.queued?.let { stringResource(R.string.comp_next_queued, it) } ?: st.inUse.takeIf { it != shown }, options, current, - note = if (view.inUseByGame) "A game is running on this Proton: the change waits until it closes." else "Applies the next time a game starts.", + note = if (view.inUseByGame) stringResource(R.string.comp_waits_long) else stringResource(R.string.comp_applies_long), chipModifier = track("cmp-$comp"), onPick = { v -> if (v != current || st.queued != null) a.onComponentSwap(p.id, comp, v) }, ) } } Text( - "Downloads, importing and deleting are on the Components page in the app.", + stringResource(R.string.drawer_components_note), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 4.dp, vertical = 12.dp), ) } @@ -931,7 +934,7 @@ private fun StopConfirm( .padding(start = 12.dp, end = 14.dp), ) { Icon(Icons.Outlined.PowerSettingsNew, contentDescription = null, tint = colors.error, modifier = Modifier.size(18.dp)) - Text("Stop", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.error) + Text(stringResource(R.string.drawer_stop), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.error) } val maxW = with(density) { StepBoxMaxWidth.toPx() }.coerceAtMost(p.right) Column( @@ -953,7 +956,7 @@ private fun StopConfirm( .padding(start = 18.dp, end = 12.dp, top = 14.dp, bottom = 12.dp), ) { Text( - "Stop session?", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, + stringResource(R.string.drawer_stop_title), fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, modifier = Modifier.graphicsLayer { alpha = items[0].value; translationY = (1f - items[0].value) * 6.dp.toPx() }, ) Row( @@ -961,13 +964,13 @@ private fun StopConfirm( modifier = Modifier.fillMaxWidth().padding(top = 14.dp), ) { ConfirmChoice( - "Cancel", danger = false, enabled = open, + stringResource(R.string.common_cancel), danger = false, enabled = open, modifier = Modifier.graphicsLayer { alpha = items[1].value; translationY = (1f - items[1].value) * 6.dp.toPx() } .focusRequester(cancelFocus).onFocusChanged { cancelFocused = it.isFocused }, onClick = onCancel, ) ConfirmChoice( - "Stop", danger = true, enabled = open, + stringResource(R.string.drawer_stop), danger = true, enabled = open, modifier = Modifier.graphicsLayer { alpha = items[2].value; translationY = (1f - items[2].value) * 6.dp.toPx() } .onFocusChanged { stopFocused = it.isFocused } .onGloballyPositioned { stopCoords = it }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6edcf1ab..2202f8d7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,9 +41,9 @@ %1$s: engine failed %1$s: needs Lossless Scaling %1$s %2$s · starting - Choose Windows games with one subfolder per game. Steam checks ownership while signed in. Confirmed games use their Steam library entries after restarting Steam; other games stay as non-Steam shortcuts. Steam may verify or download files for imported games. - Adds a Steam library and scans Windows game folders here and in steamapps/common. After the ownership check and a Steam restart, owned games use Steam entries; other games use non-Steam shortcuts. - Forget removes non-Steam shortcuts next session. Manage Steam installations in Steam. Files stay on disk. + A folder with one subfolder per Windows game. Games you own on Steam use their Steam entries after Steam restarts; the rest become non-Steam shortcuts. + Adds a Steam library and scans it for Windows games. Owned games use their Steam entries; the rest become non-Steam shortcuts. + Forget removes the shortcuts next session. Files stay on disk. Game environment Shared defaults and per-game overrides for games using Proton. Applies on the next game launch. Edit variables @@ -190,4 +190,360 @@ Cancel Install the Linux runtime from Setup first. Wait for %1$s to finish. + Cancel + Auto + Never + Always + Uninstall + Check + Update + Checking… + Working… + Forget + Add… + Use + Applies next session. + Steam session + Desktop session + Display + Resolution + Up to 720p + Up to 900p + Up to 1080p + Native + %1$s (default) + Custom · %1$d×%2$d + Custom… + 720p can make menus more responsive. + Screen ratio + Set by the custom resolution. + Auto uses at least 16:9. + Frame limit + Caps the whole session. The display switches to a refresh rate the cap divides evenly. + HDR + HDR10 output + Not available: %1$s. + Restart the app to apply. + Drivers + GPU drivers + Shared by Steam and Desktop. Set on the Components page. + Touch & controls + Touch + Touchpad + Direct + Auto: touchpad on Desktop, direct in Steam. + On-screen controls + Steam + QAM + Auto shows the controls when no controller is connected. + Controller + How Steam sees your controller. Applies next session. + Steam Deck controller + Xbox 360 controller + Deck: Quick Access button and gyro. Xbox 360: open Quick Access with Guide+A. + Back + Session + Background behavior + When the app is hidden or the screen turns off. + Manual + Auto pauses in the background and resumes when shown. Manual waits for Resume. Never keeps running. + Startup + Start Steam with DroidDeck + Opens the Steam session when DroidDeck launches. + Decky + Checking for releases… + Ready to install %1$s. + No compatible build found. Try again later. + Installed · %1$s + Update available · %1$s + Up to date · %1$s + Loader + Install latest + Starts with Steam. While on, other apps on this device may be able to control Steam. + Off. Steam\'s remote debugging port stays closed. + Uninstall Decky Loader? + Removes the loader. Your plugins and settings are kept. + Client + Steam Deck mode + Steam\'s Deck interface and Quick Access performance controls. Applies next session. + Performance overlay + Turn off if games crash or go black in Deck mode. Applies next session. + Client branch + Steam Deck public beta (required by Deck mode). + Applies next session. Steam may update itself once. + Public beta + Steam Deck public beta + Deck mode always uses Steam Deck public beta. + Added games + no game folders with a .exe found + Games folder + Another games folder + Artwork from Steam + Fills in missing art by folder name. Your own cover.jpg, hero.jpg, logo.png or icon.png in the game folder takes priority. + Launches %1$s + Launches %1$s · in %2$s + Choose another file… + Defaults to the .exe named after the folder, or else the largest. + Stretch games to fill the screen + Keeps self-resizing games full screen. Turn off if a game shows up small. Applies next session. + Stretch 16:9 games + On screens squarer than 16:9, stretches games to fill instead of showing bars. Applies next session. + Audio + DirectAudio for games + Bypasses PulseAudio for lower latency. + Steam client audio + Applies next session. + Classic + DirectAudio + Microphone + For voice chat. + Game storage + Automatic - the SD card when present + Automatic - the SD card when present (none now) + Internal only + Folder: %1$s + Second library + Games that stream assets from SD or shared storage may stutter. + Choose a folder… + Renderer + Desktop renderer + Draws the desktop. + vulkan - GPU + gles2 - GPU (experimental) + pixman - software + Vulkan runs desktop programs on the GPU, falling back to pixman if it fails. On pixman, games open full screen on the GPU. + Custom resolution + Replaces the cap and ratio. A size that doesn\'t match the screen gets bars. + Width + Height + Between 320×240 and 3840×2160. + + %1$d game + %1$d games + + OK + Delete + Restore + Download + Delete %1$s + %1$.1f MB + Runtime driver + Used by Steam, games and the desktop. Applies next session. + Import Turnip zip… + Display driver + Draws frames to the screen in both modes. Restart the app to apply. + Import an AdrenoTools zip… + never + Nightlies checked %1$s + Components + About Components + Check for new drivers + Check the Nightlies for new packages + Proton + Steam replaces Valve\'s Proton when it updates; originals are kept per build. + %1$s · %2$d swapped + %1$s · all original + %1$s · game running + GPU drivers + %1$s · re-applied at launch %2$s + Next: %1$s · after the game closes + %1$s in use: %2$s + Waits until the game closes + Applies the next time a game starts + Import .zip + Reading Protons… + No Proton installed yet. Start Steam once to download its ARM64 Proton, or add GE-Proton or CachyOS from Setup. + Original · %1$s + %1$s · this build + %1$s · earlier build + Installed · %1$d + Nothing stored for %1$s yet. This Proton\'s own files are saved as its original. + A game is running on this Proton, so this waits until it closes. + Applies the next time a game starts. + Swap %1$s? + Put %1$s into %2$s? Its original files stay saved. %3$s + Restore %1$s? + Put the %1$s original back into %2$s? %3$s + Removes the package from the app. You can download it again. + Delete this original? + This %1$s original is from an earlier build of %2$s. The current build\'s original is always kept. + On the Nightlies · %1$d + Refresh to list packages on the Nightlies. + Nothing new since the last check. + Import .wcp + Restore this Proton\'s original + Put %1$s\'s original %2$s back? + Swap + FEX, DXVK and VKD3D-Proton for each Proton that Steam runs games with.\n\nSelect an installed row to swap it in. It applies the next time a game starts, or after the running game closes.\n\nYour choices are rechecked before every launch and put back if anything changed them. Each Proton\'s original files are kept, so Steam updates never lose them. + Original + In use + New + Stored + Performance + CPU cores, session fixes and the host name. Applies next session. + Steam client cores + Override Steam\'s core choice + Pins Steam, its UI helper and gamescope to the cores below. + Client cores + Cores for Steam. + Turn on the override to choose. + Game cores + Game cores + Cores for games. With all selected, Android schedules freely. + Client interface + Threaded GL + May make Steam menus more responsive. + Zink: lazy descriptors + Recommended for drivers without descriptor buffers. + Skip GL error checks + Turns off per-call GL validation. + gamescope: realtime GPU queue + Prioritizes the compositor\'s GPU work. Can smooth frame pacing at some cost to games. + GPU + Hold the GPU at its top clock + Adreno only. Fewer hitches, more battery use and heat. + Session fixes + Turnip: sysmem rendering + Faster for Steam and most games. Required on Adreno 710/720/722. + Skip Steam\'s xalia helper + Disables Proton\'s gamepad navigation helper, which costs CPU in every game. + Run proot without seccomp + May fix missing-syscall errors on some kernels. Can reduce performance. + proot fast path + Needs proot\'s seccomp filter. + Faster file lookups and less stutter. Turn off if a program can\'t find its files. + Session identity + Host name + The name Steam sees for this device. Blank restores %1$s. + Letters, digits and inner hyphens only, up to 63 characters. + Android is set to kill this session + Installed + Available to download + Nothing new since the last check. + Restore built-in drivers + Built into the app: this removes its unpacked files and hides it. Restore built-in drivers brings it back. + Half of an Android + Linux bundle: both halves are removed. A driver in use falls back to the default. + Its files are removed from the app. + Its files are removed. It\'s in use, so the default takes over. + Display + Controls + Components + Settings + %1$s page + Steam + Desktop + Steam menu + Quick access + Open Quick Access Menu + Previous page + Next page + Performance HUD + Stretch games to fill + Auto (%1$s) + Keyboard + PC keyboard + Android + Second screen + Shows + Android apps + Launch an app + Hide + Show + No launchable apps + Session behavior + Applies now and to future sessions in this mode. + Next session + Support + Session logs + Attach to a bug report + Share logs + Background + Stop + Stop session? + Running now + No game is running on a Proton + No Proton is installed yet. + game running + re-applied at launch %1$s + Original + Download, import and delete on the Components page. + Done + Needs attention + Working + Check the Wireless debugging IP address and port, or pair again if Android removed this device. + Install + Manage + Overview + Controller + Session + Launcher + Setup + ● All set + %1$d of %2$d ready + System check + Device supported + Untested GPU + GPU not supported + %1$s · %2$s + %1$s: %2$s + Steam needs an Adreno driver; %1$s may show a black screen + Linux runtime + %1$s · %2$d%% + Not installed · about 3 GB, installed on first Play + Installed + %1$s · update available + %1$s · up to date + Child-process limit + On · Android may close Steam. Turning it off takes a minute. + Not set · Android may close Steam. Turning it off takes a minute. + Couldn\'t be checked · turning it off takes a minute. + Fix it + Details + Developer options + Use Wireless debugging + Check again + Turn limit on + Steam account + Signed in as %1$s · offline mode + Signed in as %1$s + Press Play and sign in to Steam + Tools + Controller + Controller settings are unavailable. + Session + Session logs + Saved after each session + Latest session logs + Saved session logs + Keeps the newest %1$d + Clear logs + Offline mode + Sign in to Steam first + Launcher + Theme + Launcher appearance + Use as Home screen + DroidDeck can be the Home app + DroidDeck is never offered as a Home app + Fullscreen + Hides the Android status and navigation bars + Shows the Android status and navigation bars + Default Home app + Choose a Home app + Choose + Linux apps (beta) + Flathub Store + In the menu. Some apps may not start; logs are in Download/DroidDeck. + Install Linux apps and games from Flathub + Files + Browse and manage files + Proton versions + Install ARM64 Proton builds + Performance + CPU core assignment + ROMs folder + Where emulator games are stored + Components + FEX, DXVK, VKD3D + Controls + Button mapping