package com.m57.hermescontrol import android.app.Activity import android.widget.Toast import androidx.activity.compose.BackHandler 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.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.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf 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.platform.LocalContext 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.LocalHermesStatusColors import com.m57.hermescontrol.ui.common.DisableDrawerGestures import com.m57.hermescontrol.ui.common.DrawerGestureController import com.m57.hermescontrol.ui.common.LocalDrawerGestureController import com.m57.hermescontrol.ui.plugins.MemoryProviderDetailScreen 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.SettingsLanguagePage import com.m57.hermescontrol.ui.settings.SettingsVaultPage import com.m57.hermescontrol.ui.settings.SettingsViewModel import com.m57.hermescontrol.ui.toolsets.ToolsetDetailScreen import kotlinx.coroutines.launch import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent 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) }, ) // Landing doesn't use HermesScaffold — opt out of drawer gestures explicitly (issue #619). DisableDrawerGestures() } entry { AuthLoginScreenContent( onConnected = { NavigationController.resetTo(ChatScreen) }, onBack = { NavigationController.goBack() }, ) DisableDrawerGestures() } ScreenRegistry.ALL_SCREENS.forEach { screen -> addEntryProvider(clazz = screen.key::class) { screen.content(sessionId, openDrawer) } } // ── Settings drill-down sub-pages ─────────────────────────────────── // Each passes drawerGesturesEnabled = false to HermesScaffold — this is the // single source of truth that prevents the drawer-scrim stuck-open bug // (issue #619). No global gesture set, no closeDrawer callback, no // LaunchedEffect(snapTo(Closed)) — the scaffold reconciles automatically. entry { SettingsConnectionPage( onBack = { NavigationController.goBack() }, onLogout = { /* handled by caller via goBack fallback */ }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsAppearancePage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsLanguagePage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsChatPage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsBehaviorPage( onBack = { NavigationController.goBack() }, viewModel = viewModel { SettingsViewModel() }, ) } entry { SettingsVaultPage( onBack = { NavigationController.goBack() }, ) } entry { SettingsAboutPage( onBack = { NavigationController.goBack() }, ) } // ── Toolset detail drill-down (issue #782) ──────────────────────── entry { key -> ToolsetDetailScreen( name = key.name, label = key.label, onBack = { NavigationController.goBack() }, ) } // ── Memory provider detail drill-down (issue #783) ───────────────── entry { key -> MemoryProviderDetailScreen( name = key.name, label = key.label, onBack = { NavigationController.goBack() }, ) } // ── Multi-Agent Group Chat Room ───────────────────────────────────── entry { key -> com.m57.hermescontrol.ui.bots.group.GroupChatScreen( groupName = key.groupName, onBack = { NavigationController.goBack() }, ) } // ── Kanban Task Detail ───────────────────────────────────────────── entry { key -> com.m57.hermescontrol.ui.kanban.KanbanTaskScreen( boardSlug = key.boardSlug, taskId = key.taskId, onBack = { NavigationController.goBack() }, ) } } @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() // Single source of truth for drawer gestures (issue #619). // HermesScaffold / DisableDrawerGestures reconcile each screen's preference // into this controller via SideEffect; ModalNavigationDrawer reads .enabled // below. No DRAWER_GESTURE_SCREENS set, no LaunchedEffect(snapTo(Closed)), // no closeDrawer callback — the controller closes the drawer itself when a // screen opts out. val gestureController = remember(drawerState, scope) { DrawerGestureController(drawerState, scope) } val openDrawer: () -> Unit = { scope.launch { drawerState.open() } } val context = LocalContext.current var lastBackPressTime by remember { mutableLongStateOf(0L) } val backToastText = stringResource(R.string.press_back_again_to_exit) // Double-back to exit on root ChatScreen BackHandler(enabled = currentScreen == ChatScreen && backStack.size == 1) { val now = System.currentTimeMillis() if (now - lastBackPressTime < 2000L) { (context as? Activity)?.finish() } else { lastBackPressTime = now Toast.makeText(context, backToastText, Toast.LENGTH_SHORT).show() } } // Safety fallback: if a non-chat screen somehow sits alone on stack, swipe back returns to ChatScreen BackHandler( enabled = currentScreen != ChatScreen && currentScreen != LandingScreen && backStack.size == 1, ) { NavigationController.goBack() } CompositionLocalProvider(LocalDrawerGestureController provides gestureController) { ModalNavigationDrawer( drawerState = drawerState, gesturesEnabled = gestureController.enabled, 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.primary, unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, ), ) } } Spacer(modifier = Modifier.height(8.dp)) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) Spacer(modifier = Modifier.height(12.dp)) } }, ) { Scaffold( contentWindowInsets = WindowInsets.navigationBars, ) { paddingValues -> NavDisplay( backStack = backStack, onBack = { NavigationController.goBack() }, entryProvider = appEntryProvider(sessionId, openDrawer), modifier = Modifier .padding(paddingValues) .consumeWindowInsets(paddingValues) .fillMaxSize(), ) } } } }