fix: persist presets and theme preferences

This commit is contained in:
webadderall
2026-05-17 18:32:59 +10:00
parent 6255890556
commit eef387b0ed
10 changed files with 287 additions and 28 deletions
+74
View File
@@ -0,0 +1,74 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadThemePreference, persistThemePreference } from "./ThemeContext";
function createStorageMock(initialValues: Record<string, string> = {}): Storage {
const store = new Map(Object.entries(initialValues));
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key) {
return store.get(key) ?? null;
},
key(index) {
return Array.from(store.keys())[index] ?? null;
},
removeItem(key) {
store.delete(key);
},
setItem(key, value) {
store.set(key, value);
},
};
}
function stubElectronSettings(initialValues: Record<string, unknown> = {}) {
const store = new Map(Object.entries(initialValues));
Object.defineProperty(globalThis, "electronAPI", {
configurable: true,
value: {
getAppSetting: (key: string) => (store.has(key) ? store.get(key) : null),
setAppSetting: (key: string, value: unknown) => {
store.set(key, value);
return true;
},
} as Pick<Window["electronAPI"], "getAppSetting" | "setAppSetting">,
});
return store;
}
describe("ThemeContext persistence", () => {
afterEach(() => {
vi.unstubAllGlobals();
Reflect.deleteProperty(globalThis, "electronAPI");
});
it("loads the persisted theme preference from Electron app settings", () => {
stubElectronSettings({ "recordly.theme": "dark" });
expect(loadThemePreference()).toBe("dark");
});
it("saves the theme preference to Electron app settings", () => {
const settingsStore = stubElectronSettings();
persistThemePreference("dark");
expect(settingsStore.get("recordly.theme")).toBe("dark");
});
it("falls back to localStorage when Electron settings are unavailable", () => {
vi.stubGlobal(
"localStorage",
createStorageMock({ "recordly.theme": "light" }),
);
expect(loadThemePreference()).toBe("light");
});
});
+19 -7
View File
@@ -1,4 +1,5 @@
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { loadAppSetting, saveAppSetting } from "../lib/appSettings";
export type ThemePreference = "light" | "dark" | "system";
export type ResolvedTheme = "light" | "dark";
@@ -16,7 +17,12 @@ const THEME_STORAGE_KEY = "recordly.theme";
const ThemeContext = createContext<ThemeContextValue | null>(null);
function getStoredPreference(): ThemePreference {
export function loadThemePreference(): ThemePreference {
const persisted = loadAppSetting<unknown>(THEME_STORAGE_KEY);
if (persisted === "light" || persisted === "dark" || persisted === "system") {
return persisted;
}
try {
const stored = globalThis.localStorage?.getItem(THEME_STORAGE_KEY);
if (stored === "light" || stored === "dark" || stored === "system") {
@@ -28,6 +34,16 @@ function getStoredPreference(): ThemePreference {
return "system";
}
export function persistThemePreference(pref: ThemePreference): void {
saveAppSetting(THEME_STORAGE_KEY, pref);
try {
globalThis.localStorage?.setItem(THEME_STORAGE_KEY, pref);
} catch {
// Ignore storage errors
}
}
function resolveTheme(pref: ThemePreference): ResolvedTheme {
if (pref === "system") {
return globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches
@@ -49,7 +65,7 @@ function applyThemeToDOM(theme: ResolvedTheme) {
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [preference, setPreferenceState] = useState<ThemePreference>(() => {
const stored = getStoredPreference();
const stored = loadThemePreference();
applyThemeToDOM(resolveTheme(stored));
return stored;
});
@@ -63,11 +79,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const r = resolveTheme(pref);
setResolved(r);
applyThemeToDOM(r);
try {
globalThis.localStorage?.setItem(THEME_STORAGE_KEY, pref);
} catch {
// Ignore storage errors
}
persistThemePreference(pref);
}, []);
const toggleTheme = useCallback(() => {