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/app/dashboard/(tabs)/settings.tsx | 12 + apps/mobile/app/dashboard/_layout.tsx | 8 + .../app/dashboard/bookmarks/[slug]/index.tsx | 27 ++- .../app/dashboard/settings/reader-settings.tsx | 264 +++++++++++++++++++++ 4 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 apps/mobile/app/dashboard/settings/reader-settings.tsx (limited to 'apps/mobile/app') diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index 76216e00..7c1e00d6 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -87,6 +87,18 @@ export default function Dashboard() { + + + + Reader Text Settings + + + + Show note preview in bookmark + ); } diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx index 7bf0f118..8fd04115 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { KeyboardAvoidingView } from "react-native"; +import { KeyboardAvoidingView, Pressable, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Stack, useLocalSearchParams } from "expo-router"; +import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import BookmarkAssetView from "@/components/bookmarks/BookmarkAssetView"; import BookmarkLinkTypeSelector, { BookmarkLinkType, @@ -13,12 +13,14 @@ import FullPageError from "@/components/FullPageError"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; +import { Settings } from "lucide-react-native"; import { useColorScheme } from "nativewind"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; export default function BookmarkView() { const insets = useSafeAreaInsets(); + const router = useRouter(); const { slug } = useLocalSearchParams(); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; @@ -87,11 +89,22 @@ export default function BookmarkView() { headerTintColor: isDark ? "#fff" : "#000", headerRight: () => bookmark.content.type === BookmarkTypes.LINK ? ( - setBookmarkLinkType(type)} - bookmark={bookmark} - /> + + {bookmarkLinkType === "reader" && ( + + router.push("/dashboard/settings/reader-settings") + } + > + + + )} + setBookmarkLinkType(type)} + bookmark={bookmark} + /> + ) : undefined, }} /> diff --git a/apps/mobile/app/dashboard/settings/reader-settings.tsx b/apps/mobile/app/dashboard/settings/reader-settings.tsx new file mode 100644 index 00000000..6c522557 --- /dev/null +++ b/apps/mobile/app/dashboard/settings/reader-settings.tsx @@ -0,0 +1,264 @@ +import { useEffect, useState } from "react"; +import { Pressable, ScrollView, View } from "react-native"; +import { Slider } from "react-native-awesome-slider"; +import { runOnJS, useSharedValue } from "react-native-reanimated"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { Divider } from "@/components/ui/Divider"; +import { Text } from "@/components/ui/Text"; +import { MOBILE_FONT_FAMILIES, useReaderSettings } from "@/lib/readerSettings"; +import { useColorScheme } from "@/lib/useColorScheme"; +import { Check, RotateCcw } from "lucide-react-native"; + +import { + formatFontFamily, + formatFontSize, + formatLineHeight, + READER_SETTING_CONSTRAINTS, +} from "@karakeep/shared/types/readers"; +import { ZReaderFontFamily } from "@karakeep/shared/types/users"; + +export default function ReaderSettingsPage() { + const { isDarkColorScheme: isDark } = useColorScheme(); + + const { + settings, + localOverrides, + hasLocalOverrides, + hasServerDefaults, + updateLocal, + clearAllLocal, + saveAsDefault, + clearAllDefaults, + } = useReaderSettings(); + + const { + fontSize: effectiveFontSize, + lineHeight: effectiveLineHeight, + fontFamily: effectiveFontFamily, + } = settings; + + // Shared values for sliders + const fontSizeProgress = useSharedValue(effectiveFontSize); + const fontSizeMin = useSharedValue( + READER_SETTING_CONSTRAINTS.fontSize.min, + ); + const fontSizeMax = useSharedValue( + READER_SETTING_CONSTRAINTS.fontSize.max, + ); + + const lineHeightProgress = useSharedValue(effectiveLineHeight); + const lineHeightMin = useSharedValue( + READER_SETTING_CONSTRAINTS.lineHeight.min, + ); + const lineHeightMax = useSharedValue( + READER_SETTING_CONSTRAINTS.lineHeight.max, + ); + + // Display values for showing rounded values while dragging + const [displayFontSize, setDisplayFontSize] = useState(effectiveFontSize); + const [displayLineHeight, setDisplayLineHeight] = + useState(effectiveLineHeight); + + // Sync slider progress and display values with effective settings + useEffect(() => { + fontSizeProgress.value = effectiveFontSize; + setDisplayFontSize(effectiveFontSize); + }, [effectiveFontSize]); + + useEffect(() => { + lineHeightProgress.value = effectiveLineHeight; + setDisplayLineHeight(effectiveLineHeight); + }, [effectiveLineHeight]); + + const handleFontFamilyChange = (fontFamily: ZReaderFontFamily) => { + updateLocal({ fontFamily }); + }; + + const handleFontSizeChange = (value: number) => { + updateLocal({ fontSize: Math.round(value) }); + }; + + const handleLineHeightChange = (value: number) => { + updateLocal({ lineHeight: Math.round(value * 10) / 10 }); + }; + + const handleSaveAsDefault = () => { + saveAsDefault(); + // Note: clearAllLocal is called automatically in the shared hook's onSuccess + }; + + const handleClearLocalOverrides = () => { + clearAllLocal(); + }; + + const handleClearServerDefaults = () => { + clearAllDefaults(); + }; + + const fontFamilyOptions: ZReaderFontFamily[] = ["serif", "sans", "mono"]; + + return ( + + + {/* Font Family Selection */} + + + Font Family + {localOverrides.fontFamily !== undefined && ( + (local) + )} + + + {fontFamilyOptions.map((fontFamily, index) => { + const isChecked = effectiveFontFamily === fontFamily; + return ( + + handleFontFamilyChange(fontFamily)} + className="flex flex-row items-center justify-between py-2" + > + + {formatFontFamily(fontFamily)} + + {isChecked && } + + {index < fontFamilyOptions.length - 1 && ( + + )} + + ); + })} + + + + {/* Font Size */} + + + Font Size ({formatFontSize(displayFontSize)}) + {localOverrides.fontSize !== undefined && ( + (local) + )} + + + + {READER_SETTING_CONSTRAINTS.fontSize.min} + + + null} + onValueChange={(value) => { + "worklet"; + runOnJS(setDisplayFontSize)(Math.round(value)); + }} + onSlidingComplete={(value) => + handleFontSizeChange(Math.round(value)) + } + /> + + + {READER_SETTING_CONSTRAINTS.fontSize.max} + + + + + {/* Line Height */} + + + Line Height ({formatLineHeight(displayLineHeight)}) + {localOverrides.lineHeight !== undefined && ( + (local) + )} + + + + {READER_SETTING_CONSTRAINTS.lineHeight.min} + + + null} + onValueChange={(value) => { + "worklet"; + runOnJS(setDisplayLineHeight)(Math.round(value * 10) / 10); + }} + onSlidingComplete={handleLineHeightChange} + /> + + + {READER_SETTING_CONSTRAINTS.lineHeight.max} + + + + + {/* Preview */} + + + Preview + + + + The quick brown fox jumps over the lazy dog. Pack my box with five + dozen liquor jugs. How vexingly quick daft zebras jump! + + + + + + + {/* Save as Default */} + + + Save as Default (All Devices) + + + + {/* Clear Local */} + {hasLocalOverrides && ( + + + Clear Local Overrides + + )} + + {/* Clear Server */} + {hasServerDefaults && ( + + + Clear Server Defaults + + )} + + + ); +} -- cgit v1.2.3-70-g09d2