From 9dc41e7adff78260063da8480cf53f2fd38674c1 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Fri, 12 Jun 2026 14:44:18 +0800 Subject: [PATCH] feat(reader): reference page numbers from EPUB page-list with manual page count fallback (#4549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../public/locales/ar/translation.json | 4 +- .../public/locales/bn/translation.json | 4 +- .../public/locales/bo/translation.json | 4 +- .../public/locales/de/translation.json | 4 +- .../public/locales/el/translation.json | 4 +- .../public/locales/es/translation.json | 4 +- .../public/locales/fa/translation.json | 4 +- .../public/locales/fr/translation.json | 4 +- .../public/locales/he/translation.json | 4 +- .../public/locales/hi/translation.json | 4 +- .../public/locales/hu/translation.json | 4 +- .../public/locales/id/translation.json | 4 +- .../public/locales/it/translation.json | 4 +- .../public/locales/ja/translation.json | 4 +- .../public/locales/ko/translation.json | 4 +- .../public/locales/ms/translation.json | 4 +- .../public/locales/nl/translation.json | 4 +- .../public/locales/pl/translation.json | 4 +- .../public/locales/pt-BR/translation.json | 4 +- .../public/locales/pt/translation.json | 4 +- .../public/locales/ro/translation.json | 4 +- .../public/locales/ru/translation.json | 4 +- .../public/locales/si/translation.json | 4 +- .../public/locales/sl/translation.json | 4 +- .../public/locales/sv/translation.json | 4 +- .../public/locales/ta/translation.json | 4 +- .../public/locales/th/translation.json | 4 +- .../public/locales/tr/translation.json | 4 +- .../public/locales/uk/translation.json | 4 +- .../public/locales/uz/translation.json | 4 +- .../public/locales/vi/translation.json | 4 +- .../public/locales/zh-CN/translation.json | 4 +- .../public/locales/zh-TW/translation.json | 4 +- .../__tests__/utils/reference-pages.test.ts | 134 ++++++++++++++++++ .../app/reader/components/FoliateViewer.tsx | 1 + .../src/app/reader/components/ProgressBar.tsx | 15 +- .../components/footerbar/DesktopFooterBar.tsx | 15 +- .../src/components/settings/LayoutPanel.tsx | 18 +++ apps/readest-app/src/libs/document.ts | 1 + apps/readest-app/src/services/constants.ts | 1 + apps/readest-app/src/store/readerStore.ts | 3 + apps/readest-app/src/types/book.ts | 4 +- apps/readest-app/src/utils/progress.ts | 54 +++++++ 43 files changed, 340 insertions(+), 38 deletions(-) create mode 100644 apps/readest-app/src/__tests__/utils/reference-pages.test.ts diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index e6f3a4b2..608fe2c9 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1840,5 +1840,7 @@ "Start Delay": "تأخير البدء", "Off": "إيقاف", "Look up": "بحث", - "Search on Goodreads": "البحث في Goodreads" + "Search on Goodreads": "البحث في Goodreads", + "Reference Pages": "صفحات مرجعية", + "Reference Page Count": "عدد الصفحات المرجعية" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 218ab958..9340e624 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1708,5 +1708,7 @@ "Start Delay": "শুরুর বিলম্ব", "Off": "বন্ধ", "Look up": "খুঁজুন", - "Search on Goodreads": "Goodreads-এ অনুসন্ধান করুন" + "Search on Goodreads": "Goodreads-এ অনুসন্ধান করুন", + "Reference Pages": "রেফারেন্স পৃষ্ঠা", + "Reference Page Count": "রেফারেন্স পৃষ্ঠার সংখ্যা" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 826586b5..808a2eca 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1675,5 +1675,7 @@ "Start Delay": "འགོ་འཛུགས་ཕྱིར་འགྱངས།", "Off": "ཁ་རྒྱག", "Look up": "འཚོལ་བ།", - "Search on Goodreads": "Goodreads ནང་འཚོལ།" + "Search on Goodreads": "Goodreads ནང་འཚོལ།", + "Reference Pages": "དཔེ་སྟོན་ཤོག་ངོས།", + "Reference Page Count": "དཔེ་སྟོན་ཤོག་ངོས་ཀྱི་གྲངས།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 2a074691..6723747c 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index d9816ce5..502a2c10 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1708,5 +1708,7 @@ "Start Delay": "Καθυστέρηση έναρξης", "Off": "Ανενεργό", "Look up": "Αναζήτηση", - "Search on Goodreads": "Αναζήτηση στο Goodreads" + "Search on Goodreads": "Αναζήτηση στο Goodreads", + "Reference Pages": "Σελίδες αναφοράς", + "Reference Page Count": "Αριθμός σελίδων αναφοράς" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 23d279ac..3143152c 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 2dc03215..5226e138 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1708,5 +1708,7 @@ "Start Delay": "تأخیر شروع", "Off": "خاموش", "Look up": "جستجو", - "Search on Goodreads": "جستجو در Goodreads" + "Search on Goodreads": "جستجو در Goodreads", + "Reference Pages": "صفحات مرجع", + "Reference Page Count": "تعداد صفحات مرجع" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 80d57bab..14d66a79 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 8ab25085..c304ecce 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1741,5 +1741,7 @@ "Start Delay": "השהיית התחלה", "Off": "כבוי", "Look up": "חיפוש", - "Search on Goodreads": "חיפוש ב-Goodreads" + "Search on Goodreads": "חיפוש ב-Goodreads", + "Reference Pages": "עמודי ייחוס", + "Reference Page Count": "מספר עמודי ייחוס" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index c410724a..2fedb1fe 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1708,5 +1708,7 @@ "Start Delay": "आरंभ विलंब", "Off": "बंद", "Look up": "खोजें", - "Search on Goodreads": "Goodreads पर खोजें" + "Search on Goodreads": "Goodreads पर खोजें", + "Reference Pages": "संदर्भ पृष्ठ", + "Reference Page Count": "संदर्भ पृष्ठ संख्या" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index c3a0480f..8bb5052b 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 0eaa1683..ec9708d1 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index ba23e8c2..6a6e6262 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 0736d487..c32e9d40 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1675,5 +1675,7 @@ "Start Delay": "開始遅延", "Off": "オフ", "Look up": "調べる", - "Search on Goodreads": "Goodreadsで検索" + "Search on Goodreads": "Goodreadsで検索", + "Reference Pages": "参照ページ", + "Reference Page Count": "参照ページ数" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 4b28ef5c..b5916812 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1675,5 +1675,7 @@ "Start Delay": "시작 지연", "Off": "끄기", "Look up": "찾기", - "Search on Goodreads": "Goodreads에서 검색" + "Search on Goodreads": "Goodreads에서 검색", + "Reference Pages": "참조 페이지", + "Reference Page Count": "참조 페이지 수" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index aa698879..59ca9982 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 74a17fe3..633d868d 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 633f69d6..31e52718 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index 5686f0d6..381e295b 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index fd452a78..484e329b 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index ccb0231d..8c066c00 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -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ță" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 56ed7c0f..38130e19 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1774,5 +1774,7 @@ "Start Delay": "Задержка старта", "Off": "Выкл.", "Look up": "Найти", - "Search on Goodreads": "Искать в Goodreads" + "Search on Goodreads": "Искать в Goodreads", + "Reference Pages": "Страницы бумажной книги", + "Reference Page Count": "Число страниц бумажной книги" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index f01ee7af..091e22b9 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1708,5 +1708,7 @@ "Start Delay": "ආරම්භක ප්‍රමාදය", "Off": "අක්‍රිය", "Look up": "සොයන්න", - "Search on Goodreads": "Goodreads හි සොයන්න" + "Search on Goodreads": "Goodreads හි සොයන්න", + "Reference Pages": "යොමු පිටු", + "Reference Page Count": "යොමු පිටු ගණන" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 44c86c11..73f483f5 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 9b4921b7..7b62fd9c 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index a232099e..89c56f27 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1708,5 +1708,7 @@ "Start Delay": "தொடக்க தாமதம்", "Off": "அணை", "Look up": "தேடு", - "Search on Goodreads": "Goodreads இல் தேடு" + "Search on Goodreads": "Goodreads இல் தேடு", + "Reference Pages": "குறிப்புப் பக்கங்கள்", + "Reference Page Count": "குறிப்புப் பக்கங்களின் எண்ணிக்கை" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index d14574c1..92eb2b1a 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1675,5 +1675,7 @@ "Start Delay": "หน่วงเวลาเริ่มต้น", "Off": "ปิด", "Look up": "ค้นหา", - "Search on Goodreads": "ค้นหาใน Goodreads" + "Search on Goodreads": "ค้นหาใน Goodreads", + "Reference Pages": "หน้าอ้างอิง", + "Reference Page Count": "จำนวนหน้าอ้างอิง" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 8abd8644..a7b24d3a 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -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ı" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index ffa9844e..016cf0f1 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1774,5 +1774,7 @@ "Start Delay": "Затримка старту", "Off": "Вимк.", "Look up": "Знайти", - "Search on Goodreads": "Шукати в Goodreads" + "Search on Goodreads": "Шукати в Goodreads", + "Reference Pages": "Сторінки паперової книги", + "Reference Page Count": "Кількість сторінок паперової книги" } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index b6fd69f3..0fc6bb42 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 11f44a35..ea041dc2 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 7986b176..b62bac50 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1675,5 +1675,7 @@ "Start Delay": "开始延迟", "Off": "关闭", "Look up": "查询", - "Search on Goodreads": "在 Goodreads 中搜索" + "Search on Goodreads": "在 Goodreads 中搜索", + "Reference Pages": "参考页码", + "Reference Page Count": "参考总页数" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 19da7abd..ed8d1ba5 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1675,5 +1675,7 @@ "Start Delay": "開始延遲", "Off": "關閉", "Look up": "查詢", - "Search on Goodreads": "在 Goodreads 搜尋" + "Search on Goodreads": "在 Goodreads 搜尋", + "Reference Pages": "參考頁碼", + "Reference Page Count": "參考總頁數" } diff --git a/apps/readest-app/src/__tests__/utils/reference-pages.test.ts b/apps/readest-app/src/__tests__/utils/reference-pages.test.ts new file mode 100644 index 00000000..39bc80eb --- /dev/null +++ b/apps/readest-app/src/__tests__/utils/reference-pages.test.ts @@ -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(); + }); +}); diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index 47dc5e0e..5ecbc047 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -149,6 +149,7 @@ const FoliateViewer: React.FC<{ bookKey, detail.cfi, detail.tocItem, + detail.pageItem, detail.section, pageInfo, detail.time, diff --git a/apps/readest-app/src/app/reader/components/ProgressBar.tsx b/apps/readest-app/src/app/reader/components/ProgressBar.tsx index 0c1d2467..04b74089 100644 --- a/apps/readest-app/src/app/reader/components/ProgressBar.tsx +++ b/apps/readest-app/src/app/reader/components/ProgressBar.tsx @@ -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 = ({ 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 diff --git a/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx b/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx index 07f439c7..e82ec363 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx @@ -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 = ({ 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(null); diff --git a/apps/readest-app/src/components/settings/LayoutPanel.tsx b/apps/readest-app/src/components/settings/LayoutPanel.tsx index a28c0890..26c52a59 100644 --- a/apps/readest-app/src/components/settings/LayoutPanel.tsx +++ b/apps/readest-app/src/components/settings/LayoutPanel.tsx @@ -85,6 +85,7 @@ const LayoutPanel: React.FC = ({ 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 = ({ 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 = ({ bookKey, onRegisterRese options={[ { value: 'fraction', label: _('Page Number') }, { value: 'percentage', label: _('Percentage') }, + { value: 'reference', label: _('Reference Pages') }, ]} disabled={!showProgressInfo} /> + {progressStyle === 'reference' && !bookData?.bookDoc?.pageList?.length && ( + + )} ; + pageList?: Array; sections: Array; transformTarget?: EventTarget; splitTOCHref(href: string): Array; diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index a6c07441..22c59235 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -339,6 +339,7 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = { tapToToggleFooter: false, showPaginationButtons: false, progressStyle: 'fraction', + referencePageCount: 0, progressInfoMode: 'all', animated: false, diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index b4a76973..4bcae2c1 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -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((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((set, get) => ({ location, sectionHref: tocItem?.href, sectionLabel: tocItem?.label, + pageItem, section, pageinfo, timeinfo, diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index c9f55ab9..b127d8c1 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -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; diff --git a/apps/readest-app/src/utils/progress.ts b/apps/readest-app/src/utils/progress.ts index b70649eb..d403e567 100644 --- a/apps/readest-app/src/utils/progress.ts +++ b/apps/readest-app/src/utils/progress.ts @@ -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,