## Summary * Add support for configuring and using multiple OPDS servers, replacing the previous single-server limitation. Closes https://github.com/crosspoint-reader/crosspoint-reader/issues/1178 * New OpdsServerStore singleton (modeled after WifiCredentialStore) that persists up to 8 OPDS servers to /.crosspoint/opds.json with MAC-based password obfuscation. * One-time migration from legacy single-server fields in CrossPointSettings to the new store on first boot. * New OpdsServerListActivity for the device UI — works in two modes: a settings list (add/edit/delete servers) and a picker (select which server to browse). When only one server is configured, the picker is skipped automatically. * Renamed CalibreSettingsActivity → OpdsSettingsActivity for clarity. It now edits individual OpdsServer entries (name, URL, username, password, delete). * OpdsBookBrowserActivity now receives an OpdsServer at construction and uses its credentials for all fetches/downloads, and shows the server name in the header. * HttpDownloader::fetchUrl and downloadToFile accept optional per-call username/password parameters instead of reading from global settings. * REST API endpoints on CrossPointWebServer: GET /api/opds, POST /api/opds, POST /api/opds/delete — passwords are never exposed over the API (only a hasPassword flag), and omitting the password field on update preserves the existing one. * Web UI (SettingsPage.html) with dynamic OPDS server management cards — add, edit, save, and delete servers from the browser. <img width="932" height="906" alt="SCR-20260416-stvu" src="https://github.com/user-attachments/assets/a8f18d84-4204-46a0-bb31-b73d24b3255f" /> ## Additional Context * The OpdsServerStore JSON format and obfuscation scheme are identical to WifiCredentialStore, so the same JsonSettingsIO infrastructure handles both. * The web API uses POST /api/opds/delete instead of DELETE /api/opds because the ESP32 WebServer doesn't support the DELETE method with a request body. * Existing single-server configurations are migrated automatically — no user action required. After migration the legacy CrossPointSettings fields are cleared so it only runs once. * The HttpDownloader changes are backward-compatible: the credential parameters default to empty strings, so existing callers are unaffected. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< YES >**_ --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
116 lines
3.0 KiB
C++
116 lines
3.0 KiB
C++
#pragma once
|
|
|
|
#include <HalStorage.h>
|
|
#include <NetworkUdp.h>
|
|
#include <WebServer.h>
|
|
#include <WebSocketsServer.h>
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// Structure to hold file information
|
|
struct FileInfo {
|
|
String name;
|
|
size_t size;
|
|
bool isEpub;
|
|
bool isDirectory;
|
|
};
|
|
|
|
class CrossPointWebServer {
|
|
public:
|
|
struct WsUploadStatus {
|
|
bool inProgress = false;
|
|
size_t received = 0;
|
|
size_t total = 0;
|
|
std::string filename;
|
|
std::string lastCompleteName;
|
|
size_t lastCompleteSize = 0;
|
|
unsigned long lastCompleteAt = 0;
|
|
};
|
|
|
|
// Used by POST upload handler
|
|
struct UploadState {
|
|
FsFile file;
|
|
String fileName;
|
|
String path = "/";
|
|
size_t size = 0;
|
|
bool success = false;
|
|
String error = "";
|
|
|
|
// Upload write buffer - batches small writes into larger SD card operations
|
|
// 4KB is a good balance: large enough to reduce syscall overhead, small enough
|
|
// to keep individual write times short and avoid watchdog issues
|
|
static constexpr size_t UPLOAD_BUFFER_SIZE = 4096; // 4KB buffer
|
|
std::vector<uint8_t> buffer;
|
|
size_t bufferPos = 0;
|
|
|
|
UploadState() { buffer.resize(UPLOAD_BUFFER_SIZE); }
|
|
} upload;
|
|
|
|
CrossPointWebServer();
|
|
~CrossPointWebServer();
|
|
|
|
// Start the web server (call after WiFi is connected)
|
|
void begin();
|
|
|
|
// Stop the web server
|
|
void stop();
|
|
|
|
// Call this periodically to handle client requests
|
|
void handleClient();
|
|
|
|
// Check if server is running
|
|
bool isRunning() const { return running; }
|
|
|
|
WsUploadStatus getWsUploadStatus() const;
|
|
|
|
// Get the port number
|
|
uint16_t getPort() const { return port; }
|
|
|
|
private:
|
|
std::unique_ptr<WebServer> server = nullptr;
|
|
std::unique_ptr<WebSocketsServer> wsServer = nullptr;
|
|
bool running = false;
|
|
bool apMode = false; // true when running in AP mode, false for STA mode
|
|
uint16_t port = 80;
|
|
uint16_t wsPort = 81; // WebSocket port
|
|
NetworkUDP udp;
|
|
bool udpActive = false;
|
|
|
|
// WebSocket upload state
|
|
void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length);
|
|
static void wsEventCallback(uint8_t num, WStype_t type, uint8_t* payload, size_t length);
|
|
void abortWsUpload(const char* tag);
|
|
|
|
// File scanning
|
|
void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const;
|
|
String formatFileSize(size_t bytes) const;
|
|
bool isEpubFile(const String& filename) const;
|
|
|
|
// Request handlers
|
|
void handleRoot() const;
|
|
void handleJszip() const;
|
|
void handleNotFound() const;
|
|
void handleStatus() const;
|
|
void handleFileList() const;
|
|
void handleFileListData() const;
|
|
void handleDownload() const;
|
|
void handleUpload(UploadState& state) const;
|
|
void handleUploadPost(UploadState& state) const;
|
|
void handleCreateFolder() const;
|
|
void handleRename() const;
|
|
void handleMove() const;
|
|
void handleDelete() const;
|
|
|
|
// Settings handlers
|
|
void handleSettingsPage() const;
|
|
void handleGetSettings() const;
|
|
void handlePostSettings();
|
|
|
|
// OPDS server handlers
|
|
void handleGetOpdsServers() const;
|
|
void handlePostOpdsServer();
|
|
void handleDeleteOpdsServer();
|
|
};
|