This commit is contained in:
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user