feat(settings): add settings to adjust screen brightness when reading, closes #1532 (#2197)

This commit is contained in:
Huang Xin
2025-10-11 01:02:35 +08:00
committed by GitHub
parent f696b9a573
commit ada427b134
54 changed files with 588 additions and 57 deletions
@@ -0,0 +1,157 @@
import clsx from 'clsx';
import React, { useCallback, useEffect, useMemo } from 'react';
import { PiSun, PiMoon } from 'react-icons/pi';
import { TbSunMoon } from 'react-icons/tb';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useDeviceControlStore } from '@/store/deviceStore';
import { themes } from '@/styles/themes';
import { debounce } from '@/utils/debounce';
import Slider from '@/components/Slider';
const SCREEN_BRIGHTNESS_LIMITS = {
MIN: 0,
MAX: 100,
DEFAULT: 50,
} as const;
interface ColorPanelProps {
actionTab: string;
bottomOffset: string;
}
export const ColorPanel: React.FC<ColorPanelProps> = ({ actionTab, bottomOffset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { getScreenBrightness, setScreenBrightness } = useDeviceControlStore();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor } = useThemeStore();
const [screenBrightnessValue, setScreenBrightnessValue] = React.useState(
settings.screenBrightness >= 0 ? settings.screenBrightness : SCREEN_BRIGHTNESS_LIMITS.DEFAULT,
);
useEffect(() => {
if (!appService?.isMobileApp) return;
if (settings.screenBrightness >= 0) return;
getScreenBrightness().then((brightness) => {
if (brightness >= 0.0 && brightness <= 1.0) {
const screenBrightness = Math.round(brightness * 100);
setScreenBrightnessValue(screenBrightness);
}
});
}, [appService, settings, setSettings, getScreenBrightness]);
const debouncedSetScreenBrightness = useMemo(
() =>
debounce(async (value: number) => {
settings.screenBrightness = value;
setSettings(settings);
saveSettings(envConfig, settings);
await setScreenBrightness(value / 100);
}, 100),
[envConfig, settings, setSettings, saveSettings, setScreenBrightness],
);
const handleScreenBrightnessChange = useCallback(
async (value: number) => {
if (!appService?.isMobileApp) return;
setScreenBrightnessValue(value);
debouncedSetScreenBrightness(value);
},
[appService, debouncedSetScreenBrightness],
);
const cycleThemeMode = () => {
const nextMode = themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto';
setThemeMode(nextMode);
};
const classes = clsx(
'footerbar-color-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
actionTab === 'color'
? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out'
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
);
return (
<div className={classes} style={{ bottom: bottomOffset }}>
{appService?.hasScreenBrightness && (
<Slider
label={_('Screen Brightness')}
initialValue={screenBrightnessValue}
bubbleLabel={`${screenBrightnessValue}`}
minIcon={<PiSun size={16} />}
maxIcon={<PiSun size={24} />}
onChange={handleScreenBrightnessChange}
min={SCREEN_BRIGHTNESS_LIMITS.MIN}
max={SCREEN_BRIGHTNESS_LIMITS.MAX}
/>
)}
<div className='w-full'>
<div className='flex items-center justify-between p-2'>
<span className='text-sm font-medium'>{_('Color')}</span>
</div>
<div
className='flex gap-3 overflow-x-auto p-2'
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<style>{`
.theme-scroll::-webkit-scrollbar {
display: none;
}
`}</style>
{themes.map(({ name, label, colors }) => (
<button
key={name}
onClick={() => setThemeColor(name)}
className={clsx(
'flex flex-shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'h-[40px] min-w-[80px]',
themeColor === name
? 'ring-primary ring-offset-base-200 ring-2 ring-offset-2'
: 'hover:opacity-80',
)}
style={{
backgroundColor: isDarkMode ? colors.dark['base-100'] : colors.light['base-100'],
color: isDarkMode ? colors.dark['base-content'] : colors.light['base-content'],
}}
>
<span className='text-xs font-medium'>{_(label)}</span>
</button>
))}
<button
onClick={() => cycleThemeMode()}
className={clsx(
'flex flex-shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'h-[40px] min-w-[80px]',
themeMode === 'dark'
? 'ring-primary ring-offset-base-200 ring-2 ring-offset-2'
: 'hover:opacity-80',
)}
style={{
backgroundColor: (themes.find((t) => t.name === themeColor) || themes[0]!).colors
.dark['base-100'],
color: (themes.find((t) => t.name === themeColor) || themes[0]!).colors.dark[
'base-content'
],
}}
>
{themeMode === 'light' ? (
<PiSun size={20} />
) : themeMode === 'dark' ? (
<PiMoon size={20} />
) : (
<TbSunMoon size={20} />
)}
</button>
</div>
</div>
</div>
);
};
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useEffect } from 'react';
import { FaHeadphones } from 'react-icons/fa6';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
@@ -22,6 +22,25 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
const view = getView(bookKey);
const viewState = getViewState(bookKey);
const viewSettings = getViewSettings(bookKey);
const [progressValue, setProgressValue] = React.useState(
progressValid ? progressFraction * 100 : 0,
);
useEffect(() => {
if (progressValid) {
setProgressValue(progressFraction * 100);
}
}, [progressValid, progressFraction]);
const handleProgressChange = useCallback(
(value: number) => {
setProgressValue(value);
navigationHandlers.onProgressChange(value);
},
[navigationHandlers],
);
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
return (
@@ -78,8 +97,8 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
min={0}
max={100}
aria-label={_('Jump to Location')}
value={progressValid ? progressFraction * 100 : 0}
onChange={(e) => navigationHandlers.onProgressChange(parseInt(e.target.value, 10))}
value={progressValue}
onChange={(e) => handleProgressChange(parseInt(e.target.value, 10))}
/>
<Button
icon={<FaHeadphones className={viewState?.ttsEnabled ? 'text-blue-500' : ''} />}
@@ -30,14 +30,14 @@ const MARGIN_CONSTANTS = {
interface FontLayoutPanelProps {
bookKey: string;
actionTab: string;
mobileBottomOffset: string;
bottomOffset: string;
marginIconSize: number;
}
export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
bookKey,
actionTab,
mobileBottomOffset,
bottomOffset,
marginIconSize,
}) => {
const _ = useTranslation();
@@ -98,7 +98,7 @@ export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
);
return (
<div className={classes} style={{ bottom: mobileBottomOffset }}>
<div className={classes} style={{ bottom: bottomOffset }}>
<Slider
label={_('Font Size')}
initialValue={viewSettings?.defaultFontSize ?? FONT_SIZE_LIMITS.DEFAULT}
@@ -7,6 +7,7 @@ import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
import { FooterBarProps, NavigationHandlers, FooterBarChildProps } from './types';
import { debounce } from '@/utils/debounce';
import { viewPagination } from '../../hooks/usePagination';
import MobileFooterBar from './MobileFooterBar';
import DesktopFooterBar from './DesktopFooterBar';
@@ -50,10 +51,11 @@ const FooterBar: React.FC<FooterBarProps> = ({
return (progressInfo.current + 1) / progressInfo.total;
}, [progressValid, progressInfo]);
const handleProgressChange = useCallback(
(value: number) => {
view?.goToFraction(value / 100.0);
},
const handleProgressChange = useMemo(
() =>
debounce((value: number) => {
view?.goToFraction(value / 100.0);
}, 100),
[view],
);
@@ -1,9 +1,10 @@
import React from 'react';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { FooterBarChildProps } from './types';
import { NavigationPanel } from './NavigationPanel';
import { FontLayoutPanel } from './FontLayoutPanel';
import { ColorPanel } from './ColorPanel';
import { NavigationBar } from './NavigationBar';
import { FooterBarChildProps } from './types';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
const MobileFooterBar: React.FC<FooterBarChildProps> = ({
bookKey,
@@ -22,8 +23,9 @@ const MobileFooterBar: React.FC<FooterBarChildProps> = ({
return (
<>
<ColorPanel actionTab={actionTab} bottomOffset={bottomOffset} />
<NavigationPanel
actionTab={actionTab!}
actionTab={actionTab}
progressFraction={progressFraction}
progressValid={progressValid}
navigationHandlers={navigationHandlers}
@@ -32,13 +34,13 @@ const MobileFooterBar: React.FC<FooterBarChildProps> = ({
/>
<FontLayoutPanel
bookKey={bookKey}
actionTab={actionTab!}
mobileBottomOffset={bottomOffset}
actionTab={actionTab}
bottomOffset={bottomOffset}
marginIconSize={marginIconSize}
/>
<NavigationBar
bookKey={bookKey}
actionTab={actionTab!}
actionTab={actionTab}
navPadding={navPadding}
onSetActionTab={onSetActionTab!}
/>
@@ -1,9 +1,9 @@
import clsx from 'clsx';
import React from 'react';
import { IoIosList as TOCIcon } from 'react-icons/io';
import { PiNotePencil as NoteIcon } from 'react-icons/pi';
import { RxSlider as SliderIcon } from 'react-icons/rx';
import { RiFontFamily as FontIcon } from 'react-icons/ri';
import { PiSun as ColorIcon } from 'react-icons/pi';
import { MdOutlineHeadphones as TTSIcon } from 'react-icons/md';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -39,7 +39,11 @@ export const NavigationBar: React.FC<NavigationBarProps> = ({
icon={<TOCIcon size={tocIconSize} />}
onClick={() => onSetActionTab('toc')}
/>
<Button label={_('Notes')} icon={<NoteIcon />} onClick={() => onSetActionTab('note')} />
<Button
label={_('Color')}
icon={<ColorIcon className={clsx(actionTab === 'color' && 'text-blue-500')} />}
onClick={() => onSetActionTab('color')}
/>
<Button
label={_('Reading Progress')}
icon={<SliderIcon className={clsx(actionTab === 'progress' && 'text-blue-500')} />}
@@ -1,5 +1,5 @@
import React from 'react';
import clsx from 'clsx';
import React, { useCallback, useEffect } from 'react';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
@@ -30,6 +30,25 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
sliderHeight,
}) => {
const _ = useTranslation();
const [progressValue, setProgressValue] = React.useState(
progressValid ? progressFraction * 100 : 0,
);
useEffect(() => {
if (progressValid) {
setProgressValue(progressFraction * 100);
}
}, [progressValid, progressFraction]);
const handleProgressChange = useCallback(
(value: number) => {
setProgressValue(value);
navigationHandlers.onProgressChange(value);
},
[navigationHandlers],
);
const classes = clsx(
'footerbar-progress-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
actionTab === 'progress'
@@ -43,9 +62,9 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
<Slider
label={_('Reading Progress')}
heightPx={sliderHeight}
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
initialValue={progressValid ? progressFraction * 100 : 0}
onChange={navigationHandlers.onProgressChange}
bubbleLabel={`${Math.round(progressValue)}%`}
initialValue={progressValue}
onChange={handleProgressChange}
/>
</div>
<div className='flex w-full items-center justify-between gap-x-6'>