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.hilt.navigation.compose.hiltViewModel17import androidx.lifecycle.compose.collectAsStateWithLifecycle18import androidx.navigation.NavHostController19import androidx.navigation.NavType20import androidx.navigation.compose.NavHost21import androidx.navigation.compose.composable22import androidx.navigation.compose.rememberNavController23import androidx.navigation.navArgument24import com.peachpasswords.ui.codex.CodexGenerateScreen25import com.peachpasswords.ui.codex.CodexRestoreScreen26import com.peachpasswords.ui.components.CopyFeedbackOverlay27import com.peachpasswords.ui.lock.LockScreen28import com.peachpasswords.ui.onboarding.OnboardingScreen29import 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.updates.UpdateScreen36import com.peachpasswords.ui.updates.UpdateViewModel37import com.peachpasswords.ui.updates.WhatsNewScreen38import com.peachpasswords.ui.vault.EntryDetailScreen39import com.peachpasswords.ui.vault.EntryEditScreen40import com.peachpasswords.ui.vault.ExportScreen41import com.peachpasswords.ui.vault.ImportScreen42import com.peachpasswords.ui.vault.NotesModeScreen43import com.peachpasswords.ui.vault.RecoveryWordsScreen44import com.peachpasswords.ui.vault.TrashScreen45import com.peachpasswords.ui.vault.VaultListScreen4647@Composable48fun PeachAppNavHost(49 navController: NavHostController = rememberNavController(),50 startDestination: PeachNav = PeachNav.Lock,51 isUnlocked: kotlinx.coroutines.flow.StateFlow<Boolean>? = null,52) {53 val nav: (PeachNav) -> Unit = { navController.navigate(it.route) }54 val back: () -> Unit = { navController.popBackStack() }5556 57 58 59 60 val unlocked = if (isUnlocked == null) true else isUnlocked.collectAsStateWithLifecycle().value6162 63 64 val updateVm: UpdateViewModel = hiltViewModel()65 val openUpdateRequested by updateVm.openRequested.collectAsStateWithLifecycle()66 LaunchedEffect(openUpdateRequested) {67 if (openUpdateRequested) {68 updateVm.consumeOpenRequest()69 navController.navigate(PeachNav.Update.route) { launchSingleTop = true }70 }71 }72 val whatsNewNotes by updateVm.whatsNew.collectAsStateWithLifecycle()73 74 75 LaunchedEffect(whatsNewNotes, unlocked) {76 if (whatsNewNotes != null && unlocked &&77 navController.currentDestination?.route != PeachNav.WhatsNew.route78 ) {79 navController.navigate(PeachNav.WhatsNew.route) { launchSingleTop = true }80 }81 }8283 if (isUnlocked != null) {84 LaunchedEffect(unlocked) {85 if (!unlocked) {86 val current = navController.currentDestination?.route87 if (current != null &&88 current != PeachNav.Lock.route &&89 current != PeachNav.Onboarding.route90 ) {91 navController.navigate(PeachNav.Lock.route) {92 popUpTo(0) { inclusive = true }93 launchSingleTop = true94 }95 }96 }97 }98 }99100 Box(Modifier.fillMaxSize()) {101 NavHost(102 navController = navController,103 startDestination = startDestination.route,104 enterTransition = {105 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {106 slideInVertically(107 initialOffsetY = { it },108 animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),109 ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))110 } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {111 EnterTransition.None112 } else {113 slideInVertically(114 initialOffsetY = { (it * 0.05f).toInt() },115 animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),116 ) + fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))117 }118 },119 exitTransition = {120 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {121 ExitTransition.None122 } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {123 slideOutVertically(124 targetOffsetY = { it },125 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),126 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))127 } else {128 slideOutVertically(129 targetOffsetY = { -(it * 0.03f).toInt() },130 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),131 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))132 }133 },134 popEnterTransition = {135 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {136 EnterTransition.None137 } else {138 fadeIn(tween(PeachMotion.Enter, easing = PeachMotion.Spring))139 }140 },141 popExitTransition = {142 if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {143 slideOutVertically(144 targetOffsetY = { it },145 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),146 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))147 } else {148 slideOutVertically(149 targetOffsetY = { (it * 0.05f).toInt() },150 animationSpec = tween(PeachMotion.Exit, easing = PeachMotion.Retreat),151 ) + fadeOut(tween(PeachMotion.Exit, easing = PeachMotion.Retreat))152 }153 },154 ) {155 composable(PeachNav.Onboarding.route) {156 OnboardingScreen(157 onFinished = {158 navController.navigate(PeachNav.Lock.route)159 },160 onRestore = { nav(PeachNav.Import) },161 )162 }163 composable(PeachNav.Lock.route) {164 LockScreen(165 onUnlocked = { dest ->166 navController.navigate(dest.route) {167 popUpTo(0) { inclusive = true }168 }169 },170 onNavigate = nav,171 )172 }173 composable(PeachNav.VaultList.route) {174 175 176 177 val hasOrigin = navController.previousBackStackEntry != null178 VaultListScreen(179 onNavigate = nav,180 onBack = if (hasOrigin) ({ navController.popBackStack() }) else null,181 )182 }183 composable(PeachNav.VaultNotes.route) {184 val hasOrigin = navController.previousBackStackEntry != null185 VaultListScreen(186 onNavigate = nav,187 onBack = if (hasOrigin) ({ navController.popBackStack() }) else null,188 startNoteOnly = true,189 )190 }191192 composable(193 route = PeachNav.EntryDetail.ROUTE,194 arguments = listOf(navArgument("id") { type = NavType.StringType }),195 ) { backStack ->196 197 198 199 200 201 202 203 val isRoot = navController.previousBackStackEntry == null204 EntryDetailScreen(205 id = backStack.arguments?.getString("id").orEmpty(),206 onNavigate = nav,207 onBack = if (isRoot) null else ({ navController.navigate(PeachNav.VaultList.route) }),208 )209 }210 composable(211 route = PeachNav.EntryEdit.ROUTE,212 arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true }),213 ) { backStack ->214 val editId = backStack.arguments?.getString("id")?.takeIf { it.isNotBlank() }215 if (editId == null) {216 EntryEditScreen(217 id = null,218 onBack = back,219 onOpenNote = { noteId ->220 navController.navigate(PeachNav.NotesMode.of(noteId)) {221 popUpTo(PeachNav.EntryEdit.ROUTE) { inclusive = true }222 }223 },224 )225 } else {226 EntryDetailScreen(227 id = editId,228 onNavigate = nav,229 onBack = back,230 )231 }232 }233234 composable(235 route = PeachNav.NotesMode.ROUTE,236 arguments = listOf(navArgument("id") { type = NavType.StringType }),237 ) {238 NotesModeScreen(onBack = back)239 }240 composable(PeachNav.Settings.route) { SettingsScreen(onBack = back, onNavigate = nav) }241 composable(PeachNav.CodexGenerate.route) {242 CodexGenerateScreen(243 onBack = back,244 onSetupRecovery = { nav(PeachNav.RecoverySetup) },245 onUpgradePro = { nav(PeachNav.Settings) },246 )247 }248 composable(PeachNav.CodexRestore.route) {249 CodexRestoreScreen(250 onBack = back,251 onRestored = {252 navController.navigate(PeachNav.VaultList.route) {253 popUpTo(0) { inclusive = true }254 }255 },256 )257 }258 composable(PeachNav.CodePair.route) { CodePairScreen(onBack = back) }259 composable(PeachNav.Health.route) {260 VaultHealthScreen(onBack = back, onEditEntry = { nav(PeachNav.EntryDetail(it)) })261 }262 composable(PeachNav.Trash.route) { TrashScreen(onBack = back) }263 composable(PeachNav.Import.route) {264 ImportScreen(265 onBack = back,266 onCodex = { nav(PeachNav.CodexRestore) },267 onVaultCreated = {268 navController.navigate(PeachNav.RecoverySetup.route) {269 popUpTo(0) { inclusive = true }270 }271 },272 )273 }274 composable(PeachNav.Export.route) { ExportScreen(onBack = back) }275 composable(PeachNav.Panic.route) {276 PanicScreen(277 onBack = back,278 onDestroyed = {279 navController.navigate(PeachNav.Lock.route) {280 popUpTo(0) { inclusive = true }281 }282 },283 )284 }285 composable(PeachNav.RecoverySetup.route) {286 RecoveryWordsScreen(287 requireConfirm = true,288 onDone = {289 navController.navigate(PeachNav.VaultList.route) {290 popUpTo(0) { inclusive = true }291 }292 },293 )294 }295 composable(PeachNav.RecoveryView.route) {296 RecoveryWordsScreen(requireConfirm = false, onDone = back)297 }298 composable(PeachNav.Update.route) { UpdateScreen(onBack = back) }299 composable(PeachNav.WhatsNew.route) { WhatsNewScreen(onDone = back) }300 composable(PeachNav.RecoveryEntry.route) {301 RecoveryWordEntryScreen(302 onSuccess = {303 navController.navigate(PeachNav.VaultList.route) {304 popUpTo(0) { inclusive = true }305 }306 },307 onBack = back,308 onRestore = { nav(PeachNav.Import) },309 )310 }311 }312 CopyFeedbackOverlay()313 }314}315316internal fun isVaultSheetTransition(fromRoute: String?, toRoute: String?, reverse: Boolean = false): Boolean {317 val fromEntry = isEntrySurfaceRoute(fromRoute)318 val toEntry = isEntrySurfaceRoute(toRoute)319 return if (reverse) {320 fromRoute == PeachNav.VaultList.route && toEntry321 } else {322 fromEntry && toRoute == PeachNav.VaultList.route323 }324}325326internal fun isEntrySurfaceRoute(route: String?): Boolean =327 route?.startsWith("entry/") == true || route?.startsWith("notes/") == true