Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit fa03ca0

Browse files
committed
feat(ui): add a dedicated Compose screen for editing passwords
1 parent 4c28098 commit fa03ca0

File tree

5 files changed

+156
-63
lines changed

5 files changed

+156
-63
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package app.passwordstore.ui.crypto
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.material3.MaterialTheme
9+
import androidx.compose.material3.Scaffold
10+
import androidx.compose.material3.Text
11+
import androidx.compose.material3.TextField
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.res.painterResource
16+
import androidx.compose.ui.res.stringResource
17+
import androidx.compose.ui.tooling.preview.Preview
18+
import androidx.compose.ui.unit.dp
19+
import app.passwordstore.R
20+
import app.passwordstore.data.passfile.PasswordEntry
21+
import app.passwordstore.ui.APSAppBar
22+
import app.passwordstore.ui.compose.PasswordField
23+
import app.passwordstore.ui.compose.theme.APSThemePreview
24+
import app.passwordstore.util.time.UserClock
25+
import app.passwordstore.util.totp.UriTotpFinder
26+
27+
/** Composable to show allow editing an existing [PasswordEntry]. */
28+
@Composable
29+
fun EditPasswordScreen(
30+
entryName: String,
31+
entry: PasswordEntry,
32+
onNavigateUp: () -> Unit,
33+
@Suppress("UNUSED_PARAMETER") onSave: (PasswordEntry) -> Unit,
34+
modifier: Modifier = Modifier,
35+
) {
36+
Scaffold(
37+
topBar = {
38+
APSAppBar(
39+
title = entryName,
40+
navigationIcon = painterResource(R.drawable.ic_arrow_back_black_24dp),
41+
onNavigationIconClick = onNavigateUp,
42+
backgroundColor = MaterialTheme.colorScheme.surface,
43+
)
44+
},
45+
) { paddingValues ->
46+
Box(modifier = modifier.padding(paddingValues)) {
47+
Column(modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp).fillMaxSize()) {
48+
if (entry.password != null) {
49+
PasswordField(
50+
value = entry.password!!,
51+
label = stringResource(R.string.password),
52+
initialVisibility = false,
53+
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
54+
)
55+
}
56+
ExtraContent(entry = entry)
57+
}
58+
}
59+
}
60+
}
61+
62+
@Composable
63+
private fun ExtraContent(
64+
entry: PasswordEntry,
65+
modifier: Modifier = Modifier,
66+
) {
67+
TextField(
68+
value = entry.extraContentString,
69+
onValueChange = {},
70+
label = { Text("Extra content") },
71+
modifier = modifier.fillMaxWidth(),
72+
)
73+
}
74+
75+
@Preview
76+
@Composable
77+
private fun EditPasswordScreenPreview() {
78+
APSThemePreview {
79+
EditPasswordScreen(
80+
entryName = "Test Entry",
81+
entry = createTestEntry(),
82+
onNavigateUp = {},
83+
onSave = {},
84+
)
85+
}
86+
}
87+
88+
private fun createTestEntry() =
89+
PasswordEntry(
90+
UserClock(),
91+
UriTotpFinder(),
92+
"""
93+
|My Password
94+
|otpauth://totp/ACME%20Co:[email protected]?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
95+
|login: msfjarvis
96+
|URL: example.com
97+
"""
98+
.trimMargin()
99+
.encodeToByteArray()
100+
)

app/src/main/java/app/passwordstore/ui/crypto/DecryptScreen.kt renamed to app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt

Lines changed: 17 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@ package app.passwordstore.ui.crypto
22

