refactor(web): safeLocalStorage helper for store persistence (#375)
Three nearly-identical try/catch wrappers across theme + player stores collapse to read()/write()/remove() in lib/util/safeLocalStorage.ts. Sets the pattern for future stores. Caller still does parse/serialize since the existing call sites store strings (theme preference, volume number) — no JSON wrapper needed yet. persisted.ts left alone — its JSON-payload + per-key-suffix shape is distinct enough to keep self-contained. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { untrack } from 'svelte';
|
|||||||
import { api } from '$lib/api/client';
|
import { api } from '$lib/api/client';
|
||||||
import { user } from '$lib/auth/user.svelte';
|
import { user } from '$lib/auth/user.svelte';
|
||||||
import { sessionEnd } from '$lib/auth/sessionEnd.svelte';
|
import { sessionEnd } from '$lib/auth/sessionEnd.svelte';
|
||||||
|
import * as storage from '$lib/util/safeLocalStorage';
|
||||||
import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted';
|
import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted';
|
||||||
|
|
||||||
export type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
export type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
||||||
@@ -11,15 +12,11 @@ export type RepeatMode = 'off' | 'all' | 'one';
|
|||||||
const VOLUME_KEY = 'minstrel.volume';
|
const VOLUME_KEY = 'minstrel.volume';
|
||||||
|
|
||||||
export function readStoredVolume(): number {
|
export function readStoredVolume(): number {
|
||||||
try {
|
const raw = storage.read(VOLUME_KEY);
|
||||||
const raw = localStorage.getItem(VOLUME_KEY);
|
|
||||||
if (raw == null) return 1;
|
if (raw == null) return 1;
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
if (Number.isFinite(n) && n >= 0 && n <= 1) return n;
|
if (Number.isFinite(n) && n >= 0 && n <= 1) return n;
|
||||||
return 1;
|
return 1;
|
||||||
} catch {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _queue = $state<TrackRef[]>([]);
|
let _queue = $state<TrackRef[]>([]);
|
||||||
@@ -135,7 +132,7 @@ export function seekTo(sec: number): void {
|
|||||||
export function setVolume(v: number): void {
|
export function setVolume(v: number): void {
|
||||||
const clamped = Math.max(0, Math.min(1, v));
|
const clamped = Math.max(0, Math.min(1, v));
|
||||||
_volume = clamped;
|
_volume = clamped;
|
||||||
try { localStorage.setItem(VOLUME_KEY, String(clamped)); } catch { /* ignore */ }
|
storage.write(VOLUME_KEY, String(clamped));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reportTimeUpdate(sec: number): void {
|
export function reportTimeUpdate(sec: number): void {
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
|
import * as storage from '$lib/util/safeLocalStorage';
|
||||||
|
|
||||||
const STORAGE_KEY = 'minstrel:theme';
|
const STORAGE_KEY = 'minstrel:theme';
|
||||||
|
|
||||||
export type ThemePreference = 'dark' | 'light' | 'system';
|
export type ThemePreference = 'dark' | 'light' | 'system';
|
||||||
export type ResolvedTheme = 'dark' | 'light';
|
export type ResolvedTheme = 'dark' | 'light';
|
||||||
|
|
||||||
function readPref(): ThemePreference {
|
function readPref(): ThemePreference {
|
||||||
if (typeof localStorage === 'undefined') return 'system';
|
const v = storage.read(STORAGE_KEY);
|
||||||
try {
|
|
||||||
const v = localStorage.getItem(STORAGE_KEY);
|
|
||||||
return v === 'dark' || v === 'light' || v === 'system' ? v : 'system';
|
return v === 'dark' || v === 'light' || v === 'system' ? v : 'system';
|
||||||
} catch {
|
|
||||||
return 'system';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function systemResolved(): ResolvedTheme {
|
function systemResolved(): ResolvedTheme {
|
||||||
@@ -51,9 +48,7 @@ export const resolvedTheme = {
|
|||||||
export function setTheme(pref: ThemePreference): void {
|
export function setTheme(pref: ThemePreference): void {
|
||||||
_theme = pref;
|
_theme = pref;
|
||||||
_resolved = resolve(pref);
|
_resolved = resolve(pref);
|
||||||
if (typeof localStorage !== 'undefined') {
|
storage.write(STORAGE_KEY, pref);
|
||||||
try { localStorage.setItem(STORAGE_KEY, pref); } catch { /* ignore quota / unavailable */ }
|
|
||||||
}
|
|
||||||
applyToDOM(_resolved);
|
applyToDOM(_resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user