feat(slint): seed per-row offline status from the index.db on load

A cached track now shows its "ready" check when a view is built, persisting
across navigation within the session (was: only live downloads showed state).

- offline_cache.rs: a session-wide CACHED_IDS set (mirrors fav_cache), seeded
  from index.db ready rows via load_cached_ids() on login/restore, kept in
  sync as downloads complete (mark_cached true) and copies are removed (false).
  is_cached(id) reads it.
- The 7 TrackItem builders seed cache-status from is_cached (favorites/search
  clone/precompute the id to avoid moving it before the borrow).
- auth.rs calls load_cached_ids after the offline cache activates.

qbz-slint compiles clean.

cc
This commit is contained in:
vicrodh committed 2026-05-28 09:01:06 -06:00
1 parent 43c1599400
commit 6bea6a7a0a
9 files changed
+74 -10

No files matched your search

+1 -1
View File
@@ -257,7 +257,7 @@ pub fn apply_album(window: &AppWindow, data: AlbumData) {
artist_id: track.artist_id.into(),
album_id: album_id.clone(),
removing: false,
cache_status: 0,
cache_status: if crate::offline_cache::is_cached(&track.id) { 3 } else { 0 },
cache_progress: 0.0,
unlocking: false,
})
+1 -1
View File
@@ -503,7 +503,7 @@ pub fn apply_artist(window: &AppWindow, data: ArtistData) {
artist_id: track.artist_id.into(),
album_id: track.album_id.into(),
removing: false,
cache_status: 0,
cache_status: if crate::offline_cache::is_cached(&track.id) { 3 } else { 0 },
cache_progress: 0.0,
unlocking: false,
})
+2
View File
@@ -89,6 +89,7 @@ where
// Bring up the per-user offline cache (shared index.db + library.db with Tauri).
crate::offline::activate(user_id).await;
crate::offline_cache::load_cached_ids().await;
// Persist the token so the next launch restores the session silently.
if let Err(e) = qbz_credentials::save_oauth_token(&token) {
@@ -137,6 +138,7 @@ where
core.set_session(session).await.map_err(|e| e.to_string())?;
runtime.activate(user_id).await?;
crate::offline::activate(user_id).await;
crate::offline_cache::load_cached_ids().await;
log::info!("[qbz-slint] restored saved session for user {user_id}");
Ok(Some(SessionInfo {
user_id,
+2 -2
View File
@@ -402,7 +402,7 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
let rows: Vec<TrackItem> = items
.into_iter()
.map(|t| TrackItem {
id: t.id.into(),
id: t.id.clone().into(),
number: "".into(),
title: t.title.into(),
artist: t.artist.into(),
@@ -419,7 +419,7 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
artist_id: t.artist_id.into(),
album_id: t.album_id.into(),
removing: false,
cache_status: 0,
cache_status: if crate::offline_cache::is_cached(&t.id) { 3 } else { 0 },
cache_progress: 0.0,
unlocking: false,
})
+1 -1
View File
@@ -763,7 +763,7 @@ fn top_track_to_item(t: &TopTrack) -> TrackItem {
artist_id: t.artist_id.clone().into(),
album_id: t.album_id.clone().into(),
removing: false,
cache_status: 0,
cache_status: if crate::offline_cache::is_cached(&t.id) { 3 } else { 0 },
cache_progress: 0.0,
unlocking: false,
}
+1 -1
View File
@@ -175,7 +175,7 @@ fn to_item(track: &Track) -> TrackItem {
.unwrap_or_default()
.into(),
removing: false,
cache_status: 0,
cache_status: if crate::offline_cache::is_cached(&track.id.to_string()) { 3 } else { 0 },
cache_progress: 0.0,
unlocking: false,
}
+63 -2
View File
@@ -8,16 +8,75 @@
//! The heavy lifting (download pipeline, CMAF store, vault) lives in the
//! shared `qbz-offline-cache` crate; this is the thin Slint orchestration.
use std::sync::Arc;
use std::collections::HashSet;
use std::sync::{Arc, Mutex as StdMutex, OnceLock};
use qbz_app::shell::AppRuntime;
use qbz_offline_cache::{CacheEvent, CacheEventSink, TrackCacheInfo};
use qbz_offline_cache::{CacheEvent, CacheEventSink, OfflineCacheStatus, TrackCacheInfo};
use crate::adapter::SlintAdapter;
use crate::AppWindow;
type Runtime = Arc<AppRuntime<SlintAdapter>>;
/// Session-wide set of track ids that have a READY offline copy. Seeded from
/// the index.db on login (`load_cached_ids`) and kept in sync as downloads
/// complete / copies are removed. Read at row-build time to seed each row's
/// cache-status (mirrors `fav_cache`), so a cached track shows its check when
/// the view is revisited without re-querying the DB per row.
static CACHED_IDS: OnceLock<StdMutex<HashSet<u64>>> = OnceLock::new();
fn cached_ids() -> &'static StdMutex<HashSet<u64>> {
CACHED_IDS.get_or_init(|| StdMutex::new(HashSet::new()))
}
/// True if `track_id` (string form, as the row carries it) has a ready offline
/// copy. Used to seed `TrackItem.cache-status` when building track lists.
pub fn is_cached(track_id: &str) -> bool {
let Ok(id) = track_id.parse::<u64>() else {
return false;
};
cached_ids().lock().map(|s| s.contains(&id)).unwrap_or(false)
}
fn mark_cached(track_id: u64, cached: bool) {
if let Ok(mut set) = cached_ids().lock() {
if cached {
set.insert(track_id);
} else {
set.remove(&track_id);
}
}
}
/// Seed the ready-set from the active offline cache's index.db. Called once
/// after the offline cache is activated (login / session restore).
pub async fn load_cached_ids() {
let Some(off) = crate::offline::get().await else {
return;
};
let ids: Vec<u64> = {
let guard = off.db.lock().await;
match guard.as_ref() {
Some(db) => db
.get_all_tracks()
.map(|tracks| {
tracks
.into_iter()
.filter(|t| matches!(t.status, OfflineCacheStatus::Ready))
.map(|t| t.track_id)
.collect()
})
.unwrap_or_default(),
None => Vec::new(),
}
};
if let Ok(mut set) = cached_ids().lock() {
*set = ids.into_iter().collect();
log::info!("[qbz-slint] offline: seeded {} cached track ids", set.len());
}
}
/// Build a sink that reflects cache + unlock events onto every visible row
/// matching the event's track id (and surfaces terminal toasts). Shared by
/// the cache trigger AND the play path (UnlockStart/End → padlock).
@@ -35,6 +94,7 @@ pub fn row_sink(weak: slint::Weak<AppWindow>) -> CacheEventSink {
push_status(&weak, track_id, 2, p);
}
CacheEvent::Completed { track_id, .. } => {
mark_cached(track_id, true);
push_status(&weak, track_id, 3, 1.0);
crate::toast::success_weak(&weak, "Cached for offline");
}
@@ -261,6 +321,7 @@ pub fn remove_cached(
let _ = db.remove_qobuz_cached_track(id);
}
}
mark_cached(id, false);
push_status(&weak, id, 0, 0.0);
crate::toast::success_weak(&weak, "Removed from offline");
});
+1 -1
View File
@@ -382,7 +382,7 @@ fn to_item(track: &Track) -> TrackItem {
.unwrap_or_default()
.into(),
removing: false,
cache_status: 0,
cache_status: if crate::offline_cache::is_cached(&track.id.to_string()) { 3 } else { 0 },
cache_progress: 0.0,
unlocking: false,
}
+2 -1
View File
@@ -356,6 +356,7 @@ fn album_item(row: AlbumRow) -> AlbumCardItem {
fn track_item(row: TrackRowData) -> TrackItem {
let is_favorite = crate::fav_cache::is_favorite(&row.id);
let is_cached = crate::offline_cache::is_cached(&row.id);
TrackItem {
id: row.id.into(),
number: "".into(),
@@ -372,7 +373,7 @@ fn track_item(row: TrackRowData) -> TrackItem {
artist_id: row.artist_id.into(),
album_id: row.album_id.into(),
removing: false,
cache_status: 0,
cache_status: if is_cached { 3 } else { 0 },
cache_progress: 0.0,
unlocking: false,
}