mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 15:17:37 +03:00
lets you navigate and download books now
This commit is contained in:
parent
c99b93435f
commit
231393703c
@ -60,13 +60,13 @@ bool OpdsParser::parse(const char* xmlData, const size_t length) {
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
|
||||
Serial.printf("[%lu] [OPDS] Parsed %zu books\n", millis(), books.size());
|
||||
Serial.printf("[%lu] [OPDS] Parsed %zu entries\n", millis(), entries.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpdsParser::clear() {
|
||||
books.clear();
|
||||
currentBook = OpdsBook{};
|
||||
entries.clear();
|
||||
currentEntry = OpdsEntry{};
|
||||
currentText.clear();
|
||||
inEntry = false;
|
||||
inTitle = false;
|
||||
@ -75,6 +75,16 @@ void OpdsParser::clear() {
|
||||
inId = false;
|
||||
}
|
||||
|
||||
std::vector<OpdsEntry> OpdsParser::getBooks() const {
|
||||
std::vector<OpdsEntry> books;
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.type == OpdsEntryType::BOOK) {
|
||||
books.push_back(entry);
|
||||
}
|
||||
}
|
||||
return books;
|
||||
}
|
||||
|
||||
const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], name) == 0) {
|
||||
@ -90,7 +100,7 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
||||
// Check for entry element (with or without namespace prefix)
|
||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||
self->inEntry = true;
|
||||
self->currentBook = OpdsBook{};
|
||||
self->currentEntry = OpdsEntry{};
|
||||
return;
|
||||
}
|
||||
|
||||
@ -123,16 +133,26 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for link element with acquisition rel and epub type
|
||||
// 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");
|
||||
|
||||
// Look for acquisition link with epub type
|
||||
if (rel && type && href) {
|
||||
if (strstr(rel, "opds-spec.org/acquisition") != nullptr && strcmp(type, "application/epub+zip") == 0) {
|
||||
self->currentBook.epubUrl = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,12 +163,12 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||||
|
||||
// Check for entry end
|
||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||
// Only add book if it has required fields
|
||||
if (!self->currentBook.title.empty() && !self->currentBook.epubUrl.empty()) {
|
||||
self->books.push_back(self->currentBook);
|
||||
// Only add entry if it has required fields (title and href)
|
||||
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
||||
self->entries.push_back(self->currentEntry);
|
||||
}
|
||||
self->inEntry = false;
|
||||
self->currentBook = OpdsBook{};
|
||||
self->currentEntry = OpdsEntry{};
|
||||
return;
|
||||
}
|
||||
|
||||
@ -157,7 +177,7 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||||
// Check for title end
|
||||
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
||||
if (self->inTitle) {
|
||||
self->currentBook.title = self->currentText;
|
||||
self->currentEntry.title = self->currentText;
|
||||
}
|
||||
self->inTitle = false;
|
||||
return;
|
||||
@ -172,7 +192,7 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||||
// Check for author name end
|
||||
if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
|
||||
if (self->inAuthorName) {
|
||||
self->currentBook.author = self->currentText;
|
||||
self->currentEntry.author = self->currentText;
|
||||
}
|
||||
self->inAuthorName = false;
|
||||
return;
|
||||
@ -181,7 +201,7 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||||
// Check for id end
|
||||
if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
|
||||
if (self->inId) {
|
||||
self->currentBook.id = self->currentText;
|
||||
self->currentEntry.id = self->currentText;
|
||||
}
|
||||
self->inId = false;
|
||||
return;
|
||||
|
||||
@ -5,15 +5,27 @@
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Represents a book entry from an OPDS feed.
|
||||
* Type of OPDS entry.
|
||||
*/
|
||||
struct OpdsBook {
|
||||
enum class OpdsEntryType {
|
||||
NAVIGATION, // Link to another catalog
|
||||
BOOK // Downloadable book
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an entry from an OPDS feed (either a navigation link or a book).
|
||||
*/
|
||||
struct OpdsEntry {
|
||||
OpdsEntryType type = OpdsEntryType::NAVIGATION;
|
||||
std::string title;
|
||||
std::string author;
|
||||
std::string epubUrl; // Relative URL like "/books/get/epub/3/Calibre_Library"
|
||||
std::string author; // Only for books
|
||||
std::string href; // Navigation URL or epub download URL
|
||||
std::string id;
|
||||
};
|
||||
|
||||
// Legacy alias for backward compatibility
|
||||
using OpdsBook = OpdsEntry;
|
||||
|
||||
/**
|
||||
* Parser for OPDS (Open Publication Distribution System) Atom feeds.
|
||||
* Uses the Expat XML parser to parse OPDS catalog entries.
|
||||
@ -21,8 +33,12 @@ struct OpdsBook {
|
||||
* Usage:
|
||||
* OpdsParser parser;
|
||||
* if (parser.parse(xmlData, xmlLength)) {
|
||||
* for (const auto& book : parser.getBooks()) {
|
||||
* // Process book entries
|
||||
* for (const auto& entry : parser.getEntries()) {
|
||||
* if (entry.type == OpdsEntryType::BOOK) {
|
||||
* // Downloadable book
|
||||
* } else {
|
||||
* // Navigation link to another catalog
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@ -44,13 +60,19 @@ class OpdsParser {
|
||||
bool parse(const char* xmlData, size_t length);
|
||||
|
||||
/**
|
||||
* Get the parsed books.
|
||||
* @return Vector of OpdsBook entries
|
||||
* Get the parsed entries (both navigation and book entries).
|
||||
* @return Vector of OpdsEntry entries
|
||||
*/
|
||||
const std::vector<OpdsBook>& getBooks() const { return books; }
|
||||
const std::vector<OpdsEntry>& getEntries() const { return entries; }
|
||||
|
||||
/**
|
||||
* Clear all parsed books.
|
||||
* Get only book entries (legacy compatibility).
|
||||
* @return Vector of book entries
|
||||
*/
|
||||
std::vector<OpdsEntry> getBooks() const;
|
||||
|
||||
/**
|
||||
* Clear all parsed entries.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
@ -64,8 +86,8 @@ class OpdsParser {
|
||||
static const char* findAttribute(const XML_Char** atts, const char* name);
|
||||
|
||||
XML_Parser parser = nullptr;
|
||||
std::vector<OpdsBook> books;
|
||||
OpdsBook currentBook;
|
||||
std::vector<OpdsEntry> entries;
|
||||
OpdsEntry currentEntry;
|
||||
std::string currentText;
|
||||
|
||||
// Parser state
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
namespace {
|
||||
constexpr int PAGE_ITEMS = 23;
|
||||
constexpr int SKIP_PAGE_MS = 700;
|
||||
constexpr char OPDS_FEED_PATH[] = "/opds";
|
||||
constexpr char OPDS_ROOT_PATH[] = "opds"; // No leading slash - relative to server URL
|
||||
|
||||
// Prepend http:// if no protocol specified (server will redirect to https if needed)
|
||||
std::string ensureProtocol(const std::string& url) {
|
||||
@ -20,6 +20,39 @@ std::string ensureProtocol(const std::string& url) {
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Extract host with protocol from URL (e.g., "http://example.com" from "http://example.com/path")
|
||||
std::string extractHost(const std::string& url) {
|
||||
const size_t protocolEnd = url.find("://");
|
||||
if (protocolEnd == std::string::npos) {
|
||||
// No protocol, find first slash
|
||||
const size_t firstSlash = url.find('/');
|
||||
return firstSlash == std::string::npos ? url : url.substr(0, firstSlash);
|
||||
}
|
||||
// Find the first slash after the protocol
|
||||
const size_t hostStart = protocolEnd + 3;
|
||||
const size_t pathStart = url.find('/', hostStart);
|
||||
return pathStart == std::string::npos ? url : url.substr(0, pathStart);
|
||||
}
|
||||
|
||||
// Build full URL from server URL and path
|
||||
// If path starts with /, it's an absolute path from the host root
|
||||
// Otherwise, it's relative to the server URL
|
||||
std::string buildUrl(const std::string& serverUrl, const std::string& path) {
|
||||
const std::string urlWithProtocol = ensureProtocol(serverUrl);
|
||||
if (path.empty()) {
|
||||
return urlWithProtocol;
|
||||
}
|
||||
if (path[0] == '/') {
|
||||
// Absolute path - use just the host
|
||||
return extractHost(urlWithProtocol) + path;
|
||||
}
|
||||
// Relative path - append to server URL
|
||||
if (urlWithProtocol.back() == '/') {
|
||||
return urlWithProtocol + path;
|
||||
}
|
||||
return urlWithProtocol + "/" + path;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
||||
@ -32,7 +65,9 @@ void OpdsBookBrowserActivity::onEnter() {
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
state = BrowserState::LOADING;
|
||||
books.clear();
|
||||
entries.clear();
|
||||
navigationHistory.clear();
|
||||
currentPath = OPDS_ROOT_PATH;
|
||||
selectorIndex = 0;
|
||||
errorMessage.clear();
|
||||
statusMessage = "Loading...";
|
||||
@ -45,8 +80,8 @@ void OpdsBookBrowserActivity::onEnter() {
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
|
||||
// Fetch books after setting up the display task
|
||||
fetchBooks();
|
||||
// Fetch feed after setting up the display task
|
||||
fetchFeed(currentPath);
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::onExit() {
|
||||
@ -59,27 +94,28 @@ void OpdsBookBrowserActivity::onExit() {
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
books.clear();
|
||||
entries.clear();
|
||||
navigationHistory.clear();
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::loop() {
|
||||
// Handle error state - Confirm retries, Back goes home
|
||||
// Handle error state - Confirm retries, Back goes back or home
|
||||
if (state == BrowserState::ERROR) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
updateRequired = true;
|
||||
fetchBooks();
|
||||
fetchFeed(currentPath);
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
onGoHome();
|
||||
navigateBack();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle loading state - only Back goes home
|
||||
// Handle loading state - only Back works
|
||||
if (state == BrowserState::LOADING) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
onGoHome();
|
||||
navigateBack();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -89,8 +125,8 @@ void OpdsBookBrowserActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle book list state
|
||||
if (state == BrowserState::BOOK_LIST) {
|
||||
// Handle browsing state
|
||||
if (state == BrowserState::BROWSING) {
|
||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
||||
@ -98,23 +134,28 @@ void OpdsBookBrowserActivity::loop() {
|
||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (!books.empty()) {
|
||||
downloadBook(books[selectorIndex]);
|
||||
if (!entries.empty()) {
|
||||
const auto& entry = entries[selectorIndex];
|
||||
if (entry.type == OpdsEntryType::BOOK) {
|
||||
downloadBook(entry);
|
||||
} else {
|
||||
navigateToEntry(entry);
|
||||
}
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
onGoHome();
|
||||
} else if (prevReleased && !books.empty()) {
|
||||
navigateBack();
|
||||
} else if (prevReleased && !entries.empty()) {
|
||||
if (skipPage) {
|
||||
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + books.size()) % books.size();
|
||||
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + entries.size()) % entries.size();
|
||||
} else {
|
||||
selectorIndex = (selectorIndex + books.size() - 1) % books.size();
|
||||
selectorIndex = (selectorIndex + entries.size() - 1) % entries.size();
|
||||
}
|
||||
updateRequired = true;
|
||||
} else if (nextReleased && !books.empty()) {
|
||||
} else if (nextReleased && !entries.empty()) {
|
||||
if (skipPage) {
|
||||
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % books.size();
|
||||
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % entries.size();
|
||||
} else {
|
||||
selectorIndex = (selectorIndex + 1) % books.size();
|
||||
selectorIndex = (selectorIndex + 1) % entries.size();
|
||||
}
|
||||
updateRequired = true;
|
||||
}
|
||||
@ -171,12 +212,17 @@ void OpdsBookBrowserActivity::render() const {
|
||||
return;
|
||||
}
|
||||
|
||||
// Book list state
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Download", "", "");
|
||||
// Browsing state
|
||||
// Show appropriate button hint based on selected entry type
|
||||
const char* confirmLabel = "Open";
|
||||
if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) {
|
||||
confirmLabel = "Download";
|
||||
}
|
||||
const auto labels = mappedInput.mapLabels("« Back", confirmLabel, "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
if (books.empty()) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "No books found");
|
||||
if (entries.empty()) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "No entries found");
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
@ -184,11 +230,19 @@ void OpdsBookBrowserActivity::render() const {
|
||||
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
||||
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
|
||||
|
||||
for (size_t i = pageStartIndex; i < books.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
|
||||
// Format: "Title - Author" or just "Title" if no author
|
||||
std::string displayText = books[i].title;
|
||||
if (!books[i].author.empty()) {
|
||||
displayText += " - " + books[i].author;
|
||||
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
|
||||
const auto& entry = entries[i];
|
||||
|
||||
// Format display text with type indicator
|
||||
std::string displayText;
|
||||
if (entry.type == OpdsEntryType::NAVIGATION) {
|
||||
displayText = "> " + entry.title; // Folder/navigation indicator
|
||||
} else {
|
||||
// Book: "Title - Author" or just "Title"
|
||||
displayText = entry.title;
|
||||
if (!entry.author.empty()) {
|
||||
displayText += " - " + entry.author;
|
||||
}
|
||||
}
|
||||
|
||||
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40);
|
||||
@ -198,7 +252,7 @@ void OpdsBookBrowserActivity::render() const {
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::fetchBooks() {
|
||||
void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
||||
const char* serverUrl = SETTINGS.opdsServerUrl;
|
||||
if (strlen(serverUrl) == 0) {
|
||||
state = BrowserState::ERROR;
|
||||
@ -207,7 +261,7 @@ void OpdsBookBrowserActivity::fetchBooks() {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string url = ensureProtocol(serverUrl) + OPDS_FEED_PATH;
|
||||
std::string url = buildUrl(serverUrl, path);
|
||||
Serial.printf("[%lu] [OPDS] Fetching: %s\n", millis(), url.c_str());
|
||||
|
||||
std::string content;
|
||||
@ -226,21 +280,54 @@ void OpdsBookBrowserActivity::fetchBooks() {
|
||||
return;
|
||||
}
|
||||
|
||||
books = parser.getBooks();
|
||||
entries = parser.getEntries();
|
||||
selectorIndex = 0;
|
||||
|
||||
if (books.empty()) {
|
||||
if (entries.empty()) {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = "No books found";
|
||||
errorMessage = "No entries found";
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state = BrowserState::BOOK_LIST;
|
||||
state = BrowserState::BROWSING;
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::downloadBook(const OpdsBook& book) {
|
||||
void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
|
||||
// Push current path to history before navigating
|
||||
navigationHistory.push_back(currentPath);
|
||||
currentPath = entry.href;
|
||||
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
entries.clear();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
|
||||
fetchFeed(currentPath);
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::navigateBack() {
|
||||
if (navigationHistory.empty()) {
|
||||
// At root, go home
|
||||
onGoHome();
|
||||
} else {
|
||||
// Go back to previous catalog
|
||||
currentPath = navigationHistory.back();
|
||||
navigationHistory.pop_back();
|
||||
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
entries.clear();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
|
||||
fetchFeed(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
state = BrowserState::DOWNLOADING;
|
||||
statusMessage = book.title;
|
||||
downloadProgress = 0;
|
||||
@ -248,7 +335,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsBook& book) {
|
||||
updateRequired = true;
|
||||
|
||||
// Build full download URL
|
||||
std::string downloadUrl = ensureProtocol(SETTINGS.opdsServerUrl) + book.epubUrl;
|
||||
std::string downloadUrl = buildUrl(SETTINGS.opdsServerUrl, book.href);
|
||||
|
||||
// Create sanitized filename
|
||||
std::string filename = "/" + sanitizeFilename(book.title) + ".epub";
|
||||
@ -264,7 +351,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsBook& book) {
|
||||
|
||||
if (result == HttpDownloader::OK) {
|
||||
Serial.printf("[%lu] [OPDS] Download complete: %s\n", millis(), filename.c_str());
|
||||
state = BrowserState::BOOK_LIST;
|
||||
state = BrowserState::BROWSING;
|
||||
updateRequired = true;
|
||||
} else {
|
||||
state = BrowserState::ERROR;
|
||||
|
||||
@ -12,13 +12,13 @@
|
||||
|
||||
/**
|
||||
* Activity for browsing and downloading books from an OPDS server.
|
||||
* Displays a list of available books and allows downloading EPUBs.
|
||||
* Supports navigation through catalog hierarchy and downloading EPUBs.
|
||||
*/
|
||||
class OpdsBookBrowserActivity final : public Activity {
|
||||
public:
|
||||
enum class BrowserState {
|
||||
LOADING, // Fetching OPDS feed
|
||||
BOOK_LIST, // Displaying books
|
||||
BROWSING, // Displaying entries (navigation or books)
|
||||
DOWNLOADING, // Downloading selected EPUB
|
||||
ERROR // Error state with message
|
||||
};
|
||||
@ -37,7 +37,9 @@ class OpdsBookBrowserActivity final : public Activity {
|
||||
bool updateRequired = false;
|
||||
|
||||
BrowserState state = BrowserState::LOADING;
|
||||
std::vector<OpdsBook> books;
|
||||
std::vector<OpdsEntry> entries;
|
||||
std::vector<std::string> navigationHistory; // Stack of previous feed paths for back navigation
|
||||
std::string currentPath; // Current feed path being displayed
|
||||
int selectorIndex = 0;
|
||||
std::string errorMessage;
|
||||
std::string statusMessage;
|
||||
@ -50,7 +52,9 @@ class OpdsBookBrowserActivity final : public Activity {
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
void fetchBooks();
|
||||
void downloadBook(const OpdsBook& book);
|
||||
void fetchFeed(const std::string& path);
|
||||
void navigateToEntry(const OpdsEntry& entry);
|
||||
void navigateBack();
|
||||
void downloadBook(const OpdsEntry& book);
|
||||
std::string sanitizeFilename(const std::string& title) const;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user