feat(reader): reference page numbers from EPUB page-list with manual page count fallback (#4549)
Add a 'Reference Pages' reading progress style that shows physical book page numbers in the footer progress info: - When the book carries a page list (EPUB3 nav page-list or EPUB2 NCX pageList — foliate-js already parses both and resolves the current pageItem on relocate; it was just never consumed), display the current page label and use the highest numeric label as the total, so a trailing roman-numeral index page can't corrupt the total (#672). - When the book has none, a per-book 'Reference Page Count' input appears; the reading fraction is mapped linearly onto the entered count (#4542). The count is saved per book only and never propagates to global view settings. - Falls back to percentage display when neither source is available. Verified with the sample books from #672: Caleb's Crossing (EPUB3 page-list, 419 pages) and Count Zero (EPUB2 NCX pageList/page-map, 346 pages — chapter 2 lands exactly on page 22 per its page-map), plus a stripped no-pagelist copy for the manual-count path (175/350 at 50%). Closes #672 Closes #4542 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1840,5 +1840,7 @@
|
||||
"Start Delay": "تأخير البدء",
|
||||
"Off": "إيقاف",
|
||||
"Look up": "بحث",
|
||||
"Search on Goodreads": "البحث في Goodreads"
|
||||
"Search on Goodreads": "البحث في Goodreads",
|
||||
"Reference Pages": "صفحات مرجعية",
|
||||
"Reference Page Count": "عدد الصفحات المرجعية"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "শুরুর বিলম্ব",
|
||||
"Off": "বন্ধ",
|
||||
"Look up": "খুঁজুন",
|
||||
"Search on Goodreads": "Goodreads-এ অনুসন্ধান করুন"
|
||||
"Search on Goodreads": "Goodreads-এ অনুসন্ধান করুন",
|
||||
"Reference Pages": "রেফারেন্স পৃষ্ঠা",
|
||||
"Reference Page Count": "রেফারেন্স পৃষ্ঠার সংখ্যা"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "འགོ་འཛུགས་ཕྱིར་འགྱངས།",
|
||||
"Off": "ཁ་རྒྱག",
|
||||
"Look up": "འཚོལ་བ།",
|
||||
"Search on Goodreads": "Goodreads ནང་འཚོལ།"
|
||||
"Search on Goodreads": "Goodreads ནང་འཚོལ།",
|
||||
"Reference Pages": "དཔེ་སྟོན་ཤོག་ངོས།",
|
||||
"Reference Page Count": "དཔེ་སྟོན་ཤོག་ངོས་ཀྱི་གྲངས།"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "Startverzögerung",
|
||||
"Off": "Aus",
|
||||
"Look up": "Nachschlagen",
|
||||
"Search on Goodreads": "Auf Goodreads suchen"
|
||||
"Search on Goodreads": "Auf Goodreads suchen",
|
||||
"Reference Pages": "Referenzseiten",
|
||||
"Reference Page Count": "Anzahl der Referenzseiten"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "Καθυστέρηση έναρξης",
|
||||
"Off": "Ανενεργό",
|
||||
"Look up": "Αναζήτηση",
|
||||
"Search on Goodreads": "Αναζήτηση στο Goodreads"
|
||||
"Search on Goodreads": "Αναζήτηση στο Goodreads",
|
||||
"Reference Pages": "Σελίδες αναφοράς",
|
||||
"Reference Page Count": "Αριθμός σελίδων αναφοράς"
|
||||
}
|
||||
|
||||
@@ -1741,5 +1741,7 @@
|
||||
"Start Delay": "Retardo de inicio",
|
||||
"Off": "Desactivado",
|
||||
"Look up": "Buscar",
|
||||
"Search on Goodreads": "Buscar en Goodreads"
|
||||
"Search on Goodreads": "Buscar en Goodreads",
|
||||
"Reference Pages": "Páginas de referencia",
|
||||
"Reference Page Count": "Número de páginas de referencia"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "تأخیر شروع",
|
||||
"Off": "خاموش",
|
||||
"Look up": "جستجو",
|
||||
"Search on Goodreads": "جستجو در Goodreads"
|
||||
"Search on Goodreads": "جستجو در Goodreads",
|
||||
"Reference Pages": "صفحات مرجع",
|
||||
"Reference Page Count": "تعداد صفحات مرجع"
|
||||
}
|
||||
|
||||
@@ -1741,5 +1741,7 @@
|
||||
"Start Delay": "Délai de démarrage",
|
||||
"Off": "Désactivé",
|
||||
"Look up": "Rechercher",
|
||||
"Search on Goodreads": "Rechercher sur Goodreads"
|
||||
"Search on Goodreads": "Rechercher sur Goodreads",
|
||||
"Reference Pages": "Pages de référence",
|
||||
"Reference Page Count": "Nombre de pages de référence"
|
||||
}
|
||||
|
||||
@@ -1741,5 +1741,7 @@
|
||||
"Start Delay": "השהיית התחלה",
|
||||
"Off": "כבוי",
|
||||
"Look up": "חיפוש",
|
||||
"Search on Goodreads": "חיפוש ב-Goodreads"
|
||||
"Search on Goodreads": "חיפוש ב-Goodreads",
|
||||
"Reference Pages": "עמודי ייחוס",
|
||||
"Reference Page Count": "מספר עמודי ייחוס"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "आरंभ विलंब",
|
||||
"Off": "बंद",
|
||||
"Look up": "खोजें",
|
||||
"Search on Goodreads": "Goodreads पर खोजें"
|
||||
"Search on Goodreads": "Goodreads पर खोजें",
|
||||
"Reference Pages": "संदर्भ पृष्ठ",
|
||||
"Reference Page Count": "संदर्भ पृष्ठ संख्या"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "Indítási késleltetés",
|
||||
"Off": "Ki",
|
||||
"Look up": "Keresés",
|
||||
"Search on Goodreads": "Keresés a Goodreadson"
|
||||
"Search on Goodreads": "Keresés a Goodreadson",
|
||||
"Reference Pages": "Referenciaoldalak",
|
||||
"Reference Page Count": "Referenciaoldalak száma"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "Tunda mulai",
|
||||
"Off": "Mati",
|
||||
"Look up": "Cari",
|
||||
"Search on Goodreads": "Cari di Goodreads"
|
||||
"Search on Goodreads": "Cari di Goodreads",
|
||||
"Reference Pages": "Halaman referensi",
|
||||
"Reference Page Count": "Jumlah halaman referensi"
|
||||
}
|
||||
|
||||
@@ -1741,5 +1741,7 @@
|
||||
"Start Delay": "Ritardo di avvio",
|
||||
"Off": "Disattivato",
|
||||
"Look up": "Cerca",
|
||||
"Search on Goodreads": "Cerca su Goodreads"
|
||||
"Search on Goodreads": "Cerca su Goodreads",
|
||||
"Reference Pages": "Pagine di riferimento",
|
||||
"Reference Page Count": "Numero di pagine di riferimento"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "開始遅延",
|
||||
"Off": "オフ",
|
||||
"Look up": "調べる",
|
||||
"Search on Goodreads": "Goodreadsで検索"
|
||||
"Search on Goodreads": "Goodreadsで検索",
|
||||
"Reference Pages": "参照ページ",
|
||||
"Reference Page Count": "参照ページ数"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "시작 지연",
|
||||
"Off": "끄기",
|
||||
"Look up": "찾기",
|
||||
"Search on Goodreads": "Goodreads에서 검색"
|
||||
"Search on Goodreads": "Goodreads에서 검색",
|
||||
"Reference Pages": "참조 페이지",
|
||||
"Reference Page Count": "참조 페이지 수"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "Lengah mula",
|
||||
"Off": "Mati",
|
||||
"Look up": "Cari",
|
||||
"Search on Goodreads": "Cari di Goodreads"
|
||||
"Search on Goodreads": "Cari di Goodreads",
|
||||
"Reference Pages": "Halaman rujukan",
|
||||
"Reference Page Count": "Bilangan halaman rujukan"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "Startvertraging",
|
||||
"Off": "Uit",
|
||||
"Look up": "Opzoeken",
|
||||
"Search on Goodreads": "Zoeken op Goodreads"
|
||||
"Search on Goodreads": "Zoeken op Goodreads",
|
||||
"Reference Pages": "Referentiepagina's",
|
||||
"Reference Page Count": "Aantal referentiepagina's"
|
||||
}
|
||||
|
||||
@@ -1774,5 +1774,7 @@
|
||||
"Start Delay": "Opóźnienie startu",
|
||||
"Off": "Wył.",
|
||||
"Look up": "Wyszukaj",
|
||||
"Search on Goodreads": "Szukaj w Goodreads"
|
||||
"Search on Goodreads": "Szukaj w Goodreads",
|
||||
"Reference Pages": "Strony referencyjne",
|
||||
"Reference Page Count": "Liczba stron referencyjnych"
|
||||
}
|
||||
|
||||
@@ -1741,5 +1741,7 @@
|
||||
"Start Delay": "Atraso de início",
|
||||
"Off": "Desligado",
|
||||
"Look up": "Pesquisar",
|
||||
"Search on Goodreads": "Pesquisar no Goodreads"
|
||||
"Search on Goodreads": "Pesquisar no Goodreads",
|
||||
"Reference Pages": "Páginas de referência",
|
||||
"Reference Page Count": "Número de páginas de referência"
|
||||
}
|
||||
|
||||
@@ -1741,5 +1741,7 @@
|
||||
"Start Delay": "Atraso de início",
|
||||
"Off": "Desligado",
|
||||
"Look up": "Pesquisar",
|
||||
"Search on Goodreads": "Pesquisar no Goodreads"
|
||||
"Search on Goodreads": "Pesquisar no Goodreads",
|
||||
"Reference Pages": "Páginas de referência",
|
||||
"Reference Page Count": "Número de páginas de referência"
|
||||
}
|
||||
|
||||
@@ -1741,5 +1741,7 @@
|
||||
"Start Delay": "Întârziere la pornire",
|
||||
"Off": "Dezactivat",
|
||||
"Look up": "Caută",
|
||||
"Search on Goodreads": "Caută pe Goodreads"
|
||||
"Search on Goodreads": "Caută pe Goodreads",
|
||||
"Reference Pages": "Pagini de referință",
|
||||
"Reference Page Count": "Numărul paginilor de referință"
|
||||
}
|
||||
|
||||
@@ -1774,5 +1774,7 @@
|
||||
"Start Delay": "Задержка старта",
|
||||
"Off": "Выкл.",
|
||||
"Look up": "Найти",
|
||||
"Search on Goodreads": "Искать в Goodreads"
|
||||
"Search on Goodreads": "Искать в Goodreads",
|
||||
"Reference Pages": "Страницы бумажной книги",
|
||||
"Reference Page Count": "Число страниц бумажной книги"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "ආරම්භක ප්රමාදය",
|
||||
"Off": "අක්රිය",
|
||||
"Look up": "සොයන්න",
|
||||
"Search on Goodreads": "Goodreads හි සොයන්න"
|
||||
"Search on Goodreads": "Goodreads හි සොයන්න",
|
||||
"Reference Pages": "යොමු පිටු",
|
||||
"Reference Page Count": "යොමු පිටු ගණන"
|
||||
}
|
||||
|
||||
@@ -1774,5 +1774,7 @@
|
||||
"Start Delay": "Zakasnitev začetka",
|
||||
"Off": "Izklopljeno",
|
||||
"Look up": "Poišči",
|
||||
"Search on Goodreads": "Išči v Goodreads"
|
||||
"Search on Goodreads": "Išči v Goodreads",
|
||||
"Reference Pages": "Referenčne strani",
|
||||
"Reference Page Count": "Število referenčnih strani"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "Startfördröjning",
|
||||
"Off": "Av",
|
||||
"Look up": "Slå upp",
|
||||
"Search on Goodreads": "Sök på Goodreads"
|
||||
"Search on Goodreads": "Sök på Goodreads",
|
||||
"Reference Pages": "Referenssidor",
|
||||
"Reference Page Count": "Antal referenssidor"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "தொடக்க தாமதம்",
|
||||
"Off": "அணை",
|
||||
"Look up": "தேடு",
|
||||
"Search on Goodreads": "Goodreads இல் தேடு"
|
||||
"Search on Goodreads": "Goodreads இல் தேடு",
|
||||
"Reference Pages": "குறிப்புப் பக்கங்கள்",
|
||||
"Reference Page Count": "குறிப்புப் பக்கங்களின் எண்ணிக்கை"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "หน่วงเวลาเริ่มต้น",
|
||||
"Off": "ปิด",
|
||||
"Look up": "ค้นหา",
|
||||
"Search on Goodreads": "ค้นหาใน Goodreads"
|
||||
"Search on Goodreads": "ค้นหาใน Goodreads",
|
||||
"Reference Pages": "หน้าอ้างอิง",
|
||||
"Reference Page Count": "จำนวนหน้าอ้างอิง"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "Başlangıç gecikmesi",
|
||||
"Off": "Kapalı",
|
||||
"Look up": "Ara",
|
||||
"Search on Goodreads": "Goodreads'te ara"
|
||||
"Search on Goodreads": "Goodreads'te ara",
|
||||
"Reference Pages": "Referans sayfaları",
|
||||
"Reference Page Count": "Referans sayfa sayısı"
|
||||
}
|
||||
|
||||
@@ -1774,5 +1774,7 @@
|
||||
"Start Delay": "Затримка старту",
|
||||
"Off": "Вимк.",
|
||||
"Look up": "Знайти",
|
||||
"Search on Goodreads": "Шукати в Goodreads"
|
||||
"Search on Goodreads": "Шукати в Goodreads",
|
||||
"Reference Pages": "Сторінки паперової книги",
|
||||
"Reference Page Count": "Кількість сторінок паперової книги"
|
||||
}
|
||||
|
||||
@@ -1708,5 +1708,7 @@
|
||||
"Start Delay": "Boshlash kechikishi",
|
||||
"Off": "Oʻchiq",
|
||||
"Look up": "Qidirish",
|
||||
"Search on Goodreads": "Goodreads'da qidirish"
|
||||
"Search on Goodreads": "Goodreads'da qidirish",
|
||||
"Reference Pages": "Bosma sahifalar",
|
||||
"Reference Page Count": "Bosma sahifalar soni"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "Độ trễ bắt đầu",
|
||||
"Off": "Tắt",
|
||||
"Look up": "Tra cứu",
|
||||
"Search on Goodreads": "Tìm trên Goodreads"
|
||||
"Search on Goodreads": "Tìm trên Goodreads",
|
||||
"Reference Pages": "Trang tham chiếu",
|
||||
"Reference Page Count": "Số trang tham chiếu"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "开始延迟",
|
||||
"Off": "关闭",
|
||||
"Look up": "查询",
|
||||
"Search on Goodreads": "在 Goodreads 中搜索"
|
||||
"Search on Goodreads": "在 Goodreads 中搜索",
|
||||
"Reference Pages": "参考页码",
|
||||
"Reference Page Count": "参考总页数"
|
||||
}
|
||||
|
||||
@@ -1675,5 +1675,7 @@
|
||||
"Start Delay": "開始延遲",
|
||||
"Off": "關閉",
|
||||
"Look up": "查詢",
|
||||
"Search on Goodreads": "在 Goodreads 搜尋"
|
||||
"Search on Goodreads": "在 Goodreads 搜尋",
|
||||
"Reference Pages": "參考頁碼",
|
||||
"Reference Page Count": "參考總頁數"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getReferencePageInfo } from '@/utils/progress';
|
||||
|
||||
const makePageList = (labels: string[]) => labels.map((label) => ({ label, href: '#' }));
|
||||
|
||||
describe('getReferencePageInfo', () => {
|
||||
describe('with a page list from the book (issue #672)', () => {
|
||||
it('uses the current page item label and the highest numeric label as total', () => {
|
||||
const pageList = makePageList(['1', '2', '3', '4', '5']);
|
||||
const info = getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem: { label: '3' },
|
||||
fraction: 0.5,
|
||||
referencePageCount: 0,
|
||||
});
|
||||
expect(info).toEqual({ current: '3', total: 5 });
|
||||
});
|
||||
|
||||
it('keeps non-numeric current labels (front matter roman numerals) as-is', () => {
|
||||
const pageList = makePageList(['i', 'ii', '1', '2', '3']);
|
||||
const info = getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem: { label: 'ii' },
|
||||
fraction: 0.1,
|
||||
referencePageCount: 0,
|
||||
});
|
||||
expect(info).toEqual({ current: 'ii', total: 3 });
|
||||
});
|
||||
|
||||
it('ignores trailing non-numeric labels when computing the total', () => {
|
||||
// Reported in #672: a book ends with an index page labeled "XII" after
|
||||
// page 553 — the total must stay 553, not "XII".
|
||||
const pageList = makePageList(['551', '552', '553', 'XII']);
|
||||
const info = getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem: { label: '552' },
|
||||
fraction: 0.6,
|
||||
referencePageCount: 0,
|
||||
});
|
||||
expect(info).toEqual({ current: '552', total: 553 });
|
||||
});
|
||||
|
||||
it('falls back to the entry count when no label is numeric', () => {
|
||||
const pageList = makePageList(['i', 'ii', 'iii']);
|
||||
const info = getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem: { label: 'iii' },
|
||||
fraction: 0.9,
|
||||
referencePageCount: 0,
|
||||
});
|
||||
expect(info).toEqual({ current: 'iii', total: 3 });
|
||||
});
|
||||
|
||||
it('counts pages in nested subitems', () => {
|
||||
const pageList = [
|
||||
{ label: '1', href: '#', subitems: makePageList(['2', '3']) },
|
||||
{ label: '4', href: '#' },
|
||||
];
|
||||
const info = getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem: { label: '2' },
|
||||
fraction: 0.3,
|
||||
referencePageCount: 0,
|
||||
});
|
||||
expect(info).toEqual({ current: '2', total: 4 });
|
||||
});
|
||||
|
||||
it('estimates the current page linearly when no page item is resolved yet', () => {
|
||||
// e.g. on the cover, before the first page anchor
|
||||
const pageList = makePageList(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']);
|
||||
const info = getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem: null,
|
||||
fraction: 0.45,
|
||||
referencePageCount: 0,
|
||||
});
|
||||
expect(info).toEqual({ current: '5', total: 10 });
|
||||
});
|
||||
|
||||
it('prefers the page list over a user-entered page count', () => {
|
||||
const pageList = makePageList(['1', '2', '3']);
|
||||
const info = getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem: { label: '2' },
|
||||
fraction: 0.5,
|
||||
referencePageCount: 999,
|
||||
});
|
||||
expect(info).toEqual({ current: '2', total: 3 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a user-entered page count (issue #4542)', () => {
|
||||
it('maps the reading fraction linearly onto the page count', () => {
|
||||
const info = getReferencePageInfo({
|
||||
pageList: null,
|
||||
pageItem: null,
|
||||
fraction: 0.5,
|
||||
referencePageCount: 350,
|
||||
});
|
||||
expect(info).toEqual({ current: '175', total: 350 });
|
||||
});
|
||||
|
||||
it('shows page 1 at the start of the book', () => {
|
||||
const info = getReferencePageInfo({
|
||||
pageList: undefined,
|
||||
pageItem: null,
|
||||
fraction: 0,
|
||||
referencePageCount: 350,
|
||||
});
|
||||
expect(info).toEqual({ current: '1', total: 350 });
|
||||
});
|
||||
|
||||
it('shows the last page at the end of the book', () => {
|
||||
const info = getReferencePageInfo({
|
||||
pageList: [],
|
||||
pageItem: null,
|
||||
fraction: 1,
|
||||
referencePageCount: 350,
|
||||
});
|
||||
expect(info).toEqual({ current: '350', total: 350 });
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when the book has no page list and no page count is set', () => {
|
||||
expect(
|
||||
getReferencePageInfo({
|
||||
pageList: null,
|
||||
pageItem: null,
|
||||
fraction: 0.5,
|
||||
referencePageCount: 0,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -149,6 +149,7 @@ const FoliateViewer: React.FC<{
|
||||
bookKey,
|
||||
detail.cfi,
|
||||
detail.tocItem,
|
||||
detail.pageItem,
|
||||
detail.section,
|
||||
pageInfo,
|
||||
detail.time,
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { formatNumber, formatProgress, getReferencePageInfo } from '@/utils/progress';
|
||||
import { saveViewSettings } from '@/helpers/settings';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { SIZE_PER_LOC, SIZE_PER_TIME_UNIT } from '@/services/constants';
|
||||
@@ -51,7 +51,18 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
||||
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 referenceInfo =
|
||||
readingProgressStyle === 'reference'
|
||||
? getReferencePageInfo({
|
||||
pageList: bookData?.bookDoc?.pageList,
|
||||
pageItem: progress?.pageItem,
|
||||
fraction: pageInfo && pageInfo.total > 0 ? (pageInfo.current + 1) / pageInfo.total : 0,
|
||||
referencePageCount: viewSettings.referencePageCount,
|
||||
})
|
||||
: null;
|
||||
const progressInfo = referenceInfo
|
||||
? `${referenceInfo.current}${isVertical ? ' · ' : ' / '}${referenceInfo.total}`
|
||||
: formatProgress(pageInfo?.current, pageInfo?.total, template, localize, lang);
|
||||
|
||||
const { page: current = 0, pages: total = 0 } = view?.renderer || {};
|
||||
const pagesLeft = bookData?.isFixedLayout
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { formatProgress } from '@/utils/progress';
|
||||
import { formatProgress, getReferencePageInfo } from '@/utils/progress';
|
||||
import type { FooterBarChildProps } from './types';
|
||||
import { getNavigationIcon } from './utils';
|
||||
import Button from '@/components/Button';
|
||||
@@ -38,7 +38,18 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
|
||||
const { section, pageinfo } = progress || {};
|
||||
const template = progressStyle === 'fraction' ? '{current} / {total}' : '{percent}%';
|
||||
const pageInfo = bookData?.isFixedLayout ? section : pageinfo;
|
||||
const progressInfo = formatProgress(pageInfo?.current, pageInfo?.total, template, false, 'en', 0);
|
||||
const referenceInfo =
|
||||
progressStyle === 'reference'
|
||||
? getReferencePageInfo({
|
||||
pageList: bookData?.bookDoc?.pageList,
|
||||
pageItem: progress?.pageItem,
|
||||
fraction: progressFraction,
|
||||
referencePageCount: viewSettings?.referencePageCount,
|
||||
})
|
||||
: null;
|
||||
const progressInfo = referenceInfo
|
||||
? `${referenceInfo.current} / ${referenceInfo.total}`
|
||||
: formatProgress(pageInfo?.current, pageInfo?.total, template, false, 'en', 0);
|
||||
|
||||
const rangeInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
);
|
||||
const [tapToToggleFooter, setTapToToggleFooter] = useState(viewSettings.tapToToggleFooter);
|
||||
const [progressStyle, setProgressStyle] = useState(viewSettings.progressStyle);
|
||||
const [referencePageCount, setReferencePageCount] = useState(viewSettings.referencePageCount);
|
||||
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation);
|
||||
|
||||
const resetToDefaults = useResetViewSettings();
|
||||
@@ -390,6 +391,11 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progressStyle]);
|
||||
|
||||
useEffect(() => {
|
||||
saveViewSettings(envConfig, bookKey, 'referencePageCount', referencePageCount, true, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [referencePageCount]);
|
||||
|
||||
useEffect(() => {
|
||||
saveViewSettings(envConfig, bookKey, 'tapToToggleFooter', tapToToggleFooter, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -718,10 +724,22 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
options={[
|
||||
{ value: 'fraction', label: _('Page Number') },
|
||||
{ value: 'percentage', label: _('Percentage') },
|
||||
{ value: 'reference', label: _('Reference Pages') },
|
||||
]}
|
||||
disabled={!showProgressInfo}
|
||||
/>
|
||||
</SettingsRow>
|
||||
{progressStyle === 'reference' && !bookData?.bookDoc?.pageList?.length && (
|
||||
<NumberInput
|
||||
label={_('Reference Page Count')}
|
||||
value={referencePageCount}
|
||||
onChange={setReferencePageCount}
|
||||
disabled={!showProgressInfo}
|
||||
min={0}
|
||||
max={10000}
|
||||
data-setting-id='settings.layout.referencePageCount'
|
||||
/>
|
||||
)}
|
||||
<SettingsSwitchRow
|
||||
label={_('Show Current Time')}
|
||||
checked={showCurrentTime}
|
||||
|
||||
@@ -84,6 +84,7 @@ export interface BookDoc {
|
||||
};
|
||||
dir: string;
|
||||
toc?: Array<TOCItem>;
|
||||
pageList?: Array<TOCItem>;
|
||||
sections: Array<SectionItem>;
|
||||
transformTarget?: EventTarget;
|
||||
splitTOCHref(href: string): Array<string | number>;
|
||||
|
||||
@@ -339,6 +339,7 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = {
|
||||
tapToToggleFooter: false,
|
||||
showPaginationButtons: false,
|
||||
progressStyle: 'fraction',
|
||||
referencePageCount: 0,
|
||||
progressInfoMode: 'all',
|
||||
|
||||
animated: false,
|
||||
|
||||
@@ -68,6 +68,7 @@ interface ReaderStore {
|
||||
key: string,
|
||||
location: string,
|
||||
tocItem: TOCItem,
|
||||
pageItem: BookProgress['pageItem'],
|
||||
section: PageInfo,
|
||||
pageinfo: PageInfo,
|
||||
timeinfo: TimeInfo,
|
||||
@@ -361,6 +362,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
key: string,
|
||||
location: string,
|
||||
tocItem: TOCItem,
|
||||
pageItem: BookProgress['pageItem'],
|
||||
section: PageInfo,
|
||||
pageinfo: PageInfo,
|
||||
timeinfo: TimeInfo,
|
||||
@@ -417,6 +419,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
location,
|
||||
sectionHref: tocItem?.href,
|
||||
sectionLabel: tocItem?.label,
|
||||
pageItem,
|
||||
section,
|
||||
pageinfo,
|
||||
timeinfo,
|
||||
|
||||
@@ -273,7 +273,8 @@ export interface ViewConfig {
|
||||
showBatteryPercentage: boolean;
|
||||
tapToToggleFooter: boolean;
|
||||
showPaginationButtons: boolean;
|
||||
progressStyle: 'percentage' | 'fraction';
|
||||
progressStyle: 'percentage' | 'fraction' | 'reference';
|
||||
referencePageCount: number;
|
||||
progressInfoMode: ProgressBarMode;
|
||||
|
||||
animated: boolean;
|
||||
@@ -378,6 +379,7 @@ export interface BookProgress {
|
||||
sectionLabel: string;
|
||||
section: PageInfo;
|
||||
pageinfo: PageInfo;
|
||||
pageItem?: { label?: string; href?: string } | null;
|
||||
timeinfo: TimeInfo;
|
||||
index: number;
|
||||
range: Range;
|
||||
|
||||
@@ -25,6 +25,60 @@ export function formatProgress(
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReferencePageItem {
|
||||
label?: string;
|
||||
subitems?: ReferencePageItem[] | null;
|
||||
}
|
||||
|
||||
export interface ReferencePageInfo {
|
||||
current: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const collectLabels = (items: ReferencePageItem[]): string[] =>
|
||||
items.flatMap((item) => [
|
||||
item.label?.trim() ?? '',
|
||||
...(item.subitems?.length ? collectLabels(item.subitems) : []),
|
||||
]);
|
||||
|
||||
const estimatePage = (fraction: number, total: number) =>
|
||||
Math.min(total, Math.max(1, Math.ceil(fraction * total)));
|
||||
|
||||
/**
|
||||
* Resolve the physical-book page info shown by the 'reference' progress style.
|
||||
* Prefers the book's own page list (EPUB page-list nav / NCX pageList); when
|
||||
* the book has none, maps the reading fraction linearly onto a user-entered
|
||||
* page count. Returns null when neither source is available.
|
||||
*/
|
||||
export function getReferencePageInfo({
|
||||
pageList,
|
||||
pageItem,
|
||||
fraction,
|
||||
referencePageCount,
|
||||
}: {
|
||||
pageList?: ReferencePageItem[] | null;
|
||||
pageItem?: ReferencePageItem | null;
|
||||
fraction: number;
|
||||
referencePageCount?: number;
|
||||
}): ReferencePageInfo | null {
|
||||
if (pageList?.length) {
|
||||
const labels = collectLabels(pageList).filter(Boolean);
|
||||
// The last entries may be non-numeric (e.g. a roman-numeral index page),
|
||||
// so the total is the highest numeric label, not the last one.
|
||||
const numericLabels = labels.filter((label) => /^\d+$/.test(label));
|
||||
const total = numericLabels.length ? Math.max(...numericLabels.map(Number)) : labels.length;
|
||||
const current = pageItem?.label?.trim() || String(estimatePage(fraction, total));
|
||||
return { current, total };
|
||||
}
|
||||
if (referencePageCount && referencePageCount > 0) {
|
||||
return {
|
||||
current: String(estimatePage(fraction, referencePageCount)),
|
||||
total: referencePageCount,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatNumber(
|
||||
number: number | undefined,
|
||||
localize: boolean = false,
|
||||
|
||||
Reference in New Issue
Block a user