feat: add options to reset settings in the config dialog, closes #1111 (#1475)

This commit is contained in:
Huang Xin
2025-06-26 15:59:34 +08:00
committed by GitHub
parent 460d37c656
commit bc2c3a2c9a
34 changed files with 372 additions and 92 deletions
@@ -18,12 +18,14 @@ import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { CODE_LANGUAGES, CodeLanguage, manageSyntaxHighlighting } from '@/utils/highlightjs';
import { SettingsPanelPanelProp } from './SettingsDialog';
import Select from '@/components/Select';
import ThemeEditor from './ThemeEditor';
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor, saveCustomTheme } =
useThemeStore();
@@ -44,6 +46,24 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [codeHighlighting, setcodeHighlighting] = useState(viewSettings.codeHighlighting!);
const [codeLanguage, setCodeLanguage] = useState(viewSettings.codeLanguage!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
overrideColor: setOverrideColor,
invertImgColorInDark: setInvertImgColorInDark,
codeHighlighting: setcodeHighlighting,
codeLanguage: setCodeLanguage,
});
setThemeColor('default');
setThemeMode('auto');
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (invertImgColorInDark === viewSettings.invertImgColorInDark) return;
saveViewSettings(envConfig, bookKey, 'invertImgColorInDark', invertImgColorInDark);
@@ -8,9 +8,11 @@ import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import NumberInput from './NumberInput';
const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getView, getViewSettings } = useReaderStore();
@@ -28,6 +30,26 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [animated, setAnimated] = useState(viewSettings.animated!);
const [allowScript, setAllowScript] = useState(viewSettings.allowScript!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
scrolled: setScrolledMode,
continuousScroll: setIsContinuousScroll,
scrollingOverlap: setScrollingOverlap,
volumeKeysToFlip: setVolumeKeysToFlip,
disableClick: setIsDisableClick,
swapClickArea: setSwapClickArea,
animated: setAnimated,
allowScript: setAllowScript,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isScrolledMode === viewSettings.scrolled) return;
saveViewSettings(envConfig, bookKey, 'scrolled', isScrolledMode);
@@ -3,20 +3,30 @@ import React from 'react';
import { MdCheck } from 'react-icons/md';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { SettingsPanelType } from './SettingsDialog';
import MenuItem from '@/components/MenuItem';
interface DialogMenuProps {
toggleDropdown?: () => void;
activePanel: SettingsPanelType;
setIsDropdownOpen?: (open: boolean) => void;
onReset: () => void;
resetLabel?: string;
}
const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
const DialogMenu: React.FC<DialogMenuProps> = ({ setIsDropdownOpen, onReset, resetLabel }) => {
const _ = useTranslation();
const iconSize = useDefaultIconSize();
const iconSize = useResponsiveSize(16);
const { isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } = useSettingsStore();
const handleToggleGlobal = () => {
setFontLayoutSettingsGlobal(!isFontLayoutSettingsGlobal);
toggleDropdown?.();
setIsDropdownOpen?.(false);
};
const handleResetToDefaults = () => {
onReset();
setIsDropdownOpen?.(false);
};
return (
@@ -27,24 +37,18 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
'text-base sm:text-sm',
)}
>
<button
className='hover:bg-base-200 text-base-content flex w-full items-center justify-between rounded-md p-1'
<MenuItem
label={_('Global Settings')}
tooltip={isFontLayoutSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')}
buttonClass='lg:tooltip'
Icon={
isFontLayoutSettingsGlobal ? (
<MdCheck size={iconSize} className='text-base-content' />
) : null
}
onClick={handleToggleGlobal}
>
<div className='flex items-center'>
<span style={{ minWidth: `${iconSize}px` }}>
{isFontLayoutSettingsGlobal && <MdCheck className='text-base-content' />}
</span>
<div
className='lg:tooltip'
data-tip={
isFontLayoutSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')
}
>
<span className='ml-2 whitespace-nowrap'>{_('Global Settings')}</span>
</div>
</div>
</button>
/>
<MenuItem label={resetLabel || _('Reset Settings')} onClick={handleResetToDefaults} />
</div>
);
};
@@ -24,6 +24,8 @@ import { getOSPlatform, isCJKEnv } from '@/utils/misc';
import { getSysFontsList } from '@/utils/bridge';
import { isTauriAppPlatform } from '@/services/environment';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
import FontDropdown from './FontDropDown';
@@ -79,7 +81,7 @@ const FontFace = ({
);
};
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getView, getViewSettings } = useReaderStore();
@@ -119,10 +121,10 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
break;
}
const [sysFonts, setSysFonts] = useState<string[]>(defaultSysFonts);
const [defaultFont, setDefaultFont] = useState(viewSettings.defaultFont!);
const [defaultFontSize, setDefaultFontSize] = useState(viewSettings.defaultFontSize!);
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
const [overrideFont, setOverrideFont] = useState(viewSettings.overrideFont!);
const [defaultFont, setDefaultFont] = useState(viewSettings.defaultFont!);
const [defaultCJKFont, setDefaultCJKFont] = useState(viewSettings.defaultCJKFont!);
const [serifFont, setSerifFont] = useState(viewSettings.serifFont!);
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
@@ -132,6 +134,27 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
return genCJKFontsList(sysFonts);
});
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
defaultFont: setDefaultFont,
defaultFontSize: setDefaultFontSize,
minimumFontSize: setMinFontSize,
overrideFont: setOverrideFont,
defaultCJKFont: setDefaultCJKFont,
serifFont: setSerifFont,
sansSerifFont: setSansSerifFont,
monospaceFont: setMonospaceFont,
fontWeight: setFontWeight,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setCJKFonts((prev) => {
const newFonts = genCJKFontsList(sysFonts);
@@ -8,20 +8,39 @@ import { useTranslation } from '@/hooks/useTranslation';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { getTranslators } from '@/services/translators';
import { TRANSLATED_LANGS } from '@/services/constants';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { initDayjs } from '@/utils/time';
import Select from '@/components/Select';
const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { token } = useAuth();
const { envConfig } = useEnv();
const { getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [uiLanguage, setUILanguage] = useState(viewSettings.uiLanguage!);
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider!);
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
uiLanguage: setUILanguage,
translationEnabled: setTranslationEnabled,
translationProvider: setTranslationProvider,
translateTargetLang: setTranslateTargetLang,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getCurrentUILangOption = () => {
const uiLanguage = viewSettings.uiLanguage;
return {
@@ -43,10 +62,7 @@ const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const handleSelectUILang = (event: React.ChangeEvent<HTMLSelectElement>) => {
const option = event.target.value;
saveViewSettings(envConfig, bookKey, 'uiLanguage', option, false, false);
const locale = option ? option : navigator.language;
i18n.changeLanguage(locale);
initDayjs(locale);
setUILanguage(option);
};
const getTranslationProviderOptions = () => {
@@ -97,6 +113,15 @@ const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setViewSettings(bookKey, { ...viewSettings });
};
useEffect(() => {
if (uiLanguage === viewSettings.uiLanguage) return;
saveViewSettings(envConfig, bookKey, 'uiLanguage', uiLanguage, false, false);
const locale = uiLanguage ? uiLanguage : navigator.language;
i18n.changeLanguage(locale);
initDayjs(locale);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uiLanguage]);
useEffect(() => {
if (translationEnabled === viewSettings.translationEnabled) return;
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
@@ -8,6 +8,7 @@ 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 { getMaxInlineSize } from '@/utils/config';
@@ -15,9 +16,10 @@ import { lockScreenOrientation } from '@/utils/bridge';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import { MIGHT_BE_RTL_LANGS, RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore();
@@ -63,9 +65,47 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [showBarsOnScroll, setShowBarsOnScroll] = useState(viewSettings.showBarsOnScroll!);
const [showRemainingTime, setShowRemainingTime] = useState(viewSettings.showRemainingTime!);
const [showPageNumber, setShowPageNumber] = useState(viewSettings.showPageNumber!);
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,
showPageNumber: setShowPageNumber,
});
};
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
@@ -1,17 +1,19 @@
import clsx from 'clsx';
import cssbeautify from 'cssbeautify';
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { getStyles } from '@/utils/style';
import cssValidate from '@/utils/css';
type CSSType = 'book' | 'reader';
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
@@ -29,6 +31,22 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const contentTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const uiTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
userStylesheet: setDraftContentStylesheet,
userUIStylesheet: setDraftUIStylesheet,
});
applyStyles('book', true);
applyStyles('reader', true);
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const validateCSS = (cssInput: string): { isValid: boolean; error?: string } => {
if (!cssInput.trim()) return { isValid: true };
@@ -64,9 +82,9 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}
};
const applyStyles = (type: CSSType) => {
const applyStyles = (type: CSSType, clear = false) => {
const cssInput = type === 'book' ? draftContentStylesheet : draftUIStylesheet;
const formattedCSS = cssbeautify(cssInput, {
const formattedCSS = cssbeautify(clear ? '' : cssInput, {
indent: ' ',
openbrace: 'end-of-line',
autosemicolon: true,
@@ -22,7 +22,11 @@ import ControlPanel from './ControlPanel';
import LangPanel from './LangPanel';
import MiscPanel from './MiscPanel';
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Language' | 'Custom';
export type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Language' | 'Custom';
export type SettingsPanelPanelProp = {
bookKey: string;
onRegisterReset: (resetFn: () => void) => void;
};
type TabConfig = {
tab: SettingsPanelType;
@@ -84,6 +88,28 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
localStorage.setItem('lastConfigPanel', tab);
};
const [resetFunctions, setResetFunctions] = useState<
Record<SettingsPanelType, (() => void) | null>
>({
Font: null,
Layout: null,
Color: null,
Control: null,
Language: null,
Custom: null,
});
const registerResetFunction = (panel: SettingsPanelType, resetFn: () => void) => {
setResetFunctions((prev) => ({ ...prev, [panel]: resetFn }));
};
const handleResetCurrentPanel = () => {
const resetFn = resetFunctions[activePanel];
if (resetFn) {
resetFn();
}
};
const handleClose = () => {
setFontLayoutSettingsDialogOpen(false);
};
@@ -130,6 +156,8 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
};
}, []);
const currentPanel = tabConfig.find((tab) => tab.tab === activePanel);
return (
<Dialog
isOpen={true}
@@ -140,7 +168,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
header={
<div className='flex w-full flex-col items-center'>
<div className='tab-title flex pb-2 text-base font-semibold sm:hidden'>
{tabConfig.find((tab) => tab.tab === activePanel)?.label || ''}
{currentPanel?.label || ''}
</div>
<div className='flex w-full flex-row items-center justify-between'>
<button
@@ -184,7 +212,15 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 flex items-center justify-center'
toggleButton={<PiDotsThreeVerticalBold />}
>
<DialogMenu />
<DialogMenu
activePanel={activePanel}
onReset={handleResetCurrentPanel}
resetLabel={
currentPanel
? _('Reset {{settings}}', { settings: currentPanel.label })
: undefined
}
/>
</Dropdown>
<button
onClick={handleClose}
@@ -209,12 +245,39 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
</div>
}
>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Control' && <ControlPanel bookKey={bookKey} />}
{activePanel === 'Language' && <LangPanel bookKey={bookKey} />}
{activePanel === 'Custom' && <MiscPanel bookKey={bookKey} />}
{activePanel === 'Font' && (
<FontPanel bookKey={bookKey} onRegisterReset={(fn) => registerResetFunction('Font', fn)} />
)}
{activePanel === 'Layout' && (
<LayoutPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Layout', fn)}
/>
)}
{activePanel === 'Color' && (
<ColorPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Color', fn)}
/>
)}
{activePanel === 'Control' && (
<ControlPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Control', fn)}
/>
)}
{activePanel === 'Language' && (
<LangPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Language', fn)}
/>
)}
{activePanel === 'Custom' && (
<MiscPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Custom', fn)}
/>
)}
</Dialog>
);
};