Let the player name field be cleared
Clearing the name put "Player" straight back before a real one could be typed. Three things combined to cause it: - setPlayerName coerced a blank value to "Player" on write, so deleting the last character immediately persisted the default. - The settings flow then emitted that default, and the text field's remember was keyed on the stored name, so it re-seeded itself from the value that had just been written. - The default also lived in the stored value, so a fresh install had "Player" pre-filled and had to be cleared to begin with. The stored name is now whatever the user typed, blank included. The default moved to Settings.displayName, which is what the lobby and seat labels use, so it applies where a name is shown rather than to what is being edited. The field keeps its own edit state once touched, and shows "Player" as placeholder text with a note explaining the fallback. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
This commit is contained in:
@@ -25,10 +25,23 @@ data class Settings(
|
||||
val soundEnabled: Boolean = true,
|
||||
val animationSpeed: Float = 1.0f,
|
||||
val checkForUpdatesOnLaunch: Boolean = true,
|
||||
val playerName: String = "Player",
|
||||
/**
|
||||
* Exactly what the user has typed, which may be blank while they are
|
||||
* mid-edit. Nothing substitutes a default into this value: doing so meant
|
||||
* that clearing the field wrote "Player" straight back and the text
|
||||
* snapped back before a real name could be typed.
|
||||
*/
|
||||
val playerName: String = "",
|
||||
/** Which builds the updater offers. Production unless deliberately changed. */
|
||||
val updateChannel: UpdateChannel = UpdateChannel.PRODUCTION,
|
||||
) {
|
||||
/**
|
||||
* The name other players actually see. This is where the default lives, so
|
||||
* an empty field is a display concern rather than something written back
|
||||
* into what the user is typing.
|
||||
*/
|
||||
val displayName: String get() = playerName.trim().ifBlank { DEFAULT_PLAYER_NAME }
|
||||
|
||||
/** Scales a nominal duration by the chosen speed. */
|
||||
fun scaled(millis: Long): Long =
|
||||
(millis / animationSpeed.coerceIn(MIN_SPEED, MAX_SPEED)).toLong()
|
||||
@@ -36,6 +49,7 @@ data class Settings(
|
||||
companion object {
|
||||
const val MIN_SPEED = 0.5f
|
||||
const val MAX_SPEED = 2.0f
|
||||
const val DEFAULT_PLAYER_NAME = "Player"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +68,7 @@ class SettingsRepository(private val context: Context) {
|
||||
soundEnabled = prefs[Keys.SOUND] ?: true,
|
||||
animationSpeed = prefs[Keys.SPEED] ?: 1.0f,
|
||||
checkForUpdatesOnLaunch = prefs[Keys.UPDATE_ON_LAUNCH] ?: true,
|
||||
playerName = prefs[Keys.NAME] ?: "Player",
|
||||
playerName = prefs[Keys.NAME] ?: "",
|
||||
updateChannel = prefs[Keys.CHANNEL]
|
||||
?.let { stored -> runCatching { UpdateChannel.valueOf(stored) }.getOrNull() }
|
||||
?: UpdateChannel.PRODUCTION,
|
||||
@@ -79,7 +93,8 @@ class SettingsRepository(private val context: Context) {
|
||||
context.dataStore.edit { it[Keys.UPDATE_ON_LAUNCH] = enabled }
|
||||
}
|
||||
|
||||
/** Stores the name verbatim. The fallback belongs to [Settings.displayName]. */
|
||||
suspend fun setPlayerName(name: String) {
|
||||
context.dataStore.edit { it[Keys.NAME] = name.trim().ifBlank { "Player" } }
|
||||
context.dataStore.edit { it[Keys.NAME] = name }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ fun AppRoot(env: AppEnv) {
|
||||
|
||||
is Route.Setup -> GameSetupScreen(
|
||||
descriptor = GameCatalog.byId(route.gameId)!!,
|
||||
playerName = settings.playerName,
|
||||
playerName = settings.displayName,
|
||||
onBack = { pop() },
|
||||
onPlayOffline = { config -> push(Route.Play(route.gameId, config, true, 0)) },
|
||||
onHostOnline = { optionsJson ->
|
||||
@@ -118,7 +118,7 @@ fun AppRoot(env: AppEnv) {
|
||||
is Route.Lobby -> LobbyScreen(
|
||||
env = env,
|
||||
route = route,
|
||||
playerName = settings.playerName,
|
||||
playerName = settings.displayName,
|
||||
onBack = { pop() },
|
||||
onStart = { config, hosting, seat ->
|
||||
push(Route.Play(config.gameId, config, hosting, seat))
|
||||
|
||||
@@ -36,7 +36,11 @@ fun SettingsScreen(
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var name by remember(settings.playerName) { mutableStateOf(settings.playerName) }
|
||||
// Null until the user touches the field, at which point their text takes
|
||||
// over for good. Keying this off the stored name instead meant every
|
||||
// keystroke re-seeded the field from what had just been written, so the
|
||||
// caret jumped and a cleared field refilled itself.
|
||||
var editedName by remember { mutableStateOf<String?>(null) }
|
||||
var updateStatus by remember { mutableStateOf<String?>(null) }
|
||||
var pendingRelease by remember { mutableStateOf<Updater.Release?>(null) }
|
||||
|
||||
@@ -86,14 +90,22 @@ fun SettingsScreen(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
value = editedName ?: settings.playerName,
|
||||
onValueChange = { entered ->
|
||||
name = entered
|
||||
editedName = entered
|
||||
scope.launch { env.settingsRepository.setPlayerName(entered) }
|
||||
},
|
||||
placeholder = { Text(Settings.DEFAULT_PLAYER_NAME) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
if ((editedName ?: settings.playerName).isBlank()) {
|
||||
Text(
|
||||
text = "Leave it empty and you'll show up as " +
|
||||
"${Settings.DEFAULT_PLAYER_NAME}.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Reference in New Issue
Block a user