diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 205ef925..35809988 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -480,6 +480,8 @@ STR_IMAGE_DISPLAY_BW: ">> B&W" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Gray" STR_WEATHER_MOON_INFO: "Moon" STR_WEATHER_SUN_INFO: "Sun" +STR_READER_BOOKMARKS: "Bookmarks & Footnotes" +STR_READER_UTILS: "Helper" STR_READER_TOOLS: "Tools" STR_READER_NAVIGATION: "Navigation" STR_READER_APPEARANCE: "Appearance" @@ -500,4 +502,5 @@ STR_FORCE_REFRESH: "Refresh Screen" STR_STARRED_PAGES: "Starred Pages" STR_STAR_PAGE: "Star Page" STR_NO_STARRED_PAGES: "No starred pages" +STR_RENAME: "Rename" STR_PAGE_PREFIX: "p" diff --git a/src/BookmarkStore.h b/src/BookmarkStore.h index 304a97a6..6121d8a0 100644 --- a/src/BookmarkStore.h +++ b/src/BookmarkStore.h @@ -15,6 +15,7 @@ class BookmarkStore { struct Bookmark { uint16_t spineIndex; uint16_t pageNumber; + std::string name; // optional user-provided label (empty = use default) }; // Load bookmarks from the cache directory (e.g. .crosspoint/epub_/). @@ -29,7 +30,8 @@ class BookmarkStore { } uint8_t version; - if (f.read(reinterpret_cast(&version), sizeof(version)) != sizeof(version) || version != FILE_VERSION) { + if (f.read(reinterpret_cast(&version), sizeof(version)) != sizeof(version) || version < 1 || + version > FILE_VERSION) { f.close(); return; } @@ -51,7 +53,26 @@ class BookmarkStore { f.close(); return; } - bookmarks.push_back(bm); + if (version >= 2) { + uint16_t nameLen = 0; + if (f.read(reinterpret_cast(&nameLen), sizeof(nameLen)) != sizeof(nameLen) || + nameLen > MAX_NAME_LENGTH) { + LOG_ERR("BKM", "Invalid bookmark name length at entry %d", i); + bookmarks.clear(); + f.close(); + return; + } + if (nameLen > 0) { + bm.name.resize(nameLen); + if (f.read(reinterpret_cast(&bm.name[0]), nameLen) != nameLen) { + LOG_ERR("BKM", "Truncated bookmark name at entry %d", i); + bookmarks.clear(); + f.close(); + return; + } + } + } + bookmarks.push_back(std::move(bm)); } f.close(); @@ -83,7 +104,11 @@ class BookmarkStore { bool ok = writePodChecked(FILE_VERSION) && writePodChecked(count); for (const auto& bm : bookmarks) { - ok = ok && writePodChecked(bm.spineIndex) && writePodChecked(bm.pageNumber); + const uint16_t nameLen = static_cast(std::min(bm.name.size(), MAX_NAME_LENGTH)); + ok = ok && writePodChecked(bm.spineIndex) && writePodChecked(bm.pageNumber) && writePodChecked(nameLen); + if (ok && nameLen > 0) { + ok = f.write(reinterpret_cast(bm.name.data()), nameLen) == nameLen; + } } ok = ok && f.close(); @@ -119,8 +144,24 @@ class BookmarkStore { [[nodiscard]] bool isEmpty() const { return bookmarks.empty(); } void markDirty() { dirty = true; } + // Set or clear the name for the bookmark at index. Empty name reverts to default label. + void rename(size_t index, std::string name) { + if (index >= bookmarks.size()) return; + if (name.size() > MAX_NAME_LENGTH) name.resize(MAX_NAME_LENGTH); + bookmarks[index].name = std::move(name); + dirty = true; + } + + void removeAt(size_t index) { + if (index >= bookmarks.size()) return; + bookmarks.erase(bookmarks.begin() + index); + dirty = true; + } + + static constexpr uint16_t MAX_NAME_LENGTH = 128; + private: - static constexpr uint8_t FILE_VERSION = 1; + static constexpr uint8_t FILE_VERSION = 2; static constexpr uint16_t MAX_BOOKMARKS = 1000; std::vector bookmarks; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6d062815..8068d8cb 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -216,21 +216,24 @@ void EpubReaderActivity::loop() { bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); - startActivityForResult(std::make_unique( - renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, - SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride, - bookImageRenderingOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty()), - [this](const ActivityResult& result) { - // Always apply orientation/darkness change even if the menu was cancelled - const auto& menu = std::get(result.data); - applyOrientation(menu.orientation); - applyTextDarkness(menu.textDarkness); - toggleAutoPageTurn(menu.pageTurnOption); - applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride); - if (!result.isCancelled) { - onReaderMenuConfirm(static_cast(menu.action)); - } - }); + const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast(currentSpineIndex), + static_cast(section->currentPage)); + startActivityForResult( + std::make_unique( + renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation, + !currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, SETTINGS.textDarkness, + !bookmarkStore.isEmpty(), isCurrentPageStarred), + [this](const ActivityResult& result) { + // Always apply orientation/darkness change even if the menu was cancelled + const auto& menu = std::get(result.data); + applyOrientation(menu.orientation); + applyTextDarkness(menu.textDarkness); + toggleAutoPageTurn(menu.pageTurnOption); + applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride); + if (!result.isCancelled) { + onReaderMenuConfirm(static_cast(menu.action)); + } + }); } // Long press BACK (1s+) goes to home screen @@ -516,9 +519,16 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction requestUpdate(); break; } + case EpubReaderMenuActivity::MenuAction::STAR_PAGE: { + if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) { + bookmarkStore.toggle(static_cast(currentSpineIndex), static_cast(section->currentPage)); + requestUpdate(); + } + break; + } case EpubReaderMenuActivity::MenuAction::STARRED_PAGES: { startActivityForResult( - std::make_unique(renderer, mappedInput, bookmarkStore.getAll(), epub), + std::make_unique(renderer, mappedInput, bookmarkStore, epub), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& starred = std::get(result.data); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 5fa0bda7..d6a3610a 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -13,8 +13,10 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride, - const uint8_t initialTextDarkness, const bool hasStarredPages) + const uint8_t initialTextDarkness, const bool hasStarredPages, + const bool isCurrentPageStarred) : MenuListActivity("EpubReaderMenu", renderer, mappedInput), + currentPageStarred(isCurrentPageStarred), pendingOrientation(currentOrientation), pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride), pendingImageRenderingOverride(initialImageRenderingOverride), @@ -33,14 +35,17 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION)); menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None)); menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None)); + + // Bookmarks, footnotes + menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_BOOKMARKS)); + + menuItems.push_back(SettingInfo::Action(StrId::STR_STAR_PAGE, SettingAction::None)); if (hasStarredPages) { menuItems.push_back(SettingInfo::Action(StrId::STR_STARRED_PAGES, SettingAction::None)); } if (hasFootnotes) { menuItems.push_back(SettingInfo::Action(StrId::STR_FOOTNOTES, SettingAction::None)); } - // Auto page turn: ACTION type with custom cycling in onActionSelected - menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None)); // --- Appearance --- menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_APPEARANCE)); @@ -74,6 +79,10 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa StrId::STR_TEXT_DARKNESS, {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, [this]() -> uint8_t { return pendingTextDarkness; }, [this](uint8_t v) { pendingTextDarkness = v; })); + // Helper functions, reading ruler, auto page turn, orientation + menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_UTILS)); + // Auto page turn: ACTION type with custom cycling in onActionSelected + menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None)); // Orientation: straightforward 0-3 cycle menuItems.push_back(SettingInfo::DynamicEnum( StrId::STR_ORIENTATION, @@ -103,6 +112,8 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId return MenuAction::GO_TO_PERCENT; case StrId::STR_STARRED_PAGES: return MenuAction::STARRED_PAGES; + case StrId::STR_STAR_PAGE: + return MenuAction::STAR_PAGE; case StrId::STR_FOOTNOTES: return MenuAction::FOOTNOTES; case StrId::STR_AUTO_TURN_PAGES_PER_MIN: @@ -178,6 +189,11 @@ std::string EpubReaderMenuActivity::getItemValueString(int index) const { return std::string(pageTurnLabels[selectedPageTurnOption]); } + // Star page: reflect current page's star state + if (item.nameId == StrId::STR_STAR_PAGE) { + return currentPageStarred ? std::string(tr(STR_STATE_ON)) : std::string(tr(STR_STATE_OFF)); + } + // Plain ACTION items (select chapter, screenshot, etc.) show no value if (item.type == SettingType::ACTION) return {}; diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index d8f4c099..2cf79c65 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -28,6 +28,7 @@ class EpubReaderMenuActivity final : public MenuListActivity { PUSH_LOCAL, SYNC, STARRED_PAGES, + STAR_PAGE, DELETE_CACHE }; @@ -35,14 +36,16 @@ class EpubReaderMenuActivity final : public MenuListActivity { const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride, - const uint8_t initialTextDarkness, - const bool hasStarredPages); + const uint8_t initialTextDarkness, const bool hasStarredPages, + const bool isCurrentPageStarred); void onEnter() override; void render(RenderLock&&) override; private: void buildMenuItems(bool hasFootnotes, bool hasStarredPages); + + bool currentPageStarred = false; void finishWithAction(MenuAction action); // MenuListActivity overrides diff --git a/src/activities/reader/StarredPagesActivity.cpp b/src/activities/reader/StarredPagesActivity.cpp index aaea1682..6ce8bfaa 100644 --- a/src/activities/reader/StarredPagesActivity.cpp +++ b/src/activities/reader/StarredPagesActivity.cpp @@ -4,6 +4,7 @@ #include #include "MappedInputManager.h" +#include "activities/util/KeyboardEntryActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -18,40 +19,81 @@ int StarredPagesActivity::getPageItems() const { return std::max(1, availableHeight / lineHeight); } -std::string StarredPagesActivity::getItemLabel(int index) const { - const auto& bm = bookmarks[index]; +std::string StarredPagesActivity::getDefaultLabel(int index) const { + const auto& bm = bookmarkStore.getAll()[index]; char buf[64]; if (epub) { - // Try to get chapter title from TOC const int tocIndex = epub->getTocIndexForSpineIndex(bm.spineIndex); if (tocIndex != -1) { const auto tocItem = epub->getTocItem(tocIndex); - snprintf(buf, sizeof(buf), "%d. ", index + 1); - return std::string(buf) + tocItem.title + " - " + tr(STR_PAGE_PREFIX) + std::to_string(bm.pageNumber + 1); + return tocItem.title + " - " + tr(STR_PAGE_PREFIX) + std::to_string(bm.pageNumber + 1); } - snprintf(buf, sizeof(buf), "%d. %s%d, %s%d", index + 1, tr(STR_SECTION_PREFIX), bm.spineIndex + 1, - tr(STR_PAGE_PREFIX), bm.pageNumber + 1); + snprintf(buf, sizeof(buf), "%s%d, %s%d", tr(STR_SECTION_PREFIX), bm.spineIndex + 1, tr(STR_PAGE_PREFIX), + bm.pageNumber + 1); } else { - // TXT file: just page number (spineIndex is always 0) - snprintf(buf, sizeof(buf), "%d. %s%d", index + 1, tr(STR_PAGE_PREFIX), bm.pageNumber + 1); + snprintf(buf, sizeof(buf), "%s%d", tr(STR_PAGE_PREFIX), bm.pageNumber + 1); } return std::string(buf); } +std::string StarredPagesActivity::getItemLabel(int index) const { + char prefix[16]; + snprintf(prefix, sizeof(prefix), "%d. ", index + 1); + const auto& bm = bookmarkStore.getAll()[index]; + return std::string(prefix) + (bm.name.empty() ? getDefaultLabel(index) : bm.name); +} + void StarredPagesActivity::onEnter() { Activity::onEnter(); requestUpdate(); } -void StarredPagesActivity::onExit() { Activity::onExit(); } +void StarredPagesActivity::onExit() { + bookmarkStore.save(); + Activity::onExit(); +} + +void StarredPagesActivity::startRename() { + const auto& all = bookmarkStore.getAll(); + if (all.empty() || selectorIndex >= static_cast(all.size())) return; + const int renamingIndex = selectorIndex; + const std::string initial = + all[renamingIndex].name.empty() ? getDefaultLabel(renamingIndex) : all[renamingIndex].name; + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_RENAME), initial, + BookmarkStore::MAX_NAME_LENGTH, false), + [this, renamingIndex](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kr = std::get(result.data); + bookmarkStore.rename(renamingIndex, kr.text); + } + requestUpdate(); + }); +} + +void StarredPagesActivity::deleteSelected() { + const auto& all = bookmarkStore.getAll(); + if (all.empty() || selectorIndex >= static_cast(all.size())) return; + bookmarkStore.removeAt(selectorIndex); + const int remaining = static_cast(bookmarkStore.getAll().size()); + if (remaining == 0) { + // Nothing left — drop back to the reader. + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + if (selectorIndex >= remaining) selectorIndex = remaining - 1; + requestUpdate(); +} void StarredPagesActivity::loop() { - const int totalItems = static_cast(bookmarks.size()); + const int totalItems = static_cast(bookmarkStore.getAll().size()); const int pageItems = getPageItems(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (!bookmarks.empty()) { - const auto& bm = bookmarks[selectorIndex]; + if (totalItems > 0) { + const auto& bm = bookmarkStore.getAll()[selectorIndex]; setResult(StarredPageResult{bm.spineIndex, bm.pageNumber}); finish(); } @@ -66,22 +108,33 @@ void StarredPagesActivity::loop() { return; } - buttonNavigator.onNextRelease([this, totalItems] { + if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { + startRename(); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { + deleteSelected(); + return; + } + + // Side buttons (Up/Down) drive list navigation; Left/Right are reserved for rename/delete. + buttonNavigator.onRelease({MappedInputManager::Button::Down}, [this, totalItems] { selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); requestUpdate(); }); - buttonNavigator.onPreviousRelease([this, totalItems] { + buttonNavigator.onRelease({MappedInputManager::Button::Up}, [this, totalItems] { selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); requestUpdate(); }); - buttonNavigator.onNextContinuous([this, totalItems, pageItems] { + buttonNavigator.onContinuous({MappedInputManager::Button::Down}, [this, totalItems, pageItems] { selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); requestUpdate(); }); - buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { + buttonNavigator.onContinuous({MappedInputManager::Button::Up}, [this, totalItems, pageItems] { selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); requestUpdate(); }); @@ -90,7 +143,7 @@ void StarredPagesActivity::loop() { void StarredPagesActivity::render(RenderLock&&) { renderer.clearScreen(); - const int totalItems = static_cast(bookmarks.size()); + const int totalItems = static_cast(bookmarkStore.getAll().size()); if (totalItems == 0) { renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_NO_STARRED_PAGES), true, EpdFontFamily::BOLD); @@ -130,7 +183,7 @@ void StarredPagesActivity::render(RenderLock&&) { renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, label.c_str(), !isSelected); } - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_RENAME), tr(STR_DELETE)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); diff --git a/src/activities/reader/StarredPagesActivity.h b/src/activities/reader/StarredPagesActivity.h index beb290bf..62aaf6b3 100644 --- a/src/activities/reader/StarredPagesActivity.h +++ b/src/activities/reader/StarredPagesActivity.h @@ -11,18 +11,20 @@ class StarredPagesActivity final : public Activity { std::shared_ptr epub; // nullptr for TXT files - const std::vector bookmarks; + BookmarkStore& bookmarkStore; ButtonNavigator buttonNavigator; int selectorIndex = 0; int getPageItems() const; std::string getItemLabel(int index) const; + std::string getDefaultLabel(int index) const; + void startRename(); + void deleteSelected(); public: - explicit StarredPagesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::vector& bookmarks, + explicit StarredPagesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, BookmarkStore& bookmarkStore, std::shared_ptr epub = nullptr) - : Activity("StarredPages", renderer, mappedInput), epub(std::move(epub)), bookmarks(bookmarks) {} + : Activity("StarredPages", renderer, mappedInput), epub(std::move(epub)), bookmarkStore(bookmarkStore) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 0f81f558..6a2ec736 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -160,7 +160,7 @@ void TxtReaderActivity::loop() { // Open starred pages list via Confirm button if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) { - startActivityForResult(std::make_unique(renderer, mappedInput, bookmarkStore.getAll()), + startActivityForResult(std::make_unique(renderer, mappedInput, bookmarkStore), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& starred = std::get(result.data);