From 80eb889256c7c9e93fda520a6ce96e6e25c055d4 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 10 May 2026 20:47:35 +0200 Subject: [PATCH] Add opds image download --- lib/OpdsParser/OpdsParser.cpp | 4 + lib/OpdsParser/OpdsParser.h | 1 + src/ButtonEventManager.cpp | 5 +- src/ButtonEventManager.h | 19 ++++- .../browser/OpdsBookBrowserActivity.cpp | 75 ++++++++++++++++--- src/activities/reader/EpubReaderActivity.cpp | 8 +- src/activities/reader/ReaderUtils.h | 8 +- src/activities/reader/XtcReaderActivity.cpp | 8 +- 8 files changed, 100 insertions(+), 28 deletions(-) diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index 851180b1..0986efd1 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -197,6 +197,10 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons } self->currentEntry.acquisitionLinks.push_back(acquisition); } + } else if (rel && type && strstr(rel, "opds-spec.org/image") != nullptr && + strstr(rel, "thumbnail") == nullptr && strstr(type, "image/jpeg") != nullptr && + self->currentEntry.imageHref.empty()) { + self->currentEntry.imageHref = href; } else if (type && strstr(type, "application/atom+xml") != nullptr) { if (self->currentEntry.type != OpdsEntryType::BOOK) { self->currentEntry.type = OpdsEntryType::NAVIGATION; diff --git a/lib/OpdsParser/OpdsParser.h b/lib/OpdsParser/OpdsParser.h index db537cfd..d5f863b5 100644 --- a/lib/OpdsParser/OpdsParser.h +++ b/lib/OpdsParser/OpdsParser.h @@ -31,6 +31,7 @@ struct OpdsEntry { std::string href; // Navigation URL or epub download URL std::string id; std::vector acquisitionLinks; + std::string imageHref; // Cover image URL (rel="http://opds-spec.org/image"), books only }; // Legacy alias for backward compatibility diff --git a/src/ButtonEventManager.cpp b/src/ButtonEventManager.cpp index 3558bf5e..93d63baf 100644 --- a/src/ButtonEventManager.cpp +++ b/src/ButtonEventManager.cpp @@ -14,7 +14,10 @@ int ButtonEventManager::buttonToIndex(const Button button) { return -1; } -bool ButtonEventManager::hasDoubleAction(const Button button) { +bool ButtonEventManager::hasDoubleAction(const Button button) const { + if (forcedDoubleMask & (1 << static_cast(button))) { + return true; + } using BA = CrossPointSettings::BUTTON_ACTION; switch (button) { case Button::Back: diff --git a/src/ButtonEventManager.h b/src/ButtonEventManager.h index 1cccecb5..3d12e27c 100644 --- a/src/ButtonEventManager.h +++ b/src/ButtonEventManager.h @@ -49,6 +49,16 @@ class ButtonEventManager { // Reset all per-button FSMs. Call on activity transitions to prevent bleed-through. void drain(); + // Temporarily force double-click detection for a button (adds latency to Short press). + // Call this in the Activity's transition setup or loop. + void forceDoubleAction(Button button, bool enable = true) { + if (enable) { + forcedDoubleMask |= (1 << static_cast(button)); + } else { + forcedDoubleMask &= ~(1 << static_cast(button)); + } + } + // Preserve a default event for activity processing after main loop dispatch. // This is used when the configured action is BTN_DEFAULT. void pushEventFront(Button button, PressType type); @@ -59,14 +69,17 @@ class ButtonEventManager { // Returns true if a double-click action is configured for this button. // ButtonEventManager queries CrossPointSettings internally. - static bool hasDoubleAction(Button button); + bool hasDoubleAction(Button button) const; private: - static constexpr int NUM_BUTTONS = 7; + static constexpr int NUM_BUTTONS = 9; static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = { - Button::Back, Button::Confirm, Button::Left, Button::Right, Button::PageBack, Button::PageForward, Button::Power, + Button::Back, Button::Confirm, Button::Left, Button::Right, Button::Up, + Button::Down, Button::PageBack, Button::PageForward, Button::Power, }; + uint32_t forcedDoubleMask = 0; + enum class State { Idle, Pressed, ReleasedOnce, DoublePressed }; struct PerButton { diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index e3cfa10f..563346f8 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -15,6 +15,7 @@ #include #include +#include "ButtonEventManager.h" #include "MappedInputManager.h" #include "OpdsFormatLabel.h" #include "activities/network/WifiSelectionActivity.h" @@ -63,6 +64,7 @@ void writeEntryToCache(HalFile& f, const OpdsEntry& entry) { writeString(f, entry.author); writeString(f, entry.href); writeString(f, entry.id); + writeString(f, entry.imageHref); uint16_t numLinks = entry.acquisitionLinks.size(); f.write(reinterpret_cast(&numLinks), sizeof(numLinks)); for (const auto& link : entry.acquisitionLinks) { @@ -83,6 +85,7 @@ OpdsEntry readEntryFromCache(HalFile& f) { entry.author = readString(f); entry.href = readString(f); entry.id = readString(f); + entry.imageHref = readString(f); uint16_t numLinks = 0; if (f.read(&numLinks, sizeof(numLinks)) == sizeof(numLinks)) { for (uint16_t i = 0; i < numLinks; ++i) { @@ -110,6 +113,9 @@ OpdsEntry OpdsBookBrowserActivity::getEntry(size_t index) const { void OpdsBookBrowserActivity::onEnter() { Activity::onEnter(); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, true); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, true); + state = BrowserState::CHECK_WIFI; entryOffsets.clear(); navigationHistory.clear(); @@ -131,6 +137,9 @@ void OpdsBookBrowserActivity::onEnter() { void OpdsBookBrowserActivity::onExit() { Activity::onExit(); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, false); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, false); + HalClock::wifiOff(); entryOffsets.clear(); @@ -231,17 +240,29 @@ void OpdsBookBrowserActivity::loop() { } if (!entryOffsets.empty()) { - // Navigator is restricted to Up/Down so a Left release used to launch - // search (line above) cannot also be consumed here as a previous-item - // step on the same tick. - buttonNavigator.onRelease({MappedInputManager::Button::Down}, [this] { - selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entryOffsets.size()); - requestUpdate(); - }); - buttonNavigator.onRelease({MappedInputManager::Button::Up}, [this] { - selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entryOffsets.size()); - requestUpdate(); - }); + ButtonEventManager::ButtonEvent extEvent; + while (globalButtonEvents().consumeEvent(extEvent)) { + if (extEvent.type == ButtonEventManager::PressType::Double) { + if (extEvent.button == ButtonEventManager::Button::Down) { + selectorIndex = (selectorIndex + 9) % entryOffsets.size(); + requestUpdate(); + } else if (extEvent.button == ButtonEventManager::Button::Up) { + int size = entryOffsets.size(); + selectorIndex = (selectorIndex - 9 + size) % size; + requestUpdate(); + } + } else if (extEvent.type == ButtonEventManager::PressType::Short || + extEvent.type == ButtonEventManager::PressType::Long) { + if (extEvent.button == ButtonEventManager::Button::Down) { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entryOffsets.size()); + requestUpdate(); + } else if (extEvent.button == ButtonEventManager::Button::Up) { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entryOffsets.size()); + requestUpdate(); + } + } + } + buttonNavigator.onContinuous({MappedInputManager::Button::Down}, [this] { selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entryOffsets.size(), PAGE_ITEMS); requestUpdate(); @@ -537,7 +558,37 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const OpdsAcqu // Clear any existing cache for this book just in case it's a redownload of // a previously opened book. if (acquisition.mimeType == "application/epub+zip") { - Epub(filename, "/.crosspoint").clearCache(); + if (!book.imageHref.empty()) { + const std::string coverUrl = + (book.imageHref.rfind("http", 0) == 0) ? book.imageHref : UrlUtils::buildUrl(server.url, book.imageHref); + + std::string baseFilename = filename; + size_t dotPos = baseFilename.find_last_of('.'); + if (dotPos != std::string::npos) { + baseFilename = baseFilename.substr(0, dotPos); + } + + std::string ext = ".jpg"; + if (book.imageHref.length() >= 4) { + std::string lowerHref = book.imageHref.substr(book.imageHref.length() - 4); + std::transform(lowerHref.begin(), lowerHref.end(), lowerHref.begin(), ::tolower); + if (lowerHref == ".png") { + ext = ".png"; + } + } + std::string sidecarPath = baseFilename + ext; + + const auto coverDlResult = HttpDownloader::downloadToFile( + coverUrl, sidecarPath, [this](const size_t, const size_t) { requestUpdate(true); }, server.username, + server.password); + if (coverDlResult != HttpDownloader::OK) { + LOG_ERR("OPDS", "Failed to download cover from %s (err %d)", coverUrl.c_str(), (int)coverDlResult); + Storage.remove(sidecarPath.c_str()); + } + } + + Epub epub(filename, "/.crosspoint"); + epub.clearCache(); } else if (acquisition.formatKey == "xtc" || acquisition.formatKey == "xtch") { Xtc(filename, "/.crosspoint").clearCache(); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 7a795c2c..28ca5ad9 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -328,16 +328,16 @@ void EpubReaderActivity::loop() { if (ev.type == ButtonEventManager::PressType::Short) { if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) || + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack)) || (ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) { + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left))) { delayedPrevTurn = true; continue; } if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) || + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward)) || (ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) { + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right))) { delayedNextTurn = true; continue; } diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index b0383257..851d5079 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -95,16 +95,16 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) { // because the button event system delays short events until the double-click window expires. using BA = CrossPointSettings::BUTTON_ACTION; const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && - !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack) && + !globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack) && input.wasReleased(MappedInputManager::Button::PageBack)) || (SETTINGS.btnShortLeft == BA::BTN_DEFAULT && - !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) && + !globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left) && input.wasReleased(MappedInputManager::Button::Left)); const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && - !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward) && + !globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward) && input.wasReleased(MappedInputManager::Button::PageForward)) || (SETTINGS.btnShortRight == BA::BTN_DEFAULT && - !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) && + !globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right) && input.wasReleased(MappedInputManager::Button::Right)); return {prev, next}; } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 43795b3e..d9e339c6 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -102,16 +102,16 @@ void XtcReaderActivity::loop() { if (ev.type == ButtonEventManager::PressType::Short) { if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) || + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack)) || (ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) { + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left))) { delayedPrevTurn = true; continue; } if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) || + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward)) || (ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT && - ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) { + globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right))) { delayedNextTurn = true; continue; }