Vault screen: remove top bar, inline History/Settings buttons, fix keyboard gap

This commit is contained in:
Pavel Baksy 2026-03-06 07:14:31 +01:00
parent eeb308ec63
commit 546fdaec02
2 changed files with 101 additions and 43 deletions

View File

@ -1,7 +1,10 @@
package cz.bugsy.passwordzebra.ui.navigation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Icon
@ -13,6 +16,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavDestination.Companion.hierarchy
@ -92,7 +96,10 @@ fun AppNavigation() {
}
},
) { innerPadding ->
Box(modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding())) {
val density = LocalDensity.current
val imeVisible = WindowInsets.ime.getBottom(density) > 0
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()

View File

@ -10,17 +10,21 @@ 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
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Password
import androidx.compose.material.icons.filled.LockReset
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
@ -32,14 +36,13 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -48,10 +51,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
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
@ -75,6 +82,8 @@ fun DeterministicScreen(
}
var showRotateConfirmDialog by remember { mutableStateOf(false) }
val masterPasswordFocus = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
if (state.showRotateWarning) {
AlertDialog(
@ -146,44 +155,39 @@ fun DeterministicScreen(
)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.det_screen_title)) },
actions = {
IconButton(onClick = onNavigateToHistory) {
Icon(Icons.Default.History, contentDescription = stringResource(R.string.det_history))
}
IconButton(onClick = onNavigateToSettings) {
Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.det_settings))
}
},
)
},
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.imePadding(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp)
.padding(bottom = 88.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Spacer(modifier = Modifier.height(4.dp))
// Generated password box — shown at top once available
if (state.generatedPassword.isNotEmpty()) {
// Generated password box — always visible, even when empty
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 80.dp)
.border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(12.dp),
)
.clickable { viewModel.copyAndScheduleClear(context) }
.clickable(enabled = state.generatedPassword.isNotEmpty()) {
viewModel.copyAndScheduleClear(context)
}
.padding(16.dp),
contentAlignment = Alignment.Center,
) {
if (state.generatedPassword.isNotEmpty()) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
Text(
text = state.generatedPassword,
@ -214,8 +218,10 @@ fun DeterministicScreen(
}
}
}
}
// Inline rotate button — right-aligned, error color, confirmation dialog
// Inline rotate button — only shown when password is generated
if (state.generatedPassword.isNotEmpty()) {
TextButton(
onClick = { showRotateConfirmDialog = true },
modifier = Modifier.align(Alignment.End),
@ -247,6 +253,13 @@ fun DeterministicScreen(
.fillMaxWidth()
.menuAnchor(),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
onNext = {
expanded = false
masterPasswordFocus.requestFocus()
},
),
)
ExposedDropdownMenu(
expanded = expanded && filteredHistory.isNotEmpty(),
@ -268,11 +281,22 @@ fun DeterministicScreen(
value = String(state.masterPassword),
onValueChange = { viewModel.updateMasterPassword(it.toCharArray()) },
label = { Text(stringResource(R.string.det_master_password)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(masterPasswordFocus),
singleLine = true,
visualTransformation = if (state.masterPasswordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = {
keyboardController?.hide()
viewModel.generate()
},
),
trailingIcon = {
IconButton(onClick = { viewModel.toggleMasterPasswordVisible() }) {
Icon(
@ -284,21 +308,48 @@ fun DeterministicScreen(
},
)
// Square icon generate button
Spacer(modifier = Modifier.height(4.dp))
} // end Column
// Button row: History | Generate | Settings — floats just above keyboard
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
FilledTonalIconButton(
onClick = onNavigateToHistory,
modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(12.dp),
) {
Icon(Icons.Default.History, contentDescription = stringResource(R.string.det_history))
}
Spacer(Modifier.width(20.dp))
FilledIconButton(
onClick = { viewModel.generate() },
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(60.dp),
modifier = Modifier.size(60.dp),
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Filled.Password,
imageVector = Icons.Filled.LockReset,
contentDescription = stringResource(R.string.det_generate),
)
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(Modifier.width(20.dp))
FilledTonalIconButton(
onClick = onNavigateToSettings,
modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(12.dp),
) {
Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.det_settings))
}
}
} // end outer Box
}