From d1ee45592e4a2e7a28ff85ec2bbcc8fe7d6388a4 Mon Sep 17 00:00:00 2001 From: cottongin Date: Sat, 7 Mar 2026 15:39:08 -0500 Subject: [PATCH] chore: remove temporary diff file Made-with: Cursor --- epub-reader-activity-diff.txt | 1486 --------------------------------- 1 file changed, 1486 deletions(-) delete mode 100644 epub-reader-activity-diff.txt diff --git a/epub-reader-activity-diff.txt b/epub-reader-activity-diff.txt deleted file mode 100644 index 62817f62..00000000 --- a/epub-reader-activity-diff.txt +++ /dev/null @@ -1,1486 +0,0 @@ -diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp -index d2af675..3868c4f 100644 ---- a/src/activities/reader/EpubReaderActivity.cpp -+++ b/src/activities/reader/EpubReaderActivity.cpp -@@ -1,33 +1,43 @@ - #include "EpubReaderActivity.h" - - #include --#include - #include - #include - #include - #include - #include -+#include - - #include "CrossPointSettings.h" - #include "CrossPointState.h" -+#include "EpubReaderBookmarkSelectionActivity.h" - #include "EpubReaderChapterSelectionActivity.h" --#include "EpubReaderFootnotesActivity.h" - #include "EpubReaderPercentSelectionActivity.h" -+#include "EndOfBookMenuActivity.h" - #include "KOReaderCredentialStore.h" - #include "KOReaderSyncActivity.h" - #include "MappedInputManager.h" --#include "QrDisplayActivity.h" - #include "RecentBooksStore.h" - #include "components/UITheme.h" - #include "fontIds.h" --#include "util/ScreenshotUtil.h" -+#include "util/BookManager.h" -+#include "util/BookmarkStore.h" -+#include "util/Dictionary.h" -+ -+extern void enterDeepSleep(); - - namespace { - // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() - constexpr unsigned long skipChapterMs = 700; - constexpr unsigned long goHomeMs = 1000; --// pages per minute, first item is 1 to prevent division by zero if accessed --const std::vector PAGE_TURN_LABELS = {1, 1, 3, 6, 12}; -+constexpr unsigned long longPressConfirmMs = 700; -+constexpr int statusBarMargin = 19; -+constexpr int progressBarMarginTop = 1; -+ -+// 8x8 1-bit hourglass icon for the indexing status bar indicator. -+// Format: MSB-first, 0 = black pixel, 1 = white pixel (e-ink convention). -+constexpr uint8_t kIndexingIcon[] = {0x00, 0x81, 0xC3, 0xE7, 0xE7, 0xC3, 0x81, 0x00}; -+constexpr int kIndexingIconSize = 8; - - int clampPercent(int percent) { - if (percent < 0) { -@@ -63,7 +73,7 @@ void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) { - } // namespace - - void EpubReaderActivity::onEnter() { -- Activity::onEnter(); -+ ActivityWithSubactivity::onEnter(); - - if (!epub) { - return; -@@ -100,6 +110,67 @@ void EpubReaderActivity::onEnter() { - } - } - -+ // Prerender covers and thumbnails on first open so Home and Sleep screens are instant. -+ // Each generate* call is a no-op if the file already exists, so this only does work once. -+ { -+ int totalSteps = 0; -+ if (!Storage.exists(epub->getCoverBmpPath(false).c_str())) totalSteps++; -+ if (!Storage.exists(epub->getCoverBmpPath(true).c_str())) totalSteps++; -+ for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) { -+ if (!Storage.exists(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]).c_str())) totalSteps++; -+ } -+ -+ if (totalSteps > 0) { -+ Rect popupRect = GUI.drawPopup(renderer, "Preparing book..."); -+ int completedSteps = 0; -+ -+ auto updateProgress = [&]() { -+ completedSteps++; -+ GUI.fillPopupProgress(renderer, popupRect, completedSteps * 100 / totalSteps); -+ }; -+ -+ if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) { -+ epub->generateCoverBmp(false); -+ // Fallback: generate placeholder if real cover extraction failed -+ if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) { -+ if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(false), epub->getTitle(), epub->getAuthor(), -+ 480, 800)) { -+ // Last resort: X-pattern marker -+ epub->generateInvalidFormatCoverBmp(false); -+ } -+ } -+ updateProgress(); -+ } -+ if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) { -+ epub->generateCoverBmp(true); -+ if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) { -+ if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(true), epub->getTitle(), epub->getAuthor(), -+ 480, 800)) { -+ // Last resort: X-pattern marker -+ epub->generateInvalidFormatCoverBmp(true); -+ } -+ } -+ updateProgress(); -+ } -+ for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) { -+ if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) { -+ epub->generateThumbBmp(PRERENDER_THUMB_HEIGHTS[i]); -+ // Fallback: generate placeholder thumbnail -+ if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) { -+ const int thumbHeight = PRERENDER_THUMB_HEIGHTS[i]; -+ const int thumbWidth = static_cast(thumbHeight * 0.6); -+ if (!PlaceholderCoverGenerator::generate(epub->getThumbBmpPath(thumbHeight), epub->getTitle(), -+ epub->getAuthor(), thumbWidth, thumbHeight)) { -+ // Last resort: X-pattern marker -+ epub->generateInvalidFormatThumbBmp(thumbHeight); -+ } -+ } -+ updateProgress(); -+ } -+ } -+ } -+ } -+ - // Save current epub as last opened epub and add to recent books - APP_STATE.openEpubPath = epub->getPath(); - APP_STATE.saveToFile(); -@@ -110,7 +181,7 @@ void EpubReaderActivity::onEnter() { - } - - void EpubReaderActivity::onExit() { -- Activity::onExit(); -+ ActivityWithSubactivity::onExit(); - - // Reset orientation back to portrait for the rest of the UI - renderer.setOrientation(GfxRenderer::Orientation::Portrait); -@@ -122,74 +193,152 @@ void EpubReaderActivity::onExit() { - } - - void EpubReaderActivity::loop() { -- if (!epub) { -- // Should never happen -- finish(); -- return; -- } -- -- if (automaticPageTurnActive) { -- if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || -- mappedInput.wasReleased(MappedInputManager::Button::Back)) { -- automaticPageTurnActive = false; -- // updates chapter title space to indicate page turn disabled -+ // Pass input responsibility to sub activity if exists -+ if (subActivity) { -+ subActivity->loop(); -+ // Deferred exit: process after subActivity->loop() returns to avoid use-after-free -+ if (pendingSubactivityExit) { -+ pendingSubactivityExit = false; -+ exitActivity(); - requestUpdate(); -+ skipNextButtonCheck = true; // Skip button processing to ignore stale events -+ } -+ // Deferred go home: process after subActivity->loop() returns to avoid race condition -+ if (pendingGoHome) { -+ pendingGoHome = false; -+ exitActivity(); -+ if (onGoHome) { -+ onGoHome(); -+ } -+ return; // Don't access 'this' after callback -+ } -+ if (pendingSleep) { -+ pendingSleep = false; -+ exitActivity(); -+ enterDeepSleep(); - return; - } -+ return; -+ } - -- if (!section) { -- requestUpdate(); -- return; -+ if (pendingSleep) { -+ pendingSleep = false; -+ enterDeepSleep(); -+ return; -+ } -+ -+ // Handle pending go home when no subactivity (e.g., from long press back) -+ if (pendingGoHome) { -+ pendingGoHome = false; -+ if (onGoHome) { -+ onGoHome(); - } -+ return; // Don't access 'this' after callback -+ } - -- // Skips page turn if renderingMutex is busy -- if (RenderLock::peek()) { -- lastPageTurnTime = millis(); -- return; -+ // Deferred end-of-book menu (set in render() to avoid deadlock) -+ if (pendingEndOfBookMenu) { -+ pendingEndOfBookMenu = false; -+ endOfBookMenuOpened = true; -+ const std::string path = epub->getPath(); -+ enterNewActivity(new EndOfBookMenuActivity( -+ renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) { -+ exitActivity(); -+ switch (action) { -+ case EndOfBookMenuActivity::Action::ARCHIVE: -+ if (epub) BookManager::archiveBook(epub->getPath()); -+ pendingGoHome = true; -+ break; -+ case EndOfBookMenuActivity::Action::DELETE: -+ if (epub) BookManager::deleteBook(epub->getPath()); -+ pendingGoHome = true; -+ break; -+ case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS: -+ endOfBookMenuOpened = false; -+ currentSpineIndex = epub->getSpineItemsCount() - 1; -+ nextPageNumber = UINT16_MAX; -+ section.reset(); -+ openChapterSelection(); -+ break; -+ case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: -+ currentSpineIndex = 0; -+ nextPageNumber = 0; -+ section.reset(); -+ endOfBookMenuOpened = false; -+ requestUpdate(); -+ break; -+ case EndOfBookMenuActivity::Action::CLOSE_BOOK: -+ pendingGoHome = true; -+ break; -+ case EndOfBookMenuActivity::Action::CLOSE_MENU: -+ currentSpineIndex = epub->getSpineItemsCount() - 1; -+ nextPageNumber = UINT16_MAX; -+ section.reset(); -+ endOfBookMenuOpened = false; -+ requestUpdate(); -+ break; -+ } -+ })); -+ return; -+ } -+ -+ // Skip button processing after returning from subactivity -+ // This prevents stale button release events from triggering actions -+ // We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared -+ if (skipNextButtonCheck) { -+ const bool confirmCleared = !mappedInput.isPressed(MappedInputManager::Button::Confirm) && -+ !mappedInput.wasReleased(MappedInputManager::Button::Confirm); -+ const bool backCleared = !mappedInput.isPressed(MappedInputManager::Button::Back) && -+ !mappedInput.wasReleased(MappedInputManager::Button::Back); -+ if (confirmCleared && backCleared) { -+ skipNextButtonCheck = false; -+ ignoreNextConfirmRelease = false; - } -+ return; -+ } - -- if ((millis() - lastPageTurnTime) >= pageTurnDuration) { -- pageTurn(true); -- return; -+ // Long press CONFIRM opens Table of Contents directly (skip menu) -+ if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= longPressConfirmMs) { -+ ignoreNextConfirmRelease = true; -+ if (epub && epub->getTocItemsCount() > 0) { -+ openChapterSelection(true); // skip the stale release from this long-press - } -+ return; - } - -- // Enter reader menu activity. -+ // Short press CONFIRM opens reader menu - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { -+ if (ignoreNextConfirmRelease) { -+ ignoreNextConfirmRelease = false; -+ return; -+ } - const int currentPage = section ? section->currentPage + 1 : 0; - const int totalPages = section ? section->pageCount : 0; - float bookProgress = 0.0f; -- if (epub->getBookSize() > 0 && section && section->pageCount > 0) { -+ if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) { - const float chapterProgress = static_cast(section->currentPage) / static_cast(section->pageCount); - bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; - } - const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); -- startActivityForResult(std::make_unique( -- renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, -- SETTINGS.orientation, !currentPageFootnotes.empty()), -- [this](const ActivityResult& result) { -- // Always apply orientation change even if the menu was cancelled -- const auto& menu = std::get(result.data); -- applyOrientation(menu.orientation); -- toggleAutoPageTurn(menu.pageTurnOption); -- if (!result.isCancelled) { -- onReaderMenuConfirm(static_cast(menu.action)); -- } -- }); -+ const bool hasDictionary = Dictionary::exists(); -+ const bool isBookmarked = -+ BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section ? section->currentPage : 0); -+ exitActivity(); -+ enterNewActivity(new EpubReaderMenuActivity( -+ this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, -+ SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(), epub->getPath(), -+ [this](const uint8_t orientation, const uint8_t fontSize) { onReaderMenuBack(orientation, fontSize); }, -+ [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); - } - - // Long press BACK (1s+) goes to file selection - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { -- activityManager.goToFileBrowser(epub ? epub->getPath() : ""); -+ onGoBack(); - return; - } - -- // Short press BACK goes directly to home (or restores position if viewing footnote) -+ // Short press BACK goes directly to home - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) { -- if (footnoteDepth > 0) { -- restoreSavedPosition(); -- return; -- } - onGoHome(); - return; - } -@@ -212,10 +361,11 @@ void EpubReaderActivity::loop() { - return; - } - -- // any botton press when at end of the book goes back to the last page -+ // any button press when at end of the book goes back to the last page - if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { - currentSpineIndex = epub->getSpineItemsCount() - 1; - nextPageNumber = UINT16_MAX; -+ endOfBookMenuOpened = false; - requestUpdate(); - return; - } -@@ -223,7 +373,6 @@ void EpubReaderActivity::loop() { - const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; - - if (skipChapter) { -- lastPageTurnTime = millis(); - // We don't want to delete the section mid-render, so grab the semaphore - { - RenderLock lock(*this); -@@ -242,12 +391,46 @@ void EpubReaderActivity::loop() { - } - - if (prevTriggered) { -- pageTurn(false); -+ if (section->currentPage > 0) { -+ section->currentPage--; -+ } else if (currentSpineIndex > 0) { -+ // We don't want to delete the section mid-render, so grab the semaphore -+ { -+ RenderLock lock(*this); -+ nextPageNumber = UINT16_MAX; -+ currentSpineIndex--; -+ section.reset(); -+ } -+ } -+ requestUpdate(); - } else { -- pageTurn(true); -+ if (section->currentPage < section->pageCount - 1) { -+ section->currentPage++; -+ } else { -+ // We don't want to delete the section mid-render, so grab the semaphore -+ { -+ RenderLock lock(*this); -+ nextPageNumber = 0; -+ currentSpineIndex++; -+ section.reset(); -+ } -+ } -+ requestUpdate(); - } - } - -+void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation, const uint8_t fontSize) { -+ exitActivity(); -+ // Apply the user-selected orientation when the menu is dismissed. -+ // This ensures the menu can be navigated without immediately rotating the screen. -+ applyOrientation(orientation); -+ // Apply font size change (no-op if unchanged). -+ applyFontSize(fontSize); -+ // Force a half refresh on the next render to clear menu/popup artifacts -+ pagesUntilFullRefresh = 1; -+ requestUpdate(); -+} -+ - // Translate an absolute percent into a spine index plus a normalized position - // within that spine so we can jump after the section is loaded. - void EpubReaderActivity::jumpToPercent(int percent) { -@@ -311,127 +494,367 @@ void EpubReaderActivity::jumpToPercent(int percent) { - } - } - -+void EpubReaderActivity::openChapterSelection(bool initialSkipRelease) { -+ const int currentP = section ? section->currentPage : 0; -+ const int totalP = section ? section->pageCount : 0; -+ const int spineIdx = currentSpineIndex; -+ const std::string path = epub->getPath(); -+ -+ enterNewActivity(new EpubReaderChapterSelectionActivity( -+ this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, -+ [this] { -+ exitActivity(); -+ requestUpdate(); -+ }, -+ [this](const int newSpineIndex) { -+ if (currentSpineIndex != newSpineIndex) { -+ currentSpineIndex = newSpineIndex; -+ nextPageNumber = 0; -+ section.reset(); -+ } -+ exitActivity(); -+ requestUpdate(); -+ }, -+ [this](const int newSpineIndex, const int newPage) { -+ if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { -+ currentSpineIndex = newSpineIndex; -+ nextPageNumber = newPage; -+ section.reset(); -+ } -+ exitActivity(); -+ requestUpdate(); -+ }, -+ initialSkipRelease)); -+} -+ - void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { - switch (action) { -- case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { -- const int spineIdx = currentSpineIndex; -- const std::string path = epub->getPath(); -- startActivityForResult( -- std::make_unique(renderer, mappedInput, epub, path, spineIdx), -- [this](const ActivityResult& result) { -- if (!result.isCancelled && currentSpineIndex != std::get(result.data).spineIndex) { -- RenderLock lock(*this); -- currentSpineIndex = std::get(result.data).spineIndex; -- nextPageNumber = 0; -+ case EpubReaderMenuActivity::MenuAction::ADD_BOOKMARK: { -+ const int page = section ? section->currentPage : 0; -+ -+ // Extract first full sentence from the current page for the bookmark snippet. -+ // If the first word is lowercase, the page starts mid-sentence — skip to the -+ // next sentence boundary and start collecting from there. -+ std::string snippet; -+ if (section) { -+ auto p = section->loadPageFromSectionFile(); -+ if (p) { -+ // Gather all words on the page into a flat list for easier traversal -+ std::vector allWords; -+ for (const auto& element : p->elements) { -+ const auto* line = static_cast(element.get()); -+ if (!line) continue; -+ const auto& block = line->getBlock(); -+ if (!block) continue; -+ for (const auto& word : block->getWords()) { -+ allWords.push_back(word); -+ } -+ } -+ -+ if (!allWords.empty()) { -+ size_t startIdx = 0; -+ -+ // Check if the first word starts with a lowercase letter (mid-sentence) -+ const char firstChar = allWords[0].empty() ? '\0' : allWords[0][0]; -+ if (firstChar >= 'a' && firstChar <= 'z') { -+ // Skip past the end of this partial sentence -+ for (size_t i = 0; i < allWords.size(); i++) { -+ if (!allWords[i].empty()) { -+ char last = allWords[i].back(); -+ if (last == '.' || last == '!' || last == '?' || last == ':') { -+ startIdx = i + 1; -+ break; -+ } -+ } -+ } -+ // If no sentence boundary found, fall back to using everything from the start -+ if (startIdx >= allWords.size()) { -+ startIdx = 0; -+ } -+ } -+ -+ // Collect words from startIdx until the next sentence boundary -+ for (size_t i = startIdx; i < allWords.size(); i++) { -+ if (!snippet.empty()) snippet += " "; -+ snippet += allWords[i]; -+ if (!allWords[i].empty()) { -+ char last = allWords[i].back(); -+ if (last == '.' || last == '!' || last == '?' || last == ':') { -+ break; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ BookmarkStore::addBookmark(epub->getCachePath(), currentSpineIndex, page, snippet); -+ { -+ RenderLock lock(*this); -+ GUI.drawPopup(renderer, tr(STR_BOOKMARK_ADDED)); -+ renderer.displayBuffer(HalDisplay::FAST_REFRESH); -+ } -+ vTaskDelay(750 / portTICK_PERIOD_MS); -+ // Exit the menu and return to reading — the bookmark indicator will show on re-render, -+ // and next menu open will reflect the updated state. -+ exitActivity(); -+ pagesUntilFullRefresh = 1; -+ requestUpdate(); -+ break; -+ } -+ case EpubReaderMenuActivity::MenuAction::REMOVE_BOOKMARK: { -+ const int page = section ? section->currentPage : 0; -+ BookmarkStore::removeBookmark(epub->getCachePath(), currentSpineIndex, page); -+ { -+ RenderLock lock(*this); -+ GUI.drawPopup(renderer, tr(STR_BOOKMARK_REMOVED)); -+ renderer.displayBuffer(HalDisplay::FAST_REFRESH); -+ } -+ vTaskDelay(750 / portTICK_PERIOD_MS); -+ exitActivity(); -+ pagesUntilFullRefresh = 1; -+ requestUpdate(); -+ break; -+ } -+ case EpubReaderMenuActivity::MenuAction::GO_TO_BOOKMARK: { -+ auto bookmarks = BookmarkStore::load(epub->getCachePath()); -+ -+ if (bookmarks.empty()) { -+ // No bookmarks: fall back to Table of Contents if available, otherwise go back -+ if (epub->getTocItemsCount() > 0) { -+ exitActivity(); -+ openChapterSelection(); -+ } -+ // If no TOC either, just return to reader (menu already closed by callback) -+ break; -+ } -+ -+ exitActivity(); -+ enterNewActivity(new EpubReaderBookmarkSelectionActivity( -+ this->renderer, this->mappedInput, epub, std::move(bookmarks), epub->getCachePath(), -+ [this] { -+ exitActivity(); -+ requestUpdate(); -+ }, -+ [this](const int newSpineIndex, const int newPage) { -+ if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { -+ currentSpineIndex = newSpineIndex; -+ nextPageNumber = newPage; - section.reset(); - } -- }); -+ exitActivity(); -+ requestUpdate(); -+ })); - break; - } -- case EpubReaderMenuActivity::MenuAction::FOOTNOTES: { -- startActivityForResult(std::make_unique(renderer, mappedInput, currentPageFootnotes), -- [this](const ActivityResult& result) { -- if (!result.isCancelled) { -- const auto& footnoteResult = std::get(result.data); -- navigateToHref(footnoteResult.href, true); -- } -- requestUpdate(); -- }); -+ case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { -+ exitActivity(); -+ openChapterSelection(); - break; - } - case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: { -+ // Launch the slider-based percent selector and return here on confirm/cancel. - float bookProgress = 0.0f; - if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) { - const float chapterProgress = static_cast(section->currentPage) / static_cast(section->pageCount); - bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; - } - const int initialPercent = clampPercent(static_cast(bookProgress + 0.5f)); -- startActivityForResult( -- std::make_unique(renderer, mappedInput, initialPercent), -- [this](const ActivityResult& result) { -- if (!result.isCancelled) { -- jumpToPercent(std::get(result.data).percent); -- } -- }); -+ exitActivity(); -+ enterNewActivity(new EpubReaderPercentSelectionActivity( -+ renderer, mappedInput, initialPercent, -+ [this](const int percent) { -+ // Apply the new position and exit back to the reader. -+ jumpToPercent(percent); -+ exitActivity(); -+ requestUpdate(); -+ }, -+ [this]() { -+ // Cancel selection and return to the reader. -+ exitActivity(); -+ requestUpdate(); -+ })); - break; - } -- case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: { -- if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) { -- auto p = section->loadPageFromSectionFile(); -- if (p) { -- std::string fullText; -- for (const auto& el : p->elements) { -- if (el->getTag() == TAG_PageLine) { -- const auto& line = static_cast(*el); -- if (line.getBlock()) { -- const auto& words = line.getBlock()->getWords(); -- for (const auto& w : words) { -- if (!fullText.empty()) fullText += " "; -- fullText += w; -- } -- } -+ case EpubReaderMenuActivity::MenuAction::LOOKUP: { -+ // Gather data we need while holding the render lock -+ int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; -+ std::unique_ptr pageForLookup; -+ int readerFontId; -+ std::string bookCachePath; -+ uint8_t currentOrientation; -+ std::string nextPageFirstWord; -+ { -+ RenderLock lock(*this); -+ -+ // Compute margins (same logic as render) -+ renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, -+ &orientedMarginLeft); -+ orientedMarginTop += SETTINGS.screenMargin; -+ orientedMarginLeft += SETTINGS.screenMargin; -+ orientedMarginRight += SETTINGS.screenMargin; -+ orientedMarginBottom += SETTINGS.screenMargin; -+ -+ if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { -+ auto metrics = UITheme::getInstance().getMetrics(); -+ const bool showProgressBar = -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; -+ orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin + -+ (showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0); -+ } -+ -+ // Load the current page -+ pageForLookup = section ? section->loadPageFromSectionFile() : nullptr; -+ readerFontId = SETTINGS.getReaderFontId(); -+ bookCachePath = epub->getCachePath(); -+ currentOrientation = SETTINGS.orientation; -+ -+ // Get first word of next page for cross-page hyphenation -+ if (section && section->currentPage < section->pageCount - 1) { -+ int savedPage = section->currentPage; -+ section->currentPage = savedPage + 1; -+ auto nextPage = section->loadPageFromSectionFile(); -+ section->currentPage = savedPage; -+ if (nextPage && !nextPage->elements.empty()) { -+ const auto* firstLine = static_cast(nextPage->elements[0].get()); -+ if (firstLine->getBlock() && !firstLine->getBlock()->getWords().empty()) { -+ nextPageFirstWord = firstLine->getBlock()->getWords().front(); - } - } -- if (!fullText.empty()) { -- startActivityForResult(std::make_unique(renderer, mappedInput, fullText), -- [this](const ActivityResult& result) {}); -- break; -- } - } - } -- // If no text or page loading failed, just close menu -- requestUpdate(); -+ // Lock released — safe to call enterNewActivity which takes its own lock -+ exitActivity(); -+ -+ if (pageForLookup) { -+ enterNewActivity(new DictionaryWordSelectActivity( -+ renderer, mappedInput, std::move(pageForLookup), readerFontId, orientedMarginLeft, orientedMarginTop, -+ bookCachePath, currentOrientation, [this]() { pendingSubactivityExit = true; }, nextPageFirstWord)); -+ } -+ break; -+ } -+ case EpubReaderMenuActivity::MenuAction::LOOKED_UP_WORDS: { -+ exitActivity(); -+ enterNewActivity(new LookedUpWordsActivity( -+ renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation, -+ [this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; }, -+ true)); // initialSkipRelease: consumed the long-press that triggered this - break; - } - case EpubReaderMenuActivity::MenuAction::GO_HOME: { -- onGoHome(); -- return; -+ // Defer go home to avoid race condition with display task -+ pendingGoHome = true; -+ break; - } - case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { - { - RenderLock lock(*this); -- if (epub && section) { -+ if (epub) { -+ // 2. BACKUP: Read current progress -+ // We use the current variables that track our position - uint16_t backupSpine = currentSpineIndex; - uint16_t backupPage = section->currentPage; - uint16_t backupPageCount = section->pageCount; -+ - section.reset(); -+ // 3. WIPE: Clear the cache directory - epub->clearCache(); -+ -+ // 4. RESTORE: Re-setup the directory and rewrite the progress file - epub->setupCacheDir(); -+ - saveProgress(backupSpine, backupPage, backupPageCount); -+ -+ // 5. Remove from recent books so the home screen doesn't show a stale/placeholder cover -+ RECENT_BOOKS.removeBook(epub->getPath()); - } - } -- onGoHome(); -- return; -+ // Defer go home to avoid race condition with display task -+ pendingGoHome = true; -+ break; - } -- case EpubReaderMenuActivity::MenuAction::SCREENSHOT: { -- { -- RenderLock lock(*this); -- pendingScreenshot = true; -+ case EpubReaderMenuActivity::MenuAction::ARCHIVE_BOOK: { -+ if (epub) { -+ BookManager::archiveBook(epub->getPath()); - } -- requestUpdate(); -+ pendingGoHome = true; -+ break; -+ } -+ case EpubReaderMenuActivity::MenuAction::DELETE_BOOK: { -+ if (epub) { -+ BookManager::deleteBook(epub->getPath()); -+ } -+ pendingGoHome = true; -+ break; -+ } -+ case EpubReaderMenuActivity::MenuAction::MANAGE_BOOK: -+ break; -+ case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK: { -+ if (epub) { -+ BookManager::reindexBook(epub->getPath(), false); -+ } -+ pendingGoHome = true; -+ break; -+ } -+ case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK_FULL: { -+ if (epub) { -+ BookManager::reindexBook(epub->getPath(), true); -+ } -+ pendingGoHome = true; - break; - } - case EpubReaderMenuActivity::MenuAction::SYNC: { - if (KOREADER_STORE.hasCredentials()) { - const int currentPage = section ? section->currentPage : 0; - const int totalPages = section ? section->pageCount : 0; -- startActivityForResult( -- std::make_unique(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, -- currentPage, totalPages), -- [this](const ActivityResult& result) { -- if (!result.isCancelled) { -- const auto& sync = std::get(result.data); -- if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { -- RenderLock lock(*this); -- currentSpineIndex = sync.spineIndex; -- nextPageNumber = sync.page; -- section.reset(); -- } -+ exitActivity(); -+ enterNewActivity(new KOReaderSyncActivity( -+ renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, -+ [this]() { -+ // On cancel - defer exit to avoid use-after-free -+ pendingSubactivityExit = true; -+ }, -+ [this](int newSpineIndex, int newPage) { -+ // On sync complete - update position and defer exit -+ if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { -+ currentSpineIndex = newSpineIndex; -+ nextPageNumber = newPage; -+ section.reset(); - } -- }); -+ pendingSubactivityExit = true; -+ })); - } - break; - } -+ case EpubReaderMenuActivity::MenuAction::PUSH_AND_SLEEP: { -+ if (KOREADER_STORE.hasCredentials()) { -+ const int cp = section ? section->currentPage : 0; -+ const int tp = section ? section->pageCount : 0; -+ exitActivity(); -+ enterNewActivity(new KOReaderSyncActivity( -+ renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, cp, tp, -+ [this]() { -+ // Push failed -- sleep anyway (silent failure) -+ pendingSleep = true; -+ }, -+ [this](int, int) { -+ // Push succeeded -- sleep -+ pendingSleep = true; -+ }, -+ KOReaderSyncActivity::SyncMode::PUSH_ONLY)); -+ } else { -+ // No credentials -- just sleep -+ pendingSleep = true; -+ } -+ break; -+ } -+ // Handled locally in the menu activity (cycle on Confirm, never dispatched here) -+ case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN: -+ case EpubReaderMenuActivity::MenuAction::TOGGLE_FONT_SIZE: -+ case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL: -+ break; - } - } - -@@ -462,63 +885,30 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) { - } - } - --void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) { -- if (selectedPageTurnOption == 0 || selectedPageTurnOption >= PAGE_TURN_LABELS.size()) { -- automaticPageTurnActive = false; -+void EpubReaderActivity::applyFontSize(const uint8_t fontSize) { -+ if (SETTINGS.fontSize == fontSize) { - return; - } - -- lastPageTurnTime = millis(); -- // calculates page turn duration by dividing by number of pages -- pageTurnDuration = (1UL * 60 * 1000) / PAGE_TURN_LABELS[selectedPageTurnOption]; -- automaticPageTurnActive = true; -- -- const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); -- // resets cached section so that space is reserved for auto page turn indicator when None or progress bar only -- if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { -- // Preserve current reading position so we can restore after reflow. -+ // Preserve current reading position so we can restore after reflow. -+ { - RenderLock lock(*this); - if (section) { - cachedSpineIndex = currentSpineIndex; - cachedChapterTotalPageCount = section->pageCount; - nextPageNumber = section->currentPage; - } -- section.reset(); -- } --} - --void EpubReaderActivity::pageTurn(bool isForwardTurn) { -- if (isForwardTurn) { -- if (section->currentPage < section->pageCount - 1) { -- section->currentPage++; -- } else { -- // We don't want to delete the section mid-render, so grab the semaphore -- { -- RenderLock lock(*this); -- nextPageNumber = 0; -- currentSpineIndex++; -- section.reset(); -- } -- } -- } else { -- if (section->currentPage > 0) { -- section->currentPage--; -- } else if (currentSpineIndex > 0) { -- // We don't want to delete the section mid-render, so grab the semaphore -- { -- RenderLock lock(*this); -- nextPageNumber = UINT16_MAX; -- currentSpineIndex--; -- section.reset(); -- } -- } -+ SETTINGS.fontSize = fontSize; -+ SETTINGS.saveToFile(); -+ -+ // Reset section to force re-layout with the new font size. -+ section.reset(); - } -- lastPageTurnTime = millis(); -- requestUpdate(); - } - - // TODO: Failure handling --void EpubReaderActivity::render(RenderLock&& lock) { -+void EpubReaderActivity::render(Activity::RenderLock&& lock) { - if (!epub) { - return; - } -@@ -532,12 +922,11 @@ void EpubReaderActivity::render(RenderLock&& lock) { - currentSpineIndex = epub->getSpineItemsCount(); - } - -- // Show end of book screen -+ // End of book — defer menu creation to loop() to avoid deadlock (render holds the lock) - if (currentSpineIndex == epub->getSpineItemsCount()) { -- renderer.clearScreen(); -- renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); -- renderer.displayBuffer(); -- automaticPageTurnActive = false; -+ if (!endOfBookMenuOpened) { -+ pendingEndOfBookMenu = true; -+ } - return; - } - -@@ -548,41 +937,44 @@ void EpubReaderActivity::render(RenderLock&& lock) { - orientedMarginTop += SETTINGS.screenMargin; - orientedMarginLeft += SETTINGS.screenMargin; - orientedMarginRight += SETTINGS.screenMargin; -- -- const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); -- -- // reserves space for automatic page turn indicator when no status bar or progress bar only -- if (automaticPageTurnActive && -- (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight())) { -- orientedMarginBottom += -- std::max(SETTINGS.screenMargin, -- static_cast(statusBarHeight + UITheme::getInstance().getMetrics().statusBarVerticalMargin)); -- } else { -- orientedMarginBottom += std::max(SETTINGS.screenMargin, statusBarHeight); -+ orientedMarginBottom += SETTINGS.screenMargin; -+ -+ auto metrics = UITheme::getInstance().getMetrics(); -+ -+ // Add status bar margin -+ if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { -+ // Add additional margin for status bar if progress bar is shown -+ const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; -+ orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin + -+ (showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0); - } - -+ const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; -+ const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; -+ - if (!section) { -+ loadingSection = true; -+ preIndexedNextSpine = -1; -+ - const auto filepath = epub->getSpineItem(currentSpineIndex).href; - LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex); - section = std::unique_ptr
(new Section(epub, currentSpineIndex, renderer)); - -- const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; -- const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; -- - if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, -- viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, -- SETTINGS.imageRendering)) { -+ viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { - LOG_DBG("ERS", "Cache not found, building..."); - - 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, popupFn)) { -+ viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, popupFn)) { - LOG_ERR("ERS", "Failed to persist page data to SD"); - section.reset(); -+ loadingSection = false; - return; - } - } else { -@@ -595,16 +987,6 @@ void EpubReaderActivity::render(RenderLock&& lock) { - section->currentPage = nextPageNumber; - } - -- if (!pendingAnchor.empty()) { -- if (const auto page = section->getPageForAnchor(pendingAnchor)) { -- section->currentPage = *page; -- LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page); -- } else { -- LOG_DBG("ERS", "Anchor '%s' not found in section %d", pendingAnchor.c_str(), currentSpineIndex); -- } -- 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 -@@ -625,6 +1007,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { - section->currentPage = newPage; - pendingPercentJump = false; - } -+ -+ loadingSection = false; - } - - renderer.clearScreen(); -@@ -632,18 +1016,16 @@ void EpubReaderActivity::render(RenderLock&& lock) { - if (section->pageCount == 0) { - LOG_DBG("ERS", "No pages to render"); - renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD); -- renderStatusBar(); -+ renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); - renderer.displayBuffer(); -- automaticPageTurnActive = false; - return; - } - - if (section->currentPage < 0 || section->currentPage >= section->pageCount) { - LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount); - renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD); -- renderStatusBar(); -+ renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); - renderer.displayBuffer(); -- automaticPageTurnActive = false; - return; - } - -@@ -653,26 +1035,81 @@ void EpubReaderActivity::render(RenderLock&& lock) { - LOG_ERR("ERS", "Failed to load page from SD - clearing section cache"); - section->clearCache(); - section.reset(); -+ silentIndexingActive = false; - requestUpdate(); // Try again after clearing cache -- // TODO: prevent infinite loop if the page keeps failing to load for some reason -- automaticPageTurnActive = false; -+ // TODO: prevent infinite loop if the page keeps failing to load for some reason - return; - } - -- // Collect footnotes from the loaded page -- currentPageFootnotes = std::move(p->footnotes); -+ silentIndexingActive = false; -+ const bool textOnlyPage = !p->hasImages(); -+ if (textOnlyPage && SETTINGS.indexingDisplay != CrossPointSettings::INDEXING_DISPLAY::INDEXING_POPUP && -+ section->pageCount >= 1 && -+ ((section->pageCount == 1 && section->currentPage == 0) || -+ (section->pageCount >= 2 && section->currentPage == section->pageCount - 2)) && -+ currentSpineIndex + 1 < epub->getSpineItemsCount() && preIndexedNextSpine != currentSpineIndex + 1) { -+ Section probe(epub, currentSpineIndex + 1, renderer); -+ if (probe.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), -+ SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, -+ viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { -+ preIndexedNextSpine = currentSpineIndex + 1; -+ } else { -+ silentIndexingActive = true; -+ } -+ } - - const auto start = millis(); - renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); - LOG_DBG("ERS", "Rendered page in %dms", millis() - start); - renderer.clearFontCache(); -+ -+ if (silentIndexingActive) { -+ silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight); -+ requestUpdate(); -+ } - } - saveProgress(currentSpineIndex, section->currentPage, section->pageCount); -+} -+ -+bool EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportWidth, const uint16_t viewportHeight) { -+ if (preIndexedNextSpine == currentSpineIndex + 1) { -+ silentIndexingActive = false; -+ return false; -+ } -+ -+ const bool shouldPreIndex = (section->pageCount == 1 && section->currentPage == 0) || -+ (section->pageCount >= 2 && section->currentPage == section->pageCount - 2); -+ if (!epub || !section || !shouldPreIndex) { -+ silentIndexingActive = false; -+ return false; -+ } - -- if (pendingScreenshot) { -- pendingScreenshot = false; -- ScreenshotUtil::takeScreenshot(renderer); -+ const int nextSpineIndex = currentSpineIndex + 1; -+ if (nextSpineIndex < 0 || nextSpineIndex >= epub->getSpineItemsCount()) { -+ silentIndexingActive = false; -+ return false; - } -+ -+ Section nextSection(epub, nextSpineIndex, renderer); -+ if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), -+ SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, -+ viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { -+ preIndexedNextSpine = nextSpineIndex; -+ silentIndexingActive = false; -+ return false; -+ } -+ -+ 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)) { -+ LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex); -+ silentIndexingActive = false; -+ return false; -+ } -+ preIndexedNextSpine = nextSpineIndex; -+ silentIndexingActive = false; -+ return true; - } - - void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) { -@@ -698,22 +1135,43 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or - // Force special handling for pages with images when anti-aliasing is on - bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing; - -+ if (page->countUncachedImages() > 0) { -+ page->renderTextOnly(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); -+ page->renderImagePlaceholders(renderer, orientedMarginLeft, orientedMarginTop); -+ renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); -+ renderer.displayBuffer(); -+ renderer.clearScreen(); -+ } -+ - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); -- renderStatusBar(); -+ -+ // Draw bookmark ribbon indicator in top-right corner if current page is bookmarked -+ if (section && BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage)) { -+ const int screenWidth = renderer.getScreenWidth(); -+ const int bkWidth = 12; -+ const int bkHeight = 22; -+ const int bkX = screenWidth - orientedMarginRight - bkWidth + 2; -+ const int bkY = 0; -+ const int notchDepth = bkHeight / 3; -+ const int centerX = bkX + bkWidth / 2; -+ -+ const int xPoints[5] = {bkX, bkX + bkWidth, bkX + bkWidth, centerX, bkX}; -+ const int yPoints[5] = {bkY, bkY, bkY + bkHeight, bkY + bkHeight - notchDepth, bkY + bkHeight}; -+ renderer.fillPolygon(xPoints, yPoints, 5, true); -+ } -+ -+ renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); - if (imagePageWithAA) { - // Double FAST_REFRESH with selective image blanking (pablohc's technique): - // HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust. - // Instead, blank only the image area and do two fast refreshes. -- // Step 1: Display page with image area blanked (text appears, image area white) -- // Step 2: Re-render with images and display again (images appear clean) -- int16_t imgX, imgY, imgW, imgH; -+ int imgX, imgY, imgW, imgH; - if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) { - renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); - -- // Re-render page content to restore images into the blanked area - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); -- renderStatusBar(); -+ renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); - } else { - renderer.displayBuffer(HalDisplay::HALF_REFRESH); -@@ -753,98 +1211,121 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or - renderer.restoreBwBuffer(); - } - --void EpubReaderActivity::renderStatusBar() const { -+void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, -+ const int orientedMarginLeft) const { -+ auto metrics = UITheme::getInstance().getMetrics(); -+ -+ // determine visible status bar elements -+ const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; -+ const bool showBookProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR; -+ const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; -+ const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR; -+ const bool showBookPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; -+ const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; -+ const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR || -+ SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; -+ const bool showBatteryPercentage = -+ SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; -+ -+ // Position status bar near the bottom of the logical screen, regardless of orientation -+ const auto screenHeight = renderer.getScreenHeight(); -+ const auto textY = screenHeight - orientedMarginBottom - 4; -+ int progressTextWidth = 0; -+ - // Calculate progress in book -- const int currentPage = section->currentPage + 1; -- const float pageCount = section->pageCount; -- const float sectionChapterProg = (pageCount > 0) ? (static_cast(currentPage) / pageCount) : 0; -+ const float sectionChapterProg = static_cast(section->currentPage) / section->pageCount; - const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100; - -- std::string title; -- -- int textYOffset = 0; -- -- if (automaticPageTurnActive) { -- title = tr(STR_AUTO_TURN_ENABLED) + std::to_string(60 * 1000 / pageTurnDuration); -- -- // calculates textYOffset when rendering title in status bar -- const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); -+ if (showProgressText || showProgressPercentage || showBookPercentage) { -+ // Right aligned text for progress counter -+ char progressStr[32]; - -- // offsets text if no status bar or progress bar only -- if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { -- textYOffset += UITheme::getInstance().getMetrics().statusBarVerticalMargin; -- } -- -- } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { -- title = tr(STR_UNNAMED); -- const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); -- if (tocIndex != -1) { -- const auto tocItem = epub->getTocItem(tocIndex); -- title = tocItem.title; -+ // Hide percentage when progress bar is shown to reduce clutter -+ if (showProgressPercentage) { -+ snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount, -+ bookProgress); -+ } else if (showBookPercentage) { -+ snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress); -+ } else { -+ snprintf(progressStr, sizeof(progressStr), "%d/%d", section->currentPage + 1, section->pageCount); - } - -- } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::BOOK_TITLE) { -- title = epub->getTitle(); -+ progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr); -+ renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, -+ progressStr); - } - -- GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset); --} -- --void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { -- if (!epub) return; -+ if (showBookProgressBar) { -+ // Draw progress bar at the very bottom of the screen, from edge to edge of viewable area -+ GUI.drawReadingProgressBar(renderer, static_cast(bookProgress)); -+ } - -- // Push current position onto saved stack -- if (savePosition && section && footnoteDepth < MAX_FOOTNOTE_DEPTH) { -- savedPositions[footnoteDepth] = {currentSpineIndex, section->currentPage}; -- footnoteDepth++; -- LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage); -+ if (showChapterProgressBar) { -+ // Draw chapter progress bar at the very bottom of the screen, from edge to edge of viewable area -+ const float chapterProgress = -+ (section->pageCount > 0) ? (static_cast(section->currentPage + 1) / section->pageCount) * 100 : 0; -+ GUI.drawReadingProgressBar(renderer, static_cast(chapterProgress)); - } - -- // Extract fragment anchor (e.g. "#note1" or "chapter2.xhtml#note1") -- std::string anchor; -- const auto hashPos = hrefStr.find('#'); -- if (hashPos != std::string::npos && hashPos + 1 < hrefStr.size()) { -- anchor = hrefStr.substr(hashPos + 1); -+ if (showBattery) { -+ GUI.drawBatteryLeft(renderer, Rect{orientedMarginLeft + 1, textY, metrics.batteryWidth, metrics.batteryHeight}, -+ showBatteryPercentage); - } - -- // Check for same-file anchor reference (#anchor only) -- bool sameFile = !hrefStr.empty() && hrefStr[0] == '#'; -+ if (showChapterTitle) { -+ // Centered chatper title text -+ // Page width minus existing content with 30px padding on each side -+ const int rendererableScreenWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; - -- int targetSpineIndex; -- if (sameFile) { -- targetSpineIndex = currentSpineIndex; -- } else { -- targetSpineIndex = epub->resolveHrefToSpineIndex(hrefStr); -- } -+ const int batterySize = showBattery ? (showBatteryPercentage ? 50 : 20) : 0; -+ const int titleMarginLeft = batterySize + 30; -+ const int titleMarginRight = progressTextWidth + 30; - -- if (targetSpineIndex < 0) { -- LOG_DBG("ERS", "Could not resolve href: %s", hrefStr.c_str()); -- if (savePosition && footnoteDepth > 0) footnoteDepth--; // undo push -- return; -- } -+ // Attempt to center title on the screen, but if title is too wide then later we will center it within the -+ // available space. -+ int titleMarginLeftAdjusted = std::max(titleMarginLeft, titleMarginRight); -+ int availableTitleSpace = rendererableScreenWidth - 2 * titleMarginLeftAdjusted; -+ const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); - -- { -- RenderLock lock(*this); -- pendingAnchor = std::move(anchor); -- currentSpineIndex = targetSpineIndex; -- nextPageNumber = 0; -- section.reset(); -- } -- requestUpdate(); -- LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, hrefStr.c_str()); --} -+ std::string title; -+ int titleWidth; -+ if (tocIndex == -1) { -+ title = tr(STR_UNNAMED); -+ titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); -+ } else { -+ const auto tocItem = epub->getTocItem(tocIndex); -+ title = tocItem.title; -+ titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); -+ if (titleWidth > availableTitleSpace) { -+ // Not enough space to center on the screen, center it within the remaining space instead -+ availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight; -+ titleMarginLeftAdjusted = titleMarginLeft; -+ } -+ if (titleWidth > availableTitleSpace) { -+ title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace); -+ titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); -+ } -+ } - --void EpubReaderActivity::restoreSavedPosition() { -- if (footnoteDepth <= 0) return; -- footnoteDepth--; -- const auto& pos = savedPositions[footnoteDepth]; -- LOG_DBG("ERS", "Restoring position [%d]: spine %d, page %d", footnoteDepth, pos.spineIndex, pos.pageNumber); -+ renderer.drawText(SMALL_FONT_ID, -+ titleMarginLeftAdjusted + orientedMarginLeft + (availableTitleSpace - titleWidth) / 2, textY, -+ title.c_str()); -+ } - -- { -- RenderLock lock(*this); -- currentSpineIndex = pos.spineIndex; -- nextPageNumber = pos.pageNumber; -- section.reset(); -+ if (silentIndexingActive && SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { -+ const int batteryWidth = showBattery ? (showBatteryPercentage ? 50 : 20) : 0; -+ const int indicatorX = orientedMarginLeft + batteryWidth + 8; -+ if (SETTINGS.indexingDisplay == CrossPointSettings::INDEXING_DISPLAY::INDEXING_STATUS_TEXT) { -+ renderer.drawText(SMALL_FONT_ID, indicatorX, textY, tr(STR_INDEXING)); -+ } else if (SETTINGS.indexingDisplay == CrossPointSettings::INDEXING_DISPLAY::INDEXING_STATUS_ICON) { -+ renderer.drawIcon(kIndexingIcon, indicatorX, textY - kIndexingIconSize + 2, kIndexingIconSize, kIndexingIconSize); -+ } - } -- requestUpdate(); - } -diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h -index 316677b..a8232ac 100644 ---- a/src/activities/reader/EpubReaderActivity.h -+++ b/src/activities/reader/EpubReaderActivity.h -@@ -1,64 +1,68 @@ - #pragma once - #include --#include - #include - -+#include "DictionaryWordSelectActivity.h" - #include "EpubReaderMenuActivity.h" --#include "activities/Activity.h" -+#include "LookedUpWordsActivity.h" -+#include "activities/ActivityWithSubactivity.h" - --class EpubReaderActivity final : public Activity { -+class EpubReaderActivity final : public ActivityWithSubactivity { - std::shared_ptr epub; - std::unique_ptr
section = nullptr; - int currentSpineIndex = 0; - int nextPageNumber = 0; -- // 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 skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit -- bool automaticPageTurnActive = false; -- -- // 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; -+ bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free -+ bool pendingGoHome = false; // Defer go home to avoid race condition with display task -+ bool pendingSleep = false; // Defer deep sleep until after push-and-sleep completes -+ bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit -+ bool ignoreNextConfirmRelease = false; // Suppress short-press after long-press Confirm -+ volatile bool loadingSection = false; // True during the entire !section block (read from main loop) -+ bool silentIndexingActive = false; // True while silently pre-indexing the next chapter -+ int preIndexedNextSpine = -1; // Spine index already pre-indexed (prevents re-render loop) -+ bool endOfBookMenuOpened = false; // Guard to prevent repeated opening of EndOfBookMenuActivity -+ bool pendingEndOfBookMenu = false; // Deferred: open EndOfBookMenuActivity from loop(), not render() -+ const std::function onGoBack; -+ const std::function onGoHome; - - void renderContents(std::unique_ptr page, int orientedMarginTop, int orientedMarginRight, - int orientedMarginBottom, int orientedMarginLeft); -- void renderStatusBar() const; -+ void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; -+ bool silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight); - void 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); -+ // Open the Table of Contents (chapter selection) as a subactivity. -+ // Pass initialSkipRelease=true when triggered by long-press to consume the stale release. -+ void openChapterSelection(bool initialSkipRelease = false); -+ void onReaderMenuBack(uint8_t orientation, uint8_t fontSize); - void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); - void applyOrientation(uint8_t orientation); -- void toggleAutoPageTurn(uint8_t selectedPageTurnOption); -- void pageTurn(bool isForwardTurn); -- -- // Footnote navigation -- void navigateToHref(const std::string& href, bool savePosition = false); -- void restoreSavedPosition(); -+ void applyFontSize(uint8_t fontSize); - - public: -- explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub) -- : Activity("EpubReader", renderer, mappedInput), epub(std::move(epub)) {} -+ explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, -+ const std::function& onGoBack, const std::function& onGoHome) -+ : ActivityWithSubactivity("EpubReader", renderer, mappedInput), -+ epub(std::move(epub)), -+ onGoBack(onGoBack), -+ onGoHome(onGoHome) {} - void onEnter() override; - void onExit() override; - void loop() override; -- void render(RenderLock&& lock) override; -- bool isReaderActivity() const override { return true; } -+ void render(Activity::RenderLock&& lock) override; -+ // Defer low-power mode and auto-sleep while a section is loading/building. -+ // !section covers the period before the Section object is created (including -+ // cover prerendering in onEnter). loadingSection covers the full !section block -+ // in render (including createSectionFile), during which section is non-null -+ // but the section file is still being built. -+ bool preventAutoSleep() override { return !section || loadingSection; } - };