diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index ec18a2b4..3eca31ec 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -365,6 +365,11 @@ enum class StrId : uint16_t { STR_DICT_CACHE_DELETED, STR_NO_CACHE_TO_DELETE, 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 _COUNT }; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 4b752403..e0e989cc 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -281,7 +281,7 @@ STR_HW_LEFT_LABEL: "Left (3rd button)" STR_HW_RIGHT_LABEL: "Right (4th button)" STR_GO_TO_PERCENT: "Go to %" STR_GO_HOME_BUTTON: "Go Home" -STR_SYNC_PROGRESS: "Sync Progress" +STR_SYNC_PROGRESS: "Sync Reading Progress" STR_DELETE_CACHE: "Delete Book Cache" STR_CHAPTER_PREFIX: "Chapter: " STR_PAGES_SEPARATOR: " pages | " @@ -331,3 +331,8 @@ STR_BOOKMARK_REMOVED: "Bookmark removed" STR_DICT_CACHE_DELETED: "Dictionary cache deleted" STR_NO_CACHE_TO_DELETE: "No cache to delete" 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" diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index e151dafc..15912766 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -135,7 +135,9 @@ uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const { writer.writeItem(file, fadingFix); writer.writeItem(file, embeddedStyle); 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; } @@ -267,6 +269,10 @@ bool CrossPointSettings::loadFromFile() { { uint8_t _ignore; serialization::readPod(inputFile, _ignore); } // legacy: sleepScreenGradientDir if (++settingsRead >= fileSettingsCount) break; // 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); if (frontButtonMappingRead) { diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c345a9d9..d7fade36 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -183,6 +183,12 @@ class CrossPointSettings { // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) 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; // Get singleton instance diff --git a/src/SettingsList.h b/src/SettingsList.h index 88fbd889..2f78f3f9 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -110,6 +110,23 @@ inline std::vector getSettingsList() { SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, "orientation", StrId::STR_CAT_READER), + SettingInfo::DynamicEnum( + StrId::STR_PREFERRED_PORTRAIT, {StrId::STR_PORTRAIT, StrId::STR_INVERTED}, + [] { return static_cast(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(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", StrId::STR_CAT_READER), SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index b74d147c..9f67bc50 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -250,8 +250,8 @@ void EpubReaderActivity::loop() { exitActivity(); enterNewActivity(new EpubReaderMenuActivity( this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, - SETTINGS.orientation, hasDictionary, isBookmarked, epub->getCachePath(), - [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, + SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(), + [this](const uint8_t orientation, const uint8_t fontSize) { onReaderMenuBack(orientation, fontSize); }, [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(); // Apply the user-selected orientation when the menu is dismissed. // This ensures the menu can be navigated without immediately rotating the screen. 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 pagesUntilFullRefresh = 1; requestUpdate(); @@ -563,24 +565,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction })); 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: { // Calculate values BEFORE we start destroying things const int currentP = section ? section->currentPage : 0; @@ -706,7 +690,8 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction exitActivity(); enterNewActivity(new LookedUpWordsActivity( 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; } 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) case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN: + case EpubReaderMenuActivity::MenuAction::TOGGLE_FONT_SIZE: case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL: 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 void EpubReaderActivity::render(Activity::RenderLock&& lock) { if (!epub) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 3d4240af..94ce2573 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -33,9 +33,10 @@ class EpubReaderActivity final : public ActivityWithSubactivity { void saveProgress(int spineIndex, int currentPage, int pageCount); // Jump to a percentage of the book (0-100), mapping it to spine and page. void jumpToPercent(int percent); - void onReaderMenuBack(uint8_t orientation); + void onReaderMenuBack(uint8_t orientation, uint8_t fontSize); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); void applyOrientation(uint8_t orientation); + void applyFontSize(uint8_t fontSize); public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 2653478f..ad7206ac 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -3,6 +3,7 @@ #include #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" @@ -20,6 +21,55 @@ void EpubReaderMenuActivity::loop() { return; } + // --- Orientation sub-menu mode --- + if (orientationSelectMode) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (ignoreNextConfirmRelease) { + ignoreNextConfirmRelease = false; + } else { + pendingOrientation = static_cast(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(orientationLabels.size())); + requestUpdate(); + }); + buttonNavigator.onPrevious([this] { + orientationSelectIndex = ButtonNavigator::previousIndex(orientationSelectIndex, + static_cast(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 buttonNavigator.onNext([this] { selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast(menuItems.size())); @@ -31,12 +81,29 @@ void EpubReaderMenuActivity::loop() { requestUpdate(); }); - // Use local variables for items we need to check after potential deletion if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (ignoreNextConfirmRelease) { + ignoreNextConfirmRelease = false; + return; + } + const auto selectedAction = menuItems[selectedIndex].action; if (selectedAction == MenuAction::ROTATE_SCREEN) { - // Cycle orientation preview locally; actual rotation happens on menu exit. - pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); + // Toggle between the two preferred orientations. + // 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(); return; } @@ -58,9 +125,9 @@ void EpubReaderMenuActivity::loop() { // 3. CRITICAL: Return immediately. 'this' is likely deleted now. return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - // Return the pending orientation to the parent so it can apply on exit. - onBack(pendingOrientation); - return; // Also return here just in case + // Return the pending orientation and font size to the parent so it can apply on exit. + onBack(pendingOrientation, pendingFontSize); + return; } } @@ -120,6 +187,11 @@ void EpubReaderMenuActivity::render(Activity::RenderLock&&) { 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) { // Render current letterbox fill value on the right edge of the content area. 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(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 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); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 9b6eb559..a8b9e853 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -7,6 +7,7 @@ #include #include "../ActivityWithSubactivity.h" +#include "CrossPointSettings.h" #include "util/BookSettings.h" #include "util/ButtonNavigator.h" @@ -19,6 +20,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { LOOKUP, LOOKED_UP_WORDS, ROTATE_SCREEN, + TOGGLE_FONT_SIZE, LETTERBOX_FILL, SELECT_CHAPTER, GO_TO_BOOKMARK, @@ -26,19 +28,20 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { GO_HOME, SYNC, DELETE_CACHE, - DELETE_DICT_CACHE }; 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 hasDictionary, - const bool isBookmarked, const std::string& bookCachePath, - const std::function& onBack, + const uint8_t currentOrientation, const uint8_t currentFontSize, + const bool hasDictionary, const bool isBookmarked, + const std::string& bookCachePath, + const std::function& onBack, const std::function& onAction) : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), menuItems(buildMenuItems(hasDictionary, isBookmarked)), title(title), pendingOrientation(currentOrientation), + pendingFontSize(currentFontSize), bookCachePath(bookCachePath), currentPage(currentPage), totalPages(totalPages), @@ -68,8 +71,11 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { ButtonNavigator buttonNavigator; std::string title = "Reader Menu"; uint8_t pendingOrientation = 0; + uint8_t pendingFontSize = 0; const std::vector orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}; + const std::vector fontSizeLabels = {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, + StrId::STR_X_LARGE}; std::string bookCachePath; // Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL; @@ -80,7 +86,15 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { int totalPages = 0; int bookProgressPercent = 0; - const std::function 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 onBack; const std::function onAction; // Map the internal override value to an index into letterboxFillLabels. @@ -111,19 +125,16 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { } if (hasDictionary) { 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::LETTERBOX_FILL, StrId::STR_LETTERBOX_FILL}); + items.push_back({MenuAction::ROTATE_SCREEN, 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::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_PERCENT, StrId::STR_GO_TO_PERCENT}); items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK}); items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); 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; } diff --git a/src/activities/reader/LookedUpWordsActivity.cpp b/src/activities/reader/LookedUpWordsActivity.cpp index cfc8a329..c301d96f 100644 --- a/src/activities/reader/LookedUpWordsActivity.cpp +++ b/src/activities/reader/LookedUpWordsActivity.cpp @@ -1,6 +1,7 @@ #include "LookedUpWordsActivity.h" #include +#include #include @@ -16,6 +17,9 @@ void LookedUpWordsActivity::onEnter() { ActivityWithSubactivity::onEnter(); words = LookupHistory::load(cachePath); 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(words.size()) - 1; requestUpdate(); } @@ -39,6 +43,7 @@ void LookedUpWordsActivity::loop() { return; } + // Empty list has only the sentinel entry; if even that's gone, just go back. if (words.empty()) { if (mappedInput.wasReleased(MappedInputManager::Button::Back) || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -57,6 +62,10 @@ void LookedUpWordsActivity::loop() { // Confirm delete LookupHistory::removeWord(cachePath, words[pendingDeleteIndex]); words.erase(words.begin() + pendingDeleteIndex); + // Adjust sentinel index since we removed an item before it + if (deleteDictCacheIndex > pendingDeleteIndex) { + deleteDictCacheIndex--; + } if (selectedIndex >= static_cast(words.size())) { selectedIndex = std::max(0, static_cast(words.size()) - 1); } @@ -72,9 +81,10 @@ void LookedUpWordsActivity::loop() { 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; - 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; ignoreNextConfirmRelease = true; pendingDeleteIndex = selectedIndex; @@ -106,6 +116,33 @@ void LookedUpWordsActivity::loop() { }); 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]; Rect popupLayout; @@ -197,6 +234,9 @@ void LookedUpWordsActivity::render(Activity::RenderLock&&) { const int contentTop = hintGutterHeight + metrics.topPadding + metrics.headerHeight + 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()) { renderer.drawCenteredText(UI_10_FONT_ID, contentTop + 20, "No words looked up yet"); } else { @@ -234,8 +274,8 @@ void LookedUpWordsActivity::render(Activity::RenderLock&&) { const auto labels = mappedInput.mapLabels("Cancel", "Delete", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } else { - // "Hold select to delete" hint above button hints - if (!words.empty()) { + // "Hold select to delete" hint above button hints (only when real words exist) + if (hasRealWords) { const char* deleteHint = "Hold select to delete"; const int hintWidth = renderer.getTextWidth(SMALL_FONT_ID, deleteHint); const int hintX = contentX + (renderer.getScreenWidth() - hintGutterWidth - hintWidth) / 2; diff --git a/src/activities/reader/LookedUpWordsActivity.h b/src/activities/reader/LookedUpWordsActivity.h index b786b56d..9099181d 100644 --- a/src/activities/reader/LookedUpWordsActivity.h +++ b/src/activities/reader/LookedUpWordsActivity.h @@ -10,13 +10,14 @@ class LookedUpWordsActivity final : public ActivityWithSubactivity { public: explicit LookedUpWordsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& cachePath, int readerFontId, uint8_t orientation, const std::function& onBack, - const std::function& onDone) + const std::function& onDone, bool initialSkipRelease = false) : ActivityWithSubactivity("LookedUpWords", renderer, mappedInput), cachePath(cachePath), readerFontId(readerFontId), orientation(orientation), onBack(onBack), - onDone(onDone) {} + onDone(onDone), + ignoreNextConfirmRelease(initialSkipRelease) {} void onEnter() override; void onExit() override; @@ -41,5 +42,9 @@ class LookedUpWordsActivity final : public ActivityWithSubactivity { bool ignoreNextConfirmRelease = false; 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; };