// Favourites overlay — in-session heart state shared across every surface. // // The durable record lives in Rust (local `user_data` + the Jellyfin server). // This store holds only what the *current session* has changed, so a heart // tapped on a card is reflected on the detail page and the item vanishes from // the Favourites grid without anyone refetching. It is view state, not truth. // // TRACES: UR-068 | DR-119 | UT-105 import { derived, get, writable } from "svelte/store"; import type { MediaItem } from "$lib/api/types"; /** Item id → favourite state set during this session. */ const overrides = writable>(new Map()); export const favoriteOverrides = { subscribe: overrides.subscribe }; /** * Record a favourite state locally so every mounted view agrees immediately. * Called by the toggle service after the optimistic local write. */ export function setFavorite(itemId: string, isFavorite: boolean): void { overrides.update((map) => { const next = new Map(map); next.set(itemId, isFavorite); return next; }); } /** * Forget a session override, so the item's own `userData` is authoritative * again. Used when the backend reports the server's state changed underneath * us (`favorites-changed`) — the fresh fetch that follows carries the truth. */ export function clearFavorite(itemId: string): void { overrides.update((map) => { if (!map.has(itemId)) return map; const next = new Map(map); next.delete(itemId); return next; }); } export function clearAllFavorites(): void { overrides.set(new Map()); } /** * Resolution order: a session override wins, then the item's own server-sent * `userData`, then "not favourited". * * The override has to win, or tapping the heart on a card would flip back the * moment the (unchanged) item object re-rendered. * * TRACES: UR-068 | DR-119 | UT-105 */ export function resolveIsFavorite( item: Pick | null | undefined, overrideMap: Map ): boolean { if (!item) return false; const override = overrideMap.get(item.id); if (override !== undefined) return override; return item.userData?.isFavorite ?? false; } /** Non-reactive read, for call sites outside a component. */ export function isFavoriteNow(item: Pick): boolean { return resolveIsFavorite(item, get(overrides)); } /** * Drop the items a listing should no longer show once un-favourited. * * Pure so it can be unit-tested without mounting the page: un-hearting on the * Favourites grid must remove the card, while a *newly* favourited item is * left alone (it belongs to whatever scope the caller fetched). * * TRACES: UR-067 | DR-117, DR-119 | UT-106 */ export function retainFavorites>( items: T[], overrideMap: Map ): T[] { return items.filter((item) => resolveIsFavorite(item, overrideMap)); } /** Count of items still favourited, for "hide the row when empty" decisions. */ export const hasOverrides = derived(overrides, ($o) => $o.size > 0);