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
5 changed files with 192 additions and 7 deletions

View File

@@ -8,6 +8,7 @@
#include <Serialization.h>
#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> 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);