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
2 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+139 −82
app/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 {

view file

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 /**

view file