import React, { useEffect, useState } from 'react'; import { MdOutlineAutoMode, MdOutlineScreenRotation } from 'react-icons/md'; import { MdOutlineTextRotationNone, MdTextRotateVertical } from 'react-icons/md'; import { IoPhoneLandscapeOutline, IoPhonePortraitOutline } from 'react-icons/io5'; import { TbTextDirectionRtl } from 'react-icons/tb'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useBookDataStore } from '@/store/bookDataStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useResetViewSettings } from '../../hooks/useResetSettings'; import { isCJKEnv } from '@/utils/misc'; import { getStyles } from '@/utils/style'; import { saveAndReload } from '@/utils/reload'; import { getMaxInlineSize } from '@/utils/config'; import { lockScreenOrientation } from '@/utils/bridge'; import { saveViewSettings } from '../../utils/viewSettingsHelper'; import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book'; import { MIGHT_BE_RTL_LANGS } from '@/services/constants'; import { SettingsPanelPanelProp } from './SettingsDialog'; import Select from '@/components/Select'; import NumberInput from './NumberInput'; const LayoutPanel: React.FC = ({ bookKey, onRegisterReset }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore(); const { getBookData } = useBookDataStore(); const view = getView(bookKey); const bookData = getBookData(bookKey)!; const viewSettings = getViewSettings(bookKey)!; const gridInsets = getGridInsets(bookKey)!; const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!); const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!); const [wordSpacing, setWordSpacing] = useState(viewSettings.wordSpacing!); const [letterSpacing, setLetterSpacing] = useState(viewSettings.letterSpacing!); const [textIndent, setTextIndent] = useState(viewSettings.textIndent!); const [fullJustification, setFullJustification] = useState(viewSettings.fullJustification!); const [hyphenation, setHyphenation] = useState(viewSettings.hyphenation!); const [marginTopPx, setMarginTopPx] = useState( viewSettings.marginPx || viewSettings.marginTopPx!, ); const [marginBottomPx, setMarginBottomPx] = useState(viewSettings.marginBottomPx!); const [marginLeftPx, setMarginLeftPx] = useState(viewSettings.marginLeftPx!); const [marginRightPx, setMarginRightPx] = useState(viewSettings.marginRightPx!); const [compactMarginTopPx, setCompactMarginTopPx] = useState( viewSettings.compactMarginPx || viewSettings.compactMarginTopPx!, ); const [compactMarginBottomPx, setCompactMarginBottomPx] = useState( viewSettings.compactMarginBottomPx!, ); const [gapPercent, setGapPercent] = useState(viewSettings.gapPercent!); const [compactMarginLeftPx, setCompactMarginLeftPx] = useState(viewSettings.compactMarginLeftPx!); const [compactMarginRightPx, setCompactMarginRightPx] = useState( viewSettings.compactMarginRightPx!, ); const [maxColumnCount, setMaxColumnCount] = useState(viewSettings.maxColumnCount!); const [maxInlineSize, setMaxInlineSize] = useState(viewSettings.maxInlineSize!); const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize!); const [writingMode, setWritingMode] = useState(viewSettings.writingMode!); const [overrideLayout, setOverrideLayout] = useState(viewSettings.overrideLayout!); const [doubleBorder, setDoubleBorder] = useState(viewSettings.doubleBorder!); const [borderColor, setBorderColor] = useState(viewSettings.borderColor!); const [showHeader, setShowHeader] = useState(viewSettings.showHeader!); const [showFooter, setShowFooter] = useState(viewSettings.showFooter!); const [showBarsOnScroll, setShowBarsOnScroll] = useState(viewSettings.showBarsOnScroll!); const [showRemainingTime, setShowRemainingTime] = useState(viewSettings.showRemainingTime!); const [showRemainingPages, setShowRemainingPages] = useState(viewSettings.showRemainingPages!); const [showProgressInfo, setShowProgressInfo] = useState(viewSettings.showProgressInfo!); const [progressStyle, setProgressStyle] = useState(viewSettings.progressStyle); const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation!); const resetToDefaults = useResetViewSettings(); const handleReset = () => { resetToDefaults({ paragraphMargin: setParagraphMargin, lineHeight: setLineHeight, wordSpacing: setWordSpacing, letterSpacing: setLetterSpacing, textIndent: setTextIndent, fullJustification: setFullJustification, hyphenation: setHyphenation, marginTopPx: setMarginTopPx, marginBottomPx: setMarginBottomPx, marginLeftPx: setMarginLeftPx, marginRightPx: setMarginRightPx, compactMarginTopPx: setCompactMarginTopPx, compactMarginBottomPx: setCompactMarginBottomPx, compactMarginLeftPx: setCompactMarginLeftPx, compactMarginRightPx: setCompactMarginRightPx, gapPercent: setGapPercent, maxColumnCount: setMaxColumnCount, maxInlineSize: setMaxInlineSize, maxBlockSize: setMaxBlockSize, overrideLayout: setOverrideLayout, doubleBorder: setDoubleBorder, borderColor: setBorderColor, showHeader: setShowHeader, showFooter: setShowFooter, showBarsOnScroll: setShowBarsOnScroll, showRemainingTime: setShowRemainingTime, showRemainingPages: setShowRemainingPages, showProgressInfo: setShowProgressInfo, }); }; useEffect(() => { onRegisterReset(handleReset); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { saveViewSettings(envConfig, bookKey, 'paragraphMargin', paragraphMargin); // eslint-disable-next-line react-hooks/exhaustive-deps }, [paragraphMargin]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'lineHeight', lineHeight); // eslint-disable-next-line react-hooks/exhaustive-deps }, [lineHeight]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'wordSpacing', wordSpacing); // eslint-disable-next-line react-hooks/exhaustive-deps }, [wordSpacing]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'letterSpacing', letterSpacing); // eslint-disable-next-line react-hooks/exhaustive-deps }, [letterSpacing]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'textIndent', textIndent); // eslint-disable-next-line react-hooks/exhaustive-deps }, [textIndent]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'fullJustification', fullJustification); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullJustification]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'hyphenation', hyphenation); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hyphenation]); useEffect(() => { if (marginTopPx === viewSettings.marginTopPx) return; if (viewSettings.marginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false); } saveViewSettings(envConfig, bookKey, 'marginTopPx', marginTopPx, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [marginTopPx]); useEffect(() => { if (marginBottomPx === viewSettings.marginBottomPx) return; if (viewSettings.marginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false); } saveViewSettings(envConfig, bookKey, 'marginBottomPx', marginBottomPx, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [marginBottomPx]); useEffect(() => { if (marginRightPx === viewSettings.marginRightPx) return; if (viewSettings.marginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false); } saveViewSettings(envConfig, bookKey, 'marginRightPx', marginRightPx, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [marginRightPx]); useEffect(() => { if (marginLeftPx === viewSettings.marginLeftPx) return; if (viewSettings.marginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false); } saveViewSettings(envConfig, bookKey, 'marginLeftPx', marginLeftPx, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [marginLeftPx]); useEffect(() => { if (compactMarginTopPx === viewSettings.compactMarginTopPx) return; if (viewSettings.compactMarginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false); } saveViewSettings(envConfig, bookKey, 'compactMarginTopPx', compactMarginTopPx, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [compactMarginTopPx]); useEffect(() => { if (compactMarginBottomPx === viewSettings.compactMarginBottomPx) return; if (viewSettings.compactMarginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false); } saveViewSettings( envConfig, bookKey, 'compactMarginBottomPx', compactMarginBottomPx, false, false, ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [compactMarginBottomPx]); useEffect(() => { if (compactMarginRightPx === viewSettings.compactMarginRightPx) return; if (viewSettings.compactMarginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false); } saveViewSettings( envConfig, bookKey, 'compactMarginRightPx', compactMarginRightPx, false, false, ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [compactMarginRightPx]); useEffect(() => { if (compactMarginLeftPx === viewSettings.compactMarginLeftPx) return; if (viewSettings.compactMarginPx !== undefined) { saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false); } saveViewSettings(envConfig, bookKey, 'compactMarginLeftPx', compactMarginLeftPx, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [compactMarginLeftPx]); useEffect(() => { if (gapPercent === viewSettings.gapPercent) return; saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent, false, false); view?.renderer.setAttribute('gap', `${gapPercent}%`); if (viewSettings.scrolled) { view?.renderer.setAttribute('flow', 'scrolled'); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [gapPercent]); useEffect(() => { if (maxColumnCount === viewSettings.maxColumnCount) return; saveViewSettings(envConfig, bookKey, 'maxColumnCount', maxColumnCount, false, false); view?.renderer.setAttribute('max-column-count', maxColumnCount); view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`); // eslint-disable-next-line react-hooks/exhaustive-deps }, [maxColumnCount]); useEffect(() => { if (maxInlineSize === viewSettings.maxInlineSize) return; saveViewSettings(envConfig, bookKey, 'maxInlineSize', maxInlineSize, false, false); view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`); // eslint-disable-next-line react-hooks/exhaustive-deps }, [maxInlineSize]); useEffect(() => { if (maxBlockSize === viewSettings.maxBlockSize) return; saveViewSettings(envConfig, bookKey, 'maxBlockSize', maxBlockSize, false, false); view?.renderer.setAttribute('max-block-size', `${maxBlockSize}px`); // eslint-disable-next-line react-hooks/exhaustive-deps }, [maxBlockSize]); useEffect(() => { if (writingMode === viewSettings.writingMode) return; // global settings are not supported for writing mode const prevWritingMode = viewSettings.writingMode; if (writingMode.includes('vertical')) { viewSettings.vertical = true; } else { viewSettings.vertical = false; } saveViewSettings(envConfig, bookKey, 'writingMode', writingMode, true); if (view) { view.renderer.setStyles?.(getStyles(viewSettings)); view.book.dir = getBookDirFromWritingMode(writingMode); } if ( prevWritingMode !== writingMode && (['horizontal-rl', 'vertical-rl'].includes(writingMode) || ['horizontal-rl', 'vertical-rl'].includes(prevWritingMode)) ) { saveAndReload(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [writingMode]); useEffect(() => { if (overrideLayout === viewSettings.overrideLayout) return; saveViewSettings(envConfig, bookKey, 'overrideLayout', overrideLayout); // eslint-disable-next-line react-hooks/exhaustive-deps }, [overrideLayout]); useEffect(() => { if (doubleBorder === viewSettings.doubleBorder) return; saveViewSettings(envConfig, bookKey, 'doubleBorder', doubleBorder, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [doubleBorder]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'borderColor', borderColor, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [borderColor]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'showBarsOnScroll', showBarsOnScroll, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [showBarsOnScroll]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'showRemainingTime', showRemainingTime, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [showRemainingTime]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'showRemainingPages', showRemainingPages, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [showRemainingPages]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'showProgressInfo', showProgressInfo, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [showProgressInfo]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'progressStyle', progressStyle, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [progressStyle]); useEffect(() => { if (showHeader === viewSettings.showHeader) return; if (showHeader && !viewSettings.vertical) { const minMarginTop = Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4); viewSettings.marginTopPx = Math.max(viewSettings.marginTopPx, minMarginTop); setMarginTopPx(viewSettings.marginTopPx); setViewSettings(bookKey, viewSettings); } saveViewSettings(envConfig, bookKey, 'showHeader', showHeader, false, false); // Margin and gap settings will be applied in FoliateViewer // eslint-disable-next-line react-hooks/exhaustive-deps }, [showHeader]); useEffect(() => { if (showFooter === viewSettings.showFooter) return; if (showFooter && !viewSettings.vertical) { const minMarginBottom = Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4); viewSettings.marginBottomPx = Math.max(viewSettings.marginBottomPx, minMarginBottom); setMarginBottomPx(viewSettings.marginBottomPx); setViewSettings(bookKey, viewSettings); } saveViewSettings(envConfig, bookKey, 'showFooter', showFooter, false, false); // Margin and gap settings will be applied in FoliateViewer // eslint-disable-next-line react-hooks/exhaustive-deps }, [showFooter]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'screenOrientation', screenOrientation, false, false); if (appService?.isMobileApp) { lockScreenOrientation({ orientation: screenOrientation }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [screenOrientation]); const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language); const mightBeRTLBook = MIGHT_BE_RTL_LANGS.includes(langCode) || isCJKEnv(); const isVertical = viewSettings.vertical || writingMode.includes('vertical'); return (

{_('Override Book Layout')}

setOverrideLayout(!overrideLayout)} />
{mightBeRTLBook && (

{_('Writing Mode')}

)} {viewSettings.vertical && (

{_('Border Frame')}

{_('Double Border')} setDoubleBorder(!doubleBorder)} />
{_('Border Color')}
)}

{_('Paragraph')}

{langCode !== 'zh' && ( )}
{_('Full Justification')} setFullJustification(!fullJustification)} />
{_('Hyphenation')} setHyphenation(!hyphenation)} />

{_('Page')}

{_('Header & Footer')}

{_('Show Header')} setShowHeader(!showHeader)} />
{_('Show Footer')} setShowFooter(!showFooter)} />
{_('Show Remaining Time')} { if (!showRemainingTime) { setShowRemainingTime(true); setShowRemainingPages(false); } else { setShowRemainingTime(false); } }} />
{_('Show Remaining Pages')} { if (!showRemainingPages) { setShowRemainingPages(true); setShowRemainingTime(false); } else { setShowRemainingPages(false); } }} />
{_('Show Reading Progress')} setShowProgressInfo(!showProgressInfo)} />
{_('Reading Progress Style')} setShowBarsOnScroll(!showBarsOnScroll)} />
{appService?.hasOrientationLock && (

{_('Screen')}

{_('Orientation')}
)}
); }; export default LayoutPanel;