diff --git a/app/src/main/java/cz/bugsy/passwordzebra/ui/navigation/AppNavigation.kt b/app/src/main/java/cz/bugsy/passwordzebra/ui/navigation/AppNavigation.kt index a056676..235a93f 100644 --- a/app/src/main/java/cz/bugsy/passwordzebra/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/cz/bugsy/passwordzebra/ui/navigation/AppNavigation.kt @@ -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() diff --git a/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/DeterministicScreen.kt b/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/DeterministicScreen.kt index cae40df..1202a9f 100644 --- a/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/DeterministicScreen.kt +++ b/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/DeterministicScreen.kt @@ -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()) { - Box( - modifier = Modifier - .fillMaxWidth() - .border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(12.dp), - ) - .clickable { viewModel.copyAndScheduleClear(context) } - .padding(16.dp), - ) { + // 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(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 }