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:
9 files changed
+74
-10
No files matched your search
@@ -257,7 +257,7 @@ pub fn apply_album(window: &AppWindow, data: AlbumData) {
|
|||||||
artist_id: track.artist_id.into(),
|
artist_id: track.artist_id.into(),
|
||||||
album_id: album_id.clone(),
|
album_id: album_id.clone(),
|
||||||
removing: false,
|
removing: false,
|
||||||
cache_status: 0,
|
cache_status: if crate::offline_cache::is_cached(&track.id) { 3 } else { 0 },
|
||||||
cache_progress: 0.0,
|
cache_progress: 0.0,
|
||||||
unlocking: false,
|
unlocking: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -503,7 +503,7 @@ pub fn apply_artist(window: &AppWindow, data: ArtistData) {
|
|||||||
artist_id: track.artist_id.into(),
|
artist_id: track.artist_id.into(),
|
||||||
album_id: track.album_id.into(),
|
album_id: track.album_id.into(),
|
||||||
removing: false,
|
removing: false,
|
||||||
cache_status: 0,
|
cache_status: if crate::offline_cache::is_cached(&track.id) { 3 } else { 0 },
|
||||||
cache_progress: 0.0,
|
cache_progress: 0.0,
|
||||||
unlocking: false,
|
unlocking: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ where
|
|||||||
|
|
||||||
// Bring up the per-user offline cache (shared index.db + library.db with Tauri).
|
// Bring up the per-user offline cache (shared index.db + library.db with Tauri).
|
||||||
crate::offline::activate(user_id).await;
|
crate::offline::activate(user_id).await;
|
||||||
|
crate::offline_cache::load_cached_ids().await;
|
||||||
|
|
||||||
// Persist the token so the next launch restores the session silently.
|
// Persist the token so the next launch restores the session silently.
|
||||||
if let Err(e) = qbz_credentials::save_oauth_token(&token) {
|
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())?;
|
core.set_session(session).await.map_err(|e| e.to_string())?;
|
||||||
runtime.activate(user_id).await?;
|
runtime.activate(user_id).await?;
|
||||||
crate::offline::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}");
|
log::info!("[qbz-slint] restored saved session for user {user_id}");
|
||||||
Ok(Some(SessionInfo {
|
Ok(Some(SessionInfo {
|
||||||
user_id,
|
user_id,
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
|
|||||||
let rows: Vec<TrackItem> = items
|
let rows: Vec<TrackItem> = items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| TrackItem {
|
.map(|t| TrackItem {
|
||||||
id: t.id.into(),
|
id: t.id.clone().into(),
|
||||||
number: "".into(),
|
number: "".into(),
|
||||||
title: t.title.into(),
|
title: t.title.into(),
|
||||||
artist: t.artist.into(),
|
artist: t.artist.into(),
|
||||||
@@ -419,7 +419,7 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
|
|||||||
artist_id: t.artist_id.into(),
|
artist_id: t.artist_id.into(),
|
||||||
album_id: t.album_id.into(),
|
album_id: t.album_id.into(),
|
||||||
removing: false,
|
removing: false,
|
||||||
cache_status: 0,
|
cache_status: if crate::offline_cache::is_cached(&t.id) { 3 } else { 0 },
|
||||||
cache_progress: 0.0,
|
cache_progress: 0.0,
|
||||||
unlocking: false,
|
unlocking: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -763,7 +763,7 @@ fn top_track_to_item(t: &TopTrack) -> TrackItem {
|
|||||||
artist_id: t.artist_id.clone().into(),
|
artist_id: t.artist_id.clone().into(),
|
||||||
album_id: t.album_id.clone().into(),
|
album_id: t.album_id.clone().into(),
|
||||||
removing: false,
|
removing: false,
|
||||||
cache_status: 0,
|
cache_status: if crate::offline_cache::is_cached(&t.id) { 3 } else { 0 },
|
||||||
cache_progress: 0.0,
|
cache_progress: 0.0,
|
||||||
unlocking: false,
|
unlocking: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ fn to_item(track: &Track) -> TrackItem {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into(),
|
.into(),
|
||||||
removing: false,
|
removing: false,
|
||||||
cache_status: 0,
|
cache_status: if crate::offline_cache::is_cached(&track.id.to_string()) { 3 } else { 0 },
|
||||||
cache_progress: 0.0,
|
cache_progress: 0.0,
|
||||||
unlocking: false,
|
unlocking: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,75 @@
|
|||||||
//! The heavy lifting (download pipeline, CMAF store, vault) lives in the
|
//! The heavy lifting (download pipeline, CMAF store, vault) lives in the
|
||||||
//! shared `qbz-offline-cache` crate; this is the thin Slint orchestration.
|
//! 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_app::shell::AppRuntime;
|
||||||
use qbz_offline_cache::{CacheEvent, CacheEventSink, TrackCacheInfo};
|
use qbz_offline_cache::{CacheEvent, CacheEventSink, OfflineCacheStatus, TrackCacheInfo};
|
||||||
|
|
||||||
use crate::adapter::SlintAdapter;
|
use crate::adapter::SlintAdapter;
|
||||||
use crate::AppWindow;
|
use crate::AppWindow;
|
||||||
|
|
||||||
type Runtime = Arc<AppRuntime<SlintAdapter>>;
|
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
|
/// Build a sink that reflects cache + unlock events onto every visible row
|
||||||
/// matching the event's track id (and surfaces terminal toasts). Shared by
|
/// matching the event's track id (and surfaces terminal toasts). Shared by
|
||||||
/// the cache trigger AND the play path (UnlockStart/End → padlock).
|
/// 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);
|
push_status(&weak, track_id, 2, p);
|
||||||
}
|
}
|
||||||
CacheEvent::Completed { track_id, .. } => {
|
CacheEvent::Completed { track_id, .. } => {
|
||||||
|
mark_cached(track_id, true);
|
||||||
push_status(&weak, track_id, 3, 1.0);
|
push_status(&weak, track_id, 3, 1.0);
|
||||||
crate::toast::success_weak(&weak, "Cached for offline");
|
crate::toast::success_weak(&weak, "Cached for offline");
|
||||||
}
|
}
|
||||||
@@ -261,6 +321,7 @@ pub fn remove_cached(
|
|||||||
let _ = db.remove_qobuz_cached_track(id);
|
let _ = db.remove_qobuz_cached_track(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mark_cached(id, false);
|
||||||
push_status(&weak, id, 0, 0.0);
|
push_status(&weak, id, 0, 0.0);
|
||||||
crate::toast::success_weak(&weak, "Removed from offline");
|
crate::toast::success_weak(&weak, "Removed from offline");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ fn to_item(track: &Track) -> TrackItem {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into(),
|
.into(),
|
||||||
removing: false,
|
removing: false,
|
||||||
cache_status: 0,
|
cache_status: if crate::offline_cache::is_cached(&track.id.to_string()) { 3 } else { 0 },
|
||||||
cache_progress: 0.0,
|
cache_progress: 0.0,
|
||||||
unlocking: false,
|
unlocking: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ fn album_item(row: AlbumRow) -> AlbumCardItem {
|
|||||||
|
|
||||||
fn track_item(row: TrackRowData) -> TrackItem {
|
fn track_item(row: TrackRowData) -> TrackItem {
|
||||||
let is_favorite = crate::fav_cache::is_favorite(&row.id);
|
let is_favorite = crate::fav_cache::is_favorite(&row.id);
|
||||||
|
let is_cached = crate::offline_cache::is_cached(&row.id);
|
||||||
TrackItem {
|
TrackItem {
|
||||||
id: row.id.into(),
|
id: row.id.into(),
|
||||||
number: "".into(),
|
number: "".into(),
|
||||||
@@ -372,7 +373,7 @@ fn track_item(row: TrackRowData) -> TrackItem {
|
|||||||
artist_id: row.artist_id.into(),
|
artist_id: row.artist_id.into(),
|
||||||
album_id: row.album_id.into(),
|
album_id: row.album_id.into(),
|
||||||
removing: false,
|
removing: false,
|
||||||
cache_status: 0,
|
cache_status: if is_cached { 3 } else { 0 },
|
||||||
cache_progress: 0.0,
|
cache_progress: 0.0,
|
||||||
unlocking: false,
|
unlocking: false,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user