Merge branch 'jpirnay:master' into master
This commit is contained in:
@@ -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..."
|
||||||
|
|||||||
@@ -232,6 +232,9 @@ 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_SEARCH: "Search"
|
||||||
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"
|
||||||
|
|||||||
@@ -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: "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..."
|
||||||
|
|||||||
@@ -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: "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: "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);
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,14 @@
|
|||||||
#include <OpdsStream.h>
|
#include <OpdsStream.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#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"
|
||||||
@@ -19,7 +24,7 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int PAGE_ITEMS = 23;
|
constexpr int PAGE_ITEMS = 23;
|
||||||
} // namespace
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::onEnter() {
|
void OpdsBookBrowserActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
@@ -28,12 +33,14 @@ void OpdsBookBrowserActivity::onEnter() {
|
|||||||
entries.clear();
|
entries.clear();
|
||||||
navigationHistory.clear();
|
navigationHistory.clear();
|
||||||
currentPath = ""; // Root path - user provides full URL in settings
|
currentPath = ""; // Root path - user provides full URL in settings
|
||||||
|
searchTemplate.clear();
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,26 +54,27 @@ void OpdsBookBrowserActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +83,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();
|
||||||
@@ -187,11 +176,10 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
|||||||
|
|
||||||
// Browsing state
|
// Browsing state
|
||||||
// Show appropriate button hint based on selected entry type
|
// Show appropriate button hint based on selected entry type
|
||||||
const char* confirmLabel = tr(STR_OPEN);
|
const char* confirmLabel =
|
||||||
if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) {
|
(!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN);
|
||||||
confirmLabel = tr(STR_DOWNLOAD);
|
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));
|
||||||
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()) {
|
||||||
@@ -227,15 +215,14 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.rfind("http", 0) == 0) ? path : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, path);
|
||||||
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
|
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
|
||||||
|
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
@@ -257,50 +244,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,15 +295,10 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
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.rfind("http", 0) == 0) ? book.href : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
|
||||||
|
std::string filename =
|
||||||
// Create sanitized filename: "Author - Title.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 + " - " + baseName;
|
|
||||||
}
|
|
||||||
std::string filename = "/" + StringUtils::sanitizeFilename(baseName) + ".epub";
|
|
||||||
|
|
||||||
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
|
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
|
||||||
|
|
||||||
@@ -328,7 +306,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
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) {
|
||||||
@@ -348,11 +326,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
|
|
||||||
LOG_DBG("OPDS", "Download complete: %s", filename.c_str());
|
LOG_DBG("OPDS", "Download complete: %s", filename.c_str());
|
||||||
|
|
||||||
// Invalidate any existing cache for this file to prevent stale metadata issues
|
Epub(filename, "/.crosspoint").clearCache();
|
||||||
Epub epub(filename, "/.crosspoint");
|
|
||||||
epub.clearCache();
|
|
||||||
LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str());
|
|
||||||
|
|
||||||
state = BrowserState::BROWSING;
|
state = BrowserState::BROWSING;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
@@ -362,6 +336,58 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
currentPath = url;
|
||||||
|
|
||||||
|
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
|
// 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)) {
|
||||||
@@ -371,12 +397,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();
|
||||||
|
|
||||||
@@ -386,15 +411,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