Previously the status bar showed 'BT connecting' until the next page turn. Now monitors connection state in loop() and redraws the status bar immediately when connection completes or disconnects, restoring the chapter/book title. The connecting message now takes over the entire title slot instead of prepending to it.
195 lines
11 KiB
C++
195 lines
11 KiB
C++
#pragma once
|
|
#include <Epub.h>
|
|
#include <Epub/FootnoteEntry.h>
|
|
#include <Epub/Section.h>
|
|
|
|
#include <optional>
|
|
|
|
#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> epub;
|
|
std::unique_ptr<Section> section = nullptr;
|
|
int currentSpineIndex = 0;
|
|
int nextPageNumber = 0;
|
|
std::optional<uint16_t> 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<BookmarkEntry> 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<FootnoteEntry> 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> 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> 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;
|
|
};
|