feat: add OPDS search support & next/prev page navigation (#1462)
## Summary **What is the goal of this PR?** Adds OPDS search support, allowing users to search a catalog directly from the book browser when the server exposes an OpenSearch template. **What changes are included?** - `OpdsParser`: parses the OpenSearch template URL from feed-level `<link rel="search">` elements and exposes it via `getSearchTemplate()` - `OpdsBookBrowserActivity`: fetches and stores the search template after each feed load; shows a Search hint on the Left button when a template is available; launches the existing `KeyboardEntryActivity` for query input; URL-encodes the query and fetches the result feed - Absolute search result URLs are handled correctly in `fetchFeed` (skips prepending the server base URL) - A `consumeConfirm` guard prevents the Confirm release that submits the keyboard from immediately triggering a book download on the first browsing frame after search results load ## Additional Context - Search is silently unavailable if the server does not advertise an OpenSearch template — no UI change in that case - Tested against a Calibre-Web OPDS endpoint which exposes `<link rel="search" type="application/opensearchdescription+xml">` - The inline URL encoder in `performSearch` was necessary as `StringUtils` has no such utility; worth considering extracting to `StringUtils` in a follow-up - No new dependencies introduced --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --------- Co-authored-by: kira <rammah@tuta.io> Co-authored-by: Justin Mitchell <justin@jmitch.com>
This commit is contained in:
co-authored by
kira
Justin Mitchell
parent
5c12f2f01e
commit
fa2a3d2539
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Адкрыйце гэты адрас у браўзеры"
|
|||||||
STR_OR_HTTP_PREFIX: "або http://"
|
STR_OR_HTTP_PREFIX: "або http://"
|
||||||
STR_SCAN_QR_HINT: "або адсканіруйце QR-код:"
|
STR_SCAN_QR_HINT: "або адсканіруйце QR-код:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi"
|
STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi"
|
||||||
STR_CALIBRE_WEB_URL: "Вэб-адрас Calibre"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана"
|
STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана"
|
||||||
STR_MAC_ADDRESS: "MAC-адрас:"
|
STR_MAC_ADDRESS: "MAC-адрас:"
|
||||||
STR_CHECKING_WIFI: "Праверка Wi-Fi..."
|
STR_CHECKING_WIFI: "Праверка Wi-Fi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Obriu aquest URL al navegador"
|
|||||||
STR_OR_HTTP_PREFIX: "o http://"
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
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_NETWORK_LEGEND: "* = Encriptat | + = Desat"
|
||||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||||
STR_CHECKING_WIFI: "S'està comprovant el WiFi..."
|
STR_CHECKING_WIFI: "S'està comprovant el WiFi..."
|
||||||
|
|||||||
@@ -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_OR_HTTP_PREFIX: "nebo http://"
|
||||||
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
|
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
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_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
|
||||||
STR_MAC_ADDRESS: "MAC adresa:"
|
STR_MAC_ADDRESS: "MAC adresa:"
|
||||||
STR_CHECKING_WIFI: "Kontrola WiFi..."
|
STR_CHECKING_WIFI: "Kontrola WiFi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Åbn denne URL i din browser"
|
|||||||
STR_OR_HTTP_PREFIX: "eller http://"
|
STR_OR_HTTP_PREFIX: "eller http://"
|
||||||
STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:"
|
STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt"
|
STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt"
|
||||||
STR_MAC_ADDRESS: "MAC-adresse:"
|
STR_MAC_ADDRESS: "MAC-adresse:"
|
||||||
STR_CHECKING_WIFI: "Tjekker WiFi..."
|
STR_CHECKING_WIFI: "Tjekker WiFi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open deze URL in je browser"
|
|||||||
STR_OR_HTTP_PREFIX: "of http://"
|
STR_OR_HTTP_PREFIX: "of http://"
|
||||||
STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:"
|
STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen"
|
STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen"
|
||||||
STR_MAC_ADDRESS: "MAC-adres:"
|
STR_MAC_ADDRESS: "MAC-adres:"
|
||||||
STR_CHECKING_WIFI: "Wifi controleren..."
|
STR_CHECKING_WIFI: "Wifi controleren..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open this URL in your browser"
|
|||||||
STR_OR_HTTP_PREFIX: "or http://"
|
STR_OR_HTTP_PREFIX: "or http://"
|
||||||
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
|
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
||||||
STR_MAC_ADDRESS: "MAC address:"
|
STR_MAC_ADDRESS: "MAC address:"
|
||||||
STR_CHECKING_WIFI: "Checking WiFi..."
|
STR_CHECKING_WIFI: "Checking WiFi..."
|
||||||
@@ -175,6 +175,8 @@ STR_UNNAMED: "Unnamed"
|
|||||||
STR_NO_SERVER_URL: "No server URL configured"
|
STR_NO_SERVER_URL: "No server URL configured"
|
||||||
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||||
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||||
|
STR_NEXT_PAGE: "Next Page »"
|
||||||
|
STR_PREV_PAGE: "« Previous Page"
|
||||||
STR_NETWORK_PREFIX: "Network: "
|
STR_NETWORK_PREFIX: "Network: "
|
||||||
STR_IP_ADDRESS_PREFIX: "IP Address: "
|
STR_IP_ADDRESS_PREFIX: "IP Address: "
|
||||||
STR_ERROR_GENERAL_FAILURE: "Error: General failure"
|
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_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
|
||||||
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
||||||
STR_OPDS_BROWSER: "OPDS Browser"
|
STR_OPDS_BROWSER: "OPDS Browser"
|
||||||
|
STR_SEARCH: "Search"
|
||||||
STR_COVER_CUSTOM: "Cover + Custom"
|
STR_COVER_CUSTOM: "Cover + Custom"
|
||||||
STR_MENU_RECENT_BOOKS: "Recent Books"
|
STR_MENU_RECENT_BOOKS: "Recent Books"
|
||||||
STR_NO_RECENT_BOOKS: "No recent books"
|
STR_NO_RECENT_BOOKS: "No recent books"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Avaa tämä osoite selaimessasi"
|
|||||||
STR_OR_HTTP_PREFIX: "tai http://"
|
STR_OR_HTTP_PREFIX: "tai http://"
|
||||||
STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:"
|
STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre langaton"
|
STR_CALIBRE_WIRELESS: "Calibre langaton"
|
||||||
STR_CALIBRE_WEB_URL: "Calibre-verkko-osoite"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu"
|
STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu"
|
||||||
STR_MAC_ADDRESS: "MAC-osoite:"
|
STR_MAC_ADDRESS: "MAC-osoite:"
|
||||||
STR_CHECKING_WIFI: "Tarkistetaan WiFi..."
|
STR_CHECKING_WIFI: "Tarkistetaan WiFi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Ouvrez cette URL dans un navigateur"
|
|||||||
STR_OR_HTTP_PREFIX: "ou http://"
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
STR_SCAN_QR_HINT: "ou scannez le QR code :"
|
STR_SCAN_QR_HINT: "ou scannez le QR code :"
|
||||||
STR_CALIBRE_WIRELESS: "Connexion Calibre sans fil"
|
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_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
|
||||||
STR_MAC_ADDRESS: "Adresse MAC :"
|
STR_MAC_ADDRESS: "Adresse MAC :"
|
||||||
STR_CHECKING_WIFI: "Vérification du WiFi…"
|
STR_CHECKING_WIFI: "Vérification du WiFi…"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Diese URL im Browser öffnen"
|
|||||||
STR_OR_HTTP_PREFIX: "oder http://"
|
STR_OR_HTTP_PREFIX: "oder http://"
|
||||||
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
|
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
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_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert"
|
||||||
STR_MAC_ADDRESS: "MAC-Adresse:"
|
STR_MAC_ADDRESS: "MAC-Adresse:"
|
||||||
STR_CHECKING_WIFI: "WLAN prüfen…"
|
STR_CHECKING_WIFI: "WLAN prüfen…"
|
||||||
|
|||||||
@@ -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_OR_HTTP_PREFIX: "vagy http://"
|
||||||
STR_SCAN_QR_HINT: "vagy olvasd be a QR-kódot a telefonoddal:"
|
STR_SCAN_QR_HINT: "vagy olvasd be a QR-kódot a telefonoddal:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
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_NETWORK_LEGEND: "* = Titkosított | + = Mentett"
|
||||||
STR_MAC_ADDRESS: "MAC-cím:"
|
STR_MAC_ADDRESS: "MAC-cím:"
|
||||||
STR_CHECKING_WIFI: "WiFi ellenőrzése..."
|
STR_CHECKING_WIFI: "WiFi ellenőrzése..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Apri questo URL nel tuo browser"
|
|||||||
STR_OR_HTTP_PREFIX: "o http://"
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
STR_SCAN_QR_HINT: "o scansiona il codice QR con il tuo telefono:"
|
STR_SCAN_QR_HINT: "o scansiona il codice QR con il tuo telefono:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
STR_CALIBRE_WEB_URL: "URL Web Calibre"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Criptata | + = Salvata"
|
STR_NETWORK_LEGEND: "* = Criptata | + = Salvata"
|
||||||
STR_MAC_ADDRESS: "Indirizzo MAC:"
|
STR_MAC_ADDRESS: "Indirizzo MAC:"
|
||||||
STR_CHECKING_WIFI: "Controllo WiFi in corso..."
|
STR_CHECKING_WIFI: "Controllo WiFi in corso..."
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Браузерде осы URL мекенжайын ашың
|
|||||||
STR_OR_HTTP_PREFIX: "немесе http://"
|
STR_OR_HTTP_PREFIX: "немесе http://"
|
||||||
STR_SCAN_QR_HINT: "немесе телефонмен QR кодын сканерлеңіз:"
|
STR_SCAN_QR_HINT: "немесе телефонмен QR кодын сканерлеңіз:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre сымсыз"
|
STR_CALIBRE_WIRELESS: "Calibre сымсыз"
|
||||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Шифрланған | + = Сақталған"
|
STR_NETWORK_LEGEND: "* = Шифрланған | + = Сақталған"
|
||||||
STR_MAC_ADDRESS: "MAC мекенжайы:"
|
STR_MAC_ADDRESS: "MAC мекенжайы:"
|
||||||
STR_CHECKING_WIFI: "WiFi тексерілуде..."
|
STR_CHECKING_WIFI: "WiFi тексерілуде..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Atidarykite šį adresą naršyklėje"
|
|||||||
STR_OR_HTTP_PREFIX: "arba http://"
|
STR_OR_HTTP_PREFIX: "arba http://"
|
||||||
STR_SCAN_QR_HINT: "arba nuskaitykite QR kodą:"
|
STR_SCAN_QR_HINT: "arba nuskaitykite QR kodą:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre belaidis"
|
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_NETWORK_LEGEND: "* = Užšifruota | + = Išsaugota"
|
||||||
STR_MAC_ADDRESS: "MAC adresas:"
|
STR_MAC_ADDRESS: "MAC adresas:"
|
||||||
STR_CHECKING_WIFI: "Tikrinamas WiFi..."
|
STR_CHECKING_WIFI: "Tikrinamas WiFi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otwórz ten URL w przeglądarce"
|
|||||||
STR_OR_HTTP_PREFIX: "albo http://"
|
STR_OR_HTTP_PREFIX: "albo http://"
|
||||||
STR_SCAN_QR_HINT: "albo zeskanuj kod QR telefonem:"
|
STR_SCAN_QR_HINT: "albo zeskanuj kod QR telefonem:"
|
||||||
STR_CALIBRE_WIRELESS: "Bezprzewodowe połączenie z Calibre"
|
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_NETWORK_LEGEND: "* = Zaszyfrowane | + = Zapisane"
|
||||||
STR_MAC_ADDRESS: "Adres MAC:"
|
STR_MAC_ADDRESS: "Adres MAC:"
|
||||||
STR_CHECKING_WIFI: "Sprawdzanie WiFi..."
|
STR_CHECKING_WIFI: "Sprawdzanie WiFi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra este URL seu navegador"
|
|||||||
STR_OR_HTTP_PREFIX: "ou http://"
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sem fio"
|
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_NETWORK_LEGEND: "* = Criptografada | + = Salva"
|
||||||
STR_MAC_ADDRESS: "Endereço MAC:"
|
STR_MAC_ADDRESS: "Endereço MAC:"
|
||||||
STR_CHECKING_WIFI: "Verificando Wi‑Fi..."
|
STR_CHECKING_WIFI: "Verificando Wi‑Fi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Deschideţi acest URL în browserul dvs."
|
|||||||
STR_OR_HTTP_PREFIX: "sau http://"
|
STR_OR_HTTP_PREFIX: "sau http://"
|
||||||
STR_SCAN_QR_HINT: "sau scanaţi codul QR cu telefonul dvs.:"
|
STR_SCAN_QR_HINT: "sau scanaţi codul QR cu telefonul dvs.:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
STR_CALIBRE_WEB_URL: "Calibre URL"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Criptat | + = Salvat"
|
STR_NETWORK_LEGEND: "* = Criptat | + = Salvat"
|
||||||
STR_MAC_ADDRESS: "Adresă MAC:"
|
STR_MAC_ADDRESS: "Adresă MAC:"
|
||||||
STR_CHECKING_WIFI: "Verificare WiFi..."
|
STR_CHECKING_WIFI: "Verificare WiFi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Откройте этот адрес в браузере"
|
|||||||
STR_OR_HTTP_PREFIX: "или http://"
|
STR_OR_HTTP_PREFIX: "или http://"
|
||||||
STR_SCAN_QR_HINT: "или отсканируйте QR-код:"
|
STR_SCAN_QR_HINT: "или отсканируйте QR-код:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi"
|
STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi"
|
||||||
STR_CALIBRE_WEB_URL: "Web-адрес Calibre"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
|
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
|
||||||
STR_MAC_ADDRESS: "MAC-адрес:"
|
STR_MAC_ADDRESS: "MAC-адрес:"
|
||||||
STR_CHECKING_WIFI: "Проверка Wi-Fi..."
|
STR_CHECKING_WIFI: "Проверка Wi-Fi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra esta dirección en su navegador"
|
|||||||
STR_OR_HTTP_PREFIX: "o http://"
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
STR_SCAN_QR_HINT: "o escanee el código QR con su móvil:"
|
STR_SCAN_QR_HINT: "o escanee el código QR con su móvil:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre inalámbrico"
|
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_NETWORK_LEGEND: "* (Cifrado) | + (Guardado)"
|
||||||
STR_MAC_ADDRESS: "MAC Address:"
|
STR_MAC_ADDRESS: "MAC Address:"
|
||||||
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Öppna denna adress i din browser"
|
|||||||
STR_OR_HTTP_PREFIX: "eller http://"
|
STR_OR_HTTP_PREFIX: "eller http://"
|
||||||
STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:"
|
STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Trådlöst"
|
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_NETWORK_LEGEND: "* = Krypterad | + = Sparad"
|
||||||
STR_MAC_ADDRESS: "MAC-adress:"
|
STR_MAC_ADDRESS: "MAC-adress:"
|
||||||
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
|
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Tarayıcınızda bu adresi açın"
|
|||||||
STR_OR_HTTP_PREFIX: "veya http://"
|
STR_OR_HTTP_PREFIX: "veya http://"
|
||||||
STR_SCAN_QR_HINT: "veya telefonunuzla QR kodu tarayın:"
|
STR_SCAN_QR_HINT: "veya telefonunuzla QR kodu tarayın:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Kablosuz"
|
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_NETWORK_LEGEND: "* = Şifreli | + = Kayıtlı"
|
||||||
STR_MAC_ADDRESS: "MAC adresi:"
|
STR_MAC_ADDRESS: "MAC adresi:"
|
||||||
STR_CHECKING_WIFI: "WiFi kontrol ediliyor..."
|
STR_CHECKING_WIFI: "WiFi kontrol ediliyor..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Відкрийте цю URL-адресу у вашому б
|
|||||||
STR_OR_HTTP_PREFIX: "або http://"
|
STR_OR_HTTP_PREFIX: "або http://"
|
||||||
STR_SCAN_QR_HINT: "або відскануйте QR-код телефоном:"
|
STR_SCAN_QR_HINT: "або відскануйте QR-код телефоном:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre бездротовий"
|
STR_CALIBRE_WIRELESS: "Calibre бездротовий"
|
||||||
STR_CALIBRE_WEB_URL: "URL Calibre Web"
|
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||||
STR_NETWORK_LEGEND: "* = Зашифровано | + = Збережено"
|
STR_NETWORK_LEGEND: "* = Зашифровано | + = Збережено"
|
||||||
STR_MAC_ADDRESS: "MAC адреса:"
|
STR_MAC_ADDRESS: "MAC адреса:"
|
||||||
STR_CHECKING_WIFI: "Перевірка WiFi..."
|
STR_CHECKING_WIFI: "Перевірка WiFi..."
|
||||||
|
|||||||
+53
-102
@@ -25,15 +25,12 @@ OpdsParser::~OpdsParser() {
|
|||||||
size_t OpdsParser::write(uint8_t c) { return write(&c, 1); }
|
size_t OpdsParser::write(uint8_t c) { return write(&c, 1); }
|
||||||
|
|
||||||
size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
||||||
if (errorOccured) {
|
if (errorOccured) return length;
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
XML_SetUserData(parser, this);
|
XML_SetUserData(parser, this);
|
||||||
XML_SetElementHandler(parser, startElement, endElement);
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
XML_SetCharacterDataHandler(parser, characterData);
|
XML_SetCharacterDataHandler(parser, characterData);
|
||||||
|
|
||||||
// Parse in chunks to avoid large buffer allocations
|
|
||||||
const char* currentPos = reinterpret_cast<const char*>(xmlData);
|
const char* currentPos = reinterpret_cast<const char*>(xmlData);
|
||||||
size_t remaining = length;
|
size_t remaining = length;
|
||||||
constexpr size_t chunkSize = 1024;
|
constexpr size_t chunkSize = 1024;
|
||||||
@@ -42,9 +39,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
|||||||
void* const buf = XML_GetBuffer(parser, chunkSize);
|
void* const buf = XML_GetBuffer(parser, chunkSize);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
|
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +48,9 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
|||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPos += toRead;
|
currentPos += toRead;
|
||||||
remaining -= toRead;
|
remaining -= toRead;
|
||||||
}
|
}
|
||||||
@@ -78,30 +69,25 @@ bool OpdsParser::error() const { return errorOccured; }
|
|||||||
|
|
||||||
void OpdsParser::clear() {
|
void OpdsParser::clear() {
|
||||||
entries.clear();
|
entries.clear();
|
||||||
|
searchTemplate.clear();
|
||||||
|
nextPageUrl.clear();
|
||||||
|
prevPageUrl.clear();
|
||||||
currentEntry = OpdsEntry{};
|
currentEntry = OpdsEntry{};
|
||||||
currentText.clear();
|
currentText.clear();
|
||||||
inEntry = false;
|
inEntry = inTitle = inAuthor = inAuthorName = inId = false;
|
||||||
inTitle = false;
|
|
||||||
inAuthor = false;
|
|
||||||
inAuthorName = false;
|
|
||||||
inId = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<OpdsEntry> OpdsParser::getBooks() const {
|
std::vector<OpdsEntry> OpdsParser::getBooks() const {
|
||||||
std::vector<OpdsEntry> books;
|
std::vector<OpdsEntry> books;
|
||||||
for (const auto& entry : entries) {
|
for (const auto& entry : entries) {
|
||||||
if (entry.type == OpdsEntryType::BOOK) {
|
if (entry.type == OpdsEntryType::BOOK) books.push_back(entry);
|
||||||
books.push_back(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return books;
|
return books;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
|
const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], name) == 0) {
|
if (strcmp(atts[i], name) == 0) return atts[i + 1];
|
||||||
return atts[i + 1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -109,7 +95,38 @@ 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) {
|
void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
auto* self = static_cast<OpdsParser*>(userData);
|
auto* self = static_cast<OpdsParser*>(userData);
|
||||||
|
|
||||||
// Check for entry element (with or without namespace prefix)
|
if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) {
|
||||||
|
const char* href = findAttribute(atts, "href");
|
||||||
|
if (href) {
|
||||||
|
const char* rel = findAttribute(atts, "rel");
|
||||||
|
const char* type = findAttribute(atts, "type");
|
||||||
|
|
||||||
|
if (rel && strcmp(rel, "search") == 0) {
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
||||||
|
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
||||||
|
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
||||||
|
self->currentEntry.href = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||||
self->inEntry = true;
|
self->inEntry = true;
|
||||||
self->currentEntry = OpdsEntry{};
|
self->currentEntry = OpdsEntry{};
|
||||||
@@ -118,112 +135,46 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
|||||||
|
|
||||||
if (!self->inEntry) return;
|
if (!self->inEntry) return;
|
||||||
|
|
||||||
// Check for title element
|
|
||||||
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
||||||
self->inTitle = true;
|
self->inTitle = true;
|
||||||
self->currentText.clear();
|
self->currentText.clear();
|
||||||
return;
|
} else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
|
||||||
}
|
|
||||||
|
|
||||||
// Check for author element
|
|
||||||
if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
|
|
||||||
self->inAuthor = true;
|
self->inAuthor = true;
|
||||||
return;
|
} else if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
|
||||||
}
|
|
||||||
|
|
||||||
// Check for author name element
|
|
||||||
if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
|
|
||||||
self->inAuthorName = true;
|
self->inAuthorName = true;
|
||||||
self->currentText.clear();
|
self->currentText.clear();
|
||||||
return;
|
} else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
|
||||||
}
|
|
||||||
|
|
||||||
// Check for id element
|
|
||||||
if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
|
|
||||||
self->inId = true;
|
self->inId = true;
|
||||||
self->currentText.clear();
|
self->currentText.clear();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for link element
|
|
||||||
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) {
|
|
||||||
// Check for acquisition link with epub type (this is a downloadable book)
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
// Check for navigation link (subsection or no rel specified with atom+xml type)
|
|
||||||
else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
|
||||||
// Only set navigation link if we don't already have an epub link
|
|
||||||
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
|
||||||
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
|
||||||
self->currentEntry.href = href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||||||
auto* self = static_cast<OpdsParser*>(userData);
|
auto* self = static_cast<OpdsParser*>(userData);
|
||||||
|
|
||||||
// Check for entry end
|
|
||||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||||
// Only add entry if it has required fields (title and href)
|
|
||||||
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
||||||
self->entries.push_back(self->currentEntry);
|
self->entries.push_back(self->currentEntry);
|
||||||
}
|
}
|
||||||
self->inEntry = false;
|
self->inEntry = false;
|
||||||
self->currentEntry = OpdsEntry{};
|
} else if (self->inEntry) {
|
||||||
return;
|
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
||||||
}
|
if (self->inTitle) self->currentEntry.title = self->currentText;
|
||||||
|
self->inTitle = false;
|
||||||
if (!self->inEntry) return;
|
} else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
|
||||||
|
self->inAuthor = false;
|
||||||
// Check for title end
|
} else if (self->inAuthorName && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
|
||||||
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
|
||||||
if (self->inTitle) {
|
|
||||||
self->currentEntry.title = self->currentText;
|
|
||||||
}
|
|
||||||
self->inTitle = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for author end
|
|
||||||
if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
|
|
||||||
self->inAuthor = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for author name end
|
|
||||||
if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
|
|
||||||
if (self->inAuthorName) {
|
|
||||||
self->currentEntry.author = self->currentText;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for id end
|
|
||||||
if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
|
|
||||||
if (self->inId) {
|
|
||||||
self->currentEntry.id = self->currentText;
|
|
||||||
}
|
|
||||||
self->inId = false;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) {
|
void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||||
auto* self = static_cast<OpdsParser*>(userData);
|
auto* self = static_cast<OpdsParser*>(userData);
|
||||||
|
|
||||||
// Only accumulate text when in a text element
|
|
||||||
if (self->inTitle || self->inAuthorName || self->inId) {
|
if (self->inTitle || self->inAuthorName || self->inId) {
|
||||||
self->currentText.append(s, len);
|
self->currentText.append(s, len);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ class OpdsParser final : public Print {
|
|||||||
~OpdsParser();
|
~OpdsParser();
|
||||||
|
|
||||||
// Disable copy
|
// 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(const OpdsParser&) = delete;
|
||||||
OpdsParser& operator=(const OpdsParser&) = delete;
|
OpdsParser& operator=(const OpdsParser&) = delete;
|
||||||
|
|
||||||
@@ -85,6 +88,9 @@ class OpdsParser final : public Print {
|
|||||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
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
|
// Helper to find attribute value
|
||||||
static const char* findAttribute(const XML_Char** atts, const char* name);
|
static const char* findAttribute(const XML_Char** atts, const char* name);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
|
#include "activities/util/KeyboardEntryActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
#include "network/HttpDownloader.h"
|
#include "network/HttpDownloader.h"
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int PAGE_ITEMS = 23;
|
constexpr int PAGE_ITEMS = 23;
|
||||||
} // namespace
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::onEnter() {
|
void OpdsBookBrowserActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
@@ -26,47 +27,47 @@ void OpdsBookBrowserActivity::onEnter() {
|
|||||||
state = BrowserState::CHECK_WIFI;
|
state = BrowserState::CHECK_WIFI;
|
||||||
entries.clear();
|
entries.clear();
|
||||||
navigationHistory.clear();
|
navigationHistory.clear();
|
||||||
currentPath = ""; // Root path - user provides full URL in settings
|
searchTemplate = "";
|
||||||
|
currentPath = "";
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
|
consumeConfirm = false;
|
||||||
|
consumeBack = false;
|
||||||
errorMessage.clear();
|
errorMessage.clear();
|
||||||
statusMessage = tr(STR_CHECKING_WIFI);
|
statusMessage = tr(STR_CHECKING_WIFI);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|
||||||
// Check WiFi and connect if needed, then fetch feed
|
|
||||||
checkAndConnectWifi();
|
checkAndConnectWifi();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::onExit() {
|
void OpdsBookBrowserActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
// Turn off WiFi when exiting
|
|
||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
|
|
||||||
entries.clear();
|
entries.clear();
|
||||||
navigationHistory.clear();
|
navigationHistory.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::loop() {
|
void OpdsBookBrowserActivity::loop() {
|
||||||
// Handle WiFi selection subactivity
|
if (state == BrowserState::WIFI_SELECTION || state == BrowserState::SEARCH_INPUT) {
|
||||||
if (state == BrowserState::WIFI_SELECTION) {
|
return;
|
||||||
// Should already handled by the WifiSelectionActivity
|
}
|
||||||
|
|
||||||
|
if (consumeConfirm && mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
consumeConfirm = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (consumeBack && mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
consumeBack = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle error state - Confirm retries, Back goes back or home
|
|
||||||
if (state == BrowserState::ERROR) {
|
if (state == BrowserState::ERROR) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Check if WiFi is still connected
|
|
||||||
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
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;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = tr(STR_LOADING);
|
statusMessage = tr(STR_LOADING);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
} else {
|
} else {
|
||||||
// WiFi not connected - launch WiFi selection
|
|
||||||
LOG_DBG("OPDS", "Retry: WiFi not connected, launching selection");
|
|
||||||
launchWifiSelection();
|
launchWifiSelection();
|
||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
@@ -75,59 +76,40 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle WiFi check state - only Back works
|
if (state == BrowserState::CHECK_WIFI || state == BrowserState::LOADING) {
|
||||||
if (state == BrowserState::CHECK_WIFI) {
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoHome();
|
state == BrowserState::CHECK_WIFI ? onGoHome() : navigateBack();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle loading state - only Back works
|
if (state == BrowserState::DOWNLOADING) return;
|
||||||
if (state == BrowserState::LOADING) {
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
||||||
navigateBack();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle downloading state - no input allowed
|
|
||||||
if (state == BrowserState::DOWNLOADING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle browsing state
|
|
||||||
if (state == BrowserState::BROWSING) {
|
if (state == BrowserState::BROWSING) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!entries.empty()) {
|
if (!entries.empty()) {
|
||||||
const auto& entry = entries[selectorIndex];
|
const auto& entry = entries[selectorIndex];
|
||||||
if (entry.type == OpdsEntryType::BOOK) {
|
entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry);
|
||||||
downloadBook(entry);
|
|
||||||
} else {
|
|
||||||
navigateToEntry(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
navigateBack();
|
navigateBack();
|
||||||
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||||
|
if (!searchTemplate.empty() && selectorIndex == 0) launchSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
if (!entries.empty()) {
|
if (!entries.empty()) {
|
||||||
buttonNavigator.onNextRelease([this] {
|
buttonNavigator.onNextRelease([this] {
|
||||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonNavigator.onPreviousRelease([this] {
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonNavigator.onNextContinuous([this] {
|
buttonNavigator.onNextContinuous([this] {
|
||||||
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonNavigator.onPreviousContinuous([this] {
|
buttonNavigator.onPreviousContinuous([this] {
|
||||||
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -138,21 +120,12 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
|
|
||||||
void OpdsBookBrowserActivity::render(RenderLock&&) {
|
void OpdsBookBrowserActivity::render(RenderLock&&) {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
if (state == BrowserState::CHECK_WIFI) {
|
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);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == BrowserState::LOADING) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
|
||||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
@@ -171,76 +144,50 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
|||||||
|
|
||||||
if (state == BrowserState::DOWNLOADING) {
|
if (state == BrowserState::DOWNLOADING) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING));
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING));
|
||||||
const auto maxWidth = pageWidth - 40;
|
auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), pageWidth - 40);
|
||||||
// Trim long titles to keep them within the screen bounds.
|
|
||||||
auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), maxWidth);
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, title.c_str());
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, title.c_str());
|
||||||
if (downloadTotal > 0) {
|
if (downloadTotal > 0) {
|
||||||
const int barWidth = pageWidth - 100;
|
GUI.drawProgressBar(renderer, Rect{50, pageHeight / 2 + 20, pageWidth - 100, 20}, downloadProgress,
|
||||||
constexpr int barHeight = 20;
|
downloadTotal);
|
||||||
constexpr int barX = 50;
|
|
||||||
const int barY = pageHeight / 2 + 20;
|
|
||||||
GUI.drawProgressBar(renderer, Rect{barX, barY, barWidth, barHeight}, downloadProgress, downloadTotal);
|
|
||||||
}
|
}
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browsing state
|
const char* confirmLabel =
|
||||||
// Show appropriate button hint based on selected entry type
|
(!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN);
|
||||||
const char* confirmLabel = tr(STR_OPEN);
|
const char* searchLabel = (!searchTemplate.empty() && selectorIndex == 0) ? tr(STR_SEARCH) : tr(STR_DIR_UP);
|
||||||
if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) {
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, searchLabel, tr(STR_DIR_DOWN));
|
||||||
confirmLabel = tr(STR_DOWNLOAD);
|
|
||||||
}
|
|
||||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
if (entries.empty()) {
|
if (entries.empty()) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES));
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES));
|
||||||
renderer.displayBuffer();
|
} else {
|
||||||
return;
|
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;
|
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
|
||||||
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
|
const auto& entry = entries[i];
|
||||||
|
std::string displayText = (entry.type == OpdsEntryType::NAVIGATION) ? "> " + entry.title : entry.title;
|
||||||
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
|
if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) displayText += " - " + entry.author;
|
||||||
const auto& entry = entries[i];
|
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(),
|
||||||
// Format display text with type indicator
|
i != static_cast<size_t>(selectorIndex));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<size_t>(selectorIndex));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
||||||
const char* serverUrl = SETTINGS.opdsServerUrl;
|
if (strlen(SETTINGS.opdsServerUrl) == 0) {
|
||||||
if (strlen(serverUrl) == 0) {
|
|
||||||
state = BrowserState::ERROR;
|
state = BrowserState::ERROR;
|
||||||
errorMessage = tr(STR_NO_SERVER_URL);
|
errorMessage = tr(STR_NO_SERVER_URL);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = UrlUtils::buildUrl(serverUrl, path);
|
std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, path);
|
||||||
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
|
|
||||||
|
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
|
||||||
{
|
{
|
||||||
OpdsParserStream stream{parser};
|
OpdsParserStream stream{parser};
|
||||||
if (!HttpDownloader::fetchUrl(url, stream)) {
|
if (!HttpDownloader::fetchUrl(url, stream)) {
|
||||||
@@ -258,50 +205,46 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchTemplate = parser.getSearchTemplate();
|
||||||
|
const auto& nextUrl = parser.getNextPageUrl();
|
||||||
|
const auto& prevUrl = parser.getPrevPageUrl();
|
||||||
entries = std::move(parser).getEntries();
|
entries = std::move(parser).getEntries();
|
||||||
LOG_DBG("OPDS", "Found %d entries", entries.size());
|
|
||||||
selectorIndex = 0;
|
|
||||||
|
|
||||||
if (entries.empty()) {
|
if (!prevUrl.empty()) {
|
||||||
state = BrowserState::ERROR;
|
entries.insert(entries.begin(), OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_PREV_PAGE), "", prevUrl, ""});
|
||||||
errorMessage = tr(STR_NO_ENTRIES);
|
}
|
||||||
requestUpdate();
|
if (!nextUrl.empty()) {
|
||||||
return;
|
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();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
|
void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
|
||||||
// Push current path to history before navigating
|
|
||||||
navigationHistory.push_back(currentPath);
|
navigationHistory.push_back(currentPath);
|
||||||
currentPath = entry.href;
|
currentPath = entry.href;
|
||||||
|
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = tr(STR_LOADING);
|
statusMessage = tr(STR_LOADING);
|
||||||
entries.clear();
|
entries.clear();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
requestUpdate(true); // Force update to show loading state immediately before fetch
|
requestUpdate(true);
|
||||||
|
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::navigateBack() {
|
void OpdsBookBrowserActivity::navigateBack() {
|
||||||
if (navigationHistory.empty()) {
|
if (navigationHistory.empty()) {
|
||||||
// At root, go home
|
|
||||||
onGoHome();
|
onGoHome();
|
||||||
} else {
|
} else {
|
||||||
// Go back to previous catalog
|
|
||||||
currentPath = navigationHistory.back();
|
currentPath = navigationHistory.back();
|
||||||
navigationHistory.pop_back();
|
navigationHistory.pop_back();
|
||||||
|
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = tr(STR_LOADING);
|
statusMessage = tr(STR_LOADING);
|
||||||
entries.clear();
|
entries.clear();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,48 +252,84 @@ void OpdsBookBrowserActivity::navigateBack() {
|
|||||||
void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||||
state = BrowserState::DOWNLOADING;
|
state = BrowserState::DOWNLOADING;
|
||||||
statusMessage = book.title;
|
statusMessage = book.title;
|
||||||
downloadProgress = 0;
|
downloadProgress = downloadTotal = 0;
|
||||||
downloadTotal = 0;
|
|
||||||
requestUpdate(true);
|
requestUpdate(true);
|
||||||
|
|
||||||
// Build full download URL
|
std::string downloadUrl =
|
||||||
std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
|
(book.href.find("http") == 0) ? book.href : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
|
||||||
|
std::string filename =
|
||||||
// Create sanitized filename: "Title - Author.epub" or just "Title.epub" if no author
|
"/" + StringUtils::sanitizeFilename(book.title + (book.author.empty() ? "" : " - " + book.author)) + ".epub";
|
||||||
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());
|
|
||||||
|
|
||||||
const auto result =
|
const auto result =
|
||||||
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
|
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
|
||||||
downloadProgress = downloaded;
|
downloadProgress = downloaded;
|
||||||
downloadTotal = total;
|
downloadTotal = total;
|
||||||
requestUpdate(true); // Force update to refresh progress bar
|
requestUpdate(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result == HttpDownloader::OK) {
|
if (result == HttpDownloader::OK) {
|
||||||
LOG_DBG("OPDS", "Download complete: %s", filename.c_str());
|
Epub(filename, "/.crosspoint").clearCache();
|
||||||
|
|
||||||
// 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());
|
|
||||||
|
|
||||||
state = BrowserState::BROWSING;
|
state = BrowserState::BROWSING;
|
||||||
requestUpdate();
|
|
||||||
} else {
|
} else {
|
||||||
state = BrowserState::ERROR;
|
state = BrowserState::ERROR;
|
||||||
errorMessage = tr(STR_DOWNLOAD_FAILED);
|
errorMessage = tr(STR_DOWNLOAD_FAILED);
|
||||||
requestUpdate();
|
|
||||||
}
|
}
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpdsBookBrowserActivity::launchSearch() {
|
||||||
|
consumeConfirm = true;
|
||||||
|
state = BrowserState::SEARCH_INPUT;
|
||||||
|
requestUpdate();
|
||||||
|
|
||||||
|
auto keyboard = std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SEARCH));
|
||||||
|
startActivityForResult(std::move(keyboard), [this](const ActivityResult& result) {
|
||||||
|
state = BrowserState::BROWSING;
|
||||||
|
if (!result.isCancelled) {
|
||||||
|
performSearch(std::get<KeyboardResult>(result.data).text);
|
||||||
|
} else {
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpdsBookBrowserActivity::performSearch(const std::string& query) {
|
||||||
|
if (query.empty() || searchTemplate.empty()) {
|
||||||
|
state = BrowserState::BROWSING;
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == '~')
|
||||||
|
out += static_cast<char>(c);
|
||||||
|
else {
|
||||||
|
char buf[4];
|
||||||
|
snprintf(buf, sizeof(buf), "%%%02X", c);
|
||||||
|
out += buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
navigationHistory.push_back(currentPath); // <-- add this
|
||||||
|
currentPath = url; // <-- add this
|
||||||
|
|
||||||
|
state = BrowserState::LOADING;
|
||||||
|
statusMessage = tr(STR_LOADING);
|
||||||
|
requestUpdate(true);
|
||||||
|
fetchFeed(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
||||||
// Already connected? Verify connection is valid by checking IP
|
|
||||||
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = tr(STR_LOADING);
|
statusMessage = tr(STR_LOADING);
|
||||||
@@ -358,12 +337,11 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
|||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not connected - launch WiFi selection screen directly
|
|
||||||
launchWifiSelection();
|
launchWifiSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::launchWifiSelection() {
|
void OpdsBookBrowserActivity::launchWifiSelection() {
|
||||||
|
consumeBack = consumeConfirm = true;
|
||||||
state = BrowserState::WIFI_SELECTION;
|
state = BrowserState::WIFI_SELECTION;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|
||||||
@@ -373,15 +351,11 @@ void OpdsBookBrowserActivity::launchWifiSelection() {
|
|||||||
|
|
||||||
void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
|
void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
LOG_DBG("OPDS", "WiFi connected via selection, fetching feed");
|
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = tr(STR_LOADING);
|
statusMessage = tr(STR_LOADING);
|
||||||
requestUpdate(true); // Force update to show loading state immediately before fetch
|
requestUpdate(true);
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
} else {
|
} 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.disconnect();
|
||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
state = BrowserState::ERROR;
|
state = BrowserState::ERROR;
|
||||||
|
|||||||
@@ -11,21 +11,13 @@
|
|||||||
/**
|
/**
|
||||||
* Activity for browsing and downloading books from an OPDS server.
|
* Activity for browsing and downloading books from an OPDS server.
|
||||||
* Supports navigation through catalog hierarchy and downloading EPUBs.
|
* Supports navigation through catalog hierarchy and downloading EPUBs.
|
||||||
* When WiFi connection fails, launches WiFi selection to let user connect.
|
|
||||||
*/
|
*/
|
||||||
class OpdsBookBrowserActivity final : public Activity {
|
class OpdsBookBrowserActivity final : public Activity {
|
||||||
public:
|
public:
|
||||||
enum class BrowserState {
|
enum class BrowserState { CHECK_WIFI, WIFI_SELECTION, LOADING, BROWSING, DOWNLOADING, ERROR, SEARCH_INPUT };
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
: Activity("OpdsBookBrowser", renderer, mappedInput) {}
|
: Activity("OpdsBookBrowser", renderer, mappedInput), buttonNavigator() {}
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
@@ -36,8 +28,11 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
BrowserState state = BrowserState::LOADING;
|
BrowserState state = BrowserState::LOADING;
|
||||||
std::vector<OpdsEntry> entries;
|
std::vector<OpdsEntry> entries;
|
||||||
std::vector<std::string> navigationHistory; // Stack of previous feed paths for back navigation
|
std::vector<std::string> navigationHistory;
|
||||||
std::string currentPath; // Current feed path being displayed
|
std::string currentPath;
|
||||||
|
std::string searchTemplate;
|
||||||
|
bool consumeConfirm = false;
|
||||||
|
bool consumeBack = false; // Added missing member
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
std::string errorMessage;
|
std::string errorMessage;
|
||||||
std::string statusMessage;
|
std::string statusMessage;
|
||||||
@@ -51,5 +46,7 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
void navigateToEntry(const OpdsEntry& entry);
|
void navigateToEntry(const OpdsEntry& entry);
|
||||||
void navigateBack();
|
void navigateBack();
|
||||||
void downloadBook(const OpdsEntry& book);
|
void downloadBook(const OpdsEntry& book);
|
||||||
|
void launchSearch();
|
||||||
|
void performSearch(const std::string& query);
|
||||||
bool preventAutoSleep() override { return true; }
|
bool preventAutoSleep() override { return true; }
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user