33
import androidx.compose.foundation.layout.Box
44
import androidx.compose.foundation.layout.Column
5-
import androidx.compose.foundation.layout.IntrinsicSize
65
import androidx.compose.foundation.layout.fillMaxSize
76
import androidx.compose.foundation.layout.fillMaxWidth
87
import androidx.compose.foundation.layout.padding
9-
import androidx.compose.foundation.layout.width
10-
import androidx.compose.material3.Icon
11-
import androidx.compose.material3.IconButton
128
import androidx.compose.material3.MaterialTheme
139
import androidx.compose.material3.Scaffold
1410
import androidx.compose.material3.Text
@@ -17,40 +13,28 @@ import androidx.compose.runtime.Composable
1713
import androidx.compose.runtime.collectAsState
1814
import androidx.compose.runtime.getValue
1915
import androidx.compose.ui.Modifier
20-
import androidx.compose.ui.platform.LocalClipboardManager
2116
import androidx.compose.ui.res.painterResource
2217
import androidx.compose.ui.res.stringResource
23-
import androidx.compose.ui.text.AnnotatedString
2418
import androidx.compose.ui.text.capitalize
2519
import androidx.compose.ui.text.intl.Locale
2620
import androidx.compose.ui.tooling.preview.Preview
2721
import androidx.compose.ui.unit.dp
2822
import app.passwordstore.R
2923
import app.passwordstore.data.passfile.PasswordEntry
3024
import app.passwordstore.ui.APSAppBar
25+
import app.passwordstore.ui.compose.CopyButton
3126
import app.passwordstore.ui.compose.PasswordField
3227
import app.passwordstore.ui.compose.theme.APSThemePreview
3328
import app.passwordstore.util.time.UserClock
3429
import app.passwordstore.util.totp.UriTotpFinder
3530
import kotlinx.coroutines.flow.first
3631
import kotlinx.coroutines.runBlocking
3732

