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 --- .../dashboard/preview/LinkContentSection.tsx | 34 +- .../dashboard/preview/ReaderSettingsPopover.tsx | 457 +++++++++++++++++++++ 2 files changed, 480 insertions(+), 11 deletions(-) create mode 100644 apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx (limited to 'apps/web/components/dashboard') diff --git a/apps/web/components/dashboard/preview/LinkContentSection.tsx b/apps/web/components/dashboard/preview/LinkContentSection.tsx index 64b62df6..75b6df14 100644 --- a/apps/web/components/dashboard/preview/LinkContentSection.tsx +++ b/apps/web/components/dashboard/preview/LinkContentSection.tsx @@ -17,6 +17,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useTranslation } from "@/lib/i18n/client"; +import { useReaderSettings } from "@/lib/readerSettings"; import { AlertTriangle, Archive, @@ -34,8 +35,10 @@ import { ZBookmark, ZBookmarkedLink, } from "@karakeep/shared/types/bookmarks"; +import { READER_FONT_FAMILIES } from "@karakeep/shared/types/readers"; import { contentRendererRegistry } from "./content-renderers"; +import ReaderSettingsPopover from "./ReaderSettingsPopover"; import ReaderView from "./ReaderView"; function CustomRendererErrorFallback({ error }: { error: Error }) { @@ -106,6 +109,7 @@ export default function LinkContentSection({ bookmark: ZBookmark; }) { const { t } = useTranslation(); + const { settings } = useReaderSettings(); const availableRenderers = contentRendererRegistry.getRenderers(bookmark); const defaultSection = availableRenderers.length > 0 ? availableRenderers[0].id : "cached"; @@ -135,6 +139,11 @@ export default function LinkContentSection({ @@ -213,17 +222,20 @@ export default function LinkContentSection({ {section === "cached" && ( - - - - - - - FullScreen - + <> + + + + + + + + FullScreen + + )} {content} diff --git a/apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx b/apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx new file mode 100644 index 00000000..f37b8263 --- /dev/null +++ b/apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx @@ -0,0 +1,457 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Slider } from "@/components/ui/slider"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useTranslation } from "@/lib/i18n/client"; +import { useReaderSettings } from "@/lib/readerSettings"; +import { + Globe, + Laptop, + Minus, + Plus, + RotateCcw, + Settings, + Type, + X, +} from "lucide-react"; + +import { + formatFontSize, + formatLineHeight, + READER_DEFAULTS, + READER_SETTING_CONSTRAINTS, +} from "@karakeep/shared/types/readers"; + +interface ReaderSettingsPopoverProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + variant?: "outline" | "ghost"; +} + +export default function ReaderSettingsPopover({ + open, + onOpenChange, + variant = "outline", +}: ReaderSettingsPopoverProps) { + const { t } = useTranslation(); + const { + settings, + serverSettings, + localOverrides, + sessionOverrides, + hasSessionChanges, + hasLocalOverrides, + isSaving, + updateSession, + clearSession, + saveToDevice, + clearLocalOverride, + saveToServer, + } = useReaderSettings(); + + // Helper to get the effective server value (server setting or default) + const getServerValue = (key: K) => { + return serverSettings[key] ?? READER_DEFAULTS[key]; + }; + + // Helper to check if a setting has a local override + const hasLocalOverride = (key: keyof typeof localOverrides) => { + return localOverrides[key] !== undefined; + }; + + // Build tooltip message for the settings button + const getSettingsTooltip = () => { + if (hasSessionChanges && hasLocalOverrides) { + return t("settings.info.reader_settings.tooltip_preview_and_local"); + } + if (hasSessionChanges) { + return t("settings.info.reader_settings.tooltip_preview"); + } + if (hasLocalOverrides) { + return t("settings.info.reader_settings.tooltip_local"); + } + return t("settings.info.reader_settings.tooltip_default"); + }; + + return ( + + + + + + + + +

{getSettingsTooltip()}

+
+
+ +
+
+
+ +

+ {t("settings.info.reader_settings.title")} +

+
+ {hasSessionChanges && ( + + {t("settings.info.reader_settings.preview")} + + )} +
+ +
+
+
+ +
+ {sessionOverrides.fontFamily !== undefined && ( + + {t("settings.info.reader_settings.preview_inline")} + + )} + {hasLocalOverride("fontFamily") && + sessionOverrides.fontFamily === undefined && ( + + + + + +

+ {t( + "settings.info.reader_settings.clear_override_hint", + { + value: t( + `settings.info.reader_settings.${getServerValue("fontFamily")}` as const, + ), + }, + )} +

+
+
+ )} +
+
+ +
+ +
+
+ +
+ + {formatFontSize(settings.fontSize)} + {sessionOverrides.fontSize !== undefined && ( + + {t("settings.info.reader_settings.preview_inline")} + + )} + + {hasLocalOverride("fontSize") && + sessionOverrides.fontSize === undefined && ( + + + + + +

+ {t( + "settings.info.reader_settings.clear_override_hint", + { + value: formatFontSize( + getServerValue("fontSize"), + ), + }, + )} +

+
+
+ )} +
+
+
+ + + updateSession({ fontSize: value }) + } + max={READER_SETTING_CONSTRAINTS.fontSize.max} + min={READER_SETTING_CONSTRAINTS.fontSize.min} + step={READER_SETTING_CONSTRAINTS.fontSize.step} + className={`flex-1 ${ + hasLocalOverride("fontSize") && + sessionOverrides.fontSize === undefined + ? "[&_[role=slider]]:border-primary/50" + : "" + }`} + /> + +
+
+ +
+
+ +
+ + {formatLineHeight(settings.lineHeight)} + {sessionOverrides.lineHeight !== undefined && ( + + {t("settings.info.reader_settings.preview_inline")} + + )} + + {hasLocalOverride("lineHeight") && + sessionOverrides.lineHeight === undefined && ( + + + + + +

+ {t( + "settings.info.reader_settings.clear_override_hint", + { + value: formatLineHeight( + getServerValue("lineHeight"), + ), + }, + )} +

+
+
+ )} +
+
+
+ + + updateSession({ lineHeight: value }) + } + max={READER_SETTING_CONSTRAINTS.lineHeight.max} + min={READER_SETTING_CONSTRAINTS.lineHeight.min} + step={READER_SETTING_CONSTRAINTS.lineHeight.step} + className={`flex-1 ${ + hasLocalOverride("lineHeight") && + sessionOverrides.lineHeight === undefined + ? "[&_[role=slider]]:border-primary/50" + : "" + }`} + /> + +
+
+ + {hasSessionChanges && ( + <> + + +
+ + +
+ + +
+ +

+ {t("settings.info.reader_settings.save_hint")} +

+
+ + )} + + {!hasSessionChanges && ( +

+ {t("settings.info.reader_settings.adjust_hint")} +

+ )} +
+
+
+
+ ); +} -- cgit v1.2.3-70-g09d2