// src/lib/stores/preferences.svelte.js import { invoke } from '@tauri-apps/api/core'; import { toasts } from './toasts.svelte.js'; const DEFAULTS = { theme: 'dark', zone: 'default', accessibility: { fontFamily: 'lexend', fontSize: 16, letterSpacing: 0, lineHeight: 1.5, transcriptSize: 16, bionicReading: false, reduceMotion: 'system' } }; const FONT_FAMILIES = { lexend: "'Lexend', system-ui, sans-serif", atkinson: "'Atkinson Hyperlegible Next', system-ui, sans-serif", opendyslexic: "'OpenDyslexic', system-ui, sans-serif" }; function readFromDOM() { const el = document.documentElement; return { theme: el.dataset.theme || DEFAULTS.theme, zone: el.dataset.zone || DEFAULTS.zone, accessibility: { fontFamily: el.dataset.fontFamily || DEFAULTS.accessibility.fontFamily, fontSize: parseFloat(el.style.getPropertyValue('--font-size-body')) || DEFAULTS.accessibility.fontSize, letterSpacing: parseFloat(el.style.getPropertyValue('--letter-spacing-body')) || DEFAULTS.accessibility.letterSpacing, lineHeight: parseFloat(el.style.getPropertyValue('--line-height-body')) || DEFAULTS.accessibility.lineHeight, transcriptSize: DEFAULTS.accessibility.transcriptSize, bionicReading: el.dataset.bionicReading === 'true', reduceMotion: el.dataset.reduceMotion || DEFAULTS.accessibility.reduceMotion } }; } function applyToDOM(prefs) { const el = document.documentElement; // Theme — resolve 'system' to actual value el.dataset.theme = prefs.theme === 'system' ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark') : prefs.theme; // Zone if (prefs.zone === 'default') { delete el.dataset.zone; } else { el.dataset.zone = prefs.zone; } // Accessibility — inline styles for highest specificity const a = prefs.accessibility; el.style.setProperty('--font-family-body', FONT_FAMILIES[a.fontFamily] || FONT_FAMILIES.lexend); el.style.setProperty('--font-size-body', `${a.fontSize}px`); el.style.setProperty('--letter-spacing-body', `${a.letterSpacing}em`); el.style.setProperty('--line-height-body', String(a.lineHeight)); el.dataset.bionicReading = String(a.bionicReading); el.dataset.fontFamily = a.fontFamily; // Reduce motion — three-value resolution const motionReduced = a.reduceMotion === 'on' || (a.reduceMotion === 'system' && window.matchMedia('(prefers-reduced-motion: reduce)').matches); if (motionReduced) { el.dataset.reduceMotion = 'true'; } else { delete el.dataset.reduceMotion; } } let saveTimeout = null; // Show the failure toast at most once per process so a stuck SQLite path // doesn't spam the user every time they nudge a slider. let saveFailureToastShown = false; function persistToSQLite(prefs) { clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { try { await invoke('save_preferences', { preferences: JSON.stringify(prefs) }); saveFailureToastShown = false; } catch (e) { console.error('Failed to save preferences:', e); if (!saveFailureToastShown) { const msg = typeof e === 'string' ? e : (e?.message ?? String(e)); toasts.warn( 'Could not save preferences', `${msg}. Your changes still apply for this session.`, ); saveFailureToastShown = true; } } }, 500); } let preferences = $state(readFromDOM()); export function getPreferences() { return preferences; } export function updatePreferences(updates) { preferences = { ...preferences, ...updates }; applyToDOM(preferences); persistToSQLite(preferences); } export function updateAccessibility(updates) { preferences = { ...preferences, accessibility: { ...preferences.accessibility, ...updates } }; applyToDOM(preferences); persistToSQLite(preferences); } // Re-resolve when OS preferences change if (typeof window !== 'undefined') { window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { if (preferences.theme === 'system') applyToDOM(preferences); }); window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', () => { if (preferences.accessibility.reduceMotion === 'system') applyToDOM(preferences); }); }