aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile
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
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')
-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
-rw-r--r--apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx43
-rw-r--r--apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx6
-rw-r--r--apps/mobile/lib/providers.tsx5
-rw-r--r--apps/mobile/lib/readerSettings.tsx93
-rw-r--r--apps/mobile/lib/settings.ts8
9 files changed, 443 insertions, 23 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>
+ );
+}
diff --git a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
index 730bcd08..e0b592d6 100644
--- a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
@@ -5,6 +5,7 @@ import WebView from "react-native-webview";
import { WebViewSourceUri } from "react-native-webview/lib/WebViewTypes";
import { Text } from "@/components/ui/Text";
import { useAssetUrl } from "@/lib/hooks";
+import { useReaderSettings, WEBVIEW_FONT_FAMILIES } from "@/lib/readerSettings";
import { api } from "@/lib/trpc";
import { useColorScheme } from "@/lib/useColorScheme";
@@ -38,6 +39,7 @@ export function BookmarkLinkReaderPreview({
bookmark: ZBookmark;
}) {
const { isDarkColorScheme: isDark } = useColorScheme();
+ const { settings: readerSettings } = useReaderSettings();
const {
data: bookmarkWithContent,
@@ -61,6 +63,10 @@ export function BookmarkLinkReaderPreview({
throw new Error("Wrong content type rendered");
}
+ const fontFamily = WEBVIEW_FONT_FAMILIES[readerSettings.fontFamily];
+ const fontSize = readerSettings.fontSize;
+ const lineHeight = readerSettings.lineHeight;
+
return (
<View className="flex-1 bg-background">
<WebView
@@ -73,8 +79,9 @@ export function BookmarkLinkReaderPreview({
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
- line-height: 1.6;
+ font-family: ${fontFamily};
+ font-size: ${fontSize}px;
+ line-height: ${lineHeight};
color: ${isDark ? "#e5e7eb" : "#374151"};
margin: 0;
padding: 16px;
@@ -85,17 +92,29 @@ export function BookmarkLinkReaderPreview({
img { max-width: 100%; height: auto; border-radius: 8px; }
a { color: #3b82f6; text-decoration: none; }
a:hover { text-decoration: underline; }
- blockquote {
- border-left: 4px solid ${isDark ? "#374151" : "#e5e7eb"};
- margin: 1em 0;
- padding-left: 1em;
- color: ${isDark ? "#9ca3af" : "#6b7280"};
+ blockquote {
+ border-left: 4px solid ${isDark ? "#374151" : "#e5e7eb"};
+ margin: 1em 0;
+ padding-left: 1em;
+ color: ${isDark ? "#9ca3af" : "#6b7280"};
+ }
+ pre, code {
+ font-family: ui-monospace, Menlo, Monaco, 'Courier New', monospace;
+ background: ${isDark ? "#1f2937" : "#f3f4f6"};
+ }
+ pre {
+ padding: 1em;
+ border-radius: 6px;
+ overflow-x: auto;
+ }
+ code {
+ padding: 0.2em 0.4em;
+ border-radius: 3px;
+ font-size: 0.9em;
}
- pre {
- background: ${isDark ? "#1f2937" : "#f3f4f6"};
- padding: 1em;
- border-radius: 6px;
- overflow-x: auto;
+ pre code {
+ padding: 0;
+ background: none;
}
</style>
</head>
diff --git a/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx b/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
index 58cbcc8d..c7fd4be3 100644
--- a/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
@@ -43,7 +43,7 @@ export default function BookmarkLinkTypeSelector({
}: BookmarkLinkTypeSelectorProps) {
const availableTypes = getAvailableViewTypes(bookmark);
- const allActions = [
+ const viewActions = [
{
id: "reader" as const,
title: "Reader View",
@@ -66,7 +66,7 @@ export default function BookmarkLinkTypeSelector({
},
];
- const availableActions = allActions.filter((action) =>
+ const availableViewActions = viewActions.filter((action) =>
availableTypes.includes(action.id),
);
@@ -76,7 +76,7 @@ export default function BookmarkLinkTypeSelector({
Haptics.selectionAsync();
onChange(nativeEvent.event as BookmarkLinkType);
}}
- actions={availableActions}
+ actions={availableViewActions}
shouldOpenOnLongPress={false}
>
<ChevronDown onPress={() => Haptics.selectionAsync()} color="gray" />
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 (
<TRPCProvider settings={settings}>
- <ToastProvider>{children}</ToastProvider>
+ <ReaderSettingsProvider>
+ <ToastProvider>{children}</ToastProvider>
+ </ReaderSettingsProvider>
</TRPCProvider>
);
}
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<ZReaderFontFamily, string> = {
+ 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 (
+ <BaseReaderSettingsProvider
+ getLocalOverrides={getLocalOverrides}
+ saveLocalOverrides={saveLocalOverrides}
+ >
+ {children}
+ </BaseReaderSettingsProvider>
+ );
+}
+
+// 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<typeof zSettingsSchema>;
@@ -73,3 +79,5 @@ export default function useAppSettings() {
return { ...settings, setSettings, load };
}
+
+export { useSettings };