diff options
| author | Evan Simkowitz <esimkowitz@users.noreply.github.com> | 2025-12-14 16:39:25 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-15 00:39:25 +0000 |
| commit | 7f4202afd73105b850498b55ad66922b3505f0e3 (patch) | |
| tree | a45f9f1b2599f4c9925e36dc51563b06ba6854ac /apps/web/lib/readerSettings.tsx | |
| parent | 6db14ac492cd5d9e26d0d986513771f14faa7fd0 (diff) | |
| download | karakeep-7f4202afd73105b850498b55ad66922b3505f0e3.tar.zst | |
feat: Add unified reader settings with local overrides (#2230)
* Add initial impl
* fix some format inconsistencies, add indicator in user settings when local is out of sync
* Fix sliders in user settings, unify constants and formatting
* address CodeRabbit suggestions
* add mobile implementation
* address coderabbit nitpicks
* fix responsiveness of the reader settings popover
* Move more of the web UI strings to i18n
* update translations for more coverage
* remove duplicate logic/definitions
* fix android font family
* add shared reading setting hook between web and mobile
* unify reader settings context for both web and mobile
* remove unused export
* address coderabbit suggestions
* fix tests
Diffstat (limited to 'apps/web/lib/readerSettings.tsx')
| -rw-r--r-- | apps/web/lib/readerSettings.tsx | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/apps/web/lib/readerSettings.tsx b/apps/web/lib/readerSettings.tsx new file mode 100644 index 00000000..5966f287 --- /dev/null +++ b/apps/web/lib/readerSettings.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { + createContext, + useCallback, + useContext, + useMemo, + useState, +} from "react"; + +import { + ReaderSettingsProvider as BaseReaderSettingsProvider, + useReaderSettingsContext, +} from "@karakeep/shared-react/hooks/reader-settings"; +import { + ReaderSettings, + ReaderSettingsPartial, +} from "@karakeep/shared/types/readers"; + +const LOCAL_STORAGE_KEY = "karakeep-reader-settings"; + +function getLocalOverridesFromStorage(): ReaderSettingsPartial { + if (typeof window === "undefined") return {}; + try { + const stored = localStorage.getItem(LOCAL_STORAGE_KEY); + return stored ? JSON.parse(stored) : {}; + } catch { + return {}; + } +} + +function saveLocalOverridesToStorage(overrides: ReaderSettingsPartial): void { + if (typeof window === "undefined") return; + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(overrides)); +} + +// Session overrides context - web-specific feature for live preview +interface SessionOverridesContextValue { + sessionOverrides: ReaderSettingsPartial; + setSessionOverrides: React.Dispatch< + React.SetStateAction<ReaderSettingsPartial> + >; +} + +const SessionOverridesContext = + createContext<SessionOverridesContextValue | null>(null); + +export function ReaderSettingsProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [sessionOverrides, setSessionOverrides] = + useState<ReaderSettingsPartial>({}); + + const sessionValue = useMemo( + () => ({ + sessionOverrides, + setSessionOverrides, + }), + [sessionOverrides], + ); + + // Memoize callbacks to prevent unnecessary re-renders + const getLocalOverrides = useCallback(getLocalOverridesFromStorage, []); + const saveLocalOverrides = useCallback(saveLocalOverridesToStorage, []); + const onClearSessionOverrides = useCallback(() => { + setSessionOverrides({}); + }, []); + + return ( + <BaseReaderSettingsProvider + getLocalOverrides={getLocalOverrides} + saveLocalOverrides={saveLocalOverrides} + sessionOverrides={sessionOverrides} + onClearSessionOverrides={onClearSessionOverrides} + > + <SessionOverridesContext.Provider value={sessionValue}> + {children} + </SessionOverridesContext.Provider> + </BaseReaderSettingsProvider> + ); +} + +export function useReaderSettings() { + const sessionContext = useContext(SessionOverridesContext); + if (!sessionContext) { + throw new Error( + "useReaderSettings must be used within a ReaderSettingsProvider", + ); + } + + const { sessionOverrides, setSessionOverrides } = sessionContext; + const baseSettings = useReaderSettingsContext(); + + // Update session override (live preview, not persisted) + const updateSession = useCallback( + (updates: ReaderSettingsPartial) => { + setSessionOverrides((prev) => ({ ...prev, ...updates })); + }, + [setSessionOverrides], + ); + + // Clear all session overrides + const clearSession = useCallback(() => { + setSessionOverrides({}); + }, [setSessionOverrides]); + + // Save current settings to local storage (this device only) + const saveToDevice = useCallback(() => { + const newLocalOverrides = { + ...baseSettings.localOverrides, + ...sessionOverrides, + }; + baseSettings.setLocalOverrides(newLocalOverrides); + saveLocalOverridesToStorage(newLocalOverrides); + setSessionOverrides({}); + }, [baseSettings, sessionOverrides, setSessionOverrides]); + + // Clear a single local override + const clearLocalOverride = useCallback( + (key: keyof ReaderSettings) => { + baseSettings.clearLocal(key); + }, + [baseSettings], + ); + + // Check if there are unsaved session changes + const hasSessionChanges = Object.keys(sessionOverrides).length > 0; + + return { + // Current effective settings (what should be displayed) + settings: baseSettings.settings, + + // Raw values for UI indicators + serverSettings: baseSettings.serverDefaults, + localOverrides: baseSettings.localOverrides, + sessionOverrides, + + // State indicators + hasSessionChanges, + hasLocalOverrides: baseSettings.hasLocalOverrides, + isSaving: baseSettings.isSaving, + + // Actions + updateSession, + clearSession, + saveToDevice, + clearLocalOverrides: baseSettings.clearAllLocal, + clearLocalOverride, + saveToServer: baseSettings.saveAsDefault, + updateServerSetting: baseSettings.saveAsDefault, + clearServerDefaults: baseSettings.clearAllDefaults, + }; +} |
