'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 (
);
}
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) => (
))}
)}
{!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"
/>
)}
);
}