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
+73
View File
@@ -0,0 +1,73 @@
#include "BookPages.h"
#include <algorithm>
#include <cmath>
#include <limits>
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<uint16_t>::max();
int clampToInt(const uint64_t v) {
return v > static_cast<uint64_t>(std::numeric_limits<int>::max()) ? std::numeric_limits<int>::max()
: static_cast<int>(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<uint32_t>(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<uint32_t>(liveSectionPages);
}
const double bytesPerPage = (knownBytes > 0 && knownPages > 0)
? static_cast<double>(knownBytes) / static_cast<double>(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<uint32_t>(entries[i].pages); // exact (0 = genuinely empty chapter)
} else if (i == spineIndex && liveSectionPages > 0) {
pages = static_cast<uint32_t>(std::min(liveSectionPages, MAX_SECTION_PAGES));
exact = false;
} else {
const double raw = std::floor(static_cast<double>(entries[i].bytes) / bytesPerPage + 0.5);
pages = static_cast<uint32_t>(std::clamp(raw, 1.0, static_cast<double>(MAX_SECTION_PAGES)));
exact = false;
}
total += pages;
if (i < spineIndex) {
before += pages;
}
}
pos.totalPages = clampToInt(total);
pos.currentPage = clampToInt(before + static_cast<uint64_t>(std::max(0, pageInSection)) + 1);
pos.isEstimate = !exact;
return pos;
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
// 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);
+38 -26
View File
@@ -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(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
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 } // 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<ChapterHtmlSlimParser> in BuildContext can be // Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
// constructed/destroyed where the parser's full definition is visible. // constructed/destroyed where the parser's full definition is visible.
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer) Section::Section(const std::shared_ptr<Epub>& 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); filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
int fileFontId; const RenderParams fileParams = readHeaderRenderParams(file);
uint16_t fileViewportWidth, fileViewportHeight; const RenderParams params = {fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
float fileLineCompression; viewportWidth, viewportHeight, hyphenationEnabled, embeddedStyle,
bool fileExtraParagraphSpacing; imageRendering, focusReadingEnabled};
uint8_t fileParagraphAlignment; if (!(fileParams == params)) {
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) {
file.close(); file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); clearCache();
@@ -754,7 +760,7 @@ std::string Section::getTextFromSectionFile() {
return fullText; return fullText;
} }
std::optional<uint16_t> Section::getCachedPageCount() const { std::optional<uint16_t> Section::getCachedPageCount(const RenderParams* mustMatch) const {
HalFile f; HalFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) { if (!Storage.openFileForRead("SCT", filePath, f)) {
return std::nullopt; return std::nullopt;
@@ -774,6 +780,12 @@ std::optional<uint16_t> Section::getCachedPageCount() const {
return std::nullopt; 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)); f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
uint16_t count; uint16_t count;
serialization::readPod(f, count); serialization::readPod(f, count);
+21 -1
View File
@@ -78,6 +78,23 @@ class Section {
std::unique_ptr<Page> loadPageDuringBuild(int page); std::unique_ptr<Page> loadPageDuringBuild(int page);
public: 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; uint16_t pageCount = 0;
int currentPage = 0; int currentPage = 0;
@@ -144,7 +161,10 @@ class Section {
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const; std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
// Get the page count from the section cache file without fully loading it. // Get the page count from the section cache file without fully loading it.
std::optional<uint16_t> 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<uint16_t> getCachedPageCount(const RenderParams* mustMatch = nullptr) const;
// Look up the page number for a synthetic paragraph index from XPath p[N]. // Look up the page number for a synthetic paragraph index from XPath p[N].
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const; std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
+1 -1
View File
@@ -229,7 +229,7 @@ STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter" STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
STR_FILTER_CONTRAST: "Contrast" STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar" 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_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
STR_PROGRESS_BAR: "Progress Bar" STR_PROGRESS_BAR: "Progress Bar"
STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness" STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness"
+10 -1
View File
@@ -58,6 +58,14 @@ class CrossPointSettings {
HIDE_PROGRESS = 2, HIDE_PROGRESS = 2,
STATUS_BAR_PROGRESS_BAR_COUNT 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 { enum STATUS_BAR_PROGRESS_BAR_THICKNESS {
PROGRESS_BAR_THIN = 0, PROGRESS_BAR_THIN = 0,
PROGRESS_BAR_NORMAL = 1, PROGRESS_BAR_NORMAL = 1,
@@ -189,7 +197,8 @@ class CrossPointSettings {
uint8_t sleepScreenCoverFilter = NO_FILTER; uint8_t sleepScreenCoverFilter = NO_FILTER;
// Status bar settings (statusBar retained for migration only) // Status bar settings (statusBar retained for migration only)
uint8_t statusBar = FULL; 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 statusBarBookProgressPercentage = 1;
uint8_t statusBarProgressBar = HIDE_PROGRESS; uint8_t statusBarProgressBar = HIDE_PROGRESS;
uint8_t statusBarProgressBarThickness = PROGRESS_BAR_NORMAL; uint8_t statusBarProgressBarThickness = PROGRESS_BAR_NORMAL;
+6 -6
View File
@@ -21,35 +21,35 @@
void applyLegacyStatusBarSettings(CrossPointSettings& settings) { void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(settings.statusBar)) { switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(settings.statusBar)) {
case CrossPointSettings::NONE: case CrossPointSettings::NONE:
settings.statusBarChapterPageCount = 0; settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0; settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
settings.statusBarBattery = 0; settings.statusBarBattery = 0;
break; break;
case CrossPointSettings::NO_PROGRESS: case CrossPointSettings::NO_PROGRESS:
settings.statusBarChapterPageCount = 0; settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0; settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
settings.statusBarBattery = 1; settings.statusBarBattery = 1;
break; break;
case CrossPointSettings::BOOK_PROGRESS_BAR: case CrossPointSettings::BOOK_PROGRESS_BAR:
settings.statusBarChapterPageCount = 1; settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0; settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
settings.statusBarBattery = 1; settings.statusBarBattery = 1;
break; break;
case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR: case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR:
settings.statusBarChapterPageCount = 1; settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0; settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
settings.statusBarBattery = 0; settings.statusBarBattery = 0;
break; break;
case CrossPointSettings::CHAPTER_PROGRESS_BAR: case CrossPointSettings::CHAPTER_PROGRESS_BAR:
settings.statusBarChapterPageCount = 0; settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 1; settings.statusBarBookProgressPercentage = 1;
settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS; settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
@@ -57,7 +57,7 @@ void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
break; break;
case CrossPointSettings::FULL: case CrossPointSettings::FULL:
default: default:
settings.statusBarChapterPageCount = 1; settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 1; settings.statusBarBookProgressPercentage = 1;
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
+4 -2
View File
@@ -233,8 +233,10 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
}, },
"koSendMetadata", StrId::STR_KOREADER_SYNC), "koSendMetadata", StrId::STR_KOREADER_SYNC),
// --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) ---
SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, // Key kept from the old show/hide toggle so existing settings files load unchanged.
"statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), 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, SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage,
"statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar,
+106 -3
View File
@@ -44,6 +44,22 @@ constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12};
constexpr size_t initialBookmarkCacheCapacity = 16; constexpr size_t initialBookmarkCacheCapacity = 16;
constexpr float bookmarkProgressEpsilon = 0.0001f; 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) { int clampPercent(int percent) {
if (percent < 0) { if (percent < 0) {
return 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 // End-of-Book screen reached (currentSpineIndex == spine count) means the book is
// finished. Two independent finished-book features key off this same condition. // finished. Two independent finished-book features key off this same condition.
const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount(); const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount();
@@ -980,6 +1015,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
buildViewportWidth = viewportWidth; buildViewportWidth = viewportWidth;
buildViewportHeight = viewportHeight; buildViewportHeight = viewportHeight;
ensureBookPages(viewportWidth, viewportHeight);
if (!section) { if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href; const auto filepath = epub->getSpineItem(currentSpineIndex).href;
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex); 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. // a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
applyDeferredReposition(); applyDeferredReposition();
recordCurrentSectionPages();
renderer.clearScreen(); renderer.clearScreen();
if (section->pageCount == 0) { if (section->pageCount == 0) {
@@ -1331,6 +1370,59 @@ bool EpubReaderActivity::applyDeferredReposition() {
return changed; 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) { bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, 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 // 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. // "page X of Y" and the progress bar don't read off the small build watermark.
const int currentPage = section->currentPage + 1; 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 sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100; 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; std::string title;
int textYOffset = 0; int textYOffset = 0;
@@ -1543,8 +1646,8 @@ void EpubReaderActivity::renderStatusBar() const {
title = epub->getTitle(); title = epub->getTitle();
} }
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked, GUI.drawStatusBar(renderer, bookProgress, counterPage, counterTotal, title, 0, textYOffset, true,
section->isBuilding()); currentPageBookmarked, counterIsEstimate);
} }
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <Epub.h> #include <Epub.h>
#include <Epub/BookPages.h>
#include <Epub/FootnoteEntry.h> #include <Epub/FootnoteEntry.h>
#include <Epub/Section.h> #include <Epub/Section.h>
@@ -23,6 +24,17 @@ class EpubReaderActivity final : public Activity {
int pagesUntilFullRefresh = 0; int pagesUntilFullRefresh = 0;
int cachedSpineIndex = 0; int cachedSpineIndex = 0;
int cachedChapterTotalPageCount = 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 lastPageTurnTime = 0UL;
unsigned long pageTurnDuration = 0UL; unsigned long pageTurnDuration = 0UL;
// Signals that the next render should reposition within the newly loaded section // 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. // (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). // No-op while the section is still building or when the pagination is unchanged (plain resume).
bool applyDeferredReposition(); 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); bool saveProgress(int spineIndex, int currentPage, int pageCount);
// Jump to a percentage of the book (0-100), mapping it to spine and page. // Jump to a percentage of the book (0-100), mapping it to spine and page.
void jumpToPercent(int percent); 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); snprintf(buf, sizeof(buf), "UTC%c%d:%02d", neg ? '-' : '+', hours, mins);
return buf; 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; constexpr int PROGRESS_BAR_ITEMS = 3;
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}; const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
@@ -90,7 +94,11 @@ void StatusBarSettingsActivity::onEnter() {
selectedIndex = 0; selectedIndex = 0;
visibleItemCount = halClock.isAvailable() ? FULL_MENU_ITEMS : BASE_MENU_ITEMS; 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) { if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) {
SETTINGS.statusBarProgressBar = CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; SETTINGS.statusBarProgressBar = CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
} }
@@ -163,8 +171,12 @@ void StatusBarSettingsActivity::loop() {
void StatusBarSettingsActivity::handleSelection() { void StatusBarSettingsActivity::handleSelection() {
switch (selectedIndex) { switch (selectedIndex) {
case ITEM_CHAPTER_PAGE_COUNT: case ITEM_CHAPTER_PAGE_COUNT:
SETTINGS.statusBarChapterPageCount = (SETTINGS.statusBarChapterPageCount + 1) % 2; optionPopup.show(StrId::STR_CHAPTER_PAGE_COUNT, pageCountNames, PAGE_COUNT_ITEMS, SETTINGS.statusBarPageCount,
break; [this](int idx) {
SETTINGS.statusBarPageCount = idx;
SETTINGS.saveToFile();
});
return;
case ITEM_BOOK_PROGRESS_PERCENTAGE: case ITEM_BOOK_PROGRESS_PERCENTAGE:
SETTINGS.statusBarBookProgressPercentage = (SETTINGS.statusBarBookProgressPercentage + 1) % 2; SETTINGS.statusBarBookProgressPercentage = (SETTINGS.statusBarBookProgressPercentage + 1) % 2;
break; break;
@@ -236,7 +248,7 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
[](int index) -> std::string { [](int index) -> std::string {
switch (index) { switch (index) {
case ITEM_CHAPTER_PAGE_COUNT: 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: case ITEM_BOOK_PROGRESS_PERCENTAGE:
return SETTINGS.statusBarBookProgressPercentage ? tr(STR_SHOW) : tr(STR_HIDE); return SETTINGS.statusBarBookProgressPercentage ? tr(STR_SHOW) : tr(STR_HIDE);
case ITEM_PROGRESS_BAR: case ITEM_PROGRESS_BAR:
+2 -1
View File
@@ -132,7 +132,8 @@ int UITheme::getStatusBarHeight() {
// Add status bar margin // Add status bar margin
const bool showStatusBar = 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.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
SETTINGS.statusBarClock != CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE; SETTINGS.statusBarClock != CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE;
const bool showProgressBar = const bool showProgressBar =
+10 -6
View File
@@ -27,7 +27,8 @@ constexpr int bookmarkStatusIconGap = 4;
constexpr int bookmarkStatusIconTopCrop = 2; constexpr int bookmarkStatusIconTopCrop = 2;
bool statusBarTextLaneVisible() { 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.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
(SETTINGS.statusBarClock && halClock.isAvailable()); (SETTINGS.statusBarClock && halClock.isAvailable());
} }
@@ -765,20 +766,23 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
int leftClusterWidth = 0; int leftClusterWidth = 0;
int rightClusterWidth = 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 // Right aligned text for progress counter
char progressStr[32]; 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 ? "~" : ""; const char* estimatePrefix = pageCountEstimated ? "~" : "";
if (SETTINGS.statusBarBookProgressPercentage && SETTINGS.statusBarChapterPageCount) { if (SETTINGS.statusBarBookProgressPercentage && showPageCount) {
snprintf(progressStr, sizeof(progressStr), "%s%d/%d %.0f%%", estimatePrefix, currentPage, pageCount, snprintf(progressStr, sizeof(progressStr), "%d/%s%d %.0f%%", currentPage, estimatePrefix, pageCount,
bookProgress); bookProgress);
} else if (SETTINGS.statusBarBookProgressPercentage) { } else if (SETTINGS.statusBarBookProgressPercentage) {
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress); snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
} else { } 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); int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);