ui: reorganize settings by scope

This commit is contained in:
xXJsonDeruloXx committed 2026-09-29 13:48:25 -04:00
1 parent f4d2c37fbd
commit f5853bcd4a
6 files changed
+92 -45

No files matched your search

@@ -366,10 +366,6 @@ class MainActivity : ComponentActivity() {
}
}, "share-logs").start()
},
onOffline = {
OfflineMode.setEnabled(this, !OfflineMode.enabled(this))
offline = OfflineMode.enabled(this)
},
onCredits = { showCredits = true },
onPageBack = { settingsMode = null; showPerformance = false; showProtons = false; showComponents = false; showMapping = false },
onTheme = { id -> SessionPrefs.setTheme(this, id); theme = id },
@@ -816,6 +812,8 @@ class MainActivity : ComponentActivity() {
deckyLatestRelease = if (mode == SessionService.MODE_STEAM) decky.deckyReleases.firstOrNull() else null,
deckyChecking = decky.deckyChecking, deckyStage = decky.deckyStage, deckyPercent = decky.deckyPercent,
deckyEnabled = decky.deckySupervisor, deckySessionRunning = SessionState.running,
offlineAccount = if (mode == SessionService.MODE_STEAM) offlineAccount else null,
offline = mode == SessionService.MODE_STEAM && offline,
),
ModeSettingsActions(
onResolution = { cap -> SessionPrefs.setResolutionCap(this, mode, cap); resolutionCap = cap },
@@ -866,6 +864,10 @@ class MainActivity : ComponentActivity() {
SessionPrefs.setRunSteamAtStartup(this, on)
runSteamAtStartup = on
},
onOffline = {
OfflineMode.setEnabled(this, !OfflineMode.enabled(this))
offline = OfflineMode.enabled(this)
},
onPickAddedGamesDir = { pickAddedGamesDir.launch(InAppFilePicker.buildDirIntent(this, "Choose a folder of your own games", addedGamesDirs.lastOrNull())) },
onAddedGamesArt = { on -> SessionPrefs.setAddedGamesArt(this, on); addedGamesArt = on; if (on) refreshAddedGames() },
onForgetAddedGamesDir = { dir -> SessionPrefs.setAddedGamesDirs(this, addedGamesDirs - dir); addedGamesDirs = SessionPrefs.addedGamesDirs(this); refreshAddedGames(); refresh() },
@@ -894,6 +896,12 @@ class MainActivity : ComponentActivity() {
clientOverride = clientOverride, clientCores = clientCores, gameCores = gameCores,
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia,
prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning,
frameGenEngine = FrameGen.engine(this), frameGenMultiplier = FrameGen.multiplier(this),
lsfgReady = LsfgNative.isInstalled(this),
onFrameGenPick = { engine, multiplier ->
FrameGen.set(this, engine, multiplier)
frameGenLabel = FrameGen.label(this)
},
onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on },
onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on },
onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on },
@@ -139,7 +139,6 @@ class FrontEndActions(
val onBrowseFiles: (File) -> Unit = {},
val onLogs: () -> Unit,
val onShareLogs: () -> Unit = {},
val onOffline: () -> Unit,
val onCredits: () -> Unit,
val onPageBack: () -> Unit = {},
val onTheme: (String) -> Unit = {},
@@ -158,8 +158,8 @@ internal fun SetupPanel(
var showLimitDetails by rememberSaveable { mutableStateOf(false) }
val checks = 4
val readyCount = listOf(gpuOk, s.ready && !s.busy, !limitBlocks, signedIn).count { it }
// Five tabs instead of one long scroll; LB and RB turn them from anywhere on the page.
val tabs = listOf("Overview", "Controller", "Session", "Launcher", "About")
// Four durable buckets instead of a miscellaneous Session tab; LB and RB turn them from anywhere on the page.
val tabs = listOf("Overview", "Controller", "Launcher", "Support")
var tab by rememberSaveable { mutableStateOf(0) }
val tabFocus = remember { List(tabs.size) { FocusRequester() } }
var tabTurned by remember { mutableStateOf(false) }
@@ -258,9 +258,7 @@ internal fun SetupPanel(
ControllerRows(host, s.oscMode, controller, a.controller)
}
if (s.controller == null || a.controller == null) Note("Controller settings are unavailable.")
}
2 -> {
SettingsGroup("Session") {
SettingsGroup("Navigation") {
ChoiceRow(
host, "back-actions", "Back", SessionPrefs.backActionsOrder(s.backActionsInverted),
listOf(
@@ -268,22 +266,9 @@ internal fun SetupPanel(
true to SessionPrefs.BACK_QAM_THEN_MENU,
), s.backActionsInverted, onPick = a.onBackActionsInverted,
)
SettingsRow("Frame generation", "Select the frame generation mode") {
Box {
ValueChip(s.frameGenLabel, host.open == "fg") { host.open = if (host.open == "fg") null else "fg" }
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)
ToggleRow(
host, "offline", "Offline mode",
s.offlineAccount?.let { "Signed in as $it" } ?: "Sign in to Steam first",
s.offline, enabled = s.offlineAccount != null,
) { a.onOffline() }
}
}
3 -> {
2 -> {
SettingsGroup("Launcher") {
SettingsRow("Theme", "Choose the launcher appearance") {
Box {
@@ -314,6 +299,10 @@ internal fun SetupPanel(
}
}
else -> {
SettingsGroup("Support") {
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)
}
SettingsGroup("About") {
ActionRow("Build", s.buildLabel, "Check for newer", a.onCheckLatestBuild)
ActionRow("Credits", "The people and projects DroidDeck builds on", "View", a.onCredits)
@@ -332,7 +321,7 @@ private fun ToolGrid(s: FrontEndState, a: FrontEndActions) {
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.Speed, "Performance", "CPU, rendering and session tuning", a.onPerformance),
ToolSpec(Icons.Outlined.VideogameAsset, "ROMs folder", s.romsDir ?: "Choose where emulator games are stored", a.onRoms),
)
Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
@@ -1,6 +1,7 @@
package com.droiddeck.launcher.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Row
@@ -15,6 +16,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.droiddeck.launcher.R
@@ -72,6 +74,9 @@ class ModeSettings(
val steamDeckMode: Boolean = false,
/** Steam only: start a Steam session when DroidDeck opens. */
val runSteamAtStartup: Boolean = false,
/** Steam only: remembered login and requested offline state. */
val offlineAccount: String? = null,
val offline: Boolean = false,
/** Steam only: the user's chosen Games folders; null outside Steam. */
val addedGamesDirs: List<String>? = null,
val addedGames: List<AddedGameRow> = emptyList(),
@@ -127,6 +132,7 @@ class ModeSettingsActions(
val onSteamChannel: (String) -> Unit = {},
val onSteamDeckMode: (Boolean) -> Unit = {},
val onRunSteamAtStartup: (Boolean) -> Unit = {},
val onOffline: () -> Unit = {},
val onPickAddedGamesDir: () -> Unit = {},
val onForgetAddedGamesDir: (path: String) -> Unit = {},
val onAddedGamesArt: (Boolean) -> Unit = {},
@@ -150,6 +156,22 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
var driverPage by remember { mutableStateOf<String?>(null) }
var returnTo by remember { mutableStateOf<String?>(null) }
val pageScroll = androidx.compose.foundation.rememberScrollState()
val tabs = if (steam) listOf("Display", "Controls", "Runtime", "Steam", "Games")
else listOf("Display", "Controls", "Runtime")
var tab by rememberSaveable(s.mode) { mutableStateOf(0) }
val tabFocus = remember { List(tabs.size) { androidx.compose.ui.focus.FocusRequester() } }
var tabTurned by remember { mutableStateOf(false) }
val inputModeManager = androidx.compose.ui.platform.LocalInputModeManager.current
val pickTab: (Int) -> Unit = { index -> host.open = null; tab = index; tabTurned = true }
androidx.compose.runtime.LaunchedEffect(tab) {
if (tabTurned) {
pageScroll.scrollTo(0)
if (inputModeManager.inputMode == androidx.compose.ui.input.InputMode.Keyboard) {
androidx.compose.runtime.withFrameNanos { }
runCatching { tabFocus[tab].requestFocus() }
}
}
}
val runtimeChip = remember { androidx.compose.ui.focus.FocusRequester() }
val displayChip = remember { androidx.compose.ui.focus.FocusRequester() }
val firstChip = remember { androidx.compose.ui.focus.FocusRequester() }
@@ -196,7 +218,14 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
onBack = a.onDismiss,
scroll = pageScroll,
) {
SettingsGroup("Display") {
Column(
modifier = Modifier.fillMaxWidth().bumpers(
onPrevious = { pickTab((tab + tabs.size - 1) % tabs.size) },
onNext = { pickTab((tab + 1) % tabs.size) },
),
) {
TabStrip(tabs, tab, pickTab, Modifier.padding(bottom = 4.dp), tabFocus)
if (tab == 0) SettingsGroup("Display") {
val default = SessionPrefs.defaultResolutionCap(s.mode)
var editCustom by remember { mutableStateOf(false) }
val custom = s.customResolution
@@ -214,13 +243,18 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
if (custom != null) "Set by the custom resolution." else "Auto uses at least 16:9.",
com.droiddeck.launcher.session.SessionPrefs.shapeChoices, s.shapeMode, enabled = custom == null, onPick = a.onShape,
)
if (steam && s.forceFullscreen != null) ToggleRow(
host, "fill", "Stretch games to fill the screen",
"Keeps games that resize their own window full screen. Turn it off if a game shows up small in a corner. Applies next session.",
s.forceFullscreen, onChange = a.onForceFullscreen,
)
if (editCustom) CustomResolutionDialog(
initial = custom,
onSave = { size -> editCustom = false; a.onCustomResolution(size) },
onDismiss = { editCustom = false },
)
}
SettingsGroup("HDR") {
if (tab == 0) SettingsGroup("HDR") {
ToggleRow(
host, "hdr", "HDR10 output",
s.hdrReason?.let { "Not available: $it." }
@@ -228,7 +262,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
checked = s.hdr && s.hdrReason == null, enabled = s.hdrReason == null, onChange = a.onHdr,
)
}
SettingsGroup("Drivers") {
if (tab == 2) SettingsGroup("Drivers") {
SettingsRow("Runtime driver", (if (steam) "Used by Steam and games." else "Used by desktop apps.") + " Applies next session.") {
ValueChip(
s.linuxRows.firstOrNull { it.id == s.linuxSelected }?.name ?: "Runtime default", open = false,
@@ -242,7 +276,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
) { openDriverPage("panel") }
}
}
SettingsGroup(if (steam) "Touch & controls" else "Touch") {
if (tab == 1) SettingsGroup("Controls") {
ChoiceRow(
host, "touch", "Touch", null,
listOf("auto" to "Auto", "touchpad" to "Touchpad", "direct" to "Direct"), s.touchMode,
@@ -267,7 +301,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
), s.backActionsInverted, onPick = a.onBackActionsInverted,
)
}
SettingsGroup("Session") {
if (tab == 2) SettingsGroup("Session") {
ChoiceRow(
host, "suspend", "Background behavior",
"How this session behaves when the app leaves the screen or the display turns off.",
@@ -281,14 +315,19 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
onPick = a.onSuspendPolicy,
)
}
if (steam) SettingsGroup("Startup") {
if (steam && tab == 3) SettingsGroup("Startup & account") {
ToggleRow(
host, "steam-startup", "Run Steam when DroidDeck starts",
"Open the Steam session when you launch DroidDeck.",
s.runSteamAtStartup, onChange = a.onRunSteamAtStartup,
)
ToggleRow(
host, "offline", "Offline mode",
s.offlineAccount?.let { "Signed in as $it" } ?: "Sign in to Steam first",
s.offline, enabled = s.offlineAccount != null,
) { a.onOffline() }
}
if (steam) SettingsGroup("Decky") {
if (steam && tab == 3) SettingsGroup("Decky") {
val updateAvailable = s.deckyInstalled != null && s.deckyLatestRelease != null &&
s.deckyInstalled != s.deckyLatestRelease.tag
val status = when {
@@ -340,7 +379,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
)
}
}
if (steam && s.steamChannel != null) SettingsGroup("Client") {
if (steam && tab == 3 && s.steamChannel != null) SettingsGroup("Client") {
ToggleRow(
host, "steamdeck", "Steam Deck mode",
"Enables Steam's Deck interface and Quick Access performance overlay controls. Applies next session.",
@@ -353,7 +392,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
onPick = a.onSteamChannel,
)
}
if (steam && s.addedGamesDirs != null) SettingsGroup("Added games") {
if (steam && tab == 4 && s.addedGamesDirs != null) SettingsGroup("Added games") {
for (dir in s.addedGamesDirs) {
val n = s.addedGames.count { it.folderPath.startsWith("$dir/") }
ActionRow(
@@ -378,20 +417,15 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
onPick = { path -> if (path == "__pick__") a.onPickAddedGameExe(g.folderPath) else a.onAddedGameExe(g.folderPath, path) },
)
}
if (steam && s.fexPreset != null) SettingsGroup(stringResource(R.string.game_settings_title)) {
if (steam && tab == 4 && s.fexPreset != null) SettingsGroup(stringResource(R.string.game_settings_title)) {
ChoiceRow(
host, "fex", stringResource(R.string.fex_preset_title), stringResource(R.string.fex_next_launch),
FexPreset.all.map { it.id to stringResource(it.label) }, s.fexPreset,
note = stringResource(FexPreset.byId(s.fexPreset).detail), onPick = a.onFexPreset,
)
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.",
s.forceFullscreen, onChange = a.onForceFullscreen,
)
}
if (steam && s.directAudio != null && s.mic != null) SettingsGroup("Audio") {
if (steam && tab == 2 && 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)
ChoiceRow(
host, "clientAudio", "Steam client audio", "Classic is the AAudio sink from 0.1.5. DirectAudio goes through the relay. Applies next session.",
@@ -400,7 +434,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
)
ToggleRow(host, "mic", "Microphone", "Uses the device microphone for voice chat.", s.mic, onChange = a.onMic)
}
if (steam && s.gameStorage != null) SettingsGroup("Game storage") {
if (steam && tab == 4 && s.gameStorage != null) SettingsGroup("Game 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 "")))
@@ -431,7 +465,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
}
}
}
if (!steam && s.renderer != null) SettingsGroup("Renderer") {
if (!steam && tab == 2 && s.renderer != null) SettingsGroup("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,
@@ -441,6 +475,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
onPick = a.onRenderer,
)
}
}
}
if (confirmDeckyRemoval) AlertDialog(
onDismissRequest = { confirmDeckyRemoval = false },
@@ -22,6 +22,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.droiddeck.launcher.gpu.FrameGen
import com.droiddeck.launcher.session.SessionPrefs
class CoreRow(val core: Int, val label: String)
@@ -40,6 +41,10 @@ fun PerformancePage(
prootNoSeccomp: Boolean,
guestHostname: String,
phantomWarning: String?,
frameGenEngine: String,
frameGenMultiplier: Int,
lsfgReady: Boolean,
onFrameGenPick: (engine: String, multiplier: Int) -> Unit,
onClientOverride: (Boolean) -> Unit,
onTuSysmem: (Boolean) -> Unit,
onZinkLazy: (Boolean) -> Unit,
@@ -57,9 +62,20 @@ fun PerformancePage(
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.",
lede = "Rendering, CPU cores and session tuning. Most changes apply next session.",
onBack = onDismiss,
) {
SettingsGroup("Rendering") {
val fgOpen = host.open == "fg"
SettingsRow("Frame generation", "Default for game launches; it can still be changed per game or while running.", highlighted = fgOpen) {
androidx.compose.foundation.layout.Box {
ValueChip(FrameGen.label(frameGenEngine, frameGenMultiplier), fgOpen) {
host.open = if (fgOpen) null else "fg"
}
FrameGenMenu(host, frameGenEngine, frameGenMultiplier, lsfgReady, onFrameGenPick)
}
}
}
SettingsGroup("Steam client cores") {
ToggleRow(
host, "override", "Override Steam's own core choice",
@@ -102,7 +102,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("Display", "Controls", "Components", "Session")
/** 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)