38-
/**
39-
* Composable to show a [PasswordEntry]. It can be used for both read-only usage (decrypt screen) or
40-
* read-write (encrypt screen) to allow sharing UI logic for both these screens and deferring all
41-
* the cryptographic aspects to its parent.
42-
*
43-
* When [readOnly] is `true`, the Composable assumes that we're showcasing the provided [entry] to
44-
* the user and does not offer any edit capabilities.
45-
*
46-
* When [readOnly] is `false`, the [TextField]s are rendered editable but currently do not pass up
47-
* their "updated" state to anything. This will be changed in later commits.
48-
*/
33+
/** Composable to show a decrypted [PasswordEntry]. */
4934
@Composable
50-
fun PasswordEntryScreen(
35+
fun ViewPasswordScreen(
5136
entryName: String,
5237
entry: PasswordEntry,
53-
readOnly: Boolean,
5438
onNavigateUp: () -> Unit,
5539
modifier: Modifier = Modifier,
5640
) {
@@ -71,32 +55,32 @@ fun PasswordEntryScreen(
7155
value = entry.password!!,
7256
label = stringResource(R.string.password),
7357
initialVisibility = false,
74-
readOnly = readOnly,
58+
readOnly = true,
7559
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
7660
)
7761
}
78-
if (entry.hasTotp() && readOnly) {
62+
if (entry.hasTotp()) {
7963
val totp by entry.totp.collectAsState(runBlocking { entry.totp.first() })
8064
TextField(
8165
value = totp.value,
8266
onValueChange = {},
8367
readOnly = true,
8468
label = { Text("OTP (expires in ${totp.remainingTime.inWholeSeconds}s)") },
85-
trailingIcon = { CopyButton({ totp.value }) },
69+
trailingIcon = { CopyButton(totp.value, R.string.copy_label) },
8670
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
8771
)
8872
}
89-
if (entry.username != null && readOnly) {
73+
if (entry.username != null) {
9074
TextField(
9175
value = entry.username!!,
9276
onValueChange = {},
9377
readOnly = true,
9478
label = { Text(stringResource(R.string.username)) },
95-
trailingIcon = { CopyButton({ entry.username!! }) },
79+
trailingIcon = { CopyButton(entry.username!!, R.string.copy_label) },
9680
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
9781
)
9882
}
99-
ExtraContent(entry = entry, readOnly = readOnly)
83+
ExtraContent(entry = entry)
10084
}
10185
}
10286
}
@@ -105,56 +89,27 @@ fun PasswordEntryScreen(
10589
@Composable
10690
private fun ExtraContent(
10791
entry: PasswordEntry,
108-
readOnly: Boolean,
10992
modifier: Modifier = Modifier,
11093
) {
111-
if (readOnly) {
112-
entry.extraContent.forEach { (label, value) ->
113-
TextField(
114-
value = value,
115-
onValueChange = {},
116-
readOnly = true,
117-
label = { Text(label.capitalize(Locale.current)) },
118-
trailingIcon = { CopyButton({ value }) },
119-
modifier = modifier.padding(bottom = 8.dp).fillMaxWidth(),
120-
)
121-
}
122-
} else {
94+
entry.extraContent.forEach { (label, value) ->
12395
TextField(
124-
value = entry.extraContentString,
96+
value = value,
12597
onValueChange = {},
126-
readOnly = false,
127-
label = { Text("Extra content") },
128-
modifier = modifier.fillMaxWidth(),
129-
)
130-
}
131-
}
132-
133-
@Composable
134-
private fun CopyButton(
135-
textToCopy: () -> String,
136-
modifier: Modifier = Modifier,
137-
) {
138-
val clipboard = LocalClipboardManager.current
139-
IconButton(
140-
onClick = { clipboard.setText(AnnotatedString(textToCopy())) },
141-
modifier = modifier,
142-
) {
143-
Icon(
144-
painter = painterResource(R.drawable.ic_content_copy),
145-
contentDescription = stringResource(R.string.copy_password),
98+
readOnly = true,
99+
label = { Text(label.capitalize(Locale.current)) },
100+
trailingIcon = { CopyButton(value, R.string.copy_label) },
101+
modifier = modifier.padding(bottom = 8.dp).fillMaxWidth(),
146102
)
147103
}
148104
}
149105

150106
@Preview
151107
@Composable
152-
private fun PasswordEntryPreview() {
108+
private fun ViewPasswordScreenPreview() {
153109
APSThemePreview {
154-
PasswordEntryScreen(
110+
ViewPasswordScreen(
155111
entryName = "Test Entry",
156112
entry = createTestEntry(),
157-
readOnly = true,
158113
onNavigateUp = {},
159114
)
160115
}

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
<string name="action_search">Search</string>
8989
<string name="password">Password</string>
9090
<string name="username">Username</string>
91+
<string name="copy_label">Copy</string>
9192
<string name="edit_password">Edit password</string>
9293
<string name="copy_password">Copy password</string>
9394
<string name="share_as_plaintext">Share as plaintext</string>

ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package app.passwordstore.ui.compose
22

3+
import androidx.annotation.StringRes
34
import androidx.compose.material3.Icon
45
import androidx.compose.material3.IconButton
56
import androidx.compose.material3.Text
@@ -10,7 +11,10 @@ import androidx.compose.runtime.mutableStateOf
1011
import androidx.compose.runtime.remember
1112
import androidx.compose.runtime.setValue
1213
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.platform.LocalClipboardManager
1315
import androidx.compose.ui.res.painterResource
16+
import androidx.compose.ui.res.stringResource
17+
import androidx.compose.ui.text.AnnotatedString
1418
import androidx.compose.ui.text.input.PasswordVisualTransformation
1519
import androidx.compose.ui.text.input.VisualTransformation
1620

@@ -19,8 +23,8 @@ public fun PasswordField(
1923
value: String,
2024
label: String,
2125
initialVisibility: Boolean,
22-
readOnly: Boolean,
2326
modifier: Modifier = Modifier,
27+
readOnly: Boolean = false,
2428
) {
2529
var visible by remember { mutableStateOf(initialVisibility) }
2630
TextField(
@@ -58,3 +62,21 @@ private fun ToggleButton(
5862
)
5963
}
6064
}
65+
66+
@Composable
67+
public fun CopyButton(
68+
textToCopy: String,
69+
@StringRes buttonLabelRes: Int,
70+
modifier: Modifier = Modifier,
71+
) {
72+
val clipboard = LocalClipboardManager.current
73+
IconButton(
74+
onClick = { clipboard.setText(AnnotatedString(textToCopy)) },
75+
modifier = modifier,
76+
) {
77+
Icon(
78+
painter = painterResource(R.drawable.ic_content_copy),
79+
contentDescription = stringResource(buttonLabelRes),
80+
)
81+
}
82+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!--
2+
~ Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
3+
~ SPDX-License-Identifier: GPL-3.0-only
4+
-->
5+
6+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
7+
android:width="24dp"
8+
android:height="24dp"
9+
android:tint="?attr/colorControlNormal"
10+
android:viewportWidth="24"
11+
android:viewportHeight="24">
12+
<path
13+
android:fillColor="#FFFFFF"
14+
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" />
15+
</vector>

0 commit comments

Comments
 (0)