diff --git a/web/src/lib/player/store.svelte.ts b/web/src/lib/player/store.svelte.ts index 362242e4..6a0d9d82 100644 --- a/web/src/lib/player/store.svelte.ts +++ b/web/src/lib/player/store.svelte.ts @@ -3,6 +3,7 @@ import { untrack } from 'svelte'; import { api } from '$lib/api/client'; import { user } from '$lib/auth/user.svelte'; import { sessionEnd } from '$lib/auth/sessionEnd.svelte'; +import * as storage from '$lib/util/safeLocalStorage'; import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted'; export type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'error'; @@ -11,15 +12,11 @@ export type RepeatMode = 'off' | 'all' | 'one'; const VOLUME_KEY = 'minstrel.volume'; export function readStoredVolume(): number { - try { - const raw = localStorage.getItem(VOLUME_KEY); - if (raw == null) return 1; - const n = Number(raw); - if (Number.isFinite(n) && n >= 0 && n <= 1) return n; - return 1; - } catch { - return 1; - } + const raw = storage.read(VOLUME_KEY); + if (raw == null) return 1; + const n = Number(raw); + if (Number.isFinite(n) && n >= 0 && n <= 1) return n; + return 1; } let _queue = $state([]); @@ -135,7 +132,7 @@ export function seekTo(sec: number): void { export function setVolume(v: number): void { const clamped = Math.max(0, Math.min(1, v)); _volume = clamped; - try { localStorage.setItem(VOLUME_KEY, String(clamped)); } catch { /* ignore */ } + storage.write(VOLUME_KEY, String(clamped)); } export function reportTimeUpdate(sec: number): void { diff --git a/web/src/lib/stores/theme.svelte.ts b/web/src/lib/stores/theme.svelte.ts index 1657d3a8..1aa70d97 100644 --- a/web/src/lib/stores/theme.svelte.ts +++ b/web/src/lib/stores/theme.svelte.ts @@ -1,16 +1,13 @@ +import * as storage from '$lib/util/safeLocalStorage'; + const STORAGE_KEY = 'minstrel:theme'; export type ThemePreference = 'dark' | 'light' | 'system'; export type ResolvedTheme = 'dark' | 'light'; function readPref(): ThemePreference { - if (typeof localStorage === 'undefined') return 'system'; - try { - const v = localStorage.getItem(STORAGE_KEY); - return v === 'dark' || v === 'light' || v === 'system' ? v : 'system'; - } catch { - return 'system'; - } + const v = storage.read(STORAGE_KEY); + return v === 'dark' || v === 'light' || v === 'system' ? v : 'system'; } function systemResolved(): ResolvedTheme { @@ -51,9 +48,7 @@ export const resolvedTheme = { export function setTheme(pref: ThemePreference): void { _theme = pref; _resolved = resolve(pref); - if (typeof localStorage !== 'undefined') { - try { localStorage.setItem(STORAGE_KEY, pref); } catch { /* ignore quota / unavailable */ } - } + storage.write(STORAGE_KEY, pref); applyToDOM(_resolved); } diff --git a/web/src/lib/util/safeLocalStorage.ts b/web/src/lib/util/safeLocalStorage.ts new file mode 100644 index 00000000..66f01178 --- /dev/null +++ b/web/src/lib/util/safeLocalStorage.ts @@ -0,0 +1,32 @@ +// SSR-safe localStorage wrapper. Returns null / no-ops when window +// is undefined or access throws (private mode, quota, security). +// +// Caller handles parse / serialize. For object payloads, JSON.stringify +// before write and JSON.parse(read(key) ?? '...') after. + +export function read(key: string): string | null { + if (typeof localStorage === 'undefined') return null; + try { + return localStorage.getItem(key); + } catch { + return null; + } +} + +export function write(key: string, value: string): void { + if (typeof localStorage === 'undefined') return; + try { + localStorage.setItem(key, value); + } catch { + // quota / security — drop silently + } +} + +export function remove(key: string): void { + if (typeof localStorage === 'undefined') return; + try { + localStorage.removeItem(key); + } catch { + /* ignore */ + } +}