From 245d5a7dd84902932c881a7a23adccbb0a305312 Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 28 Jan 2026 02:20:29 -0500 Subject: [PATCH] feat: Integrate bookmark support into reader activities Adds bookmark add/remove functionality to EpubReaderActivity and base ReaderActivity, with visual indicator for bookmarked pages. --- src/activities/reader/EpubReaderActivity.cpp | 163 +++++++++++++++++++ src/activities/reader/EpubReaderActivity.h | 10 +- src/activities/reader/ReaderActivity.cpp | 6 +- src/activities/reader/ReaderActivity.h | 10 +- src/activities/reader/TxtReaderActivity.h | 10 +- 5 files changed, 192 insertions(+), 7 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 1082d93..7bbc4fb 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -8,6 +8,7 @@ #include #include "BookManager.h" +#include "BookmarkStore.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "EpubReaderChapterSelectionActivity.h" @@ -17,6 +18,7 @@ #include "activities/dictionary/DictionaryMenuActivity.h" #include "activities/dictionary/DictionarySearchActivity.h" #include "activities/dictionary/EpubWordSelectionActivity.h" +#include "activities/util/QuickMenuActivity.h" #include "fontIds.h" namespace { @@ -366,6 +368,149 @@ void EpubReaderActivity::loop() { return; } + // Quick Menu power button press + if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::QUICK_MENU && + mappedInput.wasReleased(MappedInputManager::Button::Power)) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + + // Check if current page is bookmarked + bool isBookmarked = false; + if (section) { + const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage); + isBookmarked = BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset); + } + + exitActivity(); + enterNewActivity(new QuickMenuActivity( + renderer, mappedInput, + [this](QuickMenuAction action) { + // Cache values before exitActivity + EpubReaderActivity* self = this; + GfxRenderer& cachedRenderer = renderer; + MappedInputManager& cachedMappedInput = mappedInput; + Section* cachedSection = section.get(); + SemaphoreHandle_t cachedMutex = renderingMutex; + + exitActivity(); + + if (action == QuickMenuAction::DICTIONARY) { + // Open dictionary menu + self->enterNewActivity(new DictionaryMenuActivity( + cachedRenderer, cachedMappedInput, + [self](DictionaryMode mode) { + GfxRenderer& r = self->renderer; + MappedInputManager& m = self->mappedInput; + Section* s = self->section.get(); + SemaphoreHandle_t mtx = self->renderingMutex; + + self->exitActivity(); + + if (mode == DictionaryMode::ENTER_WORD) { + self->enterNewActivity(new DictionarySearchActivity(r, m, + [self]() { + self->exitActivity(); + self->updateRequired = true; + }, "")); + } else if (s) { + xSemaphoreTake(mtx, portMAX_DELAY); + auto page = s->loadPageFromSectionFile(); + if (page) { + int mt, mr, mb, ml; + r.getOrientedViewableTRBL(&mt, &mr, &mb, &ml); + mt += SETTINGS.screenMargin; + ml += SETTINGS.screenMargin; + const int fontId = SETTINGS.getReaderFontId(); + + self->enterNewActivity(new EpubWordSelectionActivity( + r, m, std::move(page), fontId, ml, mt, + [self](const std::string& word) { + self->exitActivity(); + self->enterNewActivity(new DictionarySearchActivity( + self->renderer, self->mappedInput, + [self]() { + self->exitActivity(); + self->updateRequired = true; + }, word)); + }, + [self]() { + self->exitActivity(); + self->updateRequired = true; + })); + xSemaphoreGive(mtx); + } else { + xSemaphoreGive(mtx); + self->updateRequired = true; + } + } else { + self->updateRequired = true; + } + }, + [self]() { + self->exitActivity(); + self->updateRequired = true; + }, + self->section != nullptr)); + } else if (action == QuickMenuAction::ADD_BOOKMARK) { + // Toggle bookmark on current page + if (self->section) { + const uint32_t contentOffset = self->section->getContentOffsetForPage(self->section->currentPage); + const std::string& bookPath = self->epub->getPath(); + + if (BookmarkStore::isPageBookmarked(bookPath, self->currentSpineIndex, contentOffset)) { + // Remove bookmark + BookmarkStore::removeBookmark(bookPath, self->currentSpineIndex, contentOffset); + } else { + // Add bookmark with auto-generated name + Bookmark bm; + bm.spineIndex = self->currentSpineIndex; + bm.contentOffset = contentOffset; + bm.pageNumber = self->section->currentPage; + bm.timestamp = millis() / 1000; // Approximate timestamp + + // Generate name: "Chapter - Page X" or fallback + std::string chapterTitle; + const int tocIndex = self->epub->getTocIndexForSpineIndex(self->currentSpineIndex); + if (tocIndex >= 0) { + chapterTitle = self->epub->getTocItem(tocIndex).title; + } + if (!chapterTitle.empty()) { + bm.name = chapterTitle + " - Page " + std::to_string(self->section->currentPage + 1); + } else { + bm.name = "Page " + std::to_string(self->section->currentPage + 1); + } + + BookmarkStore::addBookmark(bookPath, bm); + } + } + self->updateRequired = true; + } else if (action == QuickMenuAction::CLEAR_CACHE) { + // Navigate to Clear Cache activity + if (self->onGoToClearCache) { + xSemaphoreGive(cachedMutex); + self->onGoToClearCache(); + return; + } + self->updateRequired = true; + } else if (action == QuickMenuAction::GO_TO_SETTINGS) { + // Navigate to Settings activity + if (self->onGoToSettings) { + xSemaphoreGive(cachedMutex); + self->onGoToSettings(); + return; + } + self->updateRequired = true; + } + }, + [this]() { + EpubReaderActivity* self = this; + exitActivity(); + self->updateRequired = true; + }, + isBookmarked)); + xSemaphoreGive(renderingMutex); + return; + } + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || @@ -632,6 +777,24 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + + // Draw bookmark indicator (folded corner) if this page is bookmarked + if (section) { + const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage); + if (BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset)) { + // Draw folded corner in top-right + const int screenWidth = renderer.getScreenWidth(); + constexpr int cornerSize = 20; + const int cornerX = screenWidth - orientedMarginRight - cornerSize; + const int cornerY = orientedMarginTop; + + // Draw triangle (folded corner effect) + const int xPoints[3] = {cornerX, cornerX + cornerSize, cornerX + cornerSize}; + const int yPoints[3] = {cornerY, cornerY, cornerY + cornerSize}; + renderer.fillPolygon(xPoints, yPoints, 3, true); // Black triangle + } + } + renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index b87a51d..32f861e 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -18,6 +18,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity { bool updateRequired = false; const std::function onGoBack; const std::function onGoHome; + const std::function onGoToClearCache; + const std::function onGoToSettings; // End-of-book prompt state bool showingEndOfBookPrompt = false; @@ -38,11 +40,15 @@ class EpubReaderActivity final : public ActivityWithSubactivity { public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, - const std::function& onGoBack, const std::function& onGoHome) + const std::function& onGoBack, const std::function& onGoHome, + const std::function& onGoToClearCache = nullptr, + const std::function& onGoToSettings = nullptr) : ActivityWithSubactivity("EpubReader", renderer, mappedInput), epub(std::move(epub)), onGoBack(onGoBack), - onGoHome(onGoHome) {} + onGoHome(onGoHome), + onGoToClearCache(onGoToClearCache), + onGoToSettings(onGoToSettings) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 667afed..d425498 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -62,7 +62,11 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { currentBookPath = epubPath; exitActivity(); enterNewActivity(new EpubReaderActivity( - renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); })); + renderer, mappedInput, std::move(epub), + [this, epubPath] { goToLibrary(epubPath); }, + [this] { onGoBack(); }, + onGoToClearCache, + onGoToSettings)); } void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index c2753f3..0a1cecf 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -13,6 +13,8 @@ class ReaderActivity final : public ActivityWithSubactivity { MyLibraryActivity::Tab libraryTab; // Track which tab to return to const std::function onGoBack; const std::function onGoToLibrary; + const std::function onGoToClearCache; + const std::function onGoToSettings; static std::unique_ptr loadEpub(const std::string& path); static std::unique_ptr loadTxt(const std::string& path); static bool isTxtFile(const std::string& path); @@ -25,11 +27,15 @@ class ReaderActivity final : public ActivityWithSubactivity { public: explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, MyLibraryActivity::Tab libraryTab, const std::function& onGoBack, - const std::function& onGoToLibrary) + const std::function& onGoToLibrary, + const std::function& onGoToClearCache = nullptr, + const std::function& onGoToSettings = nullptr) : ActivityWithSubactivity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)), libraryTab(libraryTab), onGoBack(onGoBack), - onGoToLibrary(onGoToLibrary) {} + onGoToLibrary(onGoToLibrary), + onGoToClearCache(onGoToClearCache), + onGoToSettings(onGoToSettings) {} void onEnter() override; }; diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 04369be..7c2208f 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -20,6 +20,8 @@ class TxtReaderActivity final : public ActivityWithSubactivity { bool updateRequired = false; const std::function onGoBack; const std::function onGoHome; + const std::function onGoToClearCache; + const std::function onGoToSettings; // End-of-book prompt state bool showingEndOfBookPrompt = false; @@ -56,11 +58,15 @@ class TxtReaderActivity final : public ActivityWithSubactivity { public: explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr txt, - const std::function& onGoBack, const std::function& onGoHome) + const std::function& onGoBack, const std::function& onGoHome, + const std::function& onGoToClearCache = nullptr, + const std::function& onGoToSettings = nullptr) : ActivityWithSubactivity("TxtReader", renderer, mappedInput), txt(std::move(txt)), onGoBack(onGoBack), - onGoHome(onGoHome) {} + onGoHome(onGoHome), + onGoToClearCache(onGoToClearCache), + onGoToSettings(onGoToSettings) {} void onEnter() override; void onExit() override; void loop() override;