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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user