Vault screen: remove top bar, inline History/Settings buttons, fix keyboard gap
This commit is contained in:
parent
a0b269e4bf
commit
5bd0686ab5
@ -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()
|
||||||
|
|||||||
@ -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()) {
|
||||||
.padding(16.dp),
|
viewModel.copyAndScheduleClear(context)
|
||||||
) {
|
}
|
||||||
|
.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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user