mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
ui: move settings, components, drawer and setup copy to string resources, tighten wording
This commit is contained in:
1 parent
71610ddfdb
commit
0fa1f2f868
7 files changed
+714
-342
No files matched your search
@@ -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<String?>(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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<String?>(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)) {
|
||||
|
||||
@@ -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<Int, Int>?, onSave: (Pair<Int,
|
||||
val parsed = SessionPrefs.parseResolution("${w}x$h")
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Custom resolution") },
|
||||
title = { Text(stringResource(R.string.mode_custom_res_title)) },
|
||||
text = {
|
||||
androidx.compose.foundation.layout.Column {
|
||||
Text(
|
||||
"The session's display size. It replaces the cap and the shape; a size that does not match the panel's shape gets bars.",
|
||||
stringResource(R.string.mode_custom_res_text),
|
||||
fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
androidx.compose.foundation.layout.Row(
|
||||
@@ -456,12 +454,12 @@ private fun CustomResolutionDialog(initial: Pair<Int, Int>?, onSave: (Pair<Int,
|
||||
) {
|
||||
val numbers = androidx.compose.foundation.text.KeyboardOptions(keyboardType = androidx.compose.ui.text.input.KeyboardType.Number)
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
w, { v -> 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<Int, Int>?, onSave: (Pair<Int,
|
||||
}
|
||||
}
|
||||
if (parsed == null && (w.isNotEmpty() || h.isNotEmpty())) Text(
|
||||
"Between 320×240 and 3840×2160.", fontSize = 12.sp, color = MaterialTheme.colorScheme.error,
|
||||
stringResource(R.string.mode_custom_res_range), fontSize = 12.sp, color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = { androidx.compose.material3.TextButton(enabled = parsed != null, onClick = { parsed?.let(onSave) }) { Text("Use") } },
|
||||
dismissButton = { androidx.compose.material3.TextButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
confirmButton = { androidx.compose.material3.TextButton(enabled = parsed != null, onClick = { parsed?.let(onSave) }) { Text(stringResource(R.string.common_use)) } },
|
||||
dismissButton = { androidx.compose.material3.TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } },
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import com.droiddeck.launcher.R
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -62,87 +64,87 @@ fun PerformancePage(
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val coreItems = cores.map { it.core to it.label }
|
||||
SettingsPage(
|
||||
host, title = "Performance",
|
||||
lede = "CPU cores, session fixes and the host name. Applies next session.",
|
||||
host, title = stringResource(R.string.perf_title),
|
||||
lede = stringResource(R.string.perf_lede),
|
||||
onBack = onDismiss,
|
||||
) {
|
||||
SettingsGroup("Steam client cores") {
|
||||
SettingsGroup(stringResource(R.string.perf_client_cores_group)) {
|
||||
ToggleRow(
|
||||
host, "override", "Override Steam's own core choice",
|
||||
"Pins Steam, its UI helper, and gamescope to the cores below. Reapplied every few seconds.",
|
||||
host, "override", stringResource(R.string.perf_override),
|
||||
stringResource(R.string.perf_override_hint),
|
||||
clientOverride, onChange = onClientOverride,
|
||||
)
|
||||
MultiRow(
|
||||
host, "clientCores", "Client cores", if (clientOverride) "Choose cores for Steam." else "Enable the override to choose.",
|
||||
host, "clientCores", stringResource(R.string.perf_client_cores), if (clientOverride) stringResource(R.string.perf_client_cores_on) else stringResource(R.string.perf_client_cores_off),
|
||||
coreItems, clientCores, enabled = clientOverride, onToggle = onClientCore,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Game cores") {
|
||||
SettingsGroup(stringResource(R.string.perf_game_cores_group)) {
|
||||
MultiRow(
|
||||
host, "gameCores", "Game cores",
|
||||
"Choose cores for games. All selected lets Android schedule across every core.",
|
||||
host, "gameCores", stringResource(R.string.perf_game_cores),
|
||||
stringResource(R.string.perf_game_cores_hint),
|
||||
coreItems, gameCores, onToggle = onGameCore,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Client interface") {
|
||||
SettingsGroup(stringResource(R.string.perf_client_ui)) {
|
||||
ToggleRow(
|
||||
host, "glthread", "Threaded GL",
|
||||
"May improve Steam menu responsiveness. Device impact is unverified.",
|
||||
host, "glthread", stringResource(R.string.perf_glthread),
|
||||
stringResource(R.string.perf_glthread_hint),
|
||||
glThread, onChange = onGlThread,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "zink", "Zink: lazy descriptors",
|
||||
"Lazy, compact descriptors. Recommended for drivers without descriptor buffers.",
|
||||
host, "zink", stringResource(R.string.perf_zink),
|
||||
stringResource(R.string.perf_zink_hint),
|
||||
zinkLazy, onChange = onZinkLazy,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "noglerror", "Skip GL error checks",
|
||||
"Disables per-call GL validation.",
|
||||
host, "noglerror", stringResource(R.string.perf_noglerror),
|
||||
stringResource(R.string.perf_noglerror_hint),
|
||||
noGlError, onChange = onNoGlError,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "gsrealtime", "gamescope: realtime GPU queue",
|
||||
"Gives the compositor's GPU work priority over the game's. Can smooth frame pacing, but can cost games GPU time.",
|
||||
host, "gsrealtime", stringResource(R.string.perf_gsrealtime),
|
||||
stringResource(R.string.perf_gsrealtime_hint),
|
||||
gamescopeRealtime, onChange = onGamescopeRealtime,
|
||||
)
|
||||
}
|
||||
SettingsGroup("GPU") {
|
||||
SettingsGroup(stringResource(R.string.perf_gpu)) {
|
||||
ToggleRow(
|
||||
host, "gpuclock", "Hold the GPU at its top clock",
|
||||
"Adreno only. Fewer hitches from the clock ramping up, at the cost of battery and heat. Released when the session ends.",
|
||||
host, "gpuclock", stringResource(R.string.perf_gpuclock),
|
||||
stringResource(R.string.perf_gpuclock_hint),
|
||||
gpuClockPin, onChange = onGpuClockPin,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Session fixes") {
|
||||
SettingsGroup(stringResource(R.string.perf_fixes)) {
|
||||
ToggleRow(
|
||||
host, "sysmem", "Turnip: sysmem rendering",
|
||||
"On by default: faster for the Steam interface and most games. Required on Adreno 710/720/722.",
|
||||
host, "sysmem", stringResource(R.string.perf_sysmem),
|
||||
stringResource(R.string.perf_sysmem_hint),
|
||||
tuSysmem, onChange = onTuSysmem,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "xalia", "Skip Steam's xalia helper",
|
||||
"Disables Proton's gamepad navigation helper, which costs every game CPU time. Turn off only if a game needs it.",
|
||||
host, "xalia", stringResource(R.string.perf_xalia),
|
||||
stringResource(R.string.perf_xalia_hint),
|
||||
noXalia, onChange = onNoXalia,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "seccomp", "Run proot without seccomp",
|
||||
"May fix missing syscall errors on some kernels, but can reduce performance.",
|
||||
host, "seccomp", stringResource(R.string.perf_seccomp),
|
||||
stringResource(R.string.perf_seccomp_hint),
|
||||
prootNoSeccomp, onChange = onProotNoSeccomp,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "fastpath", "proot fast path",
|
||||
if (prootNoSeccomp) "Needs proot's seccomp filter: off while proot runs without seccomp."
|
||||
else "Answers file lookups inside each program instead of through proot. Faster loading and less stutter; turn off if a program cannot find its files.",
|
||||
host, "fastpath", stringResource(R.string.perf_fastpath),
|
||||
if (prootNoSeccomp) stringResource(R.string.perf_fastpath_needs_seccomp)
|
||||
else stringResource(R.string.perf_fastpath_hint),
|
||||
prootFastPath && !prootNoSeccomp, enabled = !prootNoSeccomp, onChange = onProotFastPath,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Session identity") {
|
||||
SettingsGroup(stringResource(R.string.perf_identity)) {
|
||||
var draft by remember(guestHostname) { mutableStateOf(guestHostname) }
|
||||
val valid = SessionPrefs.validGuestHostname(draft) != null
|
||||
SettingsRow(
|
||||
"Host name",
|
||||
if (valid || draft.isBlank()) "What the session and Steam report as this machine's name. Blank restores ${SessionPrefs.DEFAULT_GUEST_HOSTNAME}."
|
||||
else "Letters, digits and inner hyphens only, up to 63 characters. Not saved until it is valid.",
|
||||
stringResource(R.string.perf_hostname),
|
||||
if (valid || draft.isBlank()) stringResource(R.string.perf_hostname_hint, SessionPrefs.DEFAULT_GUEST_HOSTNAME)
|
||||
else stringResource(R.string.perf_hostname_invalid),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
draft, { v ->
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String?>(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 },
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
<string name="hud_fg_failed">%1$s: engine failed</string>
|
||||
<string name="hud_fg_needs_lossless">%1$s: needs Lossless Scaling</string>
|
||||
<string name="hud_fg_starting">%1$s %2$s · starting</string>
|
||||
<string name="added_games_import_hint">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.</string>
|
||||
<string name="second_library_import_hint">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.</string>
|
||||
<string name="added_games_forget_hint">Forget removes non-Steam shortcuts next session. Manage Steam installations in Steam. Files stay on disk.</string>
|
||||
<string name="added_games_import_hint">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.</string>
|
||||
<string name="second_library_import_hint">Adds a Steam library and scans it for Windows games. Owned games use their Steam entries; the rest become non-Steam shortcuts.</string>
|
||||
<string name="added_games_forget_hint">Forget removes the shortcuts next session. Files stay on disk.</string>
|
||||
<string name="game_env_title">Game environment</string>
|
||||
<string name="game_env_hint">Shared defaults and per-game overrides for games using Proton. Applies on the next game launch.</string>
|
||||
<string name="game_env_edit">Edit variables</string>
|
||||
@@ -190,4 +190,360 @@
|
||||
<string name="add_app_cancel">Cancel</string>
|
||||
<string name="add_app_runtime_required">Install the Linux runtime from Setup first.</string>
|
||||
<string name="add_app_busy">Wait for %1$s to finish.</string>
|
||||
<string name="common_cancel">Cancel</string>
|
||||
<string name="common_auto">Auto</string>
|
||||
<string name="common_never">Never</string>
|
||||
<string name="common_always">Always</string>
|
||||
<string name="common_uninstall">Uninstall</string>
|
||||
<string name="common_check">Check</string>
|
||||
<string name="common_update">Update</string>
|
||||
<string name="common_checking">Checking…</string>
|
||||
<string name="common_working">Working…</string>
|
||||
<string name="common_forget">Forget</string>
|
||||
<string name="common_add_ellipsis">Add…</string>
|
||||
<string name="common_use">Use</string>
|
||||
<string name="common_applies_next_session">Applies next session.</string>
|
||||
<string name="mode_steam_title">Steam session</string>
|
||||
<string name="mode_desktop_title">Desktop session</string>
|
||||
<string name="mode_display">Display</string>
|
||||
<string name="mode_resolution">Resolution</string>
|
||||
<string name="mode_res_720">Up to 720p</string>
|
||||
<string name="mode_res_900">Up to 900p</string>
|
||||
<string name="mode_res_1080">Up to 1080p</string>
|
||||
<string name="mode_res_native">Native</string>
|
||||
<string name="mode_res_default">%1$s (default)</string>
|
||||
<string name="mode_res_custom_value">Custom · %1$d×%2$d</string>
|
||||
<string name="mode_res_custom">Custom…</string>
|
||||
<string name="mode_res_note">720p can make menus more responsive.</string>
|
||||
<string name="mode_ratio">Screen ratio</string>
|
||||
<string name="mode_ratio_custom">Set by the custom resolution.</string>
|
||||
<string name="mode_ratio_auto">Auto uses at least 16:9.</string>
|
||||
<string name="mode_fps">Frame limit</string>
|
||||
<string name="mode_fps_note">Caps the whole session. The display switches to a refresh rate the cap divides evenly.</string>
|
||||
<string name="mode_hdr">HDR</string>
|
||||
<string name="mode_hdr10">HDR10 output</string>
|
||||
<string name="mode_hdr_unavailable">Not available: %1$s.</string>
|
||||
<string name="mode_hdr_restart">Restart the app to apply.</string>
|
||||
<string name="mode_drivers">Drivers</string>
|
||||
<string name="mode_gpu_drivers">GPU drivers</string>
|
||||
<string name="mode_gpu_drivers_hint">Shared by Steam and Desktop. Set on the Components page.</string>
|
||||
<string name="mode_touch_controls">Touch & controls</string>
|
||||
<string name="mode_touch">Touch</string>
|
||||
<string name="mode_touch_touchpad">Touchpad</string>
|
||||
<string name="mode_touch_direct">Direct</string>
|
||||
<string name="mode_touch_note">Auto: touchpad on Desktop, direct in Steam.</string>
|
||||
<string name="mode_osc">On-screen controls</string>
|
||||
<string name="mode_osc_qam">Steam + QAM</string>
|
||||
<string name="mode_osc_note">Auto shows the controls when no controller is connected.</string>
|
||||
<string name="mode_controller">Controller</string>
|
||||
<string name="mode_controller_hint">How Steam sees your controller. Applies next session.</string>
|
||||
<string name="mode_controller_deck">Steam Deck controller</string>
|
||||
<string name="mode_controller_x360">Xbox 360 controller</string>
|
||||
<string name="mode_controller_note">Deck: Quick Access button and gyro. Xbox 360: open Quick Access with Guide+A.</string>
|
||||
<string name="mode_back">Back</string>
|
||||
<string name="mode_session">Session</string>
|
||||
<string name="mode_suspend">Background behavior</string>
|
||||
<string name="mode_suspend_hint">When the app is hidden or the screen turns off.</string>
|
||||
<string name="mode_suspend_manual">Manual</string>
|
||||
<string name="mode_suspend_note">Auto pauses in the background and resumes when shown. Manual waits for Resume. Never keeps running.</string>
|
||||
<string name="mode_startup">Startup</string>
|
||||
<string name="mode_steam_startup">Start Steam with DroidDeck</string>
|
||||
<string name="mode_steam_startup_hint">Opens the Steam session when DroidDeck launches.</string>
|
||||
<string name="mode_decky">Decky</string>
|
||||
<string name="mode_decky_checking">Checking for releases…</string>
|
||||
<string name="mode_decky_ready">Ready to install %1$s.</string>
|
||||
<string name="mode_decky_none">No compatible build found. Try again later.</string>
|
||||
<string name="mode_decky_installed">Installed · %1$s</string>
|
||||
<string name="mode_decky_update">Update available · %1$s</string>
|
||||
<string name="mode_decky_current">Up to date · %1$s</string>
|
||||
<string name="mode_decky_loader">Loader</string>
|
||||
<string name="mode_decky_install_latest">Install latest</string>
|
||||
<string name="mode_decky_on">Starts with Steam. While on, other apps on this device may be able to control Steam.</string>
|
||||
<string name="mode_decky_off">Off. Steam\'s remote debugging port stays closed.</string>
|
||||
<string name="mode_decky_remove_title">Uninstall Decky Loader?</string>
|
||||
<string name="mode_decky_remove_text">Removes the loader. Your plugins and settings are kept.</string>
|
||||
<string name="mode_client">Client</string>
|
||||
<string name="mode_deck_mode">Steam Deck mode</string>
|
||||
<string name="mode_deck_mode_hint">Steam\'s Deck interface and Quick Access performance controls. Applies next session.</string>
|
||||
<string name="mode_mangoapp">Performance overlay</string>
|
||||
<string name="mode_mangoapp_hint">Turn off if games crash or go black in Deck mode. Applies next session.</string>
|
||||
<string name="mode_branch">Client branch</string>
|
||||
<string name="mode_branch_deck">Steam Deck public beta (required by Deck mode).</string>
|
||||
<string name="mode_branch_hint">Applies next session. Steam may update itself once.</string>
|
||||
<string name="mode_branch_public">Public beta</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deck public beta</string>
|
||||
<string name="mode_branch_note">Deck mode always uses Steam Deck public beta.</string>
|
||||
<string name="mode_added_games">Added games</string>
|
||||
<string name="mode_added_none">no game folders with a .exe found</string>
|
||||
<string name="mode_games_folder">Games folder</string>
|
||||
<string name="mode_games_folder_another">Another games folder</string>
|
||||
<string name="mode_added_art">Artwork from Steam</string>
|
||||
<string name="mode_added_art_hint">Fills in missing art by folder name. Your own cover.jpg, hero.jpg, logo.png or icon.png in the game folder takes priority.</string>
|
||||
<string name="mode_added_launches">Launches %1$s</string>
|
||||
<string name="mode_added_launches_in">Launches %1$s · in %2$s</string>
|
||||
<string name="mode_added_choose">Choose another file…</string>
|
||||
<string name="mode_added_exe_note">Defaults to the .exe named after the folder, or else the largest.</string>
|
||||
<string name="mode_fill">Stretch games to fill the screen</string>
|
||||
<string name="mode_fill_hint">Keeps self-resizing games full screen. Turn off if a game shows up small. Applies next session.</string>
|
||||
<string name="mode_stretch169">Stretch 16:9 games</string>
|
||||
<string name="mode_stretch169_hint">On screens squarer than 16:9, stretches games to fill instead of showing bars. Applies next session.</string>
|
||||
<string name="mode_audio">Audio</string>
|
||||
<string name="mode_directaudio">DirectAudio for games</string>
|
||||
<string name="mode_directaudio_hint">Bypasses PulseAudio for lower latency.</string>
|
||||
<string name="mode_client_audio">Steam client audio</string>
|
||||
<string name="mode_client_audio_hint">Applies next session.</string>
|
||||
<string name="mode_client_audio_classic">Classic</string>
|
||||
<string name="mode_client_audio_direct">DirectAudio</string>
|
||||
<string name="mode_mic">Microphone</string>
|
||||
<string name="mode_mic_hint">For voice chat.</string>
|
||||
<string name="mode_storage">Game storage</string>
|
||||
<string name="mode_storage_auto">Automatic - the SD card when present</string>
|
||||
<string name="mode_storage_auto_none">Automatic - the SD card when present (none now)</string>
|
||||
<string name="mode_storage_internal">Internal only</string>
|
||||
<string name="mode_storage_folder">Folder: %1$s</string>
|
||||
<string name="mode_second_library">Second library</string>
|
||||
<string name="mode_storage_note">Games that stream assets from SD or shared storage may stutter.</string>
|
||||
<string name="mode_choose_folder">Choose a folder…</string>
|
||||
<string name="mode_renderer">Renderer</string>
|
||||
<string name="mode_desktop_renderer">Desktop renderer</string>
|
||||
<string name="mode_renderer_hint">Draws the desktop.</string>
|
||||
<string name="mode_renderer_vulkan">vulkan - GPU</string>
|
||||
<string name="mode_renderer_gles2">gles2 - GPU (experimental)</string>
|
||||
<string name="mode_renderer_pixman">pixman - software</string>
|
||||
<string name="mode_renderer_note">Vulkan runs desktop programs on the GPU, falling back to pixman if it fails. On pixman, games open full screen on the GPU.</string>
|
||||
<string name="mode_custom_res_title">Custom resolution</string>
|
||||
<string name="mode_custom_res_text">Replaces the cap and ratio. A size that doesn\'t match the screen gets bars.</string>
|
||||
<string name="mode_width">Width</string>
|
||||
<string name="mode_height">Height</string>
|
||||
<string name="mode_custom_res_range">Between 320×240 and 3840×2160.</string>
|
||||
<plurals name="mode_added_count">
|
||||
<item quantity="one">%1$d game</item>
|
||||
<item quantity="other">%1$d games</item>
|
||||
</plurals>
|
||||
<string name="common_ok">OK</string>
|
||||
<string name="common_delete">Delete</string>
|
||||
<string name="common_restore">Restore</string>
|
||||
<string name="common_download">Download</string>
|
||||
<string name="common_delete_named">Delete %1$s</string>
|
||||
<string name="common_size_mb">%1$.1f MB</string>
|
||||
<string name="comp_runtime_driver">Runtime driver</string>
|
||||
<string name="comp_runtime_driver_hint">Used by Steam, games and the desktop. Applies next session.</string>
|
||||
<string name="comp_import_turnip">Import Turnip zip…</string>
|
||||
<string name="comp_display_driver">Display driver</string>
|
||||
<string name="comp_display_driver_hint">Draws frames to the screen in both modes. Restart the app to apply.</string>
|
||||
<string name="comp_import_adrenotools">Import an AdrenoTools zip…</string>
|
||||
<string name="comp_never">never</string>
|
||||
<string name="comp_nightlies_checked">Nightlies checked %1$s</string>
|
||||
<string name="comp_title">Components</string>
|
||||
<string name="comp_about">About Components</string>
|
||||
<string name="comp_check_drivers">Check for new drivers</string>
|
||||
<string name="comp_check_nightlies">Check the Nightlies for new packages</string>
|
||||
<string name="comp_proton">Proton</string>
|
||||
<string name="comp_proton_note">Steam replaces Valve\'s Proton when it updates; originals are kept per build.</string>
|
||||
<string name="comp_swapped">%1$s · %2$d swapped</string>
|
||||
<string name="comp_all_original">%1$s · all original</string>
|
||||
<string name="comp_game_running_suffix">%1$s · game running</string>
|
||||
<string name="comp_gpu_drivers">GPU drivers</string>
|
||||
<string name="comp_reapplied">%1$s · re-applied at launch %2$s</string>
|
||||
<string name="comp_next_queued">Next: %1$s · after the game closes</string>
|
||||
<string name="comp_in_use">%1$s in use: %2$s</string>
|
||||
<string name="comp_waits">Waits until the game closes</string>
|
||||
<string name="comp_applies_next_game">Applies the next time a game starts</string>
|
||||
<string name="comp_import_zip">Import .zip</string>
|
||||
<string name="comp_reading">Reading Protons…</string>
|
||||
<string name="comp_no_proton">No Proton installed yet. Start Steam once to download its ARM64 Proton, or add GE-Proton or CachyOS from Setup.</string>
|
||||
<string name="comp_original">Original · %1$s</string>
|
||||
<string name="comp_this_build">%1$s · this build</string>
|
||||
<string name="comp_earlier_build">%1$s · earlier build</string>
|
||||
<string name="comp_installed_count">Installed · %1$d</string>
|
||||
<string name="comp_nothing_stored">Nothing stored for %1$s yet. This Proton\'s own files are saved as its original.</string>
|
||||
<string name="comp_waits_long">A game is running on this Proton, so this waits until it closes.</string>
|
||||
<string name="comp_applies_long">Applies the next time a game starts.</string>
|
||||
<string name="comp_swap_title">Swap %1$s?</string>
|
||||
<string name="comp_swap_body">Put %1$s into %2$s? Its original files stay saved. %3$s</string>
|
||||
<string name="comp_restore_title">Restore %1$s?</string>
|
||||
<string name="comp_restore_body">Put the %1$s original back into %2$s? %3$s</string>
|
||||
<string name="comp_delete_package_body">Removes the package from the app. You can download it again.</string>
|
||||
<string name="comp_delete_original_title">Delete this original?</string>
|
||||
<string name="comp_delete_original_body">This %1$s original is from an earlier build of %2$s. The current build\'s original is always kept.</string>
|
||||
<string name="comp_nightlies_count">On the Nightlies · %1$d</string>
|
||||
<string name="comp_refresh_to_list">Refresh to list packages on the Nightlies.</string>
|
||||
<string name="comp_nothing_new">Nothing new since the last check.</string>
|
||||
<string name="comp_import_wcp">Import .wcp</string>
|
||||
<string name="comp_restore_original">Restore this Proton\'s original</string>
|
||||
<string name="comp_restore_original_body">Put %1$s\'s original %2$s back?</string>
|
||||
<string name="comp_swap">Swap</string>
|
||||
<string name="comp_about_text">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.</string>
|
||||
<string name="comp_tag_original">Original</string>
|
||||
<string name="comp_tag_in_use">In use</string>
|
||||
<string name="comp_tag_new">New</string>
|
||||
<string name="comp_tag_stored">Stored</string>
|
||||
<string name="perf_title">Performance</string>
|
||||
<string name="perf_lede">CPU cores, session fixes and the host name. Applies next session.</string>
|
||||
<string name="perf_client_cores_group">Steam client cores</string>
|
||||
<string name="perf_override">Override Steam\'s core choice</string>
|
||||
<string name="perf_override_hint">Pins Steam, its UI helper and gamescope to the cores below.</string>
|
||||
<string name="perf_client_cores">Client cores</string>
|
||||
<string name="perf_client_cores_on">Cores for Steam.</string>
|
||||
<string name="perf_client_cores_off">Turn on the override to choose.</string>
|
||||
<string name="perf_game_cores_group">Game cores</string>
|
||||
<string name="perf_game_cores">Game cores</string>
|
||||
<string name="perf_game_cores_hint">Cores for games. With all selected, Android schedules freely.</string>
|
||||
<string name="perf_client_ui">Client interface</string>
|
||||
<string name="perf_glthread">Threaded GL</string>
|
||||
<string name="perf_glthread_hint">May make Steam menus more responsive.</string>
|
||||
<string name="perf_zink">Zink: lazy descriptors</string>
|
||||
<string name="perf_zink_hint">Recommended for drivers without descriptor buffers.</string>
|
||||
<string name="perf_noglerror">Skip GL error checks</string>
|
||||
<string name="perf_noglerror_hint">Turns off per-call GL validation.</string>
|
||||
<string name="perf_gsrealtime">gamescope: realtime GPU queue</string>
|
||||
<string name="perf_gsrealtime_hint">Prioritizes the compositor\'s GPU work. Can smooth frame pacing at some cost to games.</string>
|
||||
<string name="perf_gpu">GPU</string>
|
||||
<string name="perf_gpuclock">Hold the GPU at its top clock</string>
|
||||
<string name="perf_gpuclock_hint">Adreno only. Fewer hitches, more battery use and heat.</string>
|
||||
<string name="perf_fixes">Session fixes</string>
|
||||
<string name="perf_sysmem">Turnip: sysmem rendering</string>
|
||||
<string name="perf_sysmem_hint">Faster for Steam and most games. Required on Adreno 710/720/722.</string>
|
||||
<string name="perf_xalia">Skip Steam\'s xalia helper</string>
|
||||
<string name="perf_xalia_hint">Disables Proton\'s gamepad navigation helper, which costs CPU in every game.</string>
|
||||
<string name="perf_seccomp">Run proot without seccomp</string>
|
||||
<string name="perf_seccomp_hint">May fix missing-syscall errors on some kernels. Can reduce performance.</string>
|
||||
<string name="perf_fastpath">proot fast path</string>
|
||||
<string name="perf_fastpath_needs_seccomp">Needs proot\'s seccomp filter.</string>
|
||||
<string name="perf_fastpath_hint">Faster file lookups and less stutter. Turn off if a program can\'t find its files.</string>
|
||||
<string name="perf_identity">Session identity</string>
|
||||
<string name="perf_hostname">Host name</string>
|
||||
<string name="perf_hostname_hint">The name Steam sees for this device. Blank restores %1$s.</string>
|
||||
<string name="perf_hostname_invalid">Letters, digits and inner hyphens only, up to 63 characters.</string>
|
||||
<string name="perf_phantom_warning">Android is set to kill this session</string>
|
||||
<string name="driver_installed">Installed</string>
|
||||
<string name="driver_available">Available to download</string>
|
||||
<string name="driver_nothing_new">Nothing new since the last check.</string>
|
||||
<string name="driver_restore_builtin">Restore built-in drivers</string>
|
||||
<string name="driver_delete_builtin">Built into the app: this removes its unpacked files and hides it. Restore built-in drivers brings it back.</string>
|
||||
<string name="driver_delete_bundle">Half of an Android + Linux bundle: both halves are removed. A driver in use falls back to the default.</string>
|
||||
<string name="driver_delete_plain">Its files are removed from the app.</string>
|
||||
<string name="driver_delete_in_use">Its files are removed. It\'s in use, so the default takes over.</string>
|
||||
<string name="drawer_page_display">Display</string>
|
||||
<string name="drawer_page_controls">Controls</string>
|
||||
<string name="drawer_page_components">Components</string>
|
||||
<string name="drawer_page_settings">Settings</string>
|
||||
<string name="drawer_page_desc">%1$s page</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
<string name="drawer_desktop">Desktop</string>
|
||||
<string name="drawer_steam_menu">Steam menu</string>
|
||||
<string name="drawer_quick_access">Quick access</string>
|
||||
<string name="drawer_open_qam">Open Quick Access Menu</string>
|
||||
<string name="drawer_prev_page">Previous page</string>
|
||||
<string name="drawer_next_page">Next page</string>
|
||||
<string name="drawer_hud">Performance HUD</string>
|
||||
<string name="drawer_fill">Stretch games to fill</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Keyboard</string>
|
||||
<string name="drawer_pc_keyboard">PC keyboard</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Second screen</string>
|
||||
<string name="drawer_second_screen_shows">Shows</string>
|
||||
<string name="drawer_android_apps">Android apps</string>
|
||||
<string name="drawer_launch_app">Launch an app</string>
|
||||
<string name="common_hide">Hide</string>
|
||||
<string name="common_show">Show</string>
|
||||
<string name="drawer_no_apps">No launchable apps</string>
|
||||
<string name="drawer_session_behavior">Session behavior</string>
|
||||
<string name="drawer_suspend_hint">Applies now and to future sessions in this mode.</string>
|
||||
<string name="drawer_next_session">Next session</string>
|
||||
<string name="drawer_support">Support</string>
|
||||
<string name="drawer_logs">Session logs</string>
|
||||
<string name="drawer_logs_hint">Attach to a bug report</string>
|
||||
<string name="drawer_share_logs">Share logs</string>
|
||||
<string name="drawer_background">Background</string>
|
||||
<string name="drawer_stop">Stop</string>
|
||||
<string name="drawer_stop_title">Stop session?</string>
|
||||
<string name="drawer_running_now">Running now</string>
|
||||
<string name="drawer_no_game">No game is running on a Proton</string>
|
||||
<string name="drawer_no_proton">No Proton is installed yet.</string>
|
||||
<string name="drawer_game_running">game running</string>
|
||||
<string name="drawer_reapplied">re-applied at launch %1$s</string>
|
||||
<string name="drawer_original">Original</string>
|
||||
<string name="drawer_components_note">Download, import and delete on the Components page.</string>
|
||||
<string name="setup_check_done">Done</string>
|
||||
<string name="setup_check_warn">Needs attention</string>
|
||||
<string name="setup_check_busy">Working</string>
|
||||
<string name="setup_limit_adb_failed">Check the Wireless debugging IP address and port, or pair again if Android removed this device.</string>
|
||||
<string name="setup_install">Install</string>
|
||||
<string name="setup_manage">Manage</string>
|
||||
<string name="setup_tab_overview">Overview</string>
|
||||
<string name="setup_tab_controller">Controller</string>
|
||||
<string name="setup_tab_session">Session</string>
|
||||
<string name="setup_tab_launcher">Launcher</string>
|
||||
<string name="setup_title">Setup</string>
|
||||
<string name="setup_all_set">● All set</string>
|
||||
<string name="setup_n_ready">%1$d of %2$d ready</string>
|
||||
<string name="setup_system_check">System check</string>
|
||||
<string name="setup_gpu_supported">Device supported</string>
|
||||
<string name="setup_gpu_untested">Untested GPU</string>
|
||||
<string name="setup_gpu_unsupported">GPU not supported</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam needs an Adreno driver; %1$s may show a black screen</string>
|
||||
<string name="setup_runtime">Linux runtime</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Not installed · about 3 GB, installed on first Play</string>
|
||||
<string name="setup_installed">Installed</string>
|
||||
<string name="setup_runtime_update">%1$s · update available</string>
|
||||
<string name="setup_runtime_current">%1$s · up to date</string>
|
||||
<string name="setup_limit">Child-process limit</string>
|
||||
<string name="setup_limit_on">On · Android may close Steam. Turning it off takes a minute.</string>
|
||||
<string name="setup_limit_unset">Not set · Android may close Steam. Turning it off takes a minute.</string>
|
||||
<string name="setup_limit_unknown">Couldn\'t be checked · turning it off takes a minute.</string>
|
||||
<string name="setup_fix_it">Fix it</string>
|
||||
<string name="setup_details">Details</string>
|
||||
<string name="setup_dev_options">Developer options</string>
|
||||
<string name="setup_use_wireless">Use Wireless debugging</string>
|
||||
<string name="setup_check_again">Check again</string>
|
||||
<string name="setup_limit_turn_on">Turn limit on</string>
|
||||
<string name="setup_account">Steam account</string>
|
||||
<string name="setup_signed_in_offline">Signed in as %1$s · offline mode</string>
|
||||
<string name="setup_signed_in">Signed in as %1$s</string>
|
||||
<string name="setup_sign_in">Press Play and sign in to Steam</string>
|
||||
<string name="setup_tools">Tools</string>
|
||||
<string name="setup_controller">Controller</string>
|
||||
<string name="setup_controller_unavailable">Controller settings are unavailable.</string>
|
||||
<string name="setup_session">Session</string>
|
||||
<string name="setup_logs">Session logs</string>
|
||||
<string name="setup_logs_hint">Saved after each session</string>
|
||||
<string name="setup_latest_logs">Latest session logs</string>
|
||||
<string name="setup_saved_logs">Saved session logs</string>
|
||||
<string name="setup_saved_logs_hint">Keeps the newest %1$d</string>
|
||||
<string name="setup_clear_logs">Clear logs</string>
|
||||
<string name="setup_offline">Offline mode</string>
|
||||
<string name="setup_sign_in_first">Sign in to Steam first</string>
|
||||
<string name="setup_launcher">Launcher</string>
|
||||
<string name="setup_theme">Theme</string>
|
||||
<string name="setup_theme_hint">Launcher appearance</string>
|
||||
<string name="setup_home">Use as Home screen</string>
|
||||
<string name="setup_home_on">DroidDeck can be the Home app</string>
|
||||
<string name="setup_home_off">DroidDeck is never offered as a Home app</string>
|
||||
<string name="setup_fullscreen">Fullscreen</string>
|
||||
<string name="setup_fullscreen_on">Hides the Android status and navigation bars</string>
|
||||
<string name="setup_fullscreen_off">Shows the Android status and navigation bars</string>
|
||||
<string name="setup_default_home">Default Home app</string>
|
||||
<string name="setup_choose_home">Choose a Home app</string>
|
||||
<string name="setup_choose">Choose</string>
|
||||
<string name="setup_linux_apps">Linux apps (beta)</string>
|
||||
<string name="setup_store">Flathub Store</string>
|
||||
<string name="setup_store_on">In the menu. Some apps may not start; logs are in Download/DroidDeck.</string>
|
||||
<string name="setup_store_off">Install Linux apps and games from Flathub</string>
|
||||
<string name="setup_tool_files">Files</string>
|
||||
<string name="setup_tool_files_hint">Browse and manage files</string>
|
||||
<string name="setup_tool_protons">Proton versions</string>
|
||||
<string name="setup_tool_protons_hint">Install ARM64 Proton builds</string>
|
||||
<string name="setup_tool_performance">Performance</string>
|
||||
<string name="setup_tool_performance_hint">CPU core assignment</string>
|
||||
<string name="setup_tool_roms">ROMs folder</string>
|
||||
<string name="setup_tool_roms_hint">Where emulator games are stored</string>
|
||||
<string name="setup_card_components">Components</string>
|
||||
<string name="setup_card_components_hint">FEX, DXVK, VKD3D</string>
|
||||
<string name="setup_card_controls">Controls</string>
|
||||
<string name="setup_card_controls_hint">Button mapping</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user