From 0c2f64cf87f2a89c409ec1659e6e9aa5277a37d1 Mon Sep 17 00:00:00 2001 From: kira Date: Sun, 12 Apr 2026 12:18:18 -0400 Subject: [PATCH] feat: add OPDS search support & next/prev page navigation - Parse OpenSearch template URL from OPDS feed - Launch search via KeyboardEntryActivity when template available - URL-encode queries and fetch result feed - Handle absolute search result URLs in fetchFeed - Add next/prev page navigation to OPDS browser - consumeConfirm guard prevents auto-download after search Co-authored-by: Justin Mitchell --- lib/I18n/translations/belarusian.yaml | 2 +- lib/I18n/translations/catalan.yaml | 2 +- lib/I18n/translations/czech.yaml | 2 +- lib/I18n/translations/danish.yaml | 2 +- lib/I18n/translations/dutch.yaml | 2 +- lib/I18n/translations/english.yaml | 5 +- lib/I18n/translations/finnish.yaml | 2 +- lib/I18n/translations/french.yaml | 2 +- lib/I18n/translations/german.yaml | 2 +- lib/I18n/translations/hungarian.yaml | 2 +- lib/I18n/translations/italian.yaml | 2 +- lib/I18n/translations/kazakh.yaml | 2 +- lib/I18n/translations/lithuanian.yaml | 2 +- lib/I18n/translations/polish.yaml | 2 +- lib/I18n/translations/portuguese.yaml | 2 +- lib/I18n/translations/romanian.yaml | 2 +- lib/I18n/translations/russian.yaml | 2 +- lib/I18n/translations/spanish.yaml | 2 +- lib/I18n/translations/swedish.yaml | 2 +- lib/I18n/translations/turkish.yaml | 2 +- lib/I18n/translations/ukrainian.yaml | 2 +- lib/OpdsParser/OpdsParser.cpp | 110 +++------ lib/OpdsParser/OpdsParser.h | 4 + .../browser/OpdsBookBrowserActivity.cpp | 226 ++++++------------ .../browser/OpdsBookBrowserActivity.h | 22 +- 25 files changed, 136 insertions(+), 271 deletions(-) diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index e17f3bad2..cf1d2de74 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Адкрыйце гэты адрас у браўзеры" STR_OR_HTTP_PREFIX: "або http://" STR_SCAN_QR_HINT: "або адсканіруйце QR-код:" STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi" -STR_CALIBRE_WEB_URL: "Вэб-адрас Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана" STR_MAC_ADDRESS: "MAC-адрас:" STR_CHECKING_WIFI: "Праверка Wi-Fi..." diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index a585019f1..614821a21 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Obriu aquest URL al navegador" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:" STR_CALIBRE_WIRELESS: "Calibre sense fils" -STR_CALIBRE_WEB_URL: "URL web del Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Encriptat | + = Desat" STR_MAC_ADDRESS: "Adreça MAC:" STR_CHECKING_WIFI: "S'està comprovant el WiFi..." diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 2add9e737..06261a26c 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči" STR_OR_HTTP_PREFIX: "nebo http://" STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "URL webu Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo" STR_MAC_ADDRESS: "MAC adresa:" STR_CHECKING_WIFI: "Kontrola WiFi..." diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 3e4704c91..807d98312 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Åbn denne URL i din browser" STR_OR_HTTP_PREFIX: "eller http://" STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt" STR_MAC_ADDRESS: "MAC-adresse:" STR_CHECKING_WIFI: "Tjekker WiFi..." diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 73e44d61a..38b88d77c 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open deze URL in je browser" STR_OR_HTTP_PREFIX: "of http://" STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen" STR_MAC_ADDRESS: "MAC-adres:" STR_CHECKING_WIFI: "Wifi controleren..." diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index df91c1a1c..b741a85d0 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open this URL in your browser" STR_OR_HTTP_PREFIX: "or http://" STR_SCAN_QR_HINT: "or scan QR code with your phone:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Encrypted | + = Saved" STR_MAC_ADDRESS: "MAC address:" STR_CHECKING_WIFI: "Checking WiFi..." @@ -175,6 +175,8 @@ STR_UNNAMED: "Unnamed" STR_NO_SERVER_URL: "No server URL configured" STR_FETCH_FEED_FAILED: "Failed to fetch feed" STR_PARSE_FEED_FAILED: "Failed to parse feed" +STR_NEXT_PAGE: "Next Page »" +STR_PREV_PAGE: "« Previous Page" STR_NETWORK_PREFIX: "Network: " STR_IP_ADDRESS_PREFIX: "IP Address: " STR_ERROR_GENERAL_FAILURE: "Error: General failure" @@ -229,6 +231,7 @@ STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_OPDS_BROWSER: "OPDS Browser" +STR_SEARCH: "Search" STR_COVER_CUSTOM: "Cover + Custom" STR_MENU_RECENT_BOOKS: "Recent Books" STR_NO_RECENT_BOOKS: "No recent books" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 35e581dd5..e584d7a26 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Avaa tämä osoite selaimessasi" STR_OR_HTTP_PREFIX: "tai http://" STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:" STR_CALIBRE_WIRELESS: "Calibre langaton" -STR_CALIBRE_WEB_URL: "Calibre-verkko-osoite" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu" STR_MAC_ADDRESS: "MAC-osoite:" STR_CHECKING_WIFI: "Tarkistetaan WiFi..." diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index f91e46f42..b908fa84d 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Ouvrez cette URL dans un navigateur" STR_OR_HTTP_PREFIX: "ou http://" STR_SCAN_QR_HINT: "ou scannez le QR code :" STR_CALIBRE_WIRELESS: "Connexion Calibre sans fil" -STR_CALIBRE_WEB_URL: "URL Web Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé" STR_MAC_ADDRESS: "Adresse MAC :" STR_CHECKING_WIFI: "Vérification du WiFi…" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index b53ff4944..881a2b012 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Diese URL im Browser öffnen" STR_OR_HTTP_PREFIX: "oder http://" STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre-Web-URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert" STR_MAC_ADDRESS: "MAC-Adresse:" STR_CHECKING_WIFI: "WLAN prüfen…" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index e28524ce3..38c1bc12b 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Nyisd meg ezt az URL-t a böngésződben" STR_OR_HTTP_PREFIX: "vagy http://" STR_SCAN_QR_HINT: "vagy olvasd be a QR-kódot a telefonoddal:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett" STR_MAC_ADDRESS: "MAC-cím:" STR_CHECKING_WIFI: "WiFi ellenőrzése..." diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index b18bea1f2..f7679ccf6 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Apri questo URL nel tuo browser" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "o scansiona il codice QR con il tuo telefono:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "URL Web Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Criptata | + = Salvata" STR_MAC_ADDRESS: "Indirizzo MAC:" STR_CHECKING_WIFI: "Controllo WiFi in corso..." diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 4b30e9328..66c07712b 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Браузерде осы URL мекенжайын ашың STR_OR_HTTP_PREFIX: "немесе http://" STR_SCAN_QR_HINT: "немесе телефонмен QR кодын сканерлеңіз:" STR_CALIBRE_WIRELESS: "Calibre сымсыз" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Шифрланған | + = Сақталған" STR_MAC_ADDRESS: "MAC мекенжайы:" STR_CHECKING_WIFI: "WiFi тексерілуде..." diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 91eb7c6ec..67e424abc 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Atidarykite šį adresą naršyklėje" STR_OR_HTTP_PREFIX: "arba http://" STR_SCAN_QR_HINT: "arba nuskaitykite QR kodą:" STR_CALIBRE_WIRELESS: "Calibre belaidis" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Užšifruota | + = Išsaugota" STR_MAC_ADDRESS: "MAC adresas:" STR_CHECKING_WIFI: "Tikrinamas WiFi..." diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index c5d9fed1f..a4020fbb4 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otwórz ten URL w przeglądarce" STR_OR_HTTP_PREFIX: "albo http://" STR_SCAN_QR_HINT: "albo zeskanuj kod QR telefonem:" STR_CALIBRE_WIRELESS: "Bezprzewodowe połączenie z Calibre" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Zaszyfrowane | + = Zapisane" STR_MAC_ADDRESS: "Adres MAC:" STR_CHECKING_WIFI: "Sprawdzanie WiFi..." diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 962863ad9..989806493 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra este URL seu navegador" STR_OR_HTTP_PREFIX: "ou http://" STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:" STR_CALIBRE_WIRELESS: "Calibre sem fio" -STR_CALIBRE_WEB_URL: "URL do Calibre Web" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Criptografada | + = Salva" STR_MAC_ADDRESS: "Endereço MAC:" STR_CHECKING_WIFI: "Verificando Wi‑Fi..." diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 27ec6e4a1..b41caa60f 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Deschideţi acest URL în browserul dvs." STR_OR_HTTP_PREFIX: "sau http://" STR_SCAN_QR_HINT: "sau scanaţi codul QR cu telefonul dvs.:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Criptat | + = Salvat" STR_MAC_ADDRESS: "Adresă MAC:" STR_CHECKING_WIFI: "Verificare WiFi..." diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index b1354ab67..708a0b4ba 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Откройте этот адрес в браузере" STR_OR_HTTP_PREFIX: "или http://" STR_SCAN_QR_HINT: "или отсканируйте QR-код:" STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi" -STR_CALIBRE_WEB_URL: "Web-адрес Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена" STR_MAC_ADDRESS: "MAC-адрес:" STR_CHECKING_WIFI: "Проверка Wi-Fi..." diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index d4b2ad149..be4ab7a7e 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra esta dirección en su navegador" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "o escanee el código QR con su móvil:" STR_CALIBRE_WIRELESS: "Calibre inalámbrico" -STR_CALIBRE_WEB_URL: "URL del sitio web de Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* (Cifrado) | + (Guardado)" STR_MAC_ADDRESS: "MAC Address:" STR_CHECKING_WIFI: "Verificando Wi-Fi..." diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index c5e577e3a..34c013249 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Öppna denna adress i din browser" STR_OR_HTTP_PREFIX: "eller http://" STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:" STR_CALIBRE_WIRELESS: "Calibre Trådlöst" -STR_CALIBRE_WEB_URL: "Calibre webbadress" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad" STR_MAC_ADDRESS: "MAC-adress:" STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 0c6aa1a88..667c90494 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Tarayıcınızda bu adresi açın" STR_OR_HTTP_PREFIX: "veya http://" STR_SCAN_QR_HINT: "veya telefonunuzla QR kodu tarayın:" STR_CALIBRE_WIRELESS: "Calibre Kablosuz" -STR_CALIBRE_WEB_URL: "Calibre Web Adresi" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Şifreli | + = Kayıtlı" STR_MAC_ADDRESS: "MAC adresi:" STR_CHECKING_WIFI: "WiFi kontrol ediliyor..." diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index c6a03112a..d76cad9db 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Відкрийте цю URL-адресу у вашому б STR_OR_HTTP_PREFIX: "або http://" STR_SCAN_QR_HINT: "або відскануйте QR-код телефоном:" STR_CALIBRE_WIRELESS: "Calibre бездротовий" -STR_CALIBRE_WEB_URL: "URL Calibre Web" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Зашифровано | + = Збережено" STR_MAC_ADDRESS: "MAC адреса:" STR_CHECKING_WIFI: "Перевірка WiFi..." diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index db56bff84..84feef74f 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -25,9 +25,7 @@ OpdsParser::~OpdsParser() { size_t OpdsParser::write(uint8_t c) { return write(&c, 1); } size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { - if (errorOccured) { - return length; - } + if (errorOccured) return length; XML_SetUserData(parser, this); XML_SetElementHandler(parser, startElement, endElement); @@ -41,9 +39,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { void* const buf = XML_GetBuffer(parser, chunkSize); if (!buf) { errorOccured = true; - LOG_DBG("OPDS", "Couldn't allocate memory for buffer"); XML_ParserFree(parser); - parser = nullptr; return length; } @@ -52,13 +48,9 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { if (XML_ParseBuffer(parser, static_cast(toRead), 0) == XML_STATUS_ERROR) { errorOccured = true; - LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser), - XML_ErrorString(XML_GetErrorCode(parser))); XML_ParserFree(parser); - parser = nullptr; return length; } - currentPos += toRead; remaining -= toRead; } @@ -77,31 +69,25 @@ bool OpdsParser::error() const { return errorOccured; } void OpdsParser::clear() { entries.clear(); - searchTemplate.clear(); // Reset search template on clear + searchTemplate.clear(); + nextPageUrl.clear(); + prevPageUrl.clear(); currentEntry = OpdsEntry{}; currentText.clear(); - inEntry = false; - inTitle = false; - inAuthor = false; - inAuthorName = false; - inId = false; + 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); - } + 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]; - } + if (strcmp(atts[i], name) == 0) return atts[i + 1]; } return nullptr; } @@ -109,27 +95,29 @@ const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) { void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { auto* self = static_cast(userData); - // Handle feed-level or entry-level links if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) { - const char* rel = findAttribute(atts, "rel"); - const char* type = findAttribute(atts, "type"); const char* href = findAttribute(atts, "href"); - if (href) { - // 1. Search Template: Look for search relation + const char* rel = findAttribute(atts, "rel"); + const char* type = findAttribute(atts, "type"); + if (rel && strcmp(rel, "search") == 0) { - self->searchTemplate = href; + std::string sHref(href); + if (sHref.find("{searchTerms}") != std::string::npos) { + self->searchTemplate = sHref; + } + } else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) { + self->nextPageUrl = href; + } else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) { + self->prevPageUrl = href; } - + if (self->inEntry) { - // 2. Book: acquisition link with epub type if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr && strcmp(type, "application/epub+zip") == 0) { self->currentEntry.type = OpdsEntryType::BOOK; self->currentEntry.href = href; - } - // 3. Navigation: atom+xml type - else if (type && strstr(type, "application/atom+xml") != nullptr) { + } else if (type && strstr(type, "application/atom+xml") != nullptr) { if (self->currentEntry.type != OpdsEntryType::BOOK) { self->currentEntry.type = OpdsEntryType::NAVIGATION; self->currentEntry.href = href; @@ -150,24 +138,14 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { self->inTitle = true; self->currentText.clear(); - return; - } - - if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { + } else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { self->inAuthor = true; - return; - } - - if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { + } else if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { self->inAuthorName = true; self->currentText.clear(); - return; - } - - if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { + } else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { self->inId = true; self->currentText.clear(); - return; } } @@ -179,39 +157,19 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) { self->entries.push_back(self->currentEntry); } self->inEntry = false; - self->currentEntry = OpdsEntry{}; - return; - } - - if (!self->inEntry) return; - - if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { - if (self->inTitle) { - self->currentEntry.title = self->currentText; - } - self->inTitle = false; - return; - } - - if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { - self->inAuthor = false; - return; - } - - if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { - if (self->inAuthorName) { + } else if (self->inEntry) { + if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { + if (self->inTitle) self->currentEntry.title = self->currentText; + self->inTitle = false; + } else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { + self->inAuthor = false; + } else if (self->inAuthorName && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { self->currentEntry.author = self->currentText; + self->inAuthorName = false; + } else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { + if (self->inId) self->currentEntry.id = self->currentText; + self->inId = false; } - self->inAuthorName = false; - return; - } - - if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { - if (self->inId) { - self->currentEntry.id = self->currentText; - } - self->inId = false; - return; } } diff --git a/lib/OpdsParser/OpdsParser.h b/lib/OpdsParser/OpdsParser.h index 45b36796c..9c93b89ac 100644 --- a/lib/OpdsParser/OpdsParser.h +++ b/lib/OpdsParser/OpdsParser.h @@ -50,6 +50,8 @@ class OpdsParser final : public Print { // Disable copy const std::string& getSearchTemplate() const { return searchTemplate; } + const std::string& getNextPageUrl() const { return nextPageUrl; } + const std::string& getPrevPageUrl() const { return prevPageUrl; } OpdsParser(const OpdsParser&) = delete; OpdsParser& operator=(const OpdsParser&) = delete; @@ -87,6 +89,8 @@ class OpdsParser final : public Print { static void XMLCALL characterData(void* userData, const XML_Char* s, int len); std::string searchTemplate; + std::string nextPageUrl; + std::string prevPageUrl; // Helper to find attribute value static const char* findAttribute(const XML_Char** atts, const char* name); diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 0cc4c7e32..e4c6f9fd9 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -19,7 +19,7 @@ namespace { constexpr int PAGE_ITEMS = 23; -} // namespace +} void OpdsBookBrowserActivity::onEnter() { Activity::onEnter(); @@ -28,48 +28,46 @@ void OpdsBookBrowserActivity::onEnter() { entries.clear(); navigationHistory.clear(); searchTemplate = ""; - currentPath = ""; // Root path - user provides full URL in settings + currentPath = ""; selectorIndex = 0; consumeConfirm = false; + consumeBack = false; errorMessage.clear(); statusMessage = tr(STR_CHECKING_WIFI); requestUpdate(); - // Check WiFi and connect if needed, then fetch feed checkAndConnectWifi(); } void OpdsBookBrowserActivity::onExit() { Activity::onExit(); - - // Turn off WiFi when exiting WiFi.mode(WIFI_OFF); - entries.clear(); navigationHistory.clear(); } void OpdsBookBrowserActivity::loop() { - // Handle WiFi selection / search input subactivities if (state == BrowserState::WIFI_SELECTION || state == BrowserState::SEARCH_INPUT) { - // Already handled by the child activity return; } - // Handle error state - Confirm retries, Back goes back or home + if (consumeConfirm && mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + consumeConfirm = false; + return; + } + if (consumeBack && mappedInput.wasReleased(MappedInputManager::Button::Back)) { + consumeBack = false; + return; + } + if (state == BrowserState::ERROR) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - // Check if WiFi is still connected if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { - // WiFi connected - just retry fetching the feed - LOG_DBG("OPDS", "Retry: WiFi connected, retrying fetch"); state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); requestUpdate(); fetchFeed(currentPath); } else { - // WiFi not connected - launch WiFi selection - LOG_DBG("OPDS", "Retry: WiFi not connected, launching selection"); launchWifiSelection(); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { @@ -78,65 +76,40 @@ void OpdsBookBrowserActivity::loop() { return; } - // Handle WiFi check state - only Back works - if (state == BrowserState::CHECK_WIFI) { + if (state == BrowserState::CHECK_WIFI || state == BrowserState::LOADING) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onGoHome(); + state == BrowserState::CHECK_WIFI ? onGoHome() : navigateBack(); } return; } - // Handle loading state - only Back works - if (state == BrowserState::LOADING) { - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - navigateBack(); - } - return; - } + if (state == BrowserState::DOWNLOADING) return; - // Handle downloading state - no input allowed - if (state == BrowserState::DOWNLOADING) { - return; - } - - // Handle browsing state if (state == BrowserState::BROWSING) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (consumeConfirm) { - consumeConfirm = false; - } else if (!entries.empty()) { + if (!entries.empty()) { const auto& entry = entries[selectorIndex]; - if (entry.type == OpdsEntryType::BOOK) { - downloadBook(entry); - } else { - navigateToEntry(entry); - } + entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { navigateBack(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { - if (!searchTemplate.empty()) { - launchSearch(); - } + if (!searchTemplate.empty() && selectorIndex == 0) launchSearch(); } - // Handle navigation if (!entries.empty()) { buttonNavigator.onNextRelease([this] { selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size()); requestUpdate(); }); - buttonNavigator.onPreviousRelease([this] { selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); requestUpdate(); }); - buttonNavigator.onNextContinuous([this] { selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); requestUpdate(); }); - buttonNavigator.onPreviousContinuous([this] { selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); requestUpdate(); @@ -147,21 +120,12 @@ void OpdsBookBrowserActivity::loop() { void OpdsBookBrowserActivity::render(RenderLock&&) { renderer.clearScreen(); - const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD); - if (state == BrowserState::CHECK_WIFI) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; - } - - if (state == BrowserState::LOADING) { + if (state == BrowserState::CHECK_WIFI || state == BrowserState::LOADING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); @@ -180,77 +144,50 @@ void OpdsBookBrowserActivity::render(RenderLock&&) { if (state == BrowserState::DOWNLOADING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING)); - const auto maxWidth = pageWidth - 40; - // Trim long titles to keep them within the screen bounds. - auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), maxWidth); + auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), pageWidth - 40); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, title.c_str()); if (downloadTotal > 0) { - const int barWidth = pageWidth - 100; - constexpr int barHeight = 20; - constexpr int barX = 50; - const int barY = pageHeight / 2 + 20; - GUI.drawProgressBar(renderer, Rect{barX, barY, barWidth, barHeight}, downloadProgress, downloadTotal); + GUI.drawProgressBar(renderer, Rect{50, pageHeight / 2 + 20, pageWidth - 100, 20}, downloadProgress, + downloadTotal); } renderer.displayBuffer(); return; } - // Browsing state - // Show appropriate button hint based on selected entry type - const char* confirmLabel = tr(STR_OPEN); - if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) { - confirmLabel = tr(STR_DOWNLOAD); - } - const char* searchLabel = searchTemplate.empty() ? "" : tr(STR_CUSTOM); + const char* confirmLabel = + (!entries.empty() && entries[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()) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES)); - renderer.displayBuffer(); - return; - } + } else { + const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; + renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - - for (size_t i = pageStartIndex; i < entries.size() && i < static_cast(pageStartIndex + PAGE_ITEMS); i++) { - const auto& entry = entries[i]; - - // Format display text with type indicator - std::string displayText; - if (entry.type == OpdsEntryType::NAVIGATION) { - displayText = "> " + entry.title; // Folder/navigation indicator - } else { - // Book: "Title - Author" or just "Title" - displayText = entry.title; - if (!entry.author.empty()) { - displayText += " - " + entry.author; - } + for (size_t i = pageStartIndex; i < entries.size() && i < static_cast(pageStartIndex + PAGE_ITEMS); i++) { + const auto& entry = entries[i]; + std::string displayText = (entry.type == OpdsEntryType::NAVIGATION) ? "> " + entry.title : entry.title; + if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) displayText += " - " + entry.author; + auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), pageWidth - 40); + renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), + i != static_cast(selectorIndex)); } - - auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), - i != static_cast(selectorIndex)); } - renderer.displayBuffer(); } void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { - const char* serverUrl = SETTINGS.opdsServerUrl; - if (strlen(serverUrl) == 0) { + if (strlen(SETTINGS.opdsServerUrl) == 0) { state = BrowserState::ERROR; errorMessage = tr(STR_NO_SERVER_URL); requestUpdate(); return; } - std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(serverUrl, path); - LOG_DBG("OPDS", "Fetching: %s", url.c_str()); - + std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, path); OpdsParser parser; - { OpdsParserStream stream{parser}; if (!HttpDownloader::fetchUrl(url, stream)) { @@ -269,51 +206,45 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { } searchTemplate = parser.getSearchTemplate(); - + const auto& nextUrl = parser.getNextPageUrl(); + const auto& prevUrl = parser.getPrevPageUrl(); entries = std::move(parser).getEntries(); - LOG_DBG("OPDS", "Found %d entries", entries.size()); - selectorIndex = 0; - if (entries.empty()) { - state = BrowserState::ERROR; - errorMessage = tr(STR_NO_ENTRIES); - requestUpdate(); - return; + if (!prevUrl.empty()) { + entries.insert(entries.begin(), OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_PREV_PAGE), "", prevUrl, ""}); + } + if (!nextUrl.empty()) { + entries.push_back(OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_NEXT_PAGE), "", nextUrl, ""}); } - state = BrowserState::BROWSING; + selectorIndex = 0; + state = entries.empty() ? BrowserState::ERROR : BrowserState::BROWSING; + if (entries.empty()) errorMessage = tr(STR_NO_ENTRIES); requestUpdate(); } void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { - // Push current path to history before navigating navigationHistory.push_back(currentPath); currentPath = entry.href; - state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); entries.clear(); selectorIndex = 0; - requestUpdate(true); // Force update to show loading state immediately before fetch - + requestUpdate(true); fetchFeed(currentPath); } void OpdsBookBrowserActivity::navigateBack() { if (navigationHistory.empty()) { - // At root, go home onGoHome(); } else { - // Go back to previous catalog currentPath = navigationHistory.back(); navigationHistory.pop_back(); - state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); entries.clear(); selectorIndex = 0; requestUpdate(); - fetchFeed(currentPath); } } @@ -321,58 +252,42 @@ void OpdsBookBrowserActivity::navigateBack() { void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { state = BrowserState::DOWNLOADING; statusMessage = book.title; - downloadProgress = 0; - downloadTotal = 0; + downloadProgress = downloadTotal = 0; requestUpdate(true); - // Build full download URL - std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); - - // Create sanitized filename: "Title - Author.epub" or just "Title.epub" if no author - std::string baseName = book.title; - if (!book.author.empty()) { - baseName += " - " + book.author; - } - std::string filename = "/" + StringUtils::sanitizeFilename(baseName) + ".epub"; - - LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); + std::string downloadUrl = + (book.href.find("http") == 0) ? book.href : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); + std::string filename = + "/" + StringUtils::sanitizeFilename(book.title + (book.author.empty() ? "" : " - " + book.author)) + ".epub"; const auto result = HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) { downloadProgress = downloaded; downloadTotal = total; - requestUpdate(true); // Force update to refresh progress bar + requestUpdate(true); }); if (result == HttpDownloader::OK) { - LOG_DBG("OPDS", "Download complete: %s", filename.c_str()); - - // Invalidate any existing cache for this file to prevent stale metadata issues - Epub epub(filename, "/.crosspoint"); - epub.clearCache(); - LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str()); - + Epub(filename, "/.crosspoint").clearCache(); state = BrowserState::BROWSING; - requestUpdate(); } else { state = BrowserState::ERROR; errorMessage = tr(STR_DOWNLOAD_FAILED); - requestUpdate(); } + requestUpdate(); } void OpdsBookBrowserActivity::launchSearch() { + consumeConfirm = true; state = BrowserState::SEARCH_INPUT; requestUpdate(); - auto keyboard = std::make_unique(renderer, mappedInput, tr(STR_CUSTOM)); - + auto keyboard = std::make_unique(renderer, mappedInput, tr(STR_SEARCH)); startActivityForResult(std::move(keyboard), [this](const ActivityResult& result) { + state = BrowserState::BROWSING; if (!result.isCancelled) { - consumeConfirm = true; performSearch(std::get(result.data).text); } else { - state = BrowserState::BROWSING; requestUpdate(); } }); @@ -385,14 +300,13 @@ void OpdsBookBrowserActivity::performSearch(const std::string& query) { return; } - // StringUtils has no url_encode — encode inline auto urlEncode = [](const std::string& s) { std::string out; out.reserve(s.size() * 3); for (unsigned char c : s) { - if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') out += static_cast(c); - } else { + else { char buf[4]; snprintf(buf, sizeof(buf), "%%%02X", c); out += buf; @@ -404,11 +318,11 @@ void OpdsBookBrowserActivity::performSearch(const std::string& query) { std::string url = searchTemplate; const std::string placeholder = "{searchTerms}"; const size_t pos = url.find(placeholder); - if (pos != std::string::npos) { - url.replace(pos, placeholder.length(), urlEncode(query)); - } + if (pos != std::string::npos) url.replace(pos, placeholder.length(), urlEncode(query)); + + navigationHistory.push_back(currentPath); // <-- add this + currentPath = url; // <-- add this - LOG_DBG("OPDS", "Search URL: %s", url.c_str()); state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); requestUpdate(true); @@ -416,7 +330,6 @@ void OpdsBookBrowserActivity::performSearch(const std::string& query) { } void OpdsBookBrowserActivity::checkAndConnectWifi() { - // Already connected? Verify connection is valid by checking IP if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); @@ -424,12 +337,11 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() { fetchFeed(currentPath); return; } - - // Not connected - launch WiFi selection screen directly launchWifiSelection(); } void OpdsBookBrowserActivity::launchWifiSelection() { + consumeBack = consumeConfirm = true; state = BrowserState::WIFI_SELECTION; requestUpdate(); @@ -439,15 +351,11 @@ void OpdsBookBrowserActivity::launchWifiSelection() { void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { if (connected) { - LOG_DBG("OPDS", "WiFi connected via selection, fetching feed"); state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); - requestUpdate(true); // Force update to show loading state immediately before fetch + requestUpdate(true); fetchFeed(currentPath); } else { - LOG_DBG("OPDS", "WiFi selection cancelled/failed"); - // Force disconnect to ensure clean state for next retry - // This prevents stale connection status from interfering WiFi.disconnect(); WiFi.mode(WIFI_OFF); state = BrowserState::ERROR; diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index 7c5f760ca..8b55343a3 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -11,22 +11,13 @@ /** * Activity for browsing and downloading books from an OPDS server. * Supports navigation through catalog hierarchy and downloading EPUBs. - * When WiFi connection fails, launches WiFi selection to let user connect. */ class OpdsBookBrowserActivity final : public Activity { public: - enum class BrowserState { - CHECK_WIFI, // Checking WiFi connection - WIFI_SELECTION, // WiFi selection subactivity is active - LOADING, // Fetching OPDS feed - BROWSING, // Displaying entries (navigation or books) - DOWNLOADING, // Downloading selected EPUB - ERROR, // Error state with message - SEARCH_INPUT // Keyboard entry subactivity is active - }; + enum class BrowserState { CHECK_WIFI, WIFI_SELECTION, LOADING, BROWSING, DOWNLOADING, ERROR, SEARCH_INPUT }; explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("OpdsBookBrowser", renderer, mappedInput) {} + : Activity("OpdsBookBrowser", renderer, mappedInput), buttonNavigator() {} void onEnter() override; void onExit() override; @@ -37,10 +28,11 @@ class OpdsBookBrowserActivity final : public Activity { ButtonNavigator buttonNavigator; BrowserState state = BrowserState::LOADING; std::vector entries; - std::vector navigationHistory; // Stack of previous feed paths for back navigation - std::string currentPath; // Current feed path being displayed - std::string searchTemplate; // OpenSearch template URL, empty if server has no search - bool consumeConfirm = false; // Swallows the Confirm release that closed the keyboard + std::vector navigationHistory; + std::string currentPath; + std::string searchTemplate; + bool consumeConfirm = false; + bool consumeBack = false; // Added missing member int selectorIndex = 0; std::string errorMessage; std::string statusMessage;