a7c248b9b68d

feat: multi-version changelog, locked→yellow indicator, UI polish (v2.9.35)

- Changelog now fetches release history from Kepr and shows accumulated notes for every version between last-seen and current, not just the latest. - VAULT_LOCKED sync indicator changed from red (ERROR) to amber (WARN) to match browser parity — a locked vault is transient, not broken. - Settings stack tab titles: reduced vertical padding from 12dp to 8dp for better visual balance in the fanned deck. - Entry detail search strip: restored the 1dp T3 border around the search bar. - Entry-to-entry navigation: replaced jarring slide with a quick 180ms crossfade.

time
2026-08-02 02:51 UTC · 2 months ago
machine
koh2
parent
ab1956ea1261
6 files VERSION+1 −1app/src/main/java/com/peachpasswords/sync/SyncOutcomeReason.kt+2 −1app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+9 −2app/src/main/java/com/peachpasswords/ui/settings/SettingsScreen.kt+1 −1app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+1 −0app/src/main/java/com/peachpasswords/updates/UpdateRepository.kt+48 −0
VERSION+1 −1
@@ -1 +1 @@1-2.9.341+2.9.35

view file

app/src/main/java/com/peachpasswords/sync/SyncOutcomeReason.kt+2 −1
@@ -55,7 +55,6 @@5555         get() = when (this) {5656             NO_ELIGIBLE_HOST,5757             PRIVATE_RELAY_UNSET,58-            VAULT_LOCKED,5958             FOREGROUND_START_BLOCKED,6059             VAULT_ID_MISMATCH,6160             RUN_FAILED,@@ -65,6 +64,7 @@6564             COMPLETED_NO_CHANGES,6665             NO_PAIRED_DEVICES,6766             SYNC_DISABLED,67+            VAULT_LOCKED,6868             RELAY_UNREACHABLE,6969             HANDSHAKE_FAILED,7070             SYNC_TIMEOUT,@@ -83,6 +83,7 @@8383             HANDSHAKE_FAILED,8484             SYNC_TIMEOUT,8585             CONNECTION_LOST,86+            VAULT_LOCKED,8687             -> true8788 8889             else -> false

view file

app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+9 −2
@@ -149,7 +149,9 @@149149             navController = navController,150150             startDestination = startDestination.route,151151             enterTransition = {152-                if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {152+                if (isEntryToEntryTransition(initialState.destination.route, targetState.destination.route)) {153+                    fadeIn(tween(180))154+                } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {153155                     slideInVertically(154156                         initialOffsetY = { it },155157                         animationSpec = tween(PeachMotion.Enter, easing = PeachMotion.Spring),@@ -164,7 +166,9 @@164166                 }165167             },166168             exitTransition = {167-                if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {169+                if (isEntryToEntryTransition(initialState.destination.route, targetState.destination.route)) {170+                    fadeOut(tween(180))171+                } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route)) {168172                     ExitTransition.None169173                 } else if (isVaultSheetTransition(initialState.destination.route, targetState.destination.route, reverse = true)) {170174                     slideOutVertically(@@ -408,3 +412,6 @@408412 409413 internal fun isEntrySurfaceRoute(route: String?): Boolean =410414     route?.startsWith("entry/") == true || route?.startsWith("notes/") == true415+416+internal fun isEntryToEntryTransition(fromRoute: String?, toRoute: String?): Boolean =417+    isEntrySurfaceRoute(fromRoute) && isEntrySurfaceRoute(toRoute) && fromRoute != toRoute

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsScreen.kt+1 −1
@@ -406,7 +406,7 @@406406             ) { onClick() }407407             .padding(408408                 horizontal = if (active) 18.dp else 16.dp,409-                vertical = if (active) 16.dp else 12.dp,409+                vertical = if (active) 16.dp else 8.dp,410410             ),411411     ) {412412         if (showSparkle) SparkleDust(Modifier.fillMaxSize())

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+1 −0
@@ -757,6 +757,7 @@757757                     .height(56.dp)758758                     .clip(RoundedCornerShape(16.dp))759759                     .background(peachColors.S1)760+                    .border(1.dp, peachColors.T3, RoundedCornerShape(16.dp))760761                     .padding(horizontal = 12.dp),761762                 verticalAlignment = Alignment.CenterVertically,762763             ) {

view file

app/src/main/java/com/peachpasswords/updates/UpdateRepository.kt+48 −0
@@ -16,7 +16,11 @@1616 import kotlinx.coroutines.flow.MutableStateFlow1717 import kotlinx.coroutines.flow.StateFlow1818 import kotlinx.coroutines.flow.asStateFlow19+import kotlinx.coroutines.CoroutineScope20+import kotlinx.coroutines.SupervisorJob21+import kotlinx.coroutines.launch1922 import kotlinx.coroutines.withContext23+import org.json.JSONArray2024 import okhttp3.OkHttpClient2125 import okhttp3.Request2226 import java.io.File@@ -59,6 +63,8 @@5963     /** Notes for the running version, set only while they are still unread. */6064     val whatsNew: StateFlow<String?> = _whatsNew.asStateFlow()6165 66+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)67+6268     private val _openRequested = MutableStateFlow(false)6369     /** Set when the app was launched by tapping the update notification. */6470     val openRequested: StateFlow<Boolean> = _openRequested.asStateFlow()@@ -370,8 +376,14 @@370376             _whatsNew.value = null371377             return372378         }379+        // Show cached single-version notes immediately as fallback, then380+        // fetch the full history to accumulate all versions since lastSeen.373381         _whatsNew.value = prefs.getString(KEY_NOTES, null)374382             ?.takeIf { prefs.getString(KEY_NOTES_VERSION, null) == currentVersion }383+        scope.launch {384+            val accumulated = fetchAccumulatedNotes(lastSeen, currentVersion)385+            if (accumulated != null) _whatsNew.value = accumulated386+        }375387     }376388 377389     fun markChangelogSeen() {@@ -394,10 +406,46 @@394406             ?.let { ReleaseManifest.fromCachedJson(it) }395407             ?.takeIf { isNewerVersion(it.version, currentVersion) }396408 409+    /**410+     * Fetch release history from Kepr and build accumulated notes for every411+     * version newer than [lastSeen] up to and including [currentVersion].412+     * Returns null on network failure (caller keeps the fallback).413+     */414+    private suspend fun fetchAccumulatedNotes(lastSeen: String, current: String): String? =415+        withContext(Dispatchers.IO) {416+            runCatching {417+                val request = Request.Builder()418+                    .url(HISTORY_URL)419+                    .header("User-Agent", USER_AGENT)420+                    .build()421+                client.newCall(request).execute().use { response ->422+                    if (!response.isSuccessful) return@runCatching null423+                    val arr = JSONArray(response.body?.string().orEmpty())424+                    // History is newest-first; collect versions in range.425+                    val inRange = mutableListOf<Pair<String, String>>()426+                    for (i in 0 until arr.length()) {427+                        val obj = arr.getJSONObject(i)428+                        val ver = obj.getString("version")429+                        val notes = obj.optString("notes", "")430+                        if (ver == lastSeen) break431+                        if (notes.isNotBlank()) inRange.add(ver to notes)432+                        if (ver == current) break433+                    }434+                    if (inRange.isEmpty()) return@runCatching null435+                    // Reverse to chronological order (oldest first).436+                    inRange.reverse()437+                    inRange.joinToString("\n\n") { (ver, notes) ->438+                        "## v$ver\n\n$notes"439+                    }440+                }441+            }.getOrNull()442+        }443+397444     companion object {398445         /** Kepr repo name for this app; the generic release API is keyed on it. */399446         const val KEPR_REPO = "peach-android"400447         const val LATEST_RELEASE_URL = "https://kepr.uk/api/$KEPR_REPO/releases/latest"448+        const val HISTORY_URL = "https://kepr.uk/api/$KEPR_REPO/releases/history"401449 402450         private const val APK_MIME = "application/vnd.android.package-archive"403451         private const val USER_AGENT = "Peach-Android"

view file