feat: Integrate bookmark support into reader activities

Adds bookmark add/remove functionality to EpubReaderActivity and base
ReaderActivity, with visual indicator for bookmarked pages.
This commit is contained in:
cottongin 2026-01-28 02:20:29 -05:00
parent e991fb10a6
commit 245d5a7dd8
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
5 changed files with 192 additions and 7 deletions

View File

@ -8,6 +8,7 @@
#include <Serialization.h> #include <Serialization.h>
#include "BookManager.h" #include "BookManager.h"
#include "BookmarkStore.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderChapterSelectionActivity.h"
@ -17,6 +18,7 @@
#include "activities/dictionary/DictionaryMenuActivity.h" #include "activities/dictionary/DictionaryMenuActivity.h"
#include "activities/dictionary/DictionarySearchActivity.h" #include "activities/dictionary/DictionarySearchActivity.h"
#include "activities/dictionary/EpubWordSelectionActivity.h" #include "activities/dictionary/EpubWordSelectionActivity.h"
#include "activities/util/QuickMenuActivity.h"
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
@ -366,6 +368,149 @@ void EpubReaderActivity::loop() {
return; 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) || const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
mappedInput.wasReleased(MappedInputManager::Button::Left); mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
@ -632,6 +777,24 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) { const int orientedMarginLeft) {
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); 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); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(EInkDisplay::HALF_REFRESH);

View File

@ -18,6 +18,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
bool updateRequired = false; bool updateRequired = false;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
const std::function<void()> onGoToClearCache;
const std::function<void()> onGoToSettings;
// End-of-book prompt state // End-of-book prompt state
bool showingEndOfBookPrompt = false; bool showingEndOfBookPrompt = false;
@ -38,11 +40,15 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
public: public:
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub, explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome) const std::function<void()>& onGoBack, const std::function<void()>& onGoHome,
const std::function<void()>& onGoToClearCache = nullptr,
const std::function<void()>& onGoToSettings = nullptr)
: ActivityWithSubactivity("EpubReader", renderer, mappedInput), : ActivityWithSubactivity("EpubReader", renderer, mappedInput),
epub(std::move(epub)), epub(std::move(epub)),
onGoBack(onGoBack), onGoBack(onGoBack),
onGoHome(onGoHome) {} onGoHome(onGoHome),
onGoToClearCache(onGoToClearCache),
onGoToSettings(onGoToSettings) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -62,7 +62,11 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
currentBookPath = epubPath; currentBookPath = epubPath;
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderActivity( 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> txt) { void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {

View File

@ -13,6 +13,8 @@ class ReaderActivity final : public ActivityWithSubactivity {
MyLibraryActivity::Tab libraryTab; // Track which tab to return to MyLibraryActivity::Tab libraryTab; // Track which tab to return to
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary; const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary;
const std::function<void()> onGoToClearCache;
const std::function<void()> onGoToSettings;
static std::unique_ptr<Epub> loadEpub(const std::string& path); static std::unique_ptr<Epub> loadEpub(const std::string& path);
static std::unique_ptr<Txt> loadTxt(const std::string& path); static std::unique_ptr<Txt> loadTxt(const std::string& path);
static bool isTxtFile(const std::string& path); static bool isTxtFile(const std::string& path);
@ -25,11 +27,15 @@ class ReaderActivity final : public ActivityWithSubactivity {
public: public:
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack, MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack,
const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary) const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary,
const std::function<void()>& onGoToClearCache = nullptr,
const std::function<void()>& onGoToSettings = nullptr)
: ActivityWithSubactivity("Reader", renderer, mappedInput), : ActivityWithSubactivity("Reader", renderer, mappedInput),
initialBookPath(std::move(initialBookPath)), initialBookPath(std::move(initialBookPath)),
libraryTab(libraryTab), libraryTab(libraryTab),
onGoBack(onGoBack), onGoBack(onGoBack),
onGoToLibrary(onGoToLibrary) {} onGoToLibrary(onGoToLibrary),
onGoToClearCache(onGoToClearCache),
onGoToSettings(onGoToSettings) {}
void onEnter() override; void onEnter() override;
}; };

View File

@ -20,6 +20,8 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
bool updateRequired = false; bool updateRequired = false;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
const std::function<void()> onGoToClearCache;
const std::function<void()> onGoToSettings;
// End-of-book prompt state // End-of-book prompt state
bool showingEndOfBookPrompt = false; bool showingEndOfBookPrompt = false;
@ -56,11 +58,15 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
public: public:
explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt, explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt,
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome) const std::function<void()>& onGoBack, const std::function<void()>& onGoHome,
const std::function<void()>& onGoToClearCache = nullptr,
const std::function<void()>& onGoToSettings = nullptr)
: ActivityWithSubactivity("TxtReader", renderer, mappedInput), : ActivityWithSubactivity("TxtReader", renderer, mappedInput),
txt(std::move(txt)), txt(std::move(txt)),
onGoBack(onGoBack), onGoBack(onGoBack),
onGoHome(onGoHome) {} onGoHome(onGoHome),
onGoToClearCache(onGoToClearCache),
onGoToSettings(onGoToSettings) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;