1. Clear ignoreNextConfirmRelease after transferring state to child activity, so the next Confirm press isn't silently consumed. 2. Add conditional FOOTNOTES entry to reader menu when the current book has footnotes. 3. Guard clock-minute requestUpdate() with !isReaderActivity() to prevent full e-ink re-renders every minute while reading. Made-with: Cursor
190 lines
8.3 KiB
C++
190 lines
8.3 KiB
C++
#include "EpubReaderMenuActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <I18n.h>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "MappedInputManager.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
|
|
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, bool isBookmarked, uint8_t currentFontSize,
|
|
const std::string& bookCachePath)
|
|
: Activity("EpubReaderMenu", renderer, mappedInput),
|
|
menuItems(buildMenuItems(hasFootnotes, isBookmarked)),
|
|
title(title),
|
|
pendingOrientation(currentOrientation),
|
|
pendingFontSize(currentFontSize < CrossPointSettings::FONT_SIZE_COUNT ? currentFontSize : 0),
|
|
currentPage(currentPage),
|
|
totalPages(totalPages),
|
|
bookProgressPercent(bookProgressPercent),
|
|
bookCachePath(bookCachePath) {
|
|
if (!bookCachePath.empty()) {
|
|
auto bookSettings = BookSettings::load(bookCachePath);
|
|
pendingLetterboxFill = bookSettings.letterboxFillOverride;
|
|
}
|
|
}
|
|
|
|
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
|
|
bool isBookmarked) {
|
|
std::vector<MenuItem> items;
|
|
items.reserve(16);
|
|
// Mod menu order
|
|
if (isBookmarked) {
|
|
items.push_back({MenuAction::REMOVE_BOOKMARK, StrId::STR_REMOVE_BOOKMARK});
|
|
} else {
|
|
items.push_back({MenuAction::ADD_BOOKMARK, StrId::STR_ADD_BOOKMARK});
|
|
}
|
|
items.push_back({MenuAction::DICTIONARY, StrId::STR_DICTIONARY});
|
|
if (hasFootnotes) {
|
|
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
|
}
|
|
items.push_back({MenuAction::TOGGLE_ORIENTATION, StrId::STR_TOGGLE_ORIENTATION});
|
|
items.push_back({MenuAction::TOGGLE_FONT_SIZE, StrId::STR_TOGGLE_FONT_SIZE});
|
|
items.push_back({MenuAction::LETTERBOX_FILL, StrId::STR_OVERRIDE_LETTERBOX_FILL});
|
|
items.push_back({MenuAction::TABLE_OF_CONTENTS, StrId::STR_TABLE_OF_CONTENTS});
|
|
items.push_back({MenuAction::GO_TO_BOOKMARK, StrId::STR_GO_TO_BOOKMARK});
|
|
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
|
items.push_back({MenuAction::CLOSE_BOOK, StrId::STR_CLOSE_BOOK});
|
|
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
|
items.push_back({MenuAction::PUSH_AND_SLEEP, StrId::STR_PUSH_AND_SLEEP});
|
|
items.push_back({MenuAction::MANAGE_BOOK, StrId::STR_MANAGE_BOOK});
|
|
return items;
|
|
}
|
|
|
|
void EpubReaderMenuActivity::onEnter() {
|
|
Activity::onEnter();
|
|
requestUpdate();
|
|
}
|
|
|
|
void EpubReaderMenuActivity::onExit() { Activity::onExit(); }
|
|
|
|
void EpubReaderMenuActivity::loop() {
|
|
// Handle navigation
|
|
buttonNavigator.onNext([this] {
|
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator.onPrevious([this] {
|
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
const auto selectedAction = menuItems[selectedIndex].action;
|
|
if (selectedAction == MenuAction::TOGGLE_ORIENTATION) {
|
|
// Toggle between preferred portrait and preferred landscape.
|
|
const bool isCurrentlyPortrait =
|
|
(pendingOrientation == CrossPointSettings::PORTRAIT || pendingOrientation == CrossPointSettings::INVERTED);
|
|
pendingOrientation = isCurrentlyPortrait ? SETTINGS.preferredLandscape : SETTINGS.preferredPortrait;
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
if (selectedAction == MenuAction::TOGGLE_FONT_SIZE) {
|
|
pendingFontSize = (pendingFontSize + 1) % CrossPointSettings::FONT_SIZE_COUNT;
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
if (selectedAction == MenuAction::LETTERBOX_FILL) {
|
|
int idx = (letterboxFillToIndex() + 1) % LETTERBOX_FILL_OPTION_COUNT;
|
|
pendingLetterboxFill = indexToLetterboxFill(idx);
|
|
if (!bookCachePath.empty()) saveLetterboxFill();
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption, pendingFontSize});
|
|
finish();
|
|
return;
|
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
ActivityResult result;
|
|
result.isCancelled = true;
|
|
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption, pendingFontSize};
|
|
setResult(std::move(result));
|
|
finish();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void EpubReaderMenuActivity::render(RenderLock&&) {
|
|
renderer.clearScreen();
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto orientation = renderer.getOrientation();
|
|
// Landscape orientation: button hints are drawn along a vertical edge, so we
|
|
// reserve a horizontal gutter to prevent overlap with menu content.
|
|
const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise;
|
|
const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise;
|
|
// Inverted portrait: button hints appear near the logical top, so we reserve
|
|
// vertical space to keep the header and list clear.
|
|
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
|
|
const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0;
|
|
// Landscape CW places hints on the left edge; CCW keeps them on the right.
|
|
const int contentX = isLandscapeCw ? hintGutterWidth : 0;
|
|
const int contentWidth = pageWidth - hintGutterWidth;
|
|
const int hintGutterHeight = isPortraitInverted ? 50 : 0;
|
|
const int contentY = hintGutterHeight;
|
|
|
|
// Title
|
|
const std::string truncTitle =
|
|
renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentWidth - 40, EpdFontFamily::BOLD);
|
|
// Manual centering so we can respect the content gutter.
|
|
const int titleX =
|
|
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2;
|
|
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, truncTitle.c_str(), true, EpdFontFamily::BOLD);
|
|
|
|
// Progress summary
|
|
std::string progressLine;
|
|
if (totalPages > 0) {
|
|
progressLine = std::string(tr(STR_CHAPTER_PREFIX)) + std::to_string(currentPage) + "/" +
|
|
std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR));
|
|
}
|
|
progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%";
|
|
renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str());
|
|
|
|
// Menu Items
|
|
const int startY = 75 + contentY;
|
|
constexpr int lineHeight = 30;
|
|
|
|
for (size_t i = 0; i < menuItems.size(); ++i) {
|
|
const int displayY = startY + (i * lineHeight);
|
|
const bool isSelected = (static_cast<int>(i) == selectedIndex);
|
|
|
|
if (isSelected) {
|
|
// Highlight only the content area so we don't paint over hint gutters.
|
|
renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true);
|
|
}
|
|
|
|
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, I18N.get(menuItems[i].labelId), !isSelected);
|
|
|
|
if (menuItems[i].action == MenuAction::TOGGLE_ORIENTATION) {
|
|
const char* value = I18N.get(orientationLabels[pendingOrientation]);
|
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
|
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
|
|
}
|
|
|
|
if (menuItems[i].action == MenuAction::TOGGLE_FONT_SIZE) {
|
|
const char* value = I18N.get(fontSizeLabels[pendingFontSize]);
|
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
|
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
|
|
}
|
|
|
|
if (menuItems[i].action == MenuAction::LETTERBOX_FILL) {
|
|
const char* value = I18N.get(letterboxFillLabels[letterboxFillToIndex()]);
|
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
|
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
|
|
}
|
|
}
|
|
|
|
// Footer / Hints
|
|
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();
|
|
}
|