From fd00c185f296e3c4ae0f56fadc661757199a53fc Mon Sep 17 00:00:00 2001 From: Uri Tauber Date: Wed, 15 Jul 2026 17:58:45 +0300 Subject: [PATCH] feat: whole book page count --- lib/Epub/Epub/BookPages.cpp | 73 ++++++++++++ lib/Epub/Epub/BookPages.h | 30 +++++ lib/Epub/Epub/Section.cpp | 64 +++++----- lib/Epub/Epub/Section.h | 22 +++- lib/I18n/translations/english.yaml | 2 +- src/CrossPointSettings.h | 11 +- src/JsonSettingsIO.cpp | 12 +- src/SettingsList.h | 6 +- src/activities/reader/EpubReaderActivity.cpp | 109 +++++++++++++++++- src/activities/reader/EpubReaderActivity.h | 20 ++++ .../settings/StatusBarSettingsActivity.cpp | 20 +++- src/components/UITheme.cpp | 3 +- src/components/themes/BaseTheme.cpp | 16 ++- 13 files changed, 337 insertions(+), 51 deletions(-) create mode 100644 lib/Epub/Epub/BookPages.cpp create mode 100644 lib/Epub/Epub/BookPages.h diff --git a/lib/Epub/Epub/BookPages.cpp b/lib/Epub/Epub/BookPages.cpp new file mode 100644 index 00000000..459ba9dc --- /dev/null +++ b/lib/Epub/Epub/BookPages.cpp @@ -0,0 +1,73 @@ +#include "BookPages.h" + +#include +#include +#include + +namespace { +// Coarse seed used only before any section has an exact or live count; replaced +// by the calibrated average as soon as one is available. +constexpr double DEFAULT_BYTES_PER_PAGE = 2000.0; +// A section's serialized pageCount is a uint16_t, so no estimate needs to exceed this. +constexpr int MAX_SECTION_PAGES = std::numeric_limits::max(); + +int clampToInt(const uint64_t v) { + return v > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() + : static_cast(v); +} +} // namespace + +BookPagePosition computeBookPagePosition(const BookPageEntry* entries, const int sectionCount, const int spineIndex, + const int pageInSection, const int liveSectionPages) { + BookPagePosition pos; + if (!entries || sectionCount <= 0) { + return pos; + } + + // Calibrate bytes-per-page from every section with an exact count (empty + // known-0 chapters excluded), plus the live estimate for spineIndex when it + // has no exact count yet. + uint64_t knownBytes = 0; + uint64_t knownPages = 0; + for (int i = 0; i < sectionCount; ++i) { + if (entries[i].pages > 0) { + knownBytes += entries[i].bytes; + knownPages += static_cast(entries[i].pages); + } + } + const bool useLive = spineIndex >= 0 && spineIndex < sectionCount && entries[spineIndex].pages < 0 && + liveSectionPages > 0 && entries[spineIndex].bytes > 0; + if (useLive) { + knownBytes += entries[spineIndex].bytes; + knownPages += static_cast(liveSectionPages); + } + const double bytesPerPage = (knownBytes > 0 && knownPages > 0) + ? static_cast(knownBytes) / static_cast(knownPages) + : DEFAULT_BYTES_PER_PAGE; + + uint64_t total = 0; + uint64_t before = 0; + bool exact = true; + for (int i = 0; i < sectionCount; ++i) { + uint32_t pages; + if (entries[i].pages >= 0) { + pages = static_cast(entries[i].pages); // exact (0 = genuinely empty chapter) + } else if (i == spineIndex && liveSectionPages > 0) { + pages = static_cast(std::min(liveSectionPages, MAX_SECTION_PAGES)); + exact = false; + } else { + const double raw = std::floor(static_cast(entries[i].bytes) / bytesPerPage + 0.5); + pages = static_cast(std::clamp(raw, 1.0, static_cast(MAX_SECTION_PAGES))); + exact = false; + } + total += pages; + if (i < spineIndex) { + before += pages; + } + } + + pos.totalPages = clampToInt(total); + pos.currentPage = clampToInt(before + static_cast(std::max(0, pageInSection)) + 1); + pos.isEstimate = !exact; + return pos; +} diff --git a/lib/Epub/Epub/BookPages.h b/lib/Epub/Epub/BookPages.h new file mode 100644 index 00000000..313d6bc7 --- /dev/null +++ b/lib/Epub/Epub/BookPages.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +// Whole-book page accounting for book-global "page X of Y". +// +// The finalized section cache files (sections/*.bin) are the single source of +// truth for exact per-section counts; the caller harvests them into an array of +// BookPageEntry (nothing is persisted separately). Sections without an exact +// count are estimated from their byte size, calibrated against the sections +// whose counts are known — so the total gets more accurate as more of the book +// is paginated, and becomes exact once every section is. + +struct BookPageEntry { + uint32_t bytes = 0; // uncompressed XHTML size, for estimating unknown sections + int32_t pages = -1; // exact count from a finalized section cache; -1 = unknown (0 = empty chapter) +}; + +struct BookPagePosition { + int currentPage = 0; + int totalPages = 0; + bool isEstimate = true; // true until every section has an exact count +}; + +// Book-global position: currentPage = pages before spineIndex + pageInSection + 1. +// liveSectionPages is the in-progress build's estimate for spineIndex (see +// Section::estimatedTotalPages); it is used for that section when it has no exact +// count yet and folded into the bytes-per-page calibration. Pure function: no I/O. +BookPagePosition computeBookPagePosition(const BookPageEntry* entries, int sectionCount, int spineIndex, + int pageInSection, int liveSectionPages); diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 56daeed0..dee2d817 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -39,8 +39,34 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t); + +// Read the render-parameter block of the header (everything between the version +// byte and pageCount), in writeSectionFileHeader order. The file cursor must sit +// just past the version byte. +Section::RenderParams readHeaderRenderParams(HalFile& f) { + Section::RenderParams p; + serialization::readPod(f, p.fontId); + serialization::readPod(f, p.lineCompression); + serialization::readPod(f, p.extraParagraphSpacing); + serialization::readPod(f, p.paragraphAlignment); + serialization::readPod(f, p.viewportWidth); + serialization::readPod(f, p.viewportHeight); + serialization::readPod(f, p.hyphenationEnabled); + serialization::readPod(f, p.embeddedStyle); + serialization::readPod(f, p.imageRendering); + serialization::readPod(f, p.focusReadingEnabled); + return p; +} } // namespace +bool Section::RenderParams::operator==(const RenderParams& o) const { + return fontId == o.fontId && lineCompression == o.lineCompression && + extraParagraphSpacing == o.extraParagraphSpacing && paragraphAlignment == o.paragraphAlignment && + viewportWidth == o.viewportWidth && viewportHeight == o.viewportHeight && + hyphenationEnabled == o.hyphenationEnabled && embeddedStyle == o.embeddedStyle && + imageRendering == o.imageRendering && focusReadingEnabled == o.focusReadingEnabled; +} + // Out-of-line so the unique_ptr in BuildContext can be // constructed/destroyed where the parser's full definition is visible. Section::Section(const std::shared_ptr& epub, const int spineIndex, GfxRenderer& renderer) @@ -133,31 +159,11 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con } filePartial = (version == SECTION_FILE_PARTIAL_VERSION); - int fileFontId; - uint16_t fileViewportWidth, fileViewportHeight; - float fileLineCompression; - bool fileExtraParagraphSpacing; - uint8_t fileParagraphAlignment; - bool fileHyphenationEnabled; - bool fileEmbeddedStyle; - uint8_t fileImageRendering; - bool fileFocusReadingEnabled; - serialization::readPod(file, fileFontId); - serialization::readPod(file, fileLineCompression); - serialization::readPod(file, fileExtraParagraphSpacing); - serialization::readPod(file, fileParagraphAlignment); - serialization::readPod(file, fileViewportWidth); - serialization::readPod(file, fileViewportHeight); - serialization::readPod(file, fileHyphenationEnabled); - serialization::readPod(file, fileEmbeddedStyle); - serialization::readPod(file, fileImageRendering); - serialization::readPod(file, fileFocusReadingEnabled); - - if (fontId != fileFontId || lineCompression != fileLineCompression || - extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || - viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || - hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle || - imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) { + const RenderParams fileParams = readHeaderRenderParams(file); + const RenderParams params = {fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, + viewportWidth, viewportHeight, hyphenationEnabled, embeddedStyle, + imageRendering, focusReadingEnabled}; + if (!(fileParams == params)) { file.close(); LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); clearCache(); @@ -754,7 +760,7 @@ std::string Section::getTextFromSectionFile() { return fullText; } -std::optional Section::getCachedPageCount() const { +std::optional Section::getCachedPageCount(const RenderParams* mustMatch) const { HalFile f; if (!Storage.openFileForRead("SCT", filePath, f)) { return std::nullopt; @@ -774,6 +780,12 @@ std::optional Section::getCachedPageCount() const { return std::nullopt; } + // A file cached under other render settings paginates differently; its count is + // only stale-valid for rough mapping, so reject it when the caller needs a match. + if (mustMatch && !(readHeaderRenderParams(f) == *mustMatch)) { + return std::nullopt; + } + f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t)); uint16_t count; serialization::readPod(f, count); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index d90b4558..924fef2a 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -78,6 +78,23 @@ class Section { std::unique_ptr loadPageDuringBuild(int page); public: + // The render parameters that determine pagination, in section cache header order + // (see writeSectionFileHeader). A cached page count is only valid for one set. + struct RenderParams { + int fontId = 0; + float lineCompression = 0.0f; + bool extraParagraphSpacing = false; + uint8_t paragraphAlignment = 0; + uint16_t viewportWidth = 0; + uint16_t viewportHeight = 0; + bool hyphenationEnabled = false; + bool embeddedStyle = false; + uint8_t imageRendering = 0; + bool focusReadingEnabled = false; + + bool operator==(const RenderParams& o) const; + }; + uint16_t pageCount = 0; int currentPage = 0; @@ -144,7 +161,10 @@ class Section { std::optional findAnchorDuringBuild(const std::string& anchor) const; // Get the page count from the section cache file without fully loading it. - std::optional getCachedPageCount() const; + // Finalized files only (a partial's count is just a build watermark). When + // `mustMatch` is given, the header's render parameters must equal it, so a + // count cached under different settings is never trusted. + std::optional getCachedPageCount(const RenderParams* mustMatch = nullptr) const; // Look up the page number for a synthetic paragraph index from XPath p[N]. std::optional getPageForParagraphIndex(uint16_t pIndex) const; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index aac1ea0c..713894f5 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -229,7 +229,7 @@ STR_OK_BUTTON: "OK" STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter" STR_FILTER_CONTRAST: "Contrast" STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar" -STR_CHAPTER_PAGE_COUNT: "Chapter Page Count" +STR_CHAPTER_PAGE_COUNT: "Page Count" STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage" STR_PROGRESS_BAR: "Progress Bar" STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 5df277c6..3354f08f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -58,6 +58,14 @@ class CrossPointSettings { HIDE_PROGRESS = 2, STATUS_BAR_PROGRESS_BAR_COUNT }; + // Values 0/1 keep the meaning of the old show/hide toggle (persisted under the + // legacy "statusBarChapterPageCount" key), so existing settings files load as-is. + enum STATUS_BAR_PAGE_COUNT { + HIDE_PAGE_COUNT = 0, + CHAPTER_PAGE_COUNT = 1, + BOOK_PAGE_COUNT = 2, + STATUS_BAR_PAGE_COUNT_COUNT + }; enum STATUS_BAR_PROGRESS_BAR_THICKNESS { PROGRESS_BAR_THIN = 0, PROGRESS_BAR_NORMAL = 1, @@ -189,7 +197,8 @@ class CrossPointSettings { uint8_t sleepScreenCoverFilter = NO_FILTER; // Status bar settings (statusBar retained for migration only) uint8_t statusBar = FULL; - uint8_t statusBarChapterPageCount = 1; + // STATUS_BAR_PAGE_COUNT; persisted under the legacy "statusBarChapterPageCount" key. + uint8_t statusBarPageCount = CHAPTER_PAGE_COUNT; uint8_t statusBarBookProgressPercentage = 1; uint8_t statusBarProgressBar = HIDE_PROGRESS; uint8_t statusBarProgressBarThickness = PROGRESS_BAR_NORMAL; diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index 21701efd..51c2549c 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -21,35 +21,35 @@ void applyLegacyStatusBarSettings(CrossPointSettings& settings) { switch (static_cast(settings.statusBar)) { case CrossPointSettings::NONE: - settings.statusBarChapterPageCount = 0; + settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarBattery = 0; break; case CrossPointSettings::NO_PROGRESS: - settings.statusBarChapterPageCount = 0; + settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::BOOK_PROGRESS_BAR: - settings.statusBarChapterPageCount = 1; + settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR: - settings.statusBarChapterPageCount = 1; + settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarBattery = 0; break; case CrossPointSettings::CHAPTER_PROGRESS_BAR: - settings.statusBarChapterPageCount = 0; + settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT; settings.statusBarBookProgressPercentage = 1; settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; @@ -57,7 +57,7 @@ void applyLegacyStatusBarSettings(CrossPointSettings& settings) { break; case CrossPointSettings::FULL: default: - settings.statusBarChapterPageCount = 1; + settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT; settings.statusBarBookProgressPercentage = 1; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; diff --git a/src/SettingsList.h b/src/SettingsList.h index c171cc5f..94fcf639 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -233,8 +233,10 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist }, "koSendMetadata", StrId::STR_KOREADER_SYNC), // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- - SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, - "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), + // Key kept from the old show/hide toggle so existing settings files load unchanged. + SettingInfo::Enum(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarPageCount, + {StrId::STR_HIDE, StrId::STR_CHAPTER, StrId::STR_BOOK}, "statusBarChapterPageCount", + StrId::STR_CUSTOMISE_STATUS_BAR), SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage, "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 66c9d9b4..afc3ad11 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -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(SETTINGS.extraParagraphSpacing), + SETTINGS.paragraphAlignment, + viewportWidth, + viewportHeight, + static_cast(SETTINGS.hyphenationEnabled), + static_cast(SETTINGS.embeddedStyle), + SETTINGS.imageRendering, + static_cast(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(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(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 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(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) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index e8799018..d3bb7100 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -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 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() 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); diff --git a/src/activities/settings/StatusBarSettingsActivity.cpp b/src/activities/settings/StatusBarSettingsActivity.cpp index 7bd755d6..f9383437 100644 --- a/src/activities/settings/StatusBarSettingsActivity.cpp +++ b/src/activities/settings/StatusBarSettingsActivity.cpp @@ -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: diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 9220d43c..2e15f404 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -132,7 +132,8 @@ int UITheme::getStatusBarHeight() { // Add status bar margin const bool showStatusBar = - SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || + SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT || + SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery || SETTINGS.statusBarClock != CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE; const bool showProgressBar = diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 5fe6e545..4296f9fa 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -27,7 +27,8 @@ constexpr int bookmarkStatusIconGap = 4; constexpr int bookmarkStatusIconTopCrop = 2; bool statusBarTextLaneVisible() { - return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || + return SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT || + SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery || (SETTINGS.statusBarClock && halClock.isAvailable()); } @@ -765,20 +766,23 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c int leftClusterWidth = 0; int rightClusterWidth = 0; - if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) { + const bool showPageCount = SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT; + if (SETTINGS.statusBarBookProgressPercentage || showPageCount) { // Right aligned text for progress counter char progressStr[32]; - // Prefix the page count with "~" while a still-building spine only yields an estimated total. + // Mark the total with "~" while it is only an estimate (a still-building spine's + // watermark, or a whole-book total with not-yet-paginated chapters). The current + // page is always exact, so the marker sits on the total. const char* estimatePrefix = pageCountEstimated ? "~" : ""; - if (SETTINGS.statusBarBookProgressPercentage && SETTINGS.statusBarChapterPageCount) { - snprintf(progressStr, sizeof(progressStr), "%s%d/%d %.0f%%", estimatePrefix, currentPage, pageCount, + if (SETTINGS.statusBarBookProgressPercentage && showPageCount) { + snprintf(progressStr, sizeof(progressStr), "%d/%s%d %.0f%%", currentPage, estimatePrefix, pageCount, bookProgress); } else if (SETTINGS.statusBarBookProgressPercentage) { snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress); } else { - snprintf(progressStr, sizeof(progressStr), "%s%d/%d", estimatePrefix, currentPage, pageCount); + snprintf(progressStr, sizeof(progressStr), "%d/%s%d", currentPage, estimatePrefix, pageCount); } int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);