diff --git a/src/CrossPointWebServer.cpp b/src/CrossPointWebServer.cpp index 1e20fe8..e6bb4c6 100644 --- a/src/CrossPointWebServer.cpp +++ b/src/CrossPointWebServer.cpp @@ -1,6 +1,8 @@ #include "CrossPointWebServer.h" +#include #include +#include #include "config.h" @@ -18,7 +20,7 @@ static const char* HTML_PAGE = R"rawliteral(

📚 CrossPoint Reader

+ +

Device Status

@@ -92,15 +112,289 @@ static const char* HTML_PAGE = R"rawliteral(
-

File Management

-

📁 File upload functionality coming soon...

+

+ CrossPoint E-Reader • Open Source +

+ + +)rawliteral"; +// File listing page template +static const char* FILES_PAGE_HEADER = R"rawliteral( + + + + + + CrossPoint Reader - Files + + + +

📁 File Manager

+ + +)rawliteral"; + +static const char* FILES_PAGE_FOOTER = R"rawliteral(

CrossPoint E-Reader • Open Source

+ + )rawliteral"; @@ -134,6 +428,11 @@ void CrossPointWebServer::begin() { 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(); }); + + // Upload endpoint with special handling for multipart form data + server->on("/upload", HTTP_POST, [this]() { handleUploadPost(); }, [this]() { handleUpload(); }); + server->onNotFound([this]() { handleNotFound(); }); server->begin(); @@ -197,3 +496,225 @@ void CrossPointWebServer::handleStatus() { server->send(200, "application/json", json); } + +std::vector CrossPointWebServer::scanFiles(const char* path) { + std::vector files; + + File root = SD.open(path); + if (!root) { + Serial.printf("[%lu] [WEB] Failed to open directory: %s\n", millis(), path); + return files; + } + + if (!root.isDirectory()) { + Serial.printf("[%lu] [WEB] Not a directory: %s\n", millis(), path); + root.close(); + return files; + } + + Serial.printf("[%lu] [WEB] Scanning files in: %s\n", millis(), path); + + File file = root.openNextFile(); + while (file) { + if (!file.isDirectory()) { + FileInfo info; + info.name = String(file.name()); + info.size = file.size(); + info.isEpub = isEpubFile(info.name); + files.push_back(info); + } + file.close(); + file = root.openNextFile(); + } + root.close(); + + Serial.printf("[%lu] [WEB] Found %d files\n", millis(), files.size()); + return files; +} + +String CrossPointWebServer::formatFileSize(size_t bytes) { + if (bytes < 1024) { + return String(bytes) + " B"; + } else if (bytes < 1024 * 1024) { + return String(bytes / 1024.0, 1) + " KB"; + } else { + return String(bytes / (1024.0 * 1024.0), 1) + " MB"; + } +} + +bool CrossPointWebServer::isEpubFile(const String& filename) { + String lower = filename; + lower.toLowerCase(); + return lower.endsWith(".epub"); +} + +void CrossPointWebServer::handleFileList() { + String html = FILES_PAGE_HEADER; + + // Get message from query string if present + if (server->hasArg("msg")) { + String msg = server->arg("msg"); + String msgType = server->hasArg("type") ? server->arg("type") : "success"; + html += "
" + msg + "
"; + } + + // Upload form + html += "
"; + html += "

📤 Upload eBook

"; + html += "
"; + html += "

Select an .epub file to upload:

"; + html += ""; + html += "
Only .epub files are accepted
"; + html += ""; + html += "
"; + html += "
"; + html += "
"; + html += "
"; + html += "
"; + html += "
"; + + // Scan files + std::vector files = scanFiles("/"); + + // Count epub files + int epubCount = 0; + size_t totalSize = 0; + for (const auto& file : files) { + if (file.isEpub) epubCount++; + totalSize += file.size; + } + + // File listing + html += "
"; + html += "

📁 Files on SD Card

