diff --git a/USER_GUIDE.md b/USER_GUIDE.md index e2fd673be8..037b65042f 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/docs/images/wifi/webserver_upload_dragdrop.png b/docs/images/wifi/webserver_upload_dragdrop.png new file mode 100644 index 0000000000..5ab12d1ae7 Binary files /dev/null and b/docs/images/wifi/webserver_upload_dragdrop.png differ diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 2ad915bacc..35b099767d 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/src/CrossPointSettings.h b/src/CrossPointSettings.h index 07413595b1..d001f06666 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 acdba5bb43..a60303bc38 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 08378ed389..bbb73eb106 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 38aecd268d..d1b4622566 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); diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 7e5e1a7eff..ec509a2aee 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -99,7 +99,9 @@ bool KeyboardEntryActivity::handleKeyPress() { case SpecialKeyType::Shift: delPressCount = 0; hintVisible = false; - if (urlMode || inputType == InputType::Url) return true; + // Shift is meaningless in the URL-snippet panel and the symbol layout, but + // must work for the URL letter layout so uppercase letters can be entered (#2178). + if (urlMode) return true; if (symMode) return true; shiftState = (shiftState + 1) % 2; return true; @@ -675,8 +677,8 @@ void KeyboardEntryActivity::render(RenderLock&&) { const char* label; }; const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = { - {(symMode || urlMode || inputType == InputType::Url) ? KeyboardKeyType::Disabled : KeyboardKeyType::Shift, - (symMode || urlMode || inputType == InputType::Url) ? shiftString[0] : shiftString[shiftState]}, + {(symMode || urlMode) ? KeyboardKeyType::Disabled : KeyboardKeyType::Shift, + (symMode || urlMode) ? shiftString[0] : shiftString[shiftState]}, {KeyboardKeyType::Mode, urlMode ? "abc" : (symMode ? "abc" : "#@!")}, {inputType == InputType::Url ? KeyboardKeyType::Mode : KeyboardKeyType::Space, inputType == InputType::Url ? "URL" : nullptr}, diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index 04a5b55715..bec1ba3306 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -291,6 +291,27 @@ .upload-form input[type="file"].has-files::file-selector-button { background-color: #95a5a6; } + .drop-zone { + border: 2px dashed var(--border-color); + border-radius: 6px; + padding: 14px; + text-align: center; + cursor: pointer; + transition: border-color 0.15s, background-color 0.15s; + } + .drop-zone.dragover { + border-color: var(--accent-color); + background-color: var(--accent-color-10); + } + .drop-zone-hint { + margin: 8px 0 0; + font-size: 0.85em; + color: var(--label-color); + pointer-events: none; + } + .drop-zone.dragover .drop-zone-hint { + color: var(--accent-color); + } .upload-btn { background-color: #27ae60; color: white; @@ -1526,7 +1547,10 @@

Contents

📤 Upload file

Select a file to upload to

- +
+ +

⬇ Drop files here — or click to browse

+