Add directional nav animations; back on Vault closes app
This commit is contained in:
parent
7720cb3e2a
commit
47dda9e910
@ -21,6 +21,11 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
@ -40,6 +45,7 @@ private const val ROUTE_EXPORT_IMPORT = "export_import"
|
||||
private const val ROUTE_SETTINGS = "settings"
|
||||
|
||||
private val bottomBarRoutes = setOf(ROUTE_WORD_GENERATOR, ROUTE_DETERMINISTIC)
|
||||
private val tabRouteOrder = listOf(ROUTE_WORD_GENERATOR, ROUTE_DETERMINISTIC)
|
||||
|
||||
@Composable
|
||||
fun AppNavigation() {
|
||||
@ -101,10 +107,51 @@ fun AppNavigation() {
|
||||
val bottomPadding = if (imeVisible) 0.dp else innerPadding.calculateBottomPadding()
|
||||
Box(modifier = Modifier.padding(bottom = bottomPadding)) {
|
||||
NavHost(navController = navController, startDestination = ROUTE_WORD_GENERATOR) {
|
||||
composable(ROUTE_WORD_GENERATOR) {
|
||||
WordGeneratorScreen()
|
||||
composable(
|
||||
ROUTE_WORD_GENERATOR,
|
||||
enterTransition = {
|
||||
val fromIdx = tabRouteOrder.indexOf(initialState.destination.route)
|
||||
val toIdx = tabRouteOrder.indexOf(ROUTE_WORD_GENERATOR)
|
||||
if (fromIdx != -1) slideInHorizontally(tween(200)) { if (toIdx > fromIdx) it else -it }
|
||||
else fadeIn(tween(200))
|
||||
},
|
||||
exitTransition = {
|
||||
val toIdx = tabRouteOrder.indexOf(targetState.destination.route)
|
||||
if (toIdx != -1) slideOutHorizontally(tween(200)) { if (toIdx > 0) -it else it }
|
||||
else fadeOut(tween(150))
|
||||
},
|
||||
popEnterTransition = {
|
||||
val fromIdx = tabRouteOrder.indexOf(initialState.destination.route)
|
||||
val toIdx = tabRouteOrder.indexOf(ROUTE_WORD_GENERATOR)
|
||||
if (fromIdx != -1) slideInHorizontally(tween(200)) { if (toIdx > fromIdx) it else -it }
|
||||
else fadeIn(tween(200))
|
||||
},
|
||||
popExitTransition = { fadeOut(tween(150)) },
|
||||
) {
|
||||
WordGeneratorScreen(
|
||||
onNavigateToSettings = { navController.navigate(ROUTE_SETTINGS) },
|
||||
)
|
||||
}
|
||||
composable(ROUTE_DETERMINISTIC) {
|
||||
composable(
|
||||
ROUTE_DETERMINISTIC,
|
||||
enterTransition = {
|
||||
val fromIdx = tabRouteOrder.indexOf(initialState.destination.route)
|
||||
val toIdx = tabRouteOrder.indexOf(ROUTE_DETERMINISTIC)
|
||||
if (fromIdx != -1) slideInHorizontally(tween(200)) { if (toIdx > fromIdx) it else -it }
|
||||
else fadeIn(tween(200))
|
||||
},
|
||||
exitTransition = {
|
||||
val toIdx = tabRouteOrder.indexOf(targetState.destination.route)
|
||||
if (toIdx != -1) slideOutHorizontally(tween(200)) { if (toIdx > 1) -it else it }
|
||||
else fadeOut(tween(150))
|
||||
},
|
||||
popEnterTransition = { fadeIn(tween(200)) },
|
||||
popExitTransition = {
|
||||
val toIdx = tabRouteOrder.indexOf(targetState.destination.route)
|
||||
if (toIdx != -1) slideOutHorizontally(tween(200)) { if (toIdx > 1) -it else it }
|
||||
else fadeOut(tween(150))
|
||||
},
|
||||
) {
|
||||
DeterministicScreen(
|
||||
viewModel = detViewModel,
|
||||
onNavigateToHistory = { navController.navigate(ROUTE_SERVICE_HISTORY) },
|
||||
@ -112,21 +159,39 @@ fun AppNavigation() {
|
||||
onNavigateToExport = { navController.navigate(ROUTE_EXPORT_IMPORT) },
|
||||
)
|
||||
}
|
||||
composable(ROUTE_SERVICE_HISTORY) {
|
||||
composable(
|
||||
ROUTE_SERVICE_HISTORY,
|
||||
enterTransition = { slideInHorizontally(tween(300)) { it } },
|
||||
exitTransition = { fadeOut(tween(200)) },
|
||||
popEnterTransition = { fadeIn(tween(200)) },
|
||||
popExitTransition = { slideOutHorizontally(tween(300)) { it } },
|
||||
) {
|
||||
ServiceHistoryScreen(
|
||||
viewModel = detViewModel,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onServiceSelected = { _ -> navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
composable(ROUTE_SETTINGS) {
|
||||
composable(
|
||||
ROUTE_SETTINGS,
|
||||
enterTransition = { slideInHorizontally(tween(300)) { it } },
|
||||
exitTransition = { fadeOut(tween(200)) },
|
||||
popEnterTransition = { fadeIn(tween(200)) },
|
||||
popExitTransition = { slideOutHorizontally(tween(300)) { it } },
|
||||
) {
|
||||
SettingsScreen(
|
||||
viewModel = detViewModel,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToExportImport = { navController.navigate(ROUTE_EXPORT_IMPORT) },
|
||||
)
|
||||
}
|
||||
composable(ROUTE_EXPORT_IMPORT) {
|
||||
composable(
|
||||
ROUTE_EXPORT_IMPORT,
|
||||
enterTransition = { slideInHorizontally(tween(300)) { it } },
|
||||
exitTransition = { fadeOut(tween(200)) },
|
||||
popEnterTransition = { fadeIn(tween(200)) },
|
||||
popExitTransition = { slideOutHorizontally(tween(300)) { it } },
|
||||
) {
|
||||
ExportImportScreen(
|
||||
viewModel = detViewModel,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
||||
@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@ -43,6 +43,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -62,6 +63,7 @@ import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cz.bugsy.passwordzebra.R
|
||||
@ -78,6 +80,7 @@ fun DeterministicScreen(
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
BackHandler { (context as? android.app.Activity)?.finish() }
|
||||
if (state.isLocked) {
|
||||
LockScreen(viewModel = viewModel)
|
||||
return
|
||||
@ -171,13 +174,13 @@ fun DeterministicScreen(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Generated password box — always visible, even when empty
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 80.dp)
|
||||
.height(150.dp)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@ -211,13 +214,6 @@ fun DeterministicScreen(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.det_tap_to_copy),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,7 +224,7 @@ fun DeterministicScreen(
|
||||
TextButton(
|
||||
onClick = { showRotateConfirmDialog = true },
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = Color(0xFFFFD600)),
|
||||
) {
|
||||
Icon(Icons.Default.Warning, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
@ -239,13 +235,15 @@ fun DeterministicScreen(
|
||||
// Service name with autocomplete from history
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val filteredHistory = remember(state.serviceHistory, state.serviceName) {
|
||||
state.serviceHistory.filter {
|
||||
state.serviceName.isBlank() || it.contains(state.serviceName, ignoreCase = true)
|
||||
if (state.serviceName.length < 3) emptyList()
|
||||
else state.serviceHistory.filter {
|
||||
it.startsWith(state.serviceName, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
val showDropdown = expanded && filteredHistory.size >= 2
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded && filteredHistory.isNotEmpty(),
|
||||
expanded = showDropdown,
|
||||
onExpandedChange = { expanded = it },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
@ -265,7 +263,7 @@ fun DeterministicScreen(
|
||||
),
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded && filteredHistory.isNotEmpty(),
|
||||
expanded = showDropdown,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
filteredHistory.forEach { name ->
|
||||
@ -329,7 +327,7 @@ fun DeterministicScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
.padding(start = 16.dp, top = 14.dp, end = 16.dp, bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@ -344,7 +342,7 @@ fun DeterministicScreen(
|
||||
Spacer(Modifier.width(20.dp))
|
||||
|
||||
FilledIconButton(
|
||||
onClick = { viewModel.generate() },
|
||||
onClick = { keyboardController?.hide(); viewModel.generate() },
|
||||
modifier = Modifier.size(60.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user