From d4069aeae5d06a7ffef4fb2da9e061acbacbbe3a Mon Sep 17 00:00:00 2001 From: darkbublu Date: Wed, 17 Jun 2026 07:10:26 -0700 Subject: [PATCH] feat: long-press Confirm launches KOReader sync from EPUB (#1808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Hold the menu (Confirm) button for ≥1s while reading an EPUB to launch the existing `KOReaderSyncActivity` directly — replaces the three-step path (open reader menu → scroll to Sync → confirm) with a single gesture. - Reuses `ReaderUtils::GO_HOME_MS` (same 1s threshold used by long-press Back) and the existing `KOREADER_STORE.hasCredentials()` guard. - Adds a Controls picker **"Long-press Menu"** (`longPressMenuFunction`, default **Bookmark**) that **cycles through the available functions** bound to the long-press gesture: `KOSync → Disabled → Bookmark`. The field name and `LONG_PRESS_MENU_FUNCTION` enum are intentionally general so future actions (dictionary lookup, table of contents, etc.) can be appended without another schema migration. The setting is **not** a binary toggle. - Existing menu Sync entry still works — both call sites share one extracted helper (`launchKOReaderSync`); no logic duplication. - Short-press Confirm release is gated on duration so the reader menu does not also open after a long press that *acts*, mirroring the existing long-press Back pattern. - **No-credentials fall-through:** `launchKOReaderSync()` now returns whether it acted. When the function is set to KOSync but no KOReader credentials are stored, the long-press is a no-op that **falls through to open the reader menu** — so the menu stays reachable instead of the hold silently swallowing the gesture. The release is only suppressed when sync actually launched or surfaced a save error. ## Test plan - [x] `pio run` succeeds clean for the `default` ESP32-C3 environment. - [x] On-device, value **KOSync**, valid KOReader credentials: long-press Confirm ≥1s → sync screen launches; get + update progress return HTTP 200; release does **not** also open the reader menu; returns to the same page. - [x] On-device, value **KOSync**, **no** credentials: long-press Confirm falls through and **opens the reader menu** (regression fix); short-press also opens the menu. - [x] On-device: menu → Sync still launches the same screen (shared helper) and syncs (200/200). - [x] On-device, value **Bookmark**: long-press drops a bookmark and does **not** also open the menu. - [x] On-device, value **Disabled**: long-press Confirm opens the menu on release; no sync, no bookmark. - [x] On-device: long-press Back still goes to the file browser (unchanged path). - [x] Heap: epub is released before the TLS handshake (frees ~16 KB); min free heap stayed ~84 KB during sync, well above the safe floor. No panics/OOM across the session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- USER_GUIDE.md | 9 +- lib/I18n/translations/english.yaml | 3 + open-x4-sdk | 2 +- src/CrossPointSettings.h | 14 ++ src/SettingsList.h | 4 + src/activities/reader/EpubReaderActivity.cpp | 131 +++++++++++-------- src/activities/reader/EpubReaderActivity.h | 3 + 7 files changed, 110 insertions(+), 56 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index e2fd673b..037b6504 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -241,6 +241,10 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Chapter Skip" (default) - Long-pressing skips to next/previous chapter - "Page Scroll" - Long-pressing scrolls a page up/down +- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal: + - "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page. + - "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly. + - "Disabled" - Long-press is ignored; only short-press opens the reader menu. - **Short Power Button Click**: Controls the effect of a short click of the power button: @@ -528,6 +532,7 @@ When reading an EPUB that contains footnotes, you can navigate to the footnote t * **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen. * **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen. * **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more. +* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Disabled" does nothing. A short press always opens the Reader Menu. ### Supported Languages @@ -574,9 +579,9 @@ Accessible by selecting **Chapters** from the Reader Menu. Bookmarks can be created to quickly save and restore your place in a book. -To create a bookmark, hold **Confirm** for 1 second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds. +To create a bookmark, hold **Confirm** for about half a second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds. -To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for 1 second, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel. +To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for about 0.7 seconds, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel. Bookmarks are stored in the `.crosspoint/bookmarks` folder in the JSON format. diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 2ad915ba..35b09976 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" +STR_LONG_PRESS_MENU: "Long-press Menu" STR_FONT_FAMILY: "Reader Font Family" STR_FONT_SIZE: "Reader Font Size" STR_LINE_SPACING: "Reader Line Spacing" @@ -140,6 +141,8 @@ STR_INVERTED: "Inverted" STR_LANDSCAPE_CCW: "Landscape CCW" STR_PREV_NEXT: "Prev/Next" STR_NEXT_PREV: "Next/Prev" +STR_KOSYNC: "KOSync" +STR_BOOKMARK_OPTION: "Bookmark" STR_DISABLED: "Disabled" STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SANS: "Noto Sans" diff --git a/open-x4-sdk b/open-x4-sdk index 26648d64..344c479b 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 26648d643a1c883ab2f71e1869d05fe2a0c9d498 +Subproject commit 344c479b0818cdac8f4c1e43d8c3732d638eb6bc diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 07413595..d001f066 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -136,6 +136,17 @@ class CrossPointSettings { // Short power button press actions enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, FOOTNOTES = 4, SHORT_PWRBTN_COUNT }; + // Long-press Confirm action while reading an EPUB. The setting cycles through these values. + // Persisted in settings.json by index: any new function (e.g. dictionary, bookmark) MUST use a + // value >= 2 and be appended at the END of the enumValues array in SettingsList.h, otherwise the + // stored indices shift and existing saves are silently misinterpreted. + enum LONG_PRESS_MENU_FUNCTION { + LP_MENU_KOSYNC = 0, + LP_MENU_DISABLED = 1, + LP_MENU_BOOKMARK = 2, + LONG_PRESS_MENU_FUNCTION_COUNT + }; + // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; @@ -226,6 +237,9 @@ class CrossPointSettings { uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press page turn button behavior uint8_t longPressButtonBehavior = OFF; + // Long-press Confirm function in EPUB reader (cycles through LONG_PRESS_MENU_FUNCTION values). + // Defaults to Bookmark to preserve the upstream long-press-Confirm-adds-bookmark behavior. + uint8_t longPressMenuFunction = LP_MENU_BOOKMARK; // UI Theme uint8_t uiTheme = LYRA; // Sunlight fading compensation diff --git a/src/SettingsList.h b/src/SettingsList.h index acdba5bb..a60303bc 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -171,12 +171,16 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist {StrId::STR_LONG_PRESS_BEHAVIOR_OFF, StrId::STR_LONG_PRESS_BEHAVIOR_SKIP, StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION}, "longPressButtonBehavior", StrId::STR_CAT_CONTROLS), + SettingInfo::Enum(StrId::STR_LONG_PRESS_MENU, &CrossPointSettings::longPressMenuFunction, + {StrId::STR_KOSYNC, StrId::STR_DISABLED, StrId::STR_BOOKMARK_OPTION}, "longPressMenuFunction", + StrId::STR_CAT_CONTROLS), SettingInfo::Enum( StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES}, "shortPwrBtn", StrId::STR_CAT_CONTROLS), SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack, "pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS), + // --- System --- SettingInfo::Value( StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeoutMinutes, diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 08378ed3..bbb73eb1 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -256,7 +256,9 @@ void EpubReaderActivity::loop() { requestUpdate(); } - // Enter reader menu activity. + // Enter reader menu activity on short-press Confirm. A long-press that fired a bound + // function (bookmark or KOReader sync) sets ignoreNextConfirmRelease so the release + // following the hold does not also open the menu. if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (ignoreNextConfirmRelease) { ignoreNextConfirmRelease = false; @@ -284,14 +286,32 @@ void EpubReaderActivity::loop() { } } - if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() >= ReaderUtils::BOOKMARK_HOLD_MS) { - if (!showBookmarkMessage) { - addBookmark(); - showBookmarkMessage = true; - ignoreNextConfirmRelease = true; // Prevent accidental menu open after adding bookmark - bookmarkMessageTime = millis(); - requestUpdate(); + // Long-press Confirm runs the user-selected function (SETTINGS.longPressMenuFunction). + if (mappedInput.isPressed(MappedInputManager::Button::Confirm)) { + switch (SETTINGS.longPressMenuFunction) { + case CrossPointSettings::LP_MENU_BOOKMARK: + // Hold ~0.4s drops a bookmark at the current page. + if (mappedInput.getHeldTime() >= ReaderUtils::BOOKMARK_HOLD_MS && !showBookmarkMessage) { + addBookmark(); + showBookmarkMessage = true; + ignoreNextConfirmRelease = true; // Prevent accidental menu open after adding bookmark + bookmarkMessageTime = millis(); + requestUpdate(); + } + break; + case CrossPointSettings::LP_MENU_KOSYNC: + // Hold ~1s launches KOReader sync. If sync can't run (no credentials stored), fall + // through so the normal Confirm-release still opens the reader menu. + if (mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { + if (launchKOReaderSync()) { + ignoreNextConfirmRelease = true; // sync launched or error shown; suppress menu open + return; + } + } + break; + case CrossPointSettings::LP_MENU_DISABLED: + default: + break; } } @@ -578,50 +598,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction break; } case EpubReaderMenuActivity::MenuAction::SYNC: { - if (KOREADER_STORE.hasCredentials()) { - const int currentPage = section ? section->currentPage : nextPageNumber; - const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount; - std::optional paragraphIndex; - if (section && currentPage >= 0 && currentPage < section->pageCount) { - const uint16_t paragraphPage = - currentPage > 0 ? static_cast(currentPage - 1) : static_cast(currentPage); - if (const auto pIdx = section->getParagraphIndexForPage(paragraphPage)) { - paragraphIndex = *pIdx; - } - } - - // Pre-compute local KO position and chapter name while Epub is still in RAM. - CrossPointPosition localPos = getCurrentPosition(); - SavedProgressPosition localKoPos = ProgressMapper::toSavedProgress(epub, localPos); - const int tocIdx = epub->getTocIndexForSpineIndex(currentSpineIndex); - std::string localChapterName = (tocIdx >= 0) ? epub->getTocItem(tocIdx).title : ""; - const std::string savedEpubPath = epub->getPath(); - - // Persist current position so the reader resumes at the right page on return. - // goToReader() depends on this file, so abort the sync if the write fails. - if (!saveProgress(currentSpineIndex, currentPage, totalPages)) { - LOG_ERR("KOSync", "Aborting sync because current progress could not be saved"); - pendingSyncSaveError = true; - requestUpdate(); - return; - } - - // Release Epub and Section to free ~65KB RAM for the TLS handshake. - LOG_DBG("KOSync", "Releasing epub for sync (heap before: %u)", (unsigned)ESP.getFreeHeap()); - { - RenderLock lock(*this); - if (section) { - nextPageNumber = section->currentPage; - } - section.reset(); - epub.reset(); - } - LOG_DBG("KOSync", "Epub released (heap after: %u)", (unsigned)ESP.getFreeHeap()); - - activityManager.replaceActivity(std::make_unique( - renderer, mappedInput, savedEpubPath, currentSpineIndex, currentPage, totalPages, std::move(localKoPos), - std::move(localChapterName), paragraphIndex)); - } + launchKOReaderSync(); break; } case EpubReaderMenuActivity::MenuAction::BOOKMARKS: { @@ -633,6 +610,54 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } } +bool EpubReaderActivity::launchKOReaderSync() { + if (!KOREADER_STORE.hasCredentials()) return false; // no-op: nothing to launch + + const int currentPage = section ? section->currentPage : nextPageNumber; + const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount; + std::optional paragraphIndex; + if (section && currentPage >= 0 && currentPage < section->pageCount) { + const uint16_t paragraphPage = + currentPage > 0 ? static_cast(currentPage - 1) : static_cast(currentPage); + if (const auto pIdx = section->getParagraphIndexForPage(paragraphPage)) { + paragraphIndex = *pIdx; + } + } + + // Pre-compute local KO position and chapter name while Epub is still in RAM. + CrossPointPosition localPos = getCurrentPosition(); + SavedProgressPosition localKoPos = ProgressMapper::toSavedProgress(epub, localPos); + const int tocIdx = epub->getTocIndexForSpineIndex(currentSpineIndex); + std::string localChapterName = (tocIdx >= 0) ? epub->getTocItem(tocIdx).title : ""; + const std::string savedEpubPath = epub->getPath(); + + // Persist current position so the reader resumes at the right page on return. + // goToReader() depends on this file, so abort the sync if the write fails. + if (!saveProgress(currentSpineIndex, currentPage, totalPages)) { + LOG_ERR("KOSync", "Aborting sync because current progress could not be saved"); + pendingSyncSaveError = true; + requestUpdate(); + return true; // acted: surfaced a save error to the user + } + + // Release Epub and Section to free ~65KB RAM for the TLS handshake. + LOG_DBG("KOSync", "Releasing epub for sync (heap before: %u)", (unsigned)ESP.getFreeHeap()); + { + RenderLock lock(*this); + if (section) { + nextPageNumber = section->currentPage; + } + section.reset(); + epub.reset(); + } + LOG_DBG("KOSync", "Epub released (heap after: %u)", (unsigned)ESP.getFreeHeap()); + + activityManager.replaceActivity(std::make_unique( + renderer, mappedInput, savedEpubPath, currentSpineIndex, currentPage, totalPages, std::move(localKoPos), + std::move(localChapterName), paragraphIndex)); + return true; // acted: launched the sync activity +} + void EpubReaderActivity::applyOrientation(const uint8_t orientation) { // No-op if the selected orientation matches current settings. if (SETTINGS.orientation == orientation) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 38aecd26..d1b46225 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -60,6 +60,9 @@ class EpubReaderActivity final : public Activity { // Jump to a percentage of the book (0-100), mapping it to spine and page. void jumpToPercent(int percent); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); + // Returns true if sync acted (launched, or surfaced a save error); false if it was a no-op + // because no KOReader credentials are stored. + bool launchKOReaderSync(); void applyOrientation(uint8_t orientation); void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void pageTurn(bool isForwardTurn);