From eae951a28698eb997dc7e317c60bd26795658017 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Wed, 21 Jan 2026 20:56:22 +0300 Subject: [PATCH 1/3] 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 f5e8ded..06213f4 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 8ce32a2..25c4b63 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 555cba9..0d7503c 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 9e6f373..134451d 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 6ff39c5..55b20ee 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 ad4dd2f..d34d857 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 0a58d7b..fe444da 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 b2cfeca..bb31aa4 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 943fdb4..0b53eb9 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/3] 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 55b20ee..28734ee 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 63d4887..2f3d6db 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 fe444da..21c2ef3 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 579e177..6db673f 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/3] 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 d670abb..37dcaed 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