"; + + // Summary + html += "
"; + html += "
" + String(files.size()) + "
Total Files
"; + html += "
" + String(epubCount) + "
eBooks
"; + html += "
" + formatFileSize(totalSize) + "
Total Size
"; + html += "
"; + + if (files.empty()) { + html += "
No files found on SD card
"; + } else { + html += ""; + html += ""; + + // Sort files: epub files first, then alphabetically + std::sort(files.begin(), files.end(), [](const FileInfo& a, const FileInfo& b) { + if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub; + return a.name < b.name; + }); + + for (const auto& file : files) { + String rowClass = file.isEpub ? "epub-file" : ""; + String icon = file.isEpub ? "📗" : "📄"; + String badge = file.isEpub ? "EPUB" : ""; + String ext = file.name.substring(file.name.lastIndexOf('.') + 1); + ext.toUpperCase(); + + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + } + + html += "
FilenameTypeSize
" + icon + "" + file.name + badge + "" + ext + "" + formatFileSize(file.size) + "
"; + } + + html += "
"; + + html += FILES_PAGE_FOOTER; + + server->send(200, "text/html", html); + Serial.printf("[%lu] [WEB] Served file listing page\n", millis()); +} + +// Static variables for upload handling +static File uploadFile; +static String uploadFileName; +static size_t uploadSize = 0; +static bool uploadSuccess = false; +static String uploadError = ""; + +void CrossPointWebServer::handleUpload() { + HTTPUpload& upload = server->upload(); + + if (upload.status == UPLOAD_FILE_START) { + uploadFileName = upload.filename; + uploadSize = 0; + uploadSuccess = false; + uploadError = ""; + + Serial.printf("[%lu] [WEB] Upload start: %s\n", millis(), uploadFileName.c_str()); + + // Validate file extension + if (!isEpubFile(uploadFileName)) { + uploadError = "Only .epub files are allowed"; + Serial.printf("[%lu] [WEB] Upload rejected - not an epub file\n", millis()); + return; + } + + // Create file path + String filePath = "/" + uploadFileName; + + // Check if file already exists + if (SD.exists(filePath.c_str())) { + Serial.printf("[%lu] [WEB] Overwriting existing file: %s\n", millis(), filePath.c_str()); + SD.remove(filePath.c_str()); + } + + // Open file for writing + uploadFile = SD.open(filePath.c_str(), FILE_WRITE); + if (!uploadFile) { + uploadError = "Failed to create file on SD card"; + Serial.printf("[%lu] [WEB] Failed to create file: %s\n", millis(), filePath.c_str()); + return; + } + + Serial.printf("[%lu] [WEB] File created: %s\n", millis(), filePath.c_str()); + } + else if (upload.status == UPLOAD_FILE_WRITE) { + if (uploadFile && uploadError.isEmpty()) { + size_t written = uploadFile.write(upload.buf, upload.currentSize); + if (written != upload.currentSize) { + uploadError = "Failed to write to SD card - disk may be full"; + uploadFile.close(); + Serial.printf("[%lu] [WEB] Write error - expected %d, wrote %d\n", millis(), upload.currentSize, written); + } else { + uploadSize += written; + } + } + } + else if (upload.status == UPLOAD_FILE_END) { + if (uploadFile) { + uploadFile.close(); + + if (uploadError.isEmpty()) { + uploadSuccess = true; + Serial.printf("[%lu] [WEB] Upload complete: %s (%d bytes)\n", millis(), uploadFileName.c_str(), uploadSize); + } + } + } + else if (upload.status == UPLOAD_FILE_ABORTED) { + if (uploadFile) { + uploadFile.close(); + // Try to delete the incomplete file + String filePath = "/" + uploadFileName; + SD.remove(filePath.c_str()); + } + uploadError = "Upload aborted"; + Serial.printf("[%lu] [WEB] Upload aborted\n", millis()); + } +} + +void CrossPointWebServer::handleUploadPost() { + if (uploadSuccess) { + server->send(200, "text/plain", "File uploaded successfully: " + uploadFileName); + } else { + String error = uploadError.isEmpty() ? "Unknown error during upload" : uploadError; + server->send(400, "text/plain", error); + } +} diff --git a/src/CrossPointWebServer.h b/src/CrossPointWebServer.h index 531e9ff..7ff49b3 100644 --- a/src/CrossPointWebServer.h +++ b/src/CrossPointWebServer.h @@ -3,6 +3,14 @@ #include #include #include +#include + +// Structure to hold file information +struct FileInfo { + String name; + size_t size; + bool isEpub; +}; class CrossPointWebServer { public: @@ -29,10 +37,18 @@ class CrossPointWebServer { bool running = false; uint16_t port = 80; + // File scanning + std::vector scanFiles(const char* path = "/"); + String formatFileSize(size_t bytes); + bool isEpubFile(const String& filename); + // Request handlers void handleRoot(); void handleNotFound(); void handleStatus(); + void handleFileList(); + void handleUpload(); + void handleUploadPost(); }; // Global instance diff --git a/src/screens/WifiScreen.cpp b/src/screens/WifiScreen.cpp index 1912bd6..106575d 100644 --- a/src/screens/WifiScreen.cpp +++ b/src/screens/WifiScreen.cpp @@ -41,10 +41,12 @@ void WifiScreen::onExit() { // Stop any ongoing WiFi scan WiFi.scanDelete(); - // Don't turn off WiFi if connected - if (WiFi.status() != WL_CONNECTED) { - WiFi.mode(WIFI_OFF); - } + // Stop the web server to free memory + crossPointWebServer.stop(); + + // Disconnect WiFi to free memory + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY);