diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index cd8bc3bf..6589bb3c 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.archipelago.app" minSdk = 26 targetSdk = 35 - versionCode = 42 - versionName = "0.5.22" + versionCode = 44 + versionName = "0.5.24" vectorDrawables { useSupportLibrary = true diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt index 724f7c3b..00931af1 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -108,6 +109,7 @@ fun NESController( onKey: (String) -> Unit, onMenu: () -> Unit, onPlayerToggle: () -> Unit = {}, + onToggleStyle: (() -> Unit)? = null, modifier: Modifier = Modifier, ) { val c = paletteFor(style) @@ -205,6 +207,7 @@ fun NESController( ) { PlayerPill(c, playerId, onPlayerToggle) SettingsBtn(c, Modifier, onMenu) + onToggleStyle?.let { StyleBtn(c, Modifier, it) } } } } @@ -431,6 +434,23 @@ fun SettingsBtn(c: NESPalette, modifier: Modifier = Modifier, onClick: () -> Uni } } +/** Dark/Classic style toggle — lives next to the settings gear (the menu hub + * no longer carries it). */ +@Composable +fun StyleBtn(c: NESPalette, modifier: Modifier = Modifier, onClick: () -> Unit) { + var p by remember { mutableStateOf(false) } + Box( + modifier = modifier + .size(48.dp) + .clip(CircleShape) + .background(if (p) c.capsulePress else c.capsule) + .pointerInput(Unit) { detectTapGestures(onPress = { p = true; onClick(); tryAwaitRelease(); p = false }) }, + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Default.Palette, "Controller style", Modifier.size(26.dp), tint = c.labelMuted) + } +} + /** Player ID toggle pill (P1/P2/ALL) */ @Composable fun PlayerPill(c: NESPalette, playerId: Int, onToggle: () -> Unit) { diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt index 2ee124bd..c2b0f98f 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt @@ -21,10 +21,22 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Dashboard +import androidx.compose.material.icons.filled.Dns +import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material.icons.filled.SportsEsports +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.QrCodeScanner import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField @@ -58,7 +70,6 @@ import androidx.compose.ui.unit.sp import com.archipelago.app.R import com.archipelago.app.data.ServerEntry import com.archipelago.app.ui.theme.BitcoinOrange -import com.archipelago.app.ui.theme.ControllerStyle import com.archipelago.app.ui.theme.SurfaceDark import com.archipelago.app.ui.theme.TextMuted import com.archipelago.app.ui.theme.TextPrimary @@ -83,7 +94,6 @@ fun NESMenu( servers: List, activeServer: ServerEntry?, isGamepadMode: Boolean, - controllerStyle: ControllerStyle, onDismiss: () -> Unit, onSelectServer: (ServerEntry) -> Unit, onAddServer: (ServerEntry) -> Unit, @@ -91,18 +101,21 @@ fun NESMenu( onEditServer: (ServerEntry, ServerEntry) -> Unit, onRemoveServer: (ServerEntry) -> Unit, onToggleMode: () -> Unit, - onToggleStyle: () -> Unit, onBackToWebView: (() -> Unit)? = null, onMeshParty: (() -> Unit)? = null, ) { AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) { + // Contained hub overlay: a centred glass panel (not full-screen) that + // holds the card page and its sub-pages (Nodes, FIPS) and scrolls + // inside its own bounds when content is tall. Tapping the dimmed + // backdrop dismisses. Box( Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.7f)) .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onDismiss() }, contentAlignment = Alignment.Center, ) { AnimatedVisibility(visible = visible, enter = fadeIn() + scaleIn(initialScale = 0.95f), exit = fadeOut() + scaleOut(targetScale = 0.95f)) { - MenuPanel(servers, activeServer, isGamepadMode, controllerStyle, onDismiss, onSelectServer, onAddServer, onScanQr, onEditServer, onRemoveServer, onToggleMode, onToggleStyle, onBackToWebView, onMeshParty) + MenuPanel(servers, activeServer, isGamepadMode, onDismiss, onSelectServer, onAddServer, onScanQr, onEditServer, onRemoveServer, onToggleMode, onBackToWebView, onMeshParty) } } } @@ -113,7 +126,6 @@ private fun MenuPanel( servers: List, activeServer: ServerEntry?, isGamepadMode: Boolean, - controllerStyle: ControllerStyle, onDismiss: () -> Unit, onSelectServer: (ServerEntry) -> Unit, onAddServer: (ServerEntry) -> Unit, @@ -121,7 +133,6 @@ private fun MenuPanel( onEditServer: (ServerEntry, ServerEntry) -> Unit, onRemoveServer: (ServerEntry) -> Unit, onToggleMode: () -> Unit, - onToggleStyle: () -> Unit, onBackToWebView: (() -> Unit)?, onMeshParty: (() -> Unit)?, ) { @@ -155,188 +166,256 @@ private fun MenuPanel( resetForm() } + var page by remember { mutableStateOf(HubPage.HUB) } + Column( modifier = Modifier .widthIn(max = 420.dp) + .fillMaxWidth() .padding(horizontal = 20.dp) + // Cap height so the glass panel never fills the screen; its content + // scrolls inside these bounds when tall. + .heightIn(max = 560.dp) .clip(RoundedCornerShape(PANEL_R)) - .background(PanelBg) + .background(PanelBg.copy(alpha = 0.86f)) .border(1.dp, PanelBorder, RoundedCornerShape(PANEL_R)) .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) {} - .padding(22.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), + .verticalScroll(rememberScrollState()) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - // Title - Text( - "Menu", - color = TextPrimary, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, - letterSpacing = 2.sp, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(2.dp)) - - // Servers - servers.forEach { server -> - val active = server.serialize() == activeServer?.serialize() - MenuItem( - label = server.displayName(), - selected = active, - onClick = { onSelectServer(server) }, - onEdit = { startEdit(server) }, - onRemove = { onRemoveServer(server) }, - ) - } - - if (servers.isEmpty()) { - Text("No servers", color = TextMuted, fontSize = 14.sp, modifier = Modifier.padding(vertical = 4.dp)) - } - - // Add / edit server - if (showAdd || editing != null) { - Column( - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(ROW_R)) - .background(FieldBg) - .border(1.dp, RowBorder, RoundedCornerShape(ROW_R)) - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Row( - Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { + // Header: back (on sub-pages) or title, and a close on the hub. + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + if (page == HubPage.HUB) { + Text("Menu", color = TextPrimary, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp) + IconRound(Icons.Default.Close, "Close") { onDismiss() } + } else { + Row(verticalAlignment = Alignment.CenterVertically) { + IconRound(Icons.AutoMirrored.Filled.ArrowBack, "Back") { resetForm(); page = HubPage.HUB } + Spacer(Modifier.width(12.dp)) Text( - if (editing != null) "Edit Server" else "Add Server", - color = TextMuted, - fontSize = 13.sp, - letterSpacing = 1.sp, - fontWeight = FontWeight.Medium, - ) - Text( - "Cancel", - color = TextMuted, - fontSize = 13.sp, - modifier = Modifier.clickable { resetForm() }.padding(start = 8.dp), + if (page == HubPage.NODES) "Nodes" else "FIPS Mesh", + color = TextPrimary, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.sp, ) } - GlassField( - value = nm, onValueChange = { nm = it }, - placeholder = "Name (optional)", - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next), - ) - GlassField( - value = addr, onValueChange = { addr = it.trim() }, - placeholder = "192.168.1.100", - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next), - ) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { - GlassField( - value = pwd, onValueChange = { pwd = it }, - placeholder = "Password", - modifier = Modifier.weight(1f), - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Go), - keyboardActions = KeyboardActions(onGo = { submit() }), - ) - Box( - Modifier.size(FIELD_H).clip(RoundedCornerShape(12.dp)).background(BitcoinOrange.copy(alpha = 0.15f)) - .border(1.dp, BitcoinOrange.copy(alpha = 0.4f), RoundedCornerShape(12.dp)) - .clickable { submit() }, - contentAlignment = Alignment.Center, - ) { Text("OK", color = BitcoinOrange, fontSize = 14.sp, fontWeight = FontWeight.Bold) } - } - // HTTPS scheme toggle (available on both add and edit). - Row( - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(ROW_R)) - .clickable { https = !https } - .padding(vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text("Use HTTPS", color = TextMuted, fontSize = 13.sp) - Box( - Modifier - .width(46.dp).height(26.dp) - .clip(RoundedCornerShape(13.dp)) - .background(if (https) BitcoinOrange.copy(alpha = 0.9f) else RowBg) - .border(1.dp, if (https) BitcoinOrange else RowBorder, RoundedCornerShape(13.dp)), - contentAlignment = if (https) Alignment.CenterEnd else Alignment.CenterStart, - ) { - Box( - Modifier - .padding(horizontal = 3.dp) - .size(20.dp) - .clip(RoundedCornerShape(10.dp)) - .background(if (https) Color.White else TextMuted), - ) - } - } + IconRound(Icons.Default.Close, "Close") { onDismiss() } } - } else { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { - Box(Modifier.weight(1f)) { - MenuItem(label = "Add Server", labelColor = BitcoinOrange, onClick = { showAdd = true }) + } + Spacer(Modifier.height(4.dp)) + + when (page) { + HubPage.HUB -> { + // Card page — one card per destination. Dashboard first: it's a + // peer of the others so three-finger → hub → Dashboard returns + // to the node UI, same shape as every other option. + if (onBackToWebView != null) { + HubCard(Icons.Default.Dashboard, "Dashboard", "The node's web interface") { onBackToWebView() } } - if (onScanQr != null) { - // Add server by scanning the node's pairing QR - Box( + HubCard(Icons.Default.SportsEsports, "Remote", "Game controller for the node") { + if (!isGamepadMode) onToggleMode() else onDismiss() + } + HubCard(Icons.Default.Keyboard, "Keyboard", "Type into the node") { + if (isGamepadMode) onToggleMode() else onDismiss() + } + HubCard(Icons.Default.Dns, "Nodes", activeServer?.displayName() ?: "Add or switch servers") { + page = HubPage.NODES + } + if (FipsNative.available) { + HubCard(Icons.Default.Bolt, "FIPS Mesh", "Mesh identity & status") { page = HubPage.FIPS } + } + if (onMeshParty != null) { + HubCard(Icons.Default.Groups, "Mesh Party", "Phone-to-phone chat & beam") { onMeshParty() } + } + // Dark/Classic style lives on the remote/keyboard screen next to + // the settings button — not here. + } + + HubPage.NODES -> { + servers.forEach { server -> + val active = server.serialize() == activeServer?.serialize() + MenuItem( + label = server.displayName(), + selected = active, + onClick = { onSelectServer(server) }, + onEdit = { startEdit(server) }, + onRemove = { onRemoveServer(server) }, + ) + } + if (servers.isEmpty()) { + Text("No servers", color = TextMuted, fontSize = 14.sp, modifier = Modifier.padding(vertical = 4.dp)) + } + + if (showAdd || editing != null) { + Column( Modifier - .size(ROW_H) + .fillMaxWidth() .clip(RoundedCornerShape(ROW_R)) - .background(RowBg) + .background(FieldBg) .border(1.dp, RowBorder, RoundedCornerShape(ROW_R)) - .clickable { onScanQr() }, - contentAlignment = Alignment.Center, + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Icon( - Icons.Default.QrCodeScanner, - contentDescription = stringResource(R.string.add_server_qr), - tint = BitcoinOrange, - modifier = Modifier.size(24.dp), + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + if (editing != null) "Edit Server" else "Add Server", + color = TextMuted, fontSize = 13.sp, letterSpacing = 1.sp, fontWeight = FontWeight.Medium, + ) + Text( + "Cancel", color = TextMuted, fontSize = 13.sp, + modifier = Modifier.clickable { resetForm() }.padding(start = 8.dp), + ) + } + GlassField( + value = nm, onValueChange = { nm = it }, + placeholder = "Name (optional)", + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next), ) + GlassField( + value = addr, onValueChange = { addr = it.trim() }, + placeholder = "192.168.1.100", + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next), + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + GlassField( + value = pwd, onValueChange = { pwd = it }, + placeholder = "Password", + modifier = Modifier.weight(1f), + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Go), + keyboardActions = KeyboardActions(onGo = { submit() }), + ) + Box( + Modifier.size(FIELD_H).clip(RoundedCornerShape(12.dp)).background(BitcoinOrange.copy(alpha = 0.15f)) + .border(1.dp, BitcoinOrange.copy(alpha = 0.4f), RoundedCornerShape(12.dp)) + .clickable { submit() }, + contentAlignment = Alignment.Center, + ) { Text("OK", color = BitcoinOrange, fontSize = 14.sp, fontWeight = FontWeight.Bold) } + } + // HTTPS scheme toggle (available on both add and edit). + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(ROW_R)) + .clickable { https = !https } + .padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text("Use HTTPS", color = TextMuted, fontSize = 13.sp) + Box( + Modifier + .width(46.dp).height(26.dp) + .clip(RoundedCornerShape(13.dp)) + .background(if (https) BitcoinOrange.copy(alpha = 0.9f) else RowBg) + .border(1.dp, if (https) BitcoinOrange else RowBorder, RoundedCornerShape(13.dp)), + contentAlignment = if (https) Alignment.CenterEnd else Alignment.CenterStart, + ) { + Box( + Modifier + .padding(horizontal = 3.dp) + .size(20.dp) + .clip(RoundedCornerShape(10.dp)) + .background(if (https) Color.White else TextMuted), + ) + } + } + } + } else { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Box(Modifier.weight(1f)) { + MenuItem(label = "Add Server", labelColor = BitcoinOrange, onClick = { showAdd = true }) + } + if (onScanQr != null) { + Box( + Modifier + .size(ROW_H) + .clip(RoundedCornerShape(ROW_R)) + .background(RowBg) + .border(1.dp, RowBorder, RoundedCornerShape(ROW_R)) + .clickable { onScanQr() }, + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Default.QrCodeScanner, + contentDescription = stringResource(R.string.add_server_qr), + tint = BitcoinOrange, + modifier = Modifier.size(24.dp), + ) + } + } } } } + + HubPage.FIPS -> { + FipsSection(startExpanded = true) + } } + } +} - Spacer(Modifier.height(2.dp)) - Box(Modifier.fillMaxWidth().height(1.dp).background(PanelBorder)) - Spacer(Modifier.height(2.dp)) +private enum class HubPage { HUB, NODES, FIPS } - // Mode toggle - MenuItem( - label = if (isGamepadMode) "Switch to Keyboard" else "Switch to Gamepad", - onClick = onToggleMode, - ) - - // Style toggle - MenuItem( - label = if (controllerStyle == ControllerStyle.CLASSIC) "Style: Classic" else "Style: Dark", - onClick = onToggleStyle, - ) - - // FIPS mesh oversight — identity, mesh address, link status, controls. - FipsSection() - - // Phone↔phone mesh pairing + chat - if (onMeshParty != null) { - MenuItem(label = "Mesh Party", labelColor = BitcoinOrange, onClick = onMeshParty) +/** Big tappable destination card for the hub page: icon + title + subtitle. */ +@Composable +private fun HubCard( + icon: androidx.compose.ui.graphics.vector.ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit, +) { + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(ROW_R)) + .background(RowBg) + .border(1.dp, RowBorder, RoundedCornerShape(ROW_R)) + .clickable { onClick() } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Box( + Modifier.size(40.dp).clip(RoundedCornerShape(12.dp)).background(BitcoinOrange.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon(icon, contentDescription = title, tint = BitcoinOrange, modifier = Modifier.size(22.dp)) } - - // Back to dashboard - if (onBackToWebView != null) { - MenuItem(label = "Back to Dashboard", onClick = onBackToWebView) + Column(Modifier.weight(1f)) { + Text(title, color = TextPrimary, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + Text(subtitle, color = TextMuted, fontSize = 12.sp, maxLines = 1) } } } +/** Small circular icon button used in the hub header. */ +@Composable +private fun IconRound( + icon: androidx.compose.ui.graphics.vector.ImageVector, + desc: String, + onClick: () -> Unit, +) { + Box( + Modifier + .size(40.dp) + .clip(RoundedCornerShape(20.dp)) + .background(RowBg) + .border(1.dp, RowBorder, RoundedCornerShape(20.dp)) + .clickable { onClick() }, + contentAlignment = Alignment.Center, + ) { + Icon(icon, contentDescription = desc, tint = TextPrimary, modifier = Modifier.size(20.dp)) + } +} + /** Snapshot of the phone's mesh identity + state for the FIPS menu section. */ private data class FipsInfo( val available: Boolean, @@ -354,11 +433,11 @@ private data class FipsInfo( * Collapsed by default so the menu stays compact. */ @Composable -private fun FipsSection() { +private fun FipsSection(startExpanded: Boolean = false) { if (!FipsNative.available) return val context = LocalContext.current val clipboard = LocalClipboardManager.current - var expanded by remember { mutableStateOf(false) } + var expanded by remember { mutableStateOf(startExpanded) } var info by remember { mutableStateOf(null) } // Load identity/state when the section opens (cheap DataStore + JSON read). diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt index 0b93e684..eaf3afc0 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt @@ -43,6 +43,7 @@ fun NESPortraitController( onMouseScroll: (Int) -> Unit = { _ -> }, onMenu: () -> Unit, onPlayerToggle: () -> Unit = {}, + onToggleStyle: (() -> Unit)? = null, ) { val c = paletteFor(style) val isClassic = style == ControllerStyle.CLASSIC @@ -151,6 +152,10 @@ fun NESPortraitController( PlayerPill(c, playerId, onPlayerToggle) Spacer(Modifier.width(10.dp)) SettingsBtn(c, Modifier, onMenu) + onToggleStyle?.let { + Spacer(Modifier.width(10.dp)) + StyleBtn(c, Modifier, it) + } } } } diff --git a/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt b/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt index 4642dcce..d25fb4b7 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt @@ -4,8 +4,10 @@ import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background +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.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -90,6 +92,9 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) { playerId = when (playerId) { 0 -> 1; 1 -> 2; else -> 0 } ws.playerId = playerId } + fun toggleStyle() { + controllerStyle = if (controllerStyle == ControllerStyle.CLASSIC) ControllerStyle.DARK else ControllerStyle.CLASSIC + } val connectionState by ws.state.collectAsState() val lifecycleOwner = LocalLifecycleOwner.current @@ -153,6 +158,7 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) { onKey = { ws.sendKey(it) }, onMenu = { showModal = true }, onPlayerToggle = ::togglePlayer, + onToggleStyle = ::toggleStyle, ) isGamepadMode && !isLandscape -> NESPortraitController( style = controllerStyle, @@ -163,6 +169,7 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) { onMouseScroll = { ws.sendScroll(it) }, onMenu = { showModal = true }, onPlayerToggle = ::togglePlayer, + onToggleStyle = ::toggleStyle, ) else -> { // Keyboard mode: trackpad fills top, keyboard pinned bottom @@ -182,12 +189,20 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) { modifier = Modifier.fillMaxWidth(), ) } - // Settings icon top-right in keyboard mode - com.archipelago.app.ui.components.SettingsBtn( - c = com.archipelago.app.ui.components.paletteFor(controllerStyle), - modifier = Modifier.align(Alignment.TopEnd).padding(8.dp), - onClick = { showModal = true }, - ) + // Settings + style icons top-right in keyboard mode + Row( + Modifier.align(Alignment.TopEnd).padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + com.archipelago.app.ui.components.SettingsBtn( + c = com.archipelago.app.ui.components.paletteFor(controllerStyle), + onClick = { showModal = true }, + ) + com.archipelago.app.ui.components.StyleBtn( + c = com.archipelago.app.ui.components.paletteFor(controllerStyle), + onClick = ::toggleStyle, + ) + } } } } @@ -211,7 +226,6 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) { servers = savedServers, activeServer = activeServer, isGamepadMode = isGamepadMode, - controllerStyle = controllerStyle, onDismiss = { showModal = false }, onSelectServer = { server -> scope.launch { ws.disconnect(); prefs.setActiveServer(server) }; showModal = false @@ -246,9 +260,6 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) { } }, onToggleMode = { isGamepadMode = !isGamepadMode; showModal = false }, - onToggleStyle = { - controllerStyle = if (controllerStyle == ControllerStyle.CLASSIC) ControllerStyle.DARK else ControllerStyle.CLASSIC - }, onBackToWebView = { showModal = false; onBack() }, onMeshParty = onMeshParty?.let { open -> { showModal = false; open() } }, )