1b4fede31c1d

feat: Recompose entry surfaces with design primitives (v2.3.62)

time
2026-07-08 20:55 UTC · 3 months ago
machine
koh2
parent
f487da4e7e23
4 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/components/PeachTopBar.kt+3 −0app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+71 −45app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+155 −123
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 23610020-        versionName "2.3.61"19+        versionCode 23620020+        versionName "2.3.62"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/ui/components/PeachTopBar.kt+3 −0
@@ -14,6 +14,7 @@1414 import androidx.compose.runtime.Composable1515 import androidx.compose.ui.Alignment1616 import androidx.compose.ui.Modifier17+import androidx.compose.ui.text.style.TextOverflow1718 import androidx.compose.ui.unit.dp1819 import com.peachpasswords.ui.theme.PeachSpacing1920 import com.peachpasswords.ui.theme.PeachType@@ -47,6 +48,8 @@4748                 text = title,4849                 color = peachColors.T0,4950                 style = PeachType.wordmark,51+                maxLines = 1,52+                overflow = TextOverflow.Ellipsis,5053                 modifier = Modifier.weight(1f),5154             )5255             if (actions != null) {

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+71 −45
@@ -76,10 +76,13 @@7676 import com.peachpasswords.ui.components.CharEnterText7777 import com.peachpasswords.ui.components.CopyButton7878 import com.peachpasswords.ui.components.FaviconInitials79+import com.peachpasswords.ui.components.PeachEmptyOrEndState80+import com.peachpasswords.ui.components.PeachTopBar7981 import com.peachpasswords.ui.components.TOTPRing8082 import com.peachpasswords.ui.lock.Biometrics8183 import com.peachpasswords.ui.theme.LocalPeachAccent8284 import com.peachpasswords.ui.theme.LocalReduceMotion85+import com.peachpasswords.ui.theme.PeachSpacing8386 import com.peachpasswords.ui.theme.peachColors8487 import com.peachpasswords.ui.theme.PeachType8588 import com.peachpasswords.ui.theme.ShellBackground@@ -128,52 +131,24 @@128131                 Modifier.fillMaxSize()129132                     .padding(bottom = 100.dp)130133                     .verticalScroll(rememberScrollState())131-                    .padding(horizontal = 24.dp),134+                    .padding(horizontal = PeachSpacing.xl),132135             ) {133-                // Header: back · favicon+domain (or delete confirm) · trash134-                Row(135-                    Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 8.dp),136-                    verticalAlignment = Alignment.CenterVertically,137-                ) {138-                    Icon(139-                        Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0,140-                        modifier = Modifier.size(40.dp).clip(CircleShape).clickable(onClick = onBack).padding(8.dp),141-                    )142-                    Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {143-                        when {144-                            e == null -> {}145-                            confirmingDelete -> Text(146-                                "delete “${e.title}”?",147-                                color = peachColors.Breach,148-                                style = PeachType.labelMono.copy(letterSpacing = 0.08.em),149-                            )150-                            else -> Row(151-                                verticalAlignment = Alignment.CenterVertically,152-                                horizontalArrangement = Arrangement.spacedBy(8.dp),153-                            ) {154-                                FaviconInitials(source = e.url ?: e.title, size = 16.dp)155-                                val domain = remember(e.url, e.title) { e.displayDomain() }156-                                if (domain.isNotEmpty()) {157-                                    Text(domain, color = peachColors.T2, style = TextStyle(fontFamily = PeachType.Mono, fontSize = 9.sp))158-                                }159-                            }160-                        }161-                    }162-                    if (e != null) {163-                        Icon(164-                            Icons.Outlined.DeleteOutline, contentDescription = "delete",165-                            tint = if (confirmingDelete) peachColors.Breach else peachColors.T1,166-                            modifier = Modifier.size(40.dp).clip(CircleShape)167-                                .clickable { if (confirmingDelete) vm.delete(onDone = onBack) else confirmingDelete = true }168-                                .padding(8.dp),169-                        )170-                    }171-                }136+                EntryDetailTopBar(137+                    entry = e,138+                    confirmingDelete = confirmingDelete,139+                    onBack = onBack,140+                    onDelete = {141+                        if (confirmingDelete) vm.delete(onDone = onBack) else confirmingDelete = true142+                    },143+                )144+                Spacer(Modifier.height(PeachSpacing.lg))172145 173146                 if (e == null) {174-                    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {175-                        Text("entry not found", color = peachColors.T1, style = PeachType.bodyMono)176-                    }147+                    PeachEmptyOrEndState(148+                        message = "entry not found",149+                        detail = "It may have been moved or deleted.",150+                        modifier = Modifier.padding(top = PeachSpacing.xxl),151+                    )177152                     return@Column178153                 }179154 @@ -225,7 +200,7 @@225200                         )226201                     }227202                 }228-                Spacer(Modifier.height(20.dp))203+                Spacer(Modifier.height(PeachSpacing.lg))229204 230205                 if (e.isCard()) {231206                     CardDetailFields(@@ -348,7 +323,11 @@348323                     )349324                 }350325 351-                Spacer(Modifier.height(24.dp))326+                PeachEmptyOrEndState(327+                    message = "end of entry",328+                    detail = "All saved fields for this item are above.",329+                    modifier = Modifier.padding(top = PeachSpacing.md, bottom = PeachSpacing.lg),330+                )352331             }353332 354333             if (e != null) {@@ -372,6 +351,53 @@372351             }373352         }374353     }354+}355+356+@Composable357+private fun EntryDetailTopBar(358+    entry: com.peachpasswords.vault.VaultEntry?,359+    confirmingDelete: Boolean,360+    onBack: () -> Unit,361+    onDelete: () -> Unit,362+) {363+    val domain = remember(entry?.url, entry?.title) { entry?.displayDomain().orEmpty() }364+    PeachTopBar(365+        title = when {366+            entry == null -> "entry"367+            confirmingDelete -> "delete \"${entry.title}\"?"368+            domain.isNotEmpty() -> domain369+            else -> entry.title370+        },371+        leading = {372+            Icon(373+                Icons.AutoMirrored.Rounded.ArrowBack,374+                contentDescription = "back",375+                tint = peachColors.T0,376+                modifier = Modifier377+                    .size(40.dp)378+                    .clip(CircleShape)379+                    .clickable(onClick = onBack)380+                    .padding(PeachSpacing.sm),381+            )382+            if (entry != null && !confirmingDelete) {383+                FaviconInitials(source = entry.url ?: entry.title, size = 16.dp)384+            }385+        },386+        actions = {387+            if (entry != null) {388+                Icon(389+                    Icons.Outlined.DeleteOutline,390+                    contentDescription = "delete",391+                    tint = if (confirmingDelete) peachColors.Breach else peachColors.T1,392+                    modifier = Modifier393+                        .size(40.dp)394+                        .clip(CircleShape)395+                        .clickable(onClick = onDelete)396+                        .padding(PeachSpacing.sm),397+                )398+            }399+        },400+    )375401 }376402 377403 @Composable

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+155 −123
@@ -34,7 +34,6 @@3434 import androidx.compose.ui.draw.clipToBounds3535 import androidx.compose.ui.graphics.Color3636 import androidx.compose.ui.text.TextStyle37-import androidx.compose.ui.text.font.FontStyle3837 import androidx.compose.ui.text.font.FontWeight3938 import androidx.compose.ui.text.input.PasswordVisualTransformation4039 import androidx.compose.ui.text.input.VisualTransformation@@ -45,13 +44,18 @@4544 import androidx.compose.runtime.collectAsState4645 import androidx.hilt.navigation.compose.hiltViewModel4746 import com.peachpasswords.ui.components.CopyButton47+import com.peachpasswords.ui.components.PeachEmptyOrEndState48+import com.peachpasswords.ui.components.PeachSegment49+import com.peachpasswords.ui.components.PeachSegmentedControl50+import com.peachpasswords.ui.components.PeachTopBar4851 import com.peachpasswords.ui.components.SecureFlagEffect4952 import com.peachpasswords.ui.theme.LocalPeachAccent50-import com.peachpasswords.ui.theme.peachColors5153 import com.peachpasswords.ui.theme.PeachMotion5254 import com.peachpasswords.ui.theme.PeachRadius55+import com.peachpasswords.ui.theme.PeachSpacing5356 import com.peachpasswords.ui.theme.PeachType5457 import com.peachpasswords.ui.theme.ShellBackground58+import com.peachpasswords.ui.theme.peachColors5559 5660 @Composable5761 fun EntryEditScreen(@@ -64,7 +68,6 @@6468     val f by vm.form.collectAsState()6569     val usernameSuggestions by vm.usernameSuggestions.collectAsState()6670     val emailSuggestions by vm.emailSuggestions.collectAsState()67-    val accent = LocalPeachAccent.current6871 6972     // Ghost watermark fades in (0 → 0.03α over 300ms) once the title has any text.7073     val watermarkAlpha by androidx.compose.animation.core.animateFloatAsState(@@ -74,20 +77,22 @@7477     )7578 7679     ShellBackground {77-        Column(Modifier.fillMaxSize().statusBarsPadding()) {78-            Box(Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 18.dp)) {79-                val canSave = canSaveEditForm(f)80-                Text("cancel", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 11.sp,81-                    modifier = Modifier.align(Alignment.CenterStart)82-                        .clip(RoundedCornerShape(8.dp)).clickable(onClick = onBack).padding(8.dp))83-                Text(editTitleBarLabel(vm.isNew, f.type), color = peachColors.T1,84-                    fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp,85-                    modifier = Modifier.align(Alignment.Center))86-                Text(entryEditPrimaryActionLabel(vm.isNew, f.saving), color = if (canSave && !f.saving) accent else peachColors.T2, fontFamily = PeachType.Mono, fontSize = 11.sp,87-                    modifier = Modifier.align(Alignment.CenterEnd)88-                        .clip(RoundedCornerShape(8.dp)).clickable(enabled = canSave && !f.saving) { vm.save(onBack) }.padding(8.dp))89-            }80+        Column(81+            Modifier82+                .fillMaxSize()83+                .statusBarsPadding()84+                .padding(horizontal = PeachSpacing.xl),85+        ) {86+            EntryEditTopBar(87+                title = editTitleBarLabel(vm.isNew, f.type),88+                saveLabel = entryEditPrimaryActionLabel(vm.isNew, f.saving),89+                canSave = canSaveEditForm(f) && !f.saving,90+                onCancel = onBack,91+                onSave = { vm.save(onBack) },92+            )9093 94+            Spacer(Modifier.height(PeachSpacing.lg))95+9196             Box(Modifier.fillMaxSize().clipToBounds()) {9297                 // Ghost PeachScript watermark behind the form (matches the detail hero):9398                 // the full title, fading in as the user types.@@ -103,132 +108,153 @@103108                     )104109                 }105110                 Column(106-                    Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 22.dp),111+                    Modifier.fillMaxSize().verticalScroll(rememberScrollState()),107112                 ) {108-                Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {109-                    listOf("login", "note", "card").forEach { t ->110-                        EntryTypePill(t, f.type == t) {113+                    PeachSegmentedControl(114+                        segments = listOf(115+                            PeachSegment("login", "login"),116+                            PeachSegment("note", "note"),117+                            PeachSegment("card", "card"),118+                        ),119+                        selectedKey = f.type,120+                        onSelected = { t ->111121                             // A brand-new note skips the form entirely and drops straight112122                             // into the full-screen notes editor (brief Fix 6 item 7).113123                             if (shouldOpenNotesMode(vm.isNew, t)) vm.createNoteAndOpen(onOpenNote)114124                             else vm.setType(t)125+                        },126+                    )127+                    Spacer(Modifier.height(PeachSpacing.lg))128+129+                    Field("title", f.title, vm::setTitle)130+                    if (f.type == "login") {131+                        Field("url", f.url, vm::setUrl)132+                        AdditionalUrlsEditor(133+                            urls = f.additionalUrls,134+                            onChange = vm::setAdditionalUrl,135+                            onAdd = vm::addAdditionalUrl,136+                            onRemove = vm::removeAdditionalUrl,137+                        )138+                        IdentitySuggestField("username", f.username, usernameSuggestions, vm::setUsername)139+                        IdentitySuggestField("email", f.email, emailSuggestions, vm::setEmail)140+                        PasswordField(141+                            value = f.password,142+                            show = f.showPassword,143+                            generatorOpen = f.generatorOpen,144+                            onChange = vm::setPassword,145+                            onToggleShow = vm::toggleShowPassword,146+                            onToggleGenerator = vm::toggleGenerator,147+                        )148+                        GeneratorDrawer(149+                            open = f.generatorOpen,150+                            length = f.genLength,151+                            upper = f.genUpper, lower = f.genLower, numbers = f.genNumbers, symbols = f.genSymbols,152+                            password = f.password,153+                            copied = f.genCopied,154+                            onLength = vm::setGenLength,155+                            onUpper = vm::setGenUpper, onLower = vm::setGenLower,156+                            onNumbers = vm::setGenNumbers, onSymbols = vm::setGenSymbols,157+                            onGenerate = vm::generate,158+                            onCopy = vm::copyGenerated,159+                            onUse = vm::useGeneratedPassword,160+                        )161+                        PasswordHistorySection(162+                            history = f.history,163+                            expanded = f.showHistory,164+                            onToggle = vm::toggleHistory,165+                            onRestore = vm::restorePassword,166+                            onCopy = vm::copyHistoryPassword,167+                        )168+                        if (shouldShowLoginTotp(f.type, f.showTotp, f.totp)) {169+                            Field("2FA secret (otpauth:// or base32)", f.totp, vm::setTotp)115170                         }116171                     }117-                }118-                Spacer(Modifier.height(18.dp))119-120-                Field("title", f.title, vm::setTitle)121-                if (f.type == "login") {122-                    Field("url", f.url, vm::setUrl)123-                    AdditionalUrlsEditor(124-                        urls = f.additionalUrls,125-                        onChange = vm::setAdditionalUrl,126-                        onAdd = vm::addAdditionalUrl,127-                        onRemove = vm::removeAdditionalUrl,172+                    if (f.type == "card") {173+                        CardEditFields(174+                            cardNickname = f.cardNickname,175+                            cardNumber = f.cardNumber,176+                            cardholderName = f.cardholderName,177+                            expiry = f.expiry,178+                            cvc = f.cvc,179+                            showCvc = f.showCvc,180+                            network = f.cardNetwork,181+                            onCardNickname = vm::setCardNickname,182+                            onCardNumber = vm::setCardNumber,183+                            onCardholder = vm::setCardholderName,184+                            onExpiry = vm::setExpiry,185+                            onCvc = vm::setCvc,186+                            onToggleShowCvc = vm::toggleShowCvc,187+                        )188+                    }189+                    if (f.type == "note" || shouldShowLoginNotes(f.type, f.showNotes, f.notes)) {190+                        Field("notes", f.notes, vm::setNotes, singleLine = false)191+                    }192+                    if (f.type == "login") {193+                        OptionalLoginAddLinks(194+                            showTotpLink = shouldShowAddTotpLink(f.type, f.showTotp, f.totp),195+                            showNotesLink = shouldShowAddNotesLink(f.type, f.showNotes, f.notes),196+                            onAddTotp = vm::revealTotp,197+                            onAddNotes = vm::revealNotes,198+                        )199+                    }200+                    TagChipEditor(201+                        tags = f.tags,202+                        allTags = vm.allTags,203+                        onAdd = vm::addTag,204+                        onRemove = vm::removeTag,128205                     )129-                    IdentitySuggestField("username", f.username, usernameSuggestions, vm::setUsername)130-                    IdentitySuggestField("email", f.email, emailSuggestions, vm::setEmail)131-                    PasswordField(132-                        value = f.password,133-                        show = f.showPassword,134-                        generatorOpen = f.generatorOpen,135-                        onChange = vm::setPassword,136-                        onToggleShow = vm::toggleShowPassword,137-                        onToggleGenerator = vm::toggleGenerator,138-                    )139-                    GeneratorDrawer(140-                        open = f.generatorOpen,141-                        length = f.genLength,142-                        upper = f.genUpper, lower = f.genLower, numbers = f.genNumbers, symbols = f.genSymbols,143-                        password = f.password,144-                        copied = f.genCopied,145-                        onLength = vm::setGenLength,146-                        onUpper = vm::setGenUpper, onLower = vm::setGenLower,147-                        onNumbers = vm::setGenNumbers, onSymbols = vm::setGenSymbols,148-                        onGenerate = vm::generate,149-                        onCopy = vm::copyGenerated,150-                        onUse = vm::useGeneratedPassword,151-                    )152-                    PasswordHistorySection(153-                        history = f.history,154-                        expanded = f.showHistory,155-                        onToggle = vm::toggleHistory,156-                        onRestore = vm::restorePassword,157-                        onCopy = vm::copyHistoryPassword,158-                    )159-                    if (shouldShowLoginTotp(f.type, f.showTotp, f.totp)) {160-                        Field("2FA secret (otpauth:// or base32)", f.totp, vm::setTotp)206+207+                    f.error?.let {208+                        Spacer(Modifier.height(PeachSpacing.sm))209+                        Text(it, color = peachColors.Breach, fontFamily = PeachType.Mono, fontSize = 11.sp)161210                     }162-                }163-                if (f.type == "card") {164-                    CardEditFields(165-                        cardNickname = f.cardNickname,166-                        cardNumber = f.cardNumber,167-                        cardholderName = f.cardholderName,168-                        expiry = f.expiry,169-                        cvc = f.cvc,170-                        showCvc = f.showCvc,171-                        network = f.cardNetwork,172-                        onCardNickname = vm::setCardNickname,173-                        onCardNumber = vm::setCardNumber,174-                        onCardholder = vm::setCardholderName,175-                        onExpiry = vm::setExpiry,176-                        onCvc = vm::setCvc,177-                        onToggleShowCvc = vm::toggleShowCvc,211+                    PeachEmptyOrEndState(212+                        message = editEndStateMessage(f.type),213+                        detail = "Review the fields above, then save from the top bar.",214+                        modifier = Modifier.padding(top = PeachSpacing.md, bottom = PeachSpacing.xl),178215                     )179216                 }180-                if (f.type == "note" || shouldShowLoginNotes(f.type, f.showNotes, f.notes)) {181-                    Field("notes", f.notes, vm::setNotes, singleLine = false)182-                }183-                if (f.type == "login") {184-                    OptionalLoginAddLinks(185-                        showTotpLink = shouldShowAddTotpLink(f.type, f.showTotp, f.totp),186-                        showNotesLink = shouldShowAddNotesLink(f.type, f.showNotes, f.notes),187-                        onAddTotp = vm::revealTotp,188-                        onAddNotes = vm::revealNotes,189-                    )190-                }191-                TagChipEditor(192-                    tags = f.tags,193-                    allTags = vm.allTags,194-                    onAdd = vm::addTag,195-                    onRemove = vm::removeTag,196-                )197-198-                f.error?.let {199-                    Spacer(Modifier.height(8.dp))200-                    Text(it, color = peachColors.Breach, fontFamily = PeachType.Mono, fontSize = 11.sp)201-                }202-                Spacer(Modifier.height(40.dp))203-                }204217             }205218         }206219     }207220 }208221 209222 @Composable210-private fun EntryTypePill(type: String, selected: Boolean, onClick: () -> Unit) {223+private fun EntryEditTopBar(224+    title: String,225+    saveLabel: String,226+    canSave: Boolean,227+    onCancel: () -> Unit,228+    onSave: () -> Unit,229+) {211230     val accent = LocalPeachAccent.current212-    Box(213-        modifier = Modifier214-            .clip(RoundedCornerShape(PeachRadius.TypeTab))215-            .border(216-                1.dp,217-                if (selected) accent.copy(alpha = 0.5f) else peachColors.T3,218-                RoundedCornerShape(PeachRadius.TypeTab),231+    PeachTopBar(232+        title = title,233+        leading = {234+            Text(235+                "cancel",236+                color = peachColors.T2,237+                fontFamily = PeachType.Mono,238+                fontSize = 11.sp,239+                modifier = Modifier240+                    .clip(RoundedCornerShape(8.dp))241+                    .clickable(onClick = onCancel)242+                    .padding(PeachSpacing.sm),219243             )220-            .background(if (selected) accent.copy(alpha = 0.08f) else Color.Transparent)221-            .clickable(onClick = onClick)222-            .padding(horizontal = 12.dp, vertical = 5.dp),223-    ) {224-        Text(225-            text = type.uppercase(),226-            color = if (selected) accent else peachColors.T2,227-            fontFamily = PeachType.Mono,228-            fontSize = 9.sp,229-            letterSpacing = 0.08.em,230-        )231-    }244+        },245+        actions = {246+            Text(247+                saveLabel,248+                color = if (canSave) accent else peachColors.T2,249+                fontFamily = PeachType.Mono,250+                fontSize = 11.sp,251+                modifier = Modifier252+                    .clip(RoundedCornerShape(8.dp))253+                    .clickable(enabled = canSave, onClick = onSave)254+                    .padding(PeachSpacing.sm),255+            )256+        },257+    )232258 }233259 234260 /** Centered title-bar label: "new entry" / "new note" / "new card", or "edit". */@@ -256,6 +282,12 @@256282 257283 /** Target alpha for the edit-screen ghost watermark — visible only once a title exists. */258284 internal fun editGhostWatermarkTarget(title: String): Float = if (title.isNotBlank()) 0.03f else 0f285+286+internal fun editEndStateMessage(type: String): String = when (type) {287+    "card" -> "card details ready"288+    "note" -> "note details ready"289+    else -> "login details ready"290+}259291 260292 internal fun shouldShowLoginTotp(type: String, revealed: Boolean, totp: String): Boolean =261293     type == "login" && (revealed || totp.isNotBlank())

view file