fix: several bookmarks UX improvments (#2372)
## Summary This PR enhances the EPUB reader's bookmark system with two complementary improvements: a per-page bookmark indicator icon and toggle behavior on the existing long-press action. --- ### What Changed **Bookmark Toggle (was: add-only)** The long-press Confirm action now toggles bookmarks rather than always adding. `addBookmark()` checks whether a bookmark with the same xpath already exists in the in-memory cache: - If found → removes it and shows "Bookmark removed." - If not found → adds it and shows "Bookmark added." A new `STR_BOOKMARK_REMOVED` translation string was added to support the removal message. **Bookmark Icon Indicator** A `BookmarkIcon` is now drawn at the top-right corner of the page whenever the current page has a bookmark. `updateBookmarkFlag()` is called at render time to determine whether the current page is bookmarked. **In-Memory Bookmark Cache** Bookmarks are now loaded into `cachedBookmarks` on `onEnter()` rather than being re-read from disk on every toggle. All subsequent add/remove operations work against this cache and flush to disk, avoiding redundant file reads on each bookmark action. **Faster bookmarks list** Previously, calculating "page X/Y" for each entry required decompressing the entire spine item. We now persist `si`/`pc`/`pp` (spine index, page count, and page progress) in the bookmark JSON when saving, and restore them when loading. This avoids the expensive `toCrossPoint()` loop in `onEnter()`, significantly reducing the cost of initializing the bookmarks list. --- ### Files Changed - `EpubReaderActivity.cpp` — `addBookmark()` toggle logic, `updateBookmarkFlag()` (new), icon rendering in `renderContents()`, cache initialization in `onEnter()` - `EpubReaderActivity.h` — new fields: `currentPageBookmarked`, `bookmarkRemoved`, `cachedBookmarks`; new method declaration `updateBookmarkFlag()` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_ --------- Co-authored-by: Julia Nguyen <julia@uxj.io>
This commit is contained in:
co-authored by
Julia Nguyen
parent
362dcb2a65
commit
1db1442319
@@ -238,8 +238,8 @@ class CrossPointSettings {
|
||||
// Long-press page turn button behavior
|
||||
uint8_t longPressButtonBehavior = OFF;
|
||||
// Long-press Confirm function in EPUB reader (cycles through LONG_PRESS_MENU_FUNCTION values).
|
||||
// Defaults to Bookmark to preserve the upstream long-press-Confirm-adds-bookmark behavior.
|
||||
uint8_t longPressMenuFunction = LP_MENU_BOOKMARK;
|
||||
// Defaults to Disabled so shortcut-based bookmark toggling remains opt-in.
|
||||
uint8_t longPressMenuFunction = LP_MENU_DISABLED;
|
||||
// UI Theme
|
||||
uint8_t uiTheme = LYRA;
|
||||
// Sunlight fading compensation
|
||||
|
||||
@@ -423,6 +423,9 @@ bool JsonSettingsIO::saveBookmarks(const std::vector<BookmarkEntry>& bookmarks,
|
||||
obj["xpath"] = bookmark.xpath;
|
||||
obj["percentage"] = bookmark.percentage;
|
||||
obj["summary"] = bookmark.summary;
|
||||
obj["si"] = bookmark.computedSpineIndex;
|
||||
obj["pc"] = bookmark.computedChapterPageCount;
|
||||
obj["pp"] = bookmark.computedChapterProgress;
|
||||
}
|
||||
|
||||
String json;
|
||||
@@ -447,6 +450,9 @@ bool JsonSettingsIO::loadBookmarks(std::vector<BookmarkEntry>& bookmarks, const
|
||||
bookmark.xpath = obj["xpath"] | std::string("");
|
||||
bookmark.percentage = obj["percentage"] | static_cast<float>(0);
|
||||
bookmark.summary = obj["summary"] | std::string("");
|
||||
bookmark.computedSpineIndex = obj["si"] | static_cast<uint16_t>(0);
|
||||
bookmark.computedChapterPageCount = obj["pc"] | static_cast<uint16_t>(0);
|
||||
bookmark.computedChapterProgress = obj["pp"] | static_cast<uint16_t>(0);
|
||||
}
|
||||
|
||||
LOG_DBG("BKM", "Loaded %zu bookmarks from file", bookmarks.size());
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace {
|
||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||
constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12};
|
||||
constexpr size_t initialBookmarkCacheCapacity = 16;
|
||||
constexpr float bookmarkProgressEpsilon = 0.0001f;
|
||||
|
||||
int clampPercent(int percent) {
|
||||
if (percent < 0) {
|
||||
@@ -63,6 +65,35 @@ bool isInReadFolder(const std::string& path) {
|
||||
return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/';
|
||||
}
|
||||
|
||||
struct ProgressRange {
|
||||
float start;
|
||||
float end;
|
||||
};
|
||||
|
||||
ProgressRange getPageProgressRange(const std::shared_ptr<Epub>& epub, const int spineIndex, const int page,
|
||||
const int pageCount) {
|
||||
if (pageCount <= 1) {
|
||||
return {epub->calculateProgress(spineIndex, 0.0f), epub->calculateProgress(spineIndex, 1.0f)};
|
||||
}
|
||||
|
||||
const float step = 1.0f / static_cast<float>(pageCount - 1);
|
||||
const float anchor = std::clamp(static_cast<float>(page) * step, 0.0f, 1.0f);
|
||||
const float start = std::max(0.0f, anchor - (step * 0.5f));
|
||||
const float end = std::min(1.0f, anchor + (step * 0.5f));
|
||||
return {epub->calculateProgress(spineIndex, start), epub->calculateProgress(spineIndex, end)};
|
||||
}
|
||||
|
||||
bool bookmarkMatchesProgress(const BookmarkEntry& bookmark, const SavedProgressPosition& progress,
|
||||
const ProgressRange& pageRange) {
|
||||
if (bookmark.xpath == progress.xpath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const float bookmarkProgress = std::clamp(bookmark.percentage, 0.0f, 1.0f);
|
||||
return bookmarkProgress + bookmarkProgressEpsilon >= pageRange.start &&
|
||||
bookmarkProgress - bookmarkProgressEpsilon <= pageRange.end;
|
||||
}
|
||||
|
||||
// Pick a non-colliding destination path inside /Read/ for a finished book.
|
||||
// Mirrors the suffixing scheme used elsewhere: "name.epub" -> "name (2).epub", etc.
|
||||
std::string buildReadFolderDestination(const std::string& srcPath) {
|
||||
@@ -165,6 +196,8 @@ void EpubReaderActivity::onEnter() {
|
||||
APP_STATE.saveToFile();
|
||||
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
|
||||
|
||||
loadCachedBookmarks();
|
||||
|
||||
// Trigger first update
|
||||
requestUpdate();
|
||||
}
|
||||
@@ -281,7 +314,7 @@ void EpubReaderActivity::loop() {
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||
SETTINGS.orientation, !currentPageFootnotes.empty()),
|
||||
SETTINGS.orientation, !currentPageFootnotes.empty(), !cachedBookmarks.empty()),
|
||||
[this](const ActivityResult& result) {
|
||||
// Always apply orientation change even if the menu was cancelled
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
@@ -500,6 +533,7 @@ void EpubReaderActivity::jumpToPercent(int percent) {
|
||||
|
||||
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
||||
auto progressChangeResultHandler = [this](const ActivityResult& result) {
|
||||
loadCachedBookmarks();
|
||||
if (!result.isCancelled) {
|
||||
const auto& sync = std::get<ProgressChangeResult>(result.data);
|
||||
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
|
||||
@@ -615,6 +649,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
progressChangeResultHandler);
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::TOGGLE_BOOKMARK: {
|
||||
addBookmark();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,6 +940,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateBookmarkFlag();
|
||||
|
||||
{
|
||||
auto p = section->loadPageFromSectionFile();
|
||||
if (!p) {
|
||||
@@ -933,7 +973,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
}
|
||||
|
||||
if (showBookmarkMessage) {
|
||||
GUI.drawPopup(renderer, tr(STR_BOOKMARK_ADDED));
|
||||
GUI.drawPopup(renderer, bookmarkRemoved ? tr(STR_BOOKMARK_REMOVED) : tr(STR_BOOKMARK_ADDED));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1172,7 +1212,7 @@ void EpubReaderActivity::renderStatusBar() const {
|
||||
title = epub->getTitle();
|
||||
}
|
||||
|
||||
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset);
|
||||
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked);
|
||||
}
|
||||
|
||||
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
|
||||
@@ -1234,11 +1274,31 @@ void EpubReaderActivity::restoreSavedPosition() {
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderActivity::loadCachedBookmarks() {
|
||||
cachedBookmarks.clear();
|
||||
if (cachedBookmarks.capacity() < initialBookmarkCacheCapacity) {
|
||||
cachedBookmarks.reserve(initialBookmarkCacheCapacity);
|
||||
}
|
||||
if (!epub) {
|
||||
currentPageBookmarked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string bmPath = BookmarkUtil::getBookmarkPath(epub->getPath());
|
||||
if (Storage.exists(bmPath.c_str())) {
|
||||
String json = Storage.readFile(bmPath.c_str());
|
||||
if (!json.isEmpty()) {
|
||||
JsonSettingsIO::loadBookmarks(cachedBookmarks, json.c_str());
|
||||
}
|
||||
}
|
||||
updateBookmarkFlag();
|
||||
}
|
||||
|
||||
void EpubReaderActivity::addBookmark() {
|
||||
if (!section || !epub) {
|
||||
return;
|
||||
}
|
||||
LOG_DBG("ERS", "Adding bookmark at spine %d, page %d", currentSpineIndex, section ? section->currentPage : -1);
|
||||
LOG_DBG("ERS", "Toggle bookmark at spine %d, page %d", currentSpineIndex, section ? section->currentPage : -1);
|
||||
int currentPage;
|
||||
int pageCount;
|
||||
{
|
||||
@@ -1247,45 +1307,57 @@ void EpubReaderActivity::addBookmark() {
|
||||
currentPage = section->currentPage;
|
||||
}
|
||||
|
||||
std::string pageText;
|
||||
if (currentPage >= 0 && currentPage < pageCount) {
|
||||
pageText = section->getTextFromSectionFile();
|
||||
SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition());
|
||||
const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, currentPage, pageCount);
|
||||
|
||||
const size_t bookmarkCountBeforeToggle = cachedBookmarks.size();
|
||||
cachedBookmarks.erase(
|
||||
std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(),
|
||||
[&](const BookmarkEntry& b) { return bookmarkMatchesProgress(b, progress, pageRange); }),
|
||||
cachedBookmarks.end());
|
||||
if (cachedBookmarks.size() != bookmarkCountBeforeToggle) {
|
||||
bookmarkRemoved = true;
|
||||
currentPageBookmarked = false;
|
||||
} else {
|
||||
std::string pageText;
|
||||
if (currentPage >= 0 && currentPage < pageCount) {
|
||||
pageText = section->getTextFromSectionFile();
|
||||
}
|
||||
BookmarkEntry entry;
|
||||
entry.percentage = progress.percentage;
|
||||
entry.xpath = progress.xpath;
|
||||
entry.summary = BookmarkUtil::sanitizeBookmarkSummary(pageText);
|
||||
entry.computedSpineIndex = currentSpineIndex;
|
||||
entry.computedChapterPageCount = pageCount;
|
||||
entry.computedChapterProgress = currentPage;
|
||||
cachedBookmarks.insert(cachedBookmarks.begin(), entry);
|
||||
bookmarkRemoved = false;
|
||||
currentPageBookmarked = true;
|
||||
}
|
||||
|
||||
SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition());
|
||||
|
||||
BookmarkEntry entry;
|
||||
entry.percentage = progress.percentage;
|
||||
entry.xpath = progress.xpath;
|
||||
entry.summary = BookmarkUtil::sanitizeBookmarkSummary(pageText);
|
||||
|
||||
// Add bookmark
|
||||
const std::string path = BookmarkUtil::getBookmarkPath(epub->getPath());
|
||||
LOG_DBG("ERS", "Bookmark path: %s", path.c_str());
|
||||
const std::string bookmarksDir = BookmarkUtil::getBookmarksDir();
|
||||
Storage.mkdir(bookmarksDir.c_str());
|
||||
std::vector<BookmarkEntry> bookmarks;
|
||||
if (Storage.exists(path.c_str())) {
|
||||
LOG_DBG("ERS", "Existing bookmark file found, loading bookmarks");
|
||||
String json = Storage.readFile(path.c_str());
|
||||
if (!json.isEmpty()) {
|
||||
JsonSettingsIO::loadBookmarks(bookmarks, json.c_str());
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("ERS", "No existing bookmark file, starting with empty bookmark list");
|
||||
const bool ok = JsonSettingsIO::saveBookmarks(cachedBookmarks, path.c_str());
|
||||
if (!ok) {
|
||||
LOG_ERR("ERS", "Failed to save bookmarks to: %s", path.c_str());
|
||||
}
|
||||
bookmarks.insert(bookmarks.begin(), entry);
|
||||
LOG_DBG("ERS", "Saving bookmark to file: %s", path.c_str());
|
||||
const bool ok = JsonSettingsIO::saveBookmarks(bookmarks, path.c_str());
|
||||
if (ok) {
|
||||
showBookmarkMessage = true;
|
||||
} else {
|
||||
LOG_ERR("ERS", "Failed to save bookmark to: %s", path.c_str());
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderActivity::updateBookmarkFlag() {
|
||||
if (!section || !epub || cachedBookmarks.empty()) {
|
||||
currentPageBookmarked = false;
|
||||
return;
|
||||
}
|
||||
SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition());
|
||||
const ProgressRange pageRange =
|
||||
getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount);
|
||||
currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) {
|
||||
return bookmarkMatchesProgress(b, progress, pageRange);
|
||||
});
|
||||
}
|
||||
|
||||
ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const {
|
||||
ScreenshotInfo info;
|
||||
info.readerType = ScreenshotInfo::ReaderType::Epub;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "BookmarkEntry.h"
|
||||
#include "EpubReaderMenuActivity.h"
|
||||
#include "ProgressMapper.h"
|
||||
#include "activities/Activity.h"
|
||||
@@ -34,6 +35,9 @@ class EpubReaderActivity final : public Activity {
|
||||
bool automaticPageTurnActive = false;
|
||||
bool showBookmarkMessage = false;
|
||||
bool ignoreNextConfirmRelease = false;
|
||||
bool currentPageBookmarked = false;
|
||||
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
|
||||
std::vector<BookmarkEntry> cachedBookmarks;
|
||||
// Tracks whether this book is currently removed from Recent Books by the
|
||||
// removeReadBooksFromRecents feature (set at End-of-Book, cleared if paged back in).
|
||||
bool recentsEntryRemoved = false;
|
||||
@@ -66,7 +70,9 @@ class EpubReaderActivity final : public Activity {
|
||||
void applyOrientation(uint8_t orientation);
|
||||
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
||||
void pageTurn(bool isForwardTurn);
|
||||
void loadCachedBookmarks();
|
||||
void addBookmark();
|
||||
void updateBookmarkFlag();
|
||||
|
||||
// Footnote navigation
|
||||
void navigateToHref(const std::string& href, bool savePosition = false);
|
||||
|
||||
@@ -39,14 +39,6 @@ void EpubReaderBookmarksActivity::onEnter() {
|
||||
bookmarks.shrink_to_fit();
|
||||
} else {
|
||||
JsonSettingsIO::loadBookmarks(bookmarks, json.c_str());
|
||||
|
||||
// pre-compute bookmark page values for quicker rendering
|
||||
for (auto& bookmark : bookmarks) {
|
||||
CrossPointPosition pos = ProgressMapper::toCrossPoint(epub, {bookmark.xpath, bookmark.percentage}, renderer);
|
||||
bookmark.computedSpineIndex = pos.spineIndex;
|
||||
bookmark.computedChapterPageCount = pos.totalPages;
|
||||
bookmark.computedChapterProgress = pos.pageNumber;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("EPB", "No bookmark file found at %s, starting with empty bookmarks", path.c_str());
|
||||
@@ -93,6 +85,14 @@ void EpubReaderBookmarksActivity::loop() {
|
||||
selectorIndex--;
|
||||
}
|
||||
|
||||
if (bookmarks.empty()) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
confirmingDelete = DELETE_MODE_OFF;
|
||||
return;
|
||||
@@ -186,9 +186,12 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) {
|
||||
auto bookmark = bookmarks.at(confirmingDelete >= DELETE_MODE_DISPLAY ? selectorIndex : index);
|
||||
auto tocIndex = epub->getTocIndexForSpineIndex(bookmark.computedSpineIndex);
|
||||
auto tocTitle = (tocIndex >= 0) ? (epub->getTocItem(tocIndex)).title : tr(STR_UNNAMED);
|
||||
return std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - " +
|
||||
std::to_string(bookmark.computedChapterProgress + 1) + "/" +
|
||||
std::to_string(bookmark.computedChapterPageCount) + " - " + tocTitle;
|
||||
std::string subtitle = std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - ";
|
||||
if (bookmark.computedChapterPageCount > 0) {
|
||||
subtitle += std::to_string(bookmark.computedChapterProgress + 1) + "/" +
|
||||
std::to_string(bookmark.computedChapterPageCount) + " - ";
|
||||
}
|
||||
return subtitle + tocTitle;
|
||||
};
|
||||
const auto getBookmarkIcon = [isPortrait](int index) {
|
||||
// only enabled icon in portrait mode due to limitation with rotating icons for other orientations
|
||||
@@ -208,11 +211,8 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) {
|
||||
getBookmarkTitle, getBookmarkSubtitle, getBookmarkIcon);
|
||||
|
||||
GUI.drawHelpText(renderer, Rect{contentX, pageHeight - hintGutterBottom, contentWidth, LINE_HEIGHT},
|
||||
tr(STR_HOLD_CONFIRM_TO_DELETE));
|
||||
tr(STR_HOLD_OPEN_TO_DELETE));
|
||||
}
|
||||
} else {
|
||||
GUI.drawHelpText(renderer, Rect{contentX, LINE_HEIGHT * 2, contentWidth, LINE_HEIGHT},
|
||||
tr(STR_BOOKMARK_INSTRUCTIONS));
|
||||
}
|
||||
|
||||
const auto backLabel = confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_CANCEL) : tr(STR_BACK);
|
||||
|
||||
@@ -10,23 +10,27 @@
|
||||
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::string& title, const int currentPage, const int totalPages,
|
||||
const int bookProgressPercent, const uint8_t currentOrientation,
|
||||
const bool hasFootnotes)
|
||||
const bool hasFootnotes, const bool hasBookmarks)
|
||||
: Activity("EpubReaderMenu", renderer, mappedInput),
|
||||
menuItems(buildMenuItems(hasFootnotes)),
|
||||
menuItems(buildMenuItems(hasFootnotes, hasBookmarks)),
|
||||
title(title),
|
||||
pendingOrientation(currentOrientation),
|
||||
currentPage(currentPage),
|
||||
totalPages(totalPages),
|
||||
bookProgressPercent(bookProgressPercent) {}
|
||||
|
||||
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
||||
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
|
||||
bool hasBookmarks) {
|
||||
std::vector<MenuItem> items;
|
||||
items.reserve(11);
|
||||
items.reserve(12);
|
||||
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
||||
if (hasFootnotes) {
|
||||
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
||||
}
|
||||
items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS});
|
||||
if (hasBookmarks) {
|
||||
items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS});
|
||||
}
|
||||
items.push_back({MenuAction::TOGGLE_BOOKMARK, StrId::STR_TOGGLE_BOOKMARK});
|
||||
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
|
||||
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
|
||||
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||
|
||||
@@ -18,6 +18,7 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
AUTO_PAGE_TURN,
|
||||
ROTATE_SCREEN,
|
||||
BOOKMARKS,
|
||||
TOGGLE_BOOKMARK,
|
||||
SCREENSHOT,
|
||||
DISPLAY_QR,
|
||||
GO_HOME,
|
||||
@@ -27,7 +28,7 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
|
||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||
const uint8_t currentOrientation, const bool hasFootnotes);
|
||||
const uint8_t currentOrientation, const bool hasFootnotes, bool hasBookmarks);
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
@@ -40,7 +41,7 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
StrId labelId;
|
||||
};
|
||||
|
||||
static std::vector<MenuItem> buildMenuItems(bool hasFootnotes);
|
||||
static std::vector<MenuItem> buildMenuItems(bool hasFootnotes, bool hasBookmarks);
|
||||
|
||||
// Fixed menu layout
|
||||
const std::vector<MenuItem> menuItems;
|
||||
|
||||
@@ -10,3 +10,8 @@ static const uint8_t BookmarkIcon[] = {
|
||||
0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
// size: 16x16
|
||||
static const uint8_t BookmarkStatusIcon[] = {0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F,
|
||||
0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8,
|
||||
0x0F, 0x78, 0x0E, 0x38, 0x0C, 0x18, 0x08, 0x08, 0x00, 0x00};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "I18n.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "components/icons/bookmark.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
// Internal constants
|
||||
@@ -20,6 +21,27 @@ namespace {
|
||||
constexpr int homeMenuMargin = 20;
|
||||
constexpr int homeMarginTop = 30;
|
||||
constexpr int subtitleY = 738;
|
||||
constexpr int bookmarkStatusIconWidth = 16;
|
||||
constexpr int bookmarkStatusIconHeight = 14;
|
||||
constexpr int bookmarkStatusIconGap = 4;
|
||||
constexpr int bookmarkStatusIconTopCrop = 2;
|
||||
|
||||
bool statusBarTextLaneVisible() {
|
||||
return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
|
||||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
|
||||
(SETTINGS.statusBarClock && halClock.isAvailable());
|
||||
}
|
||||
|
||||
void drawBookmarkStatusIcon(const GfxRenderer& renderer, const int x, const int y) {
|
||||
constexpr int bytesPerRow = bookmarkStatusIconWidth / 8;
|
||||
for (int row = 0; row < bookmarkStatusIconHeight; ++row) {
|
||||
for (int col = 0; col < bookmarkStatusIconWidth; ++col) {
|
||||
const uint8_t byte = BookmarkStatusIcon[(row + bookmarkStatusIconTopCrop) * bytesPerRow + col / 8];
|
||||
const uint8_t mask = 1U << (7 - (col % 8));
|
||||
renderer.drawPixel(x + col, y + row, (byte & mask) != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -727,11 +749,12 @@ 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 bool fillMargin) const {
|
||||
const bool fillMargin, const bool isPageBookmarked) const {
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
||||
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
||||
&orientedMarginLeft);
|
||||
const bool showStatusBarTextLane = statusBarTextLaneVisible();
|
||||
|
||||
// Draw Progress Text
|
||||
const auto screenHeight = renderer.getScreenHeight();
|
||||
@@ -777,14 +800,24 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
|
||||
renderer.fillRect(barMarginLeft, progressBarY, barWidth, barHeight, true);
|
||||
}
|
||||
|
||||
// Draw Bookmark
|
||||
const int leftClusterX = metrics.statusBarHorizontalMargin + orientedMarginLeft + 1;
|
||||
const bool showBookmarkIcon = showStatusBarTextLane && isPageBookmarked;
|
||||
const int bookmarkReserveWidth = showBookmarkIcon ? (bookmarkStatusIconWidth + bookmarkStatusIconGap) : 0;
|
||||
if (showBookmarkIcon) {
|
||||
const int bookmarkY = textY + 5;
|
||||
drawBookmarkStatusIcon(renderer, leftClusterX, bookmarkY);
|
||||
}
|
||||
|
||||
// Draw Battery
|
||||
const bool showBatteryPercentage =
|
||||
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
|
||||
int leftClusterWidth = bookmarkReserveWidth;
|
||||
if (SETTINGS.statusBarBattery) {
|
||||
GUI.drawBatteryLeft(renderer,
|
||||
Rect{metrics.statusBarHorizontalMargin + orientedMarginLeft + 1, textY, metrics.batteryWidth,
|
||||
metrics.batteryHeight},
|
||||
Rect{leftClusterX + bookmarkReserveWidth, textY, metrics.batteryWidth, metrics.batteryHeight},
|
||||
showBatteryPercentage);
|
||||
leftClusterWidth += showBatteryPercentage ? 50 : 20;
|
||||
}
|
||||
|
||||
// Draw Clock (X3 only — DS3231 RTC)
|
||||
@@ -808,8 +841,7 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
|
||||
const int rendererableScreenWidth =
|
||||
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
|
||||
|
||||
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
||||
const int titleMarginLeft = batterySize + 30;
|
||||
const int titleMarginLeft = leftClusterWidth + 30;
|
||||
const int clockReserve = clockTextWidth > 0 ? (clockTextWidth + 10) : 0;
|
||||
const int titleMarginRight = progressTextWidth + clockReserve + 30;
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ class BaseTheme {
|
||||
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
|
||||
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 bool fillMargin = true) const;
|
||||
const bool fillMargin = true, const bool isPageBookmarked = false) const;
|
||||
void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
|
||||
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false,
|
||||
int contentStartX = 0, int contentWidth = 0) const;
|
||||
|
||||
Reference in New Issue
Block a user