#pragma once #include #include #include #include #include "BookmarkEntry.h" #include "EndOfBookOptions.h" #include "EpubReaderMenuActivity.h" #include "ProgressMapper.h" #include "activities/Activity.h" class EpubReaderActivity final : public Activity { std::shared_ptr epub; std::unique_ptr
section = nullptr; int currentSpineIndex = 0; int nextPageNumber = 0; std::optional pendingPageJump; // Set when navigating to a footnote href with a fragment (e.g. #note1). // Cleared on the next render after the new section loads and resolves it to a page. std::string pendingAnchor; int pagesUntilFullRefresh = 0; int cachedSpineIndex = 0; int cachedChapterTotalPageCount = 0; unsigned long lastPageTurnTime = 0UL; unsigned long pageTurnDuration = 0UL; // Signals that the next render should reposition within the newly loaded section // based on a cross-book percentage jump. bool pendingPercentJump = false; // Normalized 0.0-1.0 progress within the target spine item, computed from book percentage. 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; bool ignoreNextConfirmRelease = false; bool currentPageBookmarked = false; // BLE connection state the status bar last rendered with; loop() watches for // a flip and redraws the bar so the "BT connecting" placeholder swaps back // to the chapter/book title the moment the connection completes (and // returns on disconnect) instead of waiting for the next page turn. bool statusBarBleConnected = false; bool bookmarkRemoved = false; // true when last toggle removed (controls popup text) std::vector cachedBookmarks; // Tracks whether this book is currently removed from Recent Books by the // removeReadBooksFromRecents feature (set at End-of-Book, cleared if paged back in). bool recentsEntryRemoved = false; unsigned long bookmarkMessageTime = 0UL; // Set when the reader is left at end-of-book and SETTINGS.moveFinishedToReadFolder is on. // Consumed in onExit() to relocate the finished book into /Read/. bool pendingReadFolderMove = false; // Next-book suggestion menu for the End-of-Book screen EndOfBookOptions endOfBookOptions; // Footnote support std::vector currentPageFootnotes; struct SavedPosition { int spineIndex; int pageNumber; }; static constexpr int MAX_FOOTNOTE_DEPTH = 3; SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {}; int footnoteDepth = 0; // Heap floor for entering a section build. The layout code allocates freely (line-break DP // arrays sized by word count, CSS rule lookups, glyph buffers) and under -fno-exceptions an // OOM there abort()s the firmware instead of failing cleanly -- so a starved heap must be // handled *before* the build, not after. Field data: builds succeed at ~46 KB free with BLE // resident; abort() observed at ~11 KB free. CSS styling already degrades below 48 KB // (MIN_FREE_HEAP_FOR_CSS), so 40 KB trades a few early BLE teardowns for not crashing. static constexpr size_t BUILD_MIN_FREE_HEAP = 40 * 1024; // Heap floor for rendering a page at all. Page deserialization (TextBlock word // vectors/strings) and glyph caching allocate through throwing paths that abort() // on OOM; below this floor render() sheds the BLE stack (~52 KB back, and it // restores a large contiguous block) before touching the page. Field data: a // session with no shed ground to <2.2 KB free and aborted on a page load. static constexpr size_t RENDER_MIN_FREE_HEAP = 24 * 1024; // Viewport of the last render(), captured so loop()'s lazy partial-extension start // builds with IDENTICAL layout parameters to the pages already rendered (a mismatch // would paginate differently than the partial being extended). 0 = no render yet. uint16_t buildViewportWidth = 0; uint16_t buildViewportHeight = 0; // Set when the lazy extension start failed, so loop() doesn't retry (and log) every // tick; the blocking extension in render() remains the fallback past the watermark. bool partialRebuildStartFailed = false; // Last position persisted by render()'s saveProgress, used to skip redundant // writeAtomic calls on no-op re-renders (menu/bookmark/screenshot). int lastSavedSpineIndex = -1; int lastSavedPage = -1; int lastSavedPageCount = -1; void renderContents(std::unique_ptr page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar() const; // 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; // Skip background build ticks below this free-heap floor. The parse path grows // word vectors of heap strings — throwing allocations that abort() on OOM under // -fno-exceptions (field crash: bad_alloc in ParsedText::addWord during a // background tick with the BLE stack resident). The tick is deferrable work: // page-turn transients free up between turns and the build resumes; the render // path still builds the page it actually needs regardless of this floor. // Calibrated BETWEEN the measured states: steady reading with BLE resident runs at // ~29.4 KB free / ~16.4 KB largest block (ticks are safe there — a 2-page parse // transient is a few KB), while the field crash happened at 34.7 KB free with an // ~11 KB largest block. A first cut at 32 KB/16 KB sat just ABOVE the healthy // steady state, guaranteeing a pointless BLE shed the moment any build work was // pending (the maxAlloc floor fired on a 12-byte shortfall). static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 26 * 1024; // Fragmentation floor for the same gate: free heap says how much memory exists; // maxAlloc says whether any single allocation can actually have it. static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 13 * 1024; // Gate for a background build tick: true when the heap can take parse allocations. // When BLE is what's squeezing the heap, sheds it (build-pending deferral in the // lifecycle then holds restarts off until the window is caught up) instead of // stalling the build forever below the floors. bool buildTickHeapGate(); // 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; // Reopening a partial does NOT immediately restart its extension build (a whole-chapter // re-layout from page 0 -- minutes of background CPU + SD writes on a giant spine, wasted // when the reader never crosses the watermark that session). Instead loop() starts it once // the reader is within this many pages of the watermark: at ~30s per page read and ~100-300ms // per page rebuilt, this margin gives the rebuild ample runway to catch up (and finalize) // before the reader arrives. static constexpr int PARTIAL_REBUILD_START_MARGIN = 15; // 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); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); // Opens the reader menu for the current position (short-press Confirm) void openReaderMenu(); // Returns true if sync acted (launched, or surfaced a save error); false if it was a no-op // because no KOReader credentials are stored. bool launchKOReaderSync(); void applyOrientation(uint8_t orientation); void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void pageTurn(bool isForwardTurn); void loadCachedBookmarks(); void addBookmark(); void updateBookmarkFlag(); // Footnote navigation void navigateToHref(const std::string& href, bool savePosition = false); void restoreSavedPosition(); public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub) : Activity("EpubReader", renderer, mappedInput), epub(std::move(epub)) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&& lock) override; // Full CPU speed + fast loop ticks while a section build runs: at the low-power // frequency a giant chapter's background rebuild stretches from ~40s to many // minutes, so the reader exits before it can finalize and the next open restarts // it from page 0. Reverts to normal power behavior the moment the build finishes. bool skipLoopDelay() override { return section && section->isBuilding(); } bool isReaderActivity() const override { return true; } void requestGhostCleanup() override { pagesUntilFullRefresh = 1; } ScreenshotInfo getScreenshotInfo() const override; CrossPointPosition getCurrentPosition() const; };