'use client'; import { useState, useEffect, type ChangeEvent } from 'react'; import { useConfig, ViewType } from '@/contexts/ConfigContext'; import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell'; import { SEGMENT_PRELOAD_DEPTH_MIN, SEGMENT_PRELOAD_DEPTH_MAX, SEGMENT_PRELOAD_SENTENCE_LOOKAHEAD_MIN, SEGMENT_PRELOAD_SENTENCE_LOOKAHEAD_MAX, TTS_SEGMENT_MAX_BLOCK_LENGTH_MIN, TTS_SEGMENT_MAX_BLOCK_LENGTH_MAX, TTS_SEGMENT_MAX_BLOCK_LENGTH_STEP, clampSegmentPreloadDepth, clampSegmentPreloadSentenceLookahead, clampTtsSegmentMaxBlockLength, } from '@/types/config'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { Section, ToggleRow, segmentedButtonClass, segmentedGroupClass } from '@/components/formPrimitives'; const viewTypeTextMapping = [ { id: 'single', name: 'Single Page' }, { id: 'dual', name: 'Two Pages' }, { id: 'scroll', name: 'Continuous Scroll' }, ]; const rangeInputClassName = 'w-full bg-offbase rounded-md appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-md [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-md [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent'; type MarginKey = 'header' | 'footer' | 'left' | 'right'; type RangeSettingProps = { label: string; value: number; min: number; max: number; step: number; description: string; valueWidth?: string; formatter?: (value: number) => string; onChange: (value: number) => void; }; function RangeSetting({ label, value, min, max, step, description, valueWidth = 'w-10', formatter = (next) => String(next), onChange, }: RangeSettingProps) { return (
onChange(Number(event.target.value))} className={`flex-1 ${rangeInputClassName}`} /> {formatter(value)}

{description}

); } export function DocumentSettings({ isOpen, setIsOpen, epub, html }: { isOpen: boolean, setIsOpen: (isOpen: boolean) => void, epub?: boolean, html?: boolean }) { const canWordHighlight = useFeatureFlag('enableWordHighlight'); const { viewType, skipBlank, epubTheme, smartSentenceSplitting, segmentPreloadDepthPages, segmentPreloadSentenceLookahead, ttsSegmentMaxBlockLength, headerMargin, footerMargin, leftMargin, rightMargin, updateConfigKey, pdfHighlightEnabled, epubHighlightEnabled, pdfWordHighlightEnabled, epubWordHighlightEnabled, } = useConfig(); const [localMargins, setLocalMargins] = useState({ header: headerMargin, footer: footerMargin, left: leftMargin, right: rightMargin }); const selectedView = viewTypeTextMapping.find(v => v.id === viewType) || viewTypeTextMapping[0]; const [localPreloadDepth, setLocalPreloadDepth] = useState(segmentPreloadDepthPages); const [localSentenceLookahead, setLocalSentenceLookahead] = useState(segmentPreloadSentenceLookahead); const [localMaxBlockLength, setLocalMaxBlockLength] = useState(ttsSegmentMaxBlockLength); const marginValues: Record = { header: headerMargin, footer: footerMargin, left: leftMargin, right: rightMargin, }; useEffect(() => { setLocalMargins({ header: headerMargin, footer: footerMargin, left: leftMargin, right: rightMargin }); }, [headerMargin, footerMargin, leftMargin, rightMargin]); useEffect(() => { setLocalPreloadDepth(segmentPreloadDepthPages); }, [segmentPreloadDepthPages]); useEffect(() => { setLocalSentenceLookahead(segmentPreloadSentenceLookahead); }, [segmentPreloadSentenceLookahead]); useEffect(() => { setLocalMaxBlockLength(ttsSegmentMaxBlockLength); }, [ttsSegmentMaxBlockLength]); // Handler for slider release const handleMarginChangeComplete = (margin: MarginKey) => () => { const value = localMargins[margin]; const configKey = `${margin}Margin`; if (value !== marginValues[margin]) { updateConfigKey(configKey as 'headerMargin' | 'footerMargin' | 'leftMargin' | 'rightMargin', value); } }; const handleMarginSliderChange = (margin: MarginKey) => (event: ChangeEvent) => { setLocalMargins((previous) => ({ ...previous, [margin]: Number(event.target.value), })); }; return ( setIsOpen(false)} ariaLabel="Document settings" title="Reader Settings" subtitle="Tune layout, preloading, and playback." bodyClassName="flex-1 overflow-y-auto px-4 py-4 bg-[radial-gradient(circle_at_top_right,color-mix(in_srgb,var(--accent),transparent_92%),transparent_35%)]" panelClassName="w-full sm:w-[30rem]" >
{!html && (
updateConfigKey('skipBlank', checked)} variant="flat" /> updateConfigKey('smartSentenceSplitting', checked)} variant="flat" />
String(value)} onChange={(value) => { const next = clampSegmentPreloadDepth(value); setLocalPreloadDepth(next); void updateConfigKey('segmentPreloadDepthPages', next); }} /> String(value)} onChange={(value) => { const next = clampSegmentPreloadSentenceLookahead(value); setLocalSentenceLookahead(next); void updateConfigKey('segmentPreloadSentenceLookahead', next); }} /> String(value)} onChange={(value) => { const next = clampTtsSegmentMaxBlockLength(value); setLocalMaxBlockLength(next); void updateConfigKey('ttsSegmentMaxBlockLength', next); }} />
)} {!epub && !html && (
{viewTypeTextMapping.map((view) => { const active = selectedView.id === view.id; return ( ); })}
{selectedView.id === 'scroll' ? (

Scroll mode may be slower on large PDFs.

) : null}

Text extraction margins

Ignore edge content before extraction.

{(['header', 'footer', 'left', 'right'] as MarginKey[]).map((margin) => (
{margin} {Math.round(localMargins[margin] * 100)}%
))}
)} {!epub && !html && (
updateConfigKey('pdfHighlightEnabled', checked)} variant="flat" /> updateConfigKey('pdfWordHighlightEnabled', checked)} variant="flat" />
)} {epub && (
updateConfigKey('epubTheme', checked)} variant="flat" /> updateConfigKey('epubHighlightEnabled', checked)} variant="flat" /> updateConfigKey('epubWordHighlightEnabled', checked)} variant="flat" />
)}
); }