diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2023-07-05 14:20:22 +0530 |
---|---|---|
committer | Harsh Shandilya <me@msfjarvis.dev> | 2023-07-05 14:24:35 +0530 |
commit | fa03ca0ad7e556e60a13a355cbbd44675c135f88 (patch) | |
tree | f498644dab8590e626c10a3763eab48868dda4d3 | |
parent | 4c28098cbb682c99a0a4983c70671e3f872de211 (diff) |
feat(ui): add a dedicated Compose screen for editing passwords
-rw-r--r-- | app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt | 100 | ||||
-rw-r--r-- | app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt (renamed from app/src/main/java/app/passwordstore/ui/crypto/DecryptScreen.kt) | 79 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 1 | ||||
-rw-r--r-- | ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt | 24 | ||||
-rw-r--r-- | ui-compose/src/main/res/drawable/ic_content_copy.xml | 15 |
5 files changed, 156 insertions, 63 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt b/app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt new file mode 100644 index 00000000..9c3707f7 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt @@ -0,0 +1,100 @@ +package app.passwordstore.ui.crypto + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import app.passwordstore.R +import app.passwordstore.data.passfile.PasswordEntry +import app.passwordstore.ui.APSAppBar +import app.passwordstore.ui.compose.PasswordField +import app.passwordstore.ui.compose.theme.APSThemePreview +import app.passwordstore.util.time.UserClock +import app.passwordstore.util.totp.UriTotpFinder + +/** Composable to show allow editing an existing [PasswordEntry]. */ +@Composable +fun EditPasswordScreen( + entryName: String, + entry: PasswordEntry, + onNavigateUp: () -> Unit, + @Suppress("UNUSED_PARAMETER") onSave: (PasswordEntry) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + topBar = { + APSAppBar( + title = entryName, + navigationIcon = painterResource(R.drawable.ic_arrow_back_black_24dp), + onNavigationIconClick = onNavigateUp, + backgroundColor = MaterialTheme.colorScheme.surface, + ) + }, + ) { paddingValues -> + Box(modifier = modifier.padding(paddingValues)) { + Column(modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp).fillMaxSize()) { + if (entry.password != null) { + PasswordField( + value = entry.password!!, + label = stringResource(R.string.password), + initialVisibility = false, + modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(), + ) + } + ExtraContent(entry = entry) + } + } + } +} + +@Composable +private fun ExtraContent( + entry: PasswordEntry, + modifier: Modifier = Modifier, +) { + TextField( + value = entry.extraContentString, + onValueChange = {}, + label = { Text("Extra content") }, + modifier = modifier.fillMaxWidth(), + ) +} + +@Preview +@Composable +private fun EditPasswordScreenPreview() { + APSThemePreview { + EditPasswordScreen( + entryName = "Test Entry", + entry = createTestEntry(), + onNavigateUp = {}, + onSave = {}, + ) + } +} + +private fun createTestEntry() = + PasswordEntry( + UserClock(), + UriTotpFinder(), + """ + |My Password + |otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 + |login: msfjarvis + |URL: example.com + """ + .trimMargin() + .encodeToByteArray() + ) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptScreen.kt b/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt index 2e25b56f..2ebdc3a9 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptScreen.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt @@ -2,13 +2,9 @@ package app.passwordstore.ui.crypto import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -17,10 +13,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.tooling.preview.Preview @@ -28,6 +22,7 @@ import androidx.compose.ui.unit.dp import app.passwordstore.R import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.ui.APSAppBar +import app.passwordstore.ui.compose.CopyButton import app.passwordstore.ui.compose.PasswordField import app.passwordstore.ui.compose.theme.APSThemePreview import app.passwordstore.util.time.UserClock @@ -35,22 +30,11 @@ import app.passwordstore.util.totp.UriTotpFinder import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking -/** - * Composable to show a [PasswordEntry]. It can be used for both read-only usage (decrypt screen) or - * read-write (encrypt screen) to allow sharing UI logic for both these screens and deferring all - * the cryptographic aspects to its parent. - * - * When [readOnly] is `true`, the Composable assumes that we're showcasing the provided [entry] to - * the user and does not offer any edit capabilities. - * - * When [readOnly] is `false`, the [TextField]s are rendered editable but currently do not pass up - * their "updated" state to anything. This will be changed in later commits. - */ +/** Composable to show a decrypted [PasswordEntry]. */ @Composable -fun PasswordEntryScreen( +fun ViewPasswordScreen( entryName: String, entry: PasswordEntry, - readOnly: Boolean, onNavigateUp: () -> Unit, modifier: Modifier = Modifier, ) { @@ -71,32 +55,32 @@ fun PasswordEntryScreen( value = entry.password!!, label = stringResource(R.string.password), initialVisibility = false, - readOnly = readOnly, + readOnly = true, modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(), ) } - if (entry.hasTotp() && readOnly) { + if (entry.hasTotp()) { val totp by entry.totp.collectAsState(runBlocking { entry.totp.first() }) TextField( value = totp.value, onValueChange = {}, readOnly = true, label = { Text("OTP (expires in ${totp.remainingTime.inWholeSeconds}s)") }, - trailingIcon = { CopyButton({ totp.value }) }, + trailingIcon = { CopyButton(totp.value, R.string.copy_label) }, modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(), ) } - if (entry.username != null && readOnly) { + if (entry.username != null) { TextField( value = entry.username!!, onValueChange = {}, readOnly = true, label = { Text(stringResource(R.string.username)) }, - trailingIcon = { CopyButton({ entry.username!! }) }, + trailingIcon = { CopyButton(entry.username!!, R.string.copy_label) }, modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(), ) } - ExtraContent(entry = entry, readOnly = readOnly) + ExtraContent(entry = entry) } } } @@ -105,56 +89,27 @@ fun PasswordEntryScreen( @Composable private fun ExtraContent( entry: PasswordEntry, - readOnly: Boolean, modifier: Modifier = Modifier, ) { - if (readOnly) { - entry.extraContent.forEach { (label, value) -> - TextField( - value = value, - onValueChange = {}, - readOnly = true, - label = { Text(label.capitalize(Locale.current)) }, - trailingIcon = { CopyButton({ value }) }, - modifier = modifier.padding(bottom = 8.dp).fillMaxWidth(), - ) - } - } else { + entry.extraContent.forEach { (label, value) -> TextField( - value = entry.extraContentString, + value = value, onValueChange = {}, - readOnly = false, - label = { Text("Extra content") }, - modifier = modifier.fillMaxWidth(), - ) - } -} - -@Composable -private fun CopyButton( - textToCopy: () -> String, - modifier: Modifier = Modifier, -) { - val clipboard = LocalClipboardManager.current - IconButton( - onClick = { clipboard.setText(AnnotatedString(textToCopy())) }, - modifier = modifier, - ) { - Icon( - painter = painterResource(R.drawable.ic_content_copy), - contentDescription = stringResource(R.string.copy_password), + readOnly = true, + label = { Text(label.capitalize(Locale.current)) }, + trailingIcon = { CopyButton(value, R.string.copy_label) }, + modifier = modifier.padding(bottom = 8.dp).fillMaxWidth(), ) } } @Preview @Composable -private fun PasswordEntryPreview() { +private fun ViewPasswordScreenPreview() { APSThemePreview { - PasswordEntryScreen( + ViewPasswordScreen( entryName = "Test Entry", entry = createTestEntry(), - readOnly = true, onNavigateUp = {}, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38b7dbc3..deb80de3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ <string name="action_search">Search</string> <string name="password">Password</string> <string name="username">Username</string> + <string name="copy_label">Copy</string> <string name="edit_password">Edit password</string> <string name="copy_password">Copy password</string> <string name="share_as_plaintext">Share as plaintext</string> diff --git a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt b/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt index ee202486..d2a04777 100644 --- a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt +++ b/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt @@ -1,5 +1,6 @@ package app.passwordstore.ui.compose +import androidx.annotation.StringRes import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -10,7 +11,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation @@ -19,8 +23,8 @@ public fun PasswordField( value: String, label: String, initialVisibility: Boolean, - readOnly: Boolean, modifier: Modifier = Modifier, + readOnly: Boolean = false, ) { var visible by remember { mutableStateOf(initialVisibility) } TextField( @@ -58,3 +62,21 @@ private fun ToggleButton( ) } } + +@Composable +public fun CopyButton( + textToCopy: String, + @StringRes buttonLabelRes: Int, + modifier: Modifier = Modifier, +) { + val clipboard = LocalClipboardManager.current + IconButton( + onClick = { clipboard.setText(AnnotatedString(textToCopy)) }, + modifier = modifier, + ) { + Icon( + painter = painterResource(R.drawable.ic_content_copy), + contentDescription = stringResource(buttonLabelRes), + ) + } +} diff --git a/ui-compose/src/main/res/drawable/ic_content_copy.xml b/ui-compose/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 00000000..8afc9846 --- /dev/null +++ b/ui-compose/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,15 @@ +<!-- + ~ Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + ~ SPDX-License-Identifier: GPL-3.0-only + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FFFFFF" + android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5L8,5c-1.1,0 -1.99,0.9 -1.99,2L6,21c0,1.1 0.89,2 1.99,2L19,23c1.1,0 2,-0.9 2,-2L21,11l-6,-6zM8,21L8,7h6v5h5v9L8,21z" /> +</vector> |