1487 lines
64 KiB
Plaintext
1487 lines
64 KiB
Plaintext
|
|
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 <Epub/Page.h>
|
||
|
|
-#include <Epub/blocks/TextBlock.h>
|
||
|
|
#include <FsHelpers.h>
|
||
|
|
#include <GfxRenderer.h>
|
||
|
|
#include <HalStorage.h>
|
||
|
|
#include <I18n.h>
|
||
|
|
#include <Logging.h>
|
||
|
|
+#include <PlaceholderCoverGenerator.h>
|
||
|
|
|
||
|
|
#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<int> 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<int>(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<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||
|
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||
|
|
}
|
||
|
|
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||
|
|
- startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||
|
|
- 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<MenuResult>(result.data);
|
||
|
|
- applyOrientation(menu.orientation);
|
||
|
|
- toggleAutoPageTurn(menu.pageTurnOption);
|
||
|
|
- if (!result.isCancelled) {
|
||
|
|
- onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(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<EpubReaderChapterSelectionActivity>(renderer, mappedInput, epub, path, spineIdx),
|
||
|
|
- [this](const ActivityResult& result) {
|
||
|
|
- if (!result.isCancelled && currentSpineIndex != std::get<ChapterResult>(result.data).spineIndex) {
|
||
|
|
- RenderLock lock(*this);
|
||
|
|
- currentSpineIndex = std::get<ChapterResult>(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<std::string> allWords;
|
||
|
|
+ for (const auto& element : p->elements) {
|
||
|
|
+ const auto* line = static_cast<const PageLine*>(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<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
|
||
|
|
- [this](const ActivityResult& result) {
|
||
|
|
- if (!result.isCancelled) {
|
||
|
|
- const auto& footnoteResult = std::get<FootnoteResult>(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<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||
|
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||
|
|
}
|
||
|
|
const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||
|
|
- startActivityForResult(
|
||
|
|
- std::make_unique<EpubReaderPercentSelectionActivity>(renderer, mappedInput, initialPercent),
|
||
|
|
- [this](const ActivityResult& result) {
|
||
|
|
- if (!result.isCancelled) {
|
||
|
|
- jumpToPercent(std::get<PercentResult>(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<const PageLine&>(*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<Page> 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<const PageLine*>(nextPage->elements[0].get());
|
||
|
|
+ if (firstLine->getBlock() && !firstLine->getBlock()->getWords().empty()) {
|
||
|
|
+ nextPageFirstWord = firstLine->getBlock()->getWords().front();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
- if (!fullText.empty()) {
|
||
|
|
- startActivityForResult(std::make_unique<QrDisplayActivity>(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<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex,
|
||
|
|
- currentPage, totalPages),
|
||
|
|
- [this](const ActivityResult& result) {
|
||
|
|
- if (!result.isCancelled) {
|
||
|
|
- const auto& sync = std::get<SyncResult>(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<uint8_t>(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<Section>(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> 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> 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<float>(currentPage) / pageCount) : 0;
|
||
|
|
+ const float sectionChapterProg = static_cast<float>(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<size_t>(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<float>(section->currentPage + 1) / section->pageCount) * 100 : 0;
|
||
|
|
+ GUI.drawReadingProgressBar(renderer, static_cast<size_t>(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 <Epub.h>
|
||
|
|
-#include <Epub/FootnoteEntry.h>
|
||
|
|
#include <Epub/Section.h>
|
||
|
|
|
||
|
|
+#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> epub;
|
||
|
|
std::unique_ptr<Section> 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<FootnoteEntry> 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<void()> onGoBack;
|
||
|
|
+ const std::function<void()> onGoHome;
|
||
|
|
|
||
|
|
void renderContents(std::unique_ptr<Page> 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> epub)
|
||
|
|
- : Activity("EpubReader", renderer, mappedInput), epub(std::move(epub)) {}
|
||
|
|
+ explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
|
||
|
|
+ const std::function<void()>& onGoBack, const std::function<void()>& 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; }
|
||
|
|
};
|