aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/lib/readerSettings.tsx
diff options
context:
space:
mode:
authorEvan Simkowitz <esimkowitz@users.noreply.github.com>2025-12-14 16:39:25 -0800
committerGitHub <noreply@github.com>2025-12-15 00:39:25 +0000
commit7f4202afd73105b850498b55ad66922b3505f0e3 (patch)
treea45f9f1b2599f4c9925e36dc51563b06ba6854ac /apps/web/lib/readerSettings.tsx
parent6db14ac492cd5d9e26d0d986513771f14faa7fd0 (diff)
downloadkarakeep-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.tsx155
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,
+ };
+}