"use strict"; const $ = (id) => document.getElementById(id); const STORAGE_KEY = "filehost.uploads"; const dropzone = $("dropzone"); const fileInput = $("fileInput"); const chosenFile = $("chosenFile"); const pwInput = $("pwInput"); const uploadBtn = $("uploadBtn"); const newUploadBtn = $("newUploadBtn"); const clearListBtn = $("clearListBtn"); const progress = $("progress"); const progressBar = $("progressBar"); const progressPct = $("progressPct"); const progressName = $("progressName"); const result = $("result"); const resultUrl = $("resultUrl"); const resultName = $("resultName"); const copyBtn = $("copyBtn"); const notice = $("notice"); const fileList = $("fileList"); const fileCount = $("fileCount"); const emptyState = $("emptyState"); let currentFile = null; let uploading = false; function formatBytes(bytes) { if (!Number.isFinite(bytes) || bytes < 0) return "-"; 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; currentFile = null; result.classList.add("hidden"); progress.classList.add("hidden"); progressBar.style.width = "0%"; progressPct.textContent = "0%"; chosenFile.classList.add("hidden"); chosenFile.textContent = ""; fileInput.value = ""; pwInput.value = ""; uploadBtn.disabled = false; uploadBtn.textContent = "Upload"; clearNotice(); } // ---- client-side 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 — 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 formatDate(iso) { try { return new Date(iso).toLocaleString(); } catch { return ""; } } function renderList() { const list = loadUploads(); fileList.innerHTML = ""; emptyState.classList.toggle("hidden", list.length > 0); fileCount.textContent = list.length === 0 ? "" : `${list.length} file${list.length === 1 ? "" : "s"}`; for (const item of list) { const tr = document.createElement("tr"); const nameTd = document.createElement("td"); const nameWrap = document.createElement("div"); nameWrap.className = "file-name"; const name = document.createElement("span"); name.textContent = item.originalName; name.title = item.originalName; const lock = document.createElement("span"); lock.className = "lock"; lock.textContent = "Protected"; nameWrap.appendChild(name); nameWrap.appendChild(lock); nameTd.appendChild(nameWrap); const sizeTd = document.createElement("td"); sizeTd.className = "file-size"; sizeTd.textContent = formatBytes(item.size); const dateTd = document.createElement("td"); dateTd.className = "file-date"; dateTd.textContent = formatDate(item.createdAt); const actionsTd = document.createElement("td"); const actions = document.createElement("div"); actions.className = "row-actions"; const dl = document.createElement("a"); dl.className = "btn btn-primary"; dl.href = downloadUrl(item); dl.textContent = "Download"; dl.title = "Open download link"; const copy = document.createElement("button"); copy.type = "button"; copy.className = "btn btn-ghost"; copy.textContent = "Copy"; copy.addEventListener("click", async () => { const ok = await copyText(downloadUrl(item)); setCopyFeedback(copy, ok ? "Copied" : "Failed"); }); const del = document.createElement("button"); del.type = "button"; del.className = "btn btn-danger"; del.textContent = "Delete"; del.addEventListener("click", () => deleteFile(item, () => removeUpload(item.id))); actions.appendChild(dl); actions.appendChild(copy); actions.appendChild(del); actionsTd.appendChild(actions); tr.appendChild(nameTd); tr.appendChild(sizeTd); tr.appendChild(dateTd); tr.appendChild(actionsTd); fileList.appendChild(tr); } } 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; resultUrl.value = downloadUrl(data); addUpload({ id: data.id, key: data.key, url: data.url, originalName: data.originalName, size: data.size, createdAt: new Date().toISOString(), }); } function startUpload(file, password) { if (uploading) return; clearNotice(); progress.classList.remove("hidden"); progressName.textContent = file.name; setProgress(0, file.size); uploading = true; uploadBtn.disabled = true; uploadBtn.textContent = "Uploading\u2026"; const xhr = new XMLHttpRequest(); const form = new FormData(); form.append("password", password); form.append("file", file); xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) setProgress(e.loaded, e.total); }); xhr.addEventListener("load", () => { uploading = false; uploadBtn.disabled = false; uploadBtn.textContent = "Upload"; if (xhr.status >= 200 && xhr.status < 300) { progress.classList.add("hidden"); try { showResult(JSON.parse(xhr.responseText)); } catch { 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; uploadBtn.disabled = false; uploadBtn.textContent = "Upload"; progress.classList.add("hidden"); showNotice("Network error during upload.", "error"); }); xhr.addEventListener("abort", () => { uploading = false; uploadBtn.disabled = false; uploadBtn.textContent = "Upload"; progress.classList.add("hidden"); }); xhr.open("POST", "/api/upload"); xhr.send(form); } function selectFile(file) { if (!file) return; if (file.size === 0) { showNotice("Empty files are not supported.", "error"); return; } currentFile = file; chosenFile.textContent = file.name; chosenFile.classList.remove("hidden"); clearNotice(); } function submitUpload() { if (!currentFile) { showNotice("Choose a file first.", "error"); return; } const password = pwInput.value; if (!password) { showNotice("A password is required to protect this file.", "error"); pwInput.focus(); return; } startUpload(currentFile, password); } 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; selectFile(file); }); pwInput.addEventListener("keydown", (e) => { if (e.key === "Enter") submitUpload(); }); uploadBtn.addEventListener("click", submitUpload); newUploadBtn.addEventListener("click", () => { window.scrollTo({ top: 0, behavior: "smooth" }); pwInput.focus(); }); 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; selectFile(file); }); copyBtn.addEventListener("click", async () => { const ok = await copyText(resultUrl.value); setCopyFeedback(copyBtn, ok ? "Copied" : "Failed"); }); clearListBtn.addEventListener("click", () => { if (loadUploads().length === 0) return; const ok = window.confirm( "Clear this list? This only removes the entries from THIS browser. The files remain on the server.", ); if (!ok) return; saveUploads([]); renderList(); showNotice("List cleared. Files on the server were not touched.", "info"); }); renderList();