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 49 50 51 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