mod: Phase 1 - bring forward mod-exclusive files with ActivityManager migration
Brings ~55 mod-exclusive files to the upstream-based mod/master-resync branch:
Activities (migrated to new ActivityManager pattern):
- Clock/Time: SetTimeActivity, SetTimezoneOffsetActivity, NtpSyncActivity
- Dictionary: DictionaryDefinitionActivity, DictionarySuggestionsActivity,
DictionaryWordSelectActivity, LookedUpWordsActivity
- Bookmark: EpubReaderBookmarkSelectionActivity
- Book management: BookManageMenuActivity, EndOfBookMenuActivity
- OPDS: OpdsServerListActivity, OpdsSettingsActivity
- Utility: DirectoryPickerActivity, NumericStepperActivity
Utilities (unchanged):
- BookManager, BookSettings, BookmarkStore, BootNtpSync
- Dictionary, LookupHistory, TimeSync, OpdsServerStore
Libraries: PlaceholderCover, TableData, ChapterXPathIndexer
Scripts: inject_mod_version, generate_book_icon, preview_placeholder_cover
Docs: KOReader sync XPath mapping
Migration changes:
- ActivityWithSubactivity -> Activity base class
- Callback constructors -> finish()/setResult() pattern
- enterNewActivity() -> startActivityForResult()
- Activity::RenderLock&& -> RenderLock&&
These files won't compile yet - they reference mod settings and I18n
strings that will be added in subsequent phases.
Made-with: Cursor
2026-03-07 15:10:00 -05:00
|
|
|
#include "EpubReaderBookmarkSelectionActivity.h"
|
|
|
|
|
|
|
|
|
|
#include <GfxRenderer.h>
|
|
|
|
|
|
2026-03-07 20:56:40 -05:00
|
|
|
#include "activities/ActivityResult.h"
|
mod: Phase 1 - bring forward mod-exclusive files with ActivityManager migration
Brings ~55 mod-exclusive files to the upstream-based mod/master-resync branch:
Activities (migrated to new ActivityManager pattern):
- Clock/Time: SetTimeActivity, SetTimezoneOffsetActivity, NtpSyncActivity
- Dictionary: DictionaryDefinitionActivity, DictionarySuggestionsActivity,
DictionaryWordSelectActivity, LookedUpWordsActivity
- Bookmark: EpubReaderBookmarkSelectionActivity
- Book management: BookManageMenuActivity, EndOfBookMenuActivity
- OPDS: OpdsServerListActivity, OpdsSettingsActivity
- Utility: DirectoryPickerActivity, NumericStepperActivity
Utilities (unchanged):
- BookManager, BookSettings, BookmarkStore, BootNtpSync
- Dictionary, LookupHistory, TimeSync, OpdsServerStore
Libraries: PlaceholderCover, TableData, ChapterXPathIndexer
Scripts: inject_mod_version, generate_book_icon, preview_placeholder_cover
Docs: KOReader sync XPath mapping
Migration changes:
- ActivityWithSubactivity -> Activity base class
- Callback constructors -> finish()/setResult() pattern
- enterNewActivity() -> startActivityForResult()
- Activity::RenderLock&& -> RenderLock&&
These files won't compile yet - they reference mod settings and I18n
strings that will be added in subsequent phases.
Made-with: Cursor
2026-03-07 15:10:00 -05:00
|
|
|
#include "MappedInputManager.h"
|
|
|
|
|
#include "components/UITheme.h"
|
|
|
|
|
#include "fontIds.h"
|
|
|
|
|
|
|
|
|
|
int EpubReaderBookmarkSelectionActivity::getTotalItems() const { return static_cast<int>(bookmarks.size()); }
|
|
|
|
|
|
|
|
|
|
int EpubReaderBookmarkSelectionActivity::getPageItems() const {
|
|
|
|
|
constexpr int lineHeight = 30;
|
|
|
|
|
|
|
|
|
|
const int screenHeight = renderer.getScreenHeight();
|
|
|
|
|
const auto orientation = renderer.getOrientation();
|
|
|
|
|
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
|
|
|
|
|
const int hintGutterHeight = isPortraitInverted ? 50 : 0;
|
|
|
|
|
const int startY = 60 + hintGutterHeight;
|
|
|
|
|
const int availableHeight = screenHeight - startY - lineHeight;
|
|
|
|
|
return std::max(1, availableHeight / lineHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string EpubReaderBookmarkSelectionActivity::getBookmarkPrefix(const Bookmark& bookmark) const {
|
|
|
|
|
std::string label;
|
|
|
|
|
if (epub) {
|
|
|
|
|
const int tocIndex = epub->getTocIndexForSpineIndex(bookmark.spineIndex);
|
|
|
|
|
if (tocIndex >= 0 && tocIndex < epub->getTocItemsCount()) {
|
|
|
|
|
label = epub->getTocItem(tocIndex).title;
|
|
|
|
|
} else {
|
|
|
|
|
label = "Chapter " + std::to_string(bookmark.spineIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
label = "Chapter " + std::to_string(bookmark.spineIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
if (!bookmark.snippet.empty()) {
|
|
|
|
|
label += " - " + bookmark.snippet;
|
|
|
|
|
}
|
|
|
|
|
return label;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string EpubReaderBookmarkSelectionActivity::getPageSuffix(const Bookmark& bookmark) {
|
|
|
|
|
return " - Page " + std::to_string(bookmark.pageNumber + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderBookmarkSelectionActivity::onEnter() {
|
|
|
|
|
Activity::onEnter();
|
|
|
|
|
requestUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderBookmarkSelectionActivity::onExit() { Activity::onExit(); }
|
|
|
|
|
|
|
|
|
|
void EpubReaderBookmarkSelectionActivity::loop() {
|
|
|
|
|
const int totalItems = getTotalItems();
|
|
|
|
|
|
|
|
|
|
if (totalItems == 0) {
|
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
|
|
|
|
|
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
|
|
|
ActivityResult r;
|
|
|
|
|
r.isCancelled = true;
|
|
|
|
|
setResult(std::move(r));
|
|
|
|
|
finish();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (deleteConfirmMode) {
|
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
|
|
|
if (ignoreNextConfirmRelease) {
|
|
|
|
|
ignoreNextConfirmRelease = false;
|
|
|
|
|
} else {
|
|
|
|
|
BookmarkStore::removeBookmark(cachePath, bookmarks[pendingDeleteIndex].spineIndex,
|
|
|
|
|
bookmarks[pendingDeleteIndex].pageNumber);
|
|
|
|
|
bookmarks.erase(bookmarks.begin() + pendingDeleteIndex);
|
|
|
|
|
if (selectorIndex >= static_cast<int>(bookmarks.size())) {
|
|
|
|
|
selectorIndex = std::max(0, static_cast<int>(bookmarks.size()) - 1);
|
|
|
|
|
}
|
|
|
|
|
deleteConfirmMode = false;
|
|
|
|
|
requestUpdate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
|
|
|
deleteConfirmMode = false;
|
|
|
|
|
ignoreNextConfirmRelease = false;
|
|
|
|
|
requestUpdate();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constexpr unsigned long DELETE_HOLD_MS = 700;
|
|
|
|
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= DELETE_HOLD_MS) {
|
|
|
|
|
if (totalItems > 0 && selectorIndex >= 0 && selectorIndex < totalItems) {
|
|
|
|
|
deleteConfirmMode = true;
|
|
|
|
|
ignoreNextConfirmRelease = true;
|
|
|
|
|
pendingDeleteIndex = selectorIndex;
|
|
|
|
|
requestUpdate();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int pageItems = getPageItems();
|
|
|
|
|
|
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
|
|
|
if (selectorIndex >= 0 && selectorIndex < totalItems) {
|
|
|
|
|
const auto& b = bookmarks[selectorIndex];
|
|
|
|
|
setResult(SyncResult{.spineIndex = b.spineIndex, .page = b.pageNumber});
|
|
|
|
|
finish();
|
|
|
|
|
} else {
|
|
|
|
|
ActivityResult r;
|
|
|
|
|
r.isCancelled = true;
|
|
|
|
|
setResult(std::move(r));
|
|
|
|
|
finish();
|
|
|
|
|
}
|
|
|
|
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
|
|
|
ActivityResult r;
|
|
|
|
|
r.isCancelled = true;
|
|
|
|
|
setResult(std::move(r));
|
|
|
|
|
finish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buttonNavigator.onNextRelease([this, totalItems] {
|
|
|
|
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
|
|
|
|
requestUpdate();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
|
|
|
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
|
|
|
|
requestUpdate();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
|
|
|
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
|
|
|
|
requestUpdate();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
|
|
|
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
|
|
|
|
requestUpdate();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderBookmarkSelectionActivity::render(RenderLock&&) {
|
|
|
|
|
renderer.clearScreen();
|
|
|
|
|
|
|
|
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
|
|
|
const auto orientation = renderer.getOrientation();
|
|
|
|
|
const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise;
|
|
|
|
|
const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise;
|
|
|
|
|
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
|
|
|
|
|
const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0;
|
|
|
|
|
const int contentX = isLandscapeCw ? hintGutterWidth : 0;
|
|
|
|
|
const int contentWidth = pageWidth - hintGutterWidth;
|
|
|
|
|
const int hintGutterHeight = isPortraitInverted ? 50 : 0;
|
|
|
|
|
const int contentY = hintGutterHeight;
|
|
|
|
|
const int pageItems = getPageItems();
|
|
|
|
|
const int totalItems = getTotalItems();
|
|
|
|
|
|
|
|
|
|
const int titleX =
|
|
|
|
|
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Go to Bookmark", EpdFontFamily::BOLD)) / 2;
|
|
|
|
|
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Go to Bookmark", true, EpdFontFamily::BOLD);
|
|
|
|
|
|
|
|
|
|
if (totalItems == 0) {
|
|
|
|
|
renderer.drawCenteredText(UI_10_FONT_ID, 100 + contentY, "No bookmarks", true);
|
|
|
|
|
} else {
|
|
|
|
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
|
|
|
|
renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30);
|
|
|
|
|
|
|
|
|
|
const int maxLabelWidth = contentWidth - 40 - contentX - 20;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < pageItems; i++) {
|
|
|
|
|
int itemIndex = pageStartIndex + i;
|
|
|
|
|
if (itemIndex >= totalItems) break;
|
|
|
|
|
const int displayY = 60 + contentY + i * 30;
|
|
|
|
|
const bool isSelected = (itemIndex == selectorIndex);
|
|
|
|
|
|
|
|
|
|
const std::string suffix = getPageSuffix(bookmarks[itemIndex]);
|
|
|
|
|
const int suffixWidth = renderer.getTextWidth(UI_10_FONT_ID, suffix.c_str());
|
|
|
|
|
|
|
|
|
|
const std::string prefix = getBookmarkPrefix(bookmarks[itemIndex]);
|
|
|
|
|
const std::string truncatedPrefix =
|
|
|
|
|
renderer.truncatedText(UI_10_FONT_ID, prefix.c_str(), maxLabelWidth - suffixWidth);
|
|
|
|
|
|
|
|
|
|
const std::string label = truncatedPrefix + suffix;
|
|
|
|
|
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, label.c_str(), !isSelected);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (deleteConfirmMode && pendingDeleteIndex < static_cast<int>(bookmarks.size())) {
|
|
|
|
|
const std::string suffix = getPageSuffix(bookmarks[pendingDeleteIndex]);
|
|
|
|
|
std::string msg = "Delete bookmark" + suffix + "?";
|
|
|
|
|
|
|
|
|
|
constexpr int margin = 15;
|
|
|
|
|
constexpr int popupY = 200;
|
|
|
|
|
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, msg.c_str(), EpdFontFamily::BOLD);
|
|
|
|
|
const int textHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
|
|
|
|
const int w = textWidth + margin * 2;
|
|
|
|
|
const int h = textHeight + margin * 2;
|
|
|
|
|
const int x = (renderer.getScreenWidth() - w) / 2;
|
|
|
|
|
|
|
|
|
|
renderer.fillRect(x - 2, popupY - 2, w + 4, h + 4, true);
|
|
|
|
|
renderer.fillRect(x, popupY, w, h, false);
|
|
|
|
|
|
|
|
|
|
const int textX = x + (w - textWidth) / 2;
|
|
|
|
|
const int textY = popupY + margin - 2;
|
|
|
|
|
renderer.drawText(UI_12_FONT_ID, textX, textY, msg.c_str(), true, EpdFontFamily::BOLD);
|
|
|
|
|
|
|
|
|
|
const auto labels = mappedInput.mapLabels("Cancel", "Delete", "", "");
|
|
|
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
|
|
|
} else {
|
|
|
|
|
if (!bookmarks.empty()) {
|
|
|
|
|
const char* deleteHint = "Hold select to delete";
|
|
|
|
|
const int hintWidth = renderer.getTextWidth(SMALL_FONT_ID, deleteHint);
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, (renderer.getScreenWidth() - hintWidth) / 2, renderer.getScreenHeight() - 70,
|
|
|
|
|
deleteHint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto labels = mappedInput.mapLabels("\xC2\xAB Back", "Select", "Up", "Down");
|
|
|
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderer.displayBuffer();
|
|
|
|
|
}
|