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
↓ peach-android-5c1d6eca.tar.gzatlas at this save
files changed3 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 −44app/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 unlockedapp/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 /**