import clsx from 'clsx'; import React, { useEffect, useMemo, useState } from 'react'; import { Insets } from '@/types/misc'; import { PageInfo, TimeInfo } from '@/types/book'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useBookDataStore } from '@/store/bookDataStore'; import { formatNumber, formatProgress } from '@/utils/progress'; import { saveViewSettings } from '@/helpers/settings'; import { TOCItem } from '@/libs/document'; import { SIZE_PER_LOC, SIZE_PER_TIME_UNIT } from '@/services/constants'; interface PageInfoProps { bookKey: string; toc: TOCItem[]; section?: PageInfo; pageinfo?: PageInfo; timeinfo?: TimeInfo; horizontalGap: number; contentInsets: Insets; gridInsets: Insets; } const ProgressInfoView: React.FC = ({ bookKey, toc, section, pageinfo, horizontalGap, contentInsets, gridInsets, }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); const { getBookData } = useBookDataStore(); const { getProgress, getViewSettings } = useReaderStore(); const bookData = getBookData(bookKey); const viewSettings = getViewSettings(bookKey)!; const progress = getProgress(bookKey); const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder; const isScrolled = viewSettings.scrolled; const isVertical = viewSettings.vertical; const isEink = viewSettings.isEink; const { progressStyle: readingProgressStyle } = viewSettings; const template = readingProgressStyle === 'fraction' ? isVertical ? '{current} ยท {total}' : '{current} / {total}' : '{percent}%'; const lang = localStorage?.getItem('i18nextLng') || ''; const localize = isVertical && lang.toLowerCase().startsWith('zh'); const pageInfo = bookData?.isFixedLayout ? section : pageinfo; const progressInfo = formatProgress(pageInfo?.current, pageInfo?.total, template, localize, lang); const activeHref = useMemo(() => progress?.sectionHref || null, [progress?.sectionHref]); const activeTOCItem = useMemo(() => { if (!activeHref) return null; for (const item of toc) { if (item.href === activeHref) return item; const subitem = item.subitems?.find((sub) => sub.href === activeHref); if (subitem) return subitem; } return null; }, [activeHref, toc]); const current = pageInfo?.current || 0; const total = activeTOCItem?.location ? activeTOCItem.location.next : pageInfo?.total || 0; const pages = Math.max(total - current, 0); const timeLeft = total - 1 >= current ? _('{{time}} min left in chapter', { time: formatNumber( Math.round((pages * SIZE_PER_LOC) / SIZE_PER_TIME_UNIT), localize, lang, ), }) : ''; const pageLeft = total - 1 >= current ? localize ? _('{{number}} pages left in chapter', { number: formatNumber(pages, localize, lang), }) : _('{{count}} pages left in chapter', { count: pages, }) : ''; const [progressInfoMode, setProgressInfoMode] = useState(viewSettings.progressInfoMode); const cycleProgressInfoModes = () => { if (!viewSettings.tapToToggleFooter) return; const hasRemainingInfo = viewSettings.showRemainingTime || viewSettings.showRemainingPages; const hasProgressInfo = viewSettings.showProgressInfo; const modeSequence: (typeof progressInfoMode)[] = ['all', 'remaining', 'progress', 'none']; const currentIndex = modeSequence.indexOf(progressInfoMode); for (let i = 1; i <= modeSequence.length; i++) { const nextIndex = (currentIndex + i) % modeSequence.length; const nextMode = modeSequence[nextIndex]!; const currentRenders = { remaining: progressInfoMode === 'all' || progressInfoMode === 'remaining' ? hasRemainingInfo : false, progress: progressInfoMode === 'all' || progressInfoMode === 'progress' ? hasProgressInfo : false, }; const nextRenders = { remaining: nextMode === 'all' || nextMode === 'remaining' ? hasRemainingInfo : false, progress: nextMode === 'all' || nextMode === 'progress' ? hasProgressInfo : false, }; const isDifferent = currentRenders.remaining !== nextRenders.remaining || currentRenders.progress !== nextRenders.progress; if (isDifferent) { setProgressInfoMode(nextMode); return; } } const nextIndex = (currentIndex + 1) % modeSequence.length; setProgressInfoMode(modeSequence[nextIndex]!); }; useEffect(() => { saveViewSettings(envConfig, bookKey, 'progressInfoMode', progressInfoMode); // eslint-disable-next-line react-hooks/exhaustive-deps }, [progressInfoMode]); const isMobile = appService?.isMobile || window.innerWidth < 640; return (
cycleProgressInfoModes()} aria-label={[ progress ? _('On {{current}} of {{total}} page', { current: current + 1, total: total, }) : '', timeLeft, pageLeft, ] .filter(Boolean) .join(', ')} style={ isVertical ? { bottom: `${(contentInsets.bottom - gridInsets.bottom) * 1.5}px`, left: showDoubleBorder ? `calc(${contentInsets.left}px)` : `calc(${Math.max(0, contentInsets.left - 32)}px)`, width: showDoubleBorder ? '32px' : `${contentInsets.left}px`, height: `calc(100% - ${((contentInsets.top + contentInsets.bottom) / 2) * 3}px)`, } : { paddingInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left / 2}px)`, paddingInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right / 2}px)`, paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0, } } >
); }; export default ProgressInfoView;