From 9cd88fe8399ab40fc8b4f27630530f2deaec5839 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 3 Feb 2026 00:53:26 +0800 Subject: [PATCH] fix(progress): show correct progress info for subsections in FB2, closes #3136 (#3145) --- .../src/app/reader/components/BooksGrid.tsx | 1 + .../app/reader/components/ProgressInfo.tsx | 55 +++++++++++++------ apps/readest-app/src/libs/document.ts | 2 + apps/readest-app/src/services/constants.ts | 3 + apps/readest-app/src/utils/toc.ts | 36 +++++++++--- packages/foliate-js | 2 +- 6 files changed, 73 insertions(+), 26 deletions(-) diff --git a/apps/readest-app/src/app/reader/components/BooksGrid.tsx b/apps/readest-app/src/app/reader/components/BooksGrid.tsx index 29d3a868..4aeb8274 100644 --- a/apps/readest-app/src/app/reader/components/BooksGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BooksGrid.tsx @@ -203,6 +203,7 @@ const BooksGrid: React.FC = ({ bookKeys, onCloseBook }) => { {showFooter && ( = ({ bookKey, + toc, section, pageinfo, - timeinfo, horizontalGap, contentInsets, gridInsets, @@ -31,10 +34,10 @@ const ProgressInfoView: React.FC = ({ const _ = useTranslation(); const { envConfig, appService } = useEnv(); const { getBookData } = useBookDataStore(); - const { getView, getViewSettings } = useReaderStore(); - const view = getView(bookKey); + 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; @@ -51,23 +54,41 @@ const ProgressInfoView: React.FC = ({ const lang = localStorage?.getItem('i18nextLng') || ''; const localize = isVertical && lang.toLowerCase().startsWith('zh'); - const progress = bookData?.isFixedLayout ? section : pageinfo; - const progressInfo = formatProgress(progress?.current, progress?.total, template, localize, lang); + const pageInfo = bookData?.isFixedLayout ? section : pageinfo; + const progressInfo = formatProgress(pageInfo?.current, pageInfo?.total, template, localize, lang); - const timeLeft = timeinfo - ? _('{{time}} min left in chapter', { - time: formatNumber(Math.round(timeinfo.section), localize, lang), - }) - : ''; - const { page = 0, pages = 0 } = view?.renderer || {}; + 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 = - pages - 1 > page + total - 1 >= current ? localize ? _('{{number}} pages left in chapter', { - number: formatNumber(pages - page - 1, localize, lang), + number: formatNumber(pages, localize, lang), }) : _('{{count}} pages left in chapter', { - count: pages - page - 1, + count: pages, }) : ''; @@ -131,8 +152,8 @@ const ProgressInfoView: React.FC = ({ aria-label={[ progress ? _('On {{current}} of {{total}} page', { - current: progress.current + 1, - total: progress.total, + current: current + 1, + total: total, }) : '', timeLeft, diff --git a/apps/readest-app/src/libs/document.ts b/apps/readest-app/src/libs/document.ts index c03b7f30..502143d4 100644 --- a/apps/readest-app/src/libs/document.ts +++ b/apps/readest-app/src/libs/document.ts @@ -26,8 +26,10 @@ export interface SectionItem { cfi: string; size: number; linear: string; + href?: string; location?: Location; pageSpread?: 'left' | 'right' | 'center' | ''; + subitems?: Array; createDocument: () => Promise; } diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index 75e14414..6b4dcc83 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -707,6 +707,9 @@ export const DOUBLE_CLICK_INTERVAL_THRESHOLD_MS = 250; export const DISABLE_DOUBLE_CLICK_ON_MOBILE = true; export const LONG_HOLD_THRESHOLD = 500; +export const SIZE_PER_LOC = 1500; +export const SIZE_PER_TIME_UNIT = 1600; + export const CUSTOM_THEME_TEMPLATES = [ { light: { diff --git a/apps/readest-app/src/utils/toc.ts b/apps/readest-app/src/utils/toc.ts index 54aa7205..f702541d 100644 --- a/apps/readest-app/src/utils/toc.ts +++ b/apps/readest-app/src/utils/toc.ts @@ -1,6 +1,7 @@ import { ConvertChineseVariant } from '@/types/book'; import { SectionItem, TOCItem, CFI, BookDoc } from '@/libs/document'; import { initSimpleCC, runSimpleCC } from '@/utils/simplecc'; +import { SIZE_PER_LOC } from '@/services/constants'; export const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => { for (const item of toc) { @@ -69,17 +70,38 @@ export const updateToc = async ( return acc; }, []); const totalSize = cumulativeSizes[cumulativeSizes.length - 1] || 0; - const sizePerLoc = 1500; + const totalLocations = Math.floor(totalSize / SIZE_PER_LOC); sections.forEach((section, index) => { section.location = { - current: Math.floor(cumulativeSizes[index]! / sizePerLoc), - next: Math.floor((cumulativeSizes[index]! + sizes[index]!) / sizePerLoc), - total: Math.floor(totalSize / sizePerLoc), + current: Math.floor(cumulativeSizes[index]! / SIZE_PER_LOC), + next: Math.floor((cumulativeSizes[index]! + sizes[index]!) / SIZE_PER_LOC), + total: totalLocations, }; + const subitems = section.subitems || []; + subitems.forEach((subitem, subitemIndex) => { + subitem.location = { + current: Math.floor( + (cumulativeSizes[index]! + + subitems.slice(0, subitemIndex).reduce((sum, t) => sum + (t.size || 0), 0)) / + SIZE_PER_LOC, + ), + next: Math.floor( + (cumulativeSizes[index]! + + subitems.slice(0, subitemIndex + 1).reduce((sum, t) => sum + (t.size || 0), 0)) / + SIZE_PER_LOC, + ), + total: totalLocations, + }; + }); }); const sectionsMap = sections.reduce((map: Record, section) => { map[section.id] = section; + section.subitems?.forEach((subitem) => { + if (subitem.href) { + map[subitem.href] = subitem; + } + }); return map; }, {}); @@ -112,12 +134,10 @@ const updateTocData = ( item.id ??= index++; if (item.href) { const id = bookDoc.splitTOCHref(item.href)[0]!; - const section = sectionsMap[id]; + const section = sectionsMap[item.href] || sectionsMap[id]; if (section) { item.cfi = section.cfi; - // Add location only when toc item is at the same level as the section - // otherwise the location will not be accurate - if (id === item.href || items.length <= sections.length) { + if (id === item.href || items.length <= sections.length || item.href === section.href) { item.location = section.location; } } diff --git a/packages/foliate-js b/packages/foliate-js index 29fd7aad..245d64e6 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit 29fd7aadf947639f86ce0de7f381a62b52bd32b4 +Subproject commit 245d64e653423ebd63b55ba3c7d1c1a7cf69933b