From eae951a28698eb997dc7e317c60bd26795658017 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Wed, 21 Jan 2026 20:56:22 +0300 Subject: [PATCH 1/8] Add longPressDuration setting, replace 700ms --- src/CrossPointSettings.cpp | 21 ++++++++++++++++++- src/CrossPointSettings.h | 5 +++++ .../browser/OpdsBookBrowserActivity.cpp | 3 +-- src/activities/home/MyLibraryActivity.cpp | 4 ++-- src/activities/reader/EpubReaderActivity.cpp | 3 +-- .../EpubReaderChapterSelectionActivity.cpp | 5 ++--- src/activities/reader/XtcReaderActivity.cpp | 3 +-- .../XtcReaderChapterSelectionActivity.cpp | 4 ++-- src/activities/settings/SettingsActivity.cpp | 3 ++- 9 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index f5e8ded5..06213f45 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 20; +constexpr uint8_t SETTINGS_COUNT = 21; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -49,6 +49,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, hyphenationEnabled); + serialization::writePod(outputFile, longPressDuration); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -120,6 +121,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, hyphenationEnabled); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, longPressDuration); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); @@ -195,6 +198,22 @@ int CrossPointSettings::getRefreshFrequency() const { } } +unsigned long CrossPointSettings::getLongPressDurationMs() const { + switch (longPressDuration) { + case LONG_PRESS_DURATION::LP_1S: + return 1UL * 1000; + case LONG_PRESS_DURATION::LP_2S: + default: + return 2UL * 1000; + case LONG_PRESS_DURATION::LP_3S: + return 3UL * 1000; + case LONG_PRESS_DURATION::LP_5S: + return 5UL * 1000; + case LONG_PRESS_DURATION::LP_10S: + return 10UL * 1000; + } +} + int CrossPointSettings::getReaderFontId() const { switch (fontFamily) { case BOOKERLY: diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 8ce32a2c..25c4b631 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -55,6 +55,9 @@ class CrossPointSettings { // Short power button press actions enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 }; + // Long-press duration options + enum LONG_PRESS_DURATION { LP_1S = 0, LP_2S = 1, LP_3S = 2, LP_5S = 3, LP_10S = 4 }; + // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 }; @@ -94,6 +97,7 @@ class CrossPointSettings { uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press chapter skip on side buttons uint8_t longPressChapterSkip = 1; + uint8_t longPressDuration = LP_2S; ~CrossPointSettings() = default; @@ -111,6 +115,7 @@ class CrossPointSettings { float getReaderLineCompression() const; unsigned long getSleepTimeoutMs() const; int getRefreshFrequency() const; + unsigned long getLongPressDurationMs() const; }; // Helper macro to access settings diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 555cba91..0d7503c4 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -17,7 +17,6 @@ namespace { constexpr int PAGE_ITEMS = 23; -constexpr int SKIP_PAGE_MS = 700; constexpr char OPDS_ROOT_PATH[] = "opds"; // No leading slash - relative to server URL } // namespace @@ -123,7 +122,7 @@ void OpdsBookBrowserActivity::loop() { mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + const bool skipPage = mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!entries.empty()) { diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 9e6f3734..134451dd 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -6,6 +6,7 @@ #include #include "MappedInputManager.h" +#include "CrossPointSettings.h" #include "RecentBooksStore.h" #include "ScreenComponents.h" #include "fontIds.h" @@ -20,7 +21,6 @@ constexpr int LEFT_MARGIN = 20; constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator // Timing thresholds -constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; void sortFileList(std::vector& strs) { @@ -201,7 +201,7 @@ void MyLibraryActivity::loop() { const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + const bool skipPage = mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); // Confirm button - open selected item if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6ff39c5e..55b20ee6 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -15,7 +15,6 @@ namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() -constexpr unsigned long skipChapterMs = 700; constexpr unsigned long goHomeMs = 1000; constexpr int statusBarMargin = 19; } // namespace @@ -182,7 +181,7 @@ void EpubReaderActivity::loop() { return; } - const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; + const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); if (skipChapter) { // We don't want to delete the section mid-render, so grab the semaphore diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index ad4dd2ff..d34d8577 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -4,12 +4,11 @@ #include "KOReaderCredentialStore.h" #include "KOReaderSyncActivity.h" +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "fontIds.h" namespace { -// Time threshold for treating a long press as a page-up/page-down -constexpr int SKIP_PAGE_MS = 700; } // namespace bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); } @@ -124,7 +123,7 @@ void EpubReaderChapterSelectionActivity::loop() { const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + const bool skipPage = mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); const int pageItems = getPageItems(); const int totalItems = getTotalItems(); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 0a58d7b3..fe444da3 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -19,7 +19,6 @@ #include "fontIds.h" namespace { -constexpr unsigned long skipPageMs = 700; constexpr unsigned long goHomeMs = 1000; } // namespace @@ -129,7 +128,7 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs; + const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); const int skipAmount = skipPages ? 10 : 1; if (prevReleased) { diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index b2cfecaa..bb31aa44 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -2,11 +2,11 @@ #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "fontIds.h" namespace { -constexpr int SKIP_PAGE_MS = 700; } // namespace int XtcReaderChapterSelectionActivity::getPageItems() const { @@ -80,7 +80,7 @@ void XtcReaderChapterSelectionActivity::loop() { const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + const bool skipPage = mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); const int pageItems = getPageItems(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 943fdb4c..0b53eb94 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -35,13 +35,14 @@ const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; -constexpr int controlsSettingsCount = 4; +constexpr int controlsSettingsCount = 5; const SettingInfo controlsSettings[controlsSettingsCount] = { SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout, {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}), SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}), SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), + SettingInfo::Enum("Long-press Duration", &CrossPointSettings::longPressDuration, {"1s", "2s", "3s", "5s", "10s"}), SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; constexpr int systemSettingsCount = 5; From e3b0c924c6854c783c88b30c735b33a142b281fe Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Wed, 21 Jan 2026 21:57:19 +0300 Subject: [PATCH 2/8] Add skip notification and delay before action --- src/activities/reader/EpubReaderActivity.cpp | 33 ++++++++++-- src/activities/reader/EpubReaderActivity.h | 4 ++ src/activities/reader/XtcReaderActivity.cpp | 53 ++++++++++++++++++++ src/activities/reader/XtcReaderActivity.h | 5 ++ 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 55b20ee6..28734ee8 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -186,11 +186,13 @@ void EpubReaderActivity::loop() { if (skipChapter) { // We don't want to delete the section mid-render, so grab the semaphore xSemaphoreTake(renderingMutex, portMAX_DELAY); - nextPageNumber = 0; - currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1; - section.reset(); + // Show immediate feedback for long-press skip, then schedule delayed action (500ms) + showSkipPopup("Skipping"); + delayedSkipPending = true; + delayedSkipDir = nextReleased ? +1 : -1; + delayedSkipExecuteAtMs = millis() + 500; xSemaphoreGive(renderingMutex); - updateRequired = true; + // Do not perform the skip immediately; it will be executed in display loop after delay return; } @@ -229,11 +231,21 @@ void EpubReaderActivity::loop() { void EpubReaderActivity::displayTaskLoop() { while (true) { + const uint32_t now = millis(); if (updateRequired) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); renderScreen(); xSemaphoreGive(renderingMutex); + } else if (delayedSkipPending && now >= delayedSkipExecuteAtMs) { + // Execute the delayed chapter skip now + xSemaphoreTake(renderingMutex, portMAX_DELAY); + nextPageNumber = 0; + currentSpineIndex += delayedSkipDir; + section.reset(); + delayedSkipPending = false; + xSemaphoreGive(renderingMutex); + updateRequired = true; } vTaskDelay(10 / portTICK_PERIOD_MS); } @@ -385,6 +397,19 @@ void EpubReaderActivity::renderScreen() { } } +void EpubReaderActivity::showSkipPopup(const char* text) { + constexpr int boxMargin = 20; + const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, text); + const int boxWidth = textWidth + boxMargin * 2; + const int boxHeight = renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; + const int boxX = (renderer.getScreenWidth() - boxWidth) / 2; + constexpr int boxY = 50; + renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); + renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, text); + renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); + renderer.displayBuffer(EInkDisplay::FAST_REFRESH); +} + void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 63d48872..2f3d6dbc 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -16,6 +16,9 @@ class EpubReaderActivity final : public ActivityWithSubactivity { int nextPageNumber = 0; int pagesUntilFullRefresh = 0; bool updateRequired = false; + bool delayedSkipPending = false; + int delayedSkipDir = 0; + uint32_t delayedSkipExecuteAtMs = 0; const std::function onGoBack; const std::function onGoHome; @@ -25,6 +28,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity { void renderContents(std::unique_ptr page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; + void showSkipPopup(const char* text); public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index fe444da3..21c2ef39 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -132,6 +132,16 @@ void XtcReaderActivity::loop() { const int skipAmount = skipPages ? 10 : 1; if (prevReleased) { + if (skipPages) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + showSkipPopup("Skipping"); + delayedSkipPending = true; + delayedSkipDir = -1; + delayedSkipAmount = skipAmount; + delayedSkipExecuteAtMs = millis() + 500; + xSemaphoreGive(renderingMutex); + return; + } if (currentPage >= static_cast(skipAmount)) { currentPage -= skipAmount; } else { @@ -139,6 +149,16 @@ void XtcReaderActivity::loop() { } updateRequired = true; } else if (nextReleased) { + if (skipPages) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + showSkipPopup("Skipping"); + delayedSkipPending = true; + delayedSkipDir = +1; + delayedSkipAmount = skipAmount; + delayedSkipExecuteAtMs = millis() + 500; + xSemaphoreGive(renderingMutex); + return; + } currentPage += skipAmount; if (currentPage >= xtc->getPageCount()) { currentPage = xtc->getPageCount(); // Allow showing "End of book" @@ -149,11 +169,29 @@ void XtcReaderActivity::loop() { void XtcReaderActivity::displayTaskLoop() { while (true) { + const uint32_t now = millis(); if (updateRequired) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); renderScreen(); xSemaphoreGive(renderingMutex); + } else if (delayedSkipPending && now >= delayedSkipExecuteAtMs) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (delayedSkipDir < 0) { + if (currentPage >= delayedSkipAmount) { + currentPage -= delayedSkipAmount; + } else { + currentPage = 0; + } + } else { + currentPage += delayedSkipAmount; + if (currentPage >= xtc->getPageCount()) { + currentPage = xtc->getPageCount(); + } + } + delayedSkipPending = false; + xSemaphoreGive(renderingMutex); + updateRequired = true; } vTaskDelay(10 / portTICK_PERIOD_MS); } @@ -177,6 +215,19 @@ void XtcReaderActivity::renderScreen() { saveProgress(); } +void XtcReaderActivity::showSkipPopup(const char* text) { + constexpr int boxMargin = 20; + const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, text); + const int boxWidth = textWidth + boxMargin * 2; + const int boxHeight = renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; + const int boxX = (renderer.getScreenWidth() - boxWidth) / 2; + constexpr int boxY = 50; + renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); + renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, text); + renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); + renderer.displayBuffer(EInkDisplay::FAST_REFRESH); +} + void XtcReaderActivity::renderPage() { const uint16_t pageWidth = xtc->getPageWidth(); const uint16_t pageHeight = xtc->getPageHeight(); @@ -359,6 +410,8 @@ void XtcReaderActivity::renderPage() { bitDepth); } +// scheduleSkipMessage removed: delayed skip now handled via delayedSkip* fields + void XtcReaderActivity::saveProgress() const { FsFile f; if (SdMan.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 579e1777..6db673f1 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -21,6 +21,10 @@ class XtcReaderActivity final : public ActivityWithSubactivity { uint32_t currentPage = 0; int pagesUntilFullRefresh = 0; bool updateRequired = false; + bool delayedSkipPending = false; + int delayedSkipDir = 0; + uint32_t delayedSkipExecuteAtMs = 0; + uint32_t delayedSkipAmount = 0; const std::function onGoBack; const std::function onGoHome; @@ -30,6 +34,7 @@ class XtcReaderActivity final : public ActivityWithSubactivity { void renderPage(); void saveProgress() const; void loadProgress(); + void showSkipPopup(const char* text); public: explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr xtc, From 4824247137ec0c41b57ac90d2a835f0dc1798b0b Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Wed, 21 Jan 2026 22:09:07 +0300 Subject: [PATCH 3/8] Update Chapter Navigation user guide --- USER_GUIDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index d670abb7..37dcaed3 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -166,10 +166,10 @@ The role of the volume (side) buttons can be swapped in **[Settings](#35-setting If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button. ### Chapter Navigation -* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release. -* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release. +* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button for 2 seconds, then release. +* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button for 2 seconds, then release. -This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake. +This feature can be disabled in **[Settings](#35-settings)** where the long-press hold time can also be configured. ### System Navigation From de9271062739acacec2bc161bf801f7c93b98156 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Wed, 21 Jan 2026 23:33:26 +0300 Subject: [PATCH 4/8] Detect long-press and skip chapter immediately --- src/activities/reader/EpubReaderActivity.cpp | 42 +++++++++----- src/activities/reader/EpubReaderActivity.h | 2 + src/activities/reader/XtcReaderActivity.cpp | 60 +++++++++++--------- src/activities/reader/XtcReaderActivity.h | 2 + 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 28734ee8..7ebc4443 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -162,6 +162,28 @@ void EpubReaderActivity::loop() { return; } + // Detect long-press and schedule skip immediately + const bool prevPressed = mappedInput.isPressed(MappedInputManager::Button::PageBack) || + mappedInput.isPressed(MappedInputManager::Button::Left); + const bool nextPressed = mappedInput.isPressed(MappedInputManager::Button::PageForward) || + (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + mappedInput.isPressed(MappedInputManager::Button::Power)) || + mappedInput.isPressed(MappedInputManager::Button::Right); + + if (SETTINGS.longPressChapterSkip && (prevPressed || nextPressed) && + mappedInput.getHeldTime() >= SETTINGS.getLongPressDurationMs() && !delayedSkipPending && + !awaitingReleaseAfterSkip) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + showSkipPopup("Skipping"); + delayedSkipPending = true; + delayedSkipDir = nextPressed ? +1 : -1; + delayedSkipExecuteAtMs = millis() + 500; + xSemaphoreGive(renderingMutex); + // Block release-based page change until unpressed + awaitingReleaseAfterSkip = true; + return; + } + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || @@ -173,6 +195,12 @@ void EpubReaderActivity::loop() { return; } + if (awaitingReleaseAfterSkip) { + awaitingReleaseAfterSkip = false; + skipUnpressed = true; + return; + } + // any botton press when at end of the book goes back to the last page if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { currentSpineIndex = epub->getSpineItemsCount() - 1; @@ -181,20 +209,6 @@ void EpubReaderActivity::loop() { return; } - const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); - - if (skipChapter) { - // We don't want to delete the section mid-render, so grab the semaphore - xSemaphoreTake(renderingMutex, portMAX_DELAY); - // Show immediate feedback for long-press skip, then schedule delayed action (500ms) - showSkipPopup("Skipping"); - delayedSkipPending = true; - delayedSkipDir = nextReleased ? +1 : -1; - delayedSkipExecuteAtMs = millis() + 500; - xSemaphoreGive(renderingMutex); - // Do not perform the skip immediately; it will be executed in display loop after delay - return; - } // No current section, attempt to rerender the book if (!section) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 2f3d6dbc..3b3aa41b 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -19,6 +19,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity { bool delayedSkipPending = false; int delayedSkipDir = 0; uint32_t delayedSkipExecuteAtMs = 0; + bool awaitingReleaseAfterSkip = false; + bool skipUnpressed = false; const std::function onGoBack; const std::function onGoHome; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 21c2ef39..5251cc85 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -110,6 +110,29 @@ void XtcReaderActivity::loop() { return; } + // Detect long-press and schedule skip immediately + const bool prevPressed = mappedInput.isPressed(MappedInputManager::Button::PageBack) || + mappedInput.isPressed(MappedInputManager::Button::Left); + const bool nextPressed = mappedInput.isPressed(MappedInputManager::Button::PageForward) || + (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + mappedInput.isPressed(MappedInputManager::Button::Power)) || + mappedInput.isPressed(MappedInputManager::Button::Right); + + if (SETTINGS.longPressChapterSkip && (prevPressed || nextPressed) && + mappedInput.getHeldTime() >= SETTINGS.getLongPressDurationMs() && !delayedSkipPending && + !awaitingReleaseAfterSkip) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + showSkipPopup("Skipping"); + delayedSkipPending = true; + delayedSkipDir = nextPressed ? +1 : -1; + delayedSkipAmount = 10; // long-press skip amount + delayedSkipExecuteAtMs = millis() + 500; + xSemaphoreGive(renderingMutex); + // Block release-based page change until unpressed + awaitingReleaseAfterSkip = true; + return; + } + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || @@ -121,6 +144,12 @@ void XtcReaderActivity::loop() { return; } + if (awaitingReleaseAfterSkip) { + awaitingReleaseAfterSkip = false; + skipUnpressed = true; + return; + } + // Handle end of book if (currentPage >= xtc->getPageCount()) { currentPage = xtc->getPageCount() - 1; @@ -128,38 +157,17 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > SETTINGS.getLongPressDurationMs(); - const int skipAmount = skipPages ? 10 : 1; - if (prevReleased) { - if (skipPages) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - showSkipPopup("Skipping"); - delayedSkipPending = true; - delayedSkipDir = -1; - delayedSkipAmount = skipAmount; - delayedSkipExecuteAtMs = millis() + 500; - xSemaphoreGive(renderingMutex); - return; - } - if (currentPage >= static_cast(skipAmount)) { - currentPage -= skipAmount; + // Short press: single page back + if (currentPage >= 1) { + currentPage -= 1; } else { currentPage = 0; } updateRequired = true; } else if (nextReleased) { - if (skipPages) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - showSkipPopup("Skipping"); - delayedSkipPending = true; - delayedSkipDir = +1; - delayedSkipAmount = skipAmount; - delayedSkipExecuteAtMs = millis() + 500; - xSemaphoreGive(renderingMutex); - return; - } - currentPage += skipAmount; + // Short press: single page forward + currentPage += 1; if (currentPage >= xtc->getPageCount()) { currentPage = xtc->getPageCount(); // Allow showing "End of book" } diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 6db673f1..75c20d01 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -25,6 +25,8 @@ class XtcReaderActivity final : public ActivityWithSubactivity { int delayedSkipDir = 0; uint32_t delayedSkipExecuteAtMs = 0; uint32_t delayedSkipAmount = 0; + bool awaitingReleaseAfterSkip = false; + bool skipUnpressed = false; const std::function onGoBack; const std::function onGoHome; From 344f675233548c3e74c96a970c1d2627b2ac3440 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Wed, 21 Jan 2026 23:35:01 +0300 Subject: [PATCH 5/8] Update USER_GUIDE.md --- USER_GUIDE.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 37dcaed3..470324f3 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -166,11 +166,10 @@ The role of the volume (side) buttons can be swapped in **[Settings](#35-setting If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button. ### Chapter Navigation -* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button for 2 seconds, then release. -* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button for 2 seconds, then release. - -This feature can be disabled in **[Settings](#35-settings)** where the long-press hold time can also be configured. +* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button for 2 seconds. +* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button for 2 seconds. +This long-press feature can be configured in **[Settings](#35-settings)**. ### System Navigation * **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen. From 92337be649143fb82428219f0309171e145c8f6f Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 22 Jan 2026 00:11:46 +0300 Subject: [PATCH 6/8] Don't change pages during skipping delay --- src/activities/reader/EpubReaderActivity.cpp | 7 +++++-- src/activities/reader/XtcReaderActivity.cpp | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 7ebc4443..1673d7d5 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -179,7 +179,7 @@ void EpubReaderActivity::loop() { delayedSkipDir = nextPressed ? +1 : -1; delayedSkipExecuteAtMs = millis() + 500; xSemaphoreGive(renderingMutex); - // Block release-based page change until unpressed + // Block changing page until unpressed skip button awaitingReleaseAfterSkip = true; return; } @@ -191,6 +191,10 @@ void EpubReaderActivity::loop() { mappedInput.wasReleased(MappedInputManager::Button::Power)) || mappedInput.wasReleased(MappedInputManager::Button::Right); + if (delayedSkipPending) { + return; + } + if (!prevReleased && !nextReleased) { return; } @@ -252,7 +256,6 @@ void EpubReaderActivity::displayTaskLoop() { renderScreen(); xSemaphoreGive(renderingMutex); } else if (delayedSkipPending && now >= delayedSkipExecuteAtMs) { - // Execute the delayed chapter skip now xSemaphoreTake(renderingMutex, portMAX_DELAY); nextPageNumber = 0; currentSpineIndex += delayedSkipDir; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 5251cc85..d1c69601 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -128,7 +128,7 @@ void XtcReaderActivity::loop() { delayedSkipAmount = 10; // long-press skip amount delayedSkipExecuteAtMs = millis() + 500; xSemaphoreGive(renderingMutex); - // Block release-based page change until unpressed + // Block changing page until unpressed skip button awaitingReleaseAfterSkip = true; return; } @@ -140,6 +140,10 @@ void XtcReaderActivity::loop() { mappedInput.wasReleased(MappedInputManager::Button::Power)) || mappedInput.wasReleased(MappedInputManager::Button::Right); + if (delayedSkipPending) { + return; + } + if (!prevReleased && !nextReleased) { return; } From ffc30eb18c11950c723230f146448ca83878ad17 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 22 Jan 2026 00:53:42 +0300 Subject: [PATCH 7/8] Fix unpressing while skipping delay block --- src/activities/reader/EpubReaderActivity.cpp | 8 ++++---- src/activities/reader/XtcReaderActivity.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 1673d7d5..045d6f76 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -191,10 +191,6 @@ void EpubReaderActivity::loop() { mappedInput.wasReleased(MappedInputManager::Button::Power)) || mappedInput.wasReleased(MappedInputManager::Button::Right); - if (delayedSkipPending) { - return; - } - if (!prevReleased && !nextReleased) { return; } @@ -205,6 +201,10 @@ void EpubReaderActivity::loop() { return; } + if (delayedSkipPending) { + return; + } + // any botton press when at end of the book goes back to the last page if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { currentSpineIndex = epub->getSpineItemsCount() - 1; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index d1c69601..a02aa3f0 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -140,10 +140,6 @@ void XtcReaderActivity::loop() { mappedInput.wasReleased(MappedInputManager::Button::Power)) || mappedInput.wasReleased(MappedInputManager::Button::Right); - if (delayedSkipPending) { - return; - } - if (!prevReleased && !nextReleased) { return; } @@ -154,6 +150,10 @@ void XtcReaderActivity::loop() { return; } + if (delayedSkipPending) { + return; + } + // Handle end of book if (currentPage >= xtc->getPageCount()) { currentPage = xtc->getPageCount() - 1; From 3664355b1ad8d0084af4aff9f5993e78c2be72ea Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 22 Jan 2026 02:43:40 +0300 Subject: [PATCH 8/8] Run clang-format-fix --- src/activities/home/MyLibraryActivity.cpp | 2 +- src/activities/reader/EpubReaderActivity.cpp | 3 +-- src/activities/reader/EpubReaderChapterSelectionActivity.cpp | 5 +---- src/activities/reader/XtcReaderActivity.cpp | 2 +- src/activities/reader/XtcReaderActivity.h | 2 +- src/activities/reader/XtcReaderChapterSelectionActivity.cpp | 3 --- src/activities/settings/SettingsActivity.cpp | 2 +- 7 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 134451dd..36099e5e 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -5,8 +5,8 @@ #include -#include "MappedInputManager.h" #include "CrossPointSettings.h" +#include "MappedInputManager.h" #include "RecentBooksStore.h" #include "ScreenComponents.h" #include "fontIds.h" diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 045d6f76..cd437467 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -162,7 +162,7 @@ void EpubReaderActivity::loop() { return; } - // Detect long-press and schedule skip immediately + // Detect long-press and schedule skip immediately const bool prevPressed = mappedInput.isPressed(MappedInputManager::Button::PageBack) || mappedInput.isPressed(MappedInputManager::Button::Left); const bool nextPressed = mappedInput.isPressed(MappedInputManager::Button::PageForward) || @@ -213,7 +213,6 @@ void EpubReaderActivity::loop() { return; } - // No current section, attempt to rerender the book if (!section) { updateRequired = true; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index d34d8577..cf7d4559 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -2,15 +2,12 @@ #include +#include "CrossPointSettings.h" #include "KOReaderCredentialStore.h" #include "KOReaderSyncActivity.h" -#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "fontIds.h" -namespace { -} // namespace - bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); } int EpubReaderChapterSelectionActivity::getTotalItems() const { diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index a02aa3f0..0f78f26a 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -110,7 +110,7 @@ void XtcReaderActivity::loop() { return; } - // Detect long-press and schedule skip immediately + // Detect long-press and schedule skip immediately const bool prevPressed = mappedInput.isPressed(MappedInputManager::Button::PageBack) || mappedInput.isPressed(MappedInputManager::Button::Left); const bool nextPressed = mappedInput.isPressed(MappedInputManager::Button::PageForward) || diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 75c20d01..72dff9a2 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -24,7 +24,7 @@ class XtcReaderActivity final : public ActivityWithSubactivity { bool delayedSkipPending = false; int delayedSkipDir = 0; uint32_t delayedSkipExecuteAtMs = 0; - uint32_t delayedSkipAmount = 0; + uint32_t delayedSkipAmount = 0; bool awaitingReleaseAfterSkip = false; bool skipUnpressed = false; const std::function onGoBack; diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index bb31aa44..ae426206 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -6,9 +6,6 @@ #include "MappedInputManager.h" #include "fontIds.h" -namespace { -} // namespace - int XtcReaderChapterSelectionActivity::getPageItems() const { constexpr int startY = 60; constexpr int lineHeight = 30; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 0b53eb94..2b390351 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -42,7 +42,7 @@ const SettingInfo controlsSettings[controlsSettingsCount] = { SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}), SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), - SettingInfo::Enum("Long-press Duration", &CrossPointSettings::longPressDuration, {"1s", "2s", "3s", "5s", "10s"}), + SettingInfo::Enum("Long-press Duration", &CrossPointSettings::longPressDuration, {"1s", "2s", "3s", "5s", "10s"}), SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; constexpr int systemSettingsCount = 5;