import React, { useEffect, useRef, useState } from 'react'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useBookDataStore } from '@/store/bookDataStore'; import { useSettingsStore } from '@/store/settingsStore'; import { useResetViewSettings } from '@/hooks/useResetSettings'; import { useEinkMode } from '@/hooks/useEinkMode'; import { getStyles } from '@/utils/style'; import { getMaxInlineSize } from '@/utils/config'; import { saveSysSettings, saveViewSettings } from '@/helpers/settings'; import { PageTurnStyle } from '@/types/book'; import { SettingsPanelPanelProp } from './SettingsDialog'; import { annotationToolQuickActions } from '@/app/reader/components/annotator/AnnotationTools'; import { applyPageTurnAttributes } from '@/app/reader/hooks/useCapturedTurn'; import { isTauriAppPlatform } from '@/services/environment'; import { BoxedList, NavigationRow, SettingsRow, SettingsSelect, SettingsSwitchRow, } from './primitives'; import NumberInput from './NumberInput'; import PageTurnerSettings from './PageTurnerSettings'; import AnnotationToolbarCustomizer from './AnnotationToolbarCustomizer'; import { DEFAULT_ANNOTATION_TOOLBAR_ITEMS } from '@/utils/annotationToolbar'; import { canShareText } from '@/utils/share'; import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry'; const ControlPanel: React.FC = ({ bookKey, onRegisterReset }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); const { getView, getViewSettings, recreateViewer } = useReaderStore(); const { getBookData } = useBookDataStore(); const { settings } = useSettingsStore(); const { applyEinkMode } = useEinkMode(); const bookData = getBookData(bookKey); const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings; const [isScrolledMode, setScrolledMode] = useState(viewSettings.scrolled); const [noContinuousScroll, setNoContinuousScroll] = useState(viewSettings.noContinuousScroll); const [scrollingOverlap, setScrollingOverlap] = useState(viewSettings.scrollingOverlap); const [hideScrollbar, setHideScrollbar] = useState(viewSettings.hideScrollbar || false); const [showPaginationButtons, setShowPaginationButtons] = useState( viewSettings.showPaginationButtons, ); const [isDisableClick, setIsDisableClick] = useState(viewSettings.disableClick); const [isDisableSwipe, setIsDisableSwipe] = useState(viewSettings.disableSwipe); const [fullscreenClickArea, setFullscreenClickArea] = useState(viewSettings.fullscreenClickArea); const [swapClickArea, setSwapClickArea] = useState(viewSettings.swapClickArea); const [isDisableDoubleClick, setIsDisableDoubleClick] = useState(viewSettings.disableDoubleClick); const [enableAnnotationQuickActions, setEnableAnnotationQuickActions] = useState( viewSettings.enableAnnotationQuickActions, ); const [annotationQuickAction, setAnnotationQuickAction] = useState( viewSettings.annotationQuickAction, ); const [copyToNotebook, setCopyToNotebook] = useState(viewSettings.copyToNotebook); const [showToolbarCustomizer, setShowToolbarCustomizer] = useState(false); const [animated, setAnimated] = useState(viewSettings.animated); const [pageTurnStyle, setPageTurnStyle] = useState(viewSettings.pageTurnStyle || 'push'); const [isEink, setIsEink] = useState(viewSettings.isEink); const [isColorEink, setIsColorEink] = useState(viewSettings.isColorEink); const [autoScreenBrightness, setAutoScreenBrightness] = useState(settings.autoScreenBrightness); const [swipeBrightnessGesture, setSwipeBrightnessGesture] = useState( settings.swipeBrightnessGesture, ); const [screenWakeLock, setScreenWakeLock] = useState(settings.screenWakeLock); const [allowScript, setAllowScript] = useState(viewSettings.allowScript); const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates); const [isNightlyChannel, setIsNightlyChannel] = useState(settings.updateChannel === 'nightly'); const [isTelemetryEnabled, setIsTelemetryEnabled] = useState(settings.telemetryEnabled); const resetToDefaults = useResetViewSettings(); const pageTurnerResetRef = useRef<() => void>(() => {}); const canShare = canShareText(appService); // The layered styles need an engine with full View Transitions support or // the Tauri captured-turn fallback; engines like iOS 18 WebKit crash on // the VT turns, so on the web they only get Push (readest#555). const turnStyleOptions = [ { value: 'push', label: _('Push') }, ...(appService?.supportsViewTransitionGroup || isTauriAppPlatform() ? [ { value: 'slide', label: _('Slide') }, { value: 'curl', label: _('Page Curl') }, ] : []), ]; const handleReset = () => { resetToDefaults({ scrolled: setScrolledMode, noContinuousScroll: setNoContinuousScroll, scrollingOverlap: setScrollingOverlap, hideScrollbar: setHideScrollbar, showPaginationButtons: setShowPaginationButtons, disableClick: setIsDisableClick, disableSwipe: setIsDisableSwipe, swapClickArea: setSwapClickArea, animated: setAnimated, isEink: setIsEink, allowScript: setAllowScript, fullscreenClickArea: setFullscreenClickArea, disableDoubleClick: setIsDisableDoubleClick, enableAnnotationQuickActions: setEnableAnnotationQuickActions, copyToNotebook: setCopyToNotebook, }); saveViewSettings( envConfig, bookKey, 'annotationToolbarItems', DEFAULT_ANNOTATION_TOOLBAR_ITEMS, false, true, ); pageTurnerResetRef.current(); }; useEffect(() => { onRegisterReset(handleReset); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (isScrolledMode === viewSettings.scrolled) return; saveViewSettings(envConfig, bookKey, 'scrolled', isScrolledMode); getView(bookKey)?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated'); getView(bookKey)?.renderer.setAttribute( 'max-inline-size', `${getMaxInlineSize(viewSettings)}px`, ); getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isScrolledMode]); useEffect(() => { if (noContinuousScroll === viewSettings.noContinuousScroll) return; saveViewSettings(envConfig, bookKey, 'noContinuousScroll', noContinuousScroll); if (noContinuousScroll) { getView(bookKey)?.renderer.setAttribute('no-continuous-scroll', ''); } else { getView(bookKey)?.renderer.removeAttribute('no-continuous-scroll'); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [noContinuousScroll]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'hideScrollbar', hideScrollbar, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hideScrollbar]); useEffect(() => { if (scrollingOverlap === viewSettings.scrollingOverlap) return; saveViewSettings(envConfig, bookKey, 'scrollingOverlap', scrollingOverlap, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [scrollingOverlap]); useEffect(() => { saveViewSettings( envConfig, bookKey, 'showPaginationButtons', showPaginationButtons, false, false, ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [showPaginationButtons]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'disableClick', isDisableClick, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDisableClick]); // The renderer reads `turn-style`/`no-swipe` at touchmove/touchend time, so // settings changes have to push the attributes through immediately rather // than waiting for the next recreateViewer pass. const applyTurnAttributes = () => { const view = getView(bookKey); const freshSettings = getViewSettings(bookKey); if (view && freshSettings) { applyPageTurnAttributes(view, freshSettings, !!bookData?.isFixedLayout); } }; useEffect(() => { saveViewSettings(envConfig, bookKey, 'disableSwipe', isDisableSwipe, false, false); applyTurnAttributes(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDisableSwipe]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'disableDoubleClick', isDisableDoubleClick, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDisableDoubleClick]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'fullscreenClickArea', fullscreenClickArea, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullscreenClickArea]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'swapClickArea', swapClickArea, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [swapClickArea]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'animated', animated, false, false); if (animated) { getView(bookKey)?.renderer.setAttribute('animated', ''); } else { getView(bookKey)?.renderer.removeAttribute('animated'); } // Mesh-curl eligibility depends on `animated`. applyTurnAttributes(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [animated]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'pageTurnStyle', pageTurnStyle, false, false); applyTurnAttributes(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageTurnStyle]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'isEink', isEink); if (isEink) { getView(bookKey)?.renderer.setAttribute('eink', ''); } else { getView(bookKey)?.renderer.removeAttribute('eink'); } applyEinkMode(isEink); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isEink]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'isColorEink', isColorEink); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isColorEink]); useEffect(() => { if (autoScreenBrightness === settings.autoScreenBrightness) return; saveSysSettings(envConfig, 'autoScreenBrightness', autoScreenBrightness); // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoScreenBrightness]); useEffect(() => { if (swipeBrightnessGesture === settings.swipeBrightnessGesture) return; saveSysSettings(envConfig, 'swipeBrightnessGesture', swipeBrightnessGesture); // eslint-disable-next-line react-hooks/exhaustive-deps }, [swipeBrightnessGesture]); useEffect(() => { if (screenWakeLock === settings.screenWakeLock) return; saveSysSettings(envConfig, 'screenWakeLock', screenWakeLock); // eslint-disable-next-line react-hooks/exhaustive-deps }, [screenWakeLock]); useEffect(() => { if (viewSettings.allowScript === allowScript) return; saveViewSettings(envConfig, bookKey, 'allowScript', allowScript, true, false).then(() => { recreateViewer(envConfig, bookKey); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [allowScript]); useEffect(() => { saveViewSettings( envConfig, bookKey, 'enableAnnotationQuickActions', enableAnnotationQuickActions, false, false, ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [enableAnnotationQuickActions]); useEffect(() => { saveViewSettings(envConfig, bookKey, 'copyToNotebook', copyToNotebook, false, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [copyToNotebook]); const toggleAutoCheckUpdates = () => { const newValue = !isAutoCheckUpdates; saveSysSettings(envConfig, 'autoCheckUpdates', newValue); setIsAutoCheckUpdates(newValue); }; const toggleNightlyChannel = () => { const newValue = !isNightlyChannel; saveSysSettings(envConfig, 'updateChannel', newValue ? 'nightly' : 'stable'); setIsNightlyChannel(newValue); }; const toggleTelemetry = () => { const newValue = !isTelemetryEnabled; saveSysSettings(envConfig, 'telemetryEnabled', newValue); setIsTelemetryEnabled(newValue); if (newValue) { optInTelemetry(); } else { optOutTelemetry(); } }; const getQuickActionOptions = () => { return [ { value: '', label: _('None'), }, ...annotationToolQuickActions .filter((button) => button.type !== 'share' || canShare) .map((button) => ({ value: button.type, label: _(button.label), })), ]; }; const handleSelectAnnotationQuickAction = (event: React.ChangeEvent) => { const action = event.target.value as typeof annotationQuickAction; setAnnotationQuickAction(action); saveViewSettings(envConfig, bookKey, 'annotationQuickAction', action, false, true); }; if (showToolbarCustomizer) { return ( setShowToolbarCustomizer(false)} /> ); } return (
setScrolledMode(!isScrolledMode)} /> setNoContinuousScroll(!noContinuousScroll)} data-setting-id='settings.control.scroll.noContinuousScroll' /> setHideScrollbar(!hideScrollbar)} data-setting-id='settings.control.scroll.hideScrollbar' /> setIsDisableClick(!isDisableClick)} /> setIsDisableSwipe(!isDisableSwipe)} data-setting-id='settings.control.swipeToPaginate' /> setFullscreenClickArea(!fullscreenClickArea)} data-setting-id='settings.control.clickBothSides' /> setSwapClickArea(!swapClickArea)} data-setting-id='settings.control.swapClickSides' /> setIsDisableDoubleClick(!isDisableDoubleClick)} data-setting-id='settings.control.disableDoubleClick' /> setShowPaginationButtons(!showPaginationButtons)} data-setting-id='settings.control.showPaginationButtons' /> { pageTurnerResetRef.current = fn; }} /> setEnableAnnotationQuickActions(!enableAnnotationQuickActions)} /> setCopyToNotebook(!copyToNotebook)} data-setting-id='settings.control.copyToNotebook' /> setShowToolbarCustomizer(true)} data-setting-id='settings.control.customizeToolbar' /> setAnimated(!animated)} /> opt.value === pageTurnStyle) ? pageTurnStyle : 'push' } onChange={(e) => setPageTurnStyle(e.target.value as PageTurnStyle)} ariaLabel={_('Animation Style')} options={turnStyleOptions} disabled={!animated} /> {(appService?.isAndroidApp || appService?.appPlatform === 'web') && ( setIsEink(!isEink)} data-setting-id='settings.control.einkMode' /> )} {(appService?.isAndroidApp || appService?.appPlatform === 'web') && ( setIsColorEink(!isColorEink)} data-setting-id='settings.control.colorEinkMode' /> )} {appService?.isMobileApp && ( setAutoScreenBrightness(!autoScreenBrightness)} /> )} {appService?.hasScreenBrightness && ( setSwipeBrightnessGesture(!swipeBrightnessGesture)} data-setting-id='settings.control.swipeBrightnessGesture' /> )} setScreenWakeLock(!screenWakeLock)} data-setting-id='settings.control.screenWakeLock' /> {appService?.hasUpdater && ( )} setAllowScript(!allowScript)} />
); }; export default ControlPanel;