fix(discover): correct artist follow state in the Home/ForYou Pinned carousel
The pinned artist card's follow chip was seeded at build time from FAV_ARTISTS — an in-memory-only set that was always empty at that moment (shell entry warmed tracks/albums/awards, never artists; init_for_user disk-seeded everything but artists) — and nothing re-seeded the pinned model when the follow set later arrived. Every pinned artist showed not-followed, and the toggle read the same empty set, so a second click could genuinely unfollow an artist. Wire artists into the existing disk lifecycle (the qbz-app store already had the table + CRUD): init_for_user seeds from disk, set_all_artists syncs to disk, set_artist mirrors add/remove. Add the shell-entry warm favorite_artist_ids (skipped offline) and re-seed already-built pinned artist rows when it lands (model walk, no rebuild/flicker). km
This commit is contained in:
2 files changed
+87
-6
No files matched your search
@@ -36,10 +36,11 @@ static FAV_ALBUMS: LazyLock<RwLock<HashSet<String>>> =
|
||||
static FAV_AWARDS: LazyLock<RwLock<HashSet<String>>> =
|
||||
LazyLock::new(|| RwLock::new(HashSet::new()));
|
||||
|
||||
/// Process-wide set of the user's followed ARTIST ids (u64). IN-MEMORY ONLY
|
||||
/// (no per-user disk store): the search + artist-page surfaces re-seed it from
|
||||
/// `favorite_artist_ids` on load, so the follow heart toggle always has the
|
||||
/// current state without a per-click network hit.
|
||||
/// Process-wide set of the user's followed ARTIST ids (u64). Same disk-first
|
||||
/// + network-refresh lifecycle as the album set (seeded from the per-user
|
||||
/// store in [`init_for_user`], refreshed by the shell-entry warm and the
|
||||
/// search/artist-page loaders) — the Pinned carousel's artist follow chip
|
||||
/// reads it at build time, so it must be correct from first paint and offline.
|
||||
static FAV_ARTISTS: LazyLock<RwLock<HashSet<u64>>> =
|
||||
LazyLock::new(|| RwLock::new(HashSet::new()));
|
||||
|
||||
@@ -102,6 +103,22 @@ pub fn init_for_user(base_dir: &Path) {
|
||||
}
|
||||
Err(e) => log::warn!("[qbz-slint] favorites cache award disk seed failed: {e}"),
|
||||
}
|
||||
match store.get_favorite_artist_ids() {
|
||||
Ok(ids) => {
|
||||
let set: HashSet<u64> = ids
|
||||
.into_iter()
|
||||
.filter_map(|id| u64::try_from(id).ok())
|
||||
.collect();
|
||||
log::info!(
|
||||
"[qbz-slint] favorites cache: {} artist ids seeded from disk",
|
||||
set.len()
|
||||
);
|
||||
if let Ok(mut guard) = FAV_ARTISTS.write() {
|
||||
*guard = set;
|
||||
}
|
||||
}
|
||||
Err(e) => log::warn!("[qbz-slint] favorites cache artist disk seed failed: {e}"),
|
||||
}
|
||||
if let Ok(mut guard) = STORE.lock() {
|
||||
*guard = Some(store);
|
||||
}
|
||||
@@ -255,14 +272,25 @@ pub fn is_artist_favorite(artist_id: u64) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Replace the followed-artist set with a freshly-fetched id list (in-memory).
|
||||
/// Replace the followed-artist set with a freshly-fetched id list and mirror
|
||||
/// it to the per-user store (full replace — same lifecycle as the album set).
|
||||
/// Blocking disk write; call off the UI thread.
|
||||
pub fn set_all_artists(ids: HashSet<u64>) {
|
||||
if let Ok(guard) = STORE.lock() {
|
||||
if let Some(store) = guard.as_ref() {
|
||||
let disk: Vec<i64> = ids.iter().map(|&id| id as i64).collect();
|
||||
if let Err(e) = store.sync_favorite_artists(&disk) {
|
||||
log::warn!("[qbz-slint] favorites cache artist sync failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(mut guard) = FAV_ARTISTS.write() {
|
||||
*guard = ids;
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert / remove a single artist id (optimistic follow toggle).
|
||||
/// Insert / remove a single artist id (optimistic follow toggle) and mirror
|
||||
/// the change to the per-user store so the follow survives a restart.
|
||||
pub fn set_artist(artist_id: u64, favorite: bool) {
|
||||
if let Ok(mut guard) = FAV_ARTISTS.write() {
|
||||
if favorite {
|
||||
@@ -271,6 +299,18 @@ pub fn set_artist(artist_id: u64, favorite: bool) {
|
||||
guard.remove(&artist_id);
|
||||
}
|
||||
}
|
||||
if let Ok(guard) = STORE.lock() {
|
||||
if let Some(store) = guard.as_ref() {
|
||||
let res = if favorite {
|
||||
store.add_favorite_artist(artist_id as i64)
|
||||
} else {
|
||||
store.remove_favorite_artist(artist_id as i64)
|
||||
};
|
||||
if let Err(e) = res {
|
||||
log::warn!("[qbz-slint] favorites cache artist disk update failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Followed awards ====================
|
||||
|
||||
@@ -451,6 +451,47 @@ async fn enter_shell(
|
||||
});
|
||||
}
|
||||
|
||||
// Same for followed ARTISTS — the Pinned carousel's artist follow chip
|
||||
// seeds from fav_cache at build time (its only build-time consumer), and
|
||||
// the pinned model is built BEFORE this warm lands: re-seed any already-
|
||||
// built artist rows once the fresh set arrives (walk > rebuild_pinned: no
|
||||
// model swap, no artwork-job re-dispatch, no flicker).
|
||||
{
|
||||
let runtime = runtime.clone();
|
||||
let weak = weak.clone();
|
||||
tokio::spawn(async move {
|
||||
if crate::offline_mode::engine().is_offline() {
|
||||
return;
|
||||
}
|
||||
match runtime.core().favorite_artist_ids().await {
|
||||
Ok(ids) => {
|
||||
let _ =
|
||||
tokio::task::spawn_blocking(move || fav_cache::set_all_artists(ids)).await;
|
||||
let _ = weak.upgrade_in_event_loop(move |w| {
|
||||
let pm = w.global::<PinnedState>().get_items();
|
||||
for i in 0..pm.row_count() {
|
||||
if let Some(mut it) = pm.row_data(i) {
|
||||
if it.kind == "artist" {
|
||||
let following = it
|
||||
.artist
|
||||
.id
|
||||
.parse::<u64>()
|
||||
.map(|id| fav_cache::is_artist_favorite(id))
|
||||
.unwrap_or(false);
|
||||
if it.artist.following != following {
|
||||
it.artist.following = following;
|
||||
pm.set_row_data(i, it);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => log::warn!("[qbz-slint] favorite artists warm failed: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load Audio + Playback settings into the Settings page in the
|
||||
// background — store reads and device enumeration are blocking.
|
||||
spawn_settings_snapshot_load(runtime.clone(), weak.clone(), settings_ctx.clone());
|
||||
|
||||
Reference in new issue
Block a user