diff --git a/app/src/main/java/com/marcus/meditationtimer/MainActivity.kt b/app/src/main/java/com/marcus/meditationtimer/MainActivity.kt index 2925ce0..a5ae4a2 100644 --- a/app/src/main/java/com/marcus/meditationtimer/MainActivity.kt +++ b/app/src/main/java/com/marcus/meditationtimer/MainActivity.kt @@ -342,6 +342,9 @@ private fun HistoryTab(onEditSession: (Session) -> Unit, onOpenToken: () -> Unit val sessions by SessionStore.sessions.collectAsState() val pending by SessionStore.pendingSync.collectAsState() + // Pull the server history (incl. legacy Jhana-app sessions) on open. + LaunchedEffect(Unit) { SessionStore.refresh() } + Column( Modifier .fillMaxSize() diff --git a/app/src/main/java/com/marcus/meditationtimer/SessionStore.kt b/app/src/main/java/com/marcus/meditationtimer/SessionStore.kt index e366781..a42aa46 100644 --- a/app/src/main/java/com/marcus/meditationtimer/SessionStore.kt +++ b/app/src/main/java/com/marcus/meditationtimer/SessionStore.kt @@ -55,6 +55,7 @@ object SessionStore { publish(load()) } syncPending() + refresh() } var token: String @@ -93,6 +94,68 @@ object SessionStore { } } + /** + * Pull the full server history (incl. sessions from other apps/devices, + * e.g. the legacy Jhana app) and merge it into the local log. Local + * sessions still awaiting push (synced=false) always win over the server + * copy; everything else takes the server's version. + */ + fun refresh() { + val tok = token + if (tok.isEmpty()) return + scope.launch { + val remote = fetchSessions(tok) ?: return@launch + synchronized(lock) { + val local = load().associateBy { it.id } + val merged = LinkedHashMap(local) + for (r in remote) { + val l = local[r.id] + if (l == null || l.synced) { + // Keep any locally-entered rating/note if the server has none. + merged[r.id] = r.copy( + rating = r.rating ?: l?.rating, + note = r.note ?: l?.note, + synced = true, + ) + } + } + persist(merged.values.toList()) + } + } + } + + private fun fetchSessions(tok: String): List? = try { + val conn = URL("$BASE_URL/v1/sessions?limit=1000").openConnection() as HttpURLConnection + conn.requestMethod = "GET" + conn.setRequestProperty("Authorization", "Bearer $tok") + conn.connectTimeout = 10_000 + conn.readTimeout = 10_000 + val body = if (conn.responseCode in 200..299) { + conn.inputStream.bufferedReader().use { it.readText() } + } else null + conn.disconnect() + body?.let { + val arr = JSONObject(it).getJSONArray("sessions") + (0 until arr.length()).map { i -> + val o = arr.getJSONObject(i) + Session( + id = o.getString("id"), + startedAt = o.getString("startedAt"), + endedAt = o.optString("endedAt").ifEmpty { null }, + plannedMin = o.optInt("plannedMin", 0), + actualMin = o.optInt("actualMin", 0), + completed = o.optBoolean("completed", false), + pauseCount = o.optInt("pauseCount", 0), + rating = if (o.isNull("rating")) null else o.optInt("rating"), + note = o.optString("note").ifEmpty { null }, + synced = true, + ) + } + } + } catch (_: Exception) { + null + } + private fun postSession(s: Session, tok: String): Boolean = try { val conn = URL("$BASE_URL/v1/sessions").openConnection() as HttpURLConnection conn.requestMethod = "POST"