d9f3f7e7ee34
add password reveal, move generate into the focused field, render entry chrome actions on every entry, unify top-bar icons to one outlined family, add autofill settings row
- time
- 2026-07-25 00:00 UTC · 3 months ago
- machine
- koh2
- agent
- claude-opus-5
- session
- entry field and top bar
- parent
- 45ee189c2e7e
↓ peach-android-d9f3f7e7.tar.gzatlas at this save
files changed4 files
app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+34 −0app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+32 −2app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+40 −62app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+19 −10app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+34 −0
@@ -53,6 +53,7 @@5353 import androidx.compose.ui.unit.sp5454 import androidx.compose.material3.Text5555 import com.peachpasswords.BuildConfig56+import com.peachpasswords.autofill.AutofillEnablement5657 import com.peachpasswords.ui.PeachNav5758 import com.peachpasswords.ui.components.PeachEmptyOrEndState5859 import com.peachpasswords.ui.components.PeachField@@ -281,6 +282,39 @@281282 modifier = Modifier.padding(start = 2.dp, top = 2.dp, bottom = 4.dp),282283 )283284 }285+ SubLabel("AUTOFILL", fg)286+ run {287+ // Read live rather than cached: the user leaves for system settings and288+ // comes back, so a remembered value would show the old answer.289+ val ctx = LocalContext.current290+ val enabled = AutofillEnablement.isEnabled(ctx)291+ RowLine(fg) {292+ Column(Modifier.weight(1f)) {293+ Text("Fill passwords in other apps", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)294+ Text(295+ if (enabled) {296+ "Peach is your autofill service."297+ } else {298+ "Android decides which app fills passwords. Turn Peach on in system settings."299+ },300+ color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,301+ modifier = Modifier.padding(top = 2.dp),302+ )303+ }304+ if (!enabled) {305+ SubtleButton("enable", fg) {306+ // Straight to the autofill picker with Peach preselected;307+ // some OEM builds ship no handler, so fall back rather than308+ // dead-ending on a button that does nothing.309+ runCatching { ctx.startActivity(AutofillEnablement.requestIntent(ctx)) }310+ .onFailure { runCatching { ctx.startActivity(AutofillEnablement.fallbackIntent()) } }311+ }312+ } else {313+ ValueText("on", fg)314+ }315+ }316+ }317+284318 SubLabel("PASSKEYS", fg)285319 RowLine(fg) {286320 Column(Modifier.weight(1f)) {app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+32 −2
@@ -36,6 +36,8 @@3636 import androidx.compose.foundation.text.BasicTextField3737 import androidx.compose.material.icons.Icons3838 import androidx.compose.material.icons.rounded.Check39+import androidx.compose.material.icons.outlined.Visibility40+import androidx.compose.material.icons.outlined.VisibilityOff3941 import androidx.compose.material.icons.rounded.Close4042 import androidx.compose.material3.DropdownMenu4143 import androidx.compose.material3.DropdownMenuItem@@ -142,6 +144,7 @@142144 var editing by remember(value) { mutableStateOf(false) }143145 var draft by remember(value) { mutableStateOf(value) }144146 var show by remember { mutableStateOf(false) }147+ var reveal by remember(value) { mutableStateOf(false) }145148 146149 fun beginEdit() {147150 val activity = context as? FragmentActivity@@ -160,8 +163,11 @@160163 161164 FieldShell("password") {162165 if (!editing) {166+ // Revealing is not the same act as editing, so it does not go167+ // through the biometric edit gate — the vault is already unlocked168+ // and the value is one tap from the clipboard either way.163169 Text(164- text = "•".repeat(value.length.coerceIn(8, 16)),170+ text = if (reveal) value else "•".repeat(value.length.coerceIn(8, 16)),165171 color = peachColors.T0,166172 style = PeachType.bodyMono,167173 modifier = Modifier.weight(1f).clickable { beginEdit() },@@ -170,8 +176,8 @@170176 "edit", color = accent, style = PeachType.labelMono,171177 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { beginEdit() }.padding(8.dp),172178 )179+ RevealButton(revealed = reveal, onClick = { reveal = !reveal })173180 CopyButton(copied, onCopy)174- WrenchButton(onClick = onGenerate)175181 } else {176182 Column(Modifier.weight(1f)) {177183 Row(verticalAlignment = Alignment.CenterVertically) {@@ -186,6 +192,10 @@186192 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,187193 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),188194 )195+ // Generate belongs to the moment you are choosing a196+ // password, not to the resting row — a wrench parked next197+ // to a saved password invites overwriting it by accident.198+ WrenchButton(onClick = onGenerate)189199 }190200 Spacer(Modifier.height(8.dp))191201 SaveCancelRow(@@ -194,6 +204,26 @@194204 )195205 }196206 }207+ }208+}209+210+/** Eye toggle for the resting password row. */211+@Composable212+private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {213+ Box(214+ Modifier215+ .size(36.dp)216+ .clip(RoundedCornerShape(6.dp))217+ .background(peachColors.S2)218+ .clickable(onClick = onClick),219+ contentAlignment = Alignment.Center,220+ ) {221+ Icon(222+ if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,223+ contentDescription = if (revealed) "hide password" else "reveal password",224+ tint = peachColors.T2,225+ modifier = Modifier.size(15.dp),226+ )197227 }198228 }199229 app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+40 −62
@@ -41,8 +41,10 @@4141 import androidx.compose.material.icons.Icons4242 import androidx.compose.material.icons.automirrored.rounded.ArrowBack4343 import androidx.compose.material.icons.automirrored.rounded.ArrowForward44-import androidx.compose.material.icons.automirrored.outlined.Notes45-import androidx.compose.material.icons.outlined.DeleteOutline44+import androidx.compose.material.icons.outlined.Add45+import androidx.compose.material.icons.outlined.Delete46+import androidx.compose.material.icons.outlined.Description47+import androidx.compose.material.icons.outlined.Settings4648 import androidx.compose.material.icons.outlined.Lock4749 import androidx.compose.material3.Icon4850 import androidx.compose.material3.Text@@ -474,71 +476,47 @@474476 // just below, which already shows its identity clearly.475477 },476478 actions = {477- // This entry is the deck root (no back target) — it's the closest478- // thing this app has to a "home" screen, so it's where the479- // persistent chrome actions live now, mirroring the browser's480- // peach-chrome header exactly: Notes, Settings, New entry, then481- // Lock last/trailing (peach-browser/src/popup/App.tsx ~1347-1408).482- // Delete has no browser-chrome equivalent (it lives in the entry's483- // own view there) — kept here, grouped with the other484- // content-mutating actions just before the terminal Lock action.485- if (onBack == null) {486- Icon(487- Icons.AutoMirrored.Outlined.Notes,488- contentDescription = "Notes",489- tint = peachColors.T1,490- modifier = Modifier491- .size(40.dp)492- .clip(CircleShape)493- .clickable(onClick = onOpenNotes)494- .padding(PeachSpacing.sm),495- )496- Icon(497- Icons.Rounded.Settings,498- contentDescription = "settings",499- tint = peachColors.T1,500- modifier = Modifier501- .size(40.dp)502- .clip(CircleShape)503- .clickable(onClick = onSettings)504- .padding(PeachSpacing.sm),505- )506- Icon(507- Icons.Rounded.Add,508- contentDescription = "new entry",509- tint = peachColors.T1,510- modifier = Modifier511- .size(40.dp)512- .clip(CircleShape)513- .clickable(onClick = onAddEntry)514- .padding(PeachSpacing.sm),515- )516- }479+ // The chrome actions render on EVERY entry, not just the deck root.480+ // They used to be gated on `onBack == null`, so an entry opened from481+ // the vault list showed a bare back-arrow-and-title bar while the482+ // deck root showed the full set — the same surface with two483+ // different bars, which reads as the toolbar breaking mid-swipe.484+ // The browser's header is persistent; this now is too. Order and485+ // family mirror it: Notes, Settings, New entry, Delete, Lock, all486+ // Outlined at one optical size.487+ ChromeAction(Icons.Outlined.Description, "Notes", onClick = onOpenNotes)488+ ChromeAction(Icons.Outlined.Settings, "settings", onClick = onSettings)489+ ChromeAction(Icons.Outlined.Add, "new entry", onClick = onAddEntry)517490 if (entry != null) {518- Icon(519- Icons.Outlined.DeleteOutline,520- contentDescription = "delete",491+ ChromeAction(492+ Icons.Outlined.Delete,493+ "delete",521494 tint = if (confirmingDelete) peachColors.Breach else peachColors.T1,522- modifier = Modifier523- .size(40.dp)524- .clip(CircleShape)525- .clickable(onClick = onDelete)526- .padding(PeachSpacing.sm),495+ onClick = onDelete,527496 )528497 }529- if (onBack == null) {530- Icon(531- Icons.Outlined.Lock,532- contentDescription = "Lock vault",533- tint = peachColors.T1,534- modifier = Modifier535- .size(40.dp)536- .clip(CircleShape)537- .clickable(onClick = onLock)538- .padding(PeachSpacing.sm),539- )540- }498+ ChromeAction(Icons.Outlined.Lock, "Lock vault", onClick = onLock)541499 },500+ )501+}502+503+/** One shape for every top-bar action so the cluster reads as a set. */504+@Composable505+private fun ChromeAction(506+ icon: androidx.compose.ui.graphics.vector.ImageVector,507+ label: String,508+ tint: Color = peachColors.T1,509+ onClick: () -> Unit,510+) {511+ Icon(512+ icon,513+ contentDescription = label,514+ tint = tint,515+ modifier = Modifier516+ .size(38.dp)517+ .clip(CircleShape)518+ .clickable(onClick = onClick)519+ .padding(9.dp),542520 )543521 }544522 app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+19 −10
@@ -36,9 +36,10 @@3636 import androidx.compose.foundation.shape.RoundedCornerShape3737 import androidx.compose.foundation.text.BasicTextField3838 import androidx.compose.material.icons.Icons39-import androidx.compose.material.icons.automirrored.outlined.Notes40-import androidx.compose.material.icons.outlined.DeleteOutline41-import androidx.compose.material.icons.outlined.SelectAll39+import androidx.compose.material.icons.outlined.Checklist40+import androidx.compose.material.icons.outlined.Delete41+import androidx.compose.material.icons.outlined.Description42+import androidx.compose.material.icons.outlined.Settings4243 import androidx.compose.material.icons.rounded.Add4344 import androidx.compose.material.icons.rounded.Close4445 import androidx.compose.material.icons.rounded.Search@@ -561,10 +562,16 @@561562 verticalAlignment = Alignment.CenterVertically,562563 horizontalArrangement = Arrangement.spacedBy(PeachSpacing.xs),563564 ) {565+ // One icon family, one optical size. These were previously a mix566+ // of AutoMirrored.Outlined, Outlined and Rounded, which draw at567+ // different stroke weights and visual densities — side by side in a568+ // pill they read as four unrelated glyphs rather than a set. The569+ // extension's toolbar matches because every glyph there is the same570+ // weight; this is the same discipline.564571 ClusterIcon(565572 icon = {566573 Icon(567- Icons.AutoMirrored.Outlined.Notes,574+ Icons.Outlined.Description,568575 contentDescription = "Notes only",569576 tint = if (noteOnly) accent else peachColors.T1,570577 )@@ -572,16 +579,18 @@572579 onClick = onToggleNote,573580 )574581 ClusterIcon(575- icon = { Icon(Icons.Outlined.SelectAll, contentDescription = "Select entries") },582+ icon = { Icon(Icons.Outlined.Checklist, contentDescription = "Select entries") },576583 onClick = onSelectMode,577584 )578585 ClusterIcon(579- icon = { Icon(Icons.Rounded.Settings, contentDescription = "settings") },580- onClick = onSettings,586+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "trash") },587+ onClick = onTrash,581588 )589+ // Settings last: it is the only app-level action in the cluster,590+ // and the extension likewise keeps it out of the content actions.582591 ClusterIcon(583- icon = { Icon(Icons.Outlined.DeleteOutline, contentDescription = "trash") },584- onClick = onTrash,592+ icon = { Icon(Icons.Outlined.Settings, contentDescription = "settings") },593+ onClick = onSettings,585594 )586595 }587596 }@@ -594,7 +603,7 @@594603 .size(34.dp)595604 .clip(CircleShape)596605 .clickable(onClick = onClick)597- .padding(PeachSpacing.sm),606+ .padding(7.dp),598607 contentAlignment = Alignment.Center,599608 ) {600609 androidx.compose.runtime.CompositionLocalProvider(