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 a0b269e4bf
commit 5bd0686ab5
2 changed files with 101 additions and 43 deletions

View File

@ -1,7 +1,10 @@
package cz.bugsy.passwordzebra.ui.navigation package cz.bugsy.passwordzebra.ui.navigation
import androidx.compose.foundation.layout.Box 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.foundation.layout.padding
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -13,6 +16,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource 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
@ -92,7 +96,10 @@ fun AppNavigation() {
} }
}, },
) { innerPadding -> ) { 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) { NavHost(navController = navController, startDestination = ROUTE_WORD_GENERATOR) {
composable(ROUTE_WORD_GENERATOR) { composable(ROUTE_WORD_GENERATOR) {
WordGeneratorScreen() WordGeneratorScreen()

View File

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