mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
Merge remote-tracking branch 'origin/main' into feat/armada-and-lineage
# Conflicts: # app/src/main/java/com/steamdeck/launcher/ui/MainScreen.kt # app/src/main/java/com/steamdeck/launcher/ui/ModeSettingsDialog.kt # app/src/main/java/com/steamdeck/launcher/ui/PerformanceDialog.kt
This commit is contained in:
commit
d338f73eec
11 files changed
+97
-396
No files matched your search
@@ -276,7 +276,7 @@ class MainActivity : ComponentActivity() {
|
||||
showNonAdreno?.let { release ->
|
||||
ConfirmDialog(
|
||||
title = "Not an Adreno GPU",
|
||||
text = "The runtime draws with Turnip, an Adreno driver. On ${com.steamdeck.launcher.core.DeviceSupport.gpuName()} the compositor gets no usable Vulkan device and a session comes up as sound over a black screen. The download is ${"%.0f".format(release.size / 1e6)} MB.",
|
||||
text = "Turnip supports Adreno GPUs. On ${com.steamdeck.launcher.core.DeviceSupport.gpuName()}, Steam may show a black screen. Download: ${"%.0f".format(release.size / 1e6)} MB.",
|
||||
confirm = "Install anyway",
|
||||
onConfirm = { showNonAdreno = null; install(release) },
|
||||
onDismiss = { showNonAdreno = null },
|
||||
|
||||
@@ -19,19 +19,19 @@ object FexPreset {
|
||||
)
|
||||
|
||||
val all: List<Preset> = listOf(
|
||||
Preset("", "FEX defaults", "No variables set; what every session ran on until now.", emptyList()),
|
||||
Preset("STABILITY", "Stability", "Every TSO emulation on, single-block: the slowest and the surest.", tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=0")),
|
||||
Preset("COMPATIBILITY", "Compatibility", "Every TSO emulation on, multiblock.", tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=1")),
|
||||
Preset("INTERMEDIATE", "Intermediate", "Main and half-barrier TSO on, vector and memcpy off, reduced x87.", tso(1, 0, 0, 1) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE", "Performance", "All TSO off, reduced x87, multiblock.", tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE_TSO", "Performance + TSO", "Performance with the main store ordering kept on, for the many games that need it.", tso(1, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("EXTREME", "Extreme", "Performance plus no self-modifying-code checks and FEX's block caches retuned. JIT-heavy titles and DRM can break.",
|
||||
Preset("", "FEX defaults", "Uses FEX defaults.", emptyList()),
|
||||
Preset("STABILITY", "Stability", "Safest, slowest settings.", tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=0")),
|
||||
Preset("COMPATIBILITY", "Compatibility", "Full TSO with multiblock.", tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=1")),
|
||||
Preset("INTERMEDIATE", "Intermediate", "Balanced TSO with reduced x87 precision.", tso(1, 0, 0, 1) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE", "Performance", "All TSO disabled; multiblock.", tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE_TSO", "Performance + TSO", "Performance settings with main TSO enabled.", tso(1, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("EXTREME", "Extreme", "Disables self-modifying-code checks. JIT games and DRM may break.",
|
||||
tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMCCHECKS=none", "FEX_DISABLEL2CACHE=1", "FEX_DYNAMICL1CACHE=1", "FEX_DYNAMICL1CACHEINCREASECOUNTHEURISTIC=250", "FEX_DYNAMICL1CACHEDECREASECOUNTHEURISTIC=50")),
|
||||
Preset("EXTREME_TSO", "Extreme + TSO", "Extreme with the main store ordering kept on.",
|
||||
Preset("EXTREME_TSO", "Extreme + TSO", "Extreme settings with main TSO enabled.",
|
||||
tso(1, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMCCHECKS=none", "FEX_DISABLEL2CACHE=1", "FEX_DYNAMICL1CACHE=1", "FEX_DYNAMICL1CACHEINCREASECOUNTHEURISTIC=250", "FEX_DYNAMICL1CACHEDECREASECOUNTHEURISTIC=50")),
|
||||
Preset("EXTREME_GN", "Extreme-gn", "Performance plus the small TSC scale and volatile-metadata hints; SMC checks stay on. Try before Extreme.",
|
||||
Preset("EXTREME_GN", "Extreme-gn", "Performance with TSC and metadata hints. Try before Extreme.",
|
||||
tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMALLTSCSCALE=1", "FEX_VOLATILEMETADATA=1")),
|
||||
Preset("DENUVO", "Denuvo", "Performance with full SMC checks and the hypervisor bit hidden, for titles whose anti-tamper looks for either.",
|
||||
Preset("DENUVO", "Denuvo", "Performance with full code checks and the hypervisor bit hidden.",
|
||||
tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMCCHECKS=full", "FEX_HIDEHYPERVISORBIT=1")),
|
||||
)
|
||||
|
||||
|
||||
@@ -49,11 +49,7 @@ fun DesktopAppsDialog(
|
||||
when {
|
||||
rows == null -> Text("Could not reach the package catalog", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
rows.isEmpty() -> Text("Loading the catalog…", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
else -> Text(
|
||||
" starts an emulator fullscreen under gamescope, with the GPU - on the desktop, Vulkan programs cannot draw. " +
|
||||
"ROMs are at /root/ROMs, the phone at /root/Storage.",
|
||||
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
var lastTier = 0
|
||||
for (row in rows ?: emptyList()) {
|
||||
@@ -61,9 +57,9 @@ fun DesktopAppsDialog(
|
||||
lastTier = row.tier
|
||||
Text(
|
||||
when (row.tier) {
|
||||
1 -> "Native for this hardware"
|
||||
2 -> "Upstream ARM64 builds - experimental on a phone"
|
||||
else -> "Built from source - experimental"
|
||||
1 -> "Native ARM64"
|
||||
2 -> "ARM64 · experimental"
|
||||
else -> "Source build · experimental"
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@@ -74,8 +70,8 @@ fun DesktopAppsDialog(
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(row.name, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
(if (row.installed != null) "installed ${row.installed}" else "${row.version} · ${row.size}") +
|
||||
(if (row.notes.isNotEmpty()) " - ${row.notes}" else ""),
|
||||
(if (row.installed != null) "Installed ${row.installed}" else "${row.version} · ${row.size}") +
|
||||
(if (row.notes.isNotEmpty()) " · ${row.notes}" else ""),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -264,16 +264,13 @@ private fun Rail(
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column {
|
||||
Text("SteamDeck", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground)
|
||||
Text(
|
||||
when {
|
||||
s.busy -> if (s.percent >= 0) "${s.stage} ${s.percent}%" else s.stage
|
||||
!s.ready -> "runtime not installed"
|
||||
s.available != null && s.available != s.installed -> "runtime ${s.installed} · ${s.available} available"
|
||||
s.running != null -> "${s.running} in the background"
|
||||
else -> "runtime ${s.installed ?: "?"}"
|
||||
},
|
||||
fontSize = 11.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
val status = when {
|
||||
s.busy -> if (s.percent >= 0) "${s.stage} ${s.percent}%" else s.stage
|
||||
!s.ready -> "runtime not installed"
|
||||
s.available != null && s.available != s.installed -> "runtime update available"
|
||||
else -> null
|
||||
}
|
||||
if (status != null) Text(status, fontSize = 11.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(s.busy, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut()) {
|
||||
@@ -317,7 +314,7 @@ private fun Rail(
|
||||
NavItem("Steam", "steam", selected == "steam", caret = openSteam, count = s.steamGames.size, register = register, unregister = unregister) { onSelect("steam") }
|
||||
Sub(openSteam) {
|
||||
for ((i, g) in s.steamGames.withIndex()) NavItem(g.name, "app:${g.appId}", selected == "app:${g.appId}", small = true, i = i, register = register, unregister = unregister) { onSelect("app:${g.appId}") }
|
||||
if (s.steamGames.isEmpty()) NavItem("no games installed", "x", false, small = true, muted = true, register = register, unregister = unregister) {}
|
||||
if (s.steamGames.isEmpty()) NavItem("No games", "x", false, small = true, muted = true, register = register, unregister = unregister) {}
|
||||
NavItem("Settings", "settings:steam", selected == "settings:steam", small = true, tiny = true, muted = true, i = s.steamGames.size, register = register, unregister = unregister) { a.onSteamSettings() }
|
||||
}
|
||||
NavItem("Desktop", "desktop", selected == "desktop", caret = openDesktop, count = s.emulators.count { it.installed }, register = register, unregister = unregister) { onSelect("desktop") }
|
||||
@@ -328,12 +325,12 @@ private fun Rail(
|
||||
Sub(openEmu == key) {
|
||||
for ((j, g) in e.games.withIndex()) NavItem(g.name, "rom:${e.id}:$j", selected == "rom:${e.id}:$j", small = true, tiny = true, i = j, register = register, unregister = unregister) { onSelect("rom:${e.id}:$j") }
|
||||
if (e.games.isEmpty()) NavItem(
|
||||
if (e.id == "retroarch") "browses its own games" else if (s.romsDir == null) "choose a ROMs folder" else "nothing for ${e.system} in ROMs",
|
||||
if (e.id == "retroarch") "Browse games in RetroArch" else if (s.romsDir == null) "Choose ROMs folder" else "No ${e.system} games",
|
||||
"x", false, small = true, tiny = true, muted = true, register = register, unregister = unregister,
|
||||
) { if (e.id != "retroarch") a.onRoms() }
|
||||
}
|
||||
}
|
||||
if (s.emulators.none { it.installed }) NavItem("install emulators under Desktop & apps", "x", false, small = true, muted = true, register = register, unregister = unregister) { a.onApps() }
|
||||
if (s.emulators.none { it.installed }) NavItem("Install emulators", "x", false, small = true, muted = true, register = register, unregister = unregister) { a.onApps() }
|
||||
NavItem("Settings", "settings:lxqt", selected == "settings:lxqt", small = true, tiny = true, muted = true, i = s.emulators.count { it.installed }, register = register, unregister = unregister) { a.onDesktopSettings() }
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
@@ -369,9 +366,9 @@ private fun Rail(
|
||||
}
|
||||
}
|
||||
Box {
|
||||
item("Start offline", when { s.offlineAccount == null -> "sign in first"; s.offline -> "on"; else -> "off" }, 7, null) { if (s.offlineAccount != null) menus.open = "offline" }
|
||||
item("Offline", when { s.offlineAccount == null -> "sign in first"; s.offline -> "on"; else -> "off" }, 7, null) { if (s.offlineAccount != null) menus.open = "offline" }
|
||||
AnchoredMenu(
|
||||
menus.open == "offline", onDismiss = { if (menus.open == "offline") menus.open = null }, title = "Start offline",
|
||||
menus.open == "offline", onDismiss = { if (menus.open == "offline") menus.open = null }, title = "Offline",
|
||||
) {
|
||||
MenuItem("On", checked = s.offline) { if (!s.offline) a.onOffline(); menus.open = null }
|
||||
MenuItem("Off", checked = !s.offline) { if (s.offline) a.onOffline(); menus.open = null }
|
||||
@@ -426,7 +423,7 @@ private fun RunningTile(name: String, onResume: () -> Unit) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(name, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text("running · tap to go back", fontSize = 11.sp, color = colors.onSurfaceVariant)
|
||||
Text("Resume", fontSize = 11.sp, color = colors.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,17 +586,17 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
Rise(0) { Eyebrow("Desktop") }
|
||||
Rise(3) {
|
||||
Actions {
|
||||
PrimaryButton(if (s.desktopInstalled) "Desktop" else "Install the desktop first", enabled = s.ready && !s.busy && s.desktopInstalled, onClick = a.onDesktop)
|
||||
PrimaryButton("Desktop", enabled = s.ready && !s.busy && s.desktopInstalled, onClick = a.onDesktop)
|
||||
SecondaryButton("Desktop & apps", onClick = a.onApps)
|
||||
Cog(a.onDesktopSettings)
|
||||
}
|
||||
}
|
||||
Rise(4) { SectionTitle("Emulators", "${s.emulators.count { it.installed }} installed · ${s.emulators.count { !it.installed }} available") }
|
||||
Rise(5) {
|
||||
Text("? Why emulators?", fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(bottom = 6.dp).clip(Shape10).clickable(onClick = a.onEmulatorHelp).padding(horizontal = 6.dp, vertical = 4.dp))
|
||||
Text("Why?", fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(bottom = 6.dp).clip(Shape10).clickable(onClick = a.onEmulatorHelp).padding(horizontal = 6.dp, vertical = 4.dp))
|
||||
}
|
||||
Rise(6, Modifier.weight(1f).fillMaxWidth()) {
|
||||
ArtGrid(s.emulators.map { e -> Tile(e.name, if (e.installed) (if (e.id == "retroarch") "browses its own games" else "${e.games.size} game${if (e.games.size == 1) "" else "s"}") else "not installed", null, "emu:${e.id}", e.iconRes, dim = !e.installed) { if (e.installed) a.onEmulator(e) else a.onApps() } })
|
||||
ArtGrid(s.emulators.map { e -> Tile(e.name, if (e.installed) (if (e.id == "retroarch") null else "${e.games.size} game${if (e.games.size == 1) "" else "s"}") else "Not installed", null, "emu:${e.id}", e.iconRes, dim = !e.installed) { if (e.installed) a.onEmulator(e) else a.onApps() } })
|
||||
}
|
||||
}
|
||||
selected.startsWith("emu:") -> {
|
||||
@@ -617,9 +614,9 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
Rise(4) { SectionTitle("Games", e.games.size.toString()) }
|
||||
if (e.games.isEmpty()) Rise(5) {
|
||||
Note(
|
||||
if (s.romsDir == null) "Choose a ROMs folder first (left, or the button above)."
|
||||
else if (e.id == "retroarch") "RetroArch loads its games itself: open it and browse to root › ROMs."
|
||||
else "Put ${e.system} games in ROMs/${e.system.substringBefore(' ')} (or the ROMs folder itself); the list rebuilds when this screen opens.",
|
||||
if (s.romsDir == null) "Choose a ROMs folder."
|
||||
else if (e.id == "retroarch") "Browse to /root/ROMs in RetroArch."
|
||||
else "Add ${e.system} games to ROMs/${e.system.substringBefore(' ')}.",
|
||||
)
|
||||
}
|
||||
else Rise(5, Modifier.weight(1f).fillMaxWidth()) {
|
||||
@@ -656,12 +653,12 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Note("Pick something on the left.")
|
||||
else -> Note("Select an item.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Tile(val title: String, val sub: String, val art: File?, val key: String, val iconRes: Int? = null, val dim: Boolean = false, val onClick: () -> Unit)
|
||||
private class Tile(val title: String, val sub: String?, val art: File?, val key: String, val iconRes: Int? = null, val dim: Boolean = false, val onClick: () -> Unit)
|
||||
|
||||
|
||||
@Composable
|
||||
@@ -834,7 +831,7 @@ private fun GameTile(t: Tile, wide: Boolean, square: Boolean, src: MutableIntera
|
||||
}
|
||||
Column(modifier = Modifier.padding(horizontal = 6.dp, vertical = 5.dp)) {
|
||||
Text(t.title, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(t.sub, fontSize = 8.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (t.sub != null) Text(t.sub, fontSize = 8.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,279 +1,24 @@
|
||||
package com.steamdeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.steamdeck.launcher.R
|
||||
|
||||
class MainUiState(
|
||||
val installed: String?,
|
||||
val ready: Boolean,
|
||||
val available: String?,
|
||||
val availableSize: String?,
|
||||
val busy: Boolean,
|
||||
val stage: String,
|
||||
val percent: Int,
|
||||
val failed: Boolean,
|
||||
val frameGenLabel: String,
|
||||
val desktopInstalled: Boolean,
|
||||
val offlineAccount: String?,
|
||||
val offline: Boolean,
|
||||
val romsDir: String?,
|
||||
val logsEnabled: Boolean,
|
||||
val emulators: List<Pair<String, String>> = emptyList(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
state: MainUiState,
|
||||
onPlay: () -> Unit,
|
||||
onDesktop: () -> Unit,
|
||||
onApps: () -> Unit,
|
||||
onRuntime: () -> Unit,
|
||||
onFrameGen: () -> Unit,
|
||||
onProtons: () -> Unit,
|
||||
onSteamSettings: () -> Unit,
|
||||
onDesktopSettings: () -> Unit,
|
||||
onPerformance: () -> Unit,
|
||||
onOffline: () -> Unit,
|
||||
onRoms: () -> Unit,
|
||||
onFiles: () -> Unit,
|
||||
onLaunchEmulator: (path: String) -> Unit,
|
||||
onEmulatorHelp: () -> Unit,
|
||||
onLogs: () -> Unit,
|
||||
onCredits: () -> Unit,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.systemBarsPadding(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val designHeight = 480.dp
|
||||
val k = (maxHeight / designHeight).coerceIn(0.55f, 1f)
|
||||
val colors = MaterialTheme.colorScheme
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 420.dp * k)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp * k, vertical = 12.dp * k),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(72.dp * k),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp * k))
|
||||
Text("SteamDeck", fontSize = 22.sp * k, color = colors.onBackground)
|
||||
Spacer(Modifier.height(14.dp * k))
|
||||
|
||||
val status = when {
|
||||
state.busy -> "Working…"
|
||||
!state.ready -> "Linux runtime not installed"
|
||||
else -> "Ready"
|
||||
}
|
||||
Text(status, fontSize = 15.sp * k, color = colors.onBackground)
|
||||
val detail = when {
|
||||
state.busy -> if (state.percent >= 0) "${state.stage} ${state.percent}%" else state.stage
|
||||
state.failed -> "Install failed. Nothing changed."
|
||||
!state.ready -> state.available?.let { "$it · ${state.availableSize} download, one time" }
|
||||
?: "Could not reach the runtime catalog"
|
||||
state.available != null && state.available != state.installed ->
|
||||
"Runtime ${state.installed ?: "?"} installed · ${state.available} available"
|
||||
else -> null
|
||||
}
|
||||
if (detail != null) Text(
|
||||
detail, fontSize = 12.sp * k, color = colors.onSurfaceVariant, textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 3.dp * k),
|
||||
)
|
||||
if (state.busy) {
|
||||
Spacer(Modifier.height(6.dp * k))
|
||||
if (state.percent >= 0) {
|
||||
LinearProgressIndicator(progress = { state.percent / 100f }, modifier = Modifier.fillMaxWidth())
|
||||
} else {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(14.dp * k))
|
||||
|
||||
val buttonHeight = 40.dp * k
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp * k), modifier = Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = onPlay, enabled = state.ready && !state.busy,
|
||||
modifier = Modifier.weight(1f).height(buttonHeight),
|
||||
) { Text("Play", fontSize = 14.sp * k) }
|
||||
CogButton(k, onSteamSettings)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp * k), modifier = Modifier.fillMaxWidth().padding(top = 5.dp * k)) {
|
||||
OutlinedButton(
|
||||
onClick = onDesktop, enabled = state.ready && !state.busy && state.desktopInstalled,
|
||||
modifier = Modifier.weight(1f).height(buttonHeight),
|
||||
) { Text("Desktop", fontSize = 13.sp * k, maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
CogButton(k, onDesktopSettings)
|
||||
}
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp * k),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp * k),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp * k),
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onRoms,
|
||||
contentPadding = PaddingValues(horizontal = 10.dp * k, vertical = 0.dp),
|
||||
modifier = Modifier.height(30.dp * k),
|
||||
) {
|
||||
Text(
|
||||
"ROMs: " + (state.romsDir?.substringAfterLast('/')?.ifEmpty { state.romsDir } ?: "choose folder"),
|
||||
fontSize = 11.sp * k, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
for ((label, path) in state.emulators) {
|
||||
OutlinedButton(
|
||||
onClick = { onLaunchEmulator(path) }, enabled = state.ready && !state.busy,
|
||||
contentPadding = PaddingValues(horizontal = 10.dp * k, vertical = 0.dp),
|
||||
modifier = Modifier.height(30.dp * k),
|
||||
) { Text(label, fontSize = 11.sp * k, maxLines = 1) }
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onEmulatorHelp,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
modifier = Modifier.width(30.dp * k).height(30.dp * k),
|
||||
) { Text("?", fontSize = 12.sp * k) }
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onOffline, enabled = state.offlineAccount != null,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp * k).height(buttonHeight),
|
||||
) {
|
||||
Text(
|
||||
when {
|
||||
state.offlineAccount == null -> "Start offline"
|
||||
state.offline -> "Start offline: on · ${state.offlineAccount}"
|
||||
else -> "Start offline: off"
|
||||
},
|
||||
fontSize = 13.sp * k,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp * k))
|
||||
|
||||
val runtimeLabel = when {
|
||||
state.busy -> "Working…"
|
||||
!state.ready -> "Install Linux runtime"
|
||||
state.available != null && state.available != state.installed -> "Update Linux runtime"
|
||||
else -> "Remove Linux runtime"
|
||||
}
|
||||
val tileGap = Arrangement.spacedBy(6.dp * k)
|
||||
Row(horizontalArrangement = tileGap, modifier = Modifier.fillMaxWidth()) {
|
||||
MenuTile(runtimeLabel, null, k, Modifier.weight(1f), enabled = !state.busy, onClick = onRuntime)
|
||||
MenuTile("Desktop & apps", null, k, Modifier.weight(1f), enabled = state.ready && !state.busy, onClick = onApps)
|
||||
}
|
||||
Spacer(Modifier.height(6.dp * k))
|
||||
Row(horizontalArrangement = tileGap, modifier = Modifier.fillMaxWidth()) {
|
||||
MenuTile("Frame generation", state.frameGenLabel, k, Modifier.weight(1f), onClick = onFrameGen)
|
||||
MenuTile("Compatibility tools", null, k, Modifier.weight(1f), onClick = onProtons)
|
||||
}
|
||||
Spacer(Modifier.height(6.dp * k))
|
||||
Row(horizontalArrangement = tileGap, modifier = Modifier.fillMaxWidth()) {
|
||||
MenuTile("Performance", null, k, Modifier.weight(1f), onClick = onPerformance)
|
||||
MenuTile("Files", null, k, Modifier.weight(1f), onClick = onFiles)
|
||||
}
|
||||
Spacer(Modifier.height(6.dp * k))
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
MenuTile(
|
||||
"Session logs", if (state.logsEnabled) "on · Download/SteamDeck" else "off",
|
||||
k, Modifier.weight(1f), onClick = onLogs,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(10.dp * k))
|
||||
Text(
|
||||
"by The412Banner and maxjivi05 · credits",
|
||||
fontSize = 11.sp * k, color = colors.onSurfaceVariant,
|
||||
modifier = Modifier.clickable(onClick = onCredits).padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CogButton(k: Float, onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.width(44.dp * k).height(40.dp * k),
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) { Icon(Icons.Filled.Settings, contentDescription = "Settings", modifier = Modifier.size(18.dp * k)) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuTile(
|
||||
title: String,
|
||||
value: String?,
|
||||
k: Float,
|
||||
modifier: Modifier,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick, enabled = enabled,
|
||||
modifier = modifier.height(if (value != null) 52.dp * k else 44.dp * k),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp * k, vertical = 4.dp * k),
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(title, fontSize = 12.sp * k, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center)
|
||||
if (value != null) {
|
||||
Text(
|
||||
value, fontSize = 10.sp * k, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmulatorHelpDialog(onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Emulators") },
|
||||
text = { Text("Place games in the ROMs folder. In RPCS3, install firmware from File > Install Firmware.") },
|
||||
text = { Text("Put games in /root/ROMs. Install RPCS3 firmware from File > Install Firmware.") },
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text("OK") } },
|
||||
)
|
||||
}
|
||||
@@ -296,12 +41,12 @@ fun CreditsDialog(onDismiss: () -> Unit) {
|
||||
title = { Text("Credits") },
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("The412Banner - the app, the Wayland compositor, the runtime and the Steam session.", style = MaterialTheme.typography.bodyMedium)
|
||||
Text("The412Banner: app, compositor, runtime, and Steam session.", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("maxjivi05 (Max) - the gamescope runtime this is built on: the proot session, the session shim, the fake-evdev interposer and the controller work, from WinNative.", style = MaterialTheme.typography.bodyMedium)
|
||||
Text("maxjivi05: gamescope runtime and controller support, based on WinNative.", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
"GPL-3.0. Valve, Steam, Steam Deck and Proton are trademarks of Valve Corporation; this project is not affiliated with Valve. gamescope, Mesa, Turnip, Xwayland, PulseAudio, proot and Arch Linux ARM are their authors' own.",
|
||||
"GPL-3.0. Steam, Steam Deck, and Proton are Valve trademarks. Not affiliated with Valve. Third-party software remains under its authors' licenses.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.steamdeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -78,25 +76,22 @@ class ModeSettingsActions(
|
||||
fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
val steam = s.mode == SessionService.MODE_STEAM
|
||||
val host = rememberMenuHost()
|
||||
val colors = MaterialTheme.colorScheme
|
||||
SettingsPage(
|
||||
host,
|
||||
eyebrow = if (steam) "Steam · settings" else "Desktop · settings",
|
||||
title = if (steam) "Steam session" else "Desktop session",
|
||||
lede = "Each value opens where it is. Most take effect at the next session start; the ones that need the app closed say so.",
|
||||
onBack = a.onDismiss,
|
||||
) {
|
||||
SettingsGroup("Display") {
|
||||
val default = SessionPrefs.defaultResolutionCap(s.mode)
|
||||
ChoiceRow(
|
||||
host, "res", "Resolution", "Takes effect at the next session: gamescope sizes its display once, when it starts.",
|
||||
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) },
|
||||
s.resolutionCap, note = "720p keeps the client's menus responsive; above 1080p costs frames for nothing a handheld can show.",
|
||||
s.resolutionCap, note = "720p can improve menu responsiveness.",
|
||||
onPick = a.onResolution,
|
||||
)
|
||||
ChoiceRow(
|
||||
host, "shape", "Shape", "16:9 is for a foldable: bars on either panel instead of a squashed picture.",
|
||||
host, "shape", "Shape", "Adds bars to preserve 16:9.",
|
||||
listOf("auto" to "The panel's shape", "16:9" to "16:9 with bars"), s.shapeMode, onPick = a.onShape,
|
||||
)
|
||||
}
|
||||
@@ -104,40 +99,40 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
ToggleRow(
|
||||
host, "hdr", "HDR10 output",
|
||||
s.hdrReason?.let { "Not available: $it." }
|
||||
?: "Decided once when the compositor starts: a change applies after the app is fully closed and opened again.",
|
||||
?: "Restart the app to apply.",
|
||||
checked = s.hdr && s.hdrReason == null, enabled = s.hdrReason == null, onChange = a.onHdr,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Drivers") {
|
||||
DriverRowMenu(
|
||||
host, "rt", "Runtime driver",
|
||||
"What " + (if (steam) "the Steam client and its games" else "the desktop's programs") + " render with inside the runtime. Applies at the next session start.",
|
||||
s.linuxRows, s.linuxSelected, importLabel = "Import a \"-Linux\" Turnip zip…",
|
||||
(if (steam) "Used by Steam and games." else "Used by desktop apps.") + " Applies next session.",
|
||||
s.linuxRows, s.linuxSelected, importLabel = "Import Turnip zip…",
|
||||
onSelect = a.onSelectLinux, onRemove = a.onRemoveLinux, onImport = a.onImportLinux,
|
||||
)
|
||||
DriverRowMenu(
|
||||
host, "panel", "Display driver",
|
||||
"What the app's compositor puts frames on the screen with; shared by both modes. Applies after the app is fully closed and opened again.",
|
||||
"Used by the compositor in both modes. Restart the app to apply.",
|
||||
s.androidRows, s.androidSelected, importLabel = "Import an AdrenoTools zip…",
|
||||
onSelect = a.onSelectAndroid, onRemove = a.onRemoveAndroid, onImport = a.onImportAndroid,
|
||||
)
|
||||
}
|
||||
SettingsGroup(if (steam) "Touch & controls" else "Touch") {
|
||||
ChoiceRow(
|
||||
host, "touch", "Touch", "How a finger drives the pointer. Also in the session's drawer.",
|
||||
host, "touch", "Touch", null,
|
||||
listOf("auto" to "Auto", "touchpad" to "Touchpad", "direct" to "Direct"), s.touchMode,
|
||||
note = "Auto: touchpad on the desktop, direct in Steam. Touchpad: drag moves, tap clicks. Direct: the pointer jumps under the finger.",
|
||||
note = "Auto uses touchpad on desktop and direct input in Steam. Touchpad: drag to move, tap to click.",
|
||||
onPick = a.onTouch,
|
||||
)
|
||||
if (steam && s.oscMode != null) ChoiceRow(
|
||||
host, "osc", "On-screen controls", "The virtual pad drawn over a game.",
|
||||
host, "osc", "On-screen controls", null,
|
||||
listOf(
|
||||
SessionPrefs.OSC_AUTO to "Auto",
|
||||
SessionPrefs.OSC_ALWAYS to "Always",
|
||||
SessionPrefs.OSC_STEAM_QAM to "Steam + QAM",
|
||||
SessionPrefs.OSC_NEVER to "Never",
|
||||
), s.oscMode,
|
||||
note = "Auto shows the full pad when no controller is attached. Steam + QAM shows only those two buttons.", onPick = a.onOsc,
|
||||
note = "Auto shows all controls without a controller. Steam + QAM shows only those buttons.", onPick = a.onOsc,
|
||||
)
|
||||
}
|
||||
if (steam && s.steamChannel != null) SettingsGroup("Client") {
|
||||
@@ -175,14 +170,14 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
}
|
||||
if (steam && s.fexPreset != null) SettingsGroup("Games") {
|
||||
ChoiceRow(
|
||||
host, "fex", "FEX preset", "How FEX translates the x86 games the client launches. Applies to the next game launch. Also in the session's drawer.",
|
||||
host, "fex", "FEX preset", "Applies on next game launch.",
|
||||
FexPreset.all.map { it.id to it.label }, s.fexPreset,
|
||||
note = FexPreset.byId(s.fexPreset).detail, onPick = a.onFexPreset,
|
||||
)
|
||||
}
|
||||
if (steam && s.directAudio != null && s.mic != null) SettingsGroup("Audio") {
|
||||
ToggleRow(host, "da", "DirectAudio", "Games play straight to the device through the relay, bypassing PulseAudio: lower latency. The client's own sound goes through the same relay, which drives the device from outside the session with its adaptive buffer. Off = PulseAudio drives the device itself for everything.", s.directAudio, onChange = a.onDirectAudio)
|
||||
ToggleRow(host, "mic", "Microphone", "The device's microphone for voice chat, as the client's input device. Asks for the permission once.", s.mic, onChange = a.onMic)
|
||||
ToggleRow(host, "da", "DirectAudio", "Bypasses PulseAudio for lower latency, for games and the Steam client.", s.directAudio, onChange = a.onDirectAudio)
|
||||
ToggleRow(host, "mic", "Microphone", "Uses the device microphone for voice chat.", s.mic, onChange = a.onMic)
|
||||
}
|
||||
if (steam && s.gameStorage != null) SettingsGroup("Game storage") {
|
||||
val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.second == s.gameStorage }
|
||||
@@ -195,14 +190,14 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
val open = host.open == "storage"
|
||||
SettingsRow(
|
||||
"Second library",
|
||||
"With a second place registered, every Install in Steam asks which drive, Settings › Storage lists both, and Steam moves games between them. Applies at the next session start.",
|
||||
"Adds a library location in Steam. Applies next session.",
|
||||
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 = "An SD card and shared storage go through Android's file layer: a game that streams big assets from there can stall. Keep such games internal.",
|
||||
note = "Games that stream assets from SD or shared storage may stutter. Keep them internal.",
|
||||
) {
|
||||
for ((path, label) in options) MenuItem(label, checked = path == s.gameStorage) {
|
||||
a.onGameStorage(path, if (path.isEmpty() || path == "off") "" else label.substringBefore(" ·"))
|
||||
@@ -215,13 +210,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
}
|
||||
if (!steam && s.renderer != null) SettingsGroup("Renderer") {
|
||||
ChoiceRow(
|
||||
host, "renderer", "Desktop renderer", "How labwc composites the desktop. pixman is software and works everywhere.",
|
||||
host, "renderer", "Desktop renderer", "Composites the desktop.",
|
||||
listOf("pixman" to "pixman - software", "gles2" to "gles2", "vulkan" to "vulkan"), s.renderer,
|
||||
note = "gles2 and vulkan need a real DRM render node, which the Adreno stand-in is not on most devices.", onPick = a.onRenderer,
|
||||
note = "GLES2 and Vulkan require a DRM render node, unavailable on most devices.", onPick = a.onRenderer,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Values are saved as they are picked; there is nothing to confirm.", fontSize = 11.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,64 +49,64 @@ fun PerformancePage(
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val coreItems = cores.map { it.core to it.label }
|
||||
SettingsPage(
|
||||
host, eyebrow = "Setup · performance", title = "Performance",
|
||||
lede = "Which cores the client and its games may run on, and the fixes for a device the runtime does not sit well on. All apply at the next session start.",
|
||||
host, title = "Performance",
|
||||
lede = "CPU cores and session fixes. Applies next session.",
|
||||
onBack = onDismiss,
|
||||
) {
|
||||
SettingsGroup("Steam client cores") {
|
||||
ToggleRow(
|
||||
host, "override", "Override Steam's own core choice",
|
||||
"Steam pins its interface to a subset of cores it picks - on one device 5 of 8, leaving out the fastest - which suits a running game and makes the menus sluggish when the client is all there is. On: the client, its UI helper and gamescope are pinned to the cores below instead, re-applied every few seconds.",
|
||||
"Pins Steam, its UI helper, and gamescope to the cores below. Reapplied every few seconds.",
|
||||
clientOverride, onChange = onClientOverride,
|
||||
)
|
||||
MultiRow(
|
||||
host, "clientCores", "Client cores", if (clientOverride) "Every core ticked is the usual fix." else "Turn the override on to choose.",
|
||||
coreItems, clientCores, enabled = clientOverride, note = "Tick or untick as many as you like; the menu stays open.", onToggle = onClientCore,
|
||||
host, "clientCores", "Client cores", if (clientOverride) "Choose cores for Steam." else "Enable the override to choose.",
|
||||
coreItems, clientCores, enabled = clientOverride, onToggle = onClientCore,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Game cores") {
|
||||
MultiRow(
|
||||
host, "gameCores", "Game cores",
|
||||
"Applied by exec'ing the game through taskset, so every thread inherits the mask. Every core ticked sends nothing - that is what the scheduler does unaided. Untick the small cores to keep a heavy game off them.",
|
||||
coreItems, gameCores, note = "Tick or untick as many as you like; the menu stays open.", onToggle = onGameCore,
|
||||
"Choose cores for games. All selected lets Android schedule across every core.",
|
||||
coreItems, gameCores, onToggle = onGameCore,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Client interface") {
|
||||
ToggleRow(
|
||||
host, "glthread", "Threaded GL",
|
||||
"The client's menus are drawn Chromium → ANGLE → Zink → Turnip, and that chain is what limits them. mesa_glthread marshals GL off the calling thread, the shape of this bottleneck. On by default; none of these four is device-proven yet.",
|
||||
"May improve Steam menu responsiveness. Device impact is unverified.",
|
||||
glThread, onChange = onGlThread,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "zink", "Zink: lazy descriptors",
|
||||
"Lazy descriptor updates is the mode Zink recommends on drivers without descriptor buffers. On by default.",
|
||||
"Recommended for drivers without descriptor buffers.",
|
||||
zinkLazy, onChange = onZinkLazy,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "noglerror", "Skip GL error checks",
|
||||
"MESA_NO_ERROR: the driver stops validating every GL call. On by default.",
|
||||
"Disables per-call GL validation.",
|
||||
noGlError, onChange = onNoGlError,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "deck", "Steam Deck mode",
|
||||
"Runs the client with -steamdeck: the Quick Access Menu, the performance overlay and the Deck settings pages. The SteamOS helpers it calls are stubbed and the battery it reads is provided, so the mode is safe to try; off by default until it is proven on more devices.",
|
||||
"Runs the client with -steamdeck. SteamOS helpers and battery info are provided.",
|
||||
steamDeckMode, onChange = onSteamDeckMode,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Session fixes") {
|
||||
ToggleRow(
|
||||
host, "sysmem", "Turnip: sysmem rendering",
|
||||
"Renders without the GPU's tile memory (TU_DEBUG=sysmem). A fix for an Adreno 8xx that shows corruption, and required on a 710/720/722 (set on its own there). Not a speed setting: elsewhere it COSTS frames - leave it off unless the picture is wrong.",
|
||||
"Required on Adreno 710/720/722. May fix corruption on other Adreno GPUs, but can reduce performance.",
|
||||
tuSysmem, onChange = onTuSysmem,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "xalia", "Skip Steam's xalia helper",
|
||||
"xalia is a Windows program Proton starts for gamepad navigation in Windows programs. On some devices its system calls are refused in a way it cannot cope with and the session dies seconds after Big Picture appears. Turn on if a session will not stay up.",
|
||||
"Disables Proton's gamepad navigation helper. Try if sessions crash at startup.",
|
||||
noXalia, onChange = onNoXalia,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "seccomp", "Run proot without seccomp",
|
||||
"proot normally lets most system calls run untraced, which is most of its speed. Some kernels handle that badly and refuse calls that plainly exist (\"Function not implemented\" in the log). This traces everything: slower, but correct.",
|
||||
"May fix missing syscall errors on some kernels, but can reduce performance.",
|
||||
prootNoSeccomp, onChange = onProotNoSeccomp,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ fun ProtonDialog(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Native ARM64 Proton builds beside the ARM64 Proton Valve ships. Installed when a session starts - a large download - and then chosen per game in Steam under Properties → Compatibility.",
|
||||
"Adds ARM64 Proton builds. Downloads at next session start; select per game in Steam > Properties > Compatibility.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -19,20 +19,18 @@ fun RomsDialog(path: String?, onChoose: () -> Unit, onClear: () -> Unit, onDismi
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"A folder on this device that every session shows as ROMs in the home folder " +
|
||||
"(/root/ROMs) - open it from any emulator's file dialog. All of internal " +
|
||||
"storage is there too, as Storage.",
|
||||
"This folder appears as /root/ROMs in each session. Internal storage is available at /root/Storage.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
path ?: "No folder chosen yet.",
|
||||
path ?: "No folder chosen",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (path != null) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
"Takes effect at the next session. A folder on an SD card works too.",
|
||||
"Applies next session. SD cards are supported.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -265,7 +265,14 @@ fun ActionRow(label: String, hint: String?, button: String, onClick: () -> Unit)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsPage(host: MenuHost, eyebrow: String, title: String, lede: String, onBack: () -> Unit, content: @Composable ColumnScope.() -> Unit) {
|
||||
fun SettingsPage(
|
||||
host: MenuHost,
|
||||
title: String,
|
||||
onBack: () -> Unit,
|
||||
eyebrow: String? = null,
|
||||
lede: String? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val dim by animateFloatAsState(if (host.open != null) 0.6f else 1f, Motion.tw(220), label = "pageDim")
|
||||
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 22.dp, vertical = 18.dp)) {
|
||||
@@ -275,12 +282,14 @@ fun SettingsPage(host: MenuHost, eyebrow: String, title: String, lede: String, o
|
||||
"‹ Back", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = colors.onSurfaceVariant,
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).clickable(onClick = onBack).padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Eyebrow(eyebrow)
|
||||
if (eyebrow != null) {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Eyebrow(eyebrow)
|
||||
}
|
||||
}
|
||||
}
|
||||
Rise(1) { Title(title) }
|
||||
Rise(2) { Lede(lede) }
|
||||
if (lede != null) Rise(2) { Lede(lede) }
|
||||
Rise(3, Modifier.weight(1f).fillMaxWidth()) {
|
||||
Column(modifier = Modifier.fillMaxSize().graphicsLayer { alpha = dim }.verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) { content() }
|
||||
}
|
||||
|
||||
@@ -1,51 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">SteamDeck</string>
|
||||
<string name="subtitle">Valve\'s native ARM64 Steam client, under gamescope</string>
|
||||
<string name="play">Play</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="working">Working…</string>
|
||||
<string name="runtime_missing">Linux runtime not installed</string>
|
||||
<string name="install_runtime">Install Linux runtime</string>
|
||||
<string name="update_runtime">Update Linux runtime</string>
|
||||
<string name="remove_runtime">Remove Linux runtime</string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="remove_runtime_message">This deletes the runtime, the Steam client inside it, and every game installed there.</string>
|
||||
<string name="runtime_installed">Runtime %1$s installed</string>
|
||||
<string name="runtime_update">Runtime %1$s installed · %2$s available</string>
|
||||
<string name="runtime_download_size">%1$s · %2$s download, one time</string>
|
||||
<string name="catalog_unreachable">Could not reach the runtime catalog</string>
|
||||
<string name="install_failed">Install failed - nothing was changed</string>
|
||||
<string name="session_starting">Starting the session…</string>
|
||||
<string name="loading_title">Steam is loading</string>
|
||||
<string name="loading_elapsed">%1$s elapsed · still working</string>
|
||||
<string name="loading_download">Downloading the Steam client update · %1$d%%</string>
|
||||
<string name="loading_client_download">Downloading the Steam client · %1$s</string>
|
||||
<string-array name="loading_hints">
|
||||
<item>The first run downloads the Steam client from Valve and then its update - a few minutes on a good connection.</item>
|
||||
<item>The screen stays black until the client draws its first frame; nothing is stuck while this clock is moving.</item>
|
||||
<item>You can leave. Steam keeps running in the background and the notification brings you back.</item>
|
||||
<item>A controller shows up in Steam as an Xbox 360 pad. Without one, the on-screen pad appears.</item>
|
||||
<item>Games you install go through Valve\'s own ARM64 Proton; the client fetches it the first time it is needed.</item>
|
||||
<item>First run downloads Steam. This may take a few minutes.</item>
|
||||
<item>Steam keeps running in the background.</item>
|
||||
<item>Your controller appears as an Xbox 360 pad. Otherwise, use the on-screen controls.</item>
|
||||
</string-array>
|
||||
<string name="session_ended">The session ended (%1$d)\n%2$s</string>
|
||||
<string name="session_channel">Session</string>
|
||||
<string name="session_channel_description">Keeps Steam running while the app is in the background</string>
|
||||
<string name="session_notification">Steam is running - leave this notification alone</string>
|
||||
<string name="session_channel_description">Keeps Steam running in the background</string>
|
||||
<string name="session_notification">Steam is running</string>
|
||||
<string name="stop_session">Stop session</string>
|
||||
<string name="frame_gen">Frame generation: %1$s</string>
|
||||
<string name="frame_gen_title">Frame generation</string>
|
||||
<string name="frame_gen_help">Extra frames are generated between the real ones on the way to the screen, so a game running at 30 fps looks like 60. Win-FG is built in. LSFG uses the shaders from your own copy of Lossless Scaling - install it from the Steam client in this app first. Takes effect immediately, even mid-game.</string>
|
||||
<string name="drawer_hud">Performance HUD</string>
|
||||
<string name="drawer_osc">On-screen controls: %1$s</string>
|
||||
<string name="drawer_shape">Display shape: %1$s · next session</string>
|
||||
<string name="shape_auto">panel</string>
|
||||
<string name="shape_wide">16:9</string>
|
||||
<string name="drawer_background">Send to background</string>
|
||||
<string name="osc_auto">auto</string>
|
||||
<string name="osc_always">always</string>
|
||||
<string name="osc_never">never</string>
|
||||
<string name="credits_line">by The412Banner and maxjivi05 · credits</string>
|
||||
<string name="credits_title">Credits</string>
|
||||
<string name="credits_body">SteamDeck\n\nThe412Banner - the app, the Wayland compositor, the runtime and the Steam session.\n\nmaxjivi05 (Max) - the gamescope runtime this is built on: the proot session, the session shim, the fake-evdev interposer and the controller work, from WinNative.\n\nGPL-3.0. Valve, Steam, Steam Deck and Proton are trademarks of Valve Corporation; this project is not affiliated with Valve. gamescope, Mesa, Turnip, Xwayland, PulseAudio, proot and Arch Linux ARM are their authors\' own.</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user