// Persistent user preferences (volume, muted). Single localStorage entry, // cross-tab sync via the native `storage` event. SSR-safe. // // Why not Zustand: two fields. Adding a dep + persist middleware is overkill // for ~50 LOC of plain code that already matches the activePlayerBus pattern. const STORAGE_KEY = 'djangocfg-audioplayer:prefs'; const DEFAULT_VOLUME = 1; const DEFAULT_MUTED = false; export type PlayerPreferences = { volume: number; muted: boolean; }; type Listener = (prefs: PlayerPreferences) => void; let cached: PlayerPreferences | null = null; const listeners = new Set(); let storageBound = false; function clamp01(v: number): number { if (!Number.isFinite(v)) return DEFAULT_VOLUME; return v < 0 ? 0 : v > 1 ? 1 : v; } function readFromStorage(): PlayerPreferences { if (typeof window === 'undefined') { return { volume: DEFAULT_VOLUME, muted: DEFAULT_MUTED }; } try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return { volume: DEFAULT_VOLUME, muted: DEFAULT_MUTED }; const parsed = JSON.parse(raw) as Partial | null; return { volume: typeof parsed?.volume === 'number' ? clamp01(parsed.volume) : DEFAULT_VOLUME, muted: typeof parsed?.muted === 'boolean' ? parsed.muted : DEFAULT_MUTED, }; } catch { return { volume: DEFAULT_VOLUME, muted: DEFAULT_MUTED }; } } function writeToStorage(prefs: PlayerPreferences): void { if (typeof window === 'undefined') return; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)); } catch { // quota exceeded / disabled — silently ignore. } } function bindStorage(): void { if (storageBound || typeof window === 'undefined') return; storageBound = true; window.addEventListener('storage', (e) => { if (e.key !== STORAGE_KEY) return; cached = readFromStorage(); for (const cb of listeners) cb(cached); }); } export function getPreferences(): PlayerPreferences { if (!cached) cached = readFromStorage(); bindStorage(); return cached; } function update(next: PlayerPreferences, persist = true): void { cached = next; if (persist) writeToStorage(next); for (const cb of listeners) cb(next); } export function setStoredVolume(volume: number): void { const current = getPreferences(); const v = clamp01(volume); if (v === current.volume) return; update({ ...current, volume: v }); } export function setStoredMuted(muted: boolean): void { const current = getPreferences(); if (muted === current.muted) return; update({ ...current, muted }); } export function subscribePreferences(cb: Listener): () => void { listeners.add(cb); bindStorage(); return () => listeners.delete(cb); } // Test-only. export function _resetPreferencesForTesting(): void { cached = null; if (typeof window !== 'undefined') { try { window.localStorage.removeItem(STORAGE_KEY); } catch { // ignore } } }