feat: whole book page count

This commit is contained in:
Uri Tauber
2026-07-15 17:58:45 +03:00
parent f180069643
commit fd00c185f2
13 changed files with 337 additions and 51 deletions
+106 -3
View File
@@ -44,6 +44,22 @@ constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12};
constexpr size_t initialBookmarkCacheCapacity = 16;
constexpr float bookmarkProgressEpsilon = 0.0001f;
// The render parameters the reader paginates with, as passed to loadSectionFile /
// createSectionFile / startBuild below. Used to validate other sections' cached
// page counts and to detect when a settings change invalidates the harvest.
Section::RenderParams readerRenderParams(const uint16_t viewportWidth, const uint16_t viewportHeight) {
return {SETTINGS.getReaderFontId(),
SETTINGS.getReaderLineCompression(),
static_cast<bool>(SETTINGS.extraParagraphSpacing),
SETTINGS.paragraphAlignment,
viewportWidth,
viewportHeight,
static_cast<bool>(SETTINGS.hyphenationEnabled),
static_cast<bool>(SETTINGS.embeddedStyle),
SETTINGS.imageRendering,
static_cast<bool>(SETTINGS.focusReadingEnabled)};
}
int clampPercent(int percent) {
if (percent < 0) {
return 0;
@@ -319,6 +335,25 @@ void EpubReaderActivity::loop() {
}
}
// Whole-book page counter: harvest one other section's cached page count per tick
// (a header-only peek), so the total converges to exact without visiting every
// chapter. Idle-priority — skipped whenever a render is pending or a build is
// running. No re-render is requested; the counter refreshes on the next page turn.
if (bookPages && bookPagesSweepIndex < epub->getSpineItemsCount() && !RenderLock::peek() &&
!(section && section->isBuilding())) {
RenderLock lock;
// Re-check under the lock: render() may have just reset/reallocated the table.
if (bookPages && bookPagesSweepIndex < epub->getSpineItemsCount()) {
const int index = bookPagesSweepIndex++;
if (bookPages[index].pages < 0) {
const Section peekSection(epub, index, renderer);
if (const auto count = peekSection.getCachedPageCount(&bookPagesParams)) {
bookPages[index].pages = *count;
}
}
}
}
// End-of-Book screen reached (currentSpineIndex == spine count) means the book is
// finished. Two independent finished-book features key off this same condition.
const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount();
@@ -980,6 +1015,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
buildViewportWidth = viewportWidth;
buildViewportHeight = viewportHeight;
ensureBookPages(viewportWidth, viewportHeight);
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
@@ -1222,6 +1259,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
applyDeferredReposition();
recordCurrentSectionPages();
renderer.clearScreen();
if (section->pageCount == 0) {
@@ -1331,6 +1370,59 @@ bool EpubReaderActivity::applyDeferredReposition() {
return changed;
}
void EpubReaderActivity::ensureBookPages(const uint16_t viewportWidth, const uint16_t viewportHeight) {
if (SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::BOOK_PAGE_COUNT) {
bookPages.reset();
return;
}
const Section::RenderParams params = readerRenderParams(viewportWidth, viewportHeight);
if (bookPages && params == bookPagesParams) {
return;
}
// First render, or a render-params change (font/orientation/...): the harvested
// counts are for the old pagination, so start over. The section caches themselves
// are the persistence; there is nothing else to invalidate.
bookPagesParams = params;
bookPagesSweepIndex = 0;
bookPages.reset();
const int sectionCount = epub->getSpineItemsCount();
if (sectionCount <= 0) {
return;
}
// 8 bytes per spine item, held for the reading session; freed with the activity
// or when the feature is switched off. On OOM stays null (chapter-local counts).
bookPages = makeUniqueNoThrow<BookPageEntry[]>(sectionCount);
if (!bookPages) {
LOG_ERR("ERS", "OOM: book page table (%d sections)", sectionCount);
return;
}
size_t prev = 0;
for (int i = 0; i < sectionCount; ++i) {
const size_t cum = epub->getCumulativeSpineItemSize(i);
bookPages[i].bytes = static_cast<uint32_t>(cum >= prev ? cum - prev : 0);
prev = cum;
}
}
void EpubReaderActivity::recordCurrentSectionPages() {
// Only a finalized section's pageCount is the chapter total; a building or
// partial section's is just its current watermark.
if (!bookPages || !section || section->isBuilding() || section->isPartial()) {
return;
}
if (currentSpineIndex >= 0 && currentSpineIndex < epub->getSpineItemsCount()) {
bookPages[currentSpineIndex].pages = section->pageCount;
}
}
std::optional<BookPagePosition> EpubReaderActivity::bookPagePosition() const {
if (!bookPages || !section) {
return std::nullopt;
}
return computeBookPagePosition(bookPages.get(), epub->getSpineItemsCount(), currentSpineIndex, section->currentPage,
section->estimatedTotalPages());
}
bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount);
}
@@ -1512,10 +1604,21 @@ void EpubReaderActivity::renderStatusBar() const {
// Calculate progress in book. Use the estimated total while a giant spine is still building so
// "page X of Y" and the progress bar don't read off the small build watermark.
const int currentPage = section->currentPage + 1;
const float pageCount = section->estimatedTotalPages();
const int pageCount = section->estimatedTotalPages();
const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
// Page counter values: whole-book numbers when Page Count is set to Book (and the
// table is alive), otherwise chapter-local. The progress bar stays on bookProgress.
int counterPage = currentPage;
int counterTotal = pageCount;
bool counterIsEstimate = section->isBuilding();
if (const auto book = bookPagePosition()) {
counterPage = book->currentPage;
counterTotal = book->totalPages;
counterIsEstimate = book->isEstimate;
}
std::string title;
int textYOffset = 0;
@@ -1543,8 +1646,8 @@ void EpubReaderActivity::renderStatusBar() const {
title = epub->getTitle();
}
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked,
section->isBuilding());
GUI.drawStatusBar(renderer, bookProgress, counterPage, counterTotal, title, 0, textYOffset, true,
currentPageBookmarked, counterIsEstimate);
}
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
@@ -1,5 +1,6 @@
#pragma once
#include <Epub.h>
#include <Epub/BookPages.h>
#include <Epub/FootnoteEntry.h>
#include <Epub/Section.h>
@@ -23,6 +24,17 @@ class EpubReaderActivity final : public Activity {
int pagesUntilFullRefresh = 0;
int cachedSpineIndex = 0;
int cachedChapterTotalPageCount = 0;
// Whole-book page accounting ("page X of Y" across the whole book), active only
// when the status bar Page Count is set to Book. Exact counts are harvested from
// finalized section caches — nothing extra is persisted (see BookPages.h). Null
// while inactive or on OOM; every book-page path degrades to chapter-local counts.
// Shared between the render task and loop(): only touch under the RenderLock.
std::unique_ptr<BookPageEntry[]> bookPages;
// Render params the harvested counts are valid for; a change resets the harvest.
Section::RenderParams bookPagesParams;
// Next spine index for loop()'s background sweep that peeks other sections'
// cached counts (one per tick); >= spine count once the sweep is done.
int bookPagesSweepIndex = 0;
unsigned long lastPageTurnTime = 0UL;
unsigned long pageTurnDuration = 0UL;
// Signals that the next render should reposition within the newly loaded section
@@ -113,6 +125,14 @@ class EpubReaderActivity final : public Activity {
// (used after a settings change re-paginates a chapter). Returns true if currentPage moved.
// No-op while the section is still building or when the pagination is unchanged (plain resume).
bool applyDeferredReposition();
// (Re)allocate and reset bookPages when the feature turns on or the render params
// change. Called from render() (under the RenderLock) where the viewport is known.
void ensureBookPages(uint16_t viewportWidth, uint16_t viewportHeight);
// Store the current section's exact page count once its pagination is final
// (no-op while building or partial). Caller must hold the RenderLock.
void recordCurrentSectionPages();
// Book-global position for the current page, or nullopt while inactive.
std::optional<BookPagePosition> bookPagePosition() const;
bool saveProgress(int spineIndex, int currentPage, int pageCount);
// Jump to a percentage of the book (0-100), mapping it to spine and page.
void jumpToPercent(int percent);
@@ -64,6 +64,10 @@ std::string formatUtcOffset(uint8_t biasedQ) {
snprintf(buf, sizeof(buf), "UTC%c%d:%02d", neg ? '-' : '+', hours, mins);
return buf;
}
// Order follows STATUS_BAR_PAGE_COUNT (hide=0, chapter=1, book=2).
constexpr int PAGE_COUNT_ITEMS = 3;
const StrId pageCountNames[PAGE_COUNT_ITEMS] = {StrId::STR_HIDE, StrId::STR_CHAPTER, StrId::STR_BOOK};
constexpr int PROGRESS_BAR_ITEMS = 3;
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
@@ -90,7 +94,11 @@ void StatusBarSettingsActivity::onEnter() {
selectedIndex = 0;
visibleItemCount = halClock.isAvailable() ? FULL_MENU_ITEMS : BASE_MENU_ITEMS;
// Clamp statusBarProgressBar and statusBarTitle in case of corrupt/migrated data
// Clamp enum-valued settings in case of corrupt/migrated data
if (SETTINGS.statusBarPageCount >= PAGE_COUNT_ITEMS) {
SETTINGS.statusBarPageCount = CrossPointSettings::STATUS_BAR_PAGE_COUNT::CHAPTER_PAGE_COUNT;
}
if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) {
SETTINGS.statusBarProgressBar = CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
}
@@ -163,8 +171,12 @@ void StatusBarSettingsActivity::loop() {
void StatusBarSettingsActivity::handleSelection() {
switch (selectedIndex) {
case ITEM_CHAPTER_PAGE_COUNT:
SETTINGS.statusBarChapterPageCount = (SETTINGS.statusBarChapterPageCount + 1) % 2;
break;
optionPopup.show(StrId::STR_CHAPTER_PAGE_COUNT, pageCountNames, PAGE_COUNT_ITEMS, SETTINGS.statusBarPageCount,
[this](int idx) {
SETTINGS.statusBarPageCount = idx;
SETTINGS.saveToFile();
});
return;
case ITEM_BOOK_PROGRESS_PERCENTAGE:
SETTINGS.statusBarBookProgressPercentage = (SETTINGS.statusBarBookProgressPercentage + 1) % 2;
break;
@@ -236,7 +248,7 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
[](int index) -> std::string {
switch (index) {
case ITEM_CHAPTER_PAGE_COUNT:
return SETTINGS.statusBarChapterPageCount ? tr(STR_SHOW) : tr(STR_HIDE);
return I18N.get(pageCountNames[SETTINGS.statusBarPageCount]);
case ITEM_BOOK_PROGRESS_PERCENTAGE:
return SETTINGS.statusBarBookProgressPercentage ? tr(STR_SHOW) : tr(STR_HIDE);
case ITEM_PROGRESS_BAR: