From 28e45917a256b81ed5f44711088d30eaee0aa52d Mon Sep 17 00:00:00 2001 From: James Brunton Date: Wed, 15 Oct 2025 11:53:00 +0100 Subject: [PATCH] Refactor user preferences (#4667) # Description of Changes Refactor user preferences to all be in one service and all stored in localStorage instead of indexeddb. This allows simpler & quicker accessing of them, and ensures that they're all neatly stored in one consistent place instead of spread out over local storage. --- frontend/src/App.tsx | 12 +- .../shared/RainbowThemeProvider.tsx | 3 +- .../config/configSections/GeneralSection.tsx | 2 +- frontend/src/components/tools/ToolPanel.tsx | 9 +- .../components/tools/ToolPanelModePrompt.tsx | 20 +-- frontend/src/constants/theme.ts | 8 + frontend/src/constants/toolPanel.ts | 5 + frontend/src/contexts/PreferencesContext.tsx | 51 ++---- frontend/src/contexts/ToolWorkflowContext.tsx | 24 +-- .../toolWorkflow/toolWorkflowState.ts | 22 +-- .../hooks/tools/useJsonLocalStorageState.ts | 30 ---- frontend/src/hooks/useRainbowTheme.ts | 58 +++---- .../src/hooks/useToolPanelModePreference.ts | 50 ------ frontend/src/index.tsx | 14 +- frontend/src/services/preferencesService.ts | 158 ++++++------------ 15 files changed, 136 insertions(+), 330 deletions(-) create mode 100644 frontend/src/constants/theme.ts create mode 100644 frontend/src/constants/toolPanel.ts delete mode 100644 frontend/src/hooks/tools/useJsonLocalStorageState.ts delete mode 100644 frontend/src/hooks/useToolPanelModePreference.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 88c19649f..5db513d4a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -40,9 +40,9 @@ const LoadingFallback = () => ( export default function App() { return ( }> - - - + + + @@ -62,9 +62,9 @@ export default function App() { - - - + + + ); } diff --git a/frontend/src/components/shared/RainbowThemeProvider.tsx b/frontend/src/components/shared/RainbowThemeProvider.tsx index d8a7eb765..18efba256 100644 --- a/frontend/src/components/shared/RainbowThemeProvider.tsx +++ b/frontend/src/components/shared/RainbowThemeProvider.tsx @@ -6,9 +6,10 @@ import rainbowStyles from '../../styles/rainbow.module.css'; import { ToastProvider } from '../toast'; import ToastRenderer from '../toast/ToastRenderer'; import { ToastPortalBinder } from '../toast'; +import type { ThemeMode } from '../../constants/theme'; interface RainbowThemeContextType { - themeMode: 'light' | 'dark' | 'rainbow'; + themeMode: ThemeMode; isRainbowMode: boolean; isToggleDisabled: boolean; toggleTheme: () => void; diff --git a/frontend/src/components/shared/config/configSections/GeneralSection.tsx b/frontend/src/components/shared/config/configSections/GeneralSection.tsx index a79c15d72..937ff3d07 100644 --- a/frontend/src/components/shared/config/configSections/GeneralSection.tsx +++ b/frontend/src/components/shared/config/configSections/GeneralSection.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { usePreferences } from '../../../../contexts/PreferencesContext'; -import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState'; +import type { ToolPanelMode } from '../../../../constants/toolPanel'; const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4; diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index 88089df5d..099f83349 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo } from 'react'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; +import { usePreferences } from '../../contexts/PreferencesContext'; import ToolPicker from './ToolPicker'; import SearchResults from './SearchResults'; import ToolRenderer from './ToolRenderer'; @@ -14,7 +15,6 @@ import DoubleArrowIcon from '@mui/icons-material/DoubleArrow'; import { useTranslation } from 'react-i18next'; import FullscreenToolSurface from './FullscreenToolSurface'; import { useToolPanelGeometry } from '../../hooks/tools/useToolPanelGeometry'; -import { useLocalStorageState } from '../../hooks/tools/useJsonLocalStorageState'; import { useRightRail } from '../../contexts/RightRailContext'; import { Tooltip } from '../shared/Tooltip'; import './ToolPanel.css'; @@ -45,6 +45,7 @@ export default function ToolPanel() { } = useToolWorkflow(); const { setAllRightRailButtonsDisabled } = useRightRail(); + const { preferences, updatePreference } = usePreferences(); const isFullscreenMode = toolPanelMode === 'fullscreen'; const toolPickerVisible = !readerMode; @@ -56,8 +57,6 @@ export default function ToolPanel() { setAllRightRailButtonsDisabled(fullscreenExpanded); }, [fullscreenExpanded, setAllRightRailButtonsDisabled]); - // Use custom hooks for state management - const [showLegacyDescriptions, setShowLegacyDescriptions] = useLocalStorageState('legacyToolDescriptions', false); const fullscreenGeometry = useToolPanelGeometry({ enabled: fullscreenExpanded, toolPanelRef, @@ -200,11 +199,11 @@ export default function ToolPanel() { toolRegistry={toolRegistry} filteredTools={filteredTools} selectedToolKey={selectedToolKey} - showDescriptions={showLegacyDescriptions} + showDescriptions={preferences.showLegacyToolDescriptions} matchedTextMap={matchedTextMap} onSearchChange={setSearchQuery} onSelect={(id: ToolId) => handleToolSelect(id)} - onToggleDescriptions={() => setShowLegacyDescriptions((prev) => !prev)} + onToggleDescriptions={() => updatePreference('showLegacyToolDescriptions', !preferences.showLegacyToolDescriptions)} onExitFullscreenMode={() => setToolPanelMode('sidebar')} toggleLabel={toggleLabel} geometry={fullscreenGeometry} diff --git a/frontend/src/components/tools/ToolPanelModePrompt.tsx b/frontend/src/components/tools/ToolPanelModePrompt.tsx index c6e9d3201..770cf2eee 100644 --- a/frontend/src/components/tools/ToolPanelModePrompt.tsx +++ b/frontend/src/components/tools/ToolPanelModePrompt.tsx @@ -2,17 +2,17 @@ import { useEffect, useState } from 'react'; import { Badge, Button, Card, Group, Modal, Stack, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; +import { usePreferences } from '../../contexts/PreferencesContext'; import './ToolPanelModePrompt.css'; -import { useToolPanelModePreference } from '../../hooks/useToolPanelModePreference'; -import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState'; - -// type moved to hook +import type { ToolPanelMode } from '../../constants/toolPanel'; const ToolPanelModePrompt = () => { const { t } = useTranslation(); const { toolPanelMode, setToolPanelMode } = useToolWorkflow(); + const { preferences, updatePreference } = usePreferences(); const [opened, setOpened] = useState(false); - const { hydrated, shouldShowPrompt, markPromptSeen, setPreferredMode } = useToolPanelModePreference(); + + const shouldShowPrompt = !preferences.toolPanelModePromptSeen; useEffect(() => { if (shouldShowPrompt) { @@ -22,20 +22,16 @@ const ToolPanelModePrompt = () => { const handleSelect = (mode: ToolPanelMode) => { setToolPanelMode(mode); - setPreferredMode(mode); - markPromptSeen(); + updatePreference('defaultToolPanelMode', mode); + updatePreference('toolPanelModePromptSeen', true); setOpened(false); }; const handleDismiss = () => { - markPromptSeen(); + updatePreference('toolPanelModePromptSeen', true); setOpened(false); }; - if (!hydrated) { - return null; - } - return ( ( key: K, value: UserPreferences[K] - ) => Promise; - resetPreferences: () => Promise; - isLoading: boolean; + ) => void; + resetPreferences: () => void; } const PreferencesContext = createContext(undefined); export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const loadPreferences = async () => { - try { - await preferencesService.initialize(); - const loadedPreferences = await preferencesService.getAllPreferences(); - setPreferences(loadedPreferences); - } catch (error) { - console.error('Failed to load preferences:', error); - // Keep default preferences on error - } finally { - setIsLoading(false); - } - }; - - loadPreferences(); - }, []); + const [preferences, setPreferences] = useState(() => { + // Load preferences synchronously on mount + return preferencesService.getAllPreferences(); + }); const updatePreference = useCallback( - async (key: K, value: UserPreferences[K]) => { - await preferencesService.setPreference(key, value); - setPreferences((prev) => ({ - ...prev, - [key]: value, - })); + (key: K, value: UserPreferences[K]) => { + preferencesService.setPreference(key, value); + setPreferences((prev) => ({ + ...prev, + [key]: value, + })); }, [] ); - const resetPreferences = useCallback(async () => { - await preferencesService.clearAllPreferences(); - setPreferences(DEFAULT_PREFERENCES); + const resetPreferences = useCallback(() => { + preferencesService.clearAllPreferences(); + setPreferences(preferencesService.getAllPreferences()); }, []); return ( @@ -56,7 +40,6 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c preferences, updatePreference, resetPreferences, - isLoading, }} > {children} diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index d010dfdfd..378a7bdca 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -14,11 +14,10 @@ import { filterToolRegistryByQuery } from '../utils/toolSearch'; import { useToolHistory } from '../hooks/tools/useUserToolActivity'; import { ToolWorkflowState, - TOOL_PANEL_MODE_STORAGE_KEY, createInitialState, toolWorkflowReducer, - ToolPanelMode, } from './toolWorkflow/toolWorkflowState'; +import type { ToolPanelMode } from '../constants/toolPanel'; import { usePreferences } from './PreferencesContext'; // State interface @@ -74,7 +73,7 @@ interface ToolWorkflowProviderProps { export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const [state, dispatch] = useReducer(toolWorkflowReducer, undefined, createInitialState); - const { preferences } = usePreferences(); + const { preferences, updatePreference } = usePreferences(); // Store reset functions for tools const [toolResetFunctions, setToolResetFunctions] = React.useState void>>({}); @@ -118,7 +117,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const setToolPanelMode = useCallback((mode: ToolPanelMode) => { dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode }); - }, []); + updatePreference('defaultToolPanelMode', mode); + }, [updatePreference]); const setPreviewFile = useCallback((file: File | null) => { @@ -136,27 +136,15 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { dispatch({ type: 'SET_SEARCH_QUERY', payload: query }); }, []); - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - - window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, state.toolPanelMode); - }, [state.toolPanelMode]); - // Keep tool panel mode in sync with user preference. This ensures the // Config setting (Default tool picker mode) immediately affects the app // and persists across reloads. useEffect(() => { - if (!preferences) return; const preferredMode = preferences.defaultToolPanelMode; - if (preferredMode && preferredMode !== state.toolPanelMode) { + if (preferredMode !== state.toolPanelMode) { dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: preferredMode }); - if (typeof window !== 'undefined') { - window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, preferredMode); - } } - }, [preferences.defaultToolPanelMode]); + }, [preferences.defaultToolPanelMode, state.toolPanelMode]); // Tool reset methods const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => { diff --git a/frontend/src/contexts/toolWorkflow/toolWorkflowState.ts b/frontend/src/contexts/toolWorkflow/toolWorkflowState.ts index dbf96c082..0322e9094 100644 --- a/frontend/src/contexts/toolWorkflow/toolWorkflowState.ts +++ b/frontend/src/contexts/toolWorkflow/toolWorkflowState.ts @@ -1,7 +1,5 @@ import { PageEditorFunctions } from '../../types/pageEditor'; - -// State & Modes -export type ToolPanelMode = 'sidebar' | 'fullscreen'; +import { type ToolPanelMode, DEFAULT_TOOL_PANEL_MODE } from '../../constants/toolPanel'; export interface ToolWorkflowState { // UI State @@ -28,22 +26,6 @@ export type ToolWorkflowAction = | { type: 'SET_SEARCH_QUERY'; payload: string } | { type: 'RESET_UI_STATE' }; -// Storage keys -export const TOOL_PANEL_MODE_STORAGE_KEY = 'toolPanelModePreference'; - -export const getStoredToolPanelMode = (): ToolPanelMode => { - if (typeof window === 'undefined') { - return 'sidebar'; - } - - const stored = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY); - if (stored === 'fullscreen') { - return 'fullscreen'; - } - - return 'sidebar'; -}; - export const baseState: Omit = { sidebarsVisible: true, leftPanelView: 'toolPicker', @@ -55,7 +37,7 @@ export const baseState: Omit = { export const createInitialState = (): ToolWorkflowState => ({ ...baseState, - toolPanelMode: getStoredToolPanelMode(), + toolPanelMode: DEFAULT_TOOL_PANEL_MODE, }); export function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState { diff --git a/frontend/src/hooks/tools/useJsonLocalStorageState.ts b/frontend/src/hooks/tools/useJsonLocalStorageState.ts deleted file mode 100644 index 739251f4e..000000000 --- a/frontend/src/hooks/tools/useJsonLocalStorageState.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useState, useEffect, Dispatch, SetStateAction } from 'react'; - -export function useLocalStorageState(key: string, defaultValue: T): [T, Dispatch>] { - const [state, setState] = useState(() => { - if (typeof window === 'undefined') { - return defaultValue; - } - - const stored = window.localStorage.getItem(key); - if (stored === null) { - return defaultValue; - } - - try { - return JSON.parse(stored) as T; - } catch { - return defaultValue; - } - }); - - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - - window.localStorage.setItem(key, JSON.stringify(state)); - }, [key, state]); - - return [state, setState]; -} diff --git a/frontend/src/hooks/useRainbowTheme.ts b/frontend/src/hooks/useRainbowTheme.ts index 8b272b883..5fc049007 100644 --- a/frontend/src/hooks/useRainbowTheme.ts +++ b/frontend/src/hooks/useRainbowTheme.ts @@ -1,6 +1,6 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; - -type ThemeMode = 'light' | 'dark' | 'rainbow'; +import { useCallback, useRef, useEffect } from 'react'; +import { usePreferences } from '../contexts/PreferencesContext'; +import type { ThemeMode } from '../constants/theme'; interface RainbowThemeHook { themeMode: ThemeMode; @@ -13,36 +13,19 @@ interface RainbowThemeHook { const allowRainbowMode = false; // Override to allow/disallow fun -export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): RainbowThemeHook { - // Get theme from localStorage or use initial - const [themeMode, setThemeMode] = useState(() => { - const stored = localStorage.getItem('stirling-theme'); - if (stored && ['light', 'dark', 'rainbow'].includes(stored)) { - return stored as ThemeMode; - } - try { - // Fallback to OS preference if available - const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - return prefersDark ? 'dark' : initialTheme; - } catch { - return initialTheme; - } - }); +export function useRainbowTheme(): RainbowThemeHook { + const { preferences, updatePreference } = usePreferences(); + const themeMode = preferences.theme; // Track rapid toggles for easter egg const toggleCount = useRef(0); const lastToggleTime = useRef(Date.now()); - const [isToggleDisabled, setIsToggleDisabled] = useState(false); + const isToggleDisabled = useRef(false); - // Save theme to localStorage whenever it changes + // Apply rainbow class to body whenever theme changes useEffect(() => { - localStorage.setItem('stirling-theme', themeMode); - - // Apply rainbow class to body if in rainbow mode if (themeMode === 'rainbow') { document.body.classList.add('rainbow-mode-active'); - - // Show easter egg notification showRainbowNotification(); } else { document.body.classList.remove('rainbow-mode-active'); @@ -141,7 +124,7 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb const toggleTheme = useCallback(() => { // Don't allow toggle if disabled - if (isToggleDisabled) { + if (isToggleDisabled.current) { return; } @@ -149,7 +132,7 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb // Simple exit from rainbow mode with single click (after cooldown period) if (themeMode === 'rainbow') { - setThemeMode('light'); + updatePreference('theme', 'light'); console.log('🌈 Rainbow mode deactivated. Thanks for trying it!'); showExitNotification(); return; @@ -165,14 +148,14 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb // Easter egg: Activate rainbow mode after 10 rapid toggles if (allowRainbowMode && toggleCount.current >= 10) { - setThemeMode('rainbow'); + updatePreference('theme', 'rainbow'); console.log('🌈 RAINBOW MODE ACTIVATED! 🌈 You found the secret easter egg!'); console.log('🌈 Button will be disabled for 3 seconds, then click once to exit!'); // Disable toggle for 3 seconds - setIsToggleDisabled(true); + isToggleDisabled.current = true; setTimeout(() => { - setIsToggleDisabled(false); + isToggleDisabled.current = false; console.log('🌈 Theme toggle re-enabled! Click once to exit rainbow mode.'); }, 3000); @@ -182,25 +165,26 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb } // Normal theme switching - setThemeMode(prevMode => prevMode === 'light' ? 'dark' : 'light'); - }, [themeMode, isToggleDisabled]); + const nextTheme = themeMode === 'light' ? 'dark' : 'light'; + updatePreference('theme', nextTheme); + }, [themeMode, updatePreference]); const activateRainbow = useCallback(() => { - setThemeMode('rainbow'); + updatePreference('theme', 'rainbow'); console.log('🌈 Rainbow mode manually activated!'); - }, []); + }, [updatePreference]); const deactivateRainbow = useCallback(() => { if (themeMode === 'rainbow') { - setThemeMode('light'); + updatePreference('theme', 'light'); console.log('🌈 Rainbow mode manually deactivated.'); } - }, [themeMode]); + }, [themeMode, updatePreference]); return { themeMode, isRainbowMode: themeMode === 'rainbow', - isToggleDisabled, + isToggleDisabled: isToggleDisabled.current, toggleTheme, activateRainbow, deactivateRainbow, diff --git a/frontend/src/hooks/useToolPanelModePreference.ts b/frontend/src/hooks/useToolPanelModePreference.ts deleted file mode 100644 index b8656eeae..000000000 --- a/frontend/src/hooks/useToolPanelModePreference.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { TOOL_PANEL_MODE_STORAGE_KEY, ToolPanelMode } from '../contexts/toolWorkflow/toolWorkflowState'; - -const PROMPT_SEEN_KEY = 'toolPanelModePromptSeen'; - -export function useToolPanelModePreference() { - const [hydrated, setHydrated] = useState(false); - - const getPreferredMode = useCallback((): ToolPanelMode | null => { - if (typeof window === 'undefined') return null; - const stored = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY); - return stored === 'sidebar' || stored === 'fullscreen' ? stored : null; - }, []); - - const setPreferredMode = useCallback((mode: ToolPanelMode) => { - if (typeof window === 'undefined') return; - window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, mode); - }, []); - - const hasSeenPrompt = useCallback((): boolean => { - if (typeof window === 'undefined') return true; - return window.localStorage.getItem(PROMPT_SEEN_KEY) === 'true'; - }, []); - - const markPromptSeen = useCallback(() => { - if (typeof window === 'undefined') return; - window.localStorage.setItem(PROMPT_SEEN_KEY, 'true'); - }, []); - - const shouldShowPrompt = useMemo(() => { - const seen = hasSeenPrompt(); - const pref = getPreferredMode(); - return !seen && !pref; - }, [getPreferredMode, hasSeenPrompt]); - - useEffect(() => { - setHydrated(true); - }, []); - - return { - hydrated, - getPreferredMode, - setPreferredMode, - hasSeenPrompt, - markPromptSeen, - shouldShowPrompt, - } as const; -} - - diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 431aa7bf3..083d779a5 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -12,18 +12,6 @@ import posthog from 'posthog-js'; import { PostHogProvider } from 'posthog-js/react'; import { BASE_PATH } from './constants/app'; -// Compute initial color scheme -function getInitialScheme(): 'light' | 'dark' { - const stored = localStorage.getItem('stirling-theme'); - if (stored === 'light' || stored === 'dark') return stored; - try { - const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - return prefersDark ? 'dark' : 'light'; - } catch { - return 'light'; - } -} - posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, { api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, defaults: '2025-05-24', @@ -57,7 +45,7 @@ if (!container) { const root = ReactDOM.createRoot(container); // Finds the root DOM element root.render( - + diff --git a/frontend/src/services/preferencesService.ts b/frontend/src/services/preferencesService.ts index 2f126b297..5a8ff6286 100644 --- a/frontend/src/services/preferencesService.ts +++ b/frontend/src/services/preferencesService.ts @@ -1,131 +1,83 @@ -import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState'; -import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager'; +import { type ToolPanelMode, DEFAULT_TOOL_PANEL_MODE } from '../constants/toolPanel'; +import { type ThemeMode, getSystemTheme } from '../constants/theme'; export interface UserPreferences { autoUnzip: boolean; autoUnzipFileLimit: number; defaultToolPanelMode: ToolPanelMode; + theme: ThemeMode; + toolPanelModePromptSeen: boolean; + showLegacyToolDescriptions: boolean; } export const DEFAULT_PREFERENCES: UserPreferences = { autoUnzip: true, autoUnzipFileLimit: 4, - defaultToolPanelMode: 'sidebar', + defaultToolPanelMode: DEFAULT_TOOL_PANEL_MODE, + theme: getSystemTheme(), + toolPanelModePromptSeen: false, + showLegacyToolDescriptions: false, }; +const STORAGE_KEY = 'stirlingpdf_preferences'; + class PreferencesService { - private db: IDBDatabase | null = null; - - async initialize(): Promise { - this.db = await indexedDBManager.openDatabase(DATABASE_CONFIGS.PREFERENCES); - } - - private ensureDatabase(): IDBDatabase { - if (!this.db) { - throw new Error('PreferencesService not initialized. Call initialize() first.'); - } - return this.db; - } - - async getPreference( + getPreference( key: K - ): Promise { - const db = this.ensureDatabase(); - - return new Promise((resolve) => { - const transaction = db.transaction(['preferences'], 'readonly'); - const store = transaction.objectStore('preferences'); - const request = store.get(key); - - request.onsuccess = () => { - const result = request.result; - if (result && result.value !== undefined) { - resolve(result.value); - } else { - // Return default value if preference not found - resolve(DEFAULT_PREFERENCES[key]); + ): UserPreferences[K] { + // Explicitly re-read every time in case preferences have changed in another tab etc. + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const preferences = JSON.parse(stored) as Partial; + if (key in preferences && preferences[key] !== undefined) { + return preferences[key]!; } - }; - - request.onerror = () => { - console.error('Error reading preference:', key, request.error); - // Return default value on error - resolve(DEFAULT_PREFERENCES[key]); - }; - }); + } + } catch (error) { + console.error('Error reading preference:', key, error); + } + return DEFAULT_PREFERENCES[key]; } - async setPreference( + setPreference( key: K, value: UserPreferences[K] - ): Promise { - const db = this.ensureDatabase(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(['preferences'], 'readwrite'); - const store = transaction.objectStore('preferences'); - const request = store.put({ key, value }); - - request.onsuccess = () => { - resolve(); - }; - - request.onerror = () => { - console.error('Error writing preference:', key, request.error); - reject(request.error); - }; - }); + ): void { + try { + const stored = localStorage.getItem(STORAGE_KEY); + const preferences = stored ? JSON.parse(stored) : {}; + preferences[key] = value; + localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences)); + } catch (error) { + console.error('Error writing preference:', key, error); + } } - async getAllPreferences(): Promise { - const db = this.ensureDatabase(); - - return new Promise((resolve) => { - const transaction = db.transaction(['preferences'], 'readonly'); - const store = transaction.objectStore('preferences'); - const request = store.getAll(); - - request.onsuccess = () => { - const storedPrefs: Partial = {}; - const results = request.result; - - for (const item of results) { - if (item.key && item.value !== undefined) { - storedPrefs[item.key as keyof UserPreferences] = item.value; - } - } - + getAllPreferences(): UserPreferences { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const preferences = JSON.parse(stored) as Partial; // Merge with defaults to ensure all preferences exist - resolve({ + return { ...DEFAULT_PREFERENCES, - ...storedPrefs, - }); - }; - - request.onerror = () => { - console.error('Error reading all preferences:', request.error); - // Return defaults on error - resolve({ ...DEFAULT_PREFERENCES }); - }; - }); + ...preferences, + }; + } + } catch (error) { + console.error('Error reading preferences', error); + } + return { ...DEFAULT_PREFERENCES }; } - async clearAllPreferences(): Promise { - const db = this.ensureDatabase(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(['preferences'], 'readwrite'); - const store = transaction.objectStore('preferences'); - const request = store.clear(); - - request.onsuccess = () => { - resolve(); - }; - - request.onerror = () => { - reject(request.error); - }; - }); + clearAllPreferences(): void { + try { + localStorage.removeItem(STORAGE_KEY); + } catch (error) { + console.error('Error clearing preferences:', error); + throw error; + } } }