diff --git a/CHANGELOG.md b/CHANGELOG.md index e0db3e04..f1cf2fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -349,6 +349,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +### Favorites โ€” player-bar star stays synced in track lists + +**By [@artplan1](https://github.com/artplan1), PR [#1063](https://github.com/Psychotoxical/psysonic/pull/1063)** + +* Liking a song from the **player bar**, fullscreen player, or global shortcuts now updates the star in album tracklists, playlists, Random Mix, and Favorites โ€” the row no longer reverts the instant the server sync completes. +* List views seed starred state from a one-shot fetch and merge session `starredOverrides`; clearing those overrides on sync success had only patched `currentTrack` and the queue cache, so rows fell back to stale fetched values. + + + ## [1.47.0] > **๐Ÿ™ Thank you to our amazing Discord community.** This release would not have been possible without your tireless support, quality checks, bug reports and all-round collaboration. Every report, every repro and every bit of feedback shaped what shipped here โ€” thank you. Come join us: [discord.gg/AMnDRErm4u](https://discord.gg/AMnDRErm4u) diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index f2ecefd3..5f57ae9e 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -259,6 +259,7 @@ const CONTRIBUTOR_ENTRIES = [ 'Player: cap persisted queue to ยฑ250-track window โ€” fixes QuotaExceededError on large playlists (PR #756)', 'Playlists: virtualized tracklist for large playlists โ€” no UI freeze on 10k+ tracks (PR #755)', 'Favorites: virtualized songs tracklist for large starred collections (PR #805)', + 'Favorites: player-bar star toggle stays synced with album, playlist, and favorites tracklists (PR #1063)', ], }, { diff --git a/src/store/pendingStarSync.test.ts b/src/store/pendingStarSync.test.ts index 15dc6e9a..6642bf1c 100644 --- a/src/store/pendingStarSync.test.ts +++ b/src/store/pendingStarSync.test.ts @@ -47,7 +47,7 @@ describe('pendingStarSync', () => { vi.useRealTimers(); }); - it('stars optimistically, then clears the override + patches the track on success', async () => { + it('stars optimistically, then keeps the override + patches the track on success', async () => { queueSongStar('t1', true); expect(usePlayerStore.getState().starredOverrides.t1).toBe(true); // optimistic, instant @@ -55,7 +55,7 @@ describe('pendingStarSync', () => { expect(starMock).toHaveBeenCalledWith('t1', 'song', undefined); const s = usePlayerStore.getState(); - expect('t1' in s.starredOverrides).toBe(false); // cleared on success + expect(s.starredOverrides.t1).toBe(true); // kept on success so list views stay in sync expect(s.currentTrack?.starred).toBeTruthy(); // in-memory track patched // Thin-state: the resolver cache entry is patched in place (not dropped) so // the visible queue row keeps its title and reflects the synced star โ€” @@ -86,7 +86,7 @@ describe('pendingStarSync', () => { queueSongStar('t1', false); // user toggled back off await vi.runAllTimersAsync(); expect(unstarMock).toHaveBeenCalledWith('t1', 'song', undefined); - expect('t1' in usePlayerStore.getState().starredOverrides).toBe(false); + expect(usePlayerStore.getState().starredOverrides.t1).toBe(false); // kept as durable false expect(usePlayerStore.getState().currentTrack?.starred).toBeFalsy(); }); diff --git a/src/store/pendingStarSync.ts b/src/store/pendingStarSync.ts index 61dfa3d5..de63c9a5 100644 --- a/src/store/pendingStarSync.ts +++ b/src/store/pendingStarSync.ts @@ -6,15 +6,15 @@ import { patchCachedTrack } from '../utils/library/queueTrackResolver'; * F4 โ€” pending-sync for **song** star + rating (spec ยง6.5 / R7-18). * * The player-store override maps (`starredOverrides` / `userRatingOverrides`) - * are *session-only outbound sync state*, not a permanent second source of - * truth: + * are *session-only* client truth that every list view merges over its + * one-shot-fetched state: * * 1. Set the override optimistically (instant UI). * 2. Retry the Subsonic API (`star` / `unstar` / `setRating`) with exponential * backoff; flush immediately on `online` / window focus. - * 3. On success: clear the override and patch the in-memory `Track` - * (`currentTrack` + `queue`) so the UI stays correct without the override. - * The F3 index patch-on-use runs inside the API layer, unchanged. + * 3. On **star** success: KEEP the override โ€” list views read it โ€” and patch + * the in-memory `Track`. F3 index patch-on-use runs in the API layer. + * (Ratings clear on success; see `onRatingSuccess`.) * 4. On app restart before success: the pending change is lost โ€” acceptable, * overrides are not persisted. * @@ -86,16 +86,11 @@ async function run(k: string): Promise { function onStarSuccess(id: string, starred: boolean): void { const starredVal = starred ? new Date().toISOString() : undefined; - usePlayerStore.setState(s => { - if (!(id in s.starredOverrides)) return {}; - const next = { ...s.starredOverrides }; - delete next[id]; - return { - starredOverrides: next, - currentTrack: - s.currentTrack?.id === id ? { ...s.currentTrack, starred: starredVal } : s.currentTrack, - }; - }); + // Keep the override โ€” list views merge it (step 3 atop this file). + usePlayerStore.setState(s => ({ + currentTrack: + s.currentTrack?.id === id ? { ...s.currentTrack, starred: starredVal } : s.currentTrack, + })); // Thin-state: the queue's copy lives in the resolver cache. Patch it in place // to the synced value rather than dropping it โ€” a dropped entry would blank the // visible queue row to a "โ€ฆ" placeholder until the next window re-resolve.