Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd00c185f2 |
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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<ChapterHtmlSlimParser> in BuildContext can be
|
||||
// constructed/destroyed where the parser's full definition is visible.
|
||||
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);
|
||||
|
||||
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<uint16_t> Section::getCachedPageCount() const {
|
||||
std::optional<uint16_t> Section::getCachedPageCount(const RenderParams* mustMatch) const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
@@ -774,6 +780,12 @@ std::optional<uint16_t> 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);
|
||||
|
||||
+21
-1
@@ -78,6 +78,23 @@ class Section {
|
||||
std::unique_ptr<Page> 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<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
|
||||
|
||||
// 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].
|
||||
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,35 +21,35 @@
|
||||
void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
|
||||
switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(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;
|
||||
|
||||
+4
-2
@@ -233,8 +233,10 @@ inline std::vector<SettingInfo> 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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user