feat: long-press Confirm launches KOReader sync from EPUB (#1808)
## 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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
54d5a788a5
commit
d4069aeae5
@@ -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<uint16_t> paragraphIndex;
|
||||
if (section && currentPage >= 0 && currentPage < section->pageCount) {
|
||||
const uint16_t paragraphPage =
|
||||
currentPage > 0 ? static_cast<uint16_t>(currentPage - 1) : static_cast<uint16_t>(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<KOReaderSyncActivity>(
|
||||
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<uint16_t> paragraphIndex;
|
||||
if (section && currentPage >= 0 && currentPage < section->pageCount) {
|
||||
const uint16_t paragraphPage =
|
||||
currentPage > 0 ? static_cast<uint16_t>(currentPage - 1) : static_cast<uint16_t>(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<KOReaderSyncActivity>(
|
||||
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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user