first commit
This commit is contained in:
243
app/assets/script.js
Normal file
243
app/assets/script.js
Normal file
@@ -0,0 +1,243 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
let AppConfig = null;
|
||||
let UI = {};
|
||||
|
||||
async function appLoop() {
|
||||
if (AppConfig === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateUI();
|
||||
fetchFiles();
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
if (AppConfig.Modes.Readonly) {
|
||||
UI.dropzone.style.display = "none";
|
||||
} else {
|
||||
UI.dropzone.style.display = "block";
|
||||
}
|
||||
|
||||
if (AppConfig.Modes.Sinkhole) {
|
||||
UI.fileList.style.display = "none";
|
||||
UI.sinkholeModeInfo.style.display = "block";
|
||||
} else {
|
||||
UI.fileList.style.display = "block";
|
||||
UI.sinkholeModeInfo.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
async function initApp() {
|
||||
UI.currentFileName = document.getElementById("currentFileName");
|
||||
UI.dropzone = document.getElementById("dropzone");
|
||||
UI.fileInput = document.getElementById("fileInput");
|
||||
UI.fileList = document.getElementById("file-list");
|
||||
UI.overallProgress = document.getElementById("overallProgress");
|
||||
UI.overallStatus = document.getElementById("overallStatus");
|
||||
UI.overallProgressContainer = document.getElementById(
|
||||
"overallProgressContainer"
|
||||
);
|
||||
UI.sinkholeModeInfo = document.getElementById("sinkholeModeInfo");
|
||||
|
||||
UI.dropzone.addEventListener("click", () => UI.fileInput.click());
|
||||
UI.fileInput.addEventListener("change", () => {
|
||||
if (UI.fileInput.files.length > 0) uploadFiles(UI.fileInput.files);
|
||||
});
|
||||
UI.dropzone.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
UI.dropzone.style.borderColor = "#0fff50";
|
||||
});
|
||||
UI.dropzone.addEventListener("dragleave", () => {
|
||||
UI.dropzone.style.borderColor = "#888";
|
||||
});
|
||||
UI.dropzone.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
UI.dropzone.style.borderColor = "#888";
|
||||
if (e.dataTransfer.files.length > 0) uploadFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
await loadAppConfig();
|
||||
appLoop();
|
||||
|
||||
setInterval(appLoop, 5 * 1000);
|
||||
setInterval(loadAppConfig, 60 * 1000);
|
||||
}
|
||||
|
||||
async function loadAppConfig() {
|
||||
try {
|
||||
const res = await fetch("/config/", { cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
console.error("HTTP error:", res.status);
|
||||
}
|
||||
AppConfig = await res.json();
|
||||
} catch (err) {
|
||||
console.error("Failed to load config:", err);
|
||||
AppConfig = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFiles() {
|
||||
if (AppConfig.Modes.Sinkhole) {
|
||||
UI.fileList.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(AppConfig.Endpoints.Files, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
const files = await res.json();
|
||||
|
||||
if (!UI.fileList) return;
|
||||
UI.fileList.innerHTML = "";
|
||||
files.forEach((file) => {
|
||||
const size = humanReadableSize(file.Size);
|
||||
|
||||
const li = document.createElement("li");
|
||||
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.className = "download-link";
|
||||
downloadLink.href = AppConfig.Endpoints.FilesGet.replace(
|
||||
":filename",
|
||||
encodeURIComponent(file.Name)
|
||||
);
|
||||
downloadLink.textContent = `${file.Name} (${size})`;
|
||||
|
||||
li.appendChild(downloadLink);
|
||||
|
||||
if (!AppConfig.Modes.Readonly) {
|
||||
const deleteLink = document.createElement("a");
|
||||
deleteLink.className = "delete-link";
|
||||
deleteLink.href = "#";
|
||||
deleteLink.textContent = " [Delete]";
|
||||
deleteLink.title = "Delete file";
|
||||
deleteLink.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
if (!confirm(`Do you really want to delete "${file.Name}"?`))
|
||||
return;
|
||||
try {
|
||||
const r = await fetch(
|
||||
AppConfig.Endpoints.FilesDelete.replace(
|
||||
":filename",
|
||||
encodeURIComponent(file.Name)
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
if (!r.ok) throw new Error("Delete failed " + r.status);
|
||||
fetchFiles();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
li.appendChild(deleteLink);
|
||||
}
|
||||
|
||||
UI.fileList.appendChild(li);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("fetchFiles failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function humanReadableSize(bytes) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let i = 0;
|
||||
while (bytes >= 1024 && i < units.length - 1) {
|
||||
bytes /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${bytes.toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
function humanReadableSpeed(bytesPerSec) {
|
||||
if (!isFinite(bytesPerSec) || bytesPerSec <= 0) return "—";
|
||||
if (bytesPerSec < 1024) return bytesPerSec.toFixed(0) + " B/s";
|
||||
if (bytesPerSec < 1024 * 1024)
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s";
|
||||
}
|
||||
|
||||
function uploadFiles(fileListLike) {
|
||||
const files = Array.from(fileListLike);
|
||||
if (files.length === 0) return;
|
||||
|
||||
UI.overallProgressContainer.style.display = "block";
|
||||
UI.overallProgress.value = 0;
|
||||
UI.overallStatus.textContent = "";
|
||||
UI.currentFileName.textContent = "";
|
||||
|
||||
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
||||
let uploadedBytes = 0;
|
||||
const t0 = Date.now();
|
||||
let idx = 0;
|
||||
|
||||
const uploadNext = () => {
|
||||
if (idx >= files.length) {
|
||||
UI.overallProgressContainer.style.display = "none";
|
||||
UI.overallProgress.value = 0;
|
||||
UI.overallStatus.textContent = "";
|
||||
UI.currentFileName.textContent = "";
|
||||
fetchFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files[idx];
|
||||
UI.currentFileName.textContent = file.name;
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
const form = new FormData();
|
||||
form.append("uploadfile", file);
|
||||
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (!e.lengthComputable) return;
|
||||
|
||||
const totalUploaded = uploadedBytes + e.loaded;
|
||||
const percent = (totalUploaded / totalSize) * 100;
|
||||
UI.overallProgress.value = percent;
|
||||
|
||||
const elapsed = (Date.now() - t0) / 1000;
|
||||
const speed = totalUploaded / elapsed;
|
||||
const speedStr = humanReadableSpeed(speed);
|
||||
|
||||
const remainingBytes = totalSize - totalUploaded;
|
||||
const etaSec = speed > 0 ? remainingBytes / speed : Infinity;
|
||||
const min = Math.floor(etaSec / 60);
|
||||
const sec = Math.floor(etaSec % 60);
|
||||
|
||||
UI.overallStatus.textContent =
|
||||
`${percent.toFixed(1)}% (${(totalSize / 1024 / 1024).toFixed(
|
||||
1
|
||||
)} MB total) — ` +
|
||||
`Speed: ${speedStr}, Est. time left: ${
|
||||
isFinite(etaSec) ? `${min}m ${sec}s` : "calculating…"
|
||||
}`;
|
||||
});
|
||||
|
||||
xhr.addEventListener("load", () => {
|
||||
if (xhr.status === 200) {
|
||||
uploadedBytes += file.size;
|
||||
} else {
|
||||
console.error("Upload failed with status", xhr.status);
|
||||
}
|
||||
idx++;
|
||||
uploadNext();
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", () => {
|
||||
console.error("Network/server error during upload.");
|
||||
idx++;
|
||||
uploadNext();
|
||||
});
|
||||
|
||||
xhr.open("POST", AppConfig.Endpoints.Upload);
|
||||
xhr.send(form);
|
||||
};
|
||||
|
||||
fetchFiles();
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", initApp);
|
||||
})();
|
Reference in New Issue
Block a user