package com.peachpasswords.ui import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.peachpasswords.ui.codex.CodexGenerateScreen import com.peachpasswords.ui.codex.CodexRestoreScreen import com.peachpasswords.ui.components.CopyFeedbackOverlay import com.peachpasswords.ui.lock.LockScreen import com.peachpasswords.ui.onboarding.OnboardingScreen import com.peachpasswords.ui.pairing.CodePairScreen import com.peachpasswords.ui.recovery.RecoveryWordEntryScreen import com.peachpasswords.ui.settings.PanicScreen import com.peachpasswords.ui.settings.SettingsScreen import com.peachpasswords.ui.vault.VaultHealthScreen import com.peachpasswords.ui.theme.PeachMotion import com.peachpasswords.ui.vault.EntryDetailScreen import com.peachpasswords.ui.vault.EntryEditScreen import com.peachpasswords.ui.vault.ExportScreen import com.peachpasswords.ui.vault.ImportScreen import com.peachpasswords.ui.vault.NotesModeScreen import com.peachpasswords.ui.vault.RecoveryWordsScreen import com.peachpasswords.ui.vault.TrashScreen import com.peachpasswords.ui.vault.VaultListScreen @Composable fun PeachAppNavHost( navController: NavHostController = rememberNavController(), startDestination: PeachNav = PeachNav.Lock, isUnlocked: kotlinx.coroutines.flow.StateFlow? = null, ) { val nav: (PeachNav) -> Unit = { navController.navigate(it.route) } val back: () -> Unit = { navController.popBackStack() } // When the vault locks for any reason — manual lock button, idle auto-lock, or // background lock (CF-1/CF-2) — drop to the lock screen and clear history so no // post-unlock screen lingers behind it. Guarded so it never fires on the lock // or onboarding screens (where the vault is legitimately locked). if (isUnlocked != null) { val unlocked by isUnlocked.collectAsStateWithLifecycle() LaunchedEffect(unlocked) { if (!unlocked) { val current = navController.currentDestination?.route if (current != null && current != PeachNav.Lock.route && current != PeachNav.Onboarding.route ) { navController.navigate(PeachNav.Lock.route) { popUpTo(0) { inclusive = true } launchSingleTop = true } } } } } Box(Modifier.fillMaxSize()) { NavHost( navController = navController, startDestination = startDestination.route, enterTransition = { if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) { slideInVertically( initialOffsetY = { it }, animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring), ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring)) } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) { EnterTransition.None } else { slideInVertically( initialOffsetY = { (it * 0.05f).toInt() }, animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring), ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring)) } }, exitTransition = { if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) { ExitTransition.None } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) { slideOutVertically( targetOffsetY = { it }, animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat), ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat)) } else { slideOutVertically( targetOffsetY = { -(it * 0.03f).toInt() }, animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat), ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat)) } }, popEnterTransition = { if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) { EnterTransition.None } else { fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring)) } }, popExitTransition = { if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) { slideOutVertically( targetOffsetY = { it }, animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat), ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat)) } else { slideOutVertically( targetOffsetY = { (it * 0.05f).toInt() }, animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat), ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat)) } }, ) { composable(PeachNav.Onboarding.route) { OnboardingScreen( onFinished = { navController.navigate(PeachNav.Lock.route) }, onRestore = { nav(PeachNav.Import) }, ) } composable(PeachNav.Lock.route) { LockScreen( onUnlocked = { dest -> navController.navigate(dest.route) { popUpTo(0) { inclusive = true } } }, onNavigate = nav, ) } composable(PeachNav.VaultList.route) { // C1: non-null only when the list was pushed on top of an entry // (slide-up-to-open from EntryDetailSearchStrip) — slide back // down pops back to that exact entry instance. val hasOrigin = navController.previousBackStackEntry != null VaultListScreen( onNavigate = nav, onBack = if (hasOrigin) ({ navController.popBackStack() }) else null, ) } composable(PeachNav.VaultNotes.route) { val hasOrigin = navController.previousBackStackEntry != null VaultListScreen( onNavigate = nav, onBack = if (hasOrigin) ({ navController.popBackStack() }) else null, startNoteOnly = true, ) } composable( route = PeachNav.EntryDetail.ROUTE, arguments = listOf(navArgument("id") { type = NavType.StringType }), ) { backStack -> // C1: when this entry is the post-unlock landing surface, its // unlock navigation popped Lock off the stack (see the Lock // composable below), so there's no previous entry — it's the // deck's root, not a screen pushed from the vault list, and // shouldn't offer a "back to list" affordance that implies // otherwise. Only entries reached by tapping a vault-list row // (a real push) get a real back target. val isRoot = navController.previousBackStackEntry == null EntryDetailScreen( id = backStack.arguments?.getString("id").orEmpty(), onNavigate = nav, onBack = if (isRoot) null else ({ navController.navigate(PeachNav.VaultList.route) }), ) } composable( route = PeachNav.EntryEdit.ROUTE, arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true }), ) { backStack -> val editId = backStack.arguments?.getString("id")?.takeIf { it.isNotBlank() } if (editId == null) { EntryEditScreen( id = null, onBack = back, onOpenNote = { noteId -> navController.navigate(PeachNav.NotesMode.of(noteId)) { popUpTo(PeachNav.EntryEdit.ROUTE) { inclusive = true } } }, ) } else { EntryDetailScreen( id = editId, onNavigate = nav, onBack = back, ) } } composable( route = PeachNav.NotesMode.ROUTE, arguments = listOf(navArgument("id") { type = NavType.StringType }), ) { NotesModeScreen(onBack = back) } composable(PeachNav.Settings.route) { SettingsScreen(onBack = back, onNavigate = nav) } composable(PeachNav.CodexGenerate.route) { CodexGenerateScreen( onBack = back, onSetupRecovery = { nav(PeachNav.RecoverySetup) }, onUpgradePro = { nav(PeachNav.Settings) }, ) } composable(PeachNav.CodexRestore.route) { CodexRestoreScreen( onBack = back, onRestored = { navController.navigate(PeachNav.VaultList.route) { popUpTo(0) { inclusive = true } } }, ) } composable(PeachNav.CodePair.route) { CodePairScreen(onBack = back) } composable(PeachNav.Health.route) { VaultHealthScreen(onBack = back, onEditEntry = { nav(PeachNav.EntryDetail(it)) }) } composable(PeachNav.Trash.route) { TrashScreen(onBack = back) } composable(PeachNav.Import.route) { ImportScreen( onBack = back, onCodex = { nav(PeachNav.CodexRestore) }, onVaultCreated = { navController.navigate(PeachNav.RecoverySetup.route) { popUpTo(0) { inclusive = true } } }, ) } composable(PeachNav.Export.route) { ExportScreen(onBack = back) } composable(PeachNav.Panic.route) { PanicScreen( onBack = back, onDestroyed = { navController.navigate(PeachNav.Lock.route) { popUpTo(0) { inclusive = true } } }, ) } composable(PeachNav.RecoverySetup.route) { RecoveryWordsScreen( requireConfirm = true, onDone = { navController.navigate(PeachNav.VaultList.route) { popUpTo(0) { inclusive = true } } }, ) } composable(PeachNav.RecoveryView.route) { RecoveryWordsScreen(requireConfirm = false, onDone = back) } composable(PeachNav.RecoveryEntry.route) { RecoveryWordEntryScreen( onSuccess = { navController.navigate(PeachNav.VaultList.route) { popUpTo(0) { inclusive = true } } }, onBack = back, onRestore = { nav(PeachNav.Import) }, ) } } CopyFeedbackOverlay() } } internal fun isVaultSheetTransition(fromRoute: String?, toRoute: String?, reverse: Boolean = false): Boolean { val fromEntry = isEntrySurfaceRoute(fromRoute) val toEntry = isEntrySurfaceRoute(toRoute) return if (reverse) { fromRoute == PeachNav.VaultList.route && toEntry } else { fromEntry && toRoute == PeachNav.VaultList.route } } internal fun isEntrySurfaceRoute(route: String?): Boolean = route?.startsWith("entry/") == true || route?.startsWith("notes/") == true