From 4aabcc934c05dfa266fa84ee8f61707a08f76933 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 18:22:54 +0200 Subject: [PATCH] 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,