forked from akai/readest
refactor: split FooterBar into modular components for desktop and mobile (#2181)
This commit is contained in:
@@ -14,7 +14,7 @@ import SettingsDialog from '@/components/settings/SettingsDialog';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FooterBar from './FooterBar';
|
||||
import FooterBar from './footerbar/FooterBar';
|
||||
import ProgressInfoView from './ProgressInfo';
|
||||
import Ribbon from './Ribbon';
|
||||
import Annotator from './annotator/Annotator';
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
|
||||
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
|
||||
import { FaHeadphones } from 'react-icons/fa6';
|
||||
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 { MdOutlineHeadphones as TTSIcon } from 'react-icons/md';
|
||||
import { TbBoxMargin } from 'react-icons/tb';
|
||||
import { RxLineHeight } from 'react-icons/rx';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { saveViewSettings } from '@/helpers/viewSettings';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { viewPagination } from '../hooks/usePagination';
|
||||
import { PageInfo } from '@/types/book';
|
||||
import { Insets } from '@/types/misc';
|
||||
import Button from '@/components/Button';
|
||||
import Slider from '@/components/Slider';
|
||||
import TTSControl from './tts/TTSControl';
|
||||
|
||||
interface FooterBarProps {
|
||||
bookKey: string;
|
||||
bookFormat: string;
|
||||
section?: PageInfo;
|
||||
pageinfo?: PageInfo;
|
||||
isHoveredAnim: boolean;
|
||||
gridInsets: Insets;
|
||||
}
|
||||
|
||||
const FooterBar: React.FC<FooterBarProps> = ({
|
||||
bookKey,
|
||||
bookFormat,
|
||||
section,
|
||||
pageinfo,
|
||||
isHoveredAnim,
|
||||
gridInsets,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const { getView, getViewState, getProgress, getViewSettings } = useReaderStore();
|
||||
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const [actionTab, setActionTab] = React.useState('');
|
||||
const sliderHeight = useResponsiveSize(28);
|
||||
const tocIconSize = useResponsiveSize(23);
|
||||
const fontIconSize = useResponsiveSize(18);
|
||||
const marginIconSize = useResponsiveSize(20);
|
||||
|
||||
const view = getView(bookKey);
|
||||
const config = getConfig(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const viewState = getViewState(bookKey);
|
||||
|
||||
const handleProgressChange = (value: number) => {
|
||||
view?.goToFraction(value / 100.0);
|
||||
};
|
||||
|
||||
const handleFontSizeChange = (value: number) => {
|
||||
saveViewSettings(envConfig, bookKey, 'defaultFontSize', value);
|
||||
};
|
||||
|
||||
const handleMarginChange = (value: number) => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const marginPx = Math.round((value / 100) * 88);
|
||||
const gapPercent = Math.round((value / 100) * 10);
|
||||
viewSettings.marginTopPx = marginPx;
|
||||
viewSettings.marginBottomPx = marginPx / 2;
|
||||
viewSettings.marginLeftPx = marginPx / 2;
|
||||
viewSettings.marginRightPx = marginPx / 2;
|
||||
saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent, false, false);
|
||||
view?.renderer.setAttribute('margin', `${marginPx}px`);
|
||||
view?.renderer.setAttribute('gap', `${gapPercent}%`);
|
||||
if (viewSettings?.scrolled) {
|
||||
view?.renderer.setAttribute('flow', 'scrolled');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLineHeightChange = (value: number) => {
|
||||
saveViewSettings(envConfig, bookKey, 'lineHeight', value / 10);
|
||||
};
|
||||
|
||||
const handleGoPrevPage = () => {
|
||||
viewPagination(view, viewSettings, 'left');
|
||||
};
|
||||
|
||||
const handleGoNextPage = () => {
|
||||
viewPagination(view, viewSettings, 'right');
|
||||
};
|
||||
|
||||
const handleGoPrevSection = () => {
|
||||
if (view?.renderer.prevSection) {
|
||||
view?.renderer.prevSection();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoNextSection = () => {
|
||||
if (view?.renderer.nextSection) {
|
||||
view?.renderer.nextSection();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
view?.history.back();
|
||||
};
|
||||
|
||||
const handleGoForward = () => {
|
||||
view?.history.forward();
|
||||
};
|
||||
|
||||
const handleSpeakText = async () => {
|
||||
if (!view || !progress || !viewState) return;
|
||||
if (viewState.ttsEnabled) {
|
||||
eventDispatcher.dispatch('tts-stop', { bookKey });
|
||||
} else {
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetActionTab = (tab: string) => {
|
||||
setActionTab(actionTab === tab ? '' : tab);
|
||||
if (tab === 'tts') {
|
||||
setHoveredBookKey('');
|
||||
handleSpeakText();
|
||||
} else if (tab === 'toc') {
|
||||
setHoveredBookKey('');
|
||||
if (config && config.viewSettings) {
|
||||
config.viewSettings.sideBarTab = 'toc';
|
||||
setConfig(bookKey, config);
|
||||
}
|
||||
setSideBarVisible(true);
|
||||
} else if (tab === 'note') {
|
||||
setHoveredBookKey('');
|
||||
setSideBarVisible(true);
|
||||
if (config && config.viewSettings) {
|
||||
config.viewSettings.sideBarTab = 'annotations';
|
||||
setConfig(bookKey, config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hoveredBookKey !== bookKey) {
|
||||
setActionTab('');
|
||||
}
|
||||
}, [hoveredBookKey, bookKey]);
|
||||
|
||||
const getMarginProgressValue = (marginPx: number, gapPercent: number) => {
|
||||
return (marginPx / 88 + gapPercent / 10) * 50;
|
||||
};
|
||||
|
||||
const isVisible = hoveredBookKey === bookKey;
|
||||
const ttsEnabled = viewState?.ttsEnabled;
|
||||
const progressInfo = bookFormat === 'PDF' ? section : pageinfo;
|
||||
const progressValid = !!progressInfo;
|
||||
const progressFraction =
|
||||
progressValid && progressInfo?.total > 0
|
||||
? (progressInfo!.current + 1) / progressInfo!.total || 0
|
||||
: 0;
|
||||
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
role='none'
|
||||
className={clsx(
|
||||
'absolute bottom-0 left-0 z-10 hidden w-full sm:flex sm:h-[52px]',
|
||||
// show scroll bar when vertical and scrolled in desktop
|
||||
viewSettings?.vertical && viewSettings?.scrolled && 'sm:!bottom-3 sm:!h-7',
|
||||
)}
|
||||
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
/>
|
||||
<div
|
||||
role='group'
|
||||
aria-label={_('Footer Bar')}
|
||||
className={clsx(
|
||||
'footer-bar shadow-xs bottom-0 z-10 flex w-full flex-col',
|
||||
'sm:h-[52px] sm:justify-center',
|
||||
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
|
||||
'transition-[opacity,transform] duration-300',
|
||||
// See: https://github.com/readest/readest/issues/1716
|
||||
appService?.isAndroidApp && window.innerWidth < 640 ? 'fixed' : 'absolute',
|
||||
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
// show scroll bar when vertical and scrolled in desktop
|
||||
viewSettings?.vertical && viewSettings?.scrolled && 'sm:!bottom-3 sm:!h-7',
|
||||
isVisible
|
||||
? `pointer-events-auto translate-y-0 opacity-100`
|
||||
: `pointer-events-none translate-y-full opacity-0 sm:translate-y-0`,
|
||||
)}
|
||||
dir={viewSettings?.rtl ? 'rtl' : 'ltr'}
|
||||
onFocus={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => window.innerWidth >= 640 && setHoveredBookKey('')}
|
||||
>
|
||||
{/* Mobile footer bar */}
|
||||
<div
|
||||
className={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'
|
||||
? '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',
|
||||
)}
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
label={_('Reading Progress')}
|
||||
heightPx={sliderHeight}
|
||||
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
|
||||
initialValue={progressValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => handleProgressChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
|
||||
label={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
|
||||
label={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
label={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
label={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
|
||||
label={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
|
||||
label={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'footerbar-font-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
|
||||
actionTab === 'font'
|
||||
? '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',
|
||||
)}
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<Slider
|
||||
label={_('Font Size')}
|
||||
initialValue={viewSettings?.defaultFontSize ?? 16}
|
||||
bubbleLabel={`${viewSettings?.defaultFontSize ?? 16}`}
|
||||
minLabel='A'
|
||||
maxLabel='A'
|
||||
minClassName='text-xs'
|
||||
maxClassName='text-base'
|
||||
onChange={handleFontSizeChange}
|
||||
min={8}
|
||||
max={30}
|
||||
/>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
label={_('Page Margin')}
|
||||
initialValue={getMarginProgressValue(
|
||||
viewSettings?.marginTopPx ?? 44,
|
||||
viewSettings?.gapPercent ?? 5,
|
||||
)}
|
||||
bubbleElement={<TbBoxMargin size={marginIconSize} />}
|
||||
minLabel={_('Small')}
|
||||
maxLabel={_('Large')}
|
||||
step={10}
|
||||
onChange={handleMarginChange}
|
||||
/>
|
||||
<Slider
|
||||
label={_('Line Spacing')}
|
||||
initialValue={(viewSettings?.lineHeight ?? 1.6) * 10}
|
||||
bubbleElement={<RxLineHeight size={marginIconSize} />}
|
||||
minLabel={_('Small')}
|
||||
maxLabel={_('Large')}
|
||||
min={8}
|
||||
max={24}
|
||||
onChange={handleLineHeightChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-200 z-30 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
|
||||
)}
|
||||
style={{
|
||||
paddingBottom: isMobile ? `${gridInsets.bottom * 0.33 + 16}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
label={_('Table of Contents')}
|
||||
icon={<TOCIcon size={tocIconSize} className='' />}
|
||||
onClick={() => handleSetActionTab('toc')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Notes')}
|
||||
icon={<NoteIcon className='' />}
|
||||
onClick={() => handleSetActionTab('note')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Reading Progress')}
|
||||
icon={<SliderIcon className={clsx(actionTab === 'progress' && 'text-blue-500')} />}
|
||||
onClick={() => handleSetActionTab('progress')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Font & Layout')}
|
||||
icon={
|
||||
<FontIcon
|
||||
size={fontIconSize}
|
||||
className={clsx(actionTab === 'font' && 'text-blue-500')}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetActionTab('font')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Speak')}
|
||||
icon={<TTSIcon className={ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={() => handleSetActionTab('tts')}
|
||||
/>
|
||||
</div>
|
||||
{/* Desktop / Pad footer bar */}
|
||||
<div
|
||||
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
|
||||
label={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
|
||||
label={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
label={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
label={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
{progressValid && (
|
||||
<span
|
||||
title={_('Reading Progress')}
|
||||
aria-label={_('Reading Progress') + ': ' + `${Math.round(progressFraction * 100)}%`}
|
||||
className='mx-2 text-center text-sm'
|
||||
>
|
||||
<span aria-hidden='true'>{`${Math.round(progressFraction * 100)}%`}</span>
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type='range'
|
||||
className='text-base-content mx-2 min-w-0 flex-1'
|
||||
min={0}
|
||||
max={100}
|
||||
aria-label={_('Jump to Location')}
|
||||
value={progressValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) =>
|
||||
handleProgressChange(parseInt((e.target as HTMLInputElement).value, 10))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon={<FaHeadphones className={ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={handleSpeakText}
|
||||
label={_('Speak')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
|
||||
label={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
|
||||
label={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TTSControl bookKey={bookKey} gridInsets={gridInsets} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterBar;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { FaHeadphones } from 'react-icons/fa6';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
|
||||
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
|
||||
import { getNavigationIcon, getNavigationLabel, getNavigationHandler } from './utils';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { FooterBarChildProps } from './types';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
|
||||
bookKey,
|
||||
gridInsets,
|
||||
progressValid,
|
||||
progressFraction,
|
||||
navigationHandlers,
|
||||
onSpeakText,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { getView, getViewState, getViewSettings } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const viewState = getViewState(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
|
||||
style={{ bottom: isMobile ? `${gridInsets.bottom * 0.33}px` : '0px' }}
|
||||
>
|
||||
<Button
|
||||
icon={getNavigationIcon(
|
||||
viewSettings?.rtl,
|
||||
<RiArrowLeftDoubleLine />,
|
||||
<RiArrowRightDoubleLine />,
|
||||
)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onPrevSection,
|
||||
navigationHandlers.onNextSection,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Previous Section'), _('Next Section'))}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(viewSettings?.rtl, <RiArrowLeftSLine />, <RiArrowRightSLine />)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onPrevPage,
|
||||
navigationHandlers.onNextPage,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Previous Page'), _('Next Page'))}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(viewSettings?.rtl, <RiArrowGoBackLine />, <RiArrowGoForwardLine />)}
|
||||
onClick={navigationHandlers.onGoBack}
|
||||
label={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(viewSettings?.rtl, <RiArrowGoForwardLine />, <RiArrowGoBackLine />)}
|
||||
onClick={navigationHandlers.onGoForward}
|
||||
label={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
{progressValid && (
|
||||
<span
|
||||
title={_('Reading Progress')}
|
||||
aria-label={`${_('Reading Progress')}: ${Math.round(progressFraction * 100)}%`}
|
||||
className='mx-2 text-center text-sm'
|
||||
>
|
||||
<span aria-hidden='true'>{`${Math.round(progressFraction * 100)}%`}</span>
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type='range'
|
||||
className='text-base-content mx-2 min-w-0 flex-1'
|
||||
min={0}
|
||||
max={100}
|
||||
aria-label={_('Jump to Location')}
|
||||
value={progressValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => navigationHandlers.onProgressChange(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<Button
|
||||
icon={<FaHeadphones className={viewState?.ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={onSpeakText!}
|
||||
label={_('Speak')}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(viewSettings?.rtl, <RiArrowRightSLine />, <RiArrowLeftSLine />)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onNextPage,
|
||||
navigationHandlers.onPrevPage,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Next Page'), _('Previous Page'))}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(
|
||||
viewSettings?.rtl,
|
||||
<RiArrowRightDoubleLine />,
|
||||
<RiArrowLeftDoubleLine />,
|
||||
)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onNextSection,
|
||||
navigationHandlers.onPrevSection,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Next Section'), _('Previous Section'))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopFooterBar;
|
||||
@@ -0,0 +1,140 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback } from 'react';
|
||||
import { TbBoxMargin } from 'react-icons/tb';
|
||||
import { RxLineHeight } from 'react-icons/rx';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { saveViewSettings } from '@/helpers/viewSettings';
|
||||
import Slider from '@/components/Slider';
|
||||
|
||||
const FONT_SIZE_LIMITS = {
|
||||
MIN: 8,
|
||||
MAX: 30,
|
||||
DEFAULT: 16,
|
||||
} as const;
|
||||
|
||||
const LINE_HEIGHT_LIMITS = {
|
||||
MIN: 8,
|
||||
MAX: 24,
|
||||
DEFAULT: 16,
|
||||
MULTIPLIER: 10,
|
||||
} as const;
|
||||
|
||||
const MARGIN_CONSTANTS = {
|
||||
MAX_MARGIN_PX: 88,
|
||||
MAX_GAP_PERCENT: 10,
|
||||
MARGIN_RATIO: 50,
|
||||
} as const;
|
||||
|
||||
interface FontLayoutPanelProps {
|
||||
bookKey: string;
|
||||
actionTab: string;
|
||||
mobileBottomOffset: string;
|
||||
marginIconSize: number;
|
||||
}
|
||||
|
||||
export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
|
||||
bookKey,
|
||||
actionTab,
|
||||
mobileBottomOffset,
|
||||
marginIconSize,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const view = getView(bookKey);
|
||||
|
||||
const handleFontSizeChange = useCallback(
|
||||
(value: number) => {
|
||||
saveViewSettings(envConfig, bookKey, 'defaultFontSize', value);
|
||||
},
|
||||
[envConfig, bookKey],
|
||||
);
|
||||
|
||||
const handleMarginChange = useCallback(
|
||||
(value: number) => {
|
||||
const currentViewSettings = getViewSettings(bookKey);
|
||||
if (!currentViewSettings) return;
|
||||
|
||||
const { MAX_MARGIN_PX, MAX_GAP_PERCENT } = MARGIN_CONSTANTS;
|
||||
const marginPx = Math.round((value / 100) * MAX_MARGIN_PX);
|
||||
const gapPercent = Math.round((value / 100) * MAX_GAP_PERCENT);
|
||||
|
||||
currentViewSettings.marginTopPx = marginPx;
|
||||
currentViewSettings.marginBottomPx = marginPx / 2;
|
||||
currentViewSettings.marginLeftPx = marginPx / 2;
|
||||
currentViewSettings.marginRightPx = marginPx / 2;
|
||||
|
||||
saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent, false, false);
|
||||
view?.renderer.setAttribute('margin', `${marginPx}px`);
|
||||
view?.renderer.setAttribute('gap', `${gapPercent}%`);
|
||||
|
||||
if (currentViewSettings?.scrolled) {
|
||||
view?.renderer.setAttribute('flow', 'scrolled');
|
||||
}
|
||||
},
|
||||
[envConfig, bookKey, view, getViewSettings],
|
||||
);
|
||||
|
||||
const handleLineHeightChange = useCallback(
|
||||
(value: number) => {
|
||||
saveViewSettings(envConfig, bookKey, 'lineHeight', value / LINE_HEIGHT_LIMITS.MULTIPLIER);
|
||||
},
|
||||
[envConfig, bookKey],
|
||||
);
|
||||
|
||||
const getMarginProgressValue = useCallback((marginPx: number, gapPercent: number) => {
|
||||
const { MAX_MARGIN_PX, MAX_GAP_PERCENT, MARGIN_RATIO } = MARGIN_CONSTANTS;
|
||||
return (marginPx / MAX_MARGIN_PX + gapPercent / MAX_GAP_PERCENT) * MARGIN_RATIO;
|
||||
}, []);
|
||||
|
||||
const classes = clsx(
|
||||
'footerbar-font-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
|
||||
actionTab === 'font'
|
||||
? '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: mobileBottomOffset }}>
|
||||
<Slider
|
||||
label={_('Font Size')}
|
||||
initialValue={viewSettings?.defaultFontSize ?? FONT_SIZE_LIMITS.DEFAULT}
|
||||
bubbleLabel={`${viewSettings?.defaultFontSize ?? FONT_SIZE_LIMITS.DEFAULT}`}
|
||||
minLabel='A'
|
||||
maxLabel='A'
|
||||
minClassName='text-xs'
|
||||
maxClassName='text-base'
|
||||
onChange={handleFontSizeChange}
|
||||
min={FONT_SIZE_LIMITS.MIN}
|
||||
max={FONT_SIZE_LIMITS.MAX}
|
||||
/>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
label={_('Page Margin')}
|
||||
initialValue={getMarginProgressValue(
|
||||
viewSettings?.marginTopPx ?? 44,
|
||||
viewSettings?.gapPercent ?? 5,
|
||||
)}
|
||||
bubbleElement={<TbBoxMargin size={marginIconSize} />}
|
||||
minLabel={_('Small')}
|
||||
maxLabel={_('Large')}
|
||||
step={10}
|
||||
onChange={handleMarginChange}
|
||||
/>
|
||||
<Slider
|
||||
label={_('Line Spacing')}
|
||||
initialValue={(viewSettings?.lineHeight ?? 1.6) * LINE_HEIGHT_LIMITS.MULTIPLIER}
|
||||
bubbleElement={<RxLineHeight size={marginIconSize} />}
|
||||
minLabel={_('Small')}
|
||||
maxLabel={_('Large')}
|
||||
min={LINE_HEIGHT_LIMITS.MIN}
|
||||
max={LINE_HEIGHT_LIMITS.MAX}
|
||||
onChange={handleLineHeightChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { FooterBarProps, NavigationHandlers, FooterBarChildProps } from './types';
|
||||
import { viewPagination } from '../../hooks/usePagination';
|
||||
import MobileFooterBar from './MobileFooterBar';
|
||||
import DesktopFooterBar from './DesktopFooterBar';
|
||||
import TTSControl from '../tts/TTSControl';
|
||||
|
||||
const FooterBar: React.FC<FooterBarProps> = ({
|
||||
bookKey,
|
||||
bookFormat,
|
||||
section,
|
||||
pageinfo,
|
||||
isHoveredAnim,
|
||||
gridInsets,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const { getView, getViewState, getProgress, getViewSettings } = useReaderStore();
|
||||
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
|
||||
const [actionTab, setActionTab] = useState('');
|
||||
|
||||
const view = getView(bookKey);
|
||||
const config = getConfig(bookKey);
|
||||
const viewState = getViewState(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
|
||||
const isVisible = hoveredBookKey === bookKey;
|
||||
|
||||
const progressInfo = useMemo(
|
||||
() => (bookFormat === 'PDF' ? section : pageinfo),
|
||||
[bookFormat, section, pageinfo],
|
||||
);
|
||||
|
||||
const progressValid = !!progressInfo;
|
||||
const progressFraction = useMemo(() => {
|
||||
if (!progressValid || !progressInfo?.total || progressInfo.total <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (progressInfo.current + 1) / progressInfo.total;
|
||||
}, [progressValid, progressInfo]);
|
||||
|
||||
const handleProgressChange = useCallback(
|
||||
(value: number) => {
|
||||
view?.goToFraction(value / 100.0);
|
||||
},
|
||||
[view],
|
||||
);
|
||||
|
||||
const handleGoPrevPage = useCallback(() => {
|
||||
viewPagination(view, viewSettings, 'left');
|
||||
}, [view, viewSettings]);
|
||||
|
||||
const handleGoNextPage = useCallback(() => {
|
||||
viewPagination(view, viewSettings, 'right');
|
||||
}, [view, viewSettings]);
|
||||
|
||||
const handleGoPrevSection = useCallback(() => {
|
||||
view?.renderer.prevSection?.();
|
||||
}, [view]);
|
||||
|
||||
const handleGoNextSection = useCallback(() => {
|
||||
view?.renderer.nextSection?.();
|
||||
}, [view]);
|
||||
|
||||
const handleGoBack = useCallback(() => {
|
||||
view?.history.back();
|
||||
}, [view]);
|
||||
|
||||
const handleGoForward = useCallback(() => {
|
||||
view?.history.forward();
|
||||
}, [view]);
|
||||
|
||||
const handleSpeakText = useCallback(async () => {
|
||||
if (!view || !progress || !viewState) return;
|
||||
|
||||
const eventType = viewState.ttsEnabled ? 'tts-stop' : 'tts-speak';
|
||||
eventDispatcher.dispatch(eventType, { bookKey });
|
||||
}, [view, progress, viewState, bookKey]);
|
||||
|
||||
const handleSetActionTab = useCallback(
|
||||
(tab: string) => {
|
||||
setActionTab((prevTab) => (prevTab === tab ? '' : tab));
|
||||
|
||||
if (tab === 'tts') {
|
||||
setHoveredBookKey('');
|
||||
handleSpeakText();
|
||||
} else if (tab === 'toc') {
|
||||
setHoveredBookKey('');
|
||||
if (config?.viewSettings) {
|
||||
config.viewSettings.sideBarTab = 'toc';
|
||||
setConfig(bookKey, config);
|
||||
}
|
||||
setSideBarVisible(true);
|
||||
} else if (tab === 'note') {
|
||||
setHoveredBookKey('');
|
||||
setSideBarVisible(true);
|
||||
if (config?.viewSettings) {
|
||||
config.viewSettings.sideBarTab = 'annotations';
|
||||
setConfig(bookKey, config);
|
||||
}
|
||||
}
|
||||
},
|
||||
[config, bookKey, setConfig, setSideBarVisible, setHoveredBookKey, handleSpeakText],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hoveredBookKey !== bookKey) {
|
||||
setActionTab('');
|
||||
}
|
||||
}, [hoveredBookKey, bookKey]);
|
||||
|
||||
const navigationHandlers: NavigationHandlers = useMemo(
|
||||
() => ({
|
||||
onPrevPage: handleGoPrevPage,
|
||||
onNextPage: handleGoNextPage,
|
||||
onPrevSection: handleGoPrevSection,
|
||||
onNextSection: handleGoNextSection,
|
||||
onGoBack: handleGoBack,
|
||||
onGoForward: handleGoForward,
|
||||
onProgressChange: handleProgressChange,
|
||||
}),
|
||||
[
|
||||
handleGoPrevPage,
|
||||
handleGoNextPage,
|
||||
handleGoPrevSection,
|
||||
handleGoNextSection,
|
||||
handleGoBack,
|
||||
handleGoForward,
|
||||
handleProgressChange,
|
||||
],
|
||||
);
|
||||
|
||||
const commonProps: FooterBarChildProps = {
|
||||
bookKey,
|
||||
gridInsets,
|
||||
actionTab,
|
||||
progressValid,
|
||||
progressFraction,
|
||||
navigationHandlers,
|
||||
onSetActionTab: handleSetActionTab,
|
||||
onSpeakText: handleSpeakText,
|
||||
};
|
||||
|
||||
const containerClasses = clsx(
|
||||
'footer-bar shadow-xs bottom-0 z-10 flex w-full flex-col',
|
||||
'sm:h-[52px] sm:justify-center',
|
||||
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
|
||||
'transition-[opacity,transform] duration-300',
|
||||
appService?.isAndroidApp && window.innerWidth < 640 ? 'fixed' : 'absolute',
|
||||
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
viewSettings?.vertical && viewSettings?.scrolled && 'sm:!bottom-3 sm:!h-7',
|
||||
isVisible
|
||||
? 'pointer-events-auto translate-y-0 opacity-100'
|
||||
: 'pointer-events-none translate-y-full opacity-0 sm:translate-y-0',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hover trigger area */}
|
||||
<div
|
||||
role='none'
|
||||
className={clsx(
|
||||
'absolute bottom-0 left-0 z-10 hidden w-full sm:flex sm:h-[52px]',
|
||||
viewSettings?.vertical && viewSettings?.scrolled && 'sm:!bottom-3 sm:!h-7',
|
||||
)}
|
||||
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
/>
|
||||
|
||||
{/* Main footer container */}
|
||||
<div
|
||||
role='group'
|
||||
aria-label={_('Footer Bar')}
|
||||
className={containerClasses}
|
||||
dir={viewSettings?.rtl ? 'rtl' : 'ltr'}
|
||||
onFocus={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => window.innerWidth >= 640 && setHoveredBookKey('')}
|
||||
>
|
||||
<MobileFooterBar {...commonProps} />
|
||||
<DesktopFooterBar {...commonProps} />
|
||||
</div>
|
||||
|
||||
<TTSControl bookKey={bookKey} gridInsets={gridInsets} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterBar;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { NavigationPanel } from './NavigationPanel';
|
||||
import { FontLayoutPanel } from './FontLayoutPanel';
|
||||
import { NavigationBar } from './NavigationBar';
|
||||
import { FooterBarChildProps } from './types';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const MobileFooterBar: React.FC<FooterBarChildProps> = ({
|
||||
bookKey,
|
||||
gridInsets,
|
||||
actionTab,
|
||||
progressValid,
|
||||
progressFraction,
|
||||
navigationHandlers,
|
||||
onSetActionTab,
|
||||
}) => {
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
const sliderHeight = useResponsiveSize(28);
|
||||
const marginIconSize = useResponsiveSize(20);
|
||||
const bottomOffset = isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px';
|
||||
const navPadding = isMobile ? `${gridInsets.bottom * 0.33 + 16}px` : '0px';
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationPanel
|
||||
actionTab={actionTab!}
|
||||
progressFraction={progressFraction}
|
||||
progressValid={progressValid}
|
||||
navigationHandlers={navigationHandlers}
|
||||
bottomOffset={bottomOffset}
|
||||
sliderHeight={sliderHeight}
|
||||
/>
|
||||
<FontLayoutPanel
|
||||
bookKey={bookKey}
|
||||
actionTab={actionTab!}
|
||||
mobileBottomOffset={bottomOffset}
|
||||
marginIconSize={marginIconSize}
|
||||
/>
|
||||
<NavigationBar
|
||||
bookKey={bookKey}
|
||||
actionTab={actionTab!}
|
||||
navPadding={navPadding}
|
||||
onSetActionTab={onSetActionTab!}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileFooterBar;
|
||||
@@ -0,0 +1,62 @@
|
||||
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 { MdOutlineHeadphones as TTSIcon } from 'react-icons/md';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface NavigationBarProps {
|
||||
bookKey: string;
|
||||
actionTab: string;
|
||||
navPadding: string;
|
||||
onSetActionTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const NavigationBar: React.FC<NavigationBarProps> = ({
|
||||
bookKey,
|
||||
actionTab,
|
||||
navPadding: mobileNavPadding,
|
||||
onSetActionTab,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { getViewState } = useReaderStore();
|
||||
const viewState = getViewState(bookKey);
|
||||
const tocIconSize = useResponsiveSize(23);
|
||||
const fontIconSize = useResponsiveSize(18);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('bg-base-200 z-30 mt-auto flex w-full justify-between px-8 py-4 sm:hidden')}
|
||||
style={{ paddingBottom: mobileNavPadding }}
|
||||
>
|
||||
<Button
|
||||
label={_('Table of Contents')}
|
||||
icon={<TOCIcon size={tocIconSize} />}
|
||||
onClick={() => onSetActionTab('toc')}
|
||||
/>
|
||||
<Button label={_('Notes')} icon={<NoteIcon />} onClick={() => onSetActionTab('note')} />
|
||||
<Button
|
||||
label={_('Reading Progress')}
|
||||
icon={<SliderIcon className={clsx(actionTab === 'progress' && 'text-blue-500')} />}
|
||||
onClick={() => onSetActionTab('progress')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Font & Layout')}
|
||||
icon={
|
||||
<FontIcon size={fontIconSize} className={clsx(actionTab === 'font' && 'text-blue-500')} />
|
||||
}
|
||||
onClick={() => onSetActionTab('font')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Speak')}
|
||||
icon={<TTSIcon className={viewState?.ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={() => onSetActionTab('tts')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
|
||||
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
|
||||
import { getNavigationIcon, getNavigationLabel, getNavigationHandler } from './utils';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { ViewSettings } from '@/types/book';
|
||||
import { NavigationHandlers } from './types';
|
||||
import Button from '@/components/Button';
|
||||
import Slider from '@/components/Slider';
|
||||
|
||||
interface NavigationPanelProps {
|
||||
actionTab: string;
|
||||
progressFraction: number;
|
||||
progressValid: boolean;
|
||||
navigationHandlers: NavigationHandlers;
|
||||
viewSettings?: ViewSettings;
|
||||
bottomOffset: string;
|
||||
sliderHeight: number;
|
||||
}
|
||||
|
||||
export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
actionTab,
|
||||
progressFraction,
|
||||
progressValid,
|
||||
navigationHandlers,
|
||||
viewSettings,
|
||||
bottomOffset: mobileBottomOffset,
|
||||
sliderHeight,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
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'
|
||||
? '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: mobileBottomOffset }}>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
label={_('Reading Progress')}
|
||||
heightPx={sliderHeight}
|
||||
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
|
||||
initialValue={progressValid ? progressFraction * 100 : 0}
|
||||
onChange={navigationHandlers.onProgressChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Button
|
||||
icon={getNavigationIcon(
|
||||
viewSettings?.rtl,
|
||||
<RiArrowLeftDoubleLine />,
|
||||
<RiArrowRightDoubleLine />,
|
||||
)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onPrevSection,
|
||||
navigationHandlers.onNextSection,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Previous Section'), _('Next Section'))}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(viewSettings?.rtl, <RiArrowLeftSLine />, <RiArrowRightSLine />)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onPrevPage,
|
||||
navigationHandlers.onNextPage,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Previous Page'), _('Next Page'))}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(
|
||||
viewSettings?.rtl,
|
||||
<RiArrowGoBackLine />,
|
||||
<RiArrowGoForwardLine />,
|
||||
)}
|
||||
onClick={navigationHandlers.onGoBack}
|
||||
label={_('Go Back')}
|
||||
disabled={!navigationHandlers.onGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(
|
||||
viewSettings?.rtl,
|
||||
<RiArrowGoForwardLine />,
|
||||
<RiArrowGoBackLine />,
|
||||
)}
|
||||
onClick={navigationHandlers.onGoForward}
|
||||
label={_('Go Forward')}
|
||||
disabled={!navigationHandlers.onGoForward}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(viewSettings?.rtl, <RiArrowRightSLine />, <RiArrowLeftSLine />)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onNextPage,
|
||||
navigationHandlers.onPrevPage,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Next Page'), _('Previous Page'))}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(
|
||||
viewSettings?.rtl,
|
||||
<RiArrowRightDoubleLine />,
|
||||
<RiArrowLeftDoubleLine />,
|
||||
)}
|
||||
onClick={getNavigationHandler(
|
||||
viewSettings?.rtl,
|
||||
navigationHandlers.onNextSection,
|
||||
navigationHandlers.onPrevSection,
|
||||
)}
|
||||
label={getNavigationLabel(viewSettings?.rtl, _('Next Section'), _('Previous Section'))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PageInfo } from '@/types/book';
|
||||
import { Insets } from '@/types/misc';
|
||||
|
||||
export interface FooterBarProps {
|
||||
bookKey: string;
|
||||
bookFormat: string;
|
||||
section?: PageInfo;
|
||||
pageinfo?: PageInfo;
|
||||
isHoveredAnim: boolean;
|
||||
gridInsets: Insets;
|
||||
}
|
||||
|
||||
export interface NavigationHandlers {
|
||||
onPrevPage: () => void;
|
||||
onNextPage: () => void;
|
||||
onPrevSection: () => void;
|
||||
onNextSection: () => void;
|
||||
onGoBack: () => void;
|
||||
onGoForward: () => void;
|
||||
onProgressChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export interface FooterBarChildProps {
|
||||
bookKey: string;
|
||||
navigationHandlers: NavigationHandlers;
|
||||
progressFraction: number;
|
||||
progressValid: boolean;
|
||||
gridInsets: Insets;
|
||||
actionTab: string;
|
||||
onSetActionTab: (tab: string) => void;
|
||||
onSpeakText: () => void;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
export const getNavigationIcon = (
|
||||
rtl: boolean | undefined,
|
||||
prevIcon: React.ReactNode,
|
||||
nextIcon: React.ReactNode,
|
||||
) => (rtl ? nextIcon : prevIcon);
|
||||
|
||||
export const getNavigationLabel = (
|
||||
rtl: boolean | undefined,
|
||||
prevLabel: string,
|
||||
nextLabel: string,
|
||||
) => (rtl ? nextLabel : prevLabel);
|
||||
|
||||
export const getNavigationHandler = (
|
||||
rtl: boolean | undefined,
|
||||
prevHandler: () => void,
|
||||
nextHandler: () => void,
|
||||
) => (rtl ? nextHandler : prevHandler);
|
||||
Reference in New Issue
Block a user