diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 67dee480..2aeee6d5 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -167,11 +167,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. - -This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake. +* **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. 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 2c33beb3..97138b2f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -60,6 +60,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 }; @@ -99,6 +102,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; @@ -116,6 +120,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..36099e5e 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -5,6 +5,7 @@ #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "RecentBooksStore.h" #include "ScreenComponents.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..cd437467 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 @@ -163,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 changing page until unpressed skip button + awaitingReleaseAfterSkip = true; + return; + } + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || @@ -174,6 +195,16 @@ void EpubReaderActivity::loop() { return; } + if (awaitingReleaseAfterSkip) { + awaitingReleaseAfterSkip = false; + skipUnpressed = true; + 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; @@ -182,19 +213,6 @@ void EpubReaderActivity::loop() { return; } - const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; - - 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(); - xSemaphoreGive(renderingMutex); - updateRequired = true; - return; - } - // No current section, attempt to rerender the book if (!section) { updateRequired = true; @@ -230,11 +248,20 @@ 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) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + nextPageNumber = 0; + currentSpineIndex += delayedSkipDir; + section.reset(); + delayedSkipPending = false; + xSemaphoreGive(renderingMutex); + updateRequired = true; } vTaskDelay(10 / portTICK_PERIOD_MS); } @@ -386,6 +413,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..3b3aa41b 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -16,6 +16,11 @@ class EpubReaderActivity final : public ActivityWithSubactivity { int nextPageNumber = 0; int pagesUntilFullRefresh = 0; bool updateRequired = false; + bool delayedSkipPending = false; + int delayedSkipDir = 0; + uint32_t delayedSkipExecuteAtMs = 0; + bool awaitingReleaseAfterSkip = false; + bool skipUnpressed = false; const std::function onGoBack; const std::function onGoHome; @@ -25,6 +30,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/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index ad4dd2ff..cf7d4559 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -2,16 +2,12 @@ #include +#include "CrossPointSettings.h" #include "KOReaderCredentialStore.h" #include "KOReaderSyncActivity.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(); } int EpubReaderChapterSelectionActivity::getTotalItems() const { @@ -124,7 +120,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..0f78f26a 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 @@ -111,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 changing page until unpressed skip button + awaitingReleaseAfterSkip = true; + return; + } + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || @@ -122,6 +144,16 @@ void XtcReaderActivity::loop() { return; } + if (awaitingReleaseAfterSkip) { + awaitingReleaseAfterSkip = false; + skipUnpressed = true; + return; + } + + if (delayedSkipPending) { + return; + } + // Handle end of book if (currentPage >= xtc->getPageCount()) { currentPage = xtc->getPageCount() - 1; @@ -129,18 +161,17 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs; - const int skipAmount = skipPages ? 10 : 1; - if (prevReleased) { - 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) { - currentPage += skipAmount; + // Short press: single page forward + currentPage += 1; if (currentPage >= xtc->getPageCount()) { currentPage = xtc->getPageCount(); // Allow showing "End of book" } @@ -150,11 +181,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); } @@ -178,6 +227,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(); @@ -360,6 +422,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..72dff9a2 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -21,6 +21,12 @@ 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; + bool awaitingReleaseAfterSkip = false; + bool skipUnpressed = false; const std::function onGoBack; const std::function onGoHome; @@ -30,6 +36,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, diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index b2cfecaa..ae426206 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -2,13 +2,10 @@ #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "fontIds.h" -namespace { -constexpr int SKIP_PAGE_MS = 700; -} // namespace - int XtcReaderChapterSelectionActivity::getPageItems() const { constexpr int startY = 60; constexpr int lineHeight = 30; @@ -80,7 +77,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 45b7a12d..9662e57f 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -35,7 +35,7 @@ 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, @@ -43,6 +43,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("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; constexpr int systemSettingsCount = 5;