feat(ui): progress info with cycleable display modes (#2682)

* Hideable Progress View
* feat: cycle between progress info modes

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
This commit is contained in:
mikepmiller
2025-12-12 02:19:07 -05:00
committed by GitHub
parent 34fd64c5c4
commit 5a20fae204
4 changed files with 73 additions and 15 deletions
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Insets } from '@/types/misc';
import { PageInfo, TimeInfo } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
@@ -7,6 +7,7 @@ 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';
interface PageInfoProps {
bookKey: string;
@@ -28,7 +29,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
gridInsets,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { envConfig, appService } = useEnv();
const { getBookData } = useBookDataStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
@@ -70,15 +71,59 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
})
: '';
const [progressInfoMode, setProgressInfoMode] = useState(viewSettings.progressInfoMode);
const cycleProgressInfoModes = () => {
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]);
return (
<div
role='presentation'
className={clsx(
'progressinfo absolute flex items-center justify-between font-sans',
'pointer-events-none bottom-0',
'pointer-events-auto bottom-0',
isEink ? 'text-sm font-normal' : 'text-neutral-content text-xs font-extralight',
isVertical ? 'writing-vertical-rl' : 'w-full',
isScrolled && !isVertical && 'bg-base-100',
)}
onClick={() => cycleProgressInfoModes()}
aria-label={[
progress
? _('On {{current}} of {{total}} page', {
@@ -115,15 +160,24 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
isVertical ? 'h-full' : 'h-[52px] w-full',
)}
>
{viewSettings.showRemainingTime ? (
<span className='text-start'>{timeLeft}</span>
) : viewSettings.showRemainingPages ? (
<span className='text-start'>{pageLeft}</span>
) : null}
{viewSettings.showProgressInfo && (
<span className={clsx('text-end', isVertical ? 'mt-auto' : 'ms-auto')}>
{progressInfo}
</span>
{(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>
@@ -214,6 +214,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
: 'pointer-events-none translate-y-full opacity-0 sm:translate-y-0',
);
const isMobile = appService?.isMobile || window.innerWidth < 640;
return (
<>
{/* Hover trigger area */}
@@ -222,10 +224,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
className={clsx(
'absolute bottom-0 left-0 z-10 flex h-[52px] w-full',
needHorizontalScroll && 'sm:!bottom-3 sm:!h-7',
isMobile ? 'pointer-events-none' : '',
)}
onClick={() => setHoveredBookKey(bookKey)}
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
onMouseEnter={() => !isMobile && setHoveredBookKey(bookKey)}
onTouchStart={() => !isMobile && setHoveredBookKey(bookKey)}
/>
{/* Main footer container */}
@@ -226,6 +226,7 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = {
showProgressInfo: true,
showMarginsOnScroll: false,
progressStyle: 'fraction',
progressInfoMode: 'all',
};
export const DEFAULT_TTS_CONFIG: TTSConfig = {
+1
View File
@@ -187,6 +187,7 @@ export interface ViewConfig {
showBarsOnScroll: boolean;
showMarginsOnScroll: boolean;
progressStyle: 'percentage' | 'fraction';
progressInfoMode: 'remaining' | 'progress' | 'all' | 'none';
}
export interface TTSConfig {