refactor(frontend): adopt store/storage helpers; centralize persistence

- errorDialog + hints now use createStore (drop their bespoke listener arrays).
- theme/sidebarLayout/notifications/App filters/Playlists plSort use readMerged/
  readJSON/writeJSON instead of inline try/JSON.parse/catch.
- Stats, SettingsPanel and App's channel filter/view tabs use usePersistedState
  (the 3 sites that reinvented usePersistedTab inline).
- Every siftlode.* key now sourced from the LS registry (no scattered literals).
This commit is contained in:
2026-06-26 03:30:19 +02:00
parent 7ae3802f38
commit 8456b2245d
15 changed files with 80 additions and 147 deletions
+7 -14
View File
@@ -2,6 +2,7 @@
// the Notification Center shows (info events, actions awaiting interaction, and app
// errors). History is persisted to localStorage so it survives reloads; action
// callbacks are live-only and dropped on reload.
import { LS, readJSON, readMerged, writeJSON } from "./storage";
export type NotifLevel = "info" | "success" | "warning" | "error" | "fatal";
@@ -71,8 +72,8 @@ export interface NotifyInput {
transient?: boolean; // live status: sticky toast, not persisted (see Notification.transient)
}
const HISTORY_KEY = "siftlode.notifications";
const SETTINGS_KEY = "siftlode.notifSettings";
const HISTORY_KEY = LS.notifHistory;
const SETTINGS_KEY = LS.notifSettings;
const MAX_HISTORY = 100;
const ERROR_TTL = 15000;
@@ -86,11 +87,7 @@ const DEFAULT_SETTINGS: NotifSettings = { sound: false, durationMs: 6000 };
let config: NotifSettings = loadSettings();
function loadSettings(): NotifSettings {
try {
return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}") };
} catch {
return DEFAULT_SETTINGS;
}
return readMerged(SETTINGS_KEY, DEFAULT_SETTINGS);
}
export function getNotifSettings(): NotifSettings {
@@ -99,11 +96,7 @@ export function getNotifSettings(): NotifSettings {
export function configureNotifications(p: Partial<NotifSettings>): void {
config = { ...config, ...p };
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(config));
} catch {
/* ignore */
}
writeJSON(SETTINGS_KEY, config);
}
let audioCtx: AudioContext | null = null;
@@ -137,7 +130,7 @@ let cachedUnread = 0;
function load(): Notification[] {
try {
const raw = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]");
const raw = readJSON<unknown>(HISTORY_KEY, []);
if (!Array.isArray(raw)) return [];
// Restored entries are history only. Mark them dismissed so they don't
// resurrect as active toasts on reload — their auto-dismiss timers aren't
@@ -154,7 +147,7 @@ function persist() {
// Transient status notices are live-session only — never write them to history,
// so a reload can't leave one stranded with no producer left to clear it.
const slim = items.filter((n) => !n.transient).map(({ action: _action, ...n }) => n);
localStorage.setItem(HISTORY_KEY, JSON.stringify(slim));
writeJSON(HISTORY_KEY, slim);
} catch {
/* ignore quota / serialization errors */
}