added user feedback for actions like uploading or deleting

This commit is contained in:
2025-08-28 23:36:50 +02:00
parent c8af56f1dc
commit b6b4b720df
2 changed files with 120 additions and 6 deletions

View File

@@ -2,7 +2,9 @@
"use strict"; "use strict";
let AppConfig = null; let AppConfig = null;
let UI = {}; let ErrorTimeout = null;
let Files = {};
const UI = {};
async function appLoop() { async function appLoop() {
if (AppConfig === null) { if (AppConfig === null) {
@@ -65,9 +67,13 @@
if (!res.ok) throw new Error("HTTP " + res.status); if (!res.ok) throw new Error("HTTP " + res.status);
const files = await res.json(); const files = await res.json();
Files = {};
if (!UI.fileList) return; if (!UI.fileList) return;
UI.fileList.innerHTML = ""; UI.fileList.innerHTML = "";
files.forEach((file) => { files.forEach((file) => {
Files[file.Name] = true;
const size = humanReadableSize(file.Size); const size = humanReadableSize(file.Size);
const li = document.createElement("li"); const li = document.createElement("li");
@@ -100,7 +106,11 @@
), ),
{ method: "GET" } { method: "GET" }
); );
if (!r.ok) throw new Error("Delete failed " + r.status); if (r.ok) {
showSuccess("File deleted");
} else {
showError("Delete failed");
}
fetchFiles(); fetchFiles();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -148,6 +158,7 @@
document.body.appendChild(aLogo); document.body.appendChild(aLogo);
const divDropzone = document.createElement("div"); const divDropzone = document.createElement("div");
divDropzone.className = "dropzone";
divDropzone.id = "dropzone"; divDropzone.id = "dropzone";
divDropzone.innerHTML = "Drag & drop files here or click to select"; divDropzone.innerHTML = "Drag & drop files here or click to select";
divDropzone.style.display = "none"; divDropzone.style.display = "none";
@@ -222,10 +233,91 @@
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s"; return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s";
} }
function sanitizeFilename(dirtyFilename) {
if (!dirtyFilename || dirtyFilename.trim() === "") {
return "upload.bin";
}
const filenameWithoutPath = dirtyFilename.split(/[\\/]/).pop();
const lastDot = filenameWithoutPath.lastIndexOf(".");
const extension = lastDot !== -1 ? filenameWithoutPath.slice(lastDot) : "";
let filenameWithoutPathAndExtension =
lastDot !== -1
? filenameWithoutPath.slice(0, lastDot)
: filenameWithoutPath;
let cleanedFilename = filenameWithoutPathAndExtension
.replace(/ /g, "_")
.replace(/Ä/g, "Ae")
.replace(/ä/g, "ae")
.replace(/Ö/g, "Oe")
.replace(/ö/g, "oe")
.replace(/Ü/g, "Ue")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss");
cleanedFilename = cleanedFilename.replace(/[^a-zA-Z0-9._-]+/g, "_");
while (cleanedFilename.includes("__")) {
cleanedFilename = cleanedFilename.replace(/__+/g, "_");
}
cleanedFilename = cleanedFilename.replace(/^_+|_+$/g, "");
const maxLenFilename = 128;
if (cleanedFilename.length > maxLenFilename) {
cleanedFilename = cleanedFilename.slice(0, maxLenFilename);
}
return cleanedFilename + extension;
}
function showError(msg) {
const original = "Drag & drop files here or click to select";
UI.dropzone.innerHTML = msg;
UI.dropzone.classList.add("error");
if (ErrorTimeout) clearTimeout(ErrorTimeout);
ErrorTimeout = setTimeout(() => {
UI.dropzone.innerHTML = original;
UI.dropzone.classList.remove("error");
ErrorTimeout = null;
}, 2000);
}
function showSuccess(msg) {
const original = "Drag & drop files here or click to select";
UI.dropzone.innerHTML = msg;
UI.dropzone.classList.add("success");
if (ErrorTimeout) clearTimeout(ErrorTimeout);
ErrorTimeout = setTimeout(() => {
UI.dropzone.innerHTML = original;
UI.dropzone.classList.remove("success");
ErrorTimeout = null;
}, 1500);
}
function uploadFiles(fileListLike) { function uploadFiles(fileListLike) {
const files = Array.from(fileListLike); const files = Array.from(fileListLike);
if (files.length === 0) return; if (files.length === 0) return;
for (const f of files) {
if (sanitizeFilename(f.name) == ".upload") {
showError("Invalid filename: .upload");
return;
}
if (sanitizeFilename(f.name) in Files) {
showError("File already exists: " + f.name);
return;
}
}
let noErrorOccurred = true;
UI.overallProgressContainer.style.display = "block"; UI.overallProgressContainer.style.display = "block";
UI.overallProgress.value = 0; UI.overallProgress.value = 0;
UI.overallStatus.textContent = ""; UI.overallStatus.textContent = "";
@@ -243,6 +335,9 @@
UI.overallStatus.textContent = ""; UI.overallStatus.textContent = "";
UI.currentFileName.textContent = ""; UI.currentFileName.textContent = "";
fetchFiles(); fetchFiles();
if (noErrorOccurred) {
showSuccess("Upload successful");
}
return; return;
} }
@@ -282,14 +377,21 @@
if (xhr.status === 200) { if (xhr.status === 200) {
uploadedBytes += file.size; uploadedBytes += file.size;
} else { } else {
console.error("Upload failed with status", xhr.status); if (xhr.status === 409) {
showError("File already exists: " + file.name);
noErrorOccurred = false;
} else {
showError("Upload failed: " + file.name);
noErrorOccurred = false;
}
} }
idx++; idx++;
uploadNext(); uploadNext();
}); });
xhr.addEventListener("error", () => { xhr.addEventListener("error", () => {
console.error("Network/server error during upload."); showError("Network or server error during upload.");
noErrorOccurred = false;
idx++; idx++;
uploadNext(); uploadNext();
}); });

View File

@@ -9,7 +9,7 @@ body {
} }
/* Dropzone */ /* Dropzone */
#dropzone { .dropzone {
border: 2px dashed #888; border: 2px dashed #888;
border-radius: 10px; border-radius: 10px;
color: #fefefe; color: #fefefe;
@@ -20,10 +20,22 @@ body {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
#dropzone:hover { .dropzone:hover {
color: #0fff50; color: #0fff50;
} }
.dropzone.error {
border: 2px solid #ff4d4d;
color: #ff4d4d;
font-weight: bold;
}
.dropzone.success {
border: 2px solid #0fff50;
color: #0fff50;
font-weight: bold;
}
/* File list */ /* File list */
#file-list { #file-list {
list-style: none; list-style: none;