aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2023-07-05 14:20:22 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2023-07-05 14:24:35 +0530
commitfa03ca0ad7e556e60a13a355cbbd44675c135f88 (patch)
treef498644dab8590e626c10a3763eab48868dda4d3
parent4c28098cbb682c99a0a4983c70671e3f872de211 (diff)
feat(ui): add a dedicated Compose screen for editing passwords
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt100
-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.xml1
-rw-r--r--ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt24
-rw-r--r--ui-compose/src/main/res/drawable/ic_content_copy.xml15
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>