diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/achievements/AchievementsScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/achievements/AchievementsScreen.kt index 0d0e1c8af1..b2a35fbc86 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/achievements/AchievementsScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/achievements/AchievementsScreen.kt @@ -128,11 +128,11 @@ fun AchievementsScreen(onBack: () -> Unit, viewModel: AchievementsViewModel = vi } state.error?.let { error -> - AlertDialog( - onDismissRequest = viewModel::dismissError, - title = { Text("RetroAchievements") }, - text = { Text(error) }, - confirmButton = { TextButton(onClick = viewModel::dismissError) { Text(str("action.ok")) } }, + com.armsx2.ui.common.NotifyOverlay( + title = "RetroAchievements", + message = error, + onDismiss = viewModel::dismissError, + idPrefix = "ra.error", ) } diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/bios/BiosManagerScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/bios/BiosManagerScreen.kt index 60dab61d82..d6a15f7534 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/bios/BiosManagerScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/bios/BiosManagerScreen.kt @@ -147,11 +147,11 @@ fun BiosManagerScreen(onBack: () -> Unit, game: GameInfo? = null, viewModel: Bio ) } state.error?.let { error -> - AlertDialog( - onDismissRequest = viewModel::dismissError, - title = { Text(str("setup.page.bios.title")) }, - text = { Text(error) }, - confirmButton = { TextButton(onClick = viewModel::dismissError) { Text(str("action.ok")) } }, + com.armsx2.ui.common.NotifyOverlay( + title = str("setup.page.bios.title"), + message = error, + onDismiss = viewModel::dismissError, + idPrefix = "bios.error", ) } } diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/common/ConfirmOverlay.kt b/platforms/android/app/src/main/java/com/armsx2/ui/common/ConfirmOverlay.kt index 866127f841..e993427c94 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/common/ConfirmOverlay.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/common/ConfirmOverlay.kt @@ -1,12 +1,15 @@ package com.armsx2.ui.common import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Arrangement 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.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -102,6 +105,71 @@ fun ConfirmOverlay( } } +/** + * A one-button acknowledgement — an error, a result, an explanation. The other half of the pair + * with [ConfirmOverlay]: same card, no choice to make. + * + * The body scrolls and is height-capped, and the modal declares that scroll state, so a message + * longer than the panel can be read with the pad's Up/Down once the selection has nowhere left to + * go. Every window dialog this replaces simply clipped long text with no way to reach the rest. + * + * @param idPrefix distinguishes concurrent notices' nav ids and layer. + */ +@Composable +fun NotifyOverlay( + title: String, + message: String, + onDismiss: () -> Unit, + buttonLabel: String = str("action.ok"), + idPrefix: String = "notify", +) { + val layer = "notify-overlay:$idPrefix" + val bodyScroll = rememberScrollState() + PadModal( + key = layer, + onDismiss = onDismiss, + initialFocusId = "$layer.ok", + scrollState = bodyScroll, + ) { + Surface( + modifier = Modifier + .padding(24.dp) + .widthIn(max = 420.dp), + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surface, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)), + tonalElevation = 6.dp, + ) { + Column(Modifier.padding(20.dp)) { + Text( + title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(8.dp)) + Text( + message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .heightIn(max = 340.dp) + .verticalScroll(bodyScroll), + ) + Spacer(Modifier.height(18.dp)) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + ConfirmButton( + label = buttonLabel, + id = "$layer.ok", + onClick = onDismiss, + container = MaterialTheme.colorScheme.primaryContainer, + content = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + } + } + } +} + /** * App-wide confirmation host, for prompts raised from code that has no composition of its own to * put a [ConfirmOverlay] in — a click handler deep in a view model, say. diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/memorycards/MemoryCardScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/memorycards/MemoryCardScreen.kt index 4f9afaf93b..2068dbe08e 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/memorycards/MemoryCardScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/memorycards/MemoryCardScreen.kt @@ -131,11 +131,11 @@ fun MemoryCardScreen(onBack: () -> Unit, game: GameInfo? = null, viewModel: Memo ) } (state.error ?: state.message)?.let { message -> - AlertDialog( - onDismissRequest = viewModel::dismissMessage, - title = { Text(if (state.error != null) str("memcard.title") else str("action.ok")) }, - text = { Text(message) }, - confirmButton = { TextButton(onClick = viewModel::dismissMessage, modifier = Modifier.controllerFocusable("memcard.message.ok", onConfirm = viewModel::dismissMessage)) { Text(str("action.ok")) } }, + com.armsx2.ui.common.NotifyOverlay( + title = if (state.error != null) str("memcard.title") else str("action.ok"), + message = message, + onDismiss = viewModel::dismissMessage, + idPrefix = "memcard.message", ) } } diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/patches/PatchManagerScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/patches/PatchManagerScreen.kt index 3b377655d4..418f4dbd2d 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/patches/PatchManagerScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/patches/PatchManagerScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator @@ -114,11 +113,11 @@ fun PatchManagerScreen(onBack: () -> Unit, game: GameInfo? = null, viewModel: Pa com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_OPEN) onDispose { com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_CLOSE) } } - AlertDialog( - onDismissRequest = viewModel::dismissMessage, - title = { Text(if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats")) }, - text = { Text(message) }, - confirmButton = { TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) } }, + com.armsx2.ui.common.NotifyOverlay( + title = if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats"), + message = message, + onDismiss = viewModel::dismissMessage, + idPrefix = "patches.message", ) } } @@ -269,11 +268,11 @@ fun PatchesSettingsTab(game: GameInfo? = null, viewModel: PatchManagerViewModel com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_OPEN) onDispose { com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_CLOSE) } } - AlertDialog( - onDismissRequest = viewModel::dismissMessage, - title = { Text(if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats")) }, - text = { Text(message) }, - confirmButton = { TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) } }, + com.armsx2.ui.common.NotifyOverlay( + title = if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats"), + message = message, + onDismiss = viewModel::dismissMessage, + idPrefix = "patches.message", ) } } diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/saves/SaveManagerScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/saves/SaveManagerScreen.kt index 9a09ebb395..0780b0a323 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/saves/SaveManagerScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/saves/SaveManagerScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -99,13 +98,11 @@ fun SaveManagerScreen(onBack: () -> Unit, viewModel: SaveManagerViewModel = view } (state.error ?: state.message)?.let { message -> - AlertDialog( - onDismissRequest = viewModel::dismissMessage, - title = { Text(str("savestate.title.loadManage")) }, - text = { Text(message) }, - confirmButton = { - TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) } - }, + com.armsx2.ui.common.NotifyOverlay( + title = str("savestate.title.loadManage"), + message = message, + onDismiss = viewModel::dismissMessage, + idPrefix = "saves.message", ) } } diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/settings/SettingsWidgets.kt b/platforms/android/app/src/main/java/com/armsx2/ui/settings/SettingsWidgets.kt index 623b4f712e..edfd567687 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/settings/SettingsWidgets.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/settings/SettingsWidgets.kt @@ -1272,67 +1272,15 @@ internal fun InfoHint(title: String, message: String) { com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_OPEN) onDispose { com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_CLOSE) } } - val layer = "info-hint:$title" - val bodyScroll = rememberScrollState() - com.armsx2.ui.common.PadModal( - key = layer, + // The shared acknowledgement panel: same card, same scrolling body, so a setting + // description behaves exactly like an error notice and neither can drift from the other. + com.armsx2.ui.common.NotifyOverlay( + title = title, + message = message, onDismiss = { open = false }, - // Every setting description is a candidate for being longer than the panel, so the - // pad needs Up/Down to read it — Close is the only thing here to select. - scrollState = bodyScroll, - ) { - Surface( - modifier = Modifier - .padding(24.dp) - .widthIn(max = 420.dp), - shape = RoundedCornerShape(20.dp), - color = MaterialTheme.colorScheme.surface, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)), - tonalElevation = 6.dp, - ) { - Column(Modifier.padding(20.dp)) { - Text( - title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - ) - Spacer(Modifier.height(8.dp)) - // Cap the height and scroll INSIDE it. AlertDialog did not scroll its text - // slot at all, so a description longer than the slot was CLIPPED mid-sentence - // with no way to reach the rest — which is most of the longer setting - // explanations (reported against Low Latency Mode, cut off at "...turning - // back off if the frame pacing"). - Text( - message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .heightIn(max = 340.dp) - .verticalScroll(bodyScroll), - ) - Spacer(Modifier.height(18.dp)) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Surface( - onClick = { open = false }, - modifier = Modifier.controllerFocusable( - controllerId = "$layer.close", - shape = RoundedCornerShape(14.dp), - onConfirm = { open = false }, - ), - shape = RoundedCornerShape(14.dp), - color = MaterialTheme.colorScheme.primaryContainer, - ) { - Text( - str("action.close"), - modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } - } - } - } - } + buttonLabel = str("action.close"), + idPrefix = "info:$title", + ) } } diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/textures/TextureManagerScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/textures/TextureManagerScreen.kt index 4f76d27304..c261a17418 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/textures/TextureManagerScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/textures/TextureManagerScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme @@ -96,11 +95,11 @@ fun TextureManagerScreen(onBack: () -> Unit, viewModel: TextureManagerViewModel } } (state.error ?: state.message)?.let { message -> - AlertDialog( - onDismissRequest = viewModel::dismissMessage, - title = { Text(if (state.error == null) str("action.ok") else str("renderer.section.texturePacks")) }, - text = { Text(message) }, - confirmButton = { TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) } }, + com.armsx2.ui.common.NotifyOverlay( + title = if (state.error == null) str("action.ok") else str("renderer.section.texturePacks"), + message = message, + onDismiss = viewModel::dismissMessage, + idPrefix = "textures.message", ) } }