feat(companion): three-finger opens the hub menu over the dashboard — 0.5.25
The dashboard's three-finger hold now opens the menu overlay in place instead of jumping to the remote screen; the hub's Remote and Keyboard cards do the navigating (Keyboard lands directly in keyboard mode via a nav arg). The dashboard host carries the full Nodes page — add, edit, remove and QR pairing — and Mesh Party. Also: the panel scales to 92% of screen height instead of a fixed 560dp cap so pages fit without scrolling; the FIPS sub-page no longer repeats the FIPS Mesh header inside the detail card; the safe-area injection fires an archy-insets event the web UI listens for (update-install status-bar fix, other half in neode-ui). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
24582128c5
commit
1cc18e5b72
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.archipelago.app"
|
applicationId = "com.archipelago.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 44
|
versionCode = 45
|
||||||
versionName = "0.5.24"
|
versionName = "0.5.25"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import com.archipelago.app.fips.FipsManager
|
import com.archipelago.app.fips.FipsManager
|
||||||
@ -93,14 +94,14 @@ fun NESMenu(
|
|||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
servers: List<ServerEntry>,
|
servers: List<ServerEntry>,
|
||||||
activeServer: ServerEntry?,
|
activeServer: ServerEntry?,
|
||||||
isGamepadMode: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSelectServer: (ServerEntry) -> Unit,
|
onSelectServer: (ServerEntry) -> Unit,
|
||||||
onAddServer: (ServerEntry) -> Unit,
|
onAddServer: (ServerEntry) -> Unit,
|
||||||
onScanQr: (() -> Unit)? = null,
|
onScanQr: (() -> Unit)? = null,
|
||||||
onEditServer: (ServerEntry, ServerEntry) -> Unit,
|
onEditServer: (ServerEntry, ServerEntry) -> Unit,
|
||||||
onRemoveServer: (ServerEntry) -> Unit,
|
onRemoveServer: (ServerEntry) -> Unit,
|
||||||
onToggleMode: () -> Unit,
|
onRemote: () -> Unit,
|
||||||
|
onKeyboard: () -> Unit,
|
||||||
onBackToWebView: (() -> Unit)? = null,
|
onBackToWebView: (() -> Unit)? = null,
|
||||||
onMeshParty: (() -> Unit)? = null,
|
onMeshParty: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
@ -115,7 +116,7 @@ fun NESMenu(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(visible = visible, enter = fadeIn() + scaleIn(initialScale = 0.95f), exit = fadeOut() + scaleOut(targetScale = 0.95f)) {
|
AnimatedVisibility(visible = visible, enter = fadeIn() + scaleIn(initialScale = 0.95f), exit = fadeOut() + scaleOut(targetScale = 0.95f)) {
|
||||||
MenuPanel(servers, activeServer, isGamepadMode, onDismiss, onSelectServer, onAddServer, onScanQr, onEditServer, onRemoveServer, onToggleMode, onBackToWebView, onMeshParty)
|
MenuPanel(servers, activeServer, onDismiss, onSelectServer, onAddServer, onScanQr, onEditServer, onRemoveServer, onRemote, onKeyboard, onBackToWebView, onMeshParty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,14 +126,14 @@ fun NESMenu(
|
|||||||
private fun MenuPanel(
|
private fun MenuPanel(
|
||||||
servers: List<ServerEntry>,
|
servers: List<ServerEntry>,
|
||||||
activeServer: ServerEntry?,
|
activeServer: ServerEntry?,
|
||||||
isGamepadMode: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSelectServer: (ServerEntry) -> Unit,
|
onSelectServer: (ServerEntry) -> Unit,
|
||||||
onAddServer: (ServerEntry) -> Unit,
|
onAddServer: (ServerEntry) -> Unit,
|
||||||
onScanQr: (() -> Unit)?,
|
onScanQr: (() -> Unit)?,
|
||||||
onEditServer: (ServerEntry, ServerEntry) -> Unit,
|
onEditServer: (ServerEntry, ServerEntry) -> Unit,
|
||||||
onRemoveServer: (ServerEntry) -> Unit,
|
onRemoveServer: (ServerEntry) -> Unit,
|
||||||
onToggleMode: () -> Unit,
|
onRemote: () -> Unit,
|
||||||
|
onKeyboard: () -> Unit,
|
||||||
onBackToWebView: (() -> Unit)?,
|
onBackToWebView: (() -> Unit)?,
|
||||||
onMeshParty: (() -> Unit)?,
|
onMeshParty: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
@ -173,9 +174,9 @@ private fun MenuPanel(
|
|||||||
.widthIn(max = 420.dp)
|
.widthIn(max = 420.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 20.dp)
|
.padding(horizontal = 20.dp)
|
||||||
// Cap height so the glass panel never fills the screen; its content
|
// Cap height just short of the full screen; the panel wraps short
|
||||||
// scrolls inside these bounds when tall.
|
// content and only scrolls in the rare case it outgrows this.
|
||||||
.heightIn(max = 560.dp)
|
.heightIn(max = (LocalConfiguration.current.screenHeightDp * 0.92f).dp)
|
||||||
.clip(RoundedCornerShape(PANEL_R))
|
.clip(RoundedCornerShape(PANEL_R))
|
||||||
.background(PanelBg.copy(alpha = 0.86f))
|
.background(PanelBg.copy(alpha = 0.86f))
|
||||||
.border(1.dp, PanelBorder, RoundedCornerShape(PANEL_R))
|
.border(1.dp, PanelBorder, RoundedCornerShape(PANEL_R))
|
||||||
@ -215,12 +216,8 @@ private fun MenuPanel(
|
|||||||
if (onBackToWebView != null) {
|
if (onBackToWebView != null) {
|
||||||
HubCard(Icons.Default.Dashboard, "Dashboard", "The node's web interface") { onBackToWebView() }
|
HubCard(Icons.Default.Dashboard, "Dashboard", "The node's web interface") { onBackToWebView() }
|
||||||
}
|
}
|
||||||
HubCard(Icons.Default.SportsEsports, "Remote", "Game controller for the node") {
|
HubCard(Icons.Default.SportsEsports, "Remote", "Game controller for the node") { onRemote() }
|
||||||
if (!isGamepadMode) onToggleMode() else onDismiss()
|
HubCard(Icons.Default.Keyboard, "Keyboard", "Type into the node") { onKeyboard() }
|
||||||
}
|
|
||||||
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") {
|
HubCard(Icons.Default.Dns, "Nodes", activeServer?.displayName() ?: "Add or switch servers") {
|
||||||
page = HubPage.NODES
|
page = HubPage.NODES
|
||||||
}
|
}
|
||||||
@ -356,7 +353,7 @@ private fun MenuPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HubPage.FIPS -> {
|
HubPage.FIPS -> {
|
||||||
FipsSection(startExpanded = true)
|
FipsSection(embedded = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -433,11 +430,11 @@ private data class FipsInfo(
|
|||||||
* Collapsed by default so the menu stays compact.
|
* Collapsed by default so the menu stays compact.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun FipsSection(startExpanded: Boolean = false) {
|
private fun FipsSection(embedded: Boolean = false) {
|
||||||
if (!FipsNative.available) return
|
if (!FipsNative.available) return
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
var expanded by remember { mutableStateOf(startExpanded) }
|
var expanded by remember { mutableStateOf(embedded) }
|
||||||
var info by remember { mutableStateOf<FipsInfo?>(null) }
|
var info by remember { mutableStateOf<FipsInfo?>(null) }
|
||||||
|
|
||||||
// Load identity/state when the section opens (cheap DataStore + JSON read).
|
// Load identity/state when the section opens (cheap DataStore + JSON read).
|
||||||
@ -459,7 +456,9 @@ private fun FipsSection(startExpanded: Boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
MenuItem(
|
// Embedded in the hub's FIPS sub-page the header row would duplicate
|
||||||
|
// the page title, so only the standalone (collapsible) form shows it.
|
||||||
|
if (!embedded) MenuItem(
|
||||||
label = "FIPS Mesh",
|
label = "FIPS Mesh",
|
||||||
labelColor = BitcoinOrange,
|
labelColor = BitcoinOrange,
|
||||||
onClick = { expanded = !expanded },
|
onClick = { expanded = !expanded },
|
||||||
@ -500,7 +499,7 @@ private fun FipsSection(startExpanded: Boolean = false) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
FipsManager.requestMeshRestart(context)
|
FipsManager.requestMeshRestart(context)
|
||||||
info = null
|
info = null
|
||||||
expanded = false
|
if (!embedded) expanded = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
import com.archipelago.app.data.PairResult
|
import com.archipelago.app.data.PairResult
|
||||||
import com.archipelago.app.data.ServerEntry
|
import com.archipelago.app.data.ServerEntry
|
||||||
import com.archipelago.app.data.ServerPreferences
|
import com.archipelago.app.data.ServerPreferences
|
||||||
@ -177,11 +179,25 @@ fun AppNavHost(
|
|||||||
onRemoteInput = {
|
onRemoteInput = {
|
||||||
navController.navigate(Routes.REMOTE_INPUT)
|
navController.navigate(Routes.REMOTE_INPUT)
|
||||||
},
|
},
|
||||||
|
onRemoteKeyboard = {
|
||||||
|
navController.navigate("${Routes.REMOTE_INPUT}?keyboard=true")
|
||||||
|
},
|
||||||
|
onMeshParty = {
|
||||||
|
navController.navigate(Routes.MESH_PARTY)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(Routes.REMOTE_INPUT) {
|
composable(
|
||||||
|
"${Routes.REMOTE_INPUT}?keyboard={keyboard}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("keyboard") {
|
||||||
|
type = NavType.BoolType
|
||||||
|
defaultValue = false
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) { entry ->
|
||||||
RemoteInputScreen(
|
RemoteInputScreen(
|
||||||
onBack = {
|
onBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
@ -189,6 +205,7 @@ fun AppNavHost(
|
|||||||
onMeshParty = {
|
onMeshParty = {
|
||||||
navController.navigate(Routes.MESH_PARTY)
|
navController.navigate(Routes.MESH_PARTY)
|
||||||
},
|
},
|
||||||
|
startInKeyboard = entry.arguments?.getBoolean("keyboard") == true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,12 @@ import com.archipelago.app.ui.theme.TextMuted
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) {
|
fun RemoteInputScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onMeshParty: (() -> Unit)? = null,
|
||||||
|
// Land on the keyboard instead of the gamepad (hub menu's Keyboard card).
|
||||||
|
startInKeyboard: Boolean = false,
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val prefs = remember { ServerPreferences(context) }
|
val prefs = remember { ServerPreferences(context) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@ -65,7 +70,7 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) {
|
|||||||
val savedServers by prefs.savedServers.collectAsState(initial = emptyList())
|
val savedServers by prefs.savedServers.collectAsState(initial = emptyList())
|
||||||
val activeServer by prefs.activeServer.collectAsState(initial = null)
|
val activeServer by prefs.activeServer.collectAsState(initial = null)
|
||||||
|
|
||||||
var isGamepadMode by remember { mutableStateOf(true) }
|
var isGamepadMode by remember { mutableStateOf(!startInKeyboard) }
|
||||||
var showModal by remember { mutableStateOf(false) }
|
var showModal by remember { mutableStateOf(false) }
|
||||||
var showQrScanner by remember { mutableStateOf(false) }
|
var showQrScanner by remember { mutableStateOf(false) }
|
||||||
var controllerStyle by remember { mutableStateOf(ControllerStyle.DARK) }
|
var controllerStyle by remember { mutableStateOf(ControllerStyle.DARK) }
|
||||||
@ -225,7 +230,6 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) {
|
|||||||
visible = showModal,
|
visible = showModal,
|
||||||
servers = savedServers,
|
servers = savedServers,
|
||||||
activeServer = activeServer,
|
activeServer = activeServer,
|
||||||
isGamepadMode = isGamepadMode,
|
|
||||||
onDismiss = { showModal = false },
|
onDismiss = { showModal = false },
|
||||||
onSelectServer = { server ->
|
onSelectServer = { server ->
|
||||||
scope.launch { ws.disconnect(); prefs.setActiveServer(server) }; showModal = false
|
scope.launch { ws.disconnect(); prefs.setActiveServer(server) }; showModal = false
|
||||||
@ -259,7 +263,8 @@ fun RemoteInputScreen(onBack: () -> Unit, onMeshParty: (() -> Unit)? = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onToggleMode = { isGamepadMode = !isGamepadMode; showModal = false },
|
onRemote = { isGamepadMode = true; showModal = false },
|
||||||
|
onKeyboard = { isGamepadMode = false; showModal = false },
|
||||||
onBackToWebView = { showModal = false; onBack() },
|
onBackToWebView = { showModal = false; onBack() },
|
||||||
onMeshParty = onMeshParty?.let { open -> { showModal = false; open() } },
|
onMeshParty = onMeshParty?.let { open -> { showModal = false; open() } },
|
||||||
)
|
)
|
||||||
|
|||||||
@ -88,8 +88,11 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import android.webkit.ValueCallback
|
import android.webkit.ValueCallback
|
||||||
import com.archipelago.app.R
|
import com.archipelago.app.R
|
||||||
import com.archipelago.app.data.ServerPreferences
|
import com.archipelago.app.data.ServerPreferences
|
||||||
|
import com.archipelago.app.fips.FipsManager
|
||||||
import com.archipelago.app.ui.components.GestureHintOverlay
|
import com.archipelago.app.ui.components.GestureHintOverlay
|
||||||
import com.archipelago.app.ui.components.MeshLoadingScreen
|
import com.archipelago.app.ui.components.MeshLoadingScreen
|
||||||
|
import com.archipelago.app.ui.components.NESMenu
|
||||||
|
import com.archipelago.app.ui.components.QrScannerOverlay
|
||||||
import com.archipelago.app.ui.components.WalletQrScannerModal
|
import com.archipelago.app.ui.components.WalletQrScannerModal
|
||||||
import com.archipelago.app.ui.theme.BitcoinOrange
|
import com.archipelago.app.ui.theme.BitcoinOrange
|
||||||
import com.archipelago.app.ui.theme.ErrorRed
|
import com.archipelago.app.ui.theme.ErrorRed
|
||||||
@ -179,6 +182,9 @@ private fun injectSafeAreaVars(view: WebView) {
|
|||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
style.textContent = ':root { --safe-area-top: ${sat}px; --safe-area-bottom: ${sab}px; }';
|
style.textContent = ':root { --safe-area-top: ${sat}px; --safe-area-bottom: ${sab}px; }';
|
||||||
|
// Vue components sample the var into reactive state; tell them it
|
||||||
|
// changed (an authenticated session can mount before we run).
|
||||||
|
window.dispatchEvent(new CustomEvent('archy-insets', { detail: { top: ${sat}, bottom: ${sab} } }));
|
||||||
})();
|
})();
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
null,
|
null,
|
||||||
@ -278,6 +284,10 @@ fun WebViewScreen(
|
|||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
onDisconnect: () -> Unit,
|
onDisconnect: () -> Unit,
|
||||||
onRemoteInput: () -> Unit = {},
|
onRemoteInput: () -> Unit = {},
|
||||||
|
// Like onRemoteInput but landing on the keyboard (the hub menu's Keyboard card).
|
||||||
|
onRemoteKeyboard: () -> Unit = {},
|
||||||
|
// Opens the phone-to-phone Mesh Party screen; null hides its hub card.
|
||||||
|
onMeshParty: (() -> Unit)? = null,
|
||||||
// Stored password for this server (from QR pairing or manual entry). When
|
// Stored password for this server (from QR pairing or manual entry). When
|
||||||
// non-blank, the login page is auto-filled and submitted — the one-step
|
// non-blank, the login page is auto-filled and submitted — the one-step
|
||||||
// demo flow from docs/companion-pairing-qr.md.
|
// demo flow from docs/companion-pairing-qr.md.
|
||||||
@ -356,6 +366,14 @@ fun WebViewScreen(
|
|||||||
// One-time three-finger-hold teaching overlay (initial=true: never flash
|
// One-time three-finger-hold teaching overlay (initial=true: never flash
|
||||||
// it while DataStore is still loading).
|
// it while DataStore is still loading).
|
||||||
val prefs = remember { ServerPreferences(webViewContext) }
|
val prefs = remember { ServerPreferences(webViewContext) }
|
||||||
|
|
||||||
|
// Hub menu overlay state — the three-finger hold opens the menu right here
|
||||||
|
// over the dashboard (it used to jump to the remote screen).
|
||||||
|
val savedServers by prefs.savedServers.collectAsState(initial = emptyList())
|
||||||
|
val activeServer by prefs.activeServer.collectAsState(initial = null)
|
||||||
|
var showHubMenu by remember { mutableStateOf(false) }
|
||||||
|
var showPairScanner by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val gestureHintSeen by prefs.gestureHintSeen.collectAsState(initial = true)
|
val gestureHintSeen by prefs.gestureHintSeen.collectAsState(initial = true)
|
||||||
var gestureHintDismissed by remember { mutableStateOf(false) }
|
var gestureHintDismissed by remember { mutableStateOf(false) }
|
||||||
// Don't teach the gesture on top of the login/splash — arm the overlay
|
// Don't teach the gesture on top of the login/splash — arm the overlay
|
||||||
@ -741,7 +759,8 @@ fun WebViewScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Three-finger hold (500ms) → navigate to remote input.
|
// Three-finger hold (500ms) → open the hub menu overlay
|
||||||
|
// in place (Remote/Keyboard cards do the navigating).
|
||||||
// Three fingers, not two: two-finger scroll/pinch on the
|
// Three fingers, not two: two-finger scroll/pinch on the
|
||||||
// page collided with the old two-finger hold.
|
// page collided with the old two-finger hold.
|
||||||
var threeFingerStart = 0L
|
var threeFingerStart = 0L
|
||||||
@ -759,7 +778,7 @@ fun WebViewScreen(
|
|||||||
if (pointerCount >= 3 && !threeFingerFired && threeFingerStart > 0) {
|
if (pointerCount >= 3 && !threeFingerFired && threeFingerStart > 0) {
|
||||||
if (System.currentTimeMillis() - threeFingerStart > 500) {
|
if (System.currentTimeMillis() - threeFingerStart > 500) {
|
||||||
threeFingerFired = true
|
threeFingerFired = true
|
||||||
onRemoteInput()
|
showHubMenu = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -884,6 +903,68 @@ fun WebViewScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hub menu overlay — opened by the three-finger hold, drawn above
|
||||||
|
// everything (also reachable from the error screen, where switching
|
||||||
|
// servers is exactly what's needed).
|
||||||
|
NESMenu(
|
||||||
|
visible = showHubMenu,
|
||||||
|
servers = savedServers,
|
||||||
|
activeServer = activeServer,
|
||||||
|
onDismiss = { showHubMenu = false },
|
||||||
|
onSelectServer = { server ->
|
||||||
|
showHubMenu = false
|
||||||
|
scope.launch { prefs.setActiveServer(server) }
|
||||||
|
},
|
||||||
|
onAddServer = { server ->
|
||||||
|
scope.launch {
|
||||||
|
prefs.addSavedServer(server)
|
||||||
|
if (activeServer == null) prefs.setActiveServer(server)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onScanQr = { showPairScanner = true },
|
||||||
|
onEditServer = { original, updated ->
|
||||||
|
scope.launch {
|
||||||
|
prefs.updateSavedServer(original, updated)
|
||||||
|
// Editing the live server reloads the kiosk with the new
|
||||||
|
// address/credentials via the activeServer recomposition.
|
||||||
|
if (original.serialize() == activeServer?.serialize()) {
|
||||||
|
prefs.setActiveServer(updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRemoveServer = { server ->
|
||||||
|
scope.launch {
|
||||||
|
prefs.removeSavedServer(server)
|
||||||
|
// Nothing left to show — back to the Connect screen.
|
||||||
|
val remaining = savedServers.count { it.serialize() != server.serialize() }
|
||||||
|
if (remaining == 0) {
|
||||||
|
prefs.clearActiveServer()
|
||||||
|
showHubMenu = false
|
||||||
|
onDisconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRemote = { showHubMenu = false; onRemoteInput() },
|
||||||
|
onKeyboard = { showHubMenu = false; onRemoteKeyboard() },
|
||||||
|
onBackToWebView = { showHubMenu = false },
|
||||||
|
onMeshParty = onMeshParty?.let { open -> { showHubMenu = false; open() } },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pairing-QR scan launched from the menu's Nodes page; the menu stays
|
||||||
|
// open behind it so the new entry appears as soon as it closes.
|
||||||
|
QrScannerOverlay(
|
||||||
|
visible = showPairScanner,
|
||||||
|
onDismiss = { showPairScanner = false },
|
||||||
|
onServerScanned = { scan ->
|
||||||
|
showPairScanner = false
|
||||||
|
scope.launch {
|
||||||
|
val merged = prefs.upsertServer(scan.server)
|
||||||
|
FipsManager.registerNode(webViewContext, scan.fips, merged.displayName())
|
||||||
|
if (activeServer == null) prefs.setActiveServer(merged)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user