aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-02 23:30:37 +0100
committerDanilo M. <danix@danix.xyz>2026-11-02 23:30:37 +0100
commit22bb0aae958e8458274be994f3e679cb226d2241 (patch)
treedf00014e469ccc1a06648283284b38b5ea0eb08f
parent5bea3821864212de67dc3bfc1299ec8b12134213 (diff)
downloadHardenedPass-22bb0aae958e8458274be994f3e679cb226d2241.tar.gz
HardenedPass-22bb0aae958e8458274be994f3e679cb226d2241.zip
feat(app): add the Compose entry screen
Header with avatar, name and folder; password, username and one-time code with a countdown bar in the first group; extra fields in the second. FieldRow can start revealed (the "Show the password" setting) and shows masked values in JetBrains Mono, as the old screen did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--app/src/main/java/app/passwordstore/ui/entry/EntryScreen.kt183
-rw-r--r--app/src/main/res/values-it/strings.xml6
-rw-r--r--app/src/main/res/values/strings.xml6
-rw-r--r--ui/compose/src/main/kotlin/app/passwordstore/ui/compose/FieldRow.kt13
4 files changed, 205 insertions, 3 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/entry/EntryScreen.kt b/app/src/main/java/app/passwordstore/ui/entry/EntryScreen.kt
new file mode 100644
index 00000000..d4860819
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/ui/entry/EntryScreen.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.entry
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import app.passwordstore.R
+import app.passwordstore.ui.compose.FieldRow
+import app.passwordstore.ui.compose.FieldRowLabels
+import app.passwordstore.ui.compose.GroupedSurface
+import app.passwordstore.ui.compose.LetterAvatar
+import app.passwordstore.ui.compose.theme.SpacingLarge
+import app.passwordstore.ui.compose.theme.SpacingMedium
+
+/**
+ * One entry: header (avatar, name, [folder] relative to the store root, "" at the root), then the
+ * password, username and one-time code, then the extra fields. [fields] is null until decrypted;
+ * [otp] is null when the entry has no TOTP. [onCopySecret] (password, one-time code) is separate
+ * from [onCopy] because only those copies start the clipboard clear timer.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EntryScreen(
+ name: String,
+ folder: String,
+ fields: EntryFields?,
+ otp: String?,
+ otpSecondsLeft: Long,
+ otpFractionLeft: Float,
+ showPassword: Boolean,
+ onBack: () -> Unit,
+ onEdit: () -> Unit,
+ onShare: () -> Unit,
+ onCopyName: () -> Unit,
+ onCopySecret: (String) -> Unit,
+ onCopy: (String) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var menu by remember { mutableStateOf(false) }
+ val labels =
+ FieldRowLabels(
+ copy = stringResource(R.string.copy_label),
+ show = stringResource(R.string.entry_show_password),
+ hide = stringResource(R.string.entry_hide_password),
+ )
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {},
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.entry_back))
+ }
+ },
+ actions = {
+ if (fields != null)
+ IconButton(onClick = onEdit) {
+ Icon(
+ painterResource(R.drawable.ic_edit_24dp),
+ contentDescription = stringResource(R.string.edit_password),
+ )
+ }
+ Box {
+ IconButton(onClick = { menu = true }) {
+ Icon(Icons.Filled.MoreVert, stringResource(R.string.list_more_options))
+ }
+ DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.entry_copy_name)) },
+ onClick = {
+ menu = false
+ onCopyName()
+ },
+ )
+ if (fields?.password != null)
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.share_as_plaintext)) },
+ onClick = {
+ menu = false
+ onShare()
+ },
+ leadingIcon = {
+ Icon(painterResource(R.drawable.ic_share_24dp), contentDescription = null)
+ },
+ )
+ }
+ }
+ },
+ )
+ },
+ ) { padding ->
+ Column(Modifier.padding(padding).fillMaxSize().verticalScroll(rememberScrollState())) {
+ Row(
+ Modifier.fillMaxWidth().padding(horizontal = SpacingLarge, vertical = SpacingMedium),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ LetterAvatar(name)
+ Column(Modifier.weight(1f).padding(start = SpacingLarge)) {
+ Text(name, style = MaterialTheme.typography.titleLarge)
+ Text(
+ if (folder.isEmpty()) stringResource(R.string.list_store_root) else folder,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ if (fields != null) {
+ if (fields.password != null || fields.username != null || otp != null)
+ GroupedSurface(Modifier.fillMaxWidth()) {
+ if (fields.password != null)
+ FieldRow(
+ label = stringResource(R.string.password),
+ value = fields.password,
+ labels = labels.copy(copy = stringResource(R.string.copy_password)),
+ masked = true,
+ initiallyRevealed = showPassword,
+ onCopy = { onCopySecret(fields.password) },
+ )
+ if (fields.username != null)
+ FieldRow(
+ label = stringResource(R.string.username),
+ value = fields.username,
+ labels = labels,
+ onCopy = { onCopy(fields.username) },
+ )
+ if (otp != null) {
+ FieldRow(
+ label = stringResource(R.string.otp_label_format, otpSecondsLeft),
+ value = otp,
+ labels = labels,
+ onCopy = { onCopySecret(otp) },
+ )
+ LinearProgressIndicator(
+ progress = { otpFractionLeft },
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(start = SpacingLarge, end = SpacingLarge, bottom = SpacingMedium),
+ )
+ }
+ }
+ if (fields.extras.isNotEmpty())
+ GroupedSurface(Modifier.fillMaxWidth()) {
+ fields.extras.forEach { (label, value) ->
+ key(label) { FieldRow(label, value, labels, onCopy = { onCopy(value) }) }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 5ed24efb..31c34b4d 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -352,4 +352,10 @@
<string name="list_store_root">Archivio password</string>
<string name="list_more_options">Altre opzioni</string>
<string name="list_clear_search">Cancella ricerca</string>
+
+ <!-- Compose entry view -->
+ <string name="entry_back">Indietro</string>
+ <string name="entry_show_password">Mostra password</string>
+ <string name="entry_hide_password">Nascondi password</string>
+ <string name="entry_copy_name">Copia nome</string>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9e402f6b..2e208158 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -386,4 +386,10 @@
<string name="list_store_root">Password store</string>
<string name="list_more_options">More options</string>
<string name="list_clear_search">Clear search</string>
+
+ <!-- Compose entry view -->
+ <string name="entry_back">Back</string>
+ <string name="entry_show_password">Show password</string>
+ <string name="entry_hide_password">Hide password</string>
+ <string name="entry_copy_name">Copy name</string>
</resources>
diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/FieldRow.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/FieldRow.kt
index c49ec2bc..e632acc1 100644
--- a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/FieldRow.kt
+++ b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/FieldRow.kt
@@ -21,6 +21,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
import app.passwordstore.ui.compose.preview.ThemePreviews
import app.passwordstore.ui.compose.theme.APSTheme
import app.passwordstore.ui.compose.theme.SpacingLarge
@@ -29,9 +31,12 @@ import app.passwordstore.ui.compose.theme.SpacingMedium
/** Content descriptions for [FieldRow]'s buttons, supplied by the app's translated strings. */
public data class FieldRowLabels(val copy: String, val show: String, val hide: String)
+/** Masked values (passwords) use a monospace face, so 0 and O, l and 1 stay apart. */
+private val Monospace = FontFamily(Font(R.font.jetbrainsmono_nl_regular))
+
/**
* A labelled value with an optional copy button. When [masked] is true the value is hidden behind
- * dots until the reveal toggle is pressed.
+ * dots until the reveal toggle is pressed, unless [initiallyRevealed] shows it from the start.
*/
@Composable
public fun FieldRow(
@@ -40,9 +45,10 @@ public fun FieldRow(
labels: FieldRowLabels,
modifier: Modifier = Modifier,
masked: Boolean = false,
+ initiallyRevealed: Boolean = false,
onCopy: (() -> Unit)? = null,
) {
- var revealed by rememberSaveable { mutableStateOf(false) }
+ var revealed by rememberSaveable { mutableStateOf(initiallyRevealed) }
Row(
modifier
.fillMaxWidth()
@@ -60,9 +66,10 @@ public fun FieldRow(
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
+ val style = MaterialTheme.typography.bodyLarge
Text(
if (masked && !revealed) "•".repeat(10) else value,
- style = MaterialTheme.typography.bodyLarge,
+ style = if (masked) style.copy(fontFamily = Monospace) else style,
)
}
if (masked)