feat: restructure reader menu with submenus and long-press TOC
- Replace scattered book management actions (Archive, Delete, Reindex, Delete Cache) with single "Manage Book" entry that opens BookManageMenuActivity as a submenu. - Replace scattered dictionary actions (Lookup Word, Lookup History, Delete Dict Cache) with single "Dictionary" entry that opens new DictionaryMenuActivity submenu. - Add long-press Confirm (700ms) to open Table of Contents directly from the reader, bypassing the menu. - Add STR_DICTIONARY i18n key and regenerate I18nKeys.h/I18nStrings.h. Made-with: Cursor
This commit is contained in:
94
src/activities/reader/DictionaryMenuActivity.cpp
Normal file
94
src/activities/reader/DictionaryMenuActivity.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "DictionaryMenuActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "activities/ActivityResult.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void DictionaryMenuActivity::buildMenuItems() {
|
||||
menuItems.clear();
|
||||
menuItems.reserve(3);
|
||||
menuItems.push_back({Action::LOOKUP_WORD, StrId::STR_LOOKUP_WORD});
|
||||
menuItems.push_back({Action::LOOKUP_HISTORY, StrId::STR_LOOKUP_HISTORY});
|
||||
menuItems.push_back({Action::DELETE_DICT_CACHE, StrId::STR_DELETE_DICT_CACHE});
|
||||
}
|
||||
|
||||
void DictionaryMenuActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void DictionaryMenuActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void DictionaryMenuActivity::loop() {
|
||||
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)) {
|
||||
if (selectedIndex < static_cast<int>(menuItems.size())) {
|
||||
setResult(MenuResult{.action = static_cast<int>(menuItems[selectedIndex].action)});
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult r;
|
||||
r.isCancelled = true;
|
||||
setResult(std::move(r));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void DictionaryMenuActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
constexpr int popupMargin = 20;
|
||||
constexpr int lineHeight = 30;
|
||||
constexpr int titleHeight = 40;
|
||||
const int optionCount = static_cast<int>(menuItems.size());
|
||||
const int popupH = titleHeight + popupMargin + lineHeight * optionCount + popupMargin;
|
||||
const int popupW = pageWidth - 60;
|
||||
const int popupX = (pageWidth - popupW) / 2;
|
||||
const int popupY = (pageHeight - popupH) / 2;
|
||||
|
||||
renderer.fillRect(popupX - 2, popupY - 2, popupW + 4, popupH + 4, true);
|
||||
renderer.fillRect(popupX, popupY, popupW, popupH, false);
|
||||
|
||||
renderer.drawText(UI_12_FONT_ID, popupX + popupMargin, popupY + 8, tr(STR_DICTIONARY), true, EpdFontFamily::BOLD);
|
||||
|
||||
const int dividerY = popupY + titleHeight;
|
||||
renderer.fillRect(popupX + 4, dividerY, popupW - 8, 1, true);
|
||||
|
||||
const int startY = dividerY + popupMargin / 2;
|
||||
for (int i = 0; i < optionCount; ++i) {
|
||||
const int itemY = startY + i * lineHeight;
|
||||
const bool isSelected = (i == selectedIndex);
|
||||
|
||||
if (isSelected) {
|
||||
renderer.fillRect(popupX + 2, itemY, popupW - 4, lineHeight, true);
|
||||
}
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID, popupX + popupMargin, itemY, I18N.get(menuItems[i].labelId), !isSelected);
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
40
src/activities/reader/DictionaryMenuActivity.h
Normal file
40
src/activities/reader/DictionaryMenuActivity.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
class DictionaryMenuActivity final : public Activity {
|
||||
public:
|
||||
enum class Action {
|
||||
LOOKUP_WORD,
|
||||
LOOKUP_HISTORY,
|
||||
DELETE_DICT_CACHE,
|
||||
};
|
||||
|
||||
explicit DictionaryMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("DictionaryMenu", renderer, mappedInput) {
|
||||
buildMenuItems();
|
||||
}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
struct MenuItem {
|
||||
Action action;
|
||||
StrId labelId;
|
||||
};
|
||||
|
||||
std::vector<MenuItem> menuItems;
|
||||
int selectedIndex = 0;
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
void buildMenuItems();
|
||||
};
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "activities/ActivityManager.h"
|
||||
#include "activities/home/BookManageMenuActivity.h"
|
||||
#include "DictionaryMenuActivity.h"
|
||||
#include "DictionaryWordSelectActivity.h"
|
||||
#include "EndOfBookMenuActivity.h"
|
||||
#include "EpubReaderBookmarkSelectionActivity.h"
|
||||
@@ -34,6 +36,7 @@ namespace {
|
||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||
constexpr unsigned long skipChapterMs = 700;
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
constexpr unsigned long longPressConfirmMs = 700;
|
||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
|
||||
|
||||
@@ -209,8 +212,21 @@ void EpubReaderActivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Enter reader menu activity.
|
||||
// Long press CONFIRM opens Table of Contents directly (skip menu)
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= longPressConfirmMs) {
|
||||
ignoreNextConfirmRelease = true;
|
||||
if (epub && epub->getTocItemsCount() > 0) {
|
||||
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::TABLE_OF_CONTENTS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Short press CONFIRM opens reader menu
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (ignoreNextConfirmRelease) {
|
||||
ignoreNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
float bookProgress = 0.0f;
|
||||
@@ -595,6 +611,81 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
activityManager.goHome();
|
||||
return;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::MANAGE_BOOK: {
|
||||
if (!epub) break;
|
||||
const bool isArchived = BookManager::isArchived(epub->getPath());
|
||||
startActivityForResult(
|
||||
std::make_unique<BookManageMenuActivity>(renderer, mappedInput, epub->getPath(), isArchived),
|
||||
[this](ActivityResult result) {
|
||||
if (result.isCancelled) {
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
const auto bookAction = static_cast<BookManageMenuActivity::Action>(menu.action);
|
||||
switch (bookAction) {
|
||||
case BookManageMenuActivity::Action::ARCHIVE:
|
||||
BookManager::archiveBook(epub->getPath());
|
||||
activityManager.goHome();
|
||||
return;
|
||||
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||
BookManager::unarchiveBook(epub->getPath());
|
||||
activityManager.goHome();
|
||||
return;
|
||||
case BookManageMenuActivity::Action::DELETE:
|
||||
BookManager::deleteBook(epub->getPath());
|
||||
activityManager.goHome();
|
||||
return;
|
||||
case BookManageMenuActivity::Action::DELETE_CACHE: {
|
||||
RenderLock lock(*this);
|
||||
if (section) {
|
||||
uint16_t backupSpine = currentSpineIndex;
|
||||
uint16_t backupPage = section->currentPage;
|
||||
uint16_t backupPageCount = section->pageCount;
|
||||
section.reset();
|
||||
epub->clearCache();
|
||||
epub->setupCacheDir();
|
||||
saveProgress(backupSpine, backupPage, backupPageCount);
|
||||
}
|
||||
activityManager.goHome();
|
||||
return;
|
||||
}
|
||||
case BookManageMenuActivity::Action::REINDEX:
|
||||
BookManager::reindexBook(epub->getPath(), false);
|
||||
activityManager.goHome();
|
||||
return;
|
||||
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||
BookManager::reindexBook(epub->getPath(), true);
|
||||
activityManager.goHome();
|
||||
return;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::DICTIONARY: {
|
||||
startActivityForResult(
|
||||
std::make_unique<DictionaryMenuActivity>(renderer, mappedInput),
|
||||
[this](ActivityResult result) {
|
||||
if (result.isCancelled) {
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
const auto dictAction = static_cast<DictionaryMenuActivity::Action>(menu.action);
|
||||
switch (dictAction) {
|
||||
case DictionaryMenuActivity::Action::LOOKUP_WORD:
|
||||
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::LOOKUP_WORD);
|
||||
return;
|
||||
case DictionaryMenuActivity::Action::LOOKUP_HISTORY:
|
||||
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::LOOKUP_HISTORY);
|
||||
return;
|
||||
case DictionaryMenuActivity::Action::DELETE_DICT_CACHE:
|
||||
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE);
|
||||
return;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class EpubReaderActivity final : public Activity {
|
||||
float pendingSpineProgress = 0.0f;
|
||||
bool pendingScreenshot = false;
|
||||
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
|
||||
bool ignoreNextConfirmRelease = false;
|
||||
bool automaticPageTurnActive = false;
|
||||
bool pendingEndOfBookMenu = false;
|
||||
bool endOfBookMenuOpened = false;
|
||||
|
||||
@@ -38,22 +38,17 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
|
||||
} else {
|
||||
items.push_back({MenuAction::ADD_BOOKMARK, StrId::STR_ADD_BOOKMARK});
|
||||
}
|
||||
items.push_back({MenuAction::LOOKUP_WORD, StrId::STR_LOOKUP_WORD});
|
||||
items.push_back({MenuAction::GO_TO_BOOKMARK, StrId::STR_GO_TO_BOOKMARK});
|
||||
items.push_back({MenuAction::LOOKUP_HISTORY, StrId::STR_LOOKUP_HISTORY});
|
||||
items.push_back({MenuAction::TABLE_OF_CONTENTS, StrId::STR_TABLE_OF_CONTENTS});
|
||||
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||
items.push_back({MenuAction::DICTIONARY, StrId::STR_DICTIONARY});
|
||||
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::CLOSE_BOOK, StrId::STR_CLOSE_BOOK});
|
||||
items.push_back({MenuAction::ARCHIVE_BOOK, StrId::STR_ARCHIVE_BOOK});
|
||||
items.push_back({MenuAction::DELETE_BOOK, StrId::STR_DELETE_BOOK});
|
||||
items.push_back({MenuAction::REINDEX_BOOK, StrId::STR_REINDEX_BOOK});
|
||||
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
||||
items.push_back({MenuAction::DELETE_DICT_CACHE, StrId::STR_DELETE_DICT_CACHE});
|
||||
items.push_back({MenuAction::MANAGE_BOOK, StrId::STR_MANAGE_BOOK});
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
ARCHIVE_BOOK,
|
||||
DELETE_BOOK,
|
||||
REINDEX_BOOK,
|
||||
LETTERBOX_FILL
|
||||
LETTERBOX_FILL,
|
||||
MANAGE_BOOK,
|
||||
DICTIONARY
|
||||
};
|
||||
|
||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||
|
||||
Reference in New Issue
Block a user