## Summary This PR enhances the EPUB reader's bookmark system with two complementary improvements: a per-page bookmark indicator icon and toggle behavior on the existing long-press action. --- ### What Changed **Bookmark Toggle (was: add-only)** The long-press Confirm action now toggles bookmarks rather than always adding. `addBookmark()` checks whether a bookmark with the same xpath already exists in the in-memory cache: - If found → removes it and shows "Bookmark removed." - If not found → adds it and shows "Bookmark added." A new `STR_BOOKMARK_REMOVED` translation string was added to support the removal message. **Bookmark Icon Indicator** A `BookmarkIcon` is now drawn at the top-right corner of the page whenever the current page has a bookmark. `updateBookmarkFlag()` is called at render time to determine whether the current page is bookmarked. **In-Memory Bookmark Cache** Bookmarks are now loaded into `cachedBookmarks` on `onEnter()` rather than being re-read from disk on every toggle. All subsequent add/remove operations work against this cache and flush to disk, avoiding redundant file reads on each bookmark action. **Faster bookmarks list** Previously, calculating "page X/Y" for each entry required decompressing the entire spine item. We now persist `si`/`pc`/`pp` (spine index, page count, and page progress) in the bookmark JSON when saving, and restore them when loading. This avoids the expensive `toCrossPoint()` loop in `onEnter()`, significantly reducing the cost of initializing the bookmarks list. --- ### Files Changed - `EpubReaderActivity.cpp` — `addBookmark()` toggle logic, `updateBookmarkFlag()` (new), icon rendering in `renderContents()`, cache initialization in `onEnter()` - `EpubReaderActivity.h` — new fields: `currentPageBookmarked`, `bookmarkRemoved`, `cachedBookmarks`; new method declaration `updateBookmarkFlag()` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_ --------- Co-authored-by: Julia Nguyen <julia@uxj.io>
140 lines
5.6 KiB
C++
140 lines
5.6 KiB
C++
#include "EpubReaderMenuActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <I18n.h>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
|
|
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
|
const std::string& title, const int currentPage, const int totalPages,
|
|
const int bookProgressPercent, const uint8_t currentOrientation,
|
|
const bool hasFootnotes, const bool hasBookmarks)
|
|
: Activity("EpubReaderMenu", renderer, mappedInput),
|
|
menuItems(buildMenuItems(hasFootnotes, hasBookmarks)),
|
|
title(title),
|
|
pendingOrientation(currentOrientation),
|
|
currentPage(currentPage),
|
|
totalPages(totalPages),
|
|
bookProgressPercent(bookProgressPercent) {}
|
|
|
|
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
|
|
bool hasBookmarks) {
|
|
std::vector<MenuItem> items;
|
|
items.reserve(12);
|
|
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
|
if (hasFootnotes) {
|
|
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
|
}
|
|
if (hasBookmarks) {
|
|
items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS});
|
|
}
|
|
items.push_back({MenuAction::TOGGLE_BOOKMARK, StrId::STR_TOGGLE_BOOKMARK});
|
|
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
|
|
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
|
|
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
|
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
|
|
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
|
|
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
|
|
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
|
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
|
return items;
|
|
}
|
|
|
|
void EpubReaderMenuActivity::onEnter() {
|
|
Activity::onEnter();
|
|
requestUpdate();
|
|
}
|
|
|
|
void EpubReaderMenuActivity::onExit() { Activity::onExit(); }
|
|
|
|
void EpubReaderMenuActivity::loop() {
|
|
// Handle navigation
|
|
buttonNavigator.onNext([this] {
|
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator.onPrevious([this] {
|
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
const auto selectedAction = menuItems[selectedIndex].action;
|
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
|
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
|
pendingOrientation = (pendingOrientation + 1) % orientationLabels.size();
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
if (selectedAction == MenuAction::AUTO_PAGE_TURN) {
|
|
selectedPageTurnOption = (selectedPageTurnOption + 1) % pageTurnLabels.size();
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
|
|
finish();
|
|
return;
|
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
ActivityResult result;
|
|
result.isCancelled = true;
|
|
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption};
|
|
setResult(std::move(result));
|
|
finish();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void EpubReaderMenuActivity::render(RenderLock&&) {
|
|
renderer.clearScreen();
|
|
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false);
|
|
|
|
GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight},
|
|
title.c_str());
|
|
|
|
// Progress summary
|
|
std::string progressLine;
|
|
if (totalPages > 0) {
|
|
progressLine = std::string(tr(STR_CHAPTER_PREFIX)) + std::to_string(currentPage) + "/" +
|
|
std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR));
|
|
}
|
|
progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%";
|
|
GUI.drawSubHeader(
|
|
renderer,
|
|
Rect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight, screen.width, metrics.tabBarHeight},
|
|
progressLine.c_str());
|
|
|
|
const int contentTop =
|
|
screen.y + metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
|
|
const int contentHeight = screen.height - contentTop - metrics.verticalSpacing;
|
|
|
|
GUI.drawList(
|
|
renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, menuItems.size(), selectedIndex,
|
|
[this](int index) { return I18N.get(menuItems[index].labelId); }, nullptr, nullptr,
|
|
[this](int index) {
|
|
const auto value = menuItems[index].action;
|
|
if (value == MenuAction::ROTATE_SCREEN) {
|
|
// Render current orientation value on the right edge of the content area.
|
|
return I18N.get(orientationLabels[pendingOrientation]);
|
|
} else if (value == MenuAction::AUTO_PAGE_TURN) {
|
|
// Render current page turn value on the right edge of the content area.
|
|
return pageTurnLabels[selectedPageTurnOption];
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
true);
|
|
|
|
// Footer / Hints
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
|
|
renderer.displayBuffer();
|
|
}
|