PeachAppNavHost.kt

10.6 KB · raw

1package com.peachpasswords.ui23import androidx.compose.animation.EnterTransition4import androidx.compose.animation.ExitTransition5import androidx.compose.animation.core.tween6import androidx.compose.animation.fadeIn7import androidx.compose.animation.fadeOut8import androidx.compose.animation.slideInVertically9import androidx.compose.animation.slideOutVertically10import androidx.compose.runtime.Composable11import androidx.compose.runtime.LaunchedEffect12import androidx.compose.runtime.getValue13import androidx.lifecycle.compose.collectAsStateWithLifecycle14import androidx.navigation.NavHostController15import androidx.navigation.NavType16import androidx.navigation.compose.NavHost17import androidx.navigation.compose.composable18import androidx.navigation.compose.rememberNavController19import androidx.navigation.navArgument20import com.peachpasswords.ui.codex.CodexGenerateScreen21import com.peachpasswords.ui.codex.CodexRestoreScreen22import com.peachpasswords.ui.lock.LockScreen23import com.peachpasswords.ui.onboarding.OnboardingScreen24import com.peachpasswords.ui.pairing.ChirpPairScreen25import com.peachpasswords.ui.recovery.RecoveryWordEntryScreen26import com.peachpasswords.ui.settings.PanicScreen27import com.peachpasswords.ui.settings.SettingsScreen28import com.peachpasswords.ui.vault.VaultHealthScreen29import com.peachpasswords.ui.theme.PeachMotion30import com.peachpasswords.ui.vault.EntryDetailScreen31import com.peachpasswords.ui.vault.EntryEditScreen32import com.peachpasswords.ui.vault.ExportScreen33import com.peachpasswords.ui.vault.ImportScreen34import com.peachpasswords.ui.vault.NotesModeScreen35import com.peachpasswords.ui.vault.RecoveryWordsScreen36import com.peachpasswords.ui.vault.TrashScreen37import com.peachpasswords.ui.vault.VaultListScreen3839@Composable40fun PeachAppNavHost(41    navController: NavHostController = rememberNavController(),42    startDestination: PeachNav = PeachNav.Lock,43    isUnlocked: kotlinx.coroutines.flow.StateFlow<Boolean>? = null,44) {45    val nav: (PeachNav) -> Unit = { navController.navigate(it.route) }46    val back: () -> Unit = { navController.popBackStack() }4748    // When the vault locks for any reason — manual lock button, idle auto-lock, or49    // background lock (CF-1/CF-2) — drop to the lock screen and clear history so no50    // post-unlock screen lingers behind it. Guarded so it never fires on the lock51    // or onboarding screens (where the vault is legitimately locked).52    if (isUnlocked != null) {53        val unlocked by isUnlocked.collectAsStateWithLifecycle()54        LaunchedEffect(unlocked) {55            if (!unlocked) {56                val current = navController.currentDestination?.route57                if (current != null &&58                    current != PeachNav.Lock.route &&59                    current != PeachNav.Onboarding.route60                ) {61                    navController.navigate(PeachNav.Lock.route) {62                        popUpTo(0) { inclusive = true }63                        launchSingleTop = true64                    }65                }66            }67        }68    }6970    NavHost(71        navController = navController,72        startDestination = startDestination.route,73        enterTransition = {74            if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {75                slideInVertically(76                    initialOffsetY = { it },77                    animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),78                ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))79            } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {80                EnterTransition.None81            } else {82                slideInVertically(83                    initialOffsetY = { (it * 0.05f).toInt() },84                    animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),85                ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))86            }87        },88        exitTransition = {89            if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {90                ExitTransition.None91            } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {92                slideOutVertically(93                    targetOffsetY = { it },94                    animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),95                ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))96            } else {97                slideOutVertically(98                    targetOffsetY = { -(it * 0.03f).toInt() },99                    animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),100                ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))101            }102        },103        popEnterTransition = {104            if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {105                EnterTransition.None106            } else {107                fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))108            }109        },110        popExitTransition = {111            if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {112                slideOutVertically(113                    targetOffsetY = { it },114                    animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),115                ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))116            } else {117                slideOutVertically(118                    targetOffsetY = { (it * 0.05f).toInt() },119                    animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),120                ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))121            }122        },123    ) {124        composable(PeachNav.Onboarding.route) {125            OnboardingScreen(126                onFinished = {127                    navController.navigate(PeachNav.Lock.route) {128                        popUpTo(PeachNav.Onboarding.route) { inclusive = true }129                    }130                },131            )132        }133        composable(PeachNav.Lock.route) {134            LockScreen(135                onUnlocked = { dest ->136                    navController.navigate(dest.route) {137                        popUpTo(PeachNav.Lock.route) { inclusive = true }138                    }139                },140                onNavigate = nav,141            )142        }143        composable(PeachNav.VaultList.route) { VaultListScreen(onNavigate = nav) }144145        composable(146            route = PeachNav.EntryDetail.ROUTE,147            arguments = listOf(navArgument("id") { type = NavType.StringType }),148        ) { backStack ->149            EntryDetailScreen(150                id = backStack.arguments?.getString("id").orEmpty(),151                onNavigate = nav,152                onBack = { navController.navigate(PeachNav.VaultList.route) },153            )154        }155        composable(156            route = PeachNav.EntryEdit.ROUTE,157            arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true }),158        ) { backStack ->159            val editId = backStack.arguments?.getString("id")?.takeIf { it.isNotBlank() }160            if (editId == null) {161                EntryEditScreen(162                    id = null,163                    onBack = back,164                    onOpenNote = { noteId ->165                        navController.navigate(PeachNav.NotesMode.of(noteId)) {166                            popUpTo(PeachNav.EntryEdit.ROUTE) { inclusive = true }167                        }168                    },169                )170            } else {171                EntryDetailScreen(172                    id = editId,173                    onNavigate = nav,174                    onBack = back,175                )176            }177        }178179        composable(180            route = PeachNav.NotesMode.ROUTE,181            arguments = listOf(navArgument("id") { type = NavType.StringType }),182        ) {183            NotesModeScreen(onBack = back)184        }185        composable(PeachNav.Settings.route) { SettingsScreen(onBack = back, onNavigate = nav) }186        composable(PeachNav.CodexGenerate.route) {187            CodexGenerateScreen(188                onBack = back,189                onSetupRecovery = { nav(PeachNav.RecoverySetup) },190                onUpgradePro = { nav(PeachNav.Settings) },191            )192        }193        composable(PeachNav.CodexRestore.route) { CodexRestoreScreen(onBack = back, onRestored = back) }194        composable(PeachNav.ChirpPair.route) { ChirpPairScreen(onBack = back) }195        composable(PeachNav.Health.route) {196            VaultHealthScreen(onBack = back, onEditEntry = { nav(PeachNav.EntryDetail(it)) })197        }198        composable(PeachNav.Trash.route) { TrashScreen(onBack = back) }199        composable(PeachNav.Import.route) { ImportScreen(onBack = back) }200        composable(PeachNav.Export.route) { ExportScreen(onBack = back) }201        composable(PeachNav.Panic.route) {202            PanicScreen(203                onBack = back,204                onDestroyed = {205                    navController.navigate(PeachNav.Lock.route) {206                        popUpTo(0) { inclusive = true }207                    }208                },209            )210        }211        composable(PeachNav.RecoverySetup.route) {212            RecoveryWordsScreen(213                requireConfirm = true,214                onDone = {215                    navController.navigate(PeachNav.VaultList.route) {216                        popUpTo(PeachNav.Lock.route) { inclusive = true }217                    }218                },219            )220        }221        composable(PeachNav.RecoveryView.route) {222            RecoveryWordsScreen(requireConfirm = false, onDone = back)223        }224        composable(PeachNav.RecoveryEntry.route) {225            RecoveryWordEntryScreen(226                onSuccess = {227                    navController.navigate(PeachNav.VaultList.route) {228                        popUpTo(PeachNav.Lock.route) { inclusive = true }229                    }230                },231                onBack = back,232                onRestore = { nav(PeachNav.Import) },233            )234        }235    }236}237238internal fun isVaultSheetTransition(fromRoute: String?, toRoute: String?, reverse: Boolean = false): Boolean {239    val fromEntry = isEntrySurfaceRoute(fromRoute)240    val toEntry = isEntrySurfaceRoute(toRoute)241    return if (reverse) {242        fromRoute == PeachNav.VaultList.route && toEntry243    } else {244        fromEntry && toRoute == PeachNav.VaultList.route245    }246}247248internal fun isEntrySurfaceRoute(route: String?): Boolean =249    route?.startsWith("entry/") == true || route?.startsWith("notes/") == true