a7c248b9b68d
feat: multi-version changelog, locked→yellow indicator, UI polish (v2.9.35)
- time
- 2026-08-02 02:51 UTC · 2 months ago
- machine
- koh2
- parent
- ab1956ea1261
↓ peach-android-a7c248b9.tar.gzatlas at this save
files changed6 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 −0app/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 -> falseapp/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 != toRouteapp/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())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 ) {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"