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.pairing.CodePairScreen30import com.peachpasswords.ui.recovery.RecoveryWordEntryScreen31import com.peachpasswords.ui.settings.PanicScreen32import com.peachpasswords.ui.settings.SettingsScreen33import com.peachpasswords.ui.vault.VaultHealthScreen34import com.peachpasswords.ui.theme.PeachMotion35import com.peachpasswords.ui.vault.EntryDetailScreen36import com.peachpasswords.ui.vault.EntryEditScreen37import com.peachpasswords.ui.vault.ExportScreen38import com.peachpasswords.ui.vault.ImportScreen39import com.peachpasswords.ui.vault.NotesModeScreen40import com.peachpasswords.ui.vault.RecoveryWordsScreen41import com.peachpasswords.ui.vault.TrashScreen42import com.peachpasswords.ui.vault.VaultListScreen4344@Composable45fun PeachAppNavHost(46 navController: NavHostController = rememberNavController(),47 startDestination: PeachNav = PeachNav.Lock,48 isUnlocked: kotlinx.coroutines.flow.StateFlow<Boolean>? = null,49) {50 val nav: (PeachNav) -> Unit = { navController.navigate(it.route) }51 val back: () -> Unit = { navController.popBackStack() }5253 54 55 56 57 if (isUnlocked != null) {58 val unlocked by isUnlocked.collectAsStateWithLifecycle()59 LaunchedEffect(unlocked) {60 if (!unlocked) {61 val current = navController.currentDestination?.route62 if (current != null &&63 current != PeachNav.Lock.route &&64 current != PeachNav.Onboarding.route65 ) {66 navController.navigate(PeachNav.Lock.route) {67 popUpTo(0) { inclusive = true }68 launchSingleTop = true69 }70 }71 }72 }73 }7475 Box(Modifier.fillMaxSize()) {76 NavHost(77 navController = navController,78 startDestination = startDestination.route,79 enterTransition = {80 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {81 slideInVertically(82 initialOffsetY = { it },83 animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),84 ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))85 } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {86 EnterTransition.None87 } else {88 slideInVertically(89 initialOffsetY = { (it * 0.05f).toInt() },90 animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),91 ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))92 }93 },94 exitTransition = {95 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {96 ExitTransition.None97 } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {98 slideOutVertically(99 targetOffsetY = { it },100 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),101 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))102 } else {103 slideOutVertically(104 targetOffsetY = { -(it * 0.03f).toInt() },105 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),106 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))107 }108 },109 popEnterTransition = {110 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {111 EnterTransition.None112 } else {113 fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))114 }115 },116 popExitTransition = {117 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {118 slideOutVertically(119 targetOffsetY = { it },120 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),121 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))122 } else {123 slideOutVertically(124 targetOffsetY = { (it * 0.05f).toInt() },125 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),126 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))127 }128 },129 ) {130 composable(PeachNav.Onboarding.route) {131 OnboardingScreen(132 onFinished = {133 navController.navigate(PeachNav.Lock.route)134 },135 onRestore = { nav(PeachNav.Import) },136 )137 }138 composable(PeachNav.Lock.route) {139 LockScreen(140 onUnlocked = { dest ->141 navController.navigate(dest.route) {142 popUpTo(0) { inclusive = true }143 }144 },145 onNavigate = nav,146 )147 }148 composable(PeachNav.VaultList.route) {149 150 151 152 val hasOrigin = navController.previousBackStackEntry != null153 VaultListScreen(154 onNavigate = nav,155 onBack = if (hasOrigin) ({ navController.popBackStack() }) else null,156 )157 }158 composable(PeachNav.VaultNotes.route) {159 val hasOrigin = navController.previousBackStackEntry != null160 VaultListScreen(161 onNavigate = nav,162 onBack = if (hasOrigin) ({ navController.popBackStack() }) else null,163 startNoteOnly = true,164 )165 }166167 composable(168 route = PeachNav.EntryDetail.ROUTE,169 arguments = listOf(navArgument("id") { type = NavType.StringType }),170 ) { backStack ->171 172 173 174 175 176 177 178 val isRoot = navController.previousBackStackEntry == null179 EntryDetailScreen(180 id = backStack.arguments?.getString("id").orEmpty(),181 onNavigate = nav,182 onBack = if (isRoot) null else ({ navController.navigate(PeachNav.VaultList.route) }),183 )184 }185 composable(186 route = PeachNav.EntryEdit.ROUTE,187 arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true }),188 ) { backStack ->189 val editId = backStack.arguments?.getString("id")?.takeIf { it.isNotBlank() }190 if (editId == null) {191 EntryEditScreen(192 id = null,193 onBack = back,194 onOpenNote = { noteId ->195 navController.navigate(PeachNav.NotesMode.of(noteId)) {196 popUpTo(PeachNav.EntryEdit.ROUTE) { inclusive = true }197 }198 },199 )200 } else {201 EntryDetailScreen(202 id = editId,203 onNavigate = nav,204 onBack = back,205 )206 }207 }208209 composable(210 route = PeachNav.NotesMode.ROUTE,211 arguments = listOf(navArgument("id") { type = NavType.StringType }),212 ) {213 NotesModeScreen(onBack = back)214 }215 composable(PeachNav.Settings.route) { SettingsScreen(onBack = back, onNavigate = nav) }216 composable(PeachNav.CodexGenerate.route) {217 CodexGenerateScreen(218 onBack = back,219 onSetupRecovery = { nav(PeachNav.RecoverySetup) },220 onUpgradePro = { nav(PeachNav.Settings) },221 )222 }223 composable(PeachNav.CodexRestore.route) {224 CodexRestoreScreen(225 onBack = back,226 onRestored = {227 navController.navigate(PeachNav.VaultList.route) {228 popUpTo(0) { inclusive = true }229 }230 },231 )232 }233 composable(PeachNav.ChirpPair.route) { ChirpPairScreen(onBack = back) }234 composable(PeachNav.CodePair.route) { CodePairScreen(onBack = back) }235 composable(PeachNav.Health.route) {236 VaultHealthScreen(onBack = back, onEditEntry = { nav(PeachNav.EntryDetail(it)) })237 }238 composable(PeachNav.Trash.route) { TrashScreen(onBack = back) }239 composable(PeachNav.Import.route) {240 ImportScreen(241 onBack = back,242 onCodex = { nav(PeachNav.CodexRestore) },243 onVaultCreated = {244 navController.navigate(PeachNav.RecoverySetup.route) {245 popUpTo(0) { inclusive = true }246 }247 },248 )249 }250 composable(PeachNav.Export.route) { ExportScreen(onBack = back) }251 composable(PeachNav.Panic.route) {252 PanicScreen(253 onBack = back,254 onDestroyed = {255 navController.navigate(PeachNav.Lock.route) {256 popUpTo(0) { inclusive = true }257 }258 },259 )260 }261 composable(PeachNav.RecoverySetup.route) {262 RecoveryWordsScreen(263 requireConfirm = true,264 onDone = {265 navController.navigate(PeachNav.VaultList.route) {266 popUpTo(0) { inclusive = true }267 }268 },269 )270 }271 composable(PeachNav.RecoveryView.route) {272 RecoveryWordsScreen(requireConfirm = false, onDone = back)273 }274 composable(PeachNav.RecoveryEntry.route) {275 RecoveryWordEntryScreen(276 onSuccess = {277 navController.navigate(PeachNav.VaultList.route) {278 popUpTo(0) { inclusive = true }279 }280 },281 onBack = back,282 onRestore = { nav(PeachNav.Import) },283 )284 }285 }286 CopyFeedbackOverlay()287 }288}289290internal fun isVaultSheetTransition(fromRoute: String?, toRoute: String?, reverse: Boolean = false): Boolean {291 val fromEntry = isEntrySurfaceRoute(fromRoute)292 val toEntry = isEntrySurfaceRoute(toRoute)293 return if (reverse) {294 fromRoute == PeachNav.VaultList.route && toEntry295 } else {296 fromEntry && toRoute == PeachNav.VaultList.route297 }298}299300internal fun isEntrySurfaceRoute(route: String?): Boolean =301 route?.startsWith("entry/") == true || route?.startsWith("notes/") == true