diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 569646a4..e636cbe3 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "No chapters" STR_END_OF_BOOK: "End of book" STR_EMPTY_CHAPTER: "Empty chapter" STR_INDEXING: "Indexing" +STR_INDEX_FAILED: "Failed to index - invalid book" STR_MEMORY_ERROR: "Memory error" STR_PAGE_LOAD_ERROR: "Page load error" STR_EMPTY_FILE: "Empty file" diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index dcc75cd2..be0c1466 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -893,6 +893,15 @@ void EpubReaderActivity::render(RenderLock&& lock) { GUI.drawPopup(renderer, tr(STR_SAVE_PROGRESS_FAILED)); }; + // A section build failure (e.g. an invalid/corrupt EPUB that fails XML parsing) leaves the + // "Indexing" popup on screen with no way forward. Surface an explicit error instead of hanging. + // clearScreen first so the error popup doesn't overlay the stale "Indexing" popup. + const auto showBuildError = [this]() { + renderer.clearScreen(); + GUI.drawPopup(renderer, tr(STR_INDEX_FAILED)); + automaticPageTurnActive = false; + }; + // edge case handling for sub-zero spine index if (currentSpineIndex < 0) { currentSpineIndex = 0; @@ -991,7 +1000,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) { LOG_ERR("ERS", "Failed to persist page data to SD"); section.reset(); - showPendingSyncSaveError(); + showBuildError(); return; } } else { @@ -1033,7 +1042,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) { LOG_ERR("ERS", "Failed to start section build"); section.reset(); - showPendingSyncSaveError(); + showBuildError(); return; } while (!section->isBuildComplete() && @@ -1044,7 +1053,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) { LOG_ERR("ERS", "Failed during incremental section build"); section.reset(); - showPendingSyncSaveError(); + showBuildError(); return; } } @@ -1098,7 +1107,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { SETTINGS.focusReadingEnabled)) { LOG_ERR("ERS", "Failed to start partial extension build"); section.reset(); - showPendingSyncSaveError(); + showBuildError(); return; } // Extend until either the target page exists or the build completes. @@ -1106,7 +1115,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) { LOG_ERR("ERS", "Failed during incremental section build"); section.reset(); - showPendingSyncSaveError(); + showBuildError(); return; } } @@ -1117,7 +1126,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) { LOG_ERR("ERS", "Failed during incremental section build"); section.reset(); - showPendingSyncSaveError(); + showBuildError(); return; } } @@ -1167,17 +1176,29 @@ void EpubReaderActivity::render(RenderLock&& lock) { auto p = section->loadPage(section->currentPage); if (!p) { LOG_ERR("ERS", "Failed to load page from SD - clearing section cache"); + automaticPageTurnActive = false; + // Retrying rebuilds a transiently corrupt section and usually recovers, but a page that keeps + // failing would loop forever on a blank screen, so bound the retries before giving up. + const bool giveUp = ++pageLoadRetryCount > MAX_PAGE_LOAD_RETRIES; // 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(); + if (giveUp) { + LOG_ERR("ERS", "Page load retry limit reached, aborting"); + pageLoadRetryCount = 0; // Reset so a later user-initiated navigation can try afresh + renderer.clearScreen(); + renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + showPendingSyncSaveError(); + return; + } requestUpdate(); // Try again after clearing cache - // TODO: prevent infinite loop if the page keeps failing to load for some reason - automaticPageTurnActive = false; showPendingSyncSaveError(); return; } + pageLoadRetryCount = 0; // Reset the retry counter once a page loads cleanly // Collect footnotes from the loaded page currentPageFootnotes = std::move(p->footnotes); diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 503bb23e..e9973508 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -32,6 +32,10 @@ class EpubReaderActivity final : public Activity { float pendingSpineProgress = 0.0f; bool pendingScreenshot = false; bool pendingSyncSaveError = false; + // Consecutive page-load failures. Each failure drops the section and rebuilds on the next render, + // which recovers a transiently corrupt cache; capped so a persistently bad page can't spin forever. + uint8_t pageLoadRetryCount = 0; + static constexpr uint8_t MAX_PAGE_LOAD_RETRIES = 3; bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit bool automaticPageTurnActive = false; bool showBookmarkMessage = false;