From 4aabcc934c05dfa266fa84ee8f61707a08f76933 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 18:22:54 +0200 Subject: [PATCH 1/4] Integrate and extend pr 1372 by andreaturchet --- lib/I18n/translations/english.yaml | 6 +- src/BookmarkStore.h | 137 ++++++++++++++++++ src/CrossPointSettings.h | 10 +- src/SettingsList.h | 8 +- src/activities/ActivityResult.h | 7 +- src/activities/reader/EpubReaderActivity.cpp | 43 +++++- src/activities/reader/EpubReaderActivity.h | 4 + .../reader/EpubReaderMenuActivity.cpp | 13 +- .../reader/EpubReaderMenuActivity.h | 7 +- .../reader/StarredPagesActivity.cpp | 137 ++++++++++++++++++ src/activities/reader/StarredPagesActivity.h | 30 ++++ src/activities/reader/TxtReaderActivity.cpp | 33 ++++- src/activities/reader/TxtReaderActivity.h | 4 + src/components/themes/BaseTheme.cpp | 22 ++- src/components/themes/BaseTheme.h | 2 +- 15 files changed, 443 insertions(+), 20 deletions(-) create mode 100644 src/BookmarkStore.h create mode 100644 src/activities/reader/StarredPagesActivity.cpp create mode 100644 src/activities/reader/StarredPagesActivity.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index a9632ca9..205ef925 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -496,4 +496,8 @@ STR_MENU_READER_TWEAKS: "Reader Tweaks" STR_MENU_READER_SPACING: "Spacing" STR_MENU_KOSYNC_SERVER: "Server Settings" STR_MENU_KOSYNC_AUTH: "Login / Register" -STR_FORCE_REFRESH: "Refresh Screen" \ No newline at end of file +STR_FORCE_REFRESH: "Refresh Screen" +STR_STARRED_PAGES: "Starred Pages" +STR_STAR_PAGE: "Star Page" +STR_NO_STARRED_PAGES: "No starred pages" +STR_PAGE_PREFIX: "p" diff --git a/src/BookmarkStore.h b/src/BookmarkStore.h new file mode 100644 index 00000000..304a97a6 --- /dev/null +++ b/src/BookmarkStore.h @@ -0,0 +1,137 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +// Stores starred/bookmarked pages for a single book. +// Persisted as a binary file on SD card within the book's cache directory. +class BookmarkStore { + public: + struct Bookmark { + uint16_t spineIndex; + uint16_t pageNumber; + }; + + // Load bookmarks from the cache directory (e.g. .crosspoint/epub_/). + void load(const std::string& cachePath) { + basePath = cachePath; + bookmarks.clear(); + dirty = false; + + FsFile f; + if (!Storage.openFileForRead("BKM", getFilePath(), f)) { + return; + } + + uint8_t version; + if (f.read(reinterpret_cast(&version), sizeof(version)) != sizeof(version) || version != FILE_VERSION) { + f.close(); + return; + } + + uint16_t count; + if (f.read(reinterpret_cast(&count), sizeof(count)) != sizeof(count) || count > MAX_BOOKMARKS) { + LOG_ERR("BKM", "Invalid bookmark count: %u", static_cast(count)); + f.close(); + return; + } + + bookmarks.reserve(count); + for (uint16_t i = 0; i < count; i++) { + Bookmark bm; + if (f.read(reinterpret_cast(&bm.spineIndex), sizeof(bm.spineIndex)) != sizeof(bm.spineIndex) || + f.read(reinterpret_cast(&bm.pageNumber), sizeof(bm.pageNumber)) != sizeof(bm.pageNumber)) { + LOG_ERR("BKM", "Truncated bookmarks file at entry %d", i); + bookmarks.clear(); + f.close(); + return; + } + bookmarks.push_back(bm); + } + + f.close(); + LOG_DBG("BKM", "Loaded %d bookmarks", count); + } + + // Save bookmarks to SD card (only if changed). + void save() { + if (!dirty || basePath.empty()) { + return; + } + + if (bookmarks.size() > UINT16_MAX) { + LOG_ERR("BKM", "Too many bookmarks to save: %u", static_cast(bookmarks.size())); + return; + } + + FsFile f; + if (!Storage.openFileForWrite("BKM", getFilePath(), f)) { + LOG_ERR("BKM", "Failed to save bookmarks"); + return; + } + + auto writePodChecked = [&f](const auto& value) { + return f.write(reinterpret_cast(&value), sizeof(value)) == sizeof(value); + }; + + const uint16_t count = static_cast(bookmarks.size()); + bool ok = writePodChecked(FILE_VERSION) && writePodChecked(count); + + for (const auto& bm : bookmarks) { + ok = ok && writePodChecked(bm.spineIndex) && writePodChecked(bm.pageNumber); + } + + ok = ok && f.close(); + if (!ok) { + LOG_ERR("BKM", "Failed while writing bookmarks"); + return; + } + dirty = false; + LOG_DBG("BKM", "Saved %d bookmarks", count); + } + + // Toggle bookmark for the given page. Returns true if now starred, false if removed. + bool toggle(uint16_t spineIndex, uint16_t pageNumber) { + auto it = find(spineIndex, pageNumber); + if (it != bookmarks.end()) { + bookmarks.erase(it); + dirty = true; + return false; + } + bookmarks.push_back({spineIndex, pageNumber}); + dirty = true; + return true; + } + + // Check if a page is starred. + [[nodiscard]] bool has(uint16_t spineIndex, uint16_t pageNumber) const { + return std::any_of(bookmarks.begin(), bookmarks.end(), [spineIndex, pageNumber](const Bookmark& bm) { + return bm.spineIndex == spineIndex && bm.pageNumber == pageNumber; + }); + } + + [[nodiscard]] const std::vector& getAll() const { return bookmarks; } + [[nodiscard]] bool isEmpty() const { return bookmarks.empty(); } + void markDirty() { dirty = true; } + + private: + static constexpr uint8_t FILE_VERSION = 1; + static constexpr uint16_t MAX_BOOKMARKS = 1000; + + std::vector bookmarks; + std::string basePath; + bool dirty = false; + + [[nodiscard]] std::string getFilePath() const { return basePath + "/bookmarks.bin"; } + + std::vector::iterator find(uint16_t spineIndex, uint16_t pageNumber) { + return std::find_if(bookmarks.begin(), bookmarks.end(), [spineIndex, pageNumber](const Bookmark& bm) { + return bm.spineIndex == spineIndex && bm.pageNumber == pageNumber; + }); + } +}; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 1ccd089b..8a788d98 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -128,7 +128,15 @@ class CrossPointSettings { }; // Short power button press actions - enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, FOOTNOTES = 4, SHORT_PWRBTN_COUNT }; + enum SHORT_PWRBTN { + IGNORE = 0, + SLEEP = 1, + PAGE_TURN = 2, + FORCE_REFRESH = 3, + FOOTNOTES = 4, + STAR_PAGE = 5, + SHORT_PWRBTN_COUNT + }; // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; diff --git a/src/SettingsList.h b/src/SettingsList.h index 8678321a..b7643590 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -124,10 +124,10 @@ inline const std::vector list = { {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", StrId::STR_CAT_CONTROLS), - SettingInfo::Enum( - StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, - {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES}, - "shortPwrBtn", StrId::STR_CAT_CONTROLS), + SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, + {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, + StrId::STR_FOOTNOTES, StrId::STR_STAR_PAGE}, + "shortPwrBtn", StrId::STR_CAT_CONTROLS), // --- System --- SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", diff --git a/src/activities/ActivityResult.h b/src/activities/ActivityResult.h index eecf572b..c7c716fe 100644 --- a/src/activities/ActivityResult.h +++ b/src/activities/ActivityResult.h @@ -57,8 +57,13 @@ struct FootnoteResult { std::string href; }; +struct StarredPageResult { + int spineIndex = 0; + int pageNumber = 0; +}; + using ResultVariant = std::variant; + PageResult, SyncResult, NetworkModeResult, FootnoteResult, StarredPageResult>; struct ActivityResult { bool isCancelled = false; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 88c06619..6d062815 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -23,6 +23,7 @@ #include "QrDisplayActivity.h" #include "ReaderUtils.h" #include "RecentBooksStore.h" +#include "StarredPagesActivity.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/ScreenshotUtil.h" @@ -118,6 +119,9 @@ void EpubReaderActivity::onEnter() { } } + // Load bookmarks for this book + bookmarkStore.load(epub->getCachePath()); + // Save current epub as last opened epub and add to recent books APP_STATE.openEpubPath = epub->getPath(); APP_STATE.saveToFile(); @@ -139,6 +143,9 @@ void EpubReaderActivity::onExit() { Activity::onExit(); logReaderMemSnapshot("onExit_before_release"); + // Save bookmarks before exit + bookmarkStore.save(); + // Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -212,7 +219,7 @@ void EpubReaderActivity::loop() { startActivityForResult(std::make_unique( renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride, - bookImageRenderingOverride, SETTINGS.textDarkness), + 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); @@ -265,6 +272,16 @@ void EpubReaderActivity::loop() { return; } + // Star page toggle via short power button press + if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE && + mappedInput.wasReleased(MappedInputManager::Button::Power)) { + if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) { + bookmarkStore.toggle(static_cast(currentSpineIndex), static_cast(section->currentPage)); + requestUpdate(); + } + return; + } + auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; @@ -499,6 +516,22 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction requestUpdate(); break; } + case EpubReaderMenuActivity::MenuAction::STARRED_PAGES: { + startActivityForResult( + std::make_unique(renderer, mappedInput, bookmarkStore.getAll(), epub), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& starred = std::get(result.data); + if (currentSpineIndex != starred.spineIndex || !section || section->currentPage != starred.pageNumber) { + RenderLock lock(*this); + currentSpineIndex = starred.spineIndex; + nextPageNumber = starred.pageNumber; + section.reset(); + } + } + }); + break; + } case EpubReaderMenuActivity::MenuAction::GO_HOME: { onGoHome(); return; @@ -514,6 +547,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction epub->clearCache(); epub->setupCacheDir(); saveProgress(backupSpine, backupPage, backupPageCount); + if (!bookmarkStore.isEmpty()) { + bookmarkStore.markDirty(); + bookmarkStore.save(); + } } } onGoHome(); @@ -1144,7 +1181,9 @@ void EpubReaderActivity::renderStatusBar() const { title = epub->getTitle(); } - GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset); + const bool isStarred = section && bookmarkStore.has(static_cast(currentSpineIndex), + static_cast(section->currentPage)); + GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, isStarred); } void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 8e23dd18..b07aa667 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -5,6 +5,7 @@ #include +#include "BookmarkStore.h" #include "EpubReaderMenuActivity.h" #include "ReaderUtils.h" #include "activities/Activity.h" @@ -52,6 +53,9 @@ class EpubReaderActivity final : public Activity { int8_t bookEmbeddedStyleOverride = -1; int8_t bookImageRenderingOverride = -1; + // Bookmarks (starred pages) + BookmarkStore bookmarkStore; + // Footnote support std::vector currentPageFootnotes; struct SavedPosition { diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 8e1c54cf..5fa0bda7 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -13,7 +13,7 @@ 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 uint8_t initialTextDarkness, const bool hasStarredPages) : MenuListActivity("EpubReaderMenu", renderer, mappedInput), pendingOrientation(currentOrientation), pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride), @@ -23,16 +23,19 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu currentPage(currentPage), totalPages(totalPages), bookProgressPercent(bookProgressPercent) { - buildMenuItems(hasFootnotes); + buildMenuItems(hasFootnotes, hasStarredPages); } -void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { - menuItems.reserve(18); +void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPages) { + menuItems.reserve(19); // --- Navigation --- 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)); + 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)); } @@ -98,6 +101,8 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId return MenuAction::SELECT_CHAPTER; case StrId::STR_GO_TO_PERCENT: return MenuAction::GO_TO_PERCENT; + case StrId::STR_STARRED_PAGES: + return MenuAction::STARRED_PAGES; case StrId::STR_FOOTNOTES: return MenuAction::FOOTNOTES; case StrId::STR_AUTO_TURN_PAGES_PER_MIN: diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index ea1d5dc7..d8f4c099 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -26,6 +26,8 @@ class EpubReaderMenuActivity final : public MenuListActivity { GO_HOME, PULL_REMOTE, PUSH_LOCAL, + SYNC, + STARRED_PAGES, DELETE_CACHE }; @@ -33,13 +35,14 @@ 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 uint8_t initialTextDarkness, + const bool hasStarredPages); void onEnter() override; void render(RenderLock&&) override; private: - void buildMenuItems(bool hasFootnotes); + void buildMenuItems(bool hasFootnotes, bool hasStarredPages); void finishWithAction(MenuAction action); // MenuListActivity overrides diff --git a/src/activities/reader/StarredPagesActivity.cpp b/src/activities/reader/StarredPagesActivity.cpp new file mode 100644 index 00000000..aaea1682 --- /dev/null +++ b/src/activities/reader/StarredPagesActivity.cpp @@ -0,0 +1,137 @@ +#include "StarredPagesActivity.h" + +#include +#include + +#include "MappedInputManager.h" +#include "components/UITheme.h" +#include "fontIds.h" + +int StarredPagesActivity::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 StarredPagesActivity::getItemLabel(int index) const { + const auto& bm = bookmarks[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); + } + 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); + } 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); + } + return std::string(buf); +} + +void StarredPagesActivity::onEnter() { + Activity::onEnter(); + requestUpdate(); +} + +void StarredPagesActivity::onExit() { Activity::onExit(); } + +void StarredPagesActivity::loop() { + const int totalItems = static_cast(bookmarks.size()); + const int pageItems = getPageItems(); + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (!bookmarks.empty()) { + const auto& bm = bookmarks[selectorIndex]; + setResult(StarredPageResult{bm.spineIndex, bm.pageNumber}); + finish(); + } + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + + 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 StarredPagesActivity::render(RenderLock&&) { + renderer.clearScreen(); + + const int totalItems = static_cast(bookmarks.size()); + + if (totalItems == 0) { + renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_NO_STARRED_PAGES), true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return; + } + + 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(); + + // Title + const int titleX = + contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_STARRED_PAGES), EpdFontFamily::BOLD)) / 2; + renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_STARRED_PAGES), true, EpdFontFamily::BOLD); + + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + + // Highlight selection + renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30); + + 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 label = renderer.truncatedText(UI_10_FONT_ID, getItemLabel(itemIndex).c_str(), contentWidth - 40); + 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)); + 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 new file mode 100644 index 00000000..beb290bf --- /dev/null +++ b/src/activities/reader/StarredPagesActivity.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include +#include + +#include "../Activity.h" +#include "BookmarkStore.h" +#include "util/ButtonNavigator.h" + +class StarredPagesActivity final : public Activity { + std::shared_ptr epub; // nullptr for TXT files + const std::vector bookmarks; + ButtonNavigator buttonNavigator; + int selectorIndex = 0; + + int getPageItems() const; + std::string getItemLabel(int index) const; + + public: + explicit StarredPagesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::vector& bookmarks, + std::shared_ptr epub = nullptr) + : Activity("StarredPages", renderer, mappedInput), epub(std::move(epub)), bookmarks(bookmarks) {} + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; +}; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 0df0fe1e..0f81f558 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -12,6 +12,7 @@ #include "MappedInputManager.h" #include "ReaderUtils.h" #include "RecentBooksStore.h" +#include "StarredPagesActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -99,6 +100,9 @@ void TxtReaderActivity::onEnter() { txt->setupCacheDir(); + // Load bookmarks for this file + bookmarkStore.load(txt->getCachePath()); + // Save current txt as last opened file and add to recent books auto filePath = txt->getPath(); auto fileName = filePath.substr(filePath.rfind('/') + 1); @@ -113,6 +117,9 @@ void TxtReaderActivity::onEnter() { void TxtReaderActivity::onExit() { Activity::onExit(); + // Save bookmarks before exit + bookmarkStore.save(); + // Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -143,6 +150,29 @@ void TxtReaderActivity::loop() { return; } + // Star page toggle via short power button press + if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE && + mappedInput.wasReleased(MappedInputManager::Button::Power)) { + bookmarkStore.toggle(0, static_cast(currentPage)); + requestUpdate(); + return; + } + + // Open starred pages list via Confirm button + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) { + startActivityForResult(std::make_unique(renderer, mappedInput, bookmarkStore.getAll()), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& starred = std::get(result.data); + currentPage = starred.pageNumber; + if (currentPage >= totalPages) currentPage = totalPages - 1; + if (currentPage < 0) currentPage = 0; + } + requestUpdate(); + }); + return; + } + auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; @@ -373,7 +403,8 @@ void TxtReaderActivity::renderStatusBar() const { if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { title = txt->getTitle(); } - GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title); + const bool isStarred = bookmarkStore.has(0, static_cast(currentPage)); + GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title, 0, 0, isStarred); } void TxtReaderActivity::saveProgress() const { diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index bef54ce4..f9cb16f8 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -4,6 +4,7 @@ #include +#include "BookmarkStore.h" #include "CrossPointSettings.h" #include "ReaderUtils.h" #include "activities/Activity.h" @@ -16,6 +17,9 @@ class TxtReaderActivity final : public Activity { int pagesUntilFullRefresh = 0; ReaderUtils::InputDrainGuard inputDrainGuard; + // Bookmarks (starred pages) + BookmarkStore bookmarkStore; + // Streaming text reader - stores file offsets for each page std::vector pageOffsets; // File offset for start of each page std::vector currentPageLines; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 1170fc46..ca6fcbdc 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -748,8 +748,8 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou } void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, - const int pageCount, std::string title, const int paddingBottom, - const int textYOffset) const { + const int pageCount, std::string title, const int paddingBottom, const int textYOffset, + const bool isStarred) const { auto metrics = UITheme::getInstance().getMetrics(); int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, @@ -826,9 +826,10 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight; const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0; + const int starReserve = isStarred ? (renderer.getTextWidth(SMALL_FONT_ID, "*") + 6) : 0; const int clockSize = clockTextWidth > 0 ? clockTextWidth + 8 : 0; const int titleMarginLeft = batterySize + clockSize + 30; - const int titleMarginRight = progressTextWidth + 30; + const int titleMarginRight = progressTextWidth + starReserve + 30; // Attempt to center title on the screen, but if title is too wide then later we will center it within the // available space. @@ -852,6 +853,21 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c (availableTitleSpace - titleWidth) / 2, textY, title.c_str()); } + + // Draw star indicator between title and progress text + if (isStarred) { + const int starWidth = renderer.getTextWidth(SMALL_FONT_ID, "*"); + int starX; + if (progressTextWidth > 0) { + // Place star just left of the progress text with a small gap + starX = renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - progressTextWidth - + starWidth - 6; + } else { + // No progress text, place star at right edge + starX = renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - starWidth; + } + renderer.drawText(SMALL_FONT_ID, starX, textY + textYOffset, "*"); + } } void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const { diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 68b5fab0..920912a7 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -145,7 +145,7 @@ class BaseTheme { virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const; virtual void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, std::string title, const int paddingBottom = 0, - const int textYOffset = 0) const; + const int textYOffset = 0, const bool isStarred = false) const; virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, From f9f856f961d0bad814faeb6dcfd96ca400ed5e70 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 18:23:10 +0200 Subject: [PATCH 2/4] Extending --- lib/I18n/translations/english.yaml | 3 + src/BookmarkStore.h | 49 +++++++++- src/activities/reader/EpubReaderActivity.cpp | 42 +++++---- .../reader/EpubReaderMenuActivity.cpp | 22 ++++- .../reader/EpubReaderMenuActivity.h | 7 +- .../reader/StarredPagesActivity.cpp | 91 +++++++++++++++---- src/activities/reader/StarredPagesActivity.h | 10 +- src/activities/reader/TxtReaderActivity.cpp | 2 +- 8 files changed, 177 insertions(+), 49 deletions(-) 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); From f2207cbb113bebb0d88806e7495c2a1319266f69 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 19:05:42 +0200 Subject: [PATCH 3/4] Review comments --- src/BookmarkStore.h | 11 +++++++++-- src/activities/reader/EpubReaderMenuActivity.h | 1 - src/activities/reader/TxtReaderActivity.cpp | 4 +++- src/components/themes/BaseTheme.cpp | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/BookmarkStore.h b/src/BookmarkStore.h index 6121d8a0..e1966b23 100644 --- a/src/BookmarkStore.h +++ b/src/BookmarkStore.h @@ -111,8 +111,15 @@ class BookmarkStore { } } - ok = ok && f.close(); - if (!ok) { + bool closeOk = false; + if (ok) { + closeOk = f.close(); + if (!closeOk) { + LOG_ERR("BKM", "Failed to close bookmarks file"); + return; + } + } else { + f.close(); LOG_ERR("BKM", "Failed while writing bookmarks"); return; } diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 2cf79c65..d5fc72c9 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -26,7 +26,6 @@ class EpubReaderMenuActivity final : public MenuListActivity { GO_HOME, PULL_REMOTE, PUSH_LOCAL, - SYNC, STARRED_PAGES, STAR_PAGE, DELETE_CACHE diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 6a2ec736..35b95330 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -153,7 +153,9 @@ void TxtReaderActivity::loop() { // Star page toggle via short power button press if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE && mappedInput.wasReleased(MappedInputManager::Button::Power)) { - bookmarkStore.toggle(0, static_cast(currentPage)); + if (currentPage >= 0) { + bookmarkStore.toggle(0, static_cast(currentPage)); + } requestUpdate(); return; } diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index ca6fcbdc..4edfb5b5 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -866,7 +866,8 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c // No progress text, place star at right edge starX = renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - starWidth; } - renderer.drawText(SMALL_FONT_ID, starX, textY + textYOffset, "*"); + const int starY = title.empty() ? textY : (textY + textYOffset); + renderer.drawText(SMALL_FONT_ID, starX, starY, "*"); } } From 8f898fc32b929d4d47b28a33fc84724b11ff6733 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 19:08:09 +0200 Subject: [PATCH 4/4] Update languages --- lib/I18n/translations/french.yaml | 7 +++++++ lib/I18n/translations/german.yaml | 7 +++++++ lib/I18n/translations/italian.yaml | 8 ++++++++ lib/I18n/translations/russian.yaml | 7 +++++++ lib/I18n/translations/spanish.yaml | 7 +++++++ 5 files changed, 36 insertions(+) diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index bceccbf4..640e940f 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -482,6 +482,13 @@ STR_SET_SLEEP_SCREEN: "Définir l'écran de veille" STR_SLEEP_SCREEN_SET: "Écran de veille mis à jour !" STR_IMAGE_DISPLAY_BW: ">> N/B" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Niveaux de gris" +STR_READER_BOOKMARKS: "Signets & notes de bas de page" +STR_READER_UTILS: "Utilitaire" +STR_STARRED_PAGES: "Pages marquées" +STR_STAR_PAGE: "Marquer la page" +STR_NO_STARRED_PAGES: "Aucune page marquée" +STR_RENAME: "Renommer" +STR_PAGE_PREFIX: "p" STR_READER_TOOLS: "Outils" STR_READER_NAVIGATION: "Navigation" STR_READER_APPEARANCE: "Apparence" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 24be6a92..39e60ec0 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -357,6 +357,13 @@ STR_SET_SLEEP_SCREEN: "Standby-Bild setzen" STR_SLEEP_SCREEN_SET: "Standby-Bild aktualisiert!" STR_IMAGE_DISPLAY_BW: ">> S/W" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Grau" +STR_READER_BOOKMARKS: "Lesezeichen & Fußnoten" +STR_READER_UTILS: "Hilfsfunktionen" +STR_STARRED_PAGES: "Markierte Seiten" +STR_STAR_PAGE: "Seite markieren" +STR_NO_STARRED_PAGES: "Keine markierten Seiten" +STR_RENAME: "Umbenennen" +STR_PAGE_PREFIX: "S" STR_READER_TOOLS: "Werkzeuge" STR_READER_NAVIGATION: "Navigation" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 198e8b91..3c1fd1e7 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -480,6 +480,14 @@ STR_SET_SLEEP_SCREEN: "Imposta standby" STR_SLEEP_SCREEN_SET: "Schermo standby aggiornato!" STR_IMAGE_DISPLAY_BW: ">> B/N" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Gradazioni di grigio" +STR_READER_BOOKMARKS: "Segnalibri e note" +STR_READER_UTILS: "Utilità" +STR_STARRED_PAGES: "Pagine contrassegnate" +STR_STAR_PAGE: "Contrassegna pagina" +STR_NO_STARRED_PAGES: "Nessuna pagina contrassegnata" +STR_RENAME: "Rinomina" +STR_PAGE_PREFIX: "p" + STR_WEATHER_MOON_INFO: "Luna" STR_WEATHER_SUN_INFO: "Sole" STR_READER_TOOLS: "Strumenti" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 056f7c0b..155301bc 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -482,6 +482,13 @@ STR_SET_SLEEP_SCREEN: "Установить экран сна" STR_SLEEP_SCREEN_SET: "Экран сна обновлён!" STR_IMAGE_DISPLAY_BW: ">> Ч/Б" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Оттенки серого" +STR_READER_BOOKMARKS: "Закладки и сноски" +STR_READER_UTILS: "Утилиты" +STR_STARRED_PAGES: "Отмеченные страницы" +STR_STAR_PAGE: "Отметить страницу" +STR_NO_STARRED_PAGES: "Нет отмеченных страниц" +STR_RENAME: "Переименовать" +STR_PAGE_PREFIX: "с" STR_READER_TOOLS: "Инструменты" STR_READER_NAVIGATION: "Навигация" STR_READER_APPEARANCE: "Внешний вид" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index cd1344b4..45e0ead0 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -482,6 +482,13 @@ STR_SET_SLEEP_SCREEN: "Configurar suspensión" STR_SLEEP_SCREEN_SET: "Pantalla de suspensión actualizada!" STR_IMAGE_DISPLAY_BW: ">> N/B" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Escala de grises" +STR_READER_BOOKMARKS: "Marcadores y notas" +STR_READER_UTILS: "Utilidades" +STR_STARRED_PAGES: "Páginas marcadas" +STR_STAR_PAGE: "Marcar página" +STR_NO_STARRED_PAGES: "No hay páginas marcadas" +STR_RENAME: "Renombrar" +STR_PAGE_PREFIX: "p" STR_READER_TOOLS: "Herramientas" STR_READER_NAVIGATION: "Navegación" STR_READER_APPEARANCE: "Apariencia"