@@ -4,6 +4,7 @@ import android.app.Application
44import android.content.Context
55import android.content.Intent
66import android.net.Uri
7+ import android.os.Build
78import android.os.Bundle
89import android.util.Log
910import android.view.Gravity
@@ -12,6 +13,9 @@ import android.view.ViewGroup
1213import android.widget.TextView
1314import android.widget.Toast
1415import androidx.activity.ComponentActivity
16+ import androidx.activity.BackEventCompat
17+ import androidx.activity.compose.BackHandler
18+ import androidx.activity.compose.PredictiveBackHandler
1519import androidx.activity.compose.setContent
1620import androidx.activity.result.ActivityResultLauncher
1721import androidx.activity.result.contract.ActivityResultContracts
@@ -29,15 +33,19 @@ import androidx.compose.runtime.Composable
2933import androidx.compose.runtime.DisposableEffect
3034import androidx.compose.runtime.LaunchedEffect
3135import androidx.compose.runtime.getValue
36+ import androidx.compose.runtime.mutableFloatStateOf
3237import androidx.compose.runtime.mutableStateOf
3338import androidx.compose.runtime.remember
3439import androidx.compose.runtime.rememberUpdatedState
3540import androidx.compose.runtime.rememberCoroutineScope
3641import androidx.compose.runtime.setValue
3742import androidx.compose.ui.Alignment
3843import androidx.compose.ui.Modifier
44+ import androidx.compose.ui.graphics.TransformOrigin
45+ import androidx.compose.ui.graphics.graphicsLayer
3946import androidx.compose.ui.platform.LocalContext
4047import androidx.compose.ui.platform.LocalView
48+ import androidx.compose.ui.unit.dp
4149import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
4250import androidx.core.view.WindowCompat
4351import androidx.fragment.app.FragmentActivity
@@ -53,10 +61,12 @@ import androidx.navigation.compose.composable
5361import androidx.navigation.compose.rememberNavController
5462import androidx.navigation.navArgument
5563import kotlinx.coroutines.flow.first
64+ import kotlinx.coroutines.flow.collect
5665import kotlinx.coroutines.flow.map
5766import kotlinx.coroutines.flow.distinctUntilChanged
5867import kotlinx.coroutines.Dispatchers
5968import androidx.lifecycle.lifecycleScope
69+ import kotlinx.coroutines.CancellationException
6070import kotlinx.coroutines.delay
6171import kotlinx.coroutines.launch
6272import kotlinx.coroutines.withContext
@@ -657,6 +667,9 @@ fun MonicaContent(
657667 onNavigateToExtensions = {
658668 navController.navigate(Screen .Extensions .route)
659669 },
670+ onNavigateToCommonAccountTemplates = {
671+ navController.navigate(Screen .CommonAccountTemplates .route)
672+ },
660673 onNavigateToPageCustomization = {
661674 navController.navigate(Screen .PageAdjustmentCustomization .route)
662675 },
@@ -727,16 +740,28 @@ fun MonicaContent(
727740 popExitTransition = { rightSlidePopExitTransition() }
728741 ) { backStackEntry ->
729742 val passwordId = backStackEntry.arguments?.getString(" passwordId" )?.toLongOrNull() ? : - 1L
730- AddEditPasswordScreen (
731- viewModel = viewModel,
732- totpViewModel = totpViewModel,
733- bankCardViewModel = bankCardViewModel,
734- localKeePassViewModel = localKeePassViewModel,
735- passwordId = if (passwordId == - 1L ) null else passwordId,
736- onNavigateBack = {
737- navController.popBackStack()
743+ val navigateBackFromAddEditPassword = {
744+ val popped = navController.popBackStack()
745+ if (! popped) {
746+ navController.navigate(Screen .Main .createRoute()) {
747+ popUpTo(0 ) { inclusive = true }
748+ launchSingleTop = true
749+ }
738750 }
739- )
751+ }
752+ PredictiveBackPageContainer (
753+ enabled = settings.predictiveBackForPageNavigationEnabled,
754+ onNavigateBack = navigateBackFromAddEditPassword
755+ ) {
756+ AddEditPasswordScreen (
757+ viewModel = viewModel,
758+ totpViewModel = totpViewModel,
759+ bankCardViewModel = bankCardViewModel,
760+ localKeePassViewModel = localKeePassViewModel,
761+ passwordId = if (passwordId == - 1L ) null else passwordId,
762+ onNavigateBack = navigateBackFromAddEditPassword
763+ )
764+ }
740765 }
741766
742767 composable(
@@ -1017,33 +1042,39 @@ fun MonicaContent(
10171042 val passwordId = backStackEntry.arguments?.getString(" passwordId" )?.toLongOrNull() ? : - 1L
10181043
10191044 if (passwordId > 0 ) {
1045+ val navigateBackFromPasswordDetail = {
1046+ val popped = navController.popBackStack()
1047+ if (! popped) {
1048+ navController.navigate(Screen .Main .createRoute()) {
1049+ popUpTo(0 ) { inclusive = true }
1050+ launchSingleTop = true
1051+ }
1052+ }
1053+ }
10201054 androidx.compose.runtime.CompositionLocalProvider (
10211055 takagi.ru.monica.ui.LocalAnimatedVisibilityScope provides this
10221056 ) {
1023- takagi.ru.monica.ui.screens.PasswordDetailScreen (
1024- viewModel = viewModel,
1025- passkeyViewModel = passkeyViewModel,
1026- passwordId = passwordId,
1027- disablePasswordVerification = settings.disablePasswordVerification,
1028- biometricEnabled = settings.biometricEnabled,
1029- iconCardsEnabled = settings.iconCardsEnabled && settings.passwordPageIconEnabled,
1030- unmatchedIconHandlingStrategy = settings.unmatchedIconHandlingStrategy,
1031- enableSharedBounds = false ,
1032- onNavigateBack = {
1033- val popped = navController.popBackStack()
1034- if (! popped) {
1035- navController.navigate(Screen .Main .createRoute()) {
1036- popUpTo(0 ) { inclusive = true }
1057+ PredictiveBackPageContainer (
1058+ enabled = settings.predictiveBackForPageNavigationEnabled,
1059+ onNavigateBack = navigateBackFromPasswordDetail
1060+ ) {
1061+ takagi.ru.monica.ui.screens.PasswordDetailScreen (
1062+ viewModel = viewModel,
1063+ passkeyViewModel = passkeyViewModel,
1064+ passwordId = passwordId,
1065+ disablePasswordVerification = settings.disablePasswordVerification,
1066+ biometricEnabled = settings.biometricEnabled,
1067+ iconCardsEnabled = settings.iconCardsEnabled && settings.passwordPageIconEnabled,
1068+ unmatchedIconHandlingStrategy = settings.unmatchedIconHandlingStrategy,
1069+ enableSharedBounds = false ,
1070+ onNavigateBack = navigateBackFromPasswordDetail,
1071+ onEditPassword = { id ->
1072+ navController.navigate(Screen .AddEditPassword .createRoute(id)) {
10371073 launchSingleTop = true
10381074 }
10391075 }
1040- },
1041- onEditPassword = { id ->
1042- navController.navigate(Screen .AddEditPassword .createRoute(id)) {
1043- launchSingleTop = true
1044- }
1045- }
1046- )
1076+ )
1077+ }
10471078 }
10481079 }
10491080 }
@@ -1771,6 +1802,24 @@ fun MonicaContent(
17711802 }
17721803 }
17731804
1805+ composable(
1806+ route = Screen .CommonAccountTemplates .route,
1807+ enterTransition = { rightSlideEnterTransition() },
1808+ exitTransition = { ExitTransition .None },
1809+ popEnterTransition = { EnterTransition .None },
1810+ popExitTransition = { rightSlidePopExitTransition() }
1811+ ) {
1812+ androidx.compose.runtime.CompositionLocalProvider (
1813+ takagi.ru.monica.ui.LocalAnimatedVisibilityScope provides this
1814+ ) {
1815+ takagi.ru.monica.ui.screens.CommonAccountTemplatesScreen (
1816+ onNavigateBack = {
1817+ navController.popBackStack()
1818+ }
1819+ )
1820+ }
1821+ }
1822+
17741823 composable(
17751824 route = Screen .PageAdjustmentCustomization .route,
17761825 enterTransition = { rightSlideEnterTransition() },
@@ -2125,6 +2174,56 @@ fun MonicaContent(
21252174 }
21262175}
21272176
2177+ @Composable
2178+ private fun PredictiveBackPageContainer (
2179+ enabled : Boolean ,
2180+ onNavigateBack : () -> Unit ,
2181+ content : @Composable () -> Unit
2182+ ) {
2183+ var progress by remember { mutableFloatStateOf(0f ) }
2184+ var swipeEdge by remember { mutableStateOf(BackEventCompat .EDGE_LEFT ) }
2185+
2186+ PredictiveBackHandler (enabled = enabled && Build .VERSION .SDK_INT >= Build .VERSION_CODES .UPSIDE_DOWN_CAKE ) { events ->
2187+ try {
2188+ events.collect { event ->
2189+ swipeEdge = event.swipeEdge
2190+ progress = event.progress.coerceIn(0f , 1f )
2191+ }
2192+ onNavigateBack()
2193+ } catch (_: CancellationException ) {
2194+ // Gesture canceled: just reset visual state.
2195+ } finally {
2196+ progress = 0f
2197+ }
2198+ }
2199+
2200+ BackHandler (enabled = enabled && Build .VERSION .SDK_INT < Build .VERSION_CODES .UPSIDE_DOWN_CAKE ) {
2201+ onNavigateBack()
2202+ }
2203+
2204+ val edgeDirection = if (swipeEdge == BackEventCompat .EDGE_RIGHT ) - 1f else 1f
2205+ val scale = 1f - (0.08f * progress)
2206+ val alpha = 1f - (0.08f * progress)
2207+
2208+ Box (
2209+ modifier = Modifier
2210+ .fillMaxSize()
2211+ .graphicsLayer {
2212+ val translationPx = 24 .dp.toPx() * progress * edgeDirection
2213+ translationX = translationPx
2214+ scaleX = scale
2215+ scaleY = scale
2216+ this .alpha = alpha
2217+ transformOrigin = TransformOrigin (
2218+ pivotFractionX = if (edgeDirection > 0f ) 0f else 1f ,
2219+ pivotFractionY = 0.5f
2220+ )
2221+ }
2222+ ) {
2223+ content()
2224+ }
2225+ }
2226+
21282227private fun rightSlideEnterTransition () = slideInHorizontally(
21292228 initialOffsetX = { fullWidth -> fullWidth },
21302229 animationSpec = tween(220 )
0 commit comments