diff --git a/platformio.ini b/platformio.ini index a4bdcd1..6699f0b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,6 +39,7 @@ lib_deps = BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor InputManager=symlink://open-x4-sdk/libs/hardware/InputManager EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay + ArduinoJson @ 7.4.2 [env:default] extends = base diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 8be9ef4..55f3203 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1,52 +1,19 @@ #include "CrossPointWebServer.h" +#include #include #include #include -#include "html/FilesPageFooterHtml.generated.h" -#include "html/FilesPageHeaderHtml.generated.h" +#include "html/FilesPageHtml.generated.h" #include "html/HomePageHtml.generated.h" namespace { - // Folders/files to hide from the web interface file browser // Note: Items starting with "." are automatically hidden const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"}; constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]); - -// Helper function to escape HTML special characters to prevent XSS -String escapeHtml(const String& input) { - String output; - output.reserve(input.length() * 1.1); // Pre-allocate with some extra space - - for (size_t i = 0; i < input.length(); i++) { - const char c = input.charAt(i); - switch (c) { - case '&': - output += "&"; - break; - case '<': - output += "<"; - break; - case '>': - output += ">"; - break; - case '"': - output += """; - break; - case '\'': - output += "'"; - break; - default: - output += c; - break; - } - } - return output; -} - } // namespace // File listing page template - now using generated headers: @@ -82,9 +49,11 @@ void CrossPointWebServer::begin() { // Setup routes Serial.printf("[%lu] [WEB] Setting up routes...\n", millis()); server->on("/", HTTP_GET, [this] { handleRoot(); }); - server->on("/status", HTTP_GET, [this] { handleStatus(); }); server->on("/files", HTTP_GET, [this] { handleFileList(); }); + server->on("/api/status", HTTP_GET, [this] { handleStatus(); }); + server->on("/api/files", HTTP_GET, [this] { handleFileListData(); }); + // Upload endpoint with special handling for multipart form data server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); }); @@ -183,19 +152,17 @@ void CrossPointWebServer::handleStatus() const { server->send(200, "application/json", json); } -std::vector CrossPointWebServer::scanFiles(const char* path) const { - std::vector files; - +void CrossPointWebServer::scanFiles(const char* path, const std::function& callback) const { File root = SD.open(path); if (!root) { Serial.printf("[%lu] [WEB] Failed to open directory: %s\n", millis(), path); - return files; + return; } if (!root.isDirectory()) { Serial.printf("[%lu] [WEB] Not a directory: %s\n", millis(), path); root.close(); - return files; + return; } Serial.printf("[%lu] [WEB] Scanning files in: %s\n", millis(), path); @@ -230,26 +197,13 @@ std::vector CrossPointWebServer::scanFiles(const char* path) const { info.isEpub = isEpubFile(info.name); } - files.push_back(info); + callback(info); } file.close(); file = root.openNextFile(); } root.close(); - - Serial.printf("[%lu] [WEB] Found %d items (files and folders)\n", millis(), files.size()); - return files; -} - -String CrossPointWebServer::formatFileSize(const size_t bytes) const { - if (bytes < 1024) { - return String(bytes) + " B"; - } - if (bytes < 1024 * 1024) { - return String(bytes / 1024.0, 1) + " KB"; - } - return String(bytes / (1024.0 * 1024.0), 1) + " MB"; } bool CrossPointWebServer::isEpubFile(const String& filename) const { @@ -259,10 +213,11 @@ bool CrossPointWebServer::isEpubFile(const String& filename) const { } void CrossPointWebServer::handleFileList() const { - server->setContentLength(CONTENT_LENGTH_UNKNOWN); - server->send(200, "text/html", ""); - server->sendContent(FilesPageHeaderHtml); + // server->setContentLength(CONTENT_LENGTH_UNKNOWN); + server->send(200, "text/html", FilesPageHtml); +} +void CrossPointWebServer::handleFileListData() const { // Get current path from query string (default to root) String currentPath = "/"; if (server->hasArg("path")) { @@ -277,182 +232,27 @@ void CrossPointWebServer::handleFileList() const { } } - // Get message from query string if present - if (server->hasArg("msg")) { - String msg = escapeHtml(server->arg("msg")); - String msgType = server->hasArg("type") ? escapeHtml(server->arg("type")) : "success"; - server->sendContent("
" + msg + "
"); - } + server->setContentLength(CONTENT_LENGTH_UNKNOWN); + server->send(200, "application/json", ""); + server->sendContent("["); + char output[300]; + bool seenFirst = false; + scanFiles(currentPath.c_str(), [this, output, seenFirst](const FileInfo& info) mutable { + JsonDocument doc; + doc["name"] = info.name; + doc["size"] = info.size; + doc["isDirectory"] = info.isDirectory; + doc["isEpub"] = info.isEpub; + serializeJson(doc, output, sizeof(output)); - // Hidden input to store current path for JavaScript - server->sendContent(""); - - // Scan files in current path first (we need counts for the header) - std::vector files = scanFiles(currentPath.c_str()); - - // Count items - int epubCount = 0; - int folderCount = 0; - size_t totalSize = 0; - for (const auto& file : files) { - if (file.isDirectory) { - folderCount++; + if (seenFirst) { + server->sendContent(","); } else { - if (file.isEpub) epubCount++; - totalSize += file.size; + seenFirst = true; } - } - - // Page header with inline breadcrumb and action buttons - server->sendContent( - "
" - "
" - "

📁 File Manager

"); - - // Inline breadcrumb - server->sendContent( - "
" - "/"); - - if (currentPath == "/") { - server->sendContent("🏠"); - } else { - server->sendContent("🏠"); - String pathParts = currentPath.substring(1); // Remove leading / - String buildPath = ""; - int start = 0; - int end = pathParts.indexOf('/'); - - while (start < static_cast(pathParts.length())) { - String part; - if (end == -1) { - part = pathParts.substring(start); - server->sendContent("/" + escapeHtml(part) + ""); - break; - } else { - part = pathParts.substring(start, end); - buildPath += "/" + part; - server->sendContent("/" + - escapeHtml(part) + ""); - start = end + 1; - end = pathParts.indexOf('/', start); - } - } - } - server->sendContent("
"); - - // Action buttons - server->sendContent( - "
" - "" - "" - "
" - "
"); // end page-header - - // Contents card with inline summary - - server->sendContent( - "
" - // Contents header with inline stats - "
" - "

Contents

" - ""); - server->sendContent(String(folderCount) + " folder" + (folderCount != 1 ? "s" : "") + ", " + - String(files.size() - folderCount) + " file" + ((files.size() - folderCount) != 1 ? "s" : "") + - ", " + formatFileSize(totalSize) + "
"); - - if (files.empty()) { - server->sendContent("
This folder is empty
"); - } else { - server->sendContent( - "" - ""); - - // Sort files: folders first, then epub files, then other files, alphabetically within each group - std::sort(files.begin(), files.end(), [](const FileInfo& a, const FileInfo& b) { - // Folders come first - if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory; - // Then sort by epub status (epubs first among files) - if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub; - // Then alphabetically - return a.name < b.name; - }); - - for (const auto& file : files) { - String rowClass; - String icon; - String badge; - String typeStr; - String sizeStr; - - if (file.isDirectory) { - rowClass = "folder-row"; - icon = "📁"; - badge = "FOLDER"; - typeStr = "Folder"; - sizeStr = "-"; - - // Build the path to this folder - String folderPath = currentPath; - if (!folderPath.endsWith("/")) folderPath += "/"; - folderPath += file.name; - - server->sendContent(""); - server->sendContent(""); - server->sendContent(""); - server->sendContent(""); - // Escape quotes for JavaScript string - String escapedName = file.name; - escapedName.replace("'", "\\'"); - String escapedPath = folderPath; - escapedPath.replace("'", "\\'"); - server->sendContent(""); - server->sendContent(""); - } else { - rowClass = file.isEpub ? "epub-file" : ""; - icon = file.isEpub ? "📗" : "📄"; - badge = file.isEpub ? "EPUB" : ""; - String ext = file.name.substring(file.name.lastIndexOf('.') + 1); - ext.toUpperCase(); - typeStr = ext; - sizeStr = formatFileSize(file.size); - - // Build file path for delete - String filePath = currentPath; - if (!filePath.endsWith("/")) filePath += "/"; - filePath += file.name; - - server->sendContent(""); - server->sendContent(""); - server->sendContent(""); - server->sendContent(""); - // Escape quotes for JavaScript string - String escapedName = file.name; - escapedName.replace("'", "\\'"); - String escapedPath = filePath; - escapedPath.replace("'", "\\'"); - server->sendContent(""); - server->sendContent(""); - } - } - - server->sendContent("
NameTypeSizeActions
" + icon + - "" - "" + escapeHtml(file.name) + "" + badge + - "" + typeStr + "" + sizeStr + "
" + icon + "" + escapeHtml(file.name) + badge + - "" + typeStr + "" + sizeStr + "
"); - } - - server->sendContent("
"); - server->sendContent(FilesPageFooterHtml); - // Signal end of content + server->sendContent(output); + }); + server->sendContent("]"); server->sendContent(""); Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str()); } diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index e37eb4e..327897f 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -38,7 +38,7 @@ class CrossPointWebServer { uint16_t port = 80; // File scanning - std::vector scanFiles(const char* path = "/") const; + void scanFiles(const char* path, const std::function& callback) const; String formatFileSize(size_t bytes) const; bool isEpubFile(const String& filename) const; @@ -47,6 +47,7 @@ class CrossPointWebServer { void handleNotFound() const; void handleStatus() const; void handleFileList() const; + void handleFileListData() const; void handleUpload() const; void handleUploadPost() const; void handleCreateFolder() const; diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html new file mode 100644 index 0000000..b2fff37 --- /dev/null +++ b/src/network/html/FilesPage.html @@ -0,0 +1,855 @@ + + + + + + CrossPoint Reader - Files + + + + + + + +
+
+

Contents

+ +
+ +
+
+ +
+
+
+ +
+

+ CrossPoint E-Reader • Open Source +

+
+ + + + + + + + + + + + + diff --git a/src/network/html/FilesPageFooter.html b/src/network/html/FilesPageFooter.html deleted file mode 100644 index 961753a..0000000 --- a/src/network/html/FilesPageFooter.html +++ /dev/null @@ -1,233 +0,0 @@ -
-

- CrossPoint E-Reader • Open Source -

-
- - - - - - - - - - - - - diff --git a/src/network/html/FilesPageHeader.html b/src/network/html/FilesPageHeader.html deleted file mode 100644 index 7ebfc88..0000000 --- a/src/network/html/FilesPageHeader.html +++ /dev/null @@ -1,472 +0,0 @@ - - - - - - CrossPoint Reader - Files - - - - - - diff --git a/src/network/html/HomePage.html b/src/network/html/HomePage.html index 221f069..a7fca67 100644 --- a/src/network/html/HomePage.html +++ b/src/network/html/HomePage.html @@ -107,7 +107,7 @@