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