package com.m57.hermescontrol import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.DrawerValue import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.data.ws.HermesWsClient import com.m57.hermescontrol.theme.BottomNavDisplayMode import com.m57.hermescontrol.theme.LocalHermesStatusColors import com.m57.hermescontrol.ui.settings.SettingsAboutPage import com.m57.hermescontrol.ui.settings.SettingsAppearancePage import com.m57.hermescontrol.ui.settings.SettingsBehaviorPage import com.m57.hermescontrol.ui.settings.SettingsChatPage import com.m57.hermescontrol.ui.settings.SettingsConnectionPage import com.m57.hermescontrol.ui.settings.SettingsNavBarPage import com.m57.hermescontrol.ui.settings.SettingsViewModel import kotlinx.coroutines.launch import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent import com.m57.hermescontrol.ui.pairing.PairingCodeEntryScreen as PairingCodeEntryScreenContent private fun resolveBottomNavItems(names: List): List = names.mapNotNull { name -> ScreenRegistry.ALL_SCREENS.firstOrNull { it.key::class.simpleName == name } } private val DRAWER_GESTURE_SCREENS: Set = ScreenRegistry.ALL_SCREENS.mapTo(mutableSetOf()) { it.key } private fun appEntryProvider( sessionId: String?, openDrawer: () -> Unit, ) = entryProvider { entry { // B7 (Jun 30 2026, kanban t_424): route landing screen buttons through navigateTo to prevent duplicate screens LandingScreenContent( onAuthLogin = { NavigationController.navigateTo(AuthLoginScreen) }, onPairingLogin = { NavigationController.navigateTo(PairingCodeEntryScreen) }, ) } entry { AuthLoginScreenContent( onConnected = { NavigationController.resetTo(ChatScreen) }, onBack = { NavigationController.goBack() }, ) } entry { PairingCodeEntryScreenContent( onConnected = { NavigationController.resetTo(ChatScreen) }, onBack = { NavigationController.goBack() }, ) } ScreenRegistry.ALL_SCREENS.forEach { screen -> addEntryProvider(clazz = screen.key::class) { screen.content(sessionId, openDrawer) } } // ── Settings drill-down sub-pages ─────────────────────────────────── entry { SettingsConnectionPage( onBack = { NavigationController.goBack() }, onLogout = { /* handled by caller via goBack fallback */ }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsAppearancePage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsChatPage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsNavBarPage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsBehaviorPage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsAboutPage( onBack = { NavigationController.goBack() }, onLogout = { /* handled by caller via goBack fallback */ }, viewModel = viewModel { SettingsViewModel() }, ) } } @Composable fun MainNavigation(sessionId: String? = null) { val token by AuthManager.tokenFlow.collectAsState() val hasToken = !token.isNullOrBlank() val startScreen: NavKey = if (hasToken) ChatScreen else LandingScreen val backStack = remember(startScreen) { NavBackStack(startScreen) } NavigationController.backStack = backStack val currentScreen = backStack.lastOrNull() ?: startScreen val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() // Synchronous drawer dismiss hook used by NavigationController.navigateTo // when drilling into a non-gesture sub-page (see NavigationController.closeDrawer). NavigationController.closeDrawer = { scope.launch { drawerState.close() } } val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState() val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState() val bottomNavItems = resolveBottomNavItems(bottomNavItemsState) val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.mapTo(mutableSetOf()) { it.key } } LaunchedEffect(bottomNavKeys) { NavigationController.updatePrimaryScreens(bottomNavKeys) } val showBottomBar = currentScreen != LandingScreen && currentScreen != AuthLoginScreen && currentScreen != PairingCodeEntryScreen val gesturesEnabled = currentScreen in DRAWER_GESTURE_SCREENS var drawerGesturesEnabled by remember { mutableStateOf(false) } LaunchedEffect(gesturesEnabled) { if (gesturesEnabled) { // Delay enabling gestures slightly to prevent residual swipe-to-open from back button taps kotlinx.coroutines.delay(200) drawerGesturesEnabled = true } else { drawerGesturesEnabled = false } } val openDrawer: () -> Unit = { scope.launch { drawerState.open() } } // B7 (Jun 30 2026, kanban t_424): close drawer if gestures are disabled to dismiss scrim. // Also force-close on transition into a non-gesture screen so the drawer's scrim // surface cannot intercept the first back-button tap (e.g. settings sub-pages drilled // down from the gesture-enabled Settings root). snapTo(Closed) is a safe no-op when // already closed. LaunchedEffect(drawerGesturesEnabled) { if (!drawerGesturesEnabled) { drawerState.snapTo(DrawerValue.Closed) } } ModalNavigationDrawer( drawerState = drawerState, gesturesEnabled = drawerGesturesEnabled, drawerContent = { ModalDrawerSheet( modifier = Modifier.verticalScroll(rememberScrollState()), ) { val connectionStatus by HermesWsClient.connectionStatus.collectAsState() val statusColor = when (connectionStatus) { ConnectionStatus.CONNECTED -> LocalHermesStatusColors.current.success ConnectionStatus.CONNECTING, ConnectionStatus.RECONNECTING, -> LocalHermesStatusColors.current.warning ConnectionStatus.DISCONNECTED, ConnectionStatus.NO_NETWORK, ConnectionStatus.AUTH_EXPIRED, -> LocalHermesStatusColors.current.error } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 2.dp), ) { Text( text = stringResource(R.string.nav_drawer_title), style = MaterialTheme.typography.headlineSmall.copy( fontWeight = FontWeight.Bold, ), color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(8.dp)) Box( modifier = Modifier .size(10.dp) .background(color = statusColor, shape = CircleShape), ) } Text( text = stringResource(R.string.nav_drawer_subtitle), modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, end = 16.dp), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) for (section in DrawerSection.entries) { Text( text = stringResource(section.titleRes).uppercase(), modifier = Modifier.padding( start = 16.dp, top = 8.dp, bottom = 4.dp, end = 16.dp, ), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.SemiBold, ) ScreenRegistry.ALL_SCREENS .filter { it.drawerSection == section } .forEach { entry -> NavigationDrawerItem( icon = { Icon(entry.icon, contentDescription = null) }, label = { Text(stringResource(entry.labelRes)) }, selected = currentScreen == entry.key, onClick = { scope.launch { drawerState.close() } NavigationController.navigateTo(entry.key) }, colors = NavigationDrawerItemDefaults.colors( selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer, selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, ), ) } } Spacer(modifier = Modifier.height(8.dp)) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) Spacer(modifier = Modifier.height(12.dp)) } }, ) { Scaffold( contentWindowInsets = WindowInsets.navigationBars, bottomBar = { if (showBottomBar) { val barHeight = when (bottomNavDisplayMode) { BottomNavDisplayMode.ICON_ONLY -> 56.dp BottomNavDisplayMode.TEXT_ONLY -> 44.dp BottomNavDisplayMode.ICON_AND_TEXT -> 80.dp } NavigationBar( modifier = Modifier.height(barHeight), ) { bottomNavItems.forEach { item -> val showIcon = bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT || bottomNavDisplayMode == BottomNavDisplayMode.ICON_ONLY val showLabel = bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT || bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY val isSelected = currentScreen == item.key NavigationBarItem( selected = isSelected, onClick = { NavigationController.navigateTo(item.key) }, colors = if (bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY) { NavigationBarItemDefaults.colors( indicatorColor = Color.Transparent, ) } else { NavigationBarItemDefaults.colors() }, icon = { if (showIcon) { Icon(item.icon, contentDescription = stringResource(item.labelRes)) } else { Text( text = stringResource(item.labelRes), style = MaterialTheme.typography.labelMedium, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, ) } }, label = if (showLabel && showIcon) { { Text(stringResource(item.labelRes)) } } else { null }, modifier = Modifier.testTag( "nav_${item.key::class.simpleName?.lowercase()?.removeSuffix("screen") ?: ""}", ), ) } } } }, ) { paddingValues -> NavDisplay( backStack = backStack, onBack = { NavigationController.goBack() }, entryProvider = appEntryProvider(sessionId, openDrawer), modifier = Modifier .padding(paddingValues) .consumeWindowInsets(paddingValues) .fillMaxSize(), ) } } }