f487da4e7e23
feat: Recompose vault list with design primitives (v2.3.61)
- time
- 2026-07-08 20:48 UTC · 3 months ago
- machine
- koh2
- parent
- 7044e697b520
↓ peach-android-f487da4e.tar.gzatlas at this save
files changed2 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+139 −82app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23600020- versionName "2.3.60"19+ versionCode 23610020+ versionName "2.3.61"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+139 −82
@@ -49,6 +49,7 @@4949 import androidx.compose.material3.SnackbarHost5050 import androidx.compose.material3.SnackbarHostState5151 import androidx.compose.material3.SnackbarResult52+import androidx.compose.material3.Surface5253 import androidx.compose.material3.Text5354 import androidx.compose.runtime.Composable5455 import androidx.compose.runtime.LaunchedEffect@@ -91,8 +92,13 @@9192 import com.peachpasswords.sync.SyncStatus9293 import com.peachpasswords.ui.sync.SyncViewModel9394 import com.peachpasswords.ui.PeachNav95+import com.peachpasswords.ui.components.PeachEmptyOrEndState96+import com.peachpasswords.ui.components.PeachSegment97+import com.peachpasswords.ui.components.PeachSegmentedControl98+import com.peachpasswords.ui.components.PeachTopBar9499 import com.peachpasswords.ui.components.FaviconInitials95100 import com.peachpasswords.ui.theme.LocalPeachAccent101+import com.peachpasswords.ui.theme.PeachSpacing96102 import com.peachpasswords.ui.theme.peachColors97103 import com.peachpasswords.ui.theme.PeachType98104 import com.peachpasswords.ui.theme.ShellBackground@@ -219,6 +225,12 @@219225 }220226 item {221227 NewEntryRow(onClick = { onNavigate(PeachNav.EntryEdit(null)) })228+ if (entries.isNotEmpty()) {229+ PeachEmptyOrEndState(230+ message = "all caught up",231+ modifier = Modifier.padding(top = PeachSpacing.sm),232+ )233+ }222234 Spacer(Modifier.navigationBarsPadding())223235 }224236 }@@ -344,7 +356,6 @@344356 345357 @Composable346358 private fun TopBar(syncStatus: SyncStatus, onSettings: () -> Unit, onAdd: () -> Unit, onLock: () -> Unit) {347- val accent = LocalPeachAccent.current348359 val syncDotColor by animateColorAsState(349360 targetValue = syncStatusDotColor(350361 status = syncStatus,@@ -355,33 +366,43 @@355366 ),356367 label = "sync-dot",357368 )358- // Transparent so the top-left shell bloom reads behind the header (spec §6.3).359- Row(369+ PeachTopBar(370+ title = "peach",360371 modifier = Modifier361- .fillMaxWidth()362372 .statusBarsPadding()363- .height(56.dp)364- .padding(horizontal = 16.dp),365- verticalAlignment = Alignment.CenterVertically,366- horizontalArrangement = Arrangement.SpaceBetween,367- ) {368- Text(text = "peach", color = accent, style = PeachType.wordmark)369- Row(verticalAlignment = Alignment.CenterVertically) {373+ .padding(horizontal = PeachSpacing.md, vertical = PeachSpacing.xs),374+ actions = {370375 Box(Modifier.size(6.dp).clip(CircleShape).background(syncDotColor))371- Spacer(Modifier.width(10.dp))372- Icon(373- Icons.Outlined.Lock, contentDescription = "Lock vault", tint = peachColors.T0,374- modifier = Modifier.size(40.dp).clip(CircleShape).clickable(onClick = onLock).padding(8.dp),376+ TopBarIcon(377+ icon = { Icon(Icons.Outlined.Lock, contentDescription = "Lock vault") },378+ onClick = onLock,375379 )376- Icon(377- Icons.Rounded.Add, contentDescription = "new entry", tint = peachColors.T0,378- modifier = Modifier.size(40.dp).clip(CircleShape).clickable(onClick = onAdd).padding(8.dp),380+ TopBarIcon(381+ icon = { Icon(Icons.Rounded.Add, contentDescription = "new entry") },382+ onClick = onAdd,379383 )380- Icon(381- Icons.Rounded.Settings, contentDescription = "settings", tint = peachColors.T0,382- modifier = Modifier.size(40.dp).clip(CircleShape).clickable(onClick = onSettings).padding(8.dp),384+ TopBarIcon(385+ icon = { Icon(Icons.Rounded.Settings, contentDescription = "settings") },386+ onClick = onSettings,383387 )384- }388+ },389+ )390+}391+392+@Composable393+private fun TopBarIcon(icon: @Composable () -> Unit, onClick: () -> Unit) {394+ Box(395+ modifier = Modifier396+ .size(40.dp)397+ .clip(CircleShape)398+ .clickable(onClick = onClick)399+ .padding(PeachSpacing.sm),400+ contentAlignment = Alignment.Center,401+ ) {402+ androidx.compose.runtime.CompositionLocalProvider(403+ androidx.compose.material3.LocalContentColor provides peachColors.T0,404+ content = icon,405+ )385406 }386407 }387408 @@ -407,15 +428,15 @@407428 Row(408429 Modifier409430 .fillMaxWidth()410- .padding(horizontal = 16.dp, vertical = 10.dp)431+ .padding(horizontal = PeachSpacing.md, vertical = PeachSpacing.sm)411432 .clip(RoundedCornerShape(12.dp))412433 .background(peachColors.S2)413434 .border(1.dp, peachColors.T3, RoundedCornerShape(12.dp))414- .padding(horizontal = 12.dp, vertical = 8.dp),435+ .padding(horizontal = PeachSpacing.md, vertical = PeachSpacing.sm),415436 verticalAlignment = Alignment.CenterVertically,416437 ) {417438 Icon(Icons.Rounded.Search, contentDescription = null, tint = peachColors.T2, modifier = Modifier.size(14.dp))418- Spacer(Modifier.width(10.dp))439+ Spacer(Modifier.width(PeachSpacing.sm))419440 Box(Modifier.weight(1f)) {420441 if (value.isEmpty()) {421442 Text("search vault…", color = peachColors.T2, style = PeachType.searchInput)@@ -447,9 +468,8 @@447468 onHealth: () -> Unit,448469 onTrash: () -> Unit,449470 ) {450- val accent = LocalPeachAccent.current451471 val hairline = peachColors.T3452- Row(472+ Column(453473 modifier = Modifier454474 .fillMaxWidth()455475 .drawBehind {@@ -459,48 +479,87 @@459479 size = Size(size.width, 1.dp.toPx()),460480 )461481 }462- .height(46.dp)463- .padding(horizontal = 16.dp),464- verticalAlignment = Alignment.CenterVertically,482+ .padding(horizontal = PeachSpacing.md, vertical = PeachSpacing.sm),483+ verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),465484 ) {466- SortOrder.entries.forEach { order ->467- val isActive = order == active468- Text(469- text = order.label,470- color = if (isActive) Color.Black else peachColors.T2,471- style = PeachType.sortPill,472- modifier = Modifier473- .padding(end = 6.dp)474- .clip(RoundedCornerShape(4.dp))475- .background(if (isActive) accent else Color.Transparent)476- .clickable { onSort(order) }477- .padding(horizontal = 8.dp, vertical = 3.dp),485+ PeachSegmentedControl(486+ segments = SortOrder.entries.map { PeachSegment(it.name, it.label) },487+ selectedKey = active.name,488+ onSelected = { key ->489+ SortOrder.entries.firstOrNull { it.name == key }?.let(onSort)490+ },491+ )492+ Row(493+ verticalAlignment = Alignment.CenterVertically,494+ horizontalArrangement = Arrangement.spacedBy(PeachSpacing.sm),495+ ) {496+ PeachSegmentedControl(497+ segments = listOf(498+ PeachSegment("all", "All"),499+ PeachSegment("notes", "Notes"),500+ ),501+ selectedKey = if (noteOnly) "notes" else "all",502+ onSelected = { key ->503+ if ((key == "notes") != noteOnly) onToggleNote()504+ },505+ modifier = Modifier.weight(1f),478506 )507+ ActionCluster(508+ onHealth = onHealth,509+ onSelectMode = onSelectMode,510+ onTrash = onTrash,511+ )479512 }480- Text(481- text = "NOTES",482- color = if (noteOnly) Color.Black else peachColors.T2,483- style = PeachType.sortPill,484- modifier = Modifier485- .padding(end = 6.dp)486- .clip(RoundedCornerShape(4.dp))487- .background(if (noteOnly) accent else Color.Transparent)488- .clickable { onToggleNote() }489- .padding(horizontal = 8.dp, vertical = 3.dp),513+ }514+}515+516+@Composable517+private fun ActionCluster(518+ onHealth: () -> Unit,519+ onSelectMode: () -> Unit,520+ onTrash: () -> Unit,521+) {522+ Surface(523+ color = peachColors.S1,524+ contentColor = peachColors.T1,525+ shape = RoundedCornerShape(999.dp),526+ tonalElevation = 1.dp,527+ ) {528+ Row(529+ modifier = Modifier.padding(PeachSpacing.xs),530+ verticalAlignment = Alignment.CenterVertically,531+ horizontalArrangement = Arrangement.spacedBy(PeachSpacing.xs),532+ ) {533+ ClusterIcon(534+ icon = { Icon(Icons.Outlined.FavoriteBorder, contentDescription = "health") },535+ onClick = onHealth,536+ )537+ ClusterIcon(538+ icon = { Icon(Icons.Outlined.SelectAll, contentDescription = "Select entries") },539+ onClick = onSelectMode,540+ )541+ ClusterIcon(542+ icon = { Icon(Icons.Outlined.DeleteOutline, contentDescription = "trash") },543+ onClick = onTrash,544+ )545+ }546+ }547+}548+549+@Composable550+private fun ClusterIcon(icon: @Composable () -> Unit, onClick: () -> Unit) {551+ Box(552+ modifier = Modifier553+ .size(34.dp)554+ .clip(CircleShape)555+ .clickable(onClick = onClick)556+ .padding(PeachSpacing.sm),557+ contentAlignment = Alignment.Center,558+ ) {559+ androidx.compose.runtime.CompositionLocalProvider(560+ androidx.compose.material3.LocalContentColor provides peachColors.T1,561+ content = icon,490562 )491- Spacer(Modifier.weight(1f))492- Icon(493- Icons.Outlined.FavoriteBorder, contentDescription = "health", tint = peachColors.T1,494- modifier = Modifier.size(34.dp).clip(CircleShape).clickable(onClick = onHealth).padding(8.dp),495- )496- Icon(497- Icons.Outlined.SelectAll, contentDescription = "Select entries", tint = peachColors.T1,498- modifier = Modifier.size(34.dp).clip(CircleShape).clickable(onClick = onSelectMode).padding(8.dp),499- )500- Icon(501- Icons.Outlined.DeleteOutline, contentDescription = "trash", tint = peachColors.T1,502- modifier = Modifier.size(34.dp).clip(CircleShape).clickable(onClick = onTrash).padding(8.dp),503- )504563 }505564 }506565 @@ -720,23 +779,21 @@720779 721780 @Composable722781 private fun EmptyState(onSeed: () -> Unit) {723- Column(724- Modifier.fillMaxWidth().padding(top = 80.dp, start = 28.dp, end = 28.dp),725- horizontalAlignment = Alignment.CenterHorizontally,726- ) {727- Text("No entries yet", color = peachColors.T1, style = PeachType.searchInput)728- Spacer(Modifier.height(8.dp))729- Text("add your first entry with +", color = peachColors.T2, style = PeachType.tinyMono)730- if (com.peachpasswords.BuildConfig.DEBUG) {731- Spacer(Modifier.height(20.dp))732- Text(733- "seed demo data",734- color = LocalPeachAccent.current,735- style = PeachType.labelMono,736- modifier = Modifier.clip(RoundedCornerShape(8.dp)).clickable(onClick = onSeed).padding(10.dp),737- )738- }739- }782+ PeachEmptyOrEndState(783+ message = "No entries yet",784+ detail = "add your first entry with +",785+ modifier = Modifier.padding(top = 80.dp),786+ action = {787+ if (com.peachpasswords.BuildConfig.DEBUG) {788+ Text(789+ "seed demo data",790+ color = LocalPeachAccent.current,791+ style = PeachType.labelMono,792+ modifier = Modifier.clip(RoundedCornerShape(8.dp)).clickable(onClick = onSeed).padding(10.dp),793+ )794+ }795+ },796+ )740797 }741798 742799 /**