diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index 807f82da..a2d6501f 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace { // Returns the length of href after trimming trailing slashes. @@ -146,7 +147,6 @@ void OpdsParser::flush() { bool OpdsParser::error() const { return errorOccured; } void OpdsParser::clear() { - entries.clear(); searchTemplate.clear(); osdUrl.clear(); nextPageUrl.clear(); @@ -156,14 +156,6 @@ void OpdsParser::clear() { inEntry = inTitle = inAuthor = inAuthorName = inId = false; } -std::vector OpdsParser::getBooks() const { - std::vector books; - for (const auto& entry : entries) { - if (entry.type == OpdsEntryType::BOOK) books.push_back(entry); - } - return books; -} - const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) { for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], name) == 0) return atts[i + 1]; @@ -206,6 +198,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 && strncmp(type, "image/", 6) == 0 && + 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; @@ -243,7 +239,10 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) { if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) { - self->entries.push_back(self->currentEntry); + if (self->onEntryParsed) { + self->onEntryParsed(std::move(self->currentEntry)); + self->currentEntry = OpdsEntry{}; + } } self->inEntry = false; } else if (self->inEntry) { diff --git a/lib/OpdsParser/OpdsParser.h b/lib/OpdsParser/OpdsParser.h index e97489c7..8c53e4e5 100644 --- a/lib/OpdsParser/OpdsParser.h +++ b/lib/OpdsParser/OpdsParser.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -30,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 @@ -41,14 +43,17 @@ using OpdsBook = OpdsEntry; * * Usage: * OpdsParser parser; - * if (parser.parse(xmlData, xmlLength)) { - * for (const auto& entry : parser.getEntries()) { - * if (entry.type == OpdsEntryType::BOOK) { - * // Downloadable book - * } else { - * // Navigation link to another catalog - * } + * parser.onEntryParsed = [](OpdsEntry entry) { + * if (entry.type == OpdsEntryType::BOOK) { + * // Process downloadable book + * } else { + * // Process navigation link * } + * }; + * + * // Entries are emitted immediately as they are parsed from the stream. + * if (parser.parse(xmlData, xmlLength)) { + * // Parsing completed successfully * } */ class OpdsParser final : public Print { @@ -73,24 +78,13 @@ class OpdsParser final : public Print { operator bool() { return !error(); } - /** - * Get the parsed entries (both navigation and book entries). - * @return Vector of OpdsEntry entries - */ - const std::vector& getEntries() const& { return entries; } - std::vector getEntries() && { return std::move(entries); } - - /** - * Get only book entries (legacy compatibility). - * @return Vector of book entries - */ - std::vector getBooks() const; - /** * Clear all parsed entries. */ void clear(); + std::function onEntryParsed; + private: // Expat callbacks static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); @@ -105,7 +99,6 @@ class OpdsParser final : public Print { static const char* findAttribute(const XML_Char** atts, const char* name); XML_Parser parser = nullptr; - std::vector entries; OpdsEntry currentEntry; std::string currentText; 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 98ad8053..30482653 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include +#include "ButtonEventManager.h" #include "MappedInputManager.h" #include "OpdsFormatLabel.h" #include "activities/network/WifiSelectionActivity.h" @@ -39,13 +41,83 @@ int formatItemsPerPage(const Rect& contentRect) { const int itemsPerPage = availableHeight / FORMAT_ITEM_HEIGHT; return itemsPerPage > 0 ? itemsPerPage : 1; } + +void writeString(HalFile& f, const std::string& s) { + uint16_t len = s.length(); + f.write(reinterpret_cast(&len), sizeof(len)); + if (len > 0) f.write(reinterpret_cast(s.data()), len); +} + +std::string readString(HalFile& f) { + uint16_t len = 0; + if (f.read(&len, sizeof(len)) != sizeof(len)) return ""; + if (len == 0) return ""; + std::string s(len, '\0'); + f.read(s.data(), len); + return s; +} + +void writeEntryToCache(HalFile& f, const OpdsEntry& entry) { + uint8_t type = static_cast(entry.type); + f.write(&type, sizeof(type)); + writeString(f, entry.title); + 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) { + writeString(f, link.href); + writeString(f, link.mimeType); + writeString(f, link.formatKey); + writeString(f, link.fileExtension); + } +} + +OpdsEntry readEntryFromCache(HalFile& f) { + OpdsEntry entry; + uint8_t type = 0; + if (f.read(&type, sizeof(type)) == sizeof(type)) { + entry.type = static_cast(type); + } + entry.title = readString(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) { + OpdsAcquisitionLink link; + link.href = readString(f); + link.mimeType = readString(f); + link.formatKey = readString(f); + link.fileExtension = readString(f); + entry.acquisitionLinks.push_back(link); + } + } + return entry; +} } // namespace +OpdsEntry OpdsBookBrowserActivity::getEntry(size_t index) const { + if (index >= entryOffsets.size()) return {}; + HalFile f; + if (!Storage.openFileForRead("OPDS", "/.tmp_opds_cache.dat", f)) return {}; + f.seek(entryOffsets[index]); + OpdsEntry entry = readEntryFromCache(f); + return entry; +} + void OpdsBookBrowserActivity::onEnter() { Activity::onEnter(); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, true); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, true); + state = BrowserState::CHECK_WIFI; - entries.clear(); + entryOffsets.clear(); navigationHistory.clear(); currentPath = ""; // Root path - user provides full URL in settings searchTemplate.clear(); @@ -65,11 +137,15 @@ void OpdsBookBrowserActivity::onEnter() { void OpdsBookBrowserActivity::onExit() { Activity::onExit(); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, false); + globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, false); + HalClock::wifiOff(); - entries.clear(); + entryOffsets.clear(); navigationHistory.clear(); formatSelectionLabels.clear(); + Storage.remove("/.tmp_opds_cache.dat"); } void OpdsBookBrowserActivity::loop() { @@ -112,13 +188,13 @@ void OpdsBookBrowserActivity::loop() { if (state == BrowserState::DOWNLOADING) return; if (state == BrowserState::FORMAT_SELECTION) { - if (selectedBookIndex < 0 || selectedBookIndex >= static_cast(entries.size())) { + if (selectedBookIndex < 0 || selectedBookIndex >= static_cast(entryOffsets.size())) { state = BrowserState::BROWSING; requestUpdate(); return; } - const auto& entry = entries[selectedBookIndex]; + const auto entry = getEntry(selectedBookIndex); if (entry.acquisitionLinks.empty()) { state = BrowserState::BROWSING; selectedBookIndex = -1; @@ -140,11 +216,11 @@ void OpdsBookBrowserActivity::loop() { return; } - buttonNavigator.onNextRelease([this, &entry] { + buttonNavigator.onNextRelease([this, entry] { formatSelectorIndex = ButtonNavigator::nextIndex(formatSelectorIndex, entry.acquisitionLinks.size()); requestUpdate(); }); - buttonNavigator.onPreviousRelease([this, &entry] { + buttonNavigator.onPreviousRelease([this, entry] { formatSelectorIndex = ButtonNavigator::previousIndex(formatSelectorIndex, entry.acquisitionLinks.size()); requestUpdate(); }); @@ -153,8 +229,8 @@ void OpdsBookBrowserActivity::loop() { if (state == BrowserState::BROWSING) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (!entries.empty()) { - const auto& entry = entries[selectorIndex]; + if (!entryOffsets.empty()) { + const auto entry = getEntry(selectorIndex); entry.type == OpdsEntryType::BOOK ? chooseBookFormat(entry) : navigateToEntry(entry); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { @@ -163,24 +239,36 @@ void OpdsBookBrowserActivity::loop() { if (!searchTemplate.empty() && selectorIndex == 0) launchSearch(); } - if (!entries.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, entries.size()); - requestUpdate(); - }); - buttonNavigator.onRelease({MappedInputManager::Button::Up}, [this] { - selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); - requestUpdate(); - }); + if (!entryOffsets.empty()) { + 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, entries.size(), PAGE_ITEMS); + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entryOffsets.size(), PAGE_ITEMS); requestUpdate(); }); buttonNavigator.onContinuous({MappedInputManager::Button::Up}, [this] { - selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entryOffsets.size(), PAGE_ITEMS); requestUpdate(); }); } @@ -239,7 +327,7 @@ void OpdsBookBrowserActivity::render(RenderLock&&) { } if (state == BrowserState::FORMAT_SELECTION) { - const auto& entry = entries[selectedBookIndex]; + const auto entry = getEntry(selectedBookIndex); auto title = renderer.truncatedText(UI_10_FONT_ID, entry.title.c_str(), contentRect.width - 40); renderer.drawCenteredText(UI_10_FONT_ID, midY - 40, title.c_str(), true, EpdFontFamily::BOLD); if (!entry.author.empty()) { @@ -269,12 +357,12 @@ void OpdsBookBrowserActivity::render(RenderLock&&) { // Browsing state // Show appropriate button hint based on selected entry type const char* confirmLabel = - (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN); + (!entryOffsets.empty() && getEntry(selectorIndex).type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN); const char* searchLabel = (!searchTemplate.empty() && selectorIndex == 0) ? tr(STR_SEARCH) : tr(STR_DIR_UP); const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, searchLabel, tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - if (entries.empty()) { + if (entryOffsets.empty()) { renderer.drawCenteredText(UI_10_FONT_ID, midY, tr(STR_NO_ENTRIES)); renderer.displayBuffer(); return; @@ -283,8 +371,9 @@ void OpdsBookBrowserActivity::render(RenderLock&&) { const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; renderer.fillRect(contentRect.x, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, contentRect.width - 1, 30); - for (size_t i = pageStartIndex; i < entries.size() && i < static_cast(pageStartIndex + PAGE_ITEMS); i++) { - const auto& entry = entries[i]; + for (size_t i = pageStartIndex; i < entryOffsets.size() && i < static_cast(pageStartIndex + PAGE_ITEMS); + i++) { + const auto entry = getEntry(i); // Format display text with type indicator std::string displayText; @@ -314,10 +403,27 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { return; } + entryOffsets.clear(); + std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(server.url, path); LOG_DBG("OPDS", "Fetching: %s", url.c_str()); OpdsParser parser; + HalFile cacheFile; + + if (!Storage.openFileForWrite("OPDS", "/.tmp_opds_cache.dat", cacheFile)) { + LOG_ERR("OPDS", "Could not open cache file"); + state = BrowserState::ERROR; + errorMessage = "Cache Error"; + requestUpdate(); + return; + } + + parser.onEntryParsed = [&](const OpdsEntry& entry) { + uint32_t offset = cacheFile.position(); + entryOffsets.push_back(offset); + writeEntryToCache(cacheFile, entry); + }; { OpdsParserStream stream{parser}; @@ -342,18 +448,23 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { } const auto& nextUrl = parser.getNextPageUrl(); const auto& prevUrl = parser.getPrevPageUrl(); - entries = std::move(parser).getEntries(); if (!prevUrl.empty()) { - entries.insert(entries.begin(), OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_PREV_PAGE), "", prevUrl, ""}); + std::string resolvedPrevUrl = UrlUtils::buildUrl(url, prevUrl); + OpdsEntry prevEntry{OpdsEntryType::NAVIGATION, tr(STR_PREV_PAGE), "", resolvedPrevUrl, ""}; + entryOffsets.insert(entryOffsets.begin(), cacheFile.position()); + writeEntryToCache(cacheFile, prevEntry); } if (!nextUrl.empty()) { - entries.push_back(OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_NEXT_PAGE), "", nextUrl, ""}); + std::string resolvedNextUrl = UrlUtils::buildUrl(url, nextUrl); + OpdsEntry nextEntry{OpdsEntryType::NAVIGATION, tr(STR_NEXT_PAGE), "", resolvedNextUrl, ""}; + entryOffsets.push_back(cacheFile.position()); + writeEntryToCache(cacheFile, nextEntry); } selectorIndex = 0; - state = entries.empty() ? BrowserState::ERROR : BrowserState::BROWSING; - if (entries.empty()) errorMessage = tr(STR_NO_ENTRIES); + state = entryOffsets.empty() ? BrowserState::ERROR : BrowserState::BROWSING; + if (entryOffsets.empty()) errorMessage = tr(STR_NO_ENTRIES); requestUpdate(); } @@ -362,7 +473,7 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { currentPath = entry.href; state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); - entries.clear(); + entryOffsets.clear(); selectorIndex = 0; requestUpdate(true); fetchFeed(currentPath); @@ -376,7 +487,7 @@ void OpdsBookBrowserActivity::navigateBack() { navigationHistory.pop_back(); state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); - entries.clear(); + entryOffsets.clear(); selectorIndex = 0; requestUpdate(); fetchFeed(currentPath); @@ -451,7 +562,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.resize(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/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index 45b20647..107a2541 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -37,7 +37,7 @@ class OpdsBookBrowserActivity final : public Activity { private: ButtonNavigator buttonNavigator; BrowserState state = BrowserState::LOADING; - std::vector entries; + std::vector entryOffsets; std::vector navigationHistory; std::string currentPath; std::string searchTemplate; @@ -54,6 +54,8 @@ class OpdsBookBrowserActivity final : public Activity { OpdsServer server; // Copied at construction — safe even if the store changes during browsing + OpdsEntry getEntry(size_t index) const; + void checkAndConnectWifi(); void launchWifiSelection(); void onWifiSelectionComplete(bool connected); 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; } diff --git a/src/main.cpp b/src/main.cpp index f8751668..75f25acd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -509,9 +509,6 @@ void loop() { while (buttonEventManager.consumeEvent(ev)) { const uint8_t action = actionFor(ev); if (action == BA::BTN_DEFAULT) { - if (ev.type == ButtonEventManager::PressType::Double) { - continue; - } defaultEvents.push_back(ev); continue; } diff --git a/test/opds_parser/OpdsParserTest.cpp b/test/opds_parser/OpdsParserTest.cpp index d0cb0b3b..7b88ea57 100644 --- a/test/opds_parser/OpdsParserTest.cpp +++ b/test/opds_parser/OpdsParserTest.cpp @@ -51,7 +51,9 @@ bool parseSingleBookEntry(OpdsEntry& entryOut, const char* href, const char* typ )"; + std::vector entries; OpdsParser parser; + parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml.data()), xml.size()); parser.flush(); @@ -60,7 +62,6 @@ bool parseSingleBookEntry(OpdsEntry& entryOut, const char* href, const char* typ testsFailed++; return false; } - const auto& entries = parser.getEntries(); if (entries.size() != 1) { fprintf(stderr, " FAIL: %s:%d: entries.size() == %zu, expected 1\n", __FILE__, __LINE__, entries.size()); testsFailed++; @@ -153,12 +154,13 @@ void testDistinctAcquisitionFormatsRemainSeparate() { )"; + std::vector entries; OpdsParser parser; + parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); - const auto& entries = parser.getEntries(); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 4); @@ -181,12 +183,14 @@ void testUnsupportedMimeType() { )"; + std::vector entries; OpdsParser parser; + parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); - ASSERT_SIZE(parser.getEntries().size(), 0); + ASSERT_SIZE(entries.size(), 0); PASS(); } @@ -220,12 +224,13 @@ void testEmptyHrefOrType() { )"; + std::vector entries; OpdsParser parser; + parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); - const auto& entries = parser.getEntries(); ASSERT_SIZE(entries.size(), 0); PASS(); } @@ -243,12 +248,13 @@ void testDuplicateAcquisitionLinks() { )"; + std::vector entries; OpdsParser parser; + parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); - const auto& entries = parser.getEntries(); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 2); @@ -272,12 +278,13 @@ void testIdenticalHrefAcquisitionLinksAreDeduplicated() { )"; + std::vector entries; OpdsParser parser; + parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); - const auto& entries = parser.getEntries(); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 1); @@ -299,12 +306,13 @@ void testSlashVariantHrefAcquisitionLinksAreDeduplicated() { )"; + std::vector entries; OpdsParser parser; + parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); - const auto& entries = parser.getEntries(); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 1);