From 0de252c12454fc48d325a60f0bc747790200b6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Karpi=C5=84ski?= Date: Sun, 9 Jun 2024 14:25:46 +0200 Subject: [PATCH] Use bottom sheet for password generation --- .../android/ui/components/CipherEditFields.kt | 257 ++++++++++++++++-- .../android/ui/screens/vault/Navigation.kt | 12 +- .../ui/screens/vault/PasswordGenerator.kt | 220 --------------- 3 files changed, 240 insertions(+), 249 deletions(-) delete mode 100644 app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/PasswordGenerator.kt diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/components/CipherEditFields.kt b/app/src/main/java/dev/medzik/librepass/android/ui/components/CipherEditFields.kt index f697bb5c..5387504b 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/components/CipherEditFields.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/components/CipherEditFields.kt @@ -1,31 +1,64 @@ package dev.medzik.librepass.android.ui.components import android.util.Log +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.AutoAwesome +import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.EuroSymbol +import androidx.compose.material.icons.filled.Numbers +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.filled.Title import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.google.gson.Gson +import dev.medzik.android.components.TextFieldValue +import dev.medzik.android.components.colorizePasswordTransformation import dev.medzik.android.components.rememberMutable +import dev.medzik.android.components.rememberMutableString +import dev.medzik.android.components.ui.BaseBottomSheet import dev.medzik.android.components.ui.GroupTitle +import dev.medzik.android.components.ui.SwitcherPreference +import dev.medzik.android.components.ui.rememberBottomSheetState +import dev.medzik.android.components.ui.textfield.AnimatedTextField +import dev.medzik.android.utils.runOnIOThread import dev.medzik.librepass.android.R +import dev.medzik.librepass.android.database.datastore.PasswordGeneratorPreference +import dev.medzik.librepass.android.database.datastore.readPasswordGeneratorPreference +import dev.medzik.librepass.android.database.datastore.writePasswordGeneratorPreference import dev.medzik.librepass.android.ui.screens.vault.OtpConfigure -import dev.medzik.librepass.android.ui.screens.vault.PasswordGenerator import dev.medzik.librepass.types.cipher.Cipher import dev.medzik.librepass.types.cipher.data.CipherLoginData +import kotlinx.coroutines.launch +import java.util.Random + +enum class PasswordType(val literals: String) { + NUMERIC("1234567890"), + LOWERCASE("abcdefghijklmnopqrstuvwxyz"), + UPPERCASE("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), + SYMBOLS("!@#\$%^&*()_+-=[]{}\\|;:'\",.<>/?") +} @Composable fun CipherEditFieldsLogin( @@ -33,16 +66,10 @@ fun CipherEditFieldsLogin( cipher: Cipher, button: @Composable (cipher: Cipher) -> Unit ) { + val scope = rememberCoroutineScope() + var cipherData by rememberMutable(cipher.loginData!!) - // observe username and password from navController - // used to get password from password generator - navController - .currentBackStackEntry - ?.savedStateHandle - ?.getLiveData("password")?.observeForever { - cipherData = cipherData.copy(password = it) - } // observe otp uri from navController // used to get otp uri from otp configuration screen navController @@ -95,6 +122,8 @@ fun CipherEditFieldsLogin( onValueChange = { cipherData = cipherData.copy(username = it) } ) + val passwordGeneratorSheetState = rememberBottomSheetState() + TextInputFieldBase( label = stringResource(R.string.Password), modifier = Modifier @@ -104,15 +133,9 @@ fun CipherEditFieldsLogin( onValueChange = { cipherData = cipherData.copy(password = it) }, hidden = true ) { - IconButton(onClick = { - // save cipher data as json to navController - navController.currentBackStackEntry?.savedStateHandle?.set( - "cipher", - Gson().toJson(cipherData) - ) - - navController.navigate(PasswordGenerator) - }) { + IconButton( + onClick = { passwordGeneratorSheetState.show() } + ) { Icon( imageVector = Icons.Default.AutoAwesome, contentDescription = null @@ -120,6 +143,204 @@ fun CipherEditFieldsLogin( } } + @Composable + fun PasswordGeneratorSheetContent(onSubmit: (String) -> Unit) { + val context = LocalContext.current + val clipboardManager = LocalClipboardManager.current + + var generatedPassword by rememberMutableString() + + var passwordGeneratorPreference by rememberMutable(PasswordGeneratorPreference()) + LaunchedEffect(Unit) { + passwordGeneratorPreference = readPasswordGeneratorPreference(context) + } + + fun generatePassword(): String { + var letters = PasswordType.LOWERCASE.literals + + if (passwordGeneratorPreference.capitalize) { + letters += PasswordType.UPPERCASE.literals + } + + if (passwordGeneratorPreference.includeNumbers) { + letters += PasswordType.NUMERIC.literals + } + + if (passwordGeneratorPreference.includeSymbols) { + letters += PasswordType.SYMBOLS.literals + } + + return (1..passwordGeneratorPreference.length) + .map { Random().nextInt(letters.length) } + .map(letters::get) + .joinToString("") + } + + // regenerate on options change + LaunchedEffect(passwordGeneratorPreference) { + generatedPassword = generatePassword() + } + + AnimatedTextField( + modifier = Modifier.padding(horizontal = 8.dp), + value = TextFieldValue( + value = generatedPassword, +// editable = false + ), + readOnly = true, + visualTransformation = colorizePasswordTransformation(), + trailing = { + Row { + IconButton( + onClick = { clipboardManager.setText(AnnotatedString(generatedPassword)) } + ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = null + ) + } + + IconButton( + onClick = { generatedPassword = generatePassword() } + ) { + Icon( + imageVector = Icons.Default.AutoAwesome, + contentDescription = null + ) + } + } + } + ) + + Spacer( + modifier = Modifier.padding(top = 8.dp) + ) + + AnimatedTextField( + modifier = Modifier.padding(horizontal = 8.dp), + value = TextFieldValue( + value = passwordGeneratorPreference.length.toString(), + onChange = { + try { + if (it.length in 1..3 && it.toInt() <= 256) { + passwordGeneratorPreference = passwordGeneratorPreference.copy(length = it.toInt()) + runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } + } + } catch (e: NumberFormatException) { + // ignore, just do not update input value + } + } + ), + label = stringResource(R.string.PasswordGenerator_Length), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + trailing = { + IconButton( + onClick = { + if (passwordGeneratorPreference.length > 1) { + passwordGeneratorPreference = passwordGeneratorPreference.copy( + length = passwordGeneratorPreference.length - 1 + ) + runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } + } + } + ) { + Icon( + imageVector = Icons.Default.Remove, + contentDescription = null + ) + } + + IconButton( + onClick = { + if (passwordGeneratorPreference.length < 256) { + passwordGeneratorPreference = passwordGeneratorPreference.copy( + length = passwordGeneratorPreference.length + 1 + ) + runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } + } + } + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null + ) + } + } + ) + + // Capital letters switch + SwitcherPreference( + title = stringResource(R.string.PasswordGenerator_CapitalLetters), + checked = passwordGeneratorPreference.capitalize, + onCheckedChange = { + passwordGeneratorPreference = passwordGeneratorPreference.copy(capitalize = it) + runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } + }, + icon = { + Icon( + imageVector = Icons.Default.Title, + contentDescription = null + ) + } + ) + + // Numeric switch + SwitcherPreference( + title = stringResource(R.string.PasswordGenerator_Numbers), + checked = passwordGeneratorPreference.includeNumbers, + onCheckedChange = { + passwordGeneratorPreference = passwordGeneratorPreference.copy(includeNumbers = it) + runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } + }, + icon = { + Icon( + imageVector = Icons.Default.Numbers, + contentDescription = null + ) + } + ) + + // Symbols switch + SwitcherPreference( + title = stringResource(R.string.PasswordGenerator_Symbols), + checked = passwordGeneratorPreference.includeSymbols, + onCheckedChange = { + passwordGeneratorPreference = passwordGeneratorPreference.copy(includeSymbols = it) + runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } + }, + icon = { + Icon( + imageVector = Icons.Default.EuroSymbol, + contentDescription = null + ) + } + ) + + Button( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 90.dp) + .padding(top = 16.dp, bottom = 8.dp), + onClick = { onSubmit(generatedPassword) } + ) { + Text(stringResource(R.string.Submit)) + } + } + + BaseBottomSheet(state = passwordGeneratorSheetState) { + PasswordGeneratorSheetContent( + onSubmit = { + cipherData = cipherData.copy(password = it) + + scope.launch { + passwordGeneratorSheetState.hide() + } + } + ) + } + GroupTitle( stringResource(R.string.WebsiteDetails), modifier = Modifier.padding(top = 8.dp) diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Navigation.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Navigation.kt index 826918b4..02526464 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Navigation.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Navigation.kt @@ -4,10 +4,8 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import dev.medzik.librepass.android.R -import dev.medzik.librepass.android.ui.DefaultScaffold -import dev.medzik.librepass.android.ui.TopBarWithBack import dev.medzik.librepass.android.common.navtype.CipherTypeType +import dev.medzik.librepass.android.ui.DefaultScaffold import dev.medzik.librepass.types.cipher.CipherType import kotlin.reflect.typeOf @@ -47,14 +45,6 @@ fun NavGraphBuilder.vaultNavigation(navController: NavController) { OtpConfigureScreen(navController, args) } - composable { - DefaultScaffold( - topBar = { TopBarWithBack(R.string.PasswordGenerator, navController) } - ) { - PasswordGeneratorScreen(navController) - } - } - composable { SearchScreen(navController) } diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/PasswordGenerator.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/PasswordGenerator.kt deleted file mode 100644 index ca12e366..00000000 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/PasswordGenerator.kt +++ /dev/null @@ -1,220 +0,0 @@ -package dev.medzik.librepass.android.ui.screens.vault - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.AutoAwesome -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material.icons.filled.Remove -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import dev.medzik.android.components.rememberMutable -import dev.medzik.android.components.rememberMutableString -import dev.medzik.android.components.ui.SwitcherPreference -import dev.medzik.android.utils.runOnIOThread -import dev.medzik.librepass.android.R -import dev.medzik.librepass.android.database.datastore.PasswordGeneratorPreference -import dev.medzik.librepass.android.database.datastore.readPasswordGeneratorPreference -import dev.medzik.librepass.android.database.datastore.writePasswordGeneratorPreference -import kotlinx.serialization.Serializable -import java.util.Random - -enum class PasswordType(val literals: String) { - NUMERIC("1234567890"), - LOWERCASE("abcdefghijklmnopqrstuvwxyz"), - UPPERCASE("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), - SYMBOLS("!@#\$%^&*()_+-=[]{}\\|;:'\",.<>/?") -} - -@Serializable -object PasswordGenerator - -@Composable -fun PasswordGeneratorScreen(navController: NavController) { - val context = LocalContext.current - val clipboardManager = LocalClipboardManager.current - - var generatedPassword by rememberMutableString() - - var passwordGeneratorPreference by rememberMutable(PasswordGeneratorPreference()) - LaunchedEffect(Unit) { - passwordGeneratorPreference = readPasswordGeneratorPreference(context) - } - - fun generatePassword(): String { - var letters = PasswordType.LOWERCASE.literals - - if (passwordGeneratorPreference.capitalize) { - letters += PasswordType.UPPERCASE.literals - } - - if (passwordGeneratorPreference.includeNumbers) { - letters += PasswordType.NUMERIC.literals - } - - if (passwordGeneratorPreference.includeSymbols) { - letters += PasswordType.SYMBOLS.literals - } - - return (1..passwordGeneratorPreference.length) - .map { Random().nextInt(letters.length) } - .map(letters::get) - .joinToString("") - } - - // regenerate on options change - LaunchedEffect(passwordGeneratorPreference) { - generatedPassword = generatePassword() - } - - Row { - OutlinedTextField( - modifier = Modifier.weight(1f), - value = generatedPassword, - onValueChange = {}, - readOnly = true, - trailingIcon = { - Row { - IconButton( - onClick = { clipboardManager.setText(AnnotatedString(generatedPassword)) } - ) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = null - ) - } - - IconButton( - onClick = { generatedPassword = generatePassword() } - ) { - Icon( - imageVector = Icons.Default.AutoAwesome, - contentDescription = null - ) - } - } - }, - ) - } - - Spacer(modifier = Modifier.padding(top = 8.dp)) - - // Password length options - Row( - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - value = passwordGeneratorPreference.length.toString(), - modifier = Modifier - .weight(1f) - .padding(end = 8.dp), - label = { Text(stringResource(R.string.PasswordGenerator_Length)) }, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), - onValueChange = { - try { - if (it.length in 1..3 && it.toInt() <= 256) { - passwordGeneratorPreference = passwordGeneratorPreference.copy(length = it.toInt()) - runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } - } - } catch (e: NumberFormatException) { - // ignore, just do not update input value - } - } - ) - - // - and + buttons - IconButton( - onClick = { - if (passwordGeneratorPreference.length > 1) { - passwordGeneratorPreference = passwordGeneratorPreference.copy( - length = passwordGeneratorPreference.length - 1 - ) - runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } - } - } - ) { - Icon( - imageVector = Icons.Default.Remove, - contentDescription = null - ) - } - IconButton( - onClick = { - if (passwordGeneratorPreference.length < 256) { - passwordGeneratorPreference = passwordGeneratorPreference.copy( - length = passwordGeneratorPreference.length + 1 - ) - runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } - } - } - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null - ) - } - } - - // Capital letters switch - SwitcherPreference( - title = stringResource(R.string.PasswordGenerator_CapitalLetters), - checked = passwordGeneratorPreference.capitalize, - onCheckedChange = { - passwordGeneratorPreference = passwordGeneratorPreference.copy(capitalize = it) - runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } - } - ) - - // Numeric switch - SwitcherPreference( - title = stringResource(R.string.PasswordGenerator_Numbers), - checked = passwordGeneratorPreference.includeNumbers, - onCheckedChange = { - passwordGeneratorPreference = passwordGeneratorPreference.copy(includeNumbers = it) - runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } - } - ) - - // Symbols switch - SwitcherPreference( - title = stringResource(R.string.PasswordGenerator_Symbols), - checked = passwordGeneratorPreference.includeSymbols, - onCheckedChange = { - passwordGeneratorPreference = passwordGeneratorPreference.copy(includeSymbols = it) - runOnIOThread { writePasswordGeneratorPreference(context, passwordGeneratorPreference) } - } - ) - - Button( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 90.dp) - .padding(top = 16.dp), - onClick = { - navController.previousBackStackEntry!!.savedStateHandle["password"] = generatedPassword - navController.popBackStack() - } - ) { - Text(stringResource(R.string.Submit)) - } -}