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);