From 7f4202afd73105b850498b55ad66922b3505f0e3 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 14 Dec 2025 16:39:25 -0800 Subject: 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 --- apps/mobile/lib/providers.tsx | 5 +- apps/mobile/lib/readerSettings.tsx | 93 ++++++++++++++++++++++++++++++++++++++ apps/mobile/lib/settings.ts | 8 ++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/lib/readerSettings.tsx (limited to 'apps/mobile/lib') diff --git a/apps/mobile/lib/providers.tsx b/apps/mobile/lib/providers.tsx index 938b8aeb..36ed7e71 100644 --- a/apps/mobile/lib/providers.tsx +++ b/apps/mobile/lib/providers.tsx @@ -4,6 +4,7 @@ import { ToastProvider } from "@/components/ui/Toast"; import { TRPCProvider } from "@karakeep/shared-react/providers/trpc-provider"; +import { ReaderSettingsProvider } from "./readerSettings"; import useAppSettings from "./settings"; export function Providers({ children }: { children: React.ReactNode }) { @@ -20,7 +21,9 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + ); } diff --git a/apps/mobile/lib/readerSettings.tsx b/apps/mobile/lib/readerSettings.tsx new file mode 100644 index 00000000..9a3fc835 --- /dev/null +++ b/apps/mobile/lib/readerSettings.tsx @@ -0,0 +1,93 @@ +import { ReactNode, useCallback } from "react"; +import { Platform } from "react-native"; + +import { + ReaderSettingsProvider as BaseReaderSettingsProvider, + useReaderSettingsContext, +} from "@karakeep/shared-react/hooks/reader-settings"; +import { ReaderSettingsPartial } from "@karakeep/shared/types/readers"; +import { ZReaderFontFamily } from "@karakeep/shared/types/users"; + +import { useSettings } from "./settings"; + +// Mobile-specific font families for native Text components +// On Android, use generic font family names: "serif", "sans-serif", "monospace" +// On iOS, use specific font names like "Georgia" and "Courier" +// Note: undefined means use the system default font +export const MOBILE_FONT_FAMILIES: Record< + ZReaderFontFamily, + string | undefined +> = Platform.select({ + android: { + serif: "serif", + sans: undefined, + mono: "monospace", + }, + default: { + serif: "Georgia", + sans: undefined, + mono: "Courier", + }, +})!; + +// Font families for WebView HTML content (CSS font stacks) +export const WEBVIEW_FONT_FAMILIES: Record = { + serif: "Georgia, 'Times New Roman', serif", + sans: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif", + mono: "ui-monospace, Menlo, Monaco, 'Courier New', monospace", +} as const; + +/** + * Mobile-specific provider for reader settings. + * Wraps the shared provider with mobile storage callbacks. + */ +export function ReaderSettingsProvider({ children }: { children: ReactNode }) { + // Read from zustand store directly to keep callback stable (empty deps). + const getLocalOverrides = useCallback((): ReaderSettingsPartial => { + const currentSettings = useSettings.getState().settings.settings; + return { + fontSize: currentSettings.readerFontSize, + lineHeight: currentSettings.readerLineHeight, + fontFamily: currentSettings.readerFontFamily, + }; + }, []); + + const saveLocalOverrides = useCallback((overrides: ReaderSettingsPartial) => { + const currentSettings = useSettings.getState().settings.settings; + // Remove reader settings keys first, then add back only defined ones + const { + readerFontSize: _fs, + readerLineHeight: _lh, + readerFontFamily: _ff, + ...rest + } = currentSettings; + + const newSettings = { ...rest }; + if (overrides.fontSize !== undefined) { + (newSettings as typeof currentSettings).readerFontSize = + overrides.fontSize; + } + if (overrides.lineHeight !== undefined) { + (newSettings as typeof currentSettings).readerLineHeight = + overrides.lineHeight; + } + if (overrides.fontFamily !== undefined) { + (newSettings as typeof currentSettings).readerFontFamily = + overrides.fontFamily; + } + + useSettings.getState().setSettings(newSettings); + }, []); + + return ( + + {children} + + ); +} + +// Re-export the context hook as useReaderSettings for mobile consumers +export { useReaderSettingsContext as useReaderSettings }; diff --git a/apps/mobile/lib/settings.ts b/apps/mobile/lib/settings.ts index 40a33976..745c778d 100644 --- a/apps/mobile/lib/settings.ts +++ b/apps/mobile/lib/settings.ts @@ -2,6 +2,8 @@ import * as SecureStore from "expo-secure-store"; import { z } from "zod"; import { create } from "zustand"; +import { zReaderFontFamilySchema } from "@karakeep/shared/types/users"; + const SETTING_NAME = "settings"; const zSettingsSchema = z.object({ @@ -16,6 +18,10 @@ const zSettingsSchema = z.object({ .default("reader"), showNotes: z.boolean().optional().default(false), customHeaders: z.record(z.string(), z.string()).optional().default({}), + // Reader settings (local device overrides) + readerFontSize: z.number().int().min(12).max(24).optional(), + readerLineHeight: z.number().min(1.2).max(2.5).optional(), + readerFontFamily: zReaderFontFamilySchema.optional(), }); export type Settings = z.infer; @@ -73,3 +79,5 @@ export default function useAppSettings() { return { ...settings, setSettings, load }; } + +export { useSettings }; -- cgit v1.2.3-70-g09d2