mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-28 21:07:39 +00:00
feat(ui): add a dedicated Compose screen for editing passwords
This commit is contained in:
parent
4c28098cbb
commit
fa03ca0ad7
@ -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()
|
||||
)
|
@ -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 = {},
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
15
ui-compose/src/main/res/drawable/ic_content_copy.xml
Normal file
15
ui-compose/src/main/res/drawable/ic_content_copy.xml
Normal file
@ -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>
|
Loading…
x
Reference in New Issue
Block a user