feat: Lazy incremental EPUB section indexing (#2452)
Co-authored-by: Uri Tauber <uritaube@gmail.com> Co-authored-by: Julia Nguyen <julia@uxj.io>
This commit is contained in:
co-authored by
Uri Tauber
Julia Nguyen
parent
79b4d63ee2
commit
685d4e88f9
@@ -43,6 +43,10 @@ struct PageResult {
|
||||
struct ProgressChangeResult {
|
||||
int spineIndex = 0;
|
||||
int page = 0;
|
||||
int totalPages = 0;
|
||||
std::string xpath;
|
||||
float percentage = 0.0f;
|
||||
bool hasSavedProgress = false;
|
||||
};
|
||||
|
||||
enum class NetworkMode;
|
||||
|
||||
@@ -238,6 +238,33 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drive any in-progress incremental section build forward, off the page-turn critical path,
|
||||
// but only within a small window ahead of the reader: an unbounded build monopolized the
|
||||
// RenderLock and locked out page turns. The build follows the reader instead, and instant
|
||||
// reopen comes from suspendBuild() persisting the laid-out pages as a partial on exit.
|
||||
// Skip while the render mutex is busy so we never delay a pending render; re-check
|
||||
// isBuilding() under the lock since render() may have just finished it.
|
||||
if (section && section->isBuilding() && !RenderLock::peek() &&
|
||||
static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD) {
|
||||
RenderLock lock;
|
||||
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
|
||||
// build between the outer isBuilding() check and acquiring the lock here, in which case
|
||||
// buildSomeMore() would fail and wrongly reset the section. cppcheck can't see the cross-task
|
||||
// mutation, so it flags this as always true.
|
||||
// cppcheck-suppress knownConditionTrueFalse
|
||||
if (section->isBuilding()) {
|
||||
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
|
||||
LOG_ERR("ERS", "Background section build failed");
|
||||
section.reset();
|
||||
requestUpdate();
|
||||
} else if (section->isBuildComplete() && applyDeferredReposition()) {
|
||||
// The chapter re-paginated since the saved progress (settings changed): we now know the
|
||||
// real page count, so re-render at the remapped page. No-op for an unchanged resume.
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -306,10 +333,11 @@ void EpubReaderActivity::loop() {
|
||||
ignoreNextConfirmRelease = false;
|
||||
} else {
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
const int totalPages = section ? section->estimatedTotalPages() : 0;
|
||||
float bookProgress = 0.0f;
|
||||
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
||||
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||
if (epub->getBookSize() > 0 && section && section->estimatedTotalPages() > 0) {
|
||||
const float chapterProgress =
|
||||
static_cast<float>(section->currentPage) / static_cast<float>(section->estimatedTotalPages());
|
||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
}
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
@@ -537,11 +565,32 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
loadCachedBookmarks();
|
||||
if (!result.isCancelled) {
|
||||
const auto& sync = std::get<ProgressChangeResult>(result.data);
|
||||
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
|
||||
int targetSpineIndex = sync.spineIndex;
|
||||
int targetPage = sync.page;
|
||||
const int activeTotalPages = section ? section->estimatedTotalPages() : 0;
|
||||
const bool cachedPageMatchesActiveSection = section && sync.totalPages > 0 &&
|
||||
currentSpineIndex == sync.spineIndex && sync.page >= 0 &&
|
||||
sync.page < sync.totalPages && activeTotalPages == sync.totalPages;
|
||||
|
||||
if (!cachedPageMatchesActiveSection && sync.hasSavedProgress) {
|
||||
const int totalPages = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount;
|
||||
CrossPointPosition fallback =
|
||||
ProgressMapper::toCrossPoint(epub, {sync.xpath, sync.percentage}, renderer, currentSpineIndex, totalPages);
|
||||
targetSpineIndex = fallback.spineIndex;
|
||||
targetPage = fallback.pageNumber;
|
||||
}
|
||||
|
||||
if (currentSpineIndex != targetSpineIndex) {
|
||||
RenderLock lock(*this);
|
||||
currentSpineIndex = sync.spineIndex;
|
||||
nextPageNumber = sync.page;
|
||||
currentSpineIndex = targetSpineIndex;
|
||||
nextPageNumber = targetPage;
|
||||
section.reset();
|
||||
} else if (section && section->currentPage != targetPage) {
|
||||
RenderLock lock(*this);
|
||||
const int clampedTargetPage = std::max(0, targetPage);
|
||||
section->currentPage = clampedTargetPage;
|
||||
} else if (!section) {
|
||||
nextPageNumber = targetPage;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -661,7 +710,7 @@ bool EpubReaderActivity::launchKOReaderSync() {
|
||||
if (!KOREADER_STORE.hasCredentials()) return false; // no-op: nothing to launch
|
||||
|
||||
const int currentPage = section ? section->currentPage : nextPageNumber;
|
||||
const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount;
|
||||
const int totalPages = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount;
|
||||
std::optional<uint16_t> paragraphIndex;
|
||||
if (section && currentPage >= 0 && currentPage < section->pageCount) {
|
||||
const uint16_t paragraphPage =
|
||||
@@ -759,7 +808,12 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption
|
||||
|
||||
void EpubReaderActivity::pageTurn(bool isForwardTurn) {
|
||||
if (isForwardTurn) {
|
||||
if (section->currentPage < section->pageCount - 1) {
|
||||
// Advance within the section while there are (or may still be) more pages: either a built
|
||||
// page ahead, or the section is still building (windowed), in which case more pages exist
|
||||
// beyond the current watermark and render()'s ensure-built pump will lay them out. Only when
|
||||
// the section is fully built AND we're on its last page do we move to the next spine -- using
|
||||
// the live pageCount alone would mistake the build watermark for the end of a giant spine.
|
||||
if (section->currentPage < section->pageCount - 1 || section->isBuilding()) {
|
||||
section->currentPage++;
|
||||
} else {
|
||||
// We don't want to delete the section mid-render, so grab the semaphore
|
||||
@@ -847,48 +901,130 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||
|
||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
LOG_DBG("ERS", "Cache not found, building...");
|
||||
// A finalized cache serves every page as-is. A partial cache (suspended build from a
|
||||
// previous session) serves its pages instantly too, but a build must still run to lay
|
||||
// out the rest -- it re-parses from the top in the background (HTML already cached,
|
||||
// pages are deterministic) and finalizes, so the partial machinery retires itself.
|
||||
const bool cacheLoaded = section->loadSectionFile(
|
||||
SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing,
|
||||
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled);
|
||||
if (cacheLoaded) {
|
||||
// Matching render params means identical pagination, so the saved page number is valid
|
||||
// as-is: consume any pending settings-change reposition. Without this, a chapter total
|
||||
// saved while the section was still building (i.e. a watermark, not the real count)
|
||||
// would remap the resume page against the finalized count and teleport the reader.
|
||||
cachedChapterTotalPageCount = 0;
|
||||
}
|
||||
const bool cacheComplete = cacheLoaded && !section->isPartial();
|
||||
if (!cacheComplete) {
|
||||
if (section->isPartial()) {
|
||||
LOG_DBG("ERS", "Partial cache found (%d pages), resuming build...", section->pageCount);
|
||||
} else {
|
||||
LOG_DBG("ERS", "Cache not found, building...");
|
||||
}
|
||||
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
|
||||
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
|
||||
|
||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
|
||||
LOG_ERR("ERS", "Failed to persist page data to SD");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
return;
|
||||
// Jumps that need the final pagination or the anchor map -- explicit page jumps,
|
||||
// fragment anchors, percent jumps, and cross-setting progress repositioning -- can't
|
||||
// resolve their landing page until the whole chapter is laid out, so they take the full
|
||||
// (blocking) build with the indexing popup. Everything else -- plain forward reads, resume,
|
||||
// and explicit page jumps -- only needs a specific page, so it builds incrementally to that
|
||||
// page and finishes the rest in loop(). The settings-change reposition (cachedChapterTotal*)
|
||||
// is NOT a full-build trigger: it's deferred to applyDeferredReposition() once the real page
|
||||
// count is known, so it never blocks the first page.
|
||||
// Only a percent jump truly needs the whole chapter up front (percent -> page needs the final
|
||||
// page count). Anchor jumps (TOC / chapter select / footnotes) resolve incrementally below --
|
||||
// the anchor is recorded as its page is laid out, so a chapter-top anchor lands on page 0
|
||||
// without indexing the whole chapter.
|
||||
const bool needsFullBuild = pendingPercentJump;
|
||||
if (needsFullBuild) {
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
// The popup's own refresh is a plain FAST, so force the page that replaces it onto the HALF
|
||||
// ghost-cleanup path -- otherwise the "INDEXING" text ghosts under the rendered page.
|
||||
pagesUntilFullRefresh = 1;
|
||||
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
|
||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
|
||||
LOG_ERR("ERS", "Failed to persist page data to SD");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Lay out just enough to show the landing page; loop() builds the rest behind it. Show the
|
||||
// indexing popup up front only when the build will actually be slow: a large spine (its
|
||||
// whole HTML must be inflated before page 1 can lay out -- the giant single-spine case), or
|
||||
// a deep resume/jump that must lay out many pages to reach the landing page. Tiny sections
|
||||
// build in a blink and stay popup-free.
|
||||
const int target = pendingPageJump.has_value() ? *pendingPageJump : (nextPageNumber < 0 ? 0 : nextPageNumber);
|
||||
const size_t spineBytes = epub->getCumulativeSpineItemSize(currentSpineIndex) -
|
||||
(currentSpineIndex > 0 ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0);
|
||||
// Popup only when the build will actually be slow: a big spine whose HTML still needs
|
||||
// inflating (the multi-second cost), or a deep page target. A reopen with cached HTML builds
|
||||
// fast, so no popup -- that's what made an already-indexed book look like it was reindexing.
|
||||
// A partial cache that already covers the target page shows it instantly: never popup.
|
||||
const bool willInflate = !section->hasHtmlCache();
|
||||
const bool anchorJump = !pendingAnchor.empty();
|
||||
bool showPopup;
|
||||
if (anchorJump) {
|
||||
// An anchor jump's cost is bounded by the anchor's page, not `target`. An anchor already
|
||||
// in the on-disk map (partial or finalized cache) lands instantly: no popup. Otherwise it
|
||||
// lies beyond the indexed watermark and the build may lay out the whole spine to find it,
|
||||
// so gate on spine size alone -- laying out a big spine takes seconds even with cached
|
||||
// HTML. Ordinary chapter-top TOC jumps resolve on page 0 and stay popup-free.
|
||||
showPopup = !section->findAnchor(pendingAnchor).has_value() && spineBytes > BUILD_POPUP_BYTE_THRESHOLD;
|
||||
} else {
|
||||
const bool targetAvailable = target < static_cast<int>(section->pageCount);
|
||||
showPopup = !targetAvailable &&
|
||||
((spineBytes > BUILD_POPUP_BYTE_THRESHOLD && willInflate) || target > BUILD_POPUP_PAGE_THRESHOLD);
|
||||
}
|
||||
if (showPopup) {
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
// HALF-clear the popup when the page replaces it, else "INDEXING" ghosts under the page.
|
||||
pagesUntilFullRefresh = 1;
|
||||
}
|
||||
if (!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
LOG_ERR("ERS", "Failed to start section build");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
return;
|
||||
}
|
||||
while (!section->isBuildComplete() &&
|
||||
(anchorJump ? !section->findAnchor(pendingAnchor) : static_cast<int>(section->pageCount) <= target)) {
|
||||
// Anchor jump: build until the anchor's page is laid out (usually page 0), checking a
|
||||
// partial's on-disk anchor map too so an already-indexed anchor resolves immediately.
|
||||
// Otherwise: build until the target page exists. loop() builds the rest behind it.
|
||||
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
|
||||
LOG_ERR("ERS", "Failed during incremental section build");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("ERS", "Cache found, skipping build...");
|
||||
}
|
||||
|
||||
if (pendingPageJump.has_value()) {
|
||||
if (*pendingPageJump >= section->pageCount && section->pageCount > 0) {
|
||||
section->currentPage = section->pageCount - 1;
|
||||
} else {
|
||||
section->currentPage = *pendingPageJump;
|
||||
}
|
||||
section->currentPage = *pendingPageJump;
|
||||
pendingPageJump.reset();
|
||||
} else {
|
||||
section->currentPage = nextPageNumber;
|
||||
if (section->currentPage < 0) {
|
||||
section->currentPage = 0;
|
||||
} else if (section->currentPage >= section->pageCount && section->pageCount > 0) {
|
||||
LOG_DBG("ERS", "Clamping cached page %d to %d", section->currentPage, section->pageCount - 1);
|
||||
section->currentPage = section->pageCount - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingAnchor.empty()) {
|
||||
if (const auto page = section->getPageForAnchor(pendingAnchor)) {
|
||||
// Resolve from the pages laid out so far and/or the on-disk map (finalized or partial).
|
||||
const auto page = section->findAnchor(pendingAnchor);
|
||||
if (page) {
|
||||
section->currentPage = *page;
|
||||
LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page);
|
||||
} else {
|
||||
@@ -897,17 +1033,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
pendingAnchor.clear();
|
||||
}
|
||||
|
||||
// handles changes in reader settings and reset to approximate position based on cached progress
|
||||
if (cachedChapterTotalPageCount > 0) {
|
||||
// only goes to relative position if spine index matches cached value
|
||||
if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) {
|
||||
float progress = static_cast<float>(section->currentPage) / static_cast<float>(cachedChapterTotalPageCount);
|
||||
int newPage = static_cast<int>(progress * section->pageCount);
|
||||
section->currentPage = newPage;
|
||||
}
|
||||
cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again
|
||||
}
|
||||
|
||||
if (pendingPercentJump && section->pageCount > 0) {
|
||||
// Apply the pending percent jump now that we know the new section's page count.
|
||||
int newPage = static_cast<int>(pendingSpineProgress * static_cast<float>(section->pageCount));
|
||||
@@ -919,6 +1044,57 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extend the build to the requested page if needed (for partials and in-progress builds).
|
||||
// This runs every render, so it covers both the first page and any forward turn that gets
|
||||
// ahead of the background builder; pages already built do no work here.
|
||||
while (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) {
|
||||
// Start a build to extend a partial toward the requested page.
|
||||
if (!section->isBuilding() &&
|
||||
!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
|
||||
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering,
|
||||
SETTINGS.focusReadingEnabled)) {
|
||||
LOG_ERR("ERS", "Failed to start partial extension build");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
return;
|
||||
}
|
||||
// Extend until either the target page exists or the build completes.
|
||||
while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) {
|
||||
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
|
||||
LOG_ERR("ERS", "Failed during incremental section build");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// For an in-progress incremental build, make sure the page we're about to show has been laid out.
|
||||
if (section->isBuilding()) {
|
||||
while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) {
|
||||
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
|
||||
LOG_ERR("ERS", "Failed during incremental section build");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The requested page is now as built as it will get. If it still lands past the end,
|
||||
// clamp to the last real page: the UINT16_MAX "last page" sentinel from backward chapter
|
||||
// navigation, an explicit jump beyond a finished chapter, or a stale saved position.
|
||||
// Guarded on !isBuilding() because a still-building section's pageCount is only the current
|
||||
// watermark (not the final count) and has already been driven far enough by the loops above.
|
||||
if (!section->isBuilding() && section->pageCount > 0 &&
|
||||
section->currentPage >= static_cast<int>(section->pageCount)) {
|
||||
section->currentPage = section->pageCount - 1;
|
||||
}
|
||||
|
||||
// Apply a deferred settings-change reposition now that the real page count is known (a no-op for
|
||||
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
|
||||
applyDeferredReposition();
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
if (section->pageCount == 0) {
|
||||
@@ -944,9 +1120,14 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
updateBookmarkFlag();
|
||||
|
||||
{
|
||||
auto p = section->loadPageFromSectionFile();
|
||||
// Unified page read: the in-progress build's in-RAM table if it has reached the page,
|
||||
// otherwise the on-disk file (finalized section, or a partial from a previous session).
|
||||
auto p = section->loadPage(section->currentPage);
|
||||
if (!p) {
|
||||
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
|
||||
// Abandon (not suspend) any active build BEFORE clearing: clearCache deletes the files,
|
||||
// and the destructor's suspend would otherwise commit tables into a deleted handle.
|
||||
section->abandonBuild();
|
||||
section->clearCache();
|
||||
section.reset();
|
||||
requestUpdate(); // Try again after clearing cache
|
||||
@@ -963,8 +1144,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
||||
}
|
||||
silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight);
|
||||
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
||||
saveProgress(currentSpineIndex, section->currentPage, section->estimatedTotalPages());
|
||||
|
||||
showPendingSyncSaveError();
|
||||
|
||||
@@ -978,36 +1158,28 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportWidth, const uint16_t viewportHeight) {
|
||||
if (!epub || !section || section->pageCount < 2) {
|
||||
return;
|
||||
bool EpubReaderActivity::applyDeferredReposition() {
|
||||
if (cachedChapterTotalPageCount == 0 || !section || section->isBuilding()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build the next chapter cache while the penultimate page is on screen.
|
||||
if (section->currentPage != section->pageCount - 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int nextSpineIndex = currentSpineIndex + 1;
|
||||
if (nextSpineIndex < 0 || nextSpineIndex >= epub->getSpineItemsCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Section nextSection(epub, nextSpineIndex, renderer);
|
||||
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex);
|
||||
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
|
||||
bool changed = false;
|
||||
// Only remap when the chapter actually re-paginated (e.g. after a settings change). A plain
|
||||
// resume has identical pagination, so section->pageCount == cachedChapterTotalPageCount and
|
||||
// nothing moves.
|
||||
if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) {
|
||||
const float progress = static_cast<float>(section->currentPage) / static_cast<float>(cachedChapterTotalPageCount);
|
||||
int newPage = static_cast<int>(progress * static_cast<float>(section->pageCount));
|
||||
if (newPage < 0) newPage = 0;
|
||||
if (section->pageCount > 0 && newPage >= static_cast<int>(section->pageCount)) {
|
||||
newPage = section->pageCount - 1;
|
||||
}
|
||||
if (newPage != section->currentPage) {
|
||||
section->currentPage = newPage;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
cachedChapterTotalPageCount = 0; // consumed; don't read cached progress again
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
||||
@@ -1180,9 +1352,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
}
|
||||
|
||||
void EpubReaderActivity::renderStatusBar() const {
|
||||
// Calculate progress in book
|
||||
// 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->pageCount;
|
||||
const float pageCount = section->estimatedTotalPages();
|
||||
const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
|
||||
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
|
||||
|
||||
@@ -1213,7 +1386,8 @@ void EpubReaderActivity::renderStatusBar() const {
|
||||
title = epub->getTitle();
|
||||
}
|
||||
|
||||
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked);
|
||||
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked,
|
||||
section->isBuilding());
|
||||
}
|
||||
|
||||
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
|
||||
@@ -1304,7 +1478,7 @@ void EpubReaderActivity::addBookmark() {
|
||||
int pageCount;
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
pageCount = section->pageCount;
|
||||
pageCount = section->estimatedTotalPages();
|
||||
currentPage = section->currentPage;
|
||||
}
|
||||
|
||||
@@ -1353,10 +1527,10 @@ void EpubReaderActivity::updateBookmarkFlag() {
|
||||
currentPageBookmarked = false;
|
||||
return;
|
||||
}
|
||||
const ProgressRange pageRange =
|
||||
getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount);
|
||||
const int pageCount = section->estimatedTotalPages();
|
||||
const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, section->currentPage, pageCount);
|
||||
currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) {
|
||||
return bookmarkMatchesProgress(b, currentSpineIndex, section->currentPage, section->pageCount, pageRange);
|
||||
return bookmarkMatchesProgress(b, currentSpineIndex, section->currentPage, pageCount, pageRange);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1369,9 +1543,9 @@ ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const {
|
||||
}
|
||||
if (section) {
|
||||
info.currentPage = section->currentPage + 1;
|
||||
info.totalPages = section->pageCount;
|
||||
if (epub && epub->getBookSize() > 0 && section->pageCount > 0) {
|
||||
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||
info.totalPages = section->estimatedTotalPages();
|
||||
if (epub && epub->getBookSize() > 0 && info.totalPages > 0) {
|
||||
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(info.totalPages);
|
||||
int pct = static_cast<int>(epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f + 0.5f);
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
@@ -1383,7 +1557,7 @@ ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const {
|
||||
|
||||
CrossPointPosition EpubReaderActivity::getCurrentPosition() const {
|
||||
const int currentPage = section ? section->currentPage : nextPageNumber;
|
||||
const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount;
|
||||
const int totalPages = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount;
|
||||
std::optional<uint16_t> paragraphIndex;
|
||||
if (section && currentPage >= 0 && currentPage < section->pageCount) {
|
||||
const uint16_t paragraphPage =
|
||||
|
||||
@@ -59,7 +59,31 @@ class EpubReaderActivity final : public Activity {
|
||||
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
|
||||
int orientedMarginBottom, int orientedMarginLeft);
|
||||
void renderStatusBar() const;
|
||||
void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight);
|
||||
// Pages laid out per incremental-build pump: on the render path (catching up to the page
|
||||
// being shown) and per loop() tick (background build of a large chapter). Kept small so a
|
||||
// background build chunk never noticeably delays input or a pending render.
|
||||
static constexpr int BUILD_PAGES_PER_CHUNK = 8;
|
||||
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
|
||||
// How many pages to keep laid out ahead of the reader for a still-building section. A page
|
||||
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
|
||||
// -- a tiny buffer is enough. The background build stops once the watermark is this far
|
||||
// ahead and resumes as the reader advances; building unbounded instead locked up input by
|
||||
// monopolizing the RenderLock. A giant single-spine book therefore never finalizes its .bin
|
||||
// in one sitting -- instant reopen comes from Section::suspendBuild() persisting the pages
|
||||
// already laid out as a partial file on exit/sleep.
|
||||
static constexpr int BUILD_WINDOW_AHEAD = 5;
|
||||
// Show the indexing popup when an initial build must lay out more than this many pages up front
|
||||
// (a deep resume/jump into a not-yet-built section), so it isn't a silent wait. Kept independent
|
||||
// of the small look-ahead window so ordinary landings stay popup-free.
|
||||
static constexpr int BUILD_POPUP_PAGE_THRESHOLD = 20;
|
||||
// Also show the popup when first building a spine larger than this (uncompressed bytes): its
|
||||
// whole HTML must be inflated before page 1 can lay out (the giant single-spine case), which is
|
||||
// a multi-second wait. Normal chapters are well under this and stay popup-free.
|
||||
static constexpr size_t BUILD_POPUP_BYTE_THRESHOLD = 96 * 1024;
|
||||
// Remap the cached relative reading position once the section's real page count is known
|
||||
// (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();
|
||||
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);
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "ProgressMapper.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@@ -108,8 +107,17 @@ void EpubReaderBookmarksActivity::loop() {
|
||||
return;
|
||||
}
|
||||
auto bookmark = bookmarks.at(selectorIndex);
|
||||
CrossPointPosition pos = ProgressMapper::toCrossPoint(epub, {bookmark.xpath, bookmark.percentage}, renderer);
|
||||
setResult(ProgressChangeResult{pos.spineIndex, pos.pageNumber});
|
||||
ProgressChangeResult result{};
|
||||
result.xpath = bookmark.xpath;
|
||||
result.percentage = bookmark.percentage;
|
||||
result.hasSavedProgress = true;
|
||||
if (bookmark.computedChapterPageCount > 0 && bookmark.computedChapterProgress < bookmark.computedChapterPageCount &&
|
||||
bookmark.computedSpineIndex < epub->getSpineItemsCount()) {
|
||||
result.spineIndex = bookmark.computedSpineIndex;
|
||||
result.page = bookmark.computedChapterProgress;
|
||||
result.totalPages = bookmark.computedChapterPageCount;
|
||||
}
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <Memory.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
@@ -14,6 +15,7 @@
|
||||
#include "XtcReaderActivity.h"
|
||||
#include "activities/util/BmpViewerActivity.h"
|
||||
#include "activities/util/FullScreenMessageActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
|
||||
bool ReaderActivity::isXtcFile(const std::string& path) { return FsHelpers::hasXtcExtension(path); }
|
||||
|
||||
@@ -35,6 +37,12 @@ std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
||||
LOG_ERR("READER", "Failed to allocate EPUB object");
|
||||
return nullptr;
|
||||
}
|
||||
// First open: building the spine/TOC index (book.bin) takes a couple of seconds. Show the
|
||||
// indexing popup so it isn't a silent wait on the home screen. The cachePath/hash is known at
|
||||
// construction, so this check is valid before load(); a cached open loads in a blink -> no popup.
|
||||
if (!Storage.exists((epub->getCachePath() + "/book.bin").c_str())) {
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
}
|
||||
if (epub->load(true, SETTINGS.embeddedStyle == 0)) {
|
||||
return epub;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ class Txt;
|
||||
class ReaderActivity final : public Activity {
|
||||
std::string initialBookPath;
|
||||
std::string currentBookPath; // Track current book path for navigation
|
||||
static std::unique_ptr<Epub> loadEpub(const std::string& path);
|
||||
// Non-static (unlike the other loaders): draws the first-open indexing popup, which needs the renderer.
|
||||
std::unique_ptr<Epub> loadEpub(const std::string& path);
|
||||
static std::unique_ptr<Xtc> loadXtc(const std::string& path);
|
||||
static std::unique_ptr<Txt> loadTxt(const std::string& path);
|
||||
static bool isXtcFile(const std::string& path);
|
||||
|
||||
Reference in New Issue
Block a user