mod: Phase 3 — Re-port unmerged upstream PRs
Re-applied upstream PRs not yet merged to upstream/master: - #1055: Byte-level framebuffer writes (fillPhysicalHSpan*, optimized fillRect/drawLine/fillRectDither/fillPolygon) - #1027: Word-width cache (FNV-1a, 128-entry) and hyphenation early exit in ParsedText for 7-9% layout speedup - #1068: Already present in upstream — URL hyphenation fix - #1019: Already present in upstream — file extensions in browser - #1090/#1185/#1217: KOReader sync improvements — binary credential store, document hash caching, ChapterXPathIndexer integration - #1209: OPDS multi-server — OpdsBookBrowserActivity accepts OpdsServer, directory picker for downloads, download-complete prompt with open/back options - #857: Dictionary activities already ported in Phase 1/2 - #1003: Placeholder cover already integrated in Phase 2 Also fixed: STR_OFF i18n string, include paths, replaced Epub::isValidThumbnailBmp with Storage.exists, replaced StringUtils::checkFileExtension with FsHelpers equivalents. Made-with: Cursor
This commit is contained in:
@@ -213,44 +213,6 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- KOReaderCredentialStore ----
|
||||
|
||||
bool JsonSettingsIO::saveKOReader(const KOReaderCredentialStore& store, const char* path) {
|
||||
JsonDocument doc;
|
||||
doc["username"] = store.getUsername();
|
||||
doc["password_obf"] = obfuscation::obfuscateToBase64(store.getPassword());
|
||||
doc["serverUrl"] = store.getServerUrl();
|
||||
doc["matchMethod"] = static_cast<uint8_t>(store.getMatchMethod());
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
return Storage.writeFile(path, json);
|
||||
}
|
||||
|
||||
bool JsonSettingsIO::loadKOReader(KOReaderCredentialStore& store, const char* json, bool* needsResave) {
|
||||
if (needsResave) *needsResave = false;
|
||||
JsonDocument doc;
|
||||
auto error = deserializeJson(doc, json);
|
||||
if (error) {
|
||||
LOG_ERR("KRS", "JSON parse error: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
store.username = doc["username"] | std::string("");
|
||||
bool ok = false;
|
||||
store.password = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok);
|
||||
if (!ok || store.password.empty()) {
|
||||
store.password = doc["password"] | std::string("");
|
||||
if (!store.password.empty() && needsResave) *needsResave = true;
|
||||
}
|
||||
store.serverUrl = doc["serverUrl"] | std::string("");
|
||||
uint8_t method = doc["matchMethod"] | (uint8_t)0;
|
||||
store.matchMethod = static_cast<DocumentMatchMethod>(method);
|
||||
|
||||
LOG_DBG("KRS", "Loaded KOReader credentials for user: %s", store.username.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- WifiCredentialStore ----
|
||||
|
||||
bool JsonSettingsIO::saveWifi(const WifiCredentialStore& store, const char* path) {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
class CrossPointSettings;
|
||||
class CrossPointState;
|
||||
class WifiCredentialStore;
|
||||
class KOReaderCredentialStore;
|
||||
class RecentBooksStore;
|
||||
|
||||
namespace JsonSettingsIO {
|
||||
@@ -20,10 +19,6 @@ bool loadState(CrossPointState& s, const char* json);
|
||||
bool saveWifi(const WifiCredentialStore& store, const char* path);
|
||||
bool loadWifi(WifiCredentialStore& store, const char* json, bool* needsResave = nullptr);
|
||||
|
||||
// KOReaderCredentialStore
|
||||
bool saveKOReader(const KOReaderCredentialStore& store, const char* path);
|
||||
bool loadKOReader(KOReaderCredentialStore& store, const char* json, bool* needsResave = nullptr);
|
||||
|
||||
// RecentBooksStore
|
||||
bool saveRecentBooks(const RecentBooksStore& store, const char* path);
|
||||
bool loadRecentBooks(RecentBooksStore& store, const char* json);
|
||||
|
||||
@@ -181,6 +181,10 @@ void ActivityManager::goToBrowser() {
|
||||
replaceActivity(std::make_unique<OpdsBookBrowserActivity>(renderer, mappedInput));
|
||||
}
|
||||
|
||||
void ActivityManager::goToBrowser(const OpdsServer& server) {
|
||||
replaceActivity(std::make_unique<OpdsBookBrowserActivity>(renderer, mappedInput, &server));
|
||||
}
|
||||
|
||||
void ActivityManager::goToReader(std::string path) {
|
||||
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "GfxRenderer.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerStore.h"
|
||||
|
||||
class Activity; // forward declaration
|
||||
class RenderLock; // forward declaration
|
||||
@@ -82,6 +83,7 @@ class ActivityManager {
|
||||
void goToFileBrowser(std::string path = {});
|
||||
void goToRecentBooks();
|
||||
void goToBrowser();
|
||||
void goToBrowser(const struct OpdsServer& server);
|
||||
void goToReader(std::string path);
|
||||
void goToSleep();
|
||||
void goToBoot();
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
#include <OpdsStream.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "activities/ActivityResult.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
#include "activities/util/DirectoryPickerActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "network/HttpDownloader.h"
|
||||
@@ -23,16 +25,22 @@ constexpr int PAGE_ITEMS = 23;
|
||||
void OpdsBookBrowserActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
OPDS_STORE.loadFromFile();
|
||||
// Resolve server from store if not provided at construction
|
||||
if (server.url.empty() && OPDS_STORE.hasServers()) {
|
||||
const auto* s = OPDS_STORE.getServer(0);
|
||||
if (s) server = *s;
|
||||
}
|
||||
|
||||
state = BrowserState::CHECK_WIFI;
|
||||
entries.clear();
|
||||
navigationHistory.clear();
|
||||
currentPath = ""; // Root path - user provides full URL in settings
|
||||
currentPath = "";
|
||||
selectorIndex = 0;
|
||||
errorMessage.clear();
|
||||
statusMessage = tr(STR_CHECKING_WIFI);
|
||||
requestUpdate();
|
||||
|
||||
// Check WiFi and connect if needed, then fetch feed
|
||||
checkAndConnectWifi();
|
||||
}
|
||||
|
||||
@@ -47,9 +55,7 @@ void OpdsBookBrowserActivity::onExit() {
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::loop() {
|
||||
// Handle WiFi selection subactivity
|
||||
if (state == BrowserState::WIFI_SELECTION) {
|
||||
// Should already handled by the WifiSelectionActivity
|
||||
if (state == BrowserState::WIFI_SELECTION || state == BrowserState::PICKING_DIRECTORY) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,18 +97,40 @@ void OpdsBookBrowserActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle downloading state - no input allowed
|
||||
if (state == BrowserState::DOWNLOADING) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle browsing state
|
||||
if (state == BrowserState::DOWNLOAD_COMPLETE) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
executePromptAction(0);
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
executePromptAction(promptSelection);
|
||||
return;
|
||||
}
|
||||
buttonNavigator.onNextRelease([this] {
|
||||
if (promptSelection != 1) {
|
||||
promptSelection = 1;
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
buttonNavigator.onPreviousRelease([this] {
|
||||
if (promptSelection != 0) {
|
||||
promptSelection = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == BrowserState::BROWSING) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (!entries.empty()) {
|
||||
const auto& entry = entries[selectorIndex];
|
||||
if (entry.type == OpdsEntryType::BOOK) {
|
||||
downloadBook(entry);
|
||||
launchDirectoryPicker(entry);
|
||||
} else {
|
||||
navigateToEntry(entry);
|
||||
}
|
||||
@@ -142,7 +170,8 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
|
||||
const char* headerTitle = server.name.empty() ? tr(STR_OPDS_BROWSER) : server.name.c_str();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, headerTitle, true, EpdFontFamily::BOLD);
|
||||
|
||||
if (state == BrowserState::CHECK_WIFI) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
|
||||
@@ -186,6 +215,31 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == BrowserState::DOWNLOAD_COMPLETE) {
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 50, tr(STR_DOWNLOAD_COMPLETE), true, EpdFontFamily::BOLD);
|
||||
const auto maxWidth = pageWidth - 40;
|
||||
auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), maxWidth);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, title.c_str());
|
||||
|
||||
const int buttonY = pageHeight / 2 + 20;
|
||||
const char* backText = tr(STR_BACK_TO_LISTING);
|
||||
const char* openText = tr(STR_OPEN_BOOK);
|
||||
std::string backLabel = promptSelection == 0 ? "[" + std::string(backText) + "]" : std::string(backText);
|
||||
std::string openLabel = promptSelection == 1 ? "[" + std::string(openText) + "]" : std::string(openText);
|
||||
const int backWidth = renderer.getTextWidth(UI_10_FONT_ID, backLabel.c_str());
|
||||
const int openWidth = renderer.getTextWidth(UI_10_FONT_ID, openLabel.c_str());
|
||||
constexpr int buttonSpacing = 40;
|
||||
const int totalWidth = backWidth + buttonSpacing + openWidth;
|
||||
const int startX = (pageWidth - totalWidth) / 2;
|
||||
renderer.drawText(UI_10_FONT_ID, startX, buttonY, backLabel.c_str());
|
||||
renderer.drawText(UI_10_FONT_ID, startX + backWidth + buttonSpacing, buttonY, openLabel.c_str());
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_CONFIRM), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Browsing state
|
||||
// Show appropriate button hint based on selected entry type
|
||||
const char* confirmLabel = tr(STR_OPEN);
|
||||
@@ -228,22 +282,21 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
||||
const char* serverUrl = SETTINGS.opdsServerUrl;
|
||||
if (strlen(serverUrl) == 0) {
|
||||
if (server.url.empty()) {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = tr(STR_NO_SERVER_URL);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string url = UrlUtils::buildUrl(serverUrl, path);
|
||||
std::string url = UrlUtils::buildUrl(server.url, path);
|
||||
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
|
||||
|
||||
OpdsParser parser;
|
||||
|
||||
{
|
||||
OpdsParserStream stream{parser};
|
||||
if (!HttpDownloader::fetchUrl(url, stream)) {
|
||||
if (!HttpDownloader::fetchUrl(url, stream, server.username, server.password)) {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = tr(STR_FETCH_FEED_FAILED);
|
||||
requestUpdate();
|
||||
@@ -306,41 +359,68 @@ void OpdsBookBrowserActivity::navigateBack() {
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
void OpdsBookBrowserActivity::launchDirectoryPicker(const OpdsEntry& book) {
|
||||
pendingBook = book;
|
||||
state = BrowserState::PICKING_DIRECTORY;
|
||||
requestUpdate();
|
||||
|
||||
startActivityForResult(
|
||||
std::make_unique<DirectoryPickerActivity>(renderer, mappedInput, server.downloadPath),
|
||||
[this](const ActivityResult& result) { onDirectoryPickerResult(result); });
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::onDirectoryPickerResult(const ActivityResult& result) {
|
||||
state = BrowserState::BROWSING;
|
||||
if (result.isCancelled) {
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
if (std::holds_alternative<KeyboardResult>(result.data)) {
|
||||
const std::string& directory = std::get<KeyboardResult>(result.data).text;
|
||||
downloadBook(pendingBook, directory);
|
||||
} else {
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const std::string& directory) {
|
||||
state = BrowserState::DOWNLOADING;
|
||||
statusMessage = book.title;
|
||||
downloadProgress = 0;
|
||||
downloadTotal = 0;
|
||||
requestUpdate(true);
|
||||
|
||||
// Build full download URL
|
||||
std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
|
||||
std::string downloadUrl = UrlUtils::buildUrl(server.url, book.href);
|
||||
|
||||
// Create sanitized filename: "Title - Author.epub" or just "Title.epub" if no author
|
||||
std::string baseName = book.title;
|
||||
if (!book.author.empty()) {
|
||||
baseName += " - " + book.author;
|
||||
}
|
||||
std::string filename = "/" + StringUtils::sanitizeFilename(baseName) + ".epub";
|
||||
std::string dir = directory;
|
||||
if (dir.back() != '/') dir += '/';
|
||||
std::string filename = dir + StringUtils::sanitizeFilename(baseName) + ".epub";
|
||||
|
||||
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
|
||||
|
||||
const auto result =
|
||||
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
|
||||
const auto result = HttpDownloader::downloadToFile(
|
||||
downloadUrl, filename,
|
||||
[this](const size_t downloaded, const size_t total) {
|
||||
downloadProgress = downloaded;
|
||||
downloadTotal = total;
|
||||
requestUpdate(true); // Force update to refresh progress bar
|
||||
});
|
||||
requestUpdate(true);
|
||||
},
|
||||
server.username, server.password);
|
||||
|
||||
if (result == HttpDownloader::OK) {
|
||||
LOG_DBG("OPDS", "Download complete: %s", filename.c_str());
|
||||
|
||||
// Invalidate any existing cache for this file to prevent stale metadata issues
|
||||
Epub epub(filename, "/.crosspoint");
|
||||
epub.clearCache();
|
||||
LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str());
|
||||
|
||||
state = BrowserState::BROWSING;
|
||||
downloadedFilePath = filename;
|
||||
promptSelection = server.afterDownloadAction;
|
||||
state = BrowserState::DOWNLOAD_COMPLETE;
|
||||
requestUpdate();
|
||||
} else {
|
||||
state = BrowserState::ERROR;
|
||||
@@ -349,6 +429,15 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::executePromptAction(int action) {
|
||||
if (action == 1) {
|
||||
onSelectBook(downloadedFilePath);
|
||||
return;
|
||||
}
|
||||
state = BrowserState::BROWSING;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
||||
// Already connected? Verify connection is valid by checking IP
|
||||
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
/**
|
||||
@@ -16,16 +17,19 @@
|
||||
class OpdsBookBrowserActivity final : public Activity {
|
||||
public:
|
||||
enum class BrowserState {
|
||||
CHECK_WIFI, // Checking WiFi connection
|
||||
WIFI_SELECTION, // WiFi selection subactivity is active
|
||||
LOADING, // Fetching OPDS feed
|
||||
BROWSING, // Displaying entries (navigation or books)
|
||||
DOWNLOADING, // Downloading selected EPUB
|
||||
ERROR // Error state with message
|
||||
CHECK_WIFI, // Checking WiFi connection
|
||||
WIFI_SELECTION, // WiFi selection subactivity is active
|
||||
LOADING, // Fetching OPDS feed
|
||||
BROWSING, // Displaying entries (navigation or books)
|
||||
PICKING_DIRECTORY, // Directory picker subactivity is active
|
||||
DOWNLOADING, // Downloading selected EPUB
|
||||
DOWNLOAD_COMPLETE, // Prompt: open book or go back to listing
|
||||
ERROR // Error state with message
|
||||
};
|
||||
|
||||
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("OpdsBookBrowser", renderer, mappedInput) {}
|
||||
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const OpdsServer* server = nullptr)
|
||||
: Activity("OpdsBookBrowser", renderer, mappedInput), server(server ? *server : OpdsServer{}) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
@@ -43,6 +47,11 @@ class OpdsBookBrowserActivity final : public Activity {
|
||||
std::string statusMessage;
|
||||
size_t downloadProgress = 0;
|
||||
size_t downloadTotal = 0;
|
||||
std::string downloadedFilePath;
|
||||
int promptSelection = 0; // 0 = back to listing, 1 = open book
|
||||
|
||||
OpdsServer server; // Copied at construction — safe even if the store changes during browsing
|
||||
OpdsEntry pendingBook;
|
||||
|
||||
void checkAndConnectWifi();
|
||||
void launchWifiSelection();
|
||||
@@ -50,6 +59,9 @@ class OpdsBookBrowserActivity final : public Activity {
|
||||
void fetchFeed(const std::string& path);
|
||||
void navigateToEntry(const OpdsEntry& entry);
|
||||
void navigateBack();
|
||||
void downloadBook(const OpdsEntry& book);
|
||||
void launchDirectoryPicker(const OpdsEntry& book);
|
||||
void onDirectoryPickerResult(const ActivityResult& result);
|
||||
void downloadBook(const OpdsEntry& book, const std::string& directory);
|
||||
void executePromptAction(int action);
|
||||
bool preventAutoSleep() override { return true; }
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "ActivityResult.h"
|
||||
#include "activities/ActivityResult.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@@ -244,6 +244,15 @@ std::string getFileName(std::string filename) {
|
||||
return filename.substr(0, pos);
|
||||
}
|
||||
|
||||
std::string getFileExtension(std::string filename) {
|
||||
if (filename.back() == '/') {
|
||||
return "";
|
||||
}
|
||||
const auto pos = filename.rfind('.');
|
||||
if (pos == std::string::npos) return "";
|
||||
return filename.substr(pos);
|
||||
}
|
||||
|
||||
void FileBrowserActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
@@ -262,7 +271,8 @@ void FileBrowserActivity::render(RenderLock&&) {
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
|
||||
[this](int index) { return getFileName(files[index]); }, nullptr,
|
||||
[this](int index) { return UITheme::getFileIcon(files[index]); });
|
||||
[this](int index) { return UITheme::getFileIcon(files[index]); },
|
||||
[this](int index) { return getFileExtension(files[index]); }, false);
|
||||
}
|
||||
|
||||
// Help text
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
#include "util/StringUtils.h"
|
||||
|
||||
|
||||
int HomeActivity::getMenuItemCount() const {
|
||||
int count = 4; // File Browser, Recents, File transfer, Settings
|
||||
@@ -66,7 +66,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
||||
for (RecentBook& book : recentBooks) {
|
||||
if (!book.coverBmpPath.empty()) {
|
||||
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
|
||||
if (!Epub::isValidThumbnailBmp(coverPath)) {
|
||||
if (!Storage.exists(coverPath.c_str())) {
|
||||
if (!showingLoading) {
|
||||
showingLoading = true;
|
||||
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING));
|
||||
@@ -75,7 +75,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
||||
|
||||
bool success = false;
|
||||
|
||||
if (StringUtils::checkFileExtension(book.path, ".epub")) {
|
||||
if (FsHelpers::hasEpubExtension(book.path)) {
|
||||
Epub epub(book.path, "/.crosspoint");
|
||||
if (!epub.load(false, true)) {
|
||||
epub.load(true, true);
|
||||
@@ -87,13 +87,9 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
||||
book.coverBmpPath = thumbPath;
|
||||
} else {
|
||||
const int thumbWidth = static_cast<int>(coverHeight * 0.6);
|
||||
success = PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight);
|
||||
if (!success) {
|
||||
epub.generateInvalidFormatThumbBmp(coverHeight);
|
||||
}
|
||||
PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight);
|
||||
}
|
||||
} else if (StringUtils::checkFileExtension(book.path, ".xtch") ||
|
||||
StringUtils::checkFileExtension(book.path, ".xtc")) {
|
||||
} else if (FsHelpers::hasXtcExtension(book.path)) {
|
||||
Xtc xtc(book.path, "/.crosspoint");
|
||||
if (xtc.load()) {
|
||||
success = xtc.generateThumbBmp(coverHeight);
|
||||
|
||||
@@ -52,8 +52,24 @@ class FileWriteStream final : public Stream {
|
||||
};
|
||||
} // namespace
|
||||
|
||||
static void addAuthHeader(HTTPClient& http, const std::string& username, const std::string& password) {
|
||||
if (!username.empty() || !password.empty()) {
|
||||
std::string credentials = username + ":" + password;
|
||||
String encoded = base64::encode(credentials.c_str());
|
||||
http.addHeader("Authorization", "Basic " + encoded);
|
||||
} else if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
||||
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
||||
String encoded = base64::encode(credentials.c_str());
|
||||
http.addHeader("Authorization", "Basic " + encoded);
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
|
||||
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
||||
return fetchUrl(url, outContent, "", "");
|
||||
}
|
||||
|
||||
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent, const std::string& username,
|
||||
const std::string& password) {
|
||||
std::unique_ptr<NetworkClient> client;
|
||||
if (UrlUtils::isHttpsUrl(url)) {
|
||||
auto* secureClient = new NetworkClientSecure();
|
||||
@@ -69,13 +85,7 @@ bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
|
||||
http.begin(*client, url.c_str());
|
||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||
|
||||
// Add Basic HTTP auth if credentials are configured
|
||||
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
||||
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
||||
String encoded = base64::encode(credentials.c_str());
|
||||
http.addHeader("Authorization", "Basic " + encoded);
|
||||
}
|
||||
addAuthHeader(http, username, password);
|
||||
|
||||
const int httpCode = http.GET();
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
@@ -85,7 +95,6 @@ bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
|
||||
}
|
||||
|
||||
http.writeToStream(&outContent);
|
||||
|
||||
http.end();
|
||||
|
||||
LOG_DBG("HTTP", "Fetch success");
|
||||
@@ -103,7 +112,12 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) {
|
||||
|
||||
HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& url, const std::string& destPath,
|
||||
ProgressCallback progress) {
|
||||
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
||||
return downloadToFile(url, destPath, progress, "", "");
|
||||
}
|
||||
|
||||
HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& url, const std::string& destPath,
|
||||
ProgressCallback progress, const std::string& username,
|
||||
const std::string& password) {
|
||||
std::unique_ptr<NetworkClient> client;
|
||||
if (UrlUtils::isHttpsUrl(url)) {
|
||||
auto* secureClient = new NetworkClientSecure();
|
||||
@@ -120,13 +134,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
||||
http.begin(*client, url.c_str());
|
||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||
|
||||
// Add Basic HTTP auth if credentials are configured
|
||||
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
||||
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
||||
String encoded = base64::encode(credentials.c_str());
|
||||
http.addHeader("Authorization", "Basic " + encoded);
|
||||
}
|
||||
addAuthHeader(http, username, password);
|
||||
|
||||
const int httpCode = http.GET();
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
|
||||
@@ -29,6 +29,13 @@ class HttpDownloader {
|
||||
|
||||
static bool fetchUrl(const std::string& url, Stream& stream);
|
||||
|
||||
/**
|
||||
* Fetch URL with optional HTTP Basic auth credentials.
|
||||
* When username and password are empty, falls back to CrossPointSettings credentials.
|
||||
*/
|
||||
static bool fetchUrl(const std::string& url, Stream& stream, const std::string& username,
|
||||
const std::string& password);
|
||||
|
||||
/**
|
||||
* Download a file to the SD card.
|
||||
* @param url The URL to download
|
||||
@@ -38,4 +45,12 @@ class HttpDownloader {
|
||||
*/
|
||||
static DownloadError downloadToFile(const std::string& url, const std::string& destPath,
|
||||
ProgressCallback progress = nullptr);
|
||||
|
||||
/**
|
||||
* Download a file with optional HTTP Basic auth credentials.
|
||||
* When username and password are empty, falls back to CrossPointSettings credentials.
|
||||
*/
|
||||
static DownloadError downloadToFile(const std::string& url, const std::string& destPath,
|
||||
ProgressCallback progress, const std::string& username,
|
||||
const std::string& password);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user