mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2025-12-19 15:47:40 +03:00
Add delete key
This commit is contained in:
parent
78604d3bda
commit
2c79ea8705
@ -511,6 +511,63 @@ static const char* FILES_PAGE_HEADER = R"rawliteral(
|
|||||||
.folder-btn:hover {
|
.folder-btn:hover {
|
||||||
background-color: #d68910;
|
background-color: #d68910;
|
||||||
}
|
}
|
||||||
|
/* Delete button styles */
|
||||||
|
.delete-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #95a5a6;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
.actions-col {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
/* Delete modal */
|
||||||
|
.delete-warning {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.delete-item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.delete-btn-confirm {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.delete-btn-confirm:hover {
|
||||||
|
background-color: #c0392b;
|
||||||
|
}
|
||||||
|
.delete-btn-cancel {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.delete-btn-cancel:hover {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -557,6 +614,23 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div class="modal-overlay" id="deleteModal">
|
||||||
|
<div class="modal">
|
||||||
|
<button class="modal-close" onclick="closeDeleteModal()">×</button>
|
||||||
|
<h3>🗑️ Delete Item</h3>
|
||||||
|
<div class="folder-form">
|
||||||
|
<p class="delete-warning">⚠️ This action cannot be undone!</p>
|
||||||
|
<p class="file-info">Are you sure you want to delete:</p>
|
||||||
|
<p class="delete-item-name" id="deleteItemName"></p>
|
||||||
|
<input type="hidden" id="deleteItemPath">
|
||||||
|
<input type="hidden" id="deleteItemType">
|
||||||
|
<button class="delete-btn-confirm" onclick="confirmDelete()">Delete</button>
|
||||||
|
<button class="delete-btn-cancel" onclick="closeDeleteModal()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Dropdown toggle
|
// Dropdown toggle
|
||||||
function toggleDropdown() {
|
function toggleDropdown() {
|
||||||
@ -727,6 +801,46 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral(
|
|||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete functions
|
||||||
|
function openDeleteModal(name, path, isFolder) {
|
||||||
|
document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name;
|
||||||
|
document.getElementById('deleteItemPath').value = path;
|
||||||
|
document.getElementById('deleteItemType').value = isFolder ? 'folder' : 'file';
|
||||||
|
document.getElementById('deleteModal').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('deleteModal').classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete() {
|
||||||
|
const path = document.getElementById('deleteItemPath').value;
|
||||||
|
const itemType = document.getElementById('deleteItemType').value;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('path', path);
|
||||||
|
formData.append('type', itemType);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/delete', true);
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete: ' + xhr.responseText);
|
||||||
|
closeDeleteModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {
|
||||||
|
alert('Failed to delete - network error');
|
||||||
|
closeDeleteModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -769,6 +883,9 @@ void CrossPointWebServer::begin() {
|
|||||||
// Create folder endpoint
|
// Create folder endpoint
|
||||||
server->on("/mkdir", HTTP_POST, [this]() { handleCreateFolder(); });
|
server->on("/mkdir", HTTP_POST, [this]() { handleCreateFolder(); });
|
||||||
|
|
||||||
|
// Delete file/folder endpoint
|
||||||
|
server->on("/delete", HTTP_POST, [this]() { handleDelete(); });
|
||||||
|
|
||||||
server->onNotFound([this]() { handleNotFound(); });
|
server->onNotFound([this]() { handleNotFound(); });
|
||||||
|
|
||||||
server->begin();
|
server->begin();
|
||||||
@ -1023,7 +1140,7 @@ void CrossPointWebServer::handleFileList() {
|
|||||||
html += "<div class=\"no-files\">This folder is empty</div>";
|
html += "<div class=\"no-files\">This folder is empty</div>";
|
||||||
} else {
|
} else {
|
||||||
html += "<table class=\"file-table\">";
|
html += "<table class=\"file-table\">";
|
||||||
html += "<tr><th>Name</th><th>Type</th><th>Size</th></tr>";
|
html += "<tr><th>Name</th><th>Type</th><th>Size</th><th class=\"actions-col\">Actions</th></tr>";
|
||||||
|
|
||||||
// Sort files: folders first, then epub files, then other files, alphabetically within each group
|
// 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) {
|
std::sort(files.begin(), files.end(), [](const FileInfo& a, const FileInfo& b) {
|
||||||
@ -1058,9 +1175,15 @@ void CrossPointWebServer::handleFileList() {
|
|||||||
|
|
||||||
html += "<tr class=\"" + rowClass + "\">";
|
html += "<tr class=\"" + rowClass + "\">";
|
||||||
html += "<td><span class=\"file-icon\">" + icon + "</span>";
|
html += "<td><span class=\"file-icon\">" + icon + "</span>";
|
||||||
html += "<a href=\"/files?path=" + folderPath + "\" class=\"folder-link\">" + file.name + "</a>" + badge + "</td>";
|
html += "<a href=\"/files?path=" + folderPath + "\" class=\"folder-link\">" + escapeHtml(file.name) + "</a>" + badge + "</td>";
|
||||||
html += "<td>" + typeStr + "</td>";
|
html += "<td>" + typeStr + "</td>";
|
||||||
html += "<td>" + sizeStr + "</td>";
|
html += "<td>" + sizeStr + "</td>";
|
||||||
|
// Escape quotes for JavaScript string
|
||||||
|
String escapedName = file.name;
|
||||||
|
escapedName.replace("'", "\\'");
|
||||||
|
String escapedPath = folderPath;
|
||||||
|
escapedPath.replace("'", "\\'");
|
||||||
|
html += "<td class=\"actions-col\"><button class=\"delete-btn\" onclick=\"openDeleteModal('" + escapedName + "', '" + escapedPath + "', true)\" title=\"Delete folder\">🗑️</button></td>";
|
||||||
html += "</tr>";
|
html += "</tr>";
|
||||||
} else {
|
} else {
|
||||||
rowClass = file.isEpub ? "epub-file" : "";
|
rowClass = file.isEpub ? "epub-file" : "";
|
||||||
@ -1071,10 +1194,21 @@ void CrossPointWebServer::handleFileList() {
|
|||||||
typeStr = ext;
|
typeStr = ext;
|
||||||
sizeStr = formatFileSize(file.size);
|
sizeStr = formatFileSize(file.size);
|
||||||
|
|
||||||
|
// Build file path for delete
|
||||||
|
String filePath = currentPath;
|
||||||
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
|
filePath += file.name;
|
||||||
|
|
||||||
html += "<tr class=\"" + rowClass + "\">";
|
html += "<tr class=\"" + rowClass + "\">";
|
||||||
html += "<td><span class=\"file-icon\">" + icon + "</span>" + file.name + badge + "</td>";
|
html += "<td><span class=\"file-icon\">" + icon + "</span>" + escapeHtml(file.name) + badge + "</td>";
|
||||||
html += "<td>" + typeStr + "</td>";
|
html += "<td>" + typeStr + "</td>";
|
||||||
html += "<td>" + sizeStr + "</td>";
|
html += "<td>" + sizeStr + "</td>";
|
||||||
|
// Escape quotes for JavaScript string
|
||||||
|
String escapedName = file.name;
|
||||||
|
escapedName.replace("'", "\\'");
|
||||||
|
String escapedPath = filePath;
|
||||||
|
escapedPath.replace("'", "\\'");
|
||||||
|
html += "<td class=\"actions-col\"><button class=\"delete-btn\" onclick=\"openDeleteModal('" + escapedName + "', '" + escapedPath + "', false)\" title=\"Delete file\">🗑️</button></td>";
|
||||||
html += "</tr>";
|
html += "</tr>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1246,3 +1380,85 @@ void CrossPointWebServer::handleCreateFolder() {
|
|||||||
server->send(500, "text/plain", "Failed to create folder");
|
server->send(500, "text/plain", "Failed to create folder");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleDelete() {
|
||||||
|
// Get path from form data
|
||||||
|
if (!server->hasArg("path")) {
|
||||||
|
server->send(400, "text/plain", "Missing path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String itemPath = server->arg("path");
|
||||||
|
String itemType = server->hasArg("type") ? server->arg("type") : "file";
|
||||||
|
|
||||||
|
// Validate path
|
||||||
|
if (itemPath.isEmpty() || itemPath == "/") {
|
||||||
|
server->send(400, "text/plain", "Cannot delete root directory");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure path starts with /
|
||||||
|
if (!itemPath.startsWith("/")) {
|
||||||
|
itemPath = "/" + itemPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check: prevent deletion of protected items
|
||||||
|
String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
// Check if item starts with a dot (hidden/system file)
|
||||||
|
if (itemName.startsWith(".")) {
|
||||||
|
Serial.printf("[%lu] [WEB] Delete rejected - hidden/system item: %s\n", millis(), itemPath.c_str());
|
||||||
|
server->send(403, "text/plain", "Cannot delete system files");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against explicitly protected items
|
||||||
|
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
|
||||||
|
if (itemName.equals(HIDDEN_ITEMS[i])) {
|
||||||
|
Serial.printf("[%lu] [WEB] Delete rejected - protected item: %s\n", millis(), itemPath.c_str());
|
||||||
|
server->send(403, "text/plain", "Cannot delete protected items");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item exists
|
||||||
|
if (!SD.exists(itemPath.c_str())) {
|
||||||
|
Serial.printf("[%lu] [WEB] Delete failed - item not found: %s\n", millis(), itemPath.c_str());
|
||||||
|
server->send(404, "text/plain", "Item not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEB] Attempting to delete %s: %s\n", millis(), itemType.c_str(), itemPath.c_str());
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
if (itemType == "folder") {
|
||||||
|
// For folders, try to remove (will fail if not empty)
|
||||||
|
File dir = SD.open(itemPath.c_str());
|
||||||
|
if (dir && dir.isDirectory()) {
|
||||||
|
// Check if folder is empty
|
||||||
|
File entry = dir.openNextFile();
|
||||||
|
if (entry) {
|
||||||
|
// Folder is not empty
|
||||||
|
entry.close();
|
||||||
|
dir.close();
|
||||||
|
Serial.printf("[%lu] [WEB] Delete failed - folder not empty: %s\n", millis(), itemPath.c_str());
|
||||||
|
server->send(400, "text/plain", "Folder is not empty. Delete contents first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
}
|
||||||
|
success = SD.rmdir(itemPath.c_str());
|
||||||
|
} else {
|
||||||
|
// For files, use remove
|
||||||
|
success = SD.remove(itemPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Serial.printf("[%lu] [WEB] Successfully deleted: %s\n", millis(), itemPath.c_str());
|
||||||
|
server->send(200, "text/plain", "Deleted successfully");
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [WEB] Failed to delete: %s\n", millis(), itemPath.c_str());
|
||||||
|
server->send(500, "text/plain", "Failed to delete item");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -51,6 +51,7 @@ class CrossPointWebServer {
|
|||||||
void handleUpload();
|
void handleUpload();
|
||||||
void handleUploadPost();
|
void handleUploadPost();
|
||||||
void handleCreateFolder();
|
void handleCreateFolder();
|
||||||
|
void handleDelete();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global instance
|
// Global instance
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user