f7dad03c0489
feat: Simplify entry detail for thumb reach (v2.14.19)
- time
- 2026-08-14 15:48 UTC · 2 months ago
- machine
- koh2
- agent
- gpt-5.6-sol
- session
- simplify entry detail
- parent
- 24bcf1918eae
↓ peach-android-f7dad03c.tar.gzatlas at this save
files changed4 files
VERSION+1 −1app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+2 −2app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+0 −4app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+124 −170app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+2 −2
@@ -285,10 +285,10 @@285285 },286286 )287287 } else {288- EntryDetailScreen(288+ EntryEditScreen(289289 id = editId,290- onNavigate = nav,291290 onBack = back,291+ onOpenNote = { noteId -> nav(PeachNav.NotesMode(noteId)) },292292 )293293 }294294 }app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+0 −4
@@ -180,10 +180,6 @@180180 style = PeachType.bodyMono,181181 modifier = Modifier.weight(1f).clickable { beginEdit() },182182 )183- Text(184- "edit", color = accent, style = PeachType.labelMono,185- modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { beginEdit() }.padding(8.dp),186- )187183 RevealButton(revealed = reveal, onClick = { reveal = !reveal })188184 CopyButton(copied, onCopy)189185 } else {app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+124 −170
@@ -12,14 +12,12 @@1212 import androidx.compose.foundation.border1313 import androidx.compose.foundation.clickable1414 import androidx.compose.foundation.gestures.detectHorizontalDragGestures15-import androidx.compose.foundation.gestures.detectVerticalDragGestures1615 import androidx.compose.foundation.layout.Arrangement1716 import androidx.compose.foundation.layout.Box1817 import androidx.compose.foundation.layout.Column1918 import androidx.compose.foundation.layout.Row2019 import androidx.compose.foundation.layout.Spacer2120 import androidx.compose.foundation.layout.fillMaxSize22-import androidx.compose.foundation.layout.fillMaxHeight2321 import androidx.compose.foundation.layout.fillMaxWidth2422 import androidx.compose.foundation.layout.height2523 import androidx.compose.foundation.layout.offset@@ -29,7 +27,6 @@2927 import androidx.compose.foundation.layout.statusBarsPadding3028 import androidx.compose.foundation.layout.width3129 import androidx.compose.foundation.rememberScrollState32-import androidx.compose.foundation.shape.CircleShape3330 import androidx.compose.foundation.shape.RoundedCornerShape3431 import androidx.compose.foundation.text.BasicTextField3532 import androidx.compose.material.icons.rounded.Add@@ -41,8 +38,6 @@4138 import androidx.compose.material.icons.Icons4239 import androidx.compose.material.icons.automirrored.rounded.ArrowBack4340 import androidx.compose.material.icons.automirrored.rounded.ArrowForward44-import androidx.compose.material.icons.outlined.Add45-import androidx.compose.material.icons.outlined.Delete4641 import androidx.compose.material.icons.outlined.Description4742 import androidx.compose.material.icons.outlined.Settings4843 import androidx.compose.material.icons.outlined.Lock@@ -81,14 +76,14 @@8176 import com.peachpasswords.ui.components.CharEnterText8277 import com.peachpasswords.ui.components.CopyButton8378 import com.peachpasswords.ui.components.PeachEmptyOrEndState84-import com.peachpasswords.ui.components.PeachMark85-import com.peachpasswords.ui.components.PeachTopBar79+import com.peachpasswords.ui.components.PeachWordmark8680 import com.peachpasswords.ui.components.SecureFlagEffect8781 import com.peachpasswords.ui.components.TOTPRing8882 import com.peachpasswords.ui.lock.Biometrics8983 import com.peachpasswords.ui.theme.LocalPeachAccent9084 import com.peachpasswords.ui.theme.LocalReduceMotion9185 import com.peachpasswords.ui.theme.PeachSpacing86+import com.peachpasswords.ui.theme.PeachRadius9287 import com.peachpasswords.ui.theme.peachColors9388 import com.peachpasswords.ui.theme.PeachType9489 import com.peachpasswords.ui.theme.ShellBackground@@ -154,27 +149,12 @@154149 ) {155150 Column(156151 Modifier.fillMaxSize()157- .padding(bottom = 100.dp)152+ .padding(bottom = 172.dp)158153 .verticalScroll(rememberScrollState())159154 .padding(horizontal = PeachSpacing.xl),160155 ) {161- EntryDetailTopBar(162- entry = e,163- confirmingDelete = confirmingDelete,164- onBack = onBack,165- onLock = vm::lock,166- onDelete = {167- if (confirmingDelete) {168- vm.delete(onDone = onBack ?: { onNavigate(PeachNav.VaultList) })169- } else {170- confirmingDelete = true171- }172- },173- onAddEntry = { onNavigate(PeachNav.EntryEdit(null)) },174- onSettings = { onNavigate(PeachNav.Settings) },175- onOpenNotes = { onNavigate(PeachNav.Notes) },176- )177- Spacer(Modifier.height(PeachSpacing.lg))156+ EntryDetailIdentity()157+ Spacer(Modifier.height(PeachSpacing.xxl))178158 179159 if (e == null) {180160 PeachEmptyOrEndState(@@ -375,6 +355,18 @@375355 draftText = noteEditorText(v),376356 )377357 }358+ EntryDetailActions(359+ confirmingDelete = confirmingDelete,360+ onEdit = { onNavigate(PeachNav.EntryEdit(e.id)) },361+ onDelete = {362+ if (confirmingDelete) {363+ vm.delete(onDone = onBack ?: { onNavigate(PeachNav.VaultList) })364+ } else {365+ confirmingDelete = true366+ }367+ },368+ )369+ Spacer(Modifier.height(PeachSpacing.md))378370 DetailTagRow(379371 tags = e.tags.orEmpty(),380372 allTags = allTags,@@ -415,10 +407,14 @@415407 }416408 417409 if (e != null) {418- EntryDetailSearchStrip(410+ EntryDetailBottomControls(419411 currentIndex = activeEntries.indexOfFirst { it.id == e.id },420412 totalEntries = activeEntries.size,421413 onOpenList = { onNavigate(PeachNav.VaultList) },414+ onAddEntry = { onNavigate(PeachNav.EntryEdit(null)) },415+ onOpenNotes = { onNavigate(PeachNav.Notes) },416+ onSettings = { onNavigate(PeachNav.Settings) },417+ onLock = vm::lock,422418 onPrevious = { vm.adjacentDestination(-1)?.let(onNavigate) },423419 onNext = { vm.adjacentDestination(1)?.let(onNavigate) },424420 modifier = Modifier.align(Alignment.BottomCenter),@@ -438,94 +434,67 @@438434 }439435 440436 @Composable441-private fun EntryDetailTopBar(442- entry: com.peachpasswords.vault.VaultEntry?,437+private fun EntryDetailIdentity(438+) {439+ Row(440+ Modifier.fillMaxWidth().height(64.dp),441+ verticalAlignment = Alignment.CenterVertically,442+ ) {443+ PeachWordmark()444+ Spacer(Modifier.width(PeachSpacing.md))445+ Text("VAULT", color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))446+ }447+}448+449+@Composable450+private fun EntryDetailActions(443451 confirmingDelete: Boolean,444- onBack: (() -> Unit)?,445- onLock: () -> Unit,452+ onEdit: () -> Unit,446453 onDelete: () -> Unit,447- onAddEntry: () -> Unit,448- onSettings: () -> Unit,449- onOpenNotes: () -> Unit,450454 ) {451- val domain = remember(entry?.url, entry?.title) { entry?.displayDomain().orEmpty() }452455 val accent = LocalPeachAccent.current453- PeachTopBar(454- title = when {455- confirmingDelete && entry != null -> "delete \"${entry.title}\"?"456- // Deck root (no back target) mirrors the browser's persistent457- // header, which always shows the "peach" wordmark — never the458- // current entry's identity — leaving PeachMark in `leading` below459- // to complete the wordmark pairing (see LockScreen's same pattern).460- onBack == null -> "peach"461- entry == null -> "entry"462- domain.isNotEmpty() -> domain463- else -> entry.title464- },465- leading = {466- if (onBack != null) {467- Icon(468- Icons.AutoMirrored.Rounded.ArrowBack,469- contentDescription = "back",470- tint = peachColors.T1,471- modifier = Modifier472- .size(40.dp)473- .clip(CircleShape)474- .clickable(onClick = onBack)475- .padding(PeachSpacing.sm),476- )477- } else {478- PeachMark(modifier = Modifier.padding(start = PeachSpacing.sm, end = PeachSpacing.xs))479- }480- // The favicon chip used to sit here, but it's the one loud,481- // colorful element in an otherwise quiet/muted bar (matching the482- // browser's persistent header, which carries no per-entry color483- // at all) — and it's redundant with the entry's own hero section484- // just below, which already shows its identity clearly.485- },486- actions = {487- // The chrome actions render on EVERY entry, not just the deck root.488- // They used to be gated on `onBack == null`, so an entry opened from489- // the vault list showed a bare back-arrow-and-title bar while the490- // deck root showed the full set — the same surface with two491- // different bars, which reads as the toolbar breaking mid-swipe.492- // The browser's header is persistent; this now is too. Order and493- // family mirror it: Notes, Settings, New entry, Delete, Lock, all494- // Outlined at one optical size.495- ChromeAction(Icons.Outlined.Description, "Notes", onClick = onOpenNotes)496- ChromeAction(Icons.Outlined.Settings, "settings", onClick = onSettings)497- ChromeAction(Icons.Outlined.Add, "new entry", onClick = onAddEntry)498- if (entry != null) {499- ChromeAction(500- Icons.Outlined.Delete,501- "delete",502- tint = if (confirmingDelete) peachColors.Breach else peachColors.T1,503- onClick = onDelete,504- )505- }506- ChromeAction(Icons.Outlined.Lock, "Lock vault", tint = accent, onClick = onLock)507- },508- )456+ Row(457+ Modifier.fillMaxWidth().padding(top = PeachSpacing.lg),458+ horizontalArrangement = Arrangement.spacedBy(PeachSpacing.sm),459+ ) {460+ DetailActionButton(461+ label = "edit",462+ color = peachColors.Bg,463+ background = accent,464+ border = accent,465+ onClick = onEdit,466+ modifier = Modifier.weight(1f),467+ )468+ DetailActionButton(469+ label = if (confirmingDelete) "confirm trash" else "move to trash",470+ color = if (confirmingDelete) peachColors.Breach else accent,471+ background = Color.Transparent,472+ border = if (confirmingDelete) peachColors.BreachBorder else peachColors.T3,473+ onClick = onDelete,474+ modifier = Modifier.weight(1f),475+ )476+ }509477 }510478 511-/** One shape for every top-bar action so the cluster reads as a set. */512479 @Composable513-private fun ChromeAction(514- icon: androidx.compose.ui.graphics.vector.ImageVector,480+private fun DetailActionButton(515481 label: String,516- tint: Color = peachColors.T1,482+ color: Color,483+ background: Color,484+ border: Color,517485 onClick: () -> Unit,486+ modifier: Modifier = Modifier,518487 ) {519- Icon(520- icon,521- contentDescription = label,522- tint = tint,523- modifier = Modifier524- .size(38.dp)525- .clip(CircleShape)526- .clickable(onClick = onClick)527- .padding(9.dp),528- )488+ Box(489+ modifier.height(52.dp)490+ .clip(RoundedCornerShape(PeachRadius.Button))491+ .background(background)492+ .border(1.dp, border, RoundedCornerShape(PeachRadius.Button))493+ .clickable(onClick = onClick),494+ contentAlignment = Alignment.Center,495+ ) {496+ Text(label, color = color, style = PeachType.bodyMono)497+ }529498 }530499 531500 @Composable@@ -729,10 +698,14 @@729698 }730699 731700 @Composable732-private fun EntryDetailSearchStrip(701+private fun EntryDetailBottomControls(733702 currentIndex: Int,734703 totalEntries: Int,735704 onOpenList: () -> Unit,705+ onAddEntry: () -> Unit,706+ onOpenNotes: () -> Unit,707+ onSettings: () -> Unit,708+ onLock: () -> Unit,736709 onPrevious: () -> Unit,737710 onNext: () -> Unit,738711 modifier: Modifier = Modifier,@@ -757,55 +730,10 @@757730 .navigationBarsPadding(),758731 ) {759732 Row(760- Modifier761- .fillMaxWidth()762- .padding(horizontal = 16.dp, vertical = 12.dp)763- .height(56.dp)764- .clip(RoundedCornerShape(16.dp))765- .background(peachColors.S1)766- .border(1.dp, peachColors.T3, RoundedCornerShape(16.dp))767- .padding(horizontal = 12.dp),733+ Modifier.fillMaxWidth().height(44.dp),734+ horizontalArrangement = Arrangement.Center,768735 verticalAlignment = Alignment.CenterVertically,769736 ) {770- Row(771- Modifier772- .weight(1f)773- .fillMaxHeight()774- .clickable(onClick = onOpenList)775- .pointerInput(onOpenList) {776- var opened = false777- detectVerticalDragGestures(778- onDragEnd = { opened = false },779- onDragCancel = { opened = false },780- ) { _, dragAmount ->781- if (!opened && dragAmount < -4f) {782- opened = true783- onOpenList()784- }785- }786- },787- verticalAlignment = Alignment.CenterVertically,788- ) {789- Icon(790- Icons.Rounded.Search,791- contentDescription = null,792- tint = accent,793- modifier = Modifier.size(14.dp),794- )795- Spacer(Modifier.width(8.dp))796- Text(797- "search vault...",798- color = peachColors.T2,799- style = PeachType.searchInput,800- modifier = Modifier.weight(1f),801- )802- Text(803- counter,804- color = peachColors.T2,805- style = PeachType.labelMono,806- )807- }808- Spacer(Modifier.width(8.dp))809737 Icon(810738 Icons.AutoMirrored.Rounded.ArrowBack,811739 contentDescription = "Previous entry",@@ -816,6 +744,9 @@816744 .clickable(enabled = hasEntries, onClick = onPrevious)817745 .padding(8.dp),818746 )747+ Spacer(Modifier.width(PeachSpacing.md))748+ Text(counter, color = peachColors.T2, style = PeachType.labelMono)749+ Spacer(Modifier.width(PeachSpacing.md))819750 Spacer(Modifier.width(4.dp))820751 Icon(821752 Icons.AutoMirrored.Rounded.ArrowForward,@@ -828,25 +759,57 @@828759 .padding(8.dp),829760 )830761 }762+ Row(763+ Modifier.fillMaxWidth()764+ .padding(horizontal = PeachSpacing.xl, vertical = PeachSpacing.sm)765+ .height(64.dp)766+ .clip(RoundedCornerShape(PeachRadius.Pill))767+ .background(peachColors.S1)768+ .border(1.dp, peachColors.T3, RoundedCornerShape(PeachRadius.Pill))769+ .padding(horizontal = PeachSpacing.md),770+ horizontalArrangement = Arrangement.SpaceBetween,771+ verticalAlignment = Alignment.CenterVertically,772+ ) {773+ BottomBarAction(Icons.Rounded.Search, "search vault", accent, onOpenList)774+ BottomBarAction(Icons.Rounded.Add, "new entry", peachColors.T0, onAddEntry)775+ BottomBarAction(Icons.Outlined.Description, "Notes", peachColors.T0, onOpenNotes)776+ BottomBarAction(Icons.Outlined.Settings, "settings", peachColors.T0, onSettings)777+ BottomBarAction(Icons.Outlined.Lock, "Lock vault", accent, onLock)778+ }831779 }832780 }833781 }834782 835-/** A field row: uppercase mono label over a value/action row, with a t3 hairline beneath (spec §5.4). */836783 @Composable784+private fun BottomBarAction(785+ icon: androidx.compose.ui.graphics.vector.ImageVector,786+ label: String,787+ tint: Color,788+ onClick: () -> Unit,789+) {790+ Box(791+ Modifier.size(48.dp)792+ .clip(RoundedCornerShape(PeachRadius.IconButton))793+ .clickable(onClick = onClick),794+ contentAlignment = Alignment.Center,795+ ) {796+ Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(22.dp))797+ }798+}799+800+/** A credential field card: uppercase mono label over its value and local actions. */801+@Composable837802 internal fun FieldShell(838803 label: String,839804 content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit,840805 ) {841- val hairline = peachColors.T3842806 Column(843- Modifier.fillMaxWidth().padding(vertical = 10.dp).drawBehind {844- drawRect(845- color = hairline,846- topLeft = Offset(0f, size.height - 1.dp.toPx()),847- size = Size(size.width, 1.dp.toPx()),848- )849- },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+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),850813 ) {851814 Text(label.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))852815 Spacer(Modifier.height(6.dp))@@ -875,13 +838,4 @@875838 val date = java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM)876839 .format(java.util.Date(lastPinged))877840 return "Last triggered: $date · $countLabel"878-}879-880-private fun com.peachpasswords.vault.VaultEntry.displayDomain(): String {881- val raw = url882- if (!raw.isNullOrBlank()) {883- val cleaned = raw.replace(Regex("^https?://"), "").removePrefix("www.").substringBefore('/')884- if (cleaned.isNotEmpty()) return cleaned885- }886- return ""887841 }