mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
ui: rebuild the game environment editor in the app's own style
The settings row gets a one-line hint and the same secondary button as every other row. The editor moves from a stock Material alert, with a paragraph of explanation and a column of text buttons, into the app's dialog: a profile chip with its menu, the variables in a panel with each one's value as a chip whose menu changes, removes or restores it, and a footer to add a variable, reset the profile or finish. Editing a variable uses the same dialog, its name and value as chips or the app's text fields, and the variable's note in the footer.
This commit is contained in:
1 parent
25a5d3fe5d
commit
f8feb63dde
3 files changed
+218
-158
No files matched your search
@@ -272,7 +272,7 @@ internal fun EditAppDialog(app: UserApps.App, onDismiss: () -> Unit, onSave: (St
|
||||
|
||||
/** Open from the first frame; [onDismiss] once the closing animation has run. */
|
||||
@Composable
|
||||
private fun rememberShown(onDismiss: () -> Unit): MutableTransitionState<Boolean> {
|
||||
internal fun rememberShown(onDismiss: () -> Unit): MutableTransitionState<Boolean> {
|
||||
val shown = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(shown.currentState, shown.isIdle) { if (shown.isIdle && !shown.currentState && !shown.targetState) onDismiss() }
|
||||
return shown
|
||||
@@ -283,7 +283,7 @@ private fun rememberShown(onDismiss: () -> Unit): MutableTransitionState<Boolean
|
||||
* like the app's menus, rises above the keyboard, and closes on a tap outside or B.
|
||||
*/
|
||||
@Composable
|
||||
private fun AppDialog(
|
||||
internal fun AppDialog(
|
||||
shown: MutableTransitionState<Boolean>, close: () -> Unit, label: String, wide: Boolean,
|
||||
modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
@@ -328,7 +328,7 @@ private fun AppDialog(
|
||||
|
||||
/** The eyebrow and title, with [trailing] (the tabs) beside them when there is room, else under them. */
|
||||
@Composable
|
||||
private fun DialogHeader(eyebrow: String, title: String, trailing: (@Composable () -> Unit)? = null) {
|
||||
internal fun DialogHeader(eyebrow: String, title: String, trailing: (@Composable () -> Unit)? = null) {
|
||||
val heading = @Composable {
|
||||
Column {
|
||||
Eyebrow(eyebrow)
|
||||
@@ -381,7 +381,7 @@ private fun Panel(title: String, content: @Composable ColumnScope.() -> Unit) {
|
||||
|
||||
/** Why the main button is off (or nothing), then Cancel and the main button, at the card's foot. */
|
||||
@Composable
|
||||
private fun DialogFooter(note: String?, confirm: String, enabled: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit) {
|
||||
internal fun DialogFooter(note: String?, confirm: String, enabled: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit) {
|
||||
val lineColor = LocalPalette.current.line
|
||||
Rise(2) {
|
||||
Row(
|
||||
@@ -396,7 +396,7 @@ private fun DialogFooter(note: String?, confirm: String, enabled: Boolean, onCan
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Small(text: String, error: Boolean = false) =
|
||||
internal fun Small(text: String, error: Boolean = false) =
|
||||
Text(text, fontSize = 12.sp, lineHeight = 16.sp, color = if (error) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
|
||||
private fun hideKeyboard(view: View) {
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.GameEnvironment
|
||||
import com.droiddeck.launcher.core.GameEnvironmentOptions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.session.GameEnvironmentStore
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
@@ -21,13 +43,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
// Environment variables for Proton games: one shared profile and one per game, in the app's own
|
||||
// dialog, with each variable's actions in a menu off its value as everywhere else.
|
||||
|
||||
@Composable
|
||||
fun GameEnvironmentRow(modifier: Modifier = Modifier) {
|
||||
var open by remember { mutableStateOf(false) }
|
||||
SettingsRow(stringResource(R.string.game_env_title), stringResource(R.string.game_env_hint)) {
|
||||
TextButton(onClick = { open = true }, modifier = modifier.controllerConfirm { open = true }) {
|
||||
Text(stringResource(R.string.game_env_edit))
|
||||
}
|
||||
SecondaryButton(stringResource(R.string.game_env_edit), modifier = modifier) { open = true }
|
||||
}
|
||||
if (open) GameEnvironmentEditor { open = false }
|
||||
}
|
||||
@@ -36,12 +59,13 @@ fun GameEnvironmentRow(modifier: Modifier = Modifier) {
|
||||
private fun GameEnvironmentEditor(onClose: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val coroutine = rememberCoroutineScope()
|
||||
val shown = rememberShown(onClose)
|
||||
val close = { shown.targetState = false }
|
||||
var config by remember { mutableStateOf<GameEnvironment.Config?>(null) }
|
||||
var games by remember { mutableStateOf(emptyList<Pair<String, String>>()) }
|
||||
var scope by remember { mutableStateOf("") }
|
||||
var appId by remember { mutableStateOf("") }
|
||||
var scopeMenu by remember { mutableStateOf(false) }
|
||||
var addMenu by remember { mutableStateOf(false) }
|
||||
var otherId by remember { mutableStateOf<String?>(null) }
|
||||
var menu by remember { mutableStateOf<String?>(null) }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var error by remember { mutableStateOf(false) }
|
||||
var editing by remember { mutableStateOf<Pair<String, String>?>(null) }
|
||||
@@ -62,69 +86,88 @@ private fun GameEnvironmentEditor(onClose: () -> Unit) {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!busy) onClose() },
|
||||
title = { Text(stringResource(R.string.game_env_title)) },
|
||||
confirmButton = { TextButton(onClick = onClose, enabled = !busy) { Text(stringResource(R.string.game_env_close)) } },
|
||||
text = {
|
||||
Column(Modifier.heightIn(max = 460.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(stringResource(R.string.game_env_hint))
|
||||
Text(stringResource(R.string.game_env_defaults), style = MaterialTheme.typography.bodySmall)
|
||||
if (error) Text(stringResource(R.string.game_env_error), color = MaterialTheme.colorScheme.error)
|
||||
if (config == null && !error) Text(stringResource(R.string.game_env_loading))
|
||||
val current = config
|
||||
if (current != null) {
|
||||
val profileName = if (scope.isEmpty()) stringResource(R.string.game_env_shared)
|
||||
else games.firstOrNull { it.first == scope }?.second ?: stringResource(R.string.game_env_profile, scope)
|
||||
Text(stringResource(R.string.game_env_scope), style = MaterialTheme.typography.labelMedium)
|
||||
Box {
|
||||
TextButton(onClick = { scopeMenu = true }, enabled = !busy) { Text(profileName) }
|
||||
DropdownMenu(expanded = scopeMenu, onDismissRequest = { scopeMenu = false }, modifier = Modifier.heightIn(max = 280.dp)) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.game_env_shared)) }, onClick = { scope = ""; scopeMenu = false })
|
||||
val profiles = (games + current.games.keys.filter { id -> games.none { it.first == id } }.map { it to it }).distinctBy { it.first }
|
||||
for ((id, name) in profiles) DropdownMenuItem(text = { Text(name) }, onClick = { scope = id; scopeMenu = false })
|
||||
}
|
||||
val current = config
|
||||
val shared = stringResource(R.string.game_env_shared)
|
||||
val profileName = if (scope.isEmpty()) shared
|
||||
else games.firstOrNull { it.first == scope }?.second ?: stringResource(R.string.game_env_profile, scope)
|
||||
|
||||
AppDialog(shown, close, "gameEnv", wide = false) {
|
||||
DialogHeader(stringResource(R.string.game_env_eyebrow), stringResource(R.string.game_env_title))
|
||||
Small(stringResource(R.string.game_env_applies))
|
||||
// Which profile: everything shared, or one game's.
|
||||
Panel {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.game_env_scope), fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1f))
|
||||
Box {
|
||||
ValueChip(profileName, menu == "scope", modifier = Modifier.widthIn(max = 260.dp)) {
|
||||
menu = if (menu == "scope") null else "scope"
|
||||
}
|
||||
OutlinedTextField(value = appId, onValueChange = { appId = it }, singleLine = true, enabled = !busy,
|
||||
label = { Text(stringResource(R.string.game_env_app_id)) }, modifier = Modifier.fillMaxWidth())
|
||||
TextButton(onClick = { scope = appId.toLong().toString(); appId = "" }, enabled = !busy && appId.isNotEmpty() && GameEnvironment.validScope(appId)) {
|
||||
Text(stringResource(R.string.game_env_open))
|
||||
}
|
||||
Text(stringResource(R.string.game_env_overrides), style = MaterialTheme.typography.bodySmall)
|
||||
val entries = GameEnvironment.effective(current, preset, scope).toSortedMap()
|
||||
if (entries.isEmpty()) Text(stringResource(R.string.game_env_empty))
|
||||
for ((name, value) in entries) {
|
||||
HorizontalDivider()
|
||||
Text(name, style = MaterialTheme.typography.titleSmall)
|
||||
Text(value ?: stringResource(R.string.game_env_unset))
|
||||
if (!current.entries(scope).containsKey(name)) Text(stringResource(R.string.game_env_inherited), style = MaterialTheme.typography.labelSmall)
|
||||
Row {
|
||||
TextButton(onClick = { editing = name to (value ?: "") }, enabled = !busy) { Text(stringResource(R.string.game_env_edit)) }
|
||||
if (value != null) TextButton(onClick = { save(current.withEntries(scope, current.entries(scope) + (name to null))) }, enabled = !busy) {
|
||||
Text(stringResource(R.string.game_env_remove))
|
||||
}
|
||||
}
|
||||
if (current.entries(scope).containsKey(name)) TextButton(onClick = { save(current.withEntries(scope, current.entries(scope) - name)) }, enabled = !busy) {
|
||||
Text(stringResource(R.string.game_env_restore))
|
||||
}
|
||||
}
|
||||
Box {
|
||||
TextButton(onClick = { addMenu = true }, enabled = !busy) { Text(stringResource(R.string.game_env_add)) }
|
||||
DropdownMenu(expanded = addMenu, onDismissRequest = { addMenu = false }, modifier = Modifier.heightIn(max = 280.dp)) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.game_env_custom)) }, onClick = { editing = "" to ""; addMenu = false })
|
||||
for (option in GameEnvironmentOptions.all) DropdownMenuItem(text = { Text(option.name) }, onClick = { editing = option.name to option.value; addMenu = false })
|
||||
}
|
||||
}
|
||||
TextButton(onClick = { save(current.withEntries(scope, emptyMap())) }, enabled = !busy && current.entries(scope).isNotEmpty()) {
|
||||
Text(stringResource(R.string.game_env_reset))
|
||||
AnchoredMenu(menu == "scope", onDismiss = { if (menu == "scope") menu = null }, title = stringResource(R.string.game_env_scope)) { first ->
|
||||
MenuItem(shared, checked = scope.isEmpty(), focusRequester = first) { scope = ""; otherId = null; menu = null }
|
||||
val profiles = (games + current?.games?.keys.orEmpty().filter { id -> games.none { it.first == id } }.map { it to it }).distinctBy { it.first }
|
||||
for ((id, name) in profiles) MenuItem(name, checked = scope == id) { scope = id; otherId = null; menu = null }
|
||||
MenuItem(stringResource(R.string.game_env_other), checked = false) { otherId = ""; menu = null }
|
||||
}
|
||||
}
|
||||
if (busy) LinearProgressIndicator(Modifier.fillMaxWidth())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
otherId?.let { id ->
|
||||
Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
AdbTextField(
|
||||
id, { otherId = it.filter(Char::isDigit) }, stringResource(R.string.game_env_app_id), KeyboardType.Number, ImeAction.Done,
|
||||
modifier = Modifier.weight(1f), compact = true,
|
||||
onDone = { if (GameEnvironment.validScope(id)) { scope = id.toLong().toString(); otherId = null } },
|
||||
)
|
||||
SecondaryButton(stringResource(R.string.game_env_open), enabled = id.isNotEmpty() && GameEnvironment.validScope(id)) {
|
||||
scope = id.toLong().toString(); otherId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error) Small(stringResource(R.string.game_env_error), error = true)
|
||||
if (current == null && !error) Small(stringResource(R.string.game_env_loading))
|
||||
if (current != null) {
|
||||
val own = current.entries(scope)
|
||||
val entries = GameEnvironment.effective(current, preset, scope).toSortedMap()
|
||||
if (entries.isEmpty()) Small(stringResource(R.string.game_env_empty))
|
||||
else Panel {
|
||||
entries.entries.forEachIndexed { i, (name, value) ->
|
||||
if (i > 0) Box(Modifier.fillMaxWidth().padding(horizontal = 14.dp).heightIn(min = 1.dp, max = 1.dp).background(LocalPalette.current.line))
|
||||
VariableRow(
|
||||
name, value, inherited = !own.containsKey(name), open = menu == "var:$name", enabled = !busy,
|
||||
onToggle = { menu = if (menu == "var:$name") null else "var:$name" }, onDismiss = { if (menu == "var:$name") menu = null },
|
||||
onEdit = { menu = null; editing = name to (value ?: "") },
|
||||
// A value set here or inherited can be switched off for this profile; one set here can go back to inheriting.
|
||||
onUnset = if (value != null) ({ menu = null; save(current.withEntries(scope, own + (name to null))) }) else null,
|
||||
onRestore = if (own.containsKey(name)) ({ menu = null; save(current.withEntries(scope, own - name)) }) else null,
|
||||
shared = scope.isEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (busy) LinearProgressIndicator(Modifier.fillMaxWidth())
|
||||
// Footer: add a variable, reset the profile, done.
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(top = 4.dp)) {
|
||||
Box {
|
||||
SecondaryButton(stringResource(R.string.game_env_add), enabled = current != null && !busy) { menu = if (menu == "add") null else "add" }
|
||||
AnchoredMenu(menu == "add", onDismiss = { if (menu == "add") menu = null }, title = stringResource(R.string.game_env_add)) { first ->
|
||||
for ((i, option) in GameEnvironmentOptions.all.withIndex()) MenuItem(option.name, checked = false, focusRequester = if (i == 0) first else null) {
|
||||
menu = null; editing = option.name to option.value
|
||||
}
|
||||
MenuItem(stringResource(R.string.game_env_custom), checked = false) { menu = null; editing = "" to "" }
|
||||
}
|
||||
}
|
||||
if (current != null && current.entries(scope).isNotEmpty()) FocusText(stringResource(R.string.game_env_reset), LocalPalette.current.signal) {
|
||||
save(current.withEntries(scope, emptyMap()))
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
PrimaryButton(stringResource(R.string.game_env_done), enabled = !busy, onClick = close)
|
||||
}
|
||||
}
|
||||
editing?.let { (name, value) ->
|
||||
VariableDialog(name, value, onDismiss = { editing = null }) { key, content ->
|
||||
VariableDialog(name, value, profileName, onDismiss = { editing = null }) { key, content ->
|
||||
config?.let {
|
||||
val entries = it.entries(scope).toMutableMap()
|
||||
if (name.isNotEmpty() && name != key) entries[name] = null
|
||||
@@ -136,99 +179,111 @@ private fun GameEnvironmentEditor(onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The panel the profile and the variables sit in, as a settings group does. */
|
||||
@Composable
|
||||
private fun VariableDialog(initialName: String, initialValue: String, onDismiss: () -> Unit, onSave: (String, String) -> Unit) {
|
||||
private fun Panel(content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit) =
|
||||
Column(Modifier.fillMaxWidth().clip(Shape14).background(MaterialTheme.colorScheme.surface).border(1.dp, LocalPalette.current.line, Shape14), content = content)
|
||||
|
||||
/** One variable: its name, what it is set to (and whether that comes from the shared profile), and a menu of what to do with it. */
|
||||
@Composable
|
||||
private fun VariableRow(
|
||||
name: String, value: String?, inherited: Boolean, open: Boolean, enabled: Boolean, shared: Boolean,
|
||||
onToggle: () -> Unit, onDismiss: () -> Unit, onEdit: () -> Unit, onUnset: (() -> Unit)?, onRestore: (() -> Unit)?,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(name, fontSize = 13.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (inherited) Text(stringResource(if (shared) R.string.game_env_default else R.string.game_env_inherited), fontSize = 12.sp, color = colors.onSurfaceVariant)
|
||||
}
|
||||
Box {
|
||||
ValueChip(value ?: stringResource(R.string.game_env_off), open, enabled = enabled, modifier = Modifier.widthIn(max = 200.dp), onClick = onToggle)
|
||||
AnchoredMenu(open, onDismiss = onDismiss, title = name) { first ->
|
||||
MenuItem(stringResource(R.string.game_env_change), checked = false, focusRequester = first, onClick = onEdit)
|
||||
if (onUnset != null) MenuItem(stringResource(if (shared) R.string.game_env_remove else R.string.game_env_unset), checked = false, onClick = onUnset)
|
||||
if (onRestore != null) MenuItem(stringResource(if (shared) R.string.game_env_restore_default else R.string.game_env_restore), checked = false, onClick = onRestore)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VariableDialog(initialName: String, initialValue: String, profile: String, onDismiss: () -> Unit, onSave: (String, String) -> Unit) {
|
||||
val shown = rememberShown(onDismiss)
|
||||
val close = { shown.targetState = false }
|
||||
var name by remember { mutableStateOf(initialName) }
|
||||
var value by remember { mutableStateOf(initialValue) }
|
||||
val valid = GameEnvironment.validName(name) && GameEnvironment.validValue(value)
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.game_env_edit)) },
|
||||
confirmButton = { TextButton(onClick = { onSave(name, value) }, enabled = valid) { Text(stringResource(R.string.game_env_save)) } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.game_env_cancel)) } },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
VariableNamePicker(name) { selected ->
|
||||
name = selected
|
||||
value = GameEnvironmentOptions.find(selected)?.value.orEmpty()
|
||||
}
|
||||
VariableValueEditor(name, value) { value = it }
|
||||
GameEnvironmentOptions.find(name)?.takeIf { it.detail != 0 }?.let {
|
||||
Text(stringResource(it.detail), style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (!GameEnvironment.supported(name)) Text(stringResource(R.string.game_env_unused))
|
||||
if (!valid && name.isNotEmpty()) Text(stringResource(R.string.game_env_invalid), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
)
|
||||
val option = GameEnvironmentOptions.find(name)
|
||||
AppDialog(shown, close, "gameEnvVar", wide = false) {
|
||||
DialogHeader(profile, stringResource(if (initialName.isEmpty()) R.string.game_env_add else R.string.game_env_change_title))
|
||||
VariableNamePicker(name) { selected ->
|
||||
name = selected
|
||||
value = GameEnvironmentOptions.find(selected)?.value.orEmpty()
|
||||
}
|
||||
VariableValueEditor(name, value) { value = it }
|
||||
if (!GameEnvironment.supported(name)) Small(stringResource(R.string.game_env_unused))
|
||||
if (!valid && name.isNotEmpty()) Small(stringResource(R.string.game_env_invalid), error = true)
|
||||
DialogFooter(
|
||||
note = option?.takeIf { it.detail != 0 }?.let { stringResource(it.detail) },
|
||||
confirm = stringResource(R.string.game_env_save), enabled = valid, onCancel = close,
|
||||
) { onSave(name, value); close() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VariableNamePicker(name: String, onChange: (String) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var open by remember { mutableStateOf(false) }
|
||||
var custom by remember { mutableStateOf(name.isEmpty() || GameEnvironmentOptions.find(name) == null) }
|
||||
Text(stringResource(R.string.game_env_name), style = MaterialTheme.typography.labelMedium)
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(if (custom) stringResource(R.string.game_env_custom) else name)
|
||||
}
|
||||
DropdownMenu(expanded, onDismissRequest = { expanded = false }, modifier = Modifier.heightIn(max = 280.dp)) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.game_env_custom)) }, onClick = {
|
||||
custom = true; onChange(""); expanded = false
|
||||
})
|
||||
for (option in GameEnvironmentOptions.all.sortedBy { it.name.uppercase(java.util.Locale.ROOT) }) {
|
||||
DropdownMenuItem(text = { Text(option.name) }, onClick = {
|
||||
custom = false; onChange(option.name); expanded = false
|
||||
})
|
||||
SettingsRow(stringResource(R.string.game_env_name), null, highlighted = open) {
|
||||
Box {
|
||||
ValueChip(if (custom) stringResource(R.string.game_env_custom) else name, open, modifier = Modifier.widthIn(max = 260.dp)) { open = !open }
|
||||
AnchoredMenu(open, onDismiss = { open = false }, title = stringResource(R.string.game_env_name)) { first ->
|
||||
for ((i, option) in GameEnvironmentOptions.all.sortedBy { it.name.uppercase(java.util.Locale.ROOT) }.withIndex()) {
|
||||
MenuItem(option.name, checked = !custom && option.name == name, focusRequester = if (i == 0) first else null) {
|
||||
custom = false; onChange(option.name); open = false
|
||||
}
|
||||
}
|
||||
MenuItem(stringResource(R.string.game_env_custom), checked = custom) { custom = true; onChange(""); open = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (custom) OutlinedTextField(value = name, onValueChange = onChange, singleLine = true,
|
||||
label = { Text(stringResource(R.string.game_env_name)) }, modifier = Modifier.fillMaxWidth())
|
||||
if (custom) AdbTextField(name, onChange, stringResource(R.string.game_env_name), KeyboardType.Ascii, ImeAction.Next, compact = true)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VariableValueEditor(name: String, value: String, onChange: (String) -> Unit) {
|
||||
val option = GameEnvironmentOptions.find(name)
|
||||
var expanded by remember(name) { mutableStateOf(false) }
|
||||
var open by remember(name) { mutableStateOf(false) }
|
||||
var custom by remember(name) { mutableStateOf(option == null || value !in option.choices && option.type != GameEnvironmentOptions.Type.MULTIPLE) }
|
||||
when {
|
||||
option?.type == GameEnvironmentOptions.Type.TOGGLE -> Row(
|
||||
Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
||||
) {
|
||||
Text(stringResource(R.string.game_env_value))
|
||||
Switch(checked = value == option.choices.last(), onCheckedChange = {
|
||||
onChange(if (it) option.choices.last() else option.choices.first())
|
||||
})
|
||||
option?.type == GameEnvironmentOptions.Type.TOGGLE -> SettingsRow(stringResource(R.string.game_env_value), null) {
|
||||
ToggleSwitch(checked = value == option.choices.last()) { onChange(if (it) option.choices.last() else option.choices.first()) }
|
||||
}
|
||||
option != null && option.choices.isNotEmpty() -> {
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(if (custom) stringResource(R.string.game_env_custom_value) else value.ifEmpty { stringResource(R.string.game_env_select_value) })
|
||||
}
|
||||
DropdownMenu(expanded, onDismissRequest = { expanded = false }, modifier = Modifier.heightIn(max = 280.dp)) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.game_env_custom_value)) }, onClick = { custom = true; expanded = false })
|
||||
for (choice in option.choices) {
|
||||
val selected = choice in value.split(',')
|
||||
DropdownMenuItem(text = { Text(choice) }, leadingIcon = {
|
||||
if (option.type == GameEnvironmentOptions.Type.MULTIPLE) Checkbox(checked = selected, onCheckedChange = null)
|
||||
}, onClick = {
|
||||
custom = false
|
||||
if (option.type == GameEnvironmentOptions.Type.MULTIPLE) {
|
||||
onChange(GameEnvironmentOptions.toggle(value, choice))
|
||||
} else {
|
||||
onChange(choice); expanded = false
|
||||
SettingsRow(stringResource(R.string.game_env_value), null, highlighted = open) {
|
||||
Box {
|
||||
ValueChip(if (custom) stringResource(R.string.game_env_custom_value) else value.ifEmpty { stringResource(R.string.game_env_select_value) }, open, modifier = Modifier.widthIn(max = 260.dp)) { open = !open }
|
||||
AnchoredMenu(open, onDismiss = { open = false }, title = name) { first ->
|
||||
val multiple = option.type == GameEnvironmentOptions.Type.MULTIPLE
|
||||
for ((i, choice) in option.choices.withIndex()) {
|
||||
MenuItem(choice, checked = if (multiple) choice in value.split(',') else !custom && choice == value, focusRequester = if (i == 0) first else null) {
|
||||
custom = false
|
||||
if (multiple) onChange(GameEnvironmentOptions.toggle(value, choice)) else { onChange(choice); open = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
MenuItem(stringResource(R.string.game_env_custom_value), checked = custom) { custom = true; open = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (custom) OutlinedTextField(value = value, onValueChange = onChange, singleLine = true,
|
||||
label = { Text(stringResource(R.string.game_env_value)) }, modifier = Modifier.fillMaxWidth())
|
||||
if (custom) AdbTextField(value, onChange, stringResource(R.string.game_env_value), KeyboardType.Ascii, ImeAction.Done, compact = true)
|
||||
}
|
||||
else -> OutlinedTextField(value = value, onValueChange = onChange, singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = if (option?.type == GameEnvironmentOptions.Type.NUMBER) KeyboardType.Number else KeyboardType.Text),
|
||||
label = { Text(stringResource(R.string.game_env_value)) }, modifier = Modifier.fillMaxWidth())
|
||||
else -> AdbTextField(
|
||||
value, onChange, stringResource(R.string.game_env_value),
|
||||
if (option?.type == GameEnvironmentOptions.Type.NUMBER) KeyboardType.Number else KeyboardType.Ascii, ImeAction.Done, compact = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -45,32 +45,28 @@
|
||||
<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>
|
||||
<string name="game_env_hint">Environment variables for Proton games.</string>
|
||||
<string name="game_env_edit">Edit</string>
|
||||
<string name="game_env_shared">All Proton games</string>
|
||||
<string name="game_env_scope">Game profile</string>
|
||||
<string name="game_env_app_id">Steam app ID or non-Steam compatibility ID</string>
|
||||
<string name="game_env_open">Open profile</string>
|
||||
<string name="game_env_scope">Profile</string>
|
||||
<string name="game_env_app_id">Steam app ID</string>
|
||||
<string name="game_env_open">Open</string>
|
||||
<string name="game_env_profile">Game %1$s</string>
|
||||
<string name="game_env_add">Add variable</string>
|
||||
<string name="game_env_name">Variable name</string>
|
||||
<string name="game_env_value">Value</string>
|
||||
<string name="game_env_save">Save</string>
|
||||
<string name="game_env_cancel">Cancel</string>
|
||||
<string name="game_env_close">Close</string>
|
||||
<string name="game_env_remove">Remove</string>
|
||||
<string name="game_env_restore">Restore inherited settings</string>
|
||||
<string name="game_env_restore">Inherit again</string>
|
||||
<string name="game_env_reset">Reset this profile</string>
|
||||
<string name="game_env_unset">Unset for this profile</string>
|
||||
<string name="game_env_empty">No variables set. Driver and Proton defaults apply.</string>
|
||||
<string name="game_env_empty">No variables set.</string>
|
||||
<string name="game_env_inherited">Inherited</string>
|
||||
<string name="game_env_custom">Custom variable</string>
|
||||
<string name="game_env_defaults">D3D12 feature level defaults to 12_2 and shader model to 6_9. Both can be changed or removed for each game.</string>
|
||||
<string name="game_env_error">Could not load or apply the settings. Check available storage and try again.</string>
|
||||
<string name="game_env_invalid">Use a valid variable name and a value without NUL characters (up to 8192 characters).</string>
|
||||
<string name="game_env_unused">The standard Linux Proton runtime does not use this variable; it may require a custom component.</string>
|
||||
<string name="game_env_error">Couldn\'t load or save. Check free storage and try again.</string>
|
||||
<string name="game_env_invalid">Use a valid name and a value up to 8192 characters.</string>
|
||||
<string name="game_env_unused">The standard Proton runtime doesn\'t read this variable.</string>
|
||||
<string name="game_env_loading">Loading…</string>
|
||||
<string name="game_env_overrides">Settings in this profile override shared settings and matching Steam launch-option variables. Remove unsets a variable; restore uses its inherited value.</string>
|
||||
<string name="game_env_feature">D3D12 feature-level override. 12_2 reports DirectX 12 Ultimate capabilities; unsupported features can fail. Leave unset for automatic detection.</string>
|
||||
<string name="game_env_shader">D3D12 shader-model override. 6_9 requires a sufficiently recent VKD3D and matching GPU features. Leave unset for automatic detection.</string>
|
||||
<string name="game_env_cache">Keep Mesa shader caching enabled to reduce repeated compilation. false enables caching.</string>
|
||||
@@ -894,4 +890,13 @@
|
||||
<string name="widgets_n_of_m">%1$d of %2$d</string>
|
||||
<string name="widgets_off">Off</string>
|
||||
<string name="screen_bottom">Bottom screen</string>
|
||||
<string name="game_env_applies">Applies on the next game launch. A game\'s profile overrides the shared one.</string>
|
||||
<string name="game_env_other">Another game by app ID…</string>
|
||||
<string name="game_env_done">Done</string>
|
||||
<string name="game_env_default">Default</string>
|
||||
<string name="game_env_off">Off</string>
|
||||
<string name="game_env_change">Change…</string>
|
||||
<string name="game_env_change_title">Change variable</string>
|
||||
<string name="game_env_restore_default">Back to default</string>
|
||||
<string name="game_env_eyebrow">Proton games</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user