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.foundation.layout.Box11import androidx.compose.foundation.layout.fillMaxSize12import androidx.compose.runtime.Composable13import androidx.compose.runtime.LaunchedEffect14import androidx.compose.runtime.getValue15import androidx.compose.ui.Modifier16import androidx.lifecycle.compose.collectAsStateWithLifecycle17import androidx.navigation.NavHostController18import androidx.navigation.NavType19import androidx.navigation.compose.NavHost20import androidx.navigation.compose.composable21import androidx.navigation.compose.rememberNavController22import androidx.navigation.navArgument23import com.peachpasswords.ui.codex.CodexGenerateScreen24import com.peachpasswords.ui.codex.CodexRestoreScreen25import com.peachpasswords.ui.components.CopyFeedbackOverlay26import com.peachpasswords.ui.lock.LockScreen27import com.peachpasswords.ui.onboarding.OnboardingScreen28import com.peachpasswords.ui.pairing.ChirpPairScreen29import com.peachpasswords.ui.recovery.RecoveryWordEntryScreen30import com.peachpasswords.ui.settings.PanicScreen31import com.peachpasswords.ui.settings.SettingsScreen32import com.peachpasswords.ui.vault.VaultHealthScreen33import com.peachpasswords.ui.theme.PeachMotion34import com.peachpasswords.ui.vault.EntryDetailScreen35import com.peachpasswords.ui.vault.EntryEditScreen36import com.peachpasswords.ui.vault.ExportScreen37import com.peachpasswords.ui.vault.ImportScreen38import com.peachpasswords.ui.vault.NotesModeScreen39import com.peachpasswords.ui.vault.RecoveryWordsScreen40import com.peachpasswords.ui.vault.TrashScreen41import com.peachpasswords.ui.vault.VaultListScreen4243@Composable44fun PeachAppNavHost(45 navController: NavHostController = rememberNavController(),46 startDestination: PeachNav = PeachNav.Lock,47 isUnlocked: kotlinx.coroutines.flow.StateFlow<Boolean>? = null,48) {49 val nav: (PeachNav) -> Unit = { navController.navigate(it.route) }50 val back: () -> Unit = { navController.popBackStack() }5152 53 54 55 56 if (isUnlocked != null) {57 val unlocked by isUnlocked.collectAsStateWithLifecycle()58 LaunchedEffect(unlocked) {59 if (!unlocked) {60 val current = navController.currentDestination?.route61 if (current != null &&62 current != PeachNav.Lock.route &&63 current != PeachNav.Onboarding.route64 ) {65 navController.navigate(PeachNav.Lock.route) {66 popUpTo(0) { inclusive = true }67 launchSingleTop = true68 }69 }70 }71 }72 }7374 Box(Modifier.fillMaxSize()) {75 NavHost(76 navController = navController,77 startDestination = startDestination.route,78 enterTransition = {79 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {80 slideInVertically(81 initialOffsetY = { it },82 animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),83 ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))84 } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {85 EnterTransition.None86 } else {87 slideInVertically(88 initialOffsetY = { (it * 0.05f).toInt() },89 animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),90 ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))91 }92 },93 exitTransition = {94 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {95 ExitTransition.None96 } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {97 slideOutVertically(98 targetOffsetY = { it },99 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),100 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))101 } else {102 slideOutVertically(103 targetOffsetY = { -(it * 0.03f).toInt() },104 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),105 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))106 }107 },108 popEnterTransition = {109 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {110 EnterTransition.None111 } else {112 fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))113 }114 },115 popExitTransition = {116 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {117 slideOutVertically(118 targetOffsetY = { it },119 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),120 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))121 } else {122 slideOutVertically(123 targetOffsetY = { (it * 0.05f).toInt() },124 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),125 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))126 }127 },128 ) {129 composable(PeachNav.Onboarding.route) {130 OnboardingScreen(131 onFinished = {132 navController.navigate(PeachNav.Lock.route)133 },134 onRestore = { nav(PeachNav.Import) },135 )136 }137 composable(PeachNav.Lock.route) {138 LockScreen(139 onUnlocked = { dest ->140 navController.navigate(dest.route) {141 popUpTo(0) { inclusive = true }142 }143 },144 onNavigate = nav,145 )146 }147 composable(PeachNav.VaultList.route) {148 149 150 151 val hasOrigin = navController.previousBackStackEntry != null152 VaultListScreen(153 onNavigate = nav,154 onBack = if (hasOrigin) ({ navController.popBackStack() }) else null,155 )156 }157 composable(PeachNav.VaultNotes.route) {158 val hasOrigin = navController.previousBackStackEntry != null159 VaultListScreen(160 onNavigate = nav,161 onBack = if (hasOrigin) ({ navController.popBackStack() }) else null,162 startNoteOnly = true,163 )164 }165166 composable(167 route = PeachNav.EntryDetail.ROUTE,168 arguments = listOf(navArgument("id") { type = NavType.StringType }),169 ) { backStack ->170 171 172 173 174 175 176 177 val isRoot = navController.previousBackStackEntry == null178 EntryDetailScreen(179 id = backStack.arguments?.getString("id").orEmpty(),180 onNavigate = nav,181 onBack = if (isRoot) null else ({ navController.navigate(PeachNav.VaultList.route) }),182 )183 }184 composable(185 route = PeachNav.EntryEdit.ROUTE,186 arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true }),187 ) { backStack ->188 val editId = backStack.arguments?.getString("id")?.takeIf { it.isNotBlank() }189 if (editId == null) {190 EntryEditScreen(191 id = null,192 onBack = back,193 onOpenNote = { noteId ->194 navController.navigate(PeachNav.NotesMode.of(noteId)) {195 popUpTo(PeachNav.EntryEdit.ROUTE) { inclusive = true }196 }197 },198 )199 } else {200 EntryDetailScreen(201 id = editId,202 onNavigate = nav,203 onBack = back,204 )205 }206 }207208 composable(209 route = PeachNav.NotesMode.ROUTE,210 arguments = listOf(navArgument("id") { type = NavType.StringType }),211 ) {212 NotesModeScreen(onBack = back)213 }214 composable(PeachNav.Settings.route) { SettingsScreen(onBack = back, onNavigate = nav) }215 composable(PeachNav.CodexGenerate.route) {216 CodexGenerateScreen(217 onBack = back,218 onSetupRecovery = { nav(PeachNav.RecoverySetup) },219 onUpgradePro = { nav(PeachNav.Settings) },220 )221 }222 composable(PeachNav.CodexRestore.route) {223 CodexRestoreScreen(224 onBack = back,225 onRestored = {226 navController.navigate(PeachNav.VaultList.route) {227 popUpTo(0) { inclusive = true }228 }229 },230 )231 }232 composable(PeachNav.ChirpPair.route) { ChirpPairScreen(onBack = back) }233 composable(PeachNav.Health.route) {234 VaultHealthScreen(onBack = back, onEditEntry = { nav(PeachNav.EntryDetail(it)) })235 }236 composable(PeachNav.Trash.route) { TrashScreen(onBack = back) }237 composable(PeachNav.Import.route) {238 ImportScreen(239 onBack = back,240 onCodex = { nav(PeachNav.CodexRestore) },241 onVaultCreated = {242 navController.navigate(PeachNav.RecoverySetup.route) {243 popUpTo(0) { inclusive = true }244 }245 },246 )247 }248 composable(PeachNav.Export.route) { ExportScreen(onBack = back) }249 composable(PeachNav.Panic.route) {250 PanicScreen(251 onBack = back,252 onDestroyed = {253 navController.navigate(PeachNav.Lock.route) {254 popUpTo(0) { inclusive = true }255 }256 },257 )258 }259 composable(PeachNav.RecoverySetup.route) {260 RecoveryWordsScreen(261 requireConfirm = true,262 onDone = {263 navController.navigate(PeachNav.VaultList.route) {264 popUpTo(0) { inclusive = true }265 }266 },267 )268 }269 composable(PeachNav.RecoveryView.route) {270 RecoveryWordsScreen(requireConfirm = false, onDone = back)271 }272 composable(PeachNav.RecoveryEntry.route) {273 RecoveryWordEntryScreen(274 onSuccess = {275 navController.navigate(PeachNav.VaultList.route) {276 popUpTo(0) { inclusive = true }277 }278 },279 onBack = back,280 onRestore = { nav(PeachNav.Import) },281 )282 }283 }284 CopyFeedbackOverlay()285 }286}287288internal fun isVaultSheetTransition(fromRoute: String?, toRoute: String?, reverse: Boolean = false): Boolean {289 val fromEntry = isEntrySurfaceRoute(fromRoute)290 val toEntry = isEntrySurfaceRoute(toRoute)291 return if (reverse) {292 fromRoute == PeachNav.VaultList.route && toEntry293 } else {294 fromEntry && toRoute == PeachNav.VaultList.route295 }296}297298internal fun isEntrySurfaceRoute(route: String?): Boolean =299 route?.startsWith("entry/") == true || route?.startsWith("notes/") == true