aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile/app
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/mobile/app
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/mobile/app')
-rw-r--r--apps/mobile/app/dashboard/(tabs)/settings.tsx12
-rw-r--r--apps/mobile/app/dashboard/_layout.tsx8
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx27
-rw-r--r--apps/mobile/app/dashboard/settings/reader-settings.tsx264
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>
+ );
+}