forked from akai/readest
213 lines
7.3 KiB
TypeScript
213 lines
7.3 KiB
TypeScript
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<PageInfoProps> = ({
|
|
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 (
|
|
<div
|
|
role='presentation'
|
|
className={clsx(
|
|
'progressinfo absolute bottom-0 flex items-center justify-between font-sans',
|
|
isEink ? 'text-sm font-normal' : 'text-neutral-content text-xs font-extralight',
|
|
isVertical ? 'writing-vertical-rl' : 'w-full',
|
|
isScrolled && !isVertical && 'bg-base-100',
|
|
isMobile ? 'pointer-events-auto' : 'pointer-events-none',
|
|
)}
|
|
onClick={() => 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,
|
|
}
|
|
}
|
|
>
|
|
<div
|
|
aria-hidden='true'
|
|
className={clsx(
|
|
'flex items-center justify-between',
|
|
isVertical ? 'h-full' : 'h-[52px] w-full',
|
|
)}
|
|
>
|
|
{(progressInfoMode === 'all' || progressInfoMode === 'remaining') && (
|
|
<>
|
|
{viewSettings.showRemainingTime ? (
|
|
<span className='text-start'>{timeLeft}</span>
|
|
) : viewSettings.showRemainingPages ? (
|
|
<span className='text-start'>{pageLeft}</span>
|
|
) : null}
|
|
</>
|
|
)}
|
|
|
|
{(progressInfoMode === 'all' || progressInfoMode === 'progress') && (
|
|
<>
|
|
{viewSettings.showProgressInfo && (
|
|
<span className={clsx('text-end', isVertical ? 'mt-auto' : 'ms-auto')}>
|
|
{progressInfo}
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProgressInfoView;
|