mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 22:45:41 +00:00
fix(favorites): reflect player-bar song star in track lists (#1063)
* fix(favorites): reflect player-bar song star in track lists Liking a song from the player bar / fullscreen / shortcuts wrote only to the session `starredOverrides` map, which onStarSuccess deleted once the server sync resolved. List views (AlbumDetail, Favorites, RandomMix, playlists) seed their starred state from a one-shot fetch and reflect later changes only by merging that override, so the row reverted the instant the sync completed — the like never stuck in the list. Toggling from a list row also updated the row's own local state, which is why that direction already worked. Keep the star override as the durable session source of truth (stop deleting it on success); the in-memory Track / queue-cache patches stay. The override is unpersisted and superseded by the next toggle, so it never diverges from the server. Ratings keep their existing clear-on-success behavior.
This commit is contained in:
@@ -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)',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user