Files
downloadwf/src/public/app.js
T
2026-08-15 00:28:32 +02:00

397 lines
10 KiB
JavaScript

"use strict";
const $ = (id) => document.getElementById(id);
const STORAGE_KEY = "filehost.uploads";
const dropzone = $("dropzone");
const fileInput = $("fileInput");
const pwInput = $("pwInput");
const progress = $("progress");
const progressBar = $("progressBar");
const progressPct = $("progressPct");
const progressName = $("progressName");
const result = $("result");
const resultUrl = $("resultUrl");
const resultName = $("resultName");
const resultSize = $("resultSize");
const resultLock = $("resultLock");
const copyBtn = $("copyBtn");
const deleteBtn = $("deleteBtn");
const againBtn = $("againBtn");
const notice = $("notice");
const fileListWrap = $("fileListWrap");
const fileList = $("fileList");
const clearListBtn = $("clearListBtn");
let current = null;
let uploading = false;
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB", "TB"];
let value = bytes;
let i = -1;
do {
value /= 1024;
i += 1;
} while (value >= 1024 && i < units.length - 1);
return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[i]}`;
}
function showNotice(message, kind) {
notice.textContent = message;
notice.className = kind;
}
function clearNotice() {
notice.className = "hidden";
notice.textContent = "";
}
function setProgress(loaded, total) {
const pct = total > 0 ? Math.round((loaded / total) * 100) : 0;
progressBar.style.width = `${pct}%`;
progressPct.textContent = `${pct}%`;
}
function reset() {
uploading = false;
current = null;
result.classList.add("hidden");
resultLock.classList.add("hidden");
progress.classList.add("hidden");
progressBar.style.width = "0%";
progressPct.textContent = "0%";
fileInput.value = "";
pwInput.value = "";
clearNotice();
}
// ---- client-side upload history (localStorage) ----
function loadUploads() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function saveUploads(list) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
} catch {
/* storage unavailable (e.g. private mode) — ignore */
}
}
function addUpload(record) {
const list = loadUploads();
const idx = list.findIndex((item) => item.id === record.id);
if (idx !== -1) list[idx] = record;
else list.unshift(record);
saveUploads(list);
renderList();
}
function removeUpload(id) {
saveUploads(loadUploads().filter((item) => item.id !== id));
renderList();
}
function downloadUrl(item) {
return new URL(item.url, window.location.origin).href;
}
async function copyText(text) {
const fallback = () => {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch {
ok = false;
}
ta.remove();
return ok;
};
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return fallback();
}
}
return fallback();
}
function setCopyFeedback(btn, label) {
const original = btn.textContent;
btn.textContent = label;
setTimeout(() => {
btn.textContent = original;
}, 1500);
}
function renderList() {
const list = loadUploads();
if (list.length === 0) {
fileListWrap.classList.add("hidden");
fileList.innerHTML = "";
return;
}
fileListWrap.classList.remove("hidden");
fileList.innerHTML = "";
for (const item of list) {
const li = document.createElement("li");
li.className = "file-item";
const meta = document.createElement("div");
meta.className = "file-item-meta";
const name = document.createElement("span");
name.className = "file-item-name";
name.textContent = item.originalName;
name.title = item.originalName;
if (item.protected) {
const lock = document.createElement("span");
lock.className = "lock";
lock.textContent = "Password protected";
name.appendChild(lock);
}
const sub = document.createElement("span");
sub.className = "file-item-sub";
let subText = formatBytes(item.size || 0);
if (item.createdAt) {
try {
subText += ` \u00b7 ${new Date(item.createdAt).toLocaleString()}`;
} catch {
/* ignore date formatting */
}
}
sub.textContent = subText;
meta.appendChild(name);
meta.appendChild(sub);
const actions = document.createElement("div");
actions.className = "file-item-actions";
const dl = document.createElement("a");
dl.className = "btn-link";
dl.href = downloadUrl(item);
dl.textContent = "Download";
dl.title = "Open download link";
const copy = document.createElement("button");
copy.type = "button";
copy.textContent = "Copy";
copy.addEventListener("click", async () => {
const ok = await copyText(downloadUrl(item));
setCopyFeedback(copy, ok ? "Copied" : "Copy failed");
});
const del = document.createElement("button");
del.type = "button";
del.className = "danger";
del.textContent = "Delete";
del.addEventListener("click", () => deleteFile(item, () => removeUpload(item.id)));
actions.appendChild(dl);
actions.appendChild(copy);
actions.appendChild(del);
li.appendChild(meta);
li.appendChild(actions);
fileList.appendChild(li);
}
}
async function deleteFile(item, onSuccess) {
const ok = window.confirm(`Delete "${item.originalName}" permanently? This cannot be undone.`);
if (!ok) return;
try {
const res = await fetch(`/api/files/${item.id}`, {
method: "DELETE",
headers: { "X-Delete-Key": item.key },
});
if (res.ok) {
onSuccess();
showNotice(`"${item.originalName}" was deleted.`, "info");
} else {
let message = "Deletion failed.";
try {
const data = await res.json();
if (data && data.error) message = data.error;
} catch {
/* keep fallback message */
}
showNotice(message, "error");
}
} catch {
showNotice("Deletion failed: network error.", "error");
}
}
// ---- upload flow ----
function showResult(data) {
progress.classList.add("hidden");
result.classList.remove("hidden");
resultName.textContent = data.originalName;
resultSize.textContent = formatBytes(data.size);
resultUrl.value = downloadUrl(data);
resultLock.classList.toggle("hidden", !data.protected);
current = data;
addUpload({
id: data.id,
key: data.key,
url: data.url,
originalName: data.originalName,
size: data.size,
protected: !!data.protected,
createdAt: new Date().toISOString(),
});
}
function startUpload(file) {
if (uploading) return;
clearNotice();
progress.classList.remove("hidden");
progressName.textContent = file.name;
setProgress(0, file.size);
uploading = true;
const xhr = new XMLHttpRequest();
const form = new FormData();
if (pwInput.value) form.append("password", pwInput.value);
form.append("file", file);
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) setProgress(e.loaded, e.total);
});
xhr.addEventListener("load", () => {
uploading = false;
if (xhr.status >= 200 && xhr.status < 300) {
try {
showResult(JSON.parse(xhr.responseText));
} catch {
progress.classList.add("hidden");
showNotice("Invalid server response.", "error");
}
return;
}
progress.classList.add("hidden");
let message = `Upload failed (HTTP ${xhr.status})`;
try {
const data = JSON.parse(xhr.responseText);
if (data && data.error) message = data.error;
} catch {
/* keep fallback message */
}
showNotice(message, "error");
});
xhr.addEventListener("error", () => {
uploading = false;
progress.classList.add("hidden");
showNotice("Network error during upload.", "error");
});
xhr.addEventListener("abort", () => {
uploading = false;
progress.classList.add("hidden");
});
xhr.open("POST", "/api/upload");
xhr.send(form);
}
function handleFile(file) {
if (!file) return;
if (file.size === 0) {
showNotice("Empty files are not supported.", "error");
return;
}
startUpload(file);
}
dropzone.addEventListener("click", () => {
if (!uploading) fileInput.click();
});
dropzone.addEventListener("keydown", (e) => {
if ((e.key === "Enter" || e.key === " ") && !uploading) {
e.preventDefault();
fileInput.click();
}
});
fileInput.addEventListener("change", () => {
const file = fileInput.files ? fileInput.files[0] : null;
handleFile(file);
});
let dragDepth = 0;
document.addEventListener("dragenter", (e) => {
e.preventDefault();
if (!uploading) {
dragDepth += 1;
dropzone.classList.add("drag");
}
});
document.addEventListener("dragover", (e) => e.preventDefault());
document.addEventListener("dragleave", (e) => {
e.preventDefault();
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) dropzone.classList.remove("drag");
});
document.addEventListener("drop", (e) => {
e.preventDefault();
dragDepth = 0;
dropzone.classList.remove("drag");
if (uploading) return;
const file = e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files[0] : null;
if (file) handleFile(file);
});
copyBtn.addEventListener("click", async () => {
const ok = await copyText(resultUrl.value);
setCopyFeedback(copyBtn, ok ? "Copied" : "Copy failed");
});
deleteBtn.addEventListener("click", () => {
if (!current) return;
deleteFile(current, () => {
removeUpload(current.id);
reset();
});
});
againBtn.addEventListener("click", reset);
clearListBtn.addEventListener("click", () => {
if (loadUploads().length === 0) return;
const ok = window.confirm(
"Clear this list? This only removes the entries from THIS browser. The uploaded files remain on the server.",
);
if (!ok) return;
saveUploads([]);
renderList();
showNotice("List cleared. Files on the server were not touched.", "info");
});
renderList();