Merge remote-tracking branch 'origin/master' into feat-autosync
This commit is contained in:
@@ -33,6 +33,13 @@ void ButtonEventManager::pushEvent(const Button button, const PressType type) {
|
||||
eventTail = next;
|
||||
}
|
||||
|
||||
void ButtonEventManager::pushEventFront(const Button button, const PressType type) {
|
||||
const int prev = (eventHead - 1 + EVENT_BUF) % EVENT_BUF;
|
||||
if (prev == eventTail) return; // buffer full
|
||||
eventHead = prev;
|
||||
eventBuf[eventHead] = {button, type};
|
||||
}
|
||||
|
||||
bool ButtonEventManager::consumeEvent(ButtonEvent& out) {
|
||||
if (eventHead == eventTail) return false;
|
||||
out = eventBuf[eventHead];
|
||||
|
||||
@@ -34,7 +34,7 @@ class ButtonEventManager {
|
||||
};
|
||||
|
||||
// Timing constants (milliseconds)
|
||||
static constexpr unsigned long LONG_PRESS_MS = 600;
|
||||
static constexpr unsigned long LONG_PRESS_MS = 1000;
|
||||
static constexpr unsigned long DOUBLE_WINDOW_MS = 300;
|
||||
|
||||
explicit ButtonEventManager(MappedInputManager& input) : input(input) {}
|
||||
@@ -49,6 +49,10 @@ class ButtonEventManager {
|
||||
// Reset all per-button FSMs. Call on activity transitions to prevent bleed-through.
|
||||
void drain();
|
||||
|
||||
// Preserve a default event for activity processing after main loop dispatch.
|
||||
// This is used when the configured action is BTN_DEFAULT.
|
||||
void pushEventFront(Button button, PressType type);
|
||||
|
||||
// Returns true if a double-click action is configured for this button.
|
||||
// ButtonEventManager queries CrossPointSettings internally.
|
||||
static bool hasDoubleAction(Button button);
|
||||
|
||||
@@ -58,6 +58,11 @@ void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
|
||||
}
|
||||
}
|
||||
|
||||
void enforceFixedShortActions(CrossPointSettings& settings) {
|
||||
settings.btnShortBack = static_cast<uint8_t>(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT);
|
||||
settings.btnShortConfirm = static_cast<uint8_t>(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) {
|
||||
@@ -88,11 +93,14 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (!json.isEmpty()) {
|
||||
bool resave = false;
|
||||
bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave);
|
||||
if (result && resave) {
|
||||
if (saveToFile()) {
|
||||
LOG_DBG("CPS", "Resaved settings to update format");
|
||||
} else {
|
||||
LOG_ERR("CPS", "Failed to resave settings after format update");
|
||||
if (result) {
|
||||
enforceFixedShortActions(*this);
|
||||
if (resave) {
|
||||
if (saveToFile()) {
|
||||
LOG_DBG("CPS", "Resaved settings to update format");
|
||||
} else {
|
||||
LOG_ERR("CPS", "Failed to resave settings after format update");
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -102,6 +110,7 @@ bool CrossPointSettings::loadFromFile() {
|
||||
// Fall back to binary migration
|
||||
if (Storage.exists(SETTINGS_FILE_BIN)) {
|
||||
if (loadFromBinaryFile()) {
|
||||
enforceFixedShortActions(*this);
|
||||
if (saveToFile()) {
|
||||
Storage.rename(SETTINGS_FILE_BIN, SETTINGS_FILE_BAK);
|
||||
LOG_DBG("CPS", "Migrated settings.bin to settings.json");
|
||||
|
||||
+3
-3
@@ -144,8 +144,8 @@ inline const std::vector<SettingInfo> list = {
|
||||
StrId::STR_BTN_ACT_KOREADER_SYNC
|
||||
|
||||
// Back button: short=exit reader, double=ignore, long=go home
|
||||
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack,
|
||||
{StrId::STR_BTN_DEF_EXIT_READER, BTN_ACT_OPTIONS}, "btnShortBack", StrId::STR_CAT_CONTROLS)
|
||||
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, {StrId::STR_BTN_DEF_EXIT_READER},
|
||||
"btnShortBack", StrId::STR_CAT_CONTROLS)
|
||||
.withSubcategory(StrId::STR_MENU_BTN_ACTIONS)
|
||||
.withSubmenu(StrId::STR_BTN_BACK),
|
||||
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleBack,
|
||||
@@ -156,7 +156,7 @@ inline const std::vector<SettingInfo> list = {
|
||||
.withSubmenu(StrId::STR_BTN_BACK),
|
||||
// Confirm button: short=reader menu, double=ignore, long=KOReader sync
|
||||
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortConfirm,
|
||||
{StrId::STR_BTN_DEF_READER_MENU, BTN_ACT_OPTIONS}, "btnShortConfirm", StrId::STR_CAT_CONTROLS)
|
||||
{StrId::STR_BTN_DEF_READER_MENU}, "btnShortConfirm", StrId::STR_CAT_CONTROLS)
|
||||
.withSubmenu(StrId::STR_BTN_CONFIRM),
|
||||
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleConfirm,
|
||||
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleConfirm", StrId::STR_CAT_CONTROLS)
|
||||
|
||||
@@ -18,10 +18,6 @@
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr unsigned long GO_HOME_MS = 1000;
|
||||
} // namespace
|
||||
|
||||
void sortFileList(std::vector<std::string>& strs) {
|
||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||
// Directories first
|
||||
@@ -143,119 +139,121 @@ void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) {
|
||||
void FileBrowserActivity::loop() {
|
||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||
|
||||
// Long press BACK always navigates to home
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
|
||||
// Short press BACK goes up one directory (if not root) or home (at root)
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < GO_HOME_MS) {
|
||||
if (basepath != "/") {
|
||||
const std::string oldPath = basepath;
|
||||
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||
if (basepath.empty()) basepath = "/";
|
||||
loadFiles();
|
||||
const auto pos = oldPath.find_last_of('/');
|
||||
const std::string dirName = oldPath.substr(pos + 1) + "/";
|
||||
const size_t idx = findEntry(dirName);
|
||||
selectorIndex = (idx < files.size()) ? idx : 0;
|
||||
requestUpdate();
|
||||
} else {
|
||||
onGoHome();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm short press opens selected entry; long press on a file opens it with KOReader sync.
|
||||
// Long press on a directory is ignored (no useful directory-level sync action).
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (files.empty()) return;
|
||||
|
||||
const std::string& entry = files[selectorIndex];
|
||||
const bool isDirectory = (entry.back() == '/');
|
||||
const bool longPress = mappedInput.getHeldTime() >= GO_HOME_MS;
|
||||
|
||||
if (isDirectory) {
|
||||
if (longPress) return;
|
||||
if (basepath.back() != '/') basepath += "/";
|
||||
basepath += entry.substr(0, entry.length() - 1);
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
requestUpdate();
|
||||
} else {
|
||||
std::string fullPath = basepath;
|
||||
if (fullPath.back() != '/') fullPath += "/";
|
||||
fullPath += entry;
|
||||
if (longPress && KOREADER_STORE.hasCredentials() && FsHelpers::hasEpubExtension(fullPath)) {
|
||||
auto& sync = APP_STATE.koReaderSyncSession;
|
||||
sync.autoPullEpubPath = fullPath;
|
||||
sync.exitToHomeAfterSync = false;
|
||||
APP_STATE.saveToFile();
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Back) {
|
||||
if (ev.type == ButtonEventManager::PressType::Long) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
ReturnHint hint;
|
||||
hint.target = ReturnTo::FileBrowser;
|
||||
hint.path = basepath;
|
||||
hint.selectName = entry;
|
||||
activityManager.replaceWithReader(std::move(fullPath), std::move(hint));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Left short press does nothing; long press deletes selected file after confirmation
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||
if (mappedInput.getHeldTime() < GO_HOME_MS || files.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string& entry = files[selectorIndex];
|
||||
const bool isDirectory = (entry.back() == '/');
|
||||
if (isDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string cleanBase = basepath;
|
||||
if (cleanBase.back() != '/') cleanBase += "/";
|
||||
const std::string fullPath = cleanBase + entry;
|
||||
|
||||
auto handler = [this, fullPath](const ActivityResult& res) {
|
||||
if (!res.isCancelled) {
|
||||
LOG_DBG("FileBrowser", "Attempting to delete: %s", fullPath.c_str());
|
||||
clearFileMetadata(fullPath);
|
||||
if (Storage.remove(fullPath.c_str())) {
|
||||
LOG_DBG("FileBrowser", "Deleted successfully");
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (basepath != "/") {
|
||||
const std::string oldPath = basepath;
|
||||
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||
if (basepath.empty()) basepath = "/";
|
||||
loadFiles();
|
||||
if (files.empty()) {
|
||||
selectorIndex = 0;
|
||||
} else if (selectorIndex >= files.size()) {
|
||||
selectorIndex = files.size() - 1;
|
||||
}
|
||||
requestUpdate(true);
|
||||
const auto pos = oldPath.find_last_of('/');
|
||||
const std::string dirName = oldPath.substr(pos + 1) + "/";
|
||||
const size_t idx = findEntry(dirName);
|
||||
selectorIndex = (idx < files.size()) ? idx : 0;
|
||||
requestUpdate();
|
||||
} else {
|
||||
LOG_ERR("FileBrowser", "Failed to delete file: %s", fullPath.c_str());
|
||||
onGoHome();
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("FileBrowser", "Delete cancelled by user");
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
startActivityForResult(
|
||||
std::make_unique<ConfirmationActivity>(renderer, mappedInput, tr(STR_DELETE) + std::string("? "), entry),
|
||||
handler);
|
||||
return;
|
||||
}
|
||||
if (ev.button == MappedInputManager::Button::Confirm &&
|
||||
(ev.type == ButtonEventManager::PressType::Short || ev.type == ButtonEventManager::PressType::Long)) {
|
||||
if (files.empty()) return;
|
||||
|
||||
const std::string& entry = files[selectorIndex];
|
||||
const bool isDirectory = (entry.back() == '/');
|
||||
const bool longPress = (ev.type == ButtonEventManager::PressType::Long);
|
||||
|
||||
if (isDirectory) {
|
||||
// Long press on a directory has no useful sync action; ignore.
|
||||
if (longPress) return;
|
||||
if (basepath.back() != '/') basepath += "/";
|
||||
basepath += entry.substr(0, entry.length() - 1);
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
requestUpdate();
|
||||
} else {
|
||||
std::string fullPath = basepath;
|
||||
if (fullPath.back() != '/') fullPath += "/";
|
||||
fullPath += entry;
|
||||
// Long-press on an EPUB arms an AUTO_PULL before the reader renders its first page.
|
||||
// Restricted to EPUB so long-pressing a non-EPUB cannot leak the flag to the reader.
|
||||
if (longPress && KOREADER_STORE.hasCredentials() && FsHelpers::hasEpubExtension(fullPath)) {
|
||||
auto& sync = APP_STATE.koReaderSyncSession;
|
||||
sync.autoPullEpubPath = fullPath;
|
||||
sync.exitToHomeAfterSync = false;
|
||||
APP_STATE.saveToFile();
|
||||
}
|
||||
ReturnHint hint;
|
||||
hint.target = ReturnTo::FileBrowser;
|
||||
hint.path = basepath;
|
||||
hint.selectName = entry;
|
||||
activityManager.replaceWithReader(std::move(fullPath), std::move(hint));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) {
|
||||
if (files.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string& entry = files[selectorIndex];
|
||||
const bool isDirectory = (entry.back() == '/');
|
||||
if (isDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Right opens the info page for epub files
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||
if (files.empty()) return;
|
||||
const std::string& entry = files[selectorIndex];
|
||||
if (entry.back() != '/' && (FsHelpers::hasEpubExtension(entry) || FsHelpers::hasXtcExtension(entry))) {
|
||||
std::string cleanBase = basepath;
|
||||
if (cleanBase.back() != '/') cleanBase += "/";
|
||||
startActivityForResult(std::make_unique<BookInfoActivity>(renderer, mappedInput, cleanBase + entry),
|
||||
[this](const ActivityResult&) { requestUpdate(); });
|
||||
const std::string fullPath = cleanBase + entry;
|
||||
|
||||
auto handler = [this, fullPath](const ActivityResult& res) {
|
||||
if (!res.isCancelled) {
|
||||
LOG_DBG("FileBrowser", "Attempting to delete: %s", fullPath.c_str());
|
||||
clearFileMetadata(fullPath);
|
||||
if (Storage.remove(fullPath.c_str())) {
|
||||
LOG_DBG("FileBrowser", "Deleted successfully");
|
||||
loadFiles();
|
||||
if (files.empty()) {
|
||||
selectorIndex = 0;
|
||||
} else if (selectorIndex >= files.size()) {
|
||||
selectorIndex = files.size() - 1;
|
||||
}
|
||||
requestUpdate(true);
|
||||
} else {
|
||||
LOG_ERR("FileBrowser", "Failed to delete file: %s", fullPath.c_str());
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("FileBrowser", "Delete cancelled by user");
|
||||
}
|
||||
};
|
||||
|
||||
startActivityForResult(
|
||||
std::make_unique<ConfirmationActivity>(renderer, mappedInput, tr(STR_DELETE) + std::string("? "), entry),
|
||||
handler);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (files.empty()) return;
|
||||
const std::string& entry = files[selectorIndex];
|
||||
if (entry.back() != '/' && (FsHelpers::hasEpubExtension(entry) || FsHelpers::hasXtcExtension(entry))) {
|
||||
std::string cleanBase = basepath;
|
||||
if (cleanBase.back() != '/') cleanBase += "/";
|
||||
startActivityForResult(std::make_unique<BookInfoActivity>(renderer, mappedInput, cleanBase + entry),
|
||||
[this](const ActivityResult&) { requestUpdate(); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Up/Down side buttons navigate the list
|
||||
|
||||
@@ -155,6 +155,15 @@ void EpubReaderActivity::onEnter() {
|
||||
}
|
||||
// We may want a better condition to detect if we are opening for the first time.
|
||||
// This will trigger if the book is re-opened at Chapter 0.
|
||||
if (currentSpineIndex < 0 || currentSpineIndex >= epub->getSpineItemsCount()) {
|
||||
LOG_ERR("ERS", "Invalid saved spine index %d (valid 0..%d), resetting to start", currentSpineIndex,
|
||||
epub->getSpineItemsCount() > 0 ? epub->getSpineItemsCount() - 1 : 0);
|
||||
currentSpineIndex = 0;
|
||||
nextPageNumber = 0;
|
||||
cachedSpineIndex = 0;
|
||||
cachedChapterTotalPageCount = 0;
|
||||
}
|
||||
|
||||
if (currentSpineIndex == 0) {
|
||||
int textSpineIndex = epub->getSpineIndexForTextReference();
|
||||
if (textSpineIndex != 0) {
|
||||
@@ -219,12 +228,14 @@ void EpubReaderActivity::loop() {
|
||||
}
|
||||
|
||||
if (inputDrainGuard.shouldDrain(mappedInput)) {
|
||||
buttonEvents.drain();
|
||||
return;
|
||||
}
|
||||
|
||||
if (automaticPageTurnActive) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
buttonEvents.drain();
|
||||
automaticPageTurnActive = false;
|
||||
// updates chapter title space to indicate page turn disabled
|
||||
requestUpdate();
|
||||
@@ -248,71 +259,68 @@ void EpubReaderActivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Long press CONFIRM (1s+) goes directly to KOReader sync when credentials are configured.
|
||||
// We intentionally keep long-press on the richer compare flow so advanced
|
||||
// conflict-resolution behavior stays available even after simplifying menu UX.
|
||||
// Without credentials, fall through to the regular menu on release.
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
||||
mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) {
|
||||
launchKOReaderSync(SyncLaunchMode::COMPARE);
|
||||
return;
|
||||
}
|
||||
bool delayedPrevTurn = false;
|
||||
bool delayedNextTurn = false;
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
|
||||
// Short press CONFIRM enters reader menu activity.
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
float bookProgress = 0.0f;
|
||||
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
||||
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Confirm) {
|
||||
if (ev.type == ButtonEventManager::PressType::Long && KOREADER_STORE.hasCredentials()) {
|
||||
launchKOReaderSync(SyncLaunchMode::COMPARE);
|
||||
return;
|
||||
}
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
openReaderMenu();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
||||
static_cast<uint16_t>(section->currentPage));
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
|
||||
bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride,
|
||||
SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred),
|
||||
[this](const ActivityResult& result) {
|
||||
// Always apply orientation/darkness change even if the menu was cancelled
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
applyTextDarkness(menu.textDarkness);
|
||||
toggleAutoPageTurn(menu.pageTurnOption);
|
||||
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride,
|
||||
menu.fontFamilyOverride, menu.fontSizeOverride);
|
||||
if (!result.isCancelled) {
|
||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes to home screen
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
if (tryAutoPushOnClose()) return;
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
if (ev.button == MappedInputManager::Button::Back) {
|
||||
if (ev.type == ButtonEventManager::PressType::Long) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
if (tryAutoPushOnClose()) return;
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (footnoteDepth > 0) {
|
||||
restoreSavedPosition();
|
||||
return;
|
||||
}
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
if (tryAutoPushOnClose()) return;
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Short press BACK returns to the calling activity (or restores position if viewing footnote)
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
if (footnoteDepth > 0) {
|
||||
restoreSavedPosition();
|
||||
return;
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
||||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
|
||||
delayedPrevTurn = true;
|
||||
continue;
|
||||
}
|
||||
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
||||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
|
||||
delayedNextTurn = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
if (tryAutoPushOnClose()) return;
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
if (!delayedPrevTurn && !delayedNextTurn) {
|
||||
return;
|
||||
}
|
||||
prevTriggered = delayedPrevTurn;
|
||||
nextTriggered = delayedNextTurn;
|
||||
}
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
@@ -330,65 +338,6 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs;
|
||||
|
||||
// Chapter skip navigates by TOC entries, not spine boundaries.
|
||||
// Spine items without their own TOC entry inherit the previous spine's tocIndex
|
||||
// (see BookMetadataCache), so they're treated as continuations of the last chapter.
|
||||
// At the boundaries: skipping forward past the last TOC entry jumps to end-of-book
|
||||
// (clamped in render()); skipping backward before the first TOC entry jumps to the
|
||||
// spine before the current chapter's first spine (clamped to 0 in render()).
|
||||
if (skipChapter) {
|
||||
lastPageTurnTime = millis();
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
|
||||
if (section && section->pageCount > 0) {
|
||||
const int curTocIndex = section->getTocIndexForPage(section->currentPage);
|
||||
const int nextTocIndex = nextTriggered ? curTocIndex + 1 : curTocIndex - 1;
|
||||
|
||||
if (curTocIndex < 0) {
|
||||
// No TOC entry for this spine, fall back to spine-level skip
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
||||
section.reset();
|
||||
} else if (nextTocIndex >= 0 && nextTocIndex < epub->getTocItemsCount()) {
|
||||
const int newSpineIndex = epub->getSpineIndexForTocIndex(nextTocIndex);
|
||||
|
||||
if (newSpineIndex == currentSpineIndex) {
|
||||
if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) {
|
||||
section->currentPage = *resolvedPage;
|
||||
} else {
|
||||
LOG_DBG("ERS", "No page boundary for TOC %d in spine %d, staying on current page", nextTocIndex,
|
||||
currentSpineIndex);
|
||||
}
|
||||
} else {
|
||||
pendingTocIndex = nextTocIndex;
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex = newSpineIndex;
|
||||
section.reset();
|
||||
}
|
||||
} else if (nextTriggered) {
|
||||
// Beyond last TOC entry, go to end of book
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex = epub->getSpineItemsCount();
|
||||
section.reset();
|
||||
} else {
|
||||
// Before first TOC entry, skip to spine before the current chapter
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex = epub->getTocItem(curTocIndex).spineIndex - 1;
|
||||
section.reset();
|
||||
}
|
||||
} else {
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
||||
section.reset();
|
||||
}
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
// No current section, attempt to rerender the book
|
||||
if (!section) {
|
||||
requestUpdate();
|
||||
@@ -946,6 +895,14 @@ void EpubReaderActivity::applyPendingSyncSession() {
|
||||
pendingParagraphLookup = sync.hasParagraphIndex;
|
||||
pendingParagraphIndex = sync.paragraphIndex;
|
||||
|
||||
if (restoreSpineIndex < 0 || restoreSpineIndex >= epub->getSpineItemsCount()) {
|
||||
LOG_ERR("ERS", "Invalid sync restore spine index %d, resetting to 0", restoreSpineIndex);
|
||||
restoreSpineIndex = 0;
|
||||
restorePage = 0;
|
||||
pendingParagraphLookup = false;
|
||||
pendingParagraphIndex = 0;
|
||||
}
|
||||
|
||||
if (sync.outcome == KOReaderSyncOutcomeState::APPLIED_REMOTE) {
|
||||
restoreSpineIndex = sync.resultSpineIndex;
|
||||
restorePage = sync.resultPage;
|
||||
@@ -991,6 +948,11 @@ void EpubReaderActivity::applyPendingBookmarkJump() {
|
||||
return;
|
||||
}
|
||||
LOG_DBG("ERS", "Applying pending bookmark jump: spine=%u page=%u", jump.spineIndex, jump.pageNumber);
|
||||
if (jump.spineIndex >= static_cast<uint16_t>(epub->getSpineItemsCount())) {
|
||||
LOG_ERR("ERS", "Invalid bookmark jump spine index %u, resetting to 0", jump.spineIndex);
|
||||
jump.spineIndex = 0;
|
||||
jump.pageNumber = 0;
|
||||
}
|
||||
// Transient write before initializeReader; saveProgress() overwrites with the real percent.
|
||||
if (writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0, 0)) {
|
||||
cachedSpineIndex = jump.spineIndex;
|
||||
@@ -1827,6 +1789,36 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
|
||||
return true;
|
||||
}
|
||||
|
||||
void EpubReaderActivity::openReaderMenu() {
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
float bookProgress = 0.0f;
|
||||
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
||||
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
}
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
||||
static_cast<uint16_t>(section->currentPage));
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
|
||||
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
|
||||
bookFontSizeOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred),
|
||||
[this](const ActivityResult& result) {
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
applyTextDarkness(menu.textDarkness);
|
||||
toggleAutoPageTurn(menu.pageTurnOption);
|
||||
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
|
||||
menu.fontSizeOverride);
|
||||
if (!result.isCancelled) {
|
||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
switch (action) {
|
||||
@@ -1946,35 +1938,7 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
|
||||
break;
|
||||
case BA::BTN_READER_MENU:
|
||||
if (epub) {
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
float bookProgress = 0.0f;
|
||||
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
||||
const float chapterProgress =
|
||||
static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
}
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
||||
static_cast<uint16_t>(section->currentPage));
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
|
||||
bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness,
|
||||
!bookmarkStore.isEmpty(), isCurrentPageStarred),
|
||||
[this](const ActivityResult& result) {
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
applyTextDarkness(menu.textDarkness);
|
||||
toggleAutoPageTurn(menu.pageTurnOption);
|
||||
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
|
||||
menu.fontSizeOverride);
|
||||
if (!result.isCancelled) {
|
||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||
}
|
||||
});
|
||||
openReaderMenu();
|
||||
}
|
||||
break;
|
||||
case BA::BTN_KOREADER_SYNC:
|
||||
|
||||
@@ -177,6 +177,7 @@ class EpubReaderActivity final : public Activity {
|
||||
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
||||
void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride,
|
||||
int8_t fontSizeOverride);
|
||||
void openReaderMenu();
|
||||
bool getEffectiveEmbeddedStyle() const;
|
||||
uint8_t getEffectiveImageRendering() const;
|
||||
int getEffectiveReaderFontId() const;
|
||||
|
||||
@@ -42,22 +42,40 @@ void EpubReaderChapterSelectionActivity::loop() {
|
||||
const int pageItems = getPageItems();
|
||||
const int totalItems = getTotalItems();
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex);
|
||||
if (newSpineIndex == -1) {
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
|
||||
const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex);
|
||||
if (newSpineIndex == -1) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
} else {
|
||||
setResult(ChapterResult{newSpineIndex, selectorIndex});
|
||||
finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
} else {
|
||||
setResult(ChapterResult{newSpineIndex, selectorIndex});
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
}
|
||||
|
||||
buttonNavigator.onNextRelease([this, totalItems] {
|
||||
|
||||
@@ -18,35 +18,53 @@ void EpubReaderFootnotesActivity::onEnter() {
|
||||
void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void EpubReaderFootnotesActivity::loop() {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(footnotes.size())) {
|
||||
setResult(FootnoteResult{footnotes[selectedIndex].href});
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(footnotes.size())) {
|
||||
setResult(FootnoteResult{footnotes[selectedIndex].href});
|
||||
finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
advanceSelection(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
advanceSelection(1);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buttonNavigator.onNext([this] {
|
||||
if (!footnotes.empty()) {
|
||||
selectedIndex = (selectedIndex + 1) % footnotes.size();
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
buttonNavigator.onNextRelease([this] { advanceSelection(1); });
|
||||
|
||||
buttonNavigator.onPrevious([this] {
|
||||
if (!footnotes.empty()) {
|
||||
selectedIndex = (selectedIndex - 1 + footnotes.size()) % footnotes.size();
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
buttonNavigator.onPreviousRelease([this] { advanceSelection(-1); });
|
||||
|
||||
buttonNavigator.onNextContinuous([this] { advanceSelection(1); });
|
||||
|
||||
buttonNavigator.onPreviousContinuous([this] { advanceSelection(-1); });
|
||||
}
|
||||
|
||||
void EpubReaderFootnotesActivity::advanceSelection(int delta) {
|
||||
if (footnotes.empty()) {
|
||||
return;
|
||||
}
|
||||
const int n = static_cast<int>(footnotes.size());
|
||||
selectedIndex = ((selectedIndex + delta) % n + n) % n;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderFootnotesActivity::render(RenderLock&&) {
|
||||
|
||||
@@ -21,6 +21,7 @@ class EpubReaderFootnotesActivity final : public Activity {
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
void advanceSelection(int delta);
|
||||
const std::vector<FootnoteEntry>& footnotes;
|
||||
int selectedIndex = 0;
|
||||
int scrollOffset = 0;
|
||||
|
||||
@@ -34,22 +34,34 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
|
||||
|
||||
void EpubReaderPercentSelectionActivity::loop() {
|
||||
// Back cancels, confirm selects, arrows adjust the percent.
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
setResult(PercentResult{percent});
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
|
||||
setResult(PercentResult{percent});
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); });
|
||||
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); });
|
||||
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
adjustPercent(-kSmallStep);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
adjustPercent(kSmallStep);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); });
|
||||
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); });
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
namespace {
|
||||
constexpr size_t CHUNK_SIZE = 8 * 1024;
|
||||
constexpr size_t MAX_LINE_LENGTH = 64 * 1024;
|
||||
constexpr unsigned long HEADING_SKIP_MS = 700;
|
||||
constexpr uint32_t CACHE_MAGIC = 0x4D4B4449; // "MKDI"
|
||||
constexpr uint8_t CACHE_VERSION = 3; // Bumped: nested list indent + task checkboxes
|
||||
|
||||
@@ -39,6 +38,8 @@ static std::string flattenHeadingText(const MdParser::ParsedLine& parsed) {
|
||||
void MdReaderActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
inputDrainGuard.arm();
|
||||
|
||||
if (!txt) {
|
||||
return;
|
||||
}
|
||||
@@ -222,54 +223,60 @@ void MdReaderActivity::onExit() {
|
||||
}
|
||||
|
||||
void MdReaderActivity::loop() {
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
|
||||
if (inputDrainGuard.shouldDrain(mappedInput)) {
|
||||
buttonEvents.drain();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Back) {
|
||||
if (ev.type == ButtonEventManager::PressType::Long) {
|
||||
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
|
||||
return;
|
||||
}
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !headings.empty()) {
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<MdReaderTocSelectionActivity>(renderer, mappedInput, headings, currentHeadingIndex),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool headingSkip = mappedInput.getHeldTime() > HEADING_SKIP_MS;
|
||||
if (headingSkip && !headings.empty()) {
|
||||
jumpToHeading(nextTriggered);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevTriggered && currentPage > 0) {
|
||||
currentPage--;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
} else if (nextTriggered) {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
if (!headings.empty() && ev.button == MappedInputManager::Button::Confirm &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
} else {
|
||||
onGoHome();
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<MdReaderTocSelectionActivity>(renderer, mappedInput, headings, currentHeadingIndex),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
} else {
|
||||
onGoHome();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "activities/Activity.h"
|
||||
|
||||
struct MdHeading {
|
||||
@@ -72,6 +73,7 @@ class MdReaderActivity final : public Activity {
|
||||
void assignHeadingPageNumbers();
|
||||
int getHeadingIndexForOffset(size_t offset) const;
|
||||
void jumpToHeading(bool next);
|
||||
ReaderUtils::InputDrainGuard inputDrainGuard;
|
||||
|
||||
// Word-wrap a parsed markdown line into one or more RenderedLines.
|
||||
// Returns true if all content was emitted, false if truncated by maxLines.
|
||||
|
||||
@@ -34,24 +34,41 @@ void MdReaderTocSelectionActivity::loop() {
|
||||
const int pageItems = getPageItems();
|
||||
const int totalItems = getTotalItems();
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selectorIndex >= 0 && selectorIndex < totalItems) {
|
||||
setResult(PageResult{static_cast<uint32_t>(headings[selectorIndex].pageIndex)});
|
||||
} else {
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (selectorIndex >= 0 && selectorIndex < totalItems) {
|
||||
setResult(PageResult{static_cast<uint32_t>(headings[selectorIndex].pageIndex)});
|
||||
} else {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
buttonNavigator.onNextRelease([this, totalItems] {
|
||||
|
||||
@@ -16,10 +16,13 @@ void QrDisplayActivity::onEnter() {
|
||||
void QrDisplayActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void QrDisplayActivity::loop() {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
finish();
|
||||
return;
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if ((ev.button == MappedInputManager::Button::Back || ev.button == MappedInputManager::Button::Confirm) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "ButtonEventManager.h"
|
||||
#include "MappedInputManager.h"
|
||||
|
||||
namespace ReaderUtils {
|
||||
@@ -90,13 +91,21 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
||||
// Only treat wasReleased as a page turn when the button's short-press action is default.
|
||||
// Non-default short-press actions are dispatched by the global dispatcher in main.cpp;
|
||||
// counting wasReleased as well would double-fire the action.
|
||||
// Also suppress immediate page-turns if a double-click action is configured for the button,
|
||||
// because the button event system delays short events until the double-click window expires.
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
const bool prev =
|
||||
(SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
||||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool next =
|
||||
(SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
||||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Right));
|
||||
const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack) &&
|
||||
input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
||||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) &&
|
||||
input.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward) &&
|
||||
input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
||||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) &&
|
||||
input.wasReleased(MappedInputManager::Button::Right));
|
||||
return {prev, next};
|
||||
}
|
||||
|
||||
|
||||
@@ -78,33 +78,41 @@ void StarredPagesActivity::deleteSelected() {
|
||||
void StarredPagesActivity::loop() {
|
||||
const int totalItems = static_cast<int>(bookmarkStore.getAll().size());
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalItems > 0 && ev.button == MappedInputManager::Button::Confirm &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
const auto& bm = bookmarkStore.getAll()[selectorIndex];
|
||||
setResult(StarredPageResult{bm.spineIndex, bm.pageNumber});
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalItems > 0 &&
|
||||
(ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
startRename();
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalItems > 0 &&
|
||||
(ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
deleteSelected();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalItems == 0) return;
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
const auto& bm = bookmarkStore.getAll()[selectorIndex];
|
||||
setResult(StarredPageResult{bm.spineIndex, bm.pageNumber});
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||
startRename();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||
deleteSelected();
|
||||
return;
|
||||
}
|
||||
|
||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||
|
||||
buttonNavigator.onNextRelease([this, totalItems] {
|
||||
|
||||
@@ -137,54 +137,59 @@ void TxtReaderActivity::onExit() {
|
||||
|
||||
void TxtReaderActivity::loop() {
|
||||
if (inputDrainGuard.shouldDrain(mappedInput)) {
|
||||
buttonEvents.drain();
|
||||
return;
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes to home screen
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Back) {
|
||||
if (ev.type == ButtonEventManager::PressType::Long) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Short press BACK returns to the calling activity
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (!bookmarkStore.isEmpty() && ev.button == MappedInputManager::Button::Confirm &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& starred = std::get<StarredPageResult>(result.data);
|
||||
currentPage = starred.pageNumber;
|
||||
if (currentPage >= totalPages) currentPage = totalPages - 1;
|
||||
if (currentPage < 0) currentPage = 0;
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Open starred pages list via Confirm button
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& starred = std::get<StarredPageResult>(result.data);
|
||||
currentPage = starred.pageNumber;
|
||||
if (currentPage >= totalPages) currentPage = totalPages - 1;
|
||||
if (currentPage < 0) currentPage = 0;
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevTriggered && currentPage > 0) {
|
||||
currentPage--;
|
||||
requestUpdate();
|
||||
} else if (nextTriggered) {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
requestUpdate();
|
||||
} else {
|
||||
finish();
|
||||
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
requestUpdate();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,6 @@
|
||||
#include "XtcReaderChapterSelectionActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr unsigned long skipPageMs = 700;
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
} // namespace
|
||||
|
||||
void XtcReaderActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
@@ -62,42 +57,70 @@ void XtcReaderActivity::onExit() {
|
||||
|
||||
void XtcReaderActivity::loop() {
|
||||
if (inputDrainGuard.shouldDrain(mappedInput)) {
|
||||
buttonEvents.drain();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter chapter selection activity
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
}
|
||||
});
|
||||
bool delayedPrevTurn = false;
|
||||
bool delayedNextTurn = false;
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.button == MappedInputManager::Button::Back) {
|
||||
if (ev.type == ButtonEventManager::PressType::Long) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
||||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
|
||||
delayedPrevTurn = true;
|
||||
continue;
|
||||
}
|
||||
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
||||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
|
||||
delayedNextTurn = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes to home screen
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
onGoHome();
|
||||
return;
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
if (!delayedPrevTurn && !delayedNextTurn) {
|
||||
return;
|
||||
}
|
||||
prevTriggered = delayedPrevTurn;
|
||||
nextTriggered = delayedNextTurn;
|
||||
}
|
||||
|
||||
// Short press BACK returns to the calling activity
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool prevTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||
const bool nextTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
||||
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
@@ -113,21 +136,13 @@ void XtcReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool skipPages = mappedInput.getHeldTime() > skipPageMs;
|
||||
const int skipAmount = skipPages ? 10 : 1;
|
||||
|
||||
if (prevTriggered) {
|
||||
if (currentPage >= static_cast<uint32_t>(skipAmount)) {
|
||||
currentPage -= skipAmount;
|
||||
} else {
|
||||
currentPage = 0;
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
requestUpdate();
|
||||
}
|
||||
requestUpdate();
|
||||
} else if (nextTriggered) {
|
||||
currentPage += skipAmount;
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
currentPage = xtc->getPageCount(); // Allow showing "End of book"
|
||||
}
|
||||
currentPage++;
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "ButtonEventManager.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
@@ -47,28 +48,58 @@ void XtcReaderChapterSelectionActivity::onEnter() {
|
||||
void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void XtcReaderChapterSelectionActivity::loop() {
|
||||
if (!xtc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int pageItems = getPageItems();
|
||||
const int totalItems = static_cast<int>(xtc->getChapters().size());
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
const auto& chapters = xtc->getChapters();
|
||||
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
|
||||
setResult(PageResult{chapters[selectorIndex].startPage});
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
|
||||
const auto& chapters = xtc->getChapters();
|
||||
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
|
||||
setResult(PageResult{chapters[selectorIndex].startPage});
|
||||
finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
|
||||
ev.type == ButtonEventManager::PressType::Short) {
|
||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
}
|
||||
|
||||
buttonNavigator.onNextRelease([this, totalItems] {
|
||||
if (ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) ||
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) {
|
||||
return;
|
||||
}
|
||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||
if (ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) ||
|
||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) {
|
||||
return;
|
||||
}
|
||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
@@ -388,38 +388,38 @@ void BmpViewerActivity::loop() {
|
||||
// Keep CPU awake/polling so 1st click works
|
||||
Activity::loop();
|
||||
|
||||
// Long press BACK (1s+) goes to home screen
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
|
||||
// Short press BACK returns to the calling activity
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm: toggle between 1-bit B&W and 4-level grayscale display.
|
||||
// For decoded images this always applies; for BMPs it only makes sense when the bitmap
|
||||
// actually carries greyscale data (1-bit BMPs have nothing to toggle).
|
||||
const bool toggleSupported = isBmpFile(filePath) ? bmpHasGreyscale : true;
|
||||
if (toggleSupported && mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
toggleDisplayMode();
|
||||
return;
|
||||
}
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Back) {
|
||||
if (ev.type == ButtonEventManager::PressType::Long) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
|
||||
if (toggleSupported) {
|
||||
toggleDisplayMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
if (!isBmpFile(filePath) && grayscaleDisplay && mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||
cycleDitherMode();
|
||||
return;
|
||||
}
|
||||
if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Short &&
|
||||
!isBmpFile(filePath) && grayscaleDisplay) {
|
||||
cycleDitherMode();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Next/Right button: set this image as the sleep screen
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||
setAsSleepScreen();
|
||||
return;
|
||||
if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) {
|
||||
setAsSleepScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,9 +404,14 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
if (rowIcon != nullptr) {
|
||||
UIIcon icon = rowIcon(i);
|
||||
const uint8_t* iconBitmap = iconForName(icon, iconSize);
|
||||
int renderSize = iconSize;
|
||||
if (iconBitmap == nullptr && rowSubtitle != nullptr) {
|
||||
renderSize = listIconSize;
|
||||
iconBitmap = iconForName(icon, renderSize);
|
||||
}
|
||||
if (iconBitmap != nullptr) {
|
||||
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
|
||||
itemY + iconY, iconSize, iconSize);
|
||||
itemY + iconY, renderSize, renderSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+91
-79
@@ -16,6 +16,7 @@
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "ButtonEventManager.h"
|
||||
#include "CrossPointSettings.h"
|
||||
@@ -387,88 +388,95 @@ void loop() {
|
||||
{
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
using B = MappedInputManager::Button;
|
||||
auto actionFor = [&](const ButtonEventManager::ButtonEvent& ev) -> uint8_t {
|
||||
switch (ev.button) {
|
||||
case B::Back:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortBack;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleBack;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongBack;
|
||||
}
|
||||
break;
|
||||
case B::Confirm:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortConfirm;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleConfirm;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongConfirm;
|
||||
}
|
||||
break;
|
||||
case B::Left:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortLeft;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleLeft;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongLeft;
|
||||
}
|
||||
break;
|
||||
case B::Right:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortRight;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleRight;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongRight;
|
||||
}
|
||||
break;
|
||||
case B::PageBack:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortPageBack;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoublePageBack;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongPageBack;
|
||||
}
|
||||
break;
|
||||
case B::PageForward:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortPageForward;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoublePageForward;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongPageForward;
|
||||
}
|
||||
break;
|
||||
case B::Power:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortPower;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoublePower;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongPower;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break; // Up/Down have no FSMs — ButtonEventManager never emits these
|
||||
}
|
||||
return BA::BTN_DEFAULT;
|
||||
};
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
std::vector<ButtonEventManager::ButtonEvent> defaultEvents;
|
||||
defaultEvents.reserve(8);
|
||||
while (buttonEventManager.consumeEvent(ev)) {
|
||||
auto actionFor = [&](B btn) -> uint8_t {
|
||||
switch (btn) {
|
||||
case B::Back:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortBack;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleBack;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongBack;
|
||||
}
|
||||
break;
|
||||
case B::Confirm:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortConfirm;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleConfirm;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongConfirm;
|
||||
}
|
||||
break;
|
||||
case B::Left:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortLeft;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleLeft;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongLeft;
|
||||
}
|
||||
break;
|
||||
case B::Right:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortRight;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoubleRight;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongRight;
|
||||
}
|
||||
break;
|
||||
case B::PageBack:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortPageBack;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoublePageBack;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongPageBack;
|
||||
}
|
||||
break;
|
||||
case B::PageForward:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortPageForward;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoublePageForward;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongPageForward;
|
||||
}
|
||||
break;
|
||||
case B::Power:
|
||||
switch (ev.type) {
|
||||
case ButtonEventManager::PressType::Short:
|
||||
return SETTINGS.btnShortPower;
|
||||
case ButtonEventManager::PressType::Double:
|
||||
return SETTINGS.btnDoublePower;
|
||||
case ButtonEventManager::PressType::Long:
|
||||
return SETTINGS.btnLongPower;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break; // Up/Down have no FSMs — ButtonEventManager never emits these
|
||||
const uint8_t action = actionFor(ev);
|
||||
if (action == BA::BTN_DEFAULT) {
|
||||
if (ev.type == ButtonEventManager::PressType::Double) {
|
||||
continue;
|
||||
}
|
||||
return BA::BTN_DEFAULT;
|
||||
};
|
||||
|
||||
const uint8_t action = actionFor(ev.button);
|
||||
if (action == BA::BTN_DEFAULT) continue;
|
||||
defaultEvents.push_back(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (static_cast<BA>(action)) {
|
||||
case BA::BTN_PAGE_FORWARD:
|
||||
@@ -525,6 +533,10 @@ void loop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = defaultEvents.rbegin(); it != defaultEvents.rend(); ++it) {
|
||||
buttonEventManager.pushEventFront(it->button, it->type);
|
||||
}
|
||||
}
|
||||
|
||||
const unsigned long activityStartTime = millis();
|
||||
|
||||
Reference in New Issue
Block a user