From e384bdbfc2613d567ac1d9fe205db9eb2c8c8187 Mon Sep 17 00:00:00 2001 From: Brendan O'Leary Date: Tue, 16 Dec 2025 20:18:06 -0500 Subject: [PATCH] Hide hidden folders --- src/CrossPointWebServer.cpp | 367 ++++++++++++++++++++++++++++++++---- src/CrossPointWebServer.h | 2 + 2 files changed, 337 insertions(+), 32 deletions(-) diff --git a/src/CrossPointWebServer.cpp b/src/CrossPointWebServer.cpp index e6bb4c6..e94caaa 100644 --- a/src/CrossPointWebServer.cpp +++ b/src/CrossPointWebServer.cpp @@ -9,6 +9,14 @@ // Global instance CrossPointWebServer crossPointWebServer; +// Folders/files to hide from the web interface file browser +// Note: Items starting with "." are automatically hidden +static const char* HIDDEN_ITEMS[] = { + "System Volume Information", + "XTCache" +}; +static const size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]); + // HTML page template static const char* HTML_PAGE = R"rawliteral( @@ -192,6 +200,12 @@ static const char* FILES_PAGE_HEADER = R"rawliteral( .epub-file:hover { background-color: #d4edda !important; } + .folder-row { + background-color: #fff9e6 !important; + } + .folder-row:hover { + background-color: #fff3cd !important; + } .epub-badge { display: inline-block; padding: 2px 8px; @@ -201,9 +215,44 @@ static const char* FILES_PAGE_HEADER = R"rawliteral( font-size: 0.75em; margin-left: 8px; } + .folder-badge { + display: inline-block; + padding: 2px 8px; + background-color: #f39c12; + color: white; + border-radius: 10px; + font-size: 0.75em; + margin-left: 8px; + } .file-icon { margin-right: 8px; } + .folder-link { + color: #2c3e50; + text-decoration: none; + cursor: pointer; + } + .folder-link:hover { + color: #3498db; + text-decoration: underline; + } + .breadcrumb { + padding: 10px 15px; + background-color: #f8f9fa; + border-radius: 4px; + margin-bottom: 15px; + } + .breadcrumb a { + color: #3498db; + text-decoration: none; + } + .breadcrumb a:hover { + text-decoration: underline; + } + .breadcrumb span { + color: #7f8c8d; + margin: 0 5px; + } .upload-form { margin-top: 15px; padding: 15px; @@ -298,6 +347,30 @@ static const char* FILES_PAGE_HEADER = R"rawliteral( font-size: 0.9em; color: #7f8c8d; } + .folder-form { + display: flex; + gap: 10px; + margin-top: 15px; + } + .folder-input { + flex: 1; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1em; + } + .folder-btn { + background-color: #f39c12; + color: white; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1em; + } + .folder-btn:hover { + background-color: #d68910; + } @@ -339,6 +412,7 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral( function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; + const currentPath = document.getElementById('currentPath').value; if (!file) { alert('Please select a file first!'); @@ -353,6 +427,7 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral( const formData = new FormData(); formData.append('file', file); + formData.append('path', currentPath); const progressContainer = document.getElementById('progress-container'); const progressFill = document.getElementById('progress-fill'); @@ -394,6 +469,44 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral( xhr.send(formData); } + + function createFolder() { + const folderName = document.getElementById('folderName').value.trim(); + const currentPath = document.getElementById('currentPath').value; + + if (!folderName) { + alert('Please enter a folder name!'); + return; + } + + // Validate folder name (no special characters except underscore and hyphen) + const validName = /^[a-zA-Z0-9_\-]+$/.test(folderName); + if (!validName) { + alert('Folder name can only contain letters, numbers, underscores, and hyphens.'); + return; + } + + const formData = new FormData(); + formData.append('name', folderName); + formData.append('path', currentPath); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/mkdir', true); + + xhr.onload = function() { + if (xhr.status === 200) { + window.location.reload(); + } else { + alert('Failed to create folder: ' + xhr.responseText); + } + }; + + xhr.onerror = function() { + alert('Failed to create folder - network error'); + }; + + xhr.send(formData); + } @@ -433,6 +546,9 @@ void CrossPointWebServer::begin() { // Upload endpoint with special handling for multipart form data server->on("/upload", HTTP_POST, [this]() { handleUploadPost(); }, [this]() { handleUpload(); }); + // Create folder endpoint + server->on("/mkdir", HTTP_POST, [this]() { handleCreateFolder(); }); + server->onNotFound([this]() { handleNotFound(); }); server->begin(); @@ -516,19 +632,43 @@ std::vector CrossPointWebServer::scanFiles(const char* path) { File file = root.openNextFile(); while (file) { - if (!file.isDirectory()) { + String fileName = String(file.name()); + + // Skip hidden items (starting with ".") + bool shouldHide = fileName.startsWith("."); + + // Check against explicitly hidden items list + if (!shouldHide) { + for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) { + if (fileName.equals(HIDDEN_ITEMS[i])) { + shouldHide = true; + break; + } + } + } + + if (!shouldHide) { FileInfo info; - info.name = String(file.name()); - info.size = file.size(); - info.isEpub = isEpubFile(info.name); + info.name = fileName; + info.isDirectory = file.isDirectory(); + + if (info.isDirectory) { + info.size = 0; + info.isEpub = false; + } else { + 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()); + Serial.printf("[%lu] [WEB] Found %d items (files and folders)\n", millis(), files.size()); return files; } @@ -551,6 +691,20 @@ bool CrossPointWebServer::isEpubFile(const String& filename) { void CrossPointWebServer::handleFileList() { String html = FILES_PAGE_HEADER; + // Get current path from query string (default to root) + String currentPath = "/"; + if (server->hasArg("path")) { + currentPath = server->arg("path"); + // Ensure path starts with / + if (!currentPath.startsWith("/")) { + currentPath = "/" + currentPath; + } + // Remove trailing slash unless it's root + if (currentPath.length() > 1 && currentPath.endsWith("/")) { + currentPath = currentPath.substring(0, currentPath.length() - 1); + } + } + // Get message from query string if present if (server->hasArg("msg")) { String msg = server->arg("msg"); @@ -558,67 +712,146 @@ void CrossPointWebServer::handleFileList() { html += "
" + msg + "
"; } + // Hidden input to store current path for JavaScript + html += ""; + + // Breadcrumb navigation + html += "
"; + html += "
"; + html += "🏠 Root"; + + if (currentPath != "/") { + String pathParts = currentPath.substring(1); // Remove leading / + String buildPath = ""; + int start = 0; + int end = pathParts.indexOf('/'); + + while (start < pathParts.length()) { + String part; + if (end == -1) { + part = pathParts.substring(start); + buildPath += "/" + part; + html += "/" + part + ""; + break; + } else { + part = pathParts.substring(start, end); + buildPath += "/" + part; + html += "/" + part + ""; + start = end + 1; + end = pathParts.indexOf('/', start); + } + } + } + html += "
"; + html += "
"; + // Upload form html += "
"; - html += "

📤 Upload eBook

"; + html += "

📤 Upload eBook to " + (currentPath == "/" ? "Root" : currentPath) + "

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

Select an .epub file to upload:

"; html += ""; - html += "
Only .epub files are accepted
"; + html += "
Only .epub files are accepted. File will be uploaded to: " + currentPath + "
"; html += ""; html += "
"; html += "
"; html += "
"; html += "
"; html += "
"; + + // Create folder form + html += "
"; + html += ""; + html += ""; + html += "
"; html += "
"; - // Scan files - std::vector files = scanFiles("/"); + // Scan files in current path + std::vector files = scanFiles(currentPath.c_str()); - // Count epub files + // Count items int epubCount = 0; + int folderCount = 0; size_t totalSize = 0; for (const auto& file : files) { - if (file.isEpub) epubCount++; - totalSize += file.size; + if (file.isDirectory) { + folderCount++; + } else { + if (file.isEpub) epubCount++; + totalSize += file.size; + } } // File listing html += "
"; - html += "

📁 Files on SD Card

"; + html += "

📁 Contents of " + (currentPath == "/" ? "Root" : currentPath) + "

"; // Summary html += "
"; - html += "
" + String(files.size()) + "
Total Files
"; + html += "
" + String(folderCount) + "
Folders
"; + html += "
" + String(files.size() - folderCount) + "
Files
"; html += "
" + String(epubCount) + "
eBooks
"; html += "
" + formatFileSize(totalSize) + "
Total Size
"; html += "
"; if (files.empty()) { - html += "
No files found on SD card
"; + html += "
This folder is empty
"; } else { html += ""; - html += ""; + html += ""; - // Sort files: epub files first, then alphabetically + // 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) { - if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub; + // Folders come first + if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory; + // Then sort by epub status (epubs first among files) + if (!a.isDirectory && !b.isDirectory) { + if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub; + } + // Then alphabetically 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(); + String rowClass; + String icon; + String badge; + String typeStr; + String sizeStr; - html += ""; - html += ""; - html += ""; - html += ""; - html += ""; + 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; + + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + } 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); + + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + } } html += "
FilenameTypeSize
NameTypeSize
" + icon + "" + file.name + badge + "" + ext + "" + formatFileSize(file.size) + "
" + icon + ""; + html += "" + file.name + "" + badge + "" + typeStr + "" + sizeStr + "
" + icon + "" + file.name + badge + "" + typeStr + "" + sizeStr + "
"; @@ -629,12 +862,13 @@ void CrossPointWebServer::handleFileList() { html += FILES_PAGE_FOOTER; server->send(200, "text/html", html); - Serial.printf("[%lu] [WEB] Served file listing page\n", millis()); + Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str()); } // Static variables for upload handling static File uploadFile; static String uploadFileName; +static String uploadPath = "/"; static size_t uploadSize = 0; static bool uploadSuccess = false; static String uploadError = ""; @@ -648,7 +882,22 @@ void CrossPointWebServer::handleUpload() { uploadSuccess = false; uploadError = ""; - Serial.printf("[%lu] [WEB] Upload start: %s\n", millis(), uploadFileName.c_str()); + // Get upload path from form data (defaults to root if not specified) + if (server->hasArg("path")) { + uploadPath = server->arg("path"); + // Ensure path starts with / + if (!uploadPath.startsWith("/")) { + uploadPath = "/" + uploadPath; + } + // Remove trailing slash unless it's root + if (uploadPath.length() > 1 && uploadPath.endsWith("/")) { + uploadPath = uploadPath.substring(0, uploadPath.length() - 1); + } + } else { + uploadPath = "/"; + } + + Serial.printf("[%lu] [WEB] Upload start: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str()); // Validate file extension if (!isEpubFile(uploadFileName)) { @@ -658,7 +907,9 @@ void CrossPointWebServer::handleUpload() { } // Create file path - String filePath = "/" + uploadFileName; + String filePath = uploadPath; + if (!filePath.endsWith("/")) filePath += "/"; + filePath += uploadFileName; // Check if file already exists if (SD.exists(filePath.c_str())) { @@ -702,7 +953,9 @@ void CrossPointWebServer::handleUpload() { if (uploadFile) { uploadFile.close(); // Try to delete the incomplete file - String filePath = "/" + uploadFileName; + String filePath = uploadPath; + if (!filePath.endsWith("/")) filePath += "/"; + filePath += uploadFileName; SD.remove(filePath.c_str()); } uploadError = "Upload aborted"; @@ -718,3 +971,53 @@ void CrossPointWebServer::handleUploadPost() { server->send(400, "text/plain", error); } } + +void CrossPointWebServer::handleCreateFolder() { + // Get folder name from form data + if (!server->hasArg("name")) { + server->send(400, "text/plain", "Missing folder name"); + return; + } + + String folderName = server->arg("name"); + + // Validate folder name + if (folderName.isEmpty()) { + server->send(400, "text/plain", "Folder name cannot be empty"); + return; + } + + // Get parent path + String parentPath = "/"; + if (server->hasArg("path")) { + parentPath = server->arg("path"); + if (!parentPath.startsWith("/")) { + parentPath = "/" + parentPath; + } + if (parentPath.length() > 1 && parentPath.endsWith("/")) { + parentPath = parentPath.substring(0, parentPath.length() - 1); + } + } + + // Build full folder path + String folderPath = parentPath; + if (!folderPath.endsWith("/")) folderPath += "/"; + folderPath += folderName; + + Serial.printf("[%lu] [WEB] Creating folder: %s\n", millis(), folderPath.c_str()); + + // Check if already exists + if (SD.exists(folderPath.c_str())) { + server->send(400, "text/plain", "Folder already exists"); + return; + } + + // Create the folder + if (SD.mkdir(folderPath.c_str())) { + Serial.printf("[%lu] [WEB] Folder created successfully: %s\n", millis(), folderPath.c_str()); + server->send(200, "text/plain", "Folder created: " + folderName); + } else { + Serial.printf("[%lu] [WEB] Failed to create folder: %s\n", millis(), folderPath.c_str()); + server->send(500, "text/plain", "Failed to create folder"); + } +} diff --git a/src/CrossPointWebServer.h b/src/CrossPointWebServer.h index 7ff49b3..cce551a 100644 --- a/src/CrossPointWebServer.h +++ b/src/CrossPointWebServer.h @@ -10,6 +10,7 @@ struct FileInfo { String name; size_t size; bool isEpub; + bool isDirectory; }; class CrossPointWebServer { @@ -49,6 +50,7 @@ class CrossPointWebServer { void handleFileList(); void handleUpload(); void handleUploadPost(); + void handleCreateFolder(); }; // Global instance