mod: overhaul reader menu with long-press actions and quick toggles
Consolidate dictionary items: remove "Lookup Word History" and "Delete Dictionary Cache" from the menu. Long-press on "Lookup Word" opens history; delete-dict-cache is now a sentinel entry at the bottom of the history list. Replace "Reading Orientation" with "Toggle Portrait/Landscape" that toggles between two configurable preferred orientations (new settings: Preferred Portrait, Preferred Landscape). Long-press opens a manual 4-option orientation sub-menu. Add "Toggle Font Size" menu item that cycles through font sizes and applies on menu exit (with section re-layout). Rename "Letterbox Fill" to "Override Letterbox Fill" and "Sync Progress" to "Sync Reading Progress" in reader menu. All long-press flows use ignoreNextConfirmRelease to prevent the button release from triggering actions on the subsequent screen. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -365,6 +365,11 @@ enum class StrId : uint16_t {
|
|||||||
STR_DICT_CACHE_DELETED,
|
STR_DICT_CACHE_DELETED,
|
||||||
STR_NO_CACHE_TO_DELETE,
|
STR_NO_CACHE_TO_DELETE,
|
||||||
STR_TABLE_OF_CONTENTS,
|
STR_TABLE_OF_CONTENTS,
|
||||||
|
STR_TOGGLE_ORIENTATION,
|
||||||
|
STR_TOGGLE_FONT_SIZE,
|
||||||
|
STR_OVERRIDE_LETTERBOX_FILL,
|
||||||
|
STR_PREFERRED_PORTRAIT,
|
||||||
|
STR_PREFERRED_LANDSCAPE,
|
||||||
// Sentinel - must be last
|
// Sentinel - must be last
|
||||||
_COUNT
|
_COUNT
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ STR_HW_LEFT_LABEL: "Left (3rd button)"
|
|||||||
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
||||||
STR_GO_TO_PERCENT: "Go to %"
|
STR_GO_TO_PERCENT: "Go to %"
|
||||||
STR_GO_HOME_BUTTON: "Go Home"
|
STR_GO_HOME_BUTTON: "Go Home"
|
||||||
STR_SYNC_PROGRESS: "Sync Progress"
|
STR_SYNC_PROGRESS: "Sync Reading Progress"
|
||||||
STR_DELETE_CACHE: "Delete Book Cache"
|
STR_DELETE_CACHE: "Delete Book Cache"
|
||||||
STR_CHAPTER_PREFIX: "Chapter: "
|
STR_CHAPTER_PREFIX: "Chapter: "
|
||||||
STR_PAGES_SEPARATOR: " pages | "
|
STR_PAGES_SEPARATOR: " pages | "
|
||||||
@@ -331,3 +331,8 @@ STR_BOOKMARK_REMOVED: "Bookmark removed"
|
|||||||
STR_DICT_CACHE_DELETED: "Dictionary cache deleted"
|
STR_DICT_CACHE_DELETED: "Dictionary cache deleted"
|
||||||
STR_NO_CACHE_TO_DELETE: "No cache to delete"
|
STR_NO_CACHE_TO_DELETE: "No cache to delete"
|
||||||
STR_TABLE_OF_CONTENTS: "Table of Contents"
|
STR_TABLE_OF_CONTENTS: "Table of Contents"
|
||||||
|
STR_TOGGLE_ORIENTATION: "Toggle Portrait/Landscape"
|
||||||
|
STR_TOGGLE_FONT_SIZE: "Toggle Font Size"
|
||||||
|
STR_OVERRIDE_LETTERBOX_FILL: "Override Letterbox Fill"
|
||||||
|
STR_PREFERRED_PORTRAIT: "Preferred Portrait"
|
||||||
|
STR_PREFERRED_LANDSCAPE: "Preferred Landscape"
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const {
|
|||||||
writer.writeItem(file, fadingFix);
|
writer.writeItem(file, fadingFix);
|
||||||
writer.writeItem(file, embeddedStyle);
|
writer.writeItem(file, embeddedStyle);
|
||||||
writer.writeItem(file, sleepScreenLetterboxFill);
|
writer.writeItem(file, sleepScreenLetterboxFill);
|
||||||
// New fields need to be added at end for backward compatibility
|
// New fields added at end for backward compatibility
|
||||||
|
writer.writeItem(file, preferredPortrait);
|
||||||
|
writer.writeItem(file, preferredLandscape);
|
||||||
|
|
||||||
return writer.item_count;
|
return writer.item_count;
|
||||||
}
|
}
|
||||||
@@ -267,6 +269,10 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
{ uint8_t _ignore; serialization::readPod(inputFile, _ignore); } // legacy: sleepScreenGradientDir
|
{ uint8_t _ignore; serialization::readPod(inputFile, _ignore); } // legacy: sleepScreenGradientDir
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
// New fields added at end for backward compatibility
|
// New fields added at end for backward compatibility
|
||||||
|
readAndValidate(inputFile, preferredPortrait, ORIENTATION_COUNT);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
readAndValidate(inputFile, preferredLandscape, ORIENTATION_COUNT);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
if (frontButtonMappingRead) {
|
if (frontButtonMappingRead) {
|
||||||
|
|||||||
@@ -183,6 +183,12 @@ class CrossPointSettings {
|
|||||||
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
|
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
|
||||||
uint8_t embeddedStyle = 1;
|
uint8_t embeddedStyle = 1;
|
||||||
|
|
||||||
|
// Preferred orientations for the portrait/landscape toggle in the reader menu.
|
||||||
|
// preferredPortrait: PORTRAIT (0) or INVERTED (2)
|
||||||
|
// preferredLandscape: LANDSCAPE_CW (1) or LANDSCAPE_CCW (3)
|
||||||
|
uint8_t preferredPortrait = PORTRAIT;
|
||||||
|
uint8_t preferredLandscape = LANDSCAPE_CW;
|
||||||
|
|
||||||
~CrossPointSettings() = default;
|
~CrossPointSettings() = default;
|
||||||
|
|
||||||
// Get singleton instance
|
// Get singleton instance
|
||||||
|
|||||||
@@ -110,6 +110,23 @@ inline std::vector<SettingInfo> getSettingsList() {
|
|||||||
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
|
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
|
||||||
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
|
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
|
||||||
"orientation", StrId::STR_CAT_READER),
|
"orientation", StrId::STR_CAT_READER),
|
||||||
|
SettingInfo::DynamicEnum(
|
||||||
|
StrId::STR_PREFERRED_PORTRAIT, {StrId::STR_PORTRAIT, StrId::STR_INVERTED},
|
||||||
|
[] { return static_cast<uint8_t>(SETTINGS.preferredPortrait == CrossPointSettings::INVERTED ? 1 : 0); },
|
||||||
|
[](uint8_t idx) {
|
||||||
|
SETTINGS.preferredPortrait = (idx == 1) ? CrossPointSettings::INVERTED : CrossPointSettings::PORTRAIT;
|
||||||
|
},
|
||||||
|
"preferredPortrait", StrId::STR_CAT_READER),
|
||||||
|
SettingInfo::DynamicEnum(
|
||||||
|
StrId::STR_PREFERRED_LANDSCAPE, {StrId::STR_LANDSCAPE_CW, StrId::STR_LANDSCAPE_CCW},
|
||||||
|
[] {
|
||||||
|
return static_cast<uint8_t>(SETTINGS.preferredLandscape == CrossPointSettings::LANDSCAPE_CCW ? 1 : 0);
|
||||||
|
},
|
||||||
|
[](uint8_t idx) {
|
||||||
|
SETTINGS.preferredLandscape =
|
||||||
|
(idx == 1) ? CrossPointSettings::LANDSCAPE_CCW : CrossPointSettings::LANDSCAPE_CW;
|
||||||
|
},
|
||||||
|
"preferredLandscape", StrId::STR_CAT_READER),
|
||||||
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing",
|
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing",
|
||||||
StrId::STR_CAT_READER),
|
StrId::STR_CAT_READER),
|
||||||
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
||||||
|
|||||||
@@ -250,8 +250,8 @@ void EpubReaderActivity::loop() {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new EpubReaderMenuActivity(
|
enterNewActivity(new EpubReaderMenuActivity(
|
||||||
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||||
SETTINGS.orientation, hasDictionary, isBookmarked, epub->getCachePath(),
|
SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(),
|
||||||
[this](const uint8_t orientation) { onReaderMenuBack(orientation); },
|
[this](const uint8_t orientation, const uint8_t fontSize) { onReaderMenuBack(orientation, fontSize); },
|
||||||
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,11 +342,13 @@ void EpubReaderActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) {
|
void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation, const uint8_t fontSize) {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
// Apply the user-selected orientation when the menu is dismissed.
|
// Apply the user-selected orientation when the menu is dismissed.
|
||||||
// This ensures the menu can be navigated without immediately rotating the screen.
|
// This ensures the menu can be navigated without immediately rotating the screen.
|
||||||
applyOrientation(orientation);
|
applyOrientation(orientation);
|
||||||
|
// Apply font size change (no-op if unchanged).
|
||||||
|
applyFontSize(fontSize);
|
||||||
// Force a half refresh on the next render to clear menu/popup artifacts
|
// Force a half refresh on the next render to clear menu/popup artifacts
|
||||||
pagesUntilFullRefresh = 1;
|
pagesUntilFullRefresh = 1;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -563,24 +565,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
}));
|
}));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE: {
|
|
||||||
if (Dictionary::cacheExists()) {
|
|
||||||
Dictionary::deleteCache();
|
|
||||||
{
|
|
||||||
RenderLock lock(*this);
|
|
||||||
GUI.drawPopup(renderer, tr(STR_DICT_CACHE_DELETED));
|
|
||||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{
|
|
||||||
RenderLock lock(*this);
|
|
||||||
GUI.drawPopup(renderer, tr(STR_NO_CACHE_TO_DELETE));
|
|
||||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vTaskDelay(1500 / portTICK_PERIOD_MS);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
||||||
// Calculate values BEFORE we start destroying things
|
// Calculate values BEFORE we start destroying things
|
||||||
const int currentP = section ? section->currentPage : 0;
|
const int currentP = section ? section->currentPage : 0;
|
||||||
@@ -706,7 +690,8 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new LookedUpWordsActivity(
|
enterNewActivity(new LookedUpWordsActivity(
|
||||||
renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation,
|
renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation,
|
||||||
[this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; }));
|
[this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; },
|
||||||
|
true)); // initialSkipRelease: consumed the long-press that triggered this
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||||
@@ -766,6 +751,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
}
|
}
|
||||||
// Handled locally in the menu activity (cycle on Confirm, never dispatched here)
|
// Handled locally in the menu activity (cycle on Confirm, never dispatched here)
|
||||||
case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN:
|
case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN:
|
||||||
|
case EpubReaderMenuActivity::MenuAction::TOGGLE_FONT_SIZE:
|
||||||
case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL:
|
case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -798,6 +784,28 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EpubReaderActivity::applyFontSize(const uint8_t fontSize) {
|
||||||
|
if (SETTINGS.fontSize == fontSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve current reading position so we can restore after reflow.
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
if (section) {
|
||||||
|
cachedSpineIndex = currentSpineIndex;
|
||||||
|
cachedChapterTotalPageCount = section->pageCount;
|
||||||
|
nextPageNumber = section->currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
SETTINGS.fontSize = fontSize;
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
|
||||||
|
// Reset section to force re-layout with the new font size.
|
||||||
|
section.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Failure handling
|
// TODO: Failure handling
|
||||||
void EpubReaderActivity::render(Activity::RenderLock&& lock) {
|
void EpubReaderActivity::render(Activity::RenderLock&& lock) {
|
||||||
if (!epub) {
|
if (!epub) {
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
void saveProgress(int spineIndex, int currentPage, int pageCount);
|
void saveProgress(int spineIndex, int currentPage, int pageCount);
|
||||||
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
||||||
void jumpToPercent(int percent);
|
void jumpToPercent(int percent);
|
||||||
void onReaderMenuBack(uint8_t orientation);
|
void onReaderMenuBack(uint8_t orientation, uint8_t fontSize);
|
||||||
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
||||||
void applyOrientation(uint8_t orientation);
|
void applyOrientation(uint8_t orientation);
|
||||||
|
void applyFontSize(uint8_t fontSize);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
|
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@@ -20,6 +21,55 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Orientation sub-menu mode ---
|
||||||
|
if (orientationSelectMode) {
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (ignoreNextConfirmRelease) {
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
} else {
|
||||||
|
pendingOrientation = static_cast<uint8_t>(orientationSelectIndex);
|
||||||
|
orientationSelectMode = false;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
orientationSelectMode = false;
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buttonNavigator.onNext([this] {
|
||||||
|
orientationSelectIndex = ButtonNavigator::nextIndex(orientationSelectIndex,
|
||||||
|
static_cast<int>(orientationLabels.size()));
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
orientationSelectIndex = ButtonNavigator::previousIndex(orientationSelectIndex,
|
||||||
|
static_cast<int>(orientationLabels.size()));
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Long-press detection (before release checks) ---
|
||||||
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS) {
|
||||||
|
const auto selectedAction = menuItems[selectedIndex].action;
|
||||||
|
if (selectedAction == MenuAction::LOOKUP) {
|
||||||
|
ignoreNextConfirmRelease = true;
|
||||||
|
auto cb = onAction;
|
||||||
|
cb(MenuAction::LOOKED_UP_WORDS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
||||||
|
orientationSelectMode = true;
|
||||||
|
ignoreNextConfirmRelease = true;
|
||||||
|
orientationSelectIndex = pendingOrientation;
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
buttonNavigator.onNext([this] {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
@@ -31,12 +81,29 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use local variables for items we need to check after potential deletion
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (ignoreNextConfirmRelease) {
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto selectedAction = menuItems[selectedIndex].action;
|
const auto selectedAction = menuItems[selectedIndex].action;
|
||||||
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
||||||
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
// Toggle between the two preferred orientations.
|
||||||
pendingOrientation = (pendingOrientation + 1) % orientationLabels.size();
|
// If currently in a portrait-category orientation (Portrait/Inverted), switch to preferredLandscape.
|
||||||
|
// If currently in a landscape-category orientation (CW/CCW), switch to preferredPortrait.
|
||||||
|
const bool isCurrentlyPortrait = (pendingOrientation == CrossPointSettings::PORTRAIT ||
|
||||||
|
pendingOrientation == CrossPointSettings::INVERTED);
|
||||||
|
if (isCurrentlyPortrait) {
|
||||||
|
pendingOrientation = SETTINGS.preferredLandscape;
|
||||||
|
} else {
|
||||||
|
pendingOrientation = SETTINGS.preferredPortrait;
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedAction == MenuAction::TOGGLE_FONT_SIZE) {
|
||||||
|
pendingFontSize = (pendingFontSize + 1) % CrossPointSettings::FONT_SIZE_COUNT;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,9 +125,9 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
// 3. CRITICAL: Return immediately. 'this' is likely deleted now.
|
// 3. CRITICAL: Return immediately. 'this' is likely deleted now.
|
||||||
return;
|
return;
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
// Return the pending orientation to the parent so it can apply on exit.
|
// Return the pending orientation and font size to the parent so it can apply on exit.
|
||||||
onBack(pendingOrientation);
|
onBack(pendingOrientation, pendingFontSize);
|
||||||
return; // Also return here just in case
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +187,11 @@ void EpubReaderMenuActivity::render(Activity::RenderLock&&) {
|
|||||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
||||||
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
|
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) {
|
if (menuItems[i].action == MenuAction::LETTERBOX_FILL) {
|
||||||
// Render current letterbox fill value on the right edge of the content area.
|
// Render current letterbox fill value on the right edge of the content area.
|
||||||
const char* value = I18N.get(letterboxFillLabels[letterboxFillToIndex()]);
|
const char* value = I18N.get(letterboxFillLabels[letterboxFillToIndex()]);
|
||||||
@@ -128,6 +200,30 @@ void EpubReaderMenuActivity::render(Activity::RenderLock&&) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Orientation sub-menu overlay ---
|
||||||
|
if (orientationSelectMode) {
|
||||||
|
constexpr int popupMargin = 15;
|
||||||
|
constexpr int popupLineHeight = 28;
|
||||||
|
const int optionCount = static_cast<int>(orientationLabels.size());
|
||||||
|
const int popupH = popupMargin * 2 + popupLineHeight * optionCount;
|
||||||
|
const int popupW = contentWidth - 60;
|
||||||
|
const int popupX = contentX + (contentWidth - popupW) / 2;
|
||||||
|
const int popupY = 180 + hintGutterHeight;
|
||||||
|
|
||||||
|
renderer.fillRect(popupX - 2, popupY - 2, popupW + 4, popupH + 4, true);
|
||||||
|
renderer.fillRect(popupX, popupY, popupW, popupH, false);
|
||||||
|
|
||||||
|
for (int i = 0; i < optionCount; ++i) {
|
||||||
|
const int optY = popupY + popupMargin + i * popupLineHeight;
|
||||||
|
const bool isSel = (i == orientationSelectIndex);
|
||||||
|
if (isSel) {
|
||||||
|
renderer.fillRect(popupX + 2, optY, popupW - 4, popupLineHeight, true);
|
||||||
|
}
|
||||||
|
const char* label = I18N.get(orientationLabels[i]);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, popupX + popupMargin, optY, label, !isSel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Footer / Hints
|
// Footer / Hints
|
||||||
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_DIR_UP), tr(STR_DIR_DOWN));
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "util/BookSettings.h"
|
#include "util/BookSettings.h"
|
||||||
#include "util/ButtonNavigator.h"
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
LOOKUP,
|
LOOKUP,
|
||||||
LOOKED_UP_WORDS,
|
LOOKED_UP_WORDS,
|
||||||
ROTATE_SCREEN,
|
ROTATE_SCREEN,
|
||||||
|
TOGGLE_FONT_SIZE,
|
||||||
LETTERBOX_FILL,
|
LETTERBOX_FILL,
|
||||||
SELECT_CHAPTER,
|
SELECT_CHAPTER,
|
||||||
GO_TO_BOOKMARK,
|
GO_TO_BOOKMARK,
|
||||||
@@ -26,19 +28,20 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
GO_HOME,
|
GO_HOME,
|
||||||
SYNC,
|
SYNC,
|
||||||
DELETE_CACHE,
|
DELETE_CACHE,
|
||||||
DELETE_DICT_CACHE
|
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||||
const uint8_t currentOrientation, const bool hasDictionary,
|
const uint8_t currentOrientation, const uint8_t currentFontSize,
|
||||||
const bool isBookmarked, const std::string& bookCachePath,
|
const bool hasDictionary, const bool isBookmarked,
|
||||||
const std::function<void(uint8_t)>& onBack,
|
const std::string& bookCachePath,
|
||||||
|
const std::function<void(uint8_t, uint8_t)>& onBack,
|
||||||
const std::function<void(MenuAction)>& onAction)
|
const std::function<void(MenuAction)>& onAction)
|
||||||
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
|
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
|
||||||
menuItems(buildMenuItems(hasDictionary, isBookmarked)),
|
menuItems(buildMenuItems(hasDictionary, isBookmarked)),
|
||||||
title(title),
|
title(title),
|
||||||
pendingOrientation(currentOrientation),
|
pendingOrientation(currentOrientation),
|
||||||
|
pendingFontSize(currentFontSize),
|
||||||
bookCachePath(bookCachePath),
|
bookCachePath(bookCachePath),
|
||||||
currentPage(currentPage),
|
currentPage(currentPage),
|
||||||
totalPages(totalPages),
|
totalPages(totalPages),
|
||||||
@@ -68,8 +71,11 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
std::string title = "Reader Menu";
|
std::string title = "Reader Menu";
|
||||||
uint8_t pendingOrientation = 0;
|
uint8_t pendingOrientation = 0;
|
||||||
|
uint8_t pendingFontSize = 0;
|
||||||
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
|
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
|
||||||
StrId::STR_LANDSCAPE_CCW};
|
StrId::STR_LANDSCAPE_CCW};
|
||||||
|
const std::vector<StrId> fontSizeLabels = {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE,
|
||||||
|
StrId::STR_X_LARGE};
|
||||||
std::string bookCachePath;
|
std::string bookCachePath;
|
||||||
// Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None
|
// Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None
|
||||||
uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL;
|
uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL;
|
||||||
@@ -80,7 +86,15 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
int totalPages = 0;
|
int totalPages = 0;
|
||||||
int bookProgressPercent = 0;
|
int bookProgressPercent = 0;
|
||||||
|
|
||||||
const std::function<void(uint8_t)> onBack;
|
// Long-press state
|
||||||
|
bool ignoreNextConfirmRelease = false;
|
||||||
|
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||||
|
|
||||||
|
// Orientation sub-menu state (entered via long-press on Toggle Portrait/Landscape)
|
||||||
|
bool orientationSelectMode = false;
|
||||||
|
int orientationSelectIndex = 0;
|
||||||
|
|
||||||
|
const std::function<void(uint8_t, uint8_t)> onBack;
|
||||||
const std::function<void(MenuAction)> onAction;
|
const std::function<void(MenuAction)> onAction;
|
||||||
|
|
||||||
// Map the internal override value to an index into letterboxFillLabels.
|
// Map the internal override value to an index into letterboxFillLabels.
|
||||||
@@ -111,19 +125,16 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
}
|
}
|
||||||
if (hasDictionary) {
|
if (hasDictionary) {
|
||||||
items.push_back({MenuAction::LOOKUP, StrId::STR_LOOKUP_WORD});
|
items.push_back({MenuAction::LOOKUP, StrId::STR_LOOKUP_WORD});
|
||||||
items.push_back({MenuAction::LOOKED_UP_WORDS, StrId::STR_LOOKUP_HISTORY});
|
|
||||||
}
|
}
|
||||||
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
|
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_TOGGLE_ORIENTATION});
|
||||||
items.push_back({MenuAction::LETTERBOX_FILL, StrId::STR_LETTERBOX_FILL});
|
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::SELECT_CHAPTER, StrId::STR_TABLE_OF_CONTENTS});
|
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_TABLE_OF_CONTENTS});
|
||||||
items.push_back({MenuAction::GO_TO_BOOKMARK, StrId::STR_GO_TO_BOOKMARK});
|
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::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||||
items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK});
|
items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK});
|
||||||
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
||||||
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
||||||
if (hasDictionary) {
|
|
||||||
items.push_back({MenuAction::DELETE_DICT_CACHE, StrId::STR_DELETE_DICT_CACHE});
|
|
||||||
}
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "LookedUpWordsActivity.h"
|
#include "LookedUpWordsActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -16,6 +17,9 @@ void LookedUpWordsActivity::onEnter() {
|
|||||||
ActivityWithSubactivity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
words = LookupHistory::load(cachePath);
|
words = LookupHistory::load(cachePath);
|
||||||
std::reverse(words.begin(), words.end());
|
std::reverse(words.begin(), words.end());
|
||||||
|
// Append the "Delete Dictionary Cache" sentinel entry
|
||||||
|
words.push_back("\xE2\x80\x94 " + std::string(tr(STR_DELETE_DICT_CACHE)));
|
||||||
|
deleteDictCacheIndex = static_cast<int>(words.size()) - 1;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +43,7 @@ void LookedUpWordsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty list has only the sentinel entry; if even that's gone, just go back.
|
||||||
if (words.empty()) {
|
if (words.empty()) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
@@ -57,6 +62,10 @@ void LookedUpWordsActivity::loop() {
|
|||||||
// Confirm delete
|
// Confirm delete
|
||||||
LookupHistory::removeWord(cachePath, words[pendingDeleteIndex]);
|
LookupHistory::removeWord(cachePath, words[pendingDeleteIndex]);
|
||||||
words.erase(words.begin() + pendingDeleteIndex);
|
words.erase(words.begin() + pendingDeleteIndex);
|
||||||
|
// Adjust sentinel index since we removed an item before it
|
||||||
|
if (deleteDictCacheIndex > pendingDeleteIndex) {
|
||||||
|
deleteDictCacheIndex--;
|
||||||
|
}
|
||||||
if (selectedIndex >= static_cast<int>(words.size())) {
|
if (selectedIndex >= static_cast<int>(words.size())) {
|
||||||
selectedIndex = std::max(0, static_cast<int>(words.size()) - 1);
|
selectedIndex = std::max(0, static_cast<int>(words.size()) - 1);
|
||||||
}
|
}
|
||||||
@@ -72,9 +81,10 @@ void LookedUpWordsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect long press on Confirm to trigger delete
|
// Detect long press on Confirm to trigger delete (only for real word entries, not sentinel)
|
||||||
constexpr unsigned long DELETE_HOLD_MS = 700;
|
constexpr unsigned long DELETE_HOLD_MS = 700;
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= DELETE_HOLD_MS) {
|
if (selectedIndex != deleteDictCacheIndex &&
|
||||||
|
mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= DELETE_HOLD_MS) {
|
||||||
deleteConfirmMode = true;
|
deleteConfirmMode = true;
|
||||||
ignoreNextConfirmRelease = true;
|
ignoreNextConfirmRelease = true;
|
||||||
pendingDeleteIndex = selectedIndex;
|
pendingDeleteIndex = selectedIndex;
|
||||||
@@ -106,6 +116,33 @@ void LookedUpWordsActivity::loop() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
// Consume stale release from long-press navigation into this activity
|
||||||
|
if (ignoreNextConfirmRelease) {
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the "Delete Dictionary Cache" sentinel entry
|
||||||
|
if (selectedIndex == deleteDictCacheIndex) {
|
||||||
|
if (Dictionary::cacheExists()) {
|
||||||
|
Dictionary::deleteCache();
|
||||||
|
{
|
||||||
|
Activity::RenderLock lock(*this);
|
||||||
|
GUI.drawPopup(renderer, tr(STR_DICT_CACHE_DELETED));
|
||||||
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
Activity::RenderLock lock(*this);
|
||||||
|
GUI.drawPopup(renderer, tr(STR_NO_CACHE_TO_DELETE));
|
||||||
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vTaskDelay(1500 / portTICK_PERIOD_MS);
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const std::string& headword = words[selectedIndex];
|
const std::string& headword = words[selectedIndex];
|
||||||
|
|
||||||
Rect popupLayout;
|
Rect popupLayout;
|
||||||
@@ -197,6 +234,9 @@ void LookedUpWordsActivity::render(Activity::RenderLock&&) {
|
|||||||
const int contentTop = hintGutterHeight + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
const int contentTop = hintGutterHeight + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||||
|
|
||||||
|
// The list always has at least the sentinel entry
|
||||||
|
const bool hasRealWords = (deleteDictCacheIndex > 0);
|
||||||
|
|
||||||
if (words.empty()) {
|
if (words.empty()) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, contentTop + 20, "No words looked up yet");
|
renderer.drawCenteredText(UI_10_FONT_ID, contentTop + 20, "No words looked up yet");
|
||||||
} else {
|
} else {
|
||||||
@@ -234,8 +274,8 @@ void LookedUpWordsActivity::render(Activity::RenderLock&&) {
|
|||||||
const auto labels = mappedInput.mapLabels("Cancel", "Delete", "", "");
|
const auto labels = mappedInput.mapLabels("Cancel", "Delete", "", "");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
} else {
|
} else {
|
||||||
// "Hold select to delete" hint above button hints
|
// "Hold select to delete" hint above button hints (only when real words exist)
|
||||||
if (!words.empty()) {
|
if (hasRealWords) {
|
||||||
const char* deleteHint = "Hold select to delete";
|
const char* deleteHint = "Hold select to delete";
|
||||||
const int hintWidth = renderer.getTextWidth(SMALL_FONT_ID, deleteHint);
|
const int hintWidth = renderer.getTextWidth(SMALL_FONT_ID, deleteHint);
|
||||||
const int hintX = contentX + (renderer.getScreenWidth() - hintGutterWidth - hintWidth) / 2;
|
const int hintX = contentX + (renderer.getScreenWidth() - hintGutterWidth - hintWidth) / 2;
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ class LookedUpWordsActivity final : public ActivityWithSubactivity {
|
|||||||
public:
|
public:
|
||||||
explicit LookedUpWordsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& cachePath,
|
explicit LookedUpWordsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& cachePath,
|
||||||
int readerFontId, uint8_t orientation, const std::function<void()>& onBack,
|
int readerFontId, uint8_t orientation, const std::function<void()>& onBack,
|
||||||
const std::function<void()>& onDone)
|
const std::function<void()>& onDone, bool initialSkipRelease = false)
|
||||||
: ActivityWithSubactivity("LookedUpWords", renderer, mappedInput),
|
: ActivityWithSubactivity("LookedUpWords", renderer, mappedInput),
|
||||||
cachePath(cachePath),
|
cachePath(cachePath),
|
||||||
readerFontId(readerFontId),
|
readerFontId(readerFontId),
|
||||||
orientation(orientation),
|
orientation(orientation),
|
||||||
onBack(onBack),
|
onBack(onBack),
|
||||||
onDone(onDone) {}
|
onDone(onDone),
|
||||||
|
ignoreNextConfirmRelease(initialSkipRelease) {}
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
@@ -41,5 +42,9 @@ class LookedUpWordsActivity final : public ActivityWithSubactivity {
|
|||||||
bool ignoreNextConfirmRelease = false;
|
bool ignoreNextConfirmRelease = false;
|
||||||
int pendingDeleteIndex = 0;
|
int pendingDeleteIndex = 0;
|
||||||
|
|
||||||
|
// Sentinel index: the "Delete Dictionary Cache" entry at the end of the list.
|
||||||
|
// -1 if not present (shouldn't happen when dictionary exists).
|
||||||
|
int deleteDictCacheIndex = -1;
|
||||||
|
|
||||||
int getPageItems() const;
|
int getPageItems() const;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user