import { useCallback, useEffect, useRef, 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 { ReaderPreview, ReaderPreviewRef, } from "@/components/reader/ReaderPreview"; 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); // Refs to track latest display values (avoids stale closures in callbacks) const displayFontSizeRef = useRef(displayFontSize); displayFontSizeRef.current = displayFontSize; const displayLineHeightRef = useRef(displayLineHeight); displayLineHeightRef.current = displayLineHeight; // Ref for the WebView preview component const previewRef = useRef(null); // Functions to update preview styles via IPC (called from worklets via runOnJS) const updatePreviewFontSize = useCallback( (fontSize: number) => { setDisplayFontSize(fontSize); previewRef.current?.updateStyles( effectiveFontFamily, fontSize, displayLineHeightRef.current, ); }, [effectiveFontFamily], ); const updatePreviewLineHeight = useCallback( (lineHeight: number) => { setDisplayLineHeight(lineHeight); previewRef.current?.updateStyles( effectiveFontFamily, displayFontSizeRef.current, lineHeight, ); }, [effectiveFontFamily], ); // 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 }); // Update preview immediately with new font family previewRef.current?.updateStyles( fontFamily, displayFontSize, displayLineHeight, ); }; 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(updatePreviewFontSize)(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(updatePreviewLineHeight)(Math.round(value * 10) / 10); }} onSlidingComplete={handleLineHeightChange} /> {READER_SETTING_CONSTRAINTS.lineHeight.max} {/* Preview */} Preview {/* Save as Default */} Save as Default (All Devices) {/* Clear Local */} {hasLocalOverrides && ( Clear Local Overrides )} {/* Clear Server */} {hasServerDefaults && ( Clear Server Defaults )} ); }