5c1d6ecabf7a

style: Join entry credentials into one card (v2.14.25)

time
2026-08-14 17:32 UTC · 2 months ago
machine
koh2
agent
gpt-5.6-sol
session
joined credential card
parent
fb95dc6219e5
3 files VERSION+1 −1app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+6 −2app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+76 −44
VERSION+1 −1
@@ -1 +1 @@1-2.14.241+2.14.25

view file

app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+6 −2
@@ -99,12 +99,14 @@9999     displayText: AnnotatedString? = null,100100     draftText: String? = null,101101     onEditingChange: (Boolean) -> Unit = {},102+    stacked: Boolean = false,103+    divider: Boolean = false,102104 ) {103105     val editable = draftText ?: value104106     var editing by remember(value) { mutableStateOf(false) }105107     var draft by remember(value) { mutableStateOf(editable) }106108 107-    FieldShell(label) {109+    FieldShell(label, stacked = stacked, divider = divider) {108110         if (!editing) {109111             val isEmpty = value.isBlank()110112             Text(@@ -147,6 +149,8 @@147149     onCopy: () -> Unit,148150     onSave: (String) -> Unit,149151     onGenerate: () -> Unit,152+    stacked: Boolean = false,153+    divider: Boolean = false,150154 ) {151155     val accent = LocalPeachAccent.current152156     val context = LocalContext.current@@ -170,7 +174,7 @@170174         }171175     }172176 173-    FieldShell("password") {177+    FieldShell("password", stacked = stacked, divider = divider) {174178         if (!editing) {175179             // Revealing is not the same act as editing, so it does not go176180             // through the biometric edit gate — the vault is already unlocked

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+76 −44
@@ -228,43 +228,61 @@228228                         copyButton = { copied, onCopy -> CopyButton(copied, onCopy) },229229                     )230230                 } else {231-                    if (!e.isNote()) {232-                        EditableFieldRow(233-                            label = "url", field = "url", value = e.url.orEmpty(),234-                            copied = copiedField == "url",235-                            onCopy = { vm.copy("url", e.url) },236-                            onSave = { vm.updateField("url", it) },237-                            placeholder = "add url",238-                            onEditingChange = { urlEditing = it },239-                        )240-                    }241-                    // Browser distinguishes USERNAME and EMAIL as separate242-                    // optional fields, with a "+ add X" link for whichever the243-                    // entry doesn't have yet — an entry can carry both. This244-                    // used to just omit the row entirely for a blank field,245-                    // with no way to add it from the detail screen.246231                     var showUsernameField by remember(e.id) { mutableStateOf(!e.username.isNullOrBlank()) }247232                     var showEmailField by remember(e.id) { mutableStateOf(!e.email.isNullOrBlank()) }248-                    if (showUsernameField) {249-                        EditableFieldRow(250-                            label = "username", field = "username", value = e.username.orEmpty(),251-                            copied = copiedField == "username",252-                            onCopy = { vm.copy("username", e.username) },253-                            onSave = { vm.updateField("username", it) },254-                            suggestions = identitySuggestions,255-                            placeholder = "add username",256-                        )233+                    val hasPassword = !e.password.isNullOrBlank()234+                    if (!e.isNote()) {235+                        CredentialStack {236+                            EditableFieldRow(237+                                label = "url", field = "url", value = e.url.orEmpty(),238+                                copied = copiedField == "url",239+                                onCopy = { vm.copy("url", e.url) },240+                                onSave = { vm.updateField("url", it) },241+                                placeholder = "add url",242+                                onEditingChange = { urlEditing = it },243+                                stacked = true,244+                                divider = showUsernameField || showEmailField || hasPassword,245+                            )246+                            // Browser distinguishes USERNAME and EMAIL as separate247+                            // optional fields, with a "+ add X" link for whichever the248+                            // entry doesn't have yet — an entry can carry both.249+                            if (showUsernameField) {250+                                EditableFieldRow(251+                                    label = "username", field = "username", value = e.username.orEmpty(),252+                                    copied = copiedField == "username",253+                                    onCopy = { vm.copy("username", e.username) },254+                                    onSave = { vm.updateField("username", it) },255+                                    suggestions = identitySuggestions,256+                                    placeholder = "add username",257+                                    stacked = true,258+                                    divider = showEmailField || hasPassword,259+                                )260+                            }261+                            if (showEmailField) {262+                                EditableFieldRow(263+                                    label = "email", field = "email", value = e.email.orEmpty(),264+                                    copied = copiedField == "email",265+                                    onCopy = { vm.copy("email", e.email) },266+                                    onSave = { vm.updateField("email", it) },267+                                    suggestions = identitySuggestions,268+                                    placeholder = "add email",269+                                    stacked = true,270+                                    divider = hasPassword,271+                                )272+                            }273+                            e.password?.takeIf { it.isNotBlank() }?.let { pw ->274+                                EditablePasswordRow(275+                                    value = pw,276+                                    copied = copiedField == "password",277+                                    entryTitle = e.title,278+                                    onCopy = { vm.copy("password", pw) },279+                                    onSave = { vm.updateField("password", it) },280+                                    onGenerate = { generatorOpen = true },281+                                    stacked = true,282+                                )283+                            }284+                        }257285                     }258-                    if (showEmailField) {259-                        EditableFieldRow(260-                            label = "email", field = "email", value = e.email.orEmpty(),261-                            copied = copiedField == "email",262-                            onCopy = { vm.copy("email", e.email) },263-                            onSave = { vm.updateField("email", it) },264-                            suggestions = identitySuggestions,265-                            placeholder = "add email",266-                        )267-                    }268286                     if (!showUsernameField || !showEmailField) {269287                         Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {270288                             if (!showUsernameField) {@@ -298,16 +316,6 @@298316                     if (e.hasPasskey()) {299317                         PasskeyDetailSection(e)300318                     }301-                    e.password?.takeIf { it.isNotBlank() }?.let { pw ->302-                        EditablePasswordRow(303-                            value = pw,304-                            copied = copiedField == "password",305-                            entryTitle = e.title,306-                            onCopy = { vm.copy("password", pw) },307-                            onSave = { vm.updateField("password", it) },308-                            onGenerate = { generatorOpen = true },309-                        )310-                    }311319                     // Always show the history affordance (browser parity) — visible312320                     // even before any password change, expanding to an empty-state313321                     // explainer so the feature is discoverable rather than hidden@@ -764,8 +772,21 @@764772 internal fun FieldShell(765773     label: String,766774     modifier: Modifier = Modifier,775+    stacked: Boolean = false,776+    divider: Boolean = false,767777     content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit,768778 ) {779+    if (stacked) {780+        Column(modifier.fillMaxWidth()) {781+            Column(Modifier.fillMaxWidth().padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md)) {782+                Text(label.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))783+                Spacer(Modifier.height(6.dp))784+                Row(verticalAlignment = Alignment.CenterVertically, content = content)785+            }786+            if (divider) Box(Modifier.fillMaxWidth().height(1.dp).background(peachColors.T3))787+        }788+        return789+    }769790     Column(770791         modifier.fillMaxWidth()771792             .padding(vertical = PeachSpacing.xs)@@ -778,6 +799,17 @@778799         Spacer(Modifier.height(6.dp))779800         Row(verticalAlignment = Alignment.CenterVertically, content = content)780801     }802+}803+804+@Composable805+private fun CredentialStack(content: @Composable () -> Unit) {806+    Column(807+        Modifier.fillMaxWidth()808+            .padding(vertical = PeachSpacing.xs)809+            .clip(RoundedCornerShape(PeachRadius.StackCard))810+            .background(peachColors.SurfaceCard)811+            .border(1.dp, peachColors.T3, RoundedCornerShape(PeachRadius.StackCard)),812+    ) { content() }781813 }782814 783815 /**

view file