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
4 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 −10
app/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)) {

view file

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 

view file

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 

view file

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(

view file