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/mobile/app | |
| 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/mobile/app')
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/settings.tsx | 12 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/_layout.tsx | 8 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx | 27 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/settings/reader-settings.tsx | 264 |
4 files changed, 304 insertions, 7 deletions
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 @@ -88,6 +88,18 @@ export default function Dashboard() { </Link> </View> <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> + <Link + asChild + href="/dashboard/settings/reader-settings" + className="flex-1" + > + <Pressable className="flex flex-row justify-between"> + <Text>Reader Text Settings</Text> + <ChevronRight /> + </Pressable> + </Link> + </View> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> <Text>Show note preview in bookmark</Text> <Switch value={settings.showNotes} diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx index eb1cbe4b..260071f0 100644 --- a/apps/mobile/app/dashboard/_layout.tsx +++ b/apps/mobile/app/dashboard/_layout.tsx @@ -144,6 +144,14 @@ export default function Dashboard() { headerBackTitle: "Back", }} /> + <Stack.Screen + name="settings/reader-settings" + options={{ + title: "Reader Settings", + headerTitle: "Reader Settings", + headerBackTitle: "Back", + }} + /> </StyledStack> ); } 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 ? ( - <BookmarkLinkTypeSelector - type={bookmarkLinkType} - onChange={(type) => setBookmarkLinkType(type)} - bookmark={bookmark} - /> + <View className="flex-row items-center gap-3"> + {bookmarkLinkType === "reader" && ( + <Pressable + onPress={() => + router.push("/dashboard/settings/reader-settings") + } + > + <Settings size={20} color="gray" /> + </Pressable> + )} + <BookmarkLinkTypeSelector + type={bookmarkLinkType} + onChange={(type) => setBookmarkLinkType(type)} + bookmark={bookmark} + /> + </View> ) : 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<number>(effectiveFontSize); + const fontSizeMin = useSharedValue<number>( + READER_SETTING_CONSTRAINTS.fontSize.min, + ); + const fontSizeMax = useSharedValue<number>( + READER_SETTING_CONSTRAINTS.fontSize.max, + ); + + const lineHeightProgress = useSharedValue<number>(effectiveLineHeight); + const lineHeightMin = useSharedValue<number>( + READER_SETTING_CONSTRAINTS.lineHeight.min, + ); + const lineHeightMax = useSharedValue<number>( + 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 ( + <CustomSafeAreaView> + <ScrollView + className="w-full" + contentContainerClassName="items-center gap-4 px-4 py-2" + > + {/* Font Family Selection */} + <View className="w-full"> + <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground"> + Font Family + {localOverrides.fontFamily !== undefined && ( + <Text className="text-blue-500"> (local)</Text> + )} + </Text> + <View className="w-full rounded-lg bg-card px-4 py-2"> + {fontFamilyOptions.map((fontFamily, index) => { + const isChecked = effectiveFontFamily === fontFamily; + return ( + <View key={fontFamily}> + <Pressable + onPress={() => handleFontFamilyChange(fontFamily)} + className="flex flex-row items-center justify-between py-2" + > + <Text + style={{ + fontFamily: MOBILE_FONT_FAMILIES[fontFamily], + }} + > + {formatFontFamily(fontFamily)} + </Text> + {isChecked && <Check color="rgb(0, 122, 255)" />} + </Pressable> + {index < fontFamilyOptions.length - 1 && ( + <Divider orientation="horizontal" className="h-0.5" /> + )} + </View> + ); + })} + </View> + </View> + + {/* Font Size */} + <View className="w-full"> + <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground"> + Font Size ({formatFontSize(displayFontSize)}) + {localOverrides.fontSize !== undefined && ( + <Text className="text-blue-500"> (local)</Text> + )} + </Text> + <View className="flex w-full flex-row items-center gap-3 rounded-lg bg-card px-4 py-3"> + <Text className="text-muted-foreground"> + {READER_SETTING_CONSTRAINTS.fontSize.min} + </Text> + <View className="flex-1"> + <Slider + progress={fontSizeProgress} + minimumValue={fontSizeMin} + maximumValue={fontSizeMax} + renderBubble={() => null} + onValueChange={(value) => { + "worklet"; + runOnJS(setDisplayFontSize)(Math.round(value)); + }} + onSlidingComplete={(value) => + handleFontSizeChange(Math.round(value)) + } + /> + </View> + <Text className="text-muted-foreground"> + {READER_SETTING_CONSTRAINTS.fontSize.max} + </Text> + </View> + </View> + + {/* Line Height */} + <View className="w-full"> + <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground"> + Line Height ({formatLineHeight(displayLineHeight)}) + {localOverrides.lineHeight !== undefined && ( + <Text className="text-blue-500"> (local)</Text> + )} + </Text> + <View className="flex w-full flex-row items-center gap-3 rounded-lg bg-card px-4 py-3"> + <Text className="text-muted-foreground"> + {READER_SETTING_CONSTRAINTS.lineHeight.min} + </Text> + <View className="flex-1"> + <Slider + progress={lineHeightProgress} + minimumValue={lineHeightMin} + maximumValue={lineHeightMax} + renderBubble={() => null} + onValueChange={(value) => { + "worklet"; + runOnJS(setDisplayLineHeight)(Math.round(value * 10) / 10); + }} + onSlidingComplete={handleLineHeightChange} + /> + </View> + <Text className="text-muted-foreground"> + {READER_SETTING_CONSTRAINTS.lineHeight.max} + </Text> + </View> + </View> + + {/* Preview */} + <View className="w-full"> + <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground"> + Preview + </Text> + <View className="w-full rounded-lg bg-card px-4 py-3"> + <Text + style={{ + fontFamily: MOBILE_FONT_FAMILIES[effectiveFontFamily], + fontSize: effectiveFontSize, + lineHeight: effectiveFontSize * effectiveLineHeight, + }} + className="text-foreground" + > + The quick brown fox jumps over the lazy dog. Pack my box with five + dozen liquor jugs. How vexingly quick daft zebras jump! + </Text> + </View> + </View> + + <Divider orientation="horizontal" className="my-2 w-full" /> + + {/* Save as Default */} + <Pressable + onPress={handleSaveAsDefault} + disabled={!hasLocalOverrides} + className="w-full rounded-lg bg-card px-4 py-3" + > + <Text + className={`text-center ${hasLocalOverrides ? "text-blue-500" : "text-muted-foreground"}`} + > + Save as Default (All Devices) + </Text> + </Pressable> + + {/* Clear Local */} + {hasLocalOverrides && ( + <Pressable + onPress={handleClearLocalOverrides} + className="flex w-full flex-row items-center justify-center gap-2 rounded-lg bg-card px-4 py-3" + > + <RotateCcw size={16} color={isDark ? "#9ca3af" : "#6b7280"} /> + <Text className="text-muted-foreground">Clear Local Overrides</Text> + </Pressable> + )} + + {/* Clear Server */} + {hasServerDefaults && ( + <Pressable + onPress={handleClearServerDefaults} + className="flex w-full flex-row items-center justify-center gap-2 rounded-lg bg-card px-4 py-3" + > + <RotateCcw size={16} color={isDark ? "#9ca3af" : "#6b7280"} /> + <Text className="text-muted-foreground">Clear Server Defaults</Text> + </Pressable> + )} + </ScrollView> + </CustomSafeAreaView> + ); +} |
