(function () { const form = document.querySelector("#upload-form"); const dropZone = document.querySelector(".drop-zone"); const fileInput = document.querySelector("#file-input"); const fileSummary = document.querySelector("#file-summary"); const progress = document.querySelector("#upload-progress"); const uploadStatus = document.querySelector("#upload-status"); const result = document.querySelector("#upload-result"); const resultMeta = document.querySelector("#result-meta"); const resultList = document.querySelector("#result-list"); const uploadQueue = document.querySelector("#upload-queue"); const totalProgressBar = document.querySelector("#total-progress-bar"); const copyURL = document.querySelector("#copy-url"); const openBox = document.querySelector("#open-box"); const manageLink = document.querySelector("#manage-link"); const fileBrowser = document.querySelector("[data-file-browser]"); const viewButtons = document.querySelectorAll("[data-view-button]"); const previewImages = document.querySelector("[data-preview-images]"); const previewActions = document.querySelectorAll("[data-preview-action]"); const fileContextMenu = document.querySelector("[data-file-context-menu]"); let ctrlCopyMode = false; let contextFile = null; const contextMenuCloseDistance = 80; if (fileBrowser) { viewButtons.forEach((button) => { button.addEventListener("click", () => { const view = button.getAttribute("data-view-button"); fileBrowser.classList.toggle("is-list", view === "list"); fileBrowser.classList.toggle("is-thumbs", view === "thumbs"); viewButtons.forEach((item) => item.classList.toggle("is-active", item === button)); }); }); if (previewImages) { previewImages.addEventListener("click", () => { fileBrowser.classList.toggle("images-only"); previewImages.classList.toggle("is-active"); }); } } if (fileBrowser && fileContextMenu) { fileBrowser.addEventListener("contextmenu", (event) => { const card = event.target.closest("[data-file-context]"); if (!card) { return; } event.preventDefault(); contextFile = { previewURL: card.dataset.previewUrl, viewURL: card.dataset.viewUrl, downloadURL: card.dataset.downloadUrl, fileName: card.dataset.fileName, }; showContextMenu(event.clientX, event.clientY); }); fileContextMenu.addEventListener("click", async (event) => { const button = event.target.closest("[data-context-action]"); if (!button || !contextFile) { return; } const shouldHide = await runContextAction(button.dataset.contextAction, contextFile); if (shouldHide !== false) { hideContextMenu(); } }); document.addEventListener("click", (event) => { if (!fileContextMenu.contains(event.target)) { hideContextMenu(); } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { hideContextMenu(); } }); document.addEventListener("mousemove", (event) => { if (fileContextMenu.hidden || isPointerNearContextMenu(event.clientX, event.clientY)) { return; } hideContextMenu(); }); window.addEventListener("resize", hideContextMenu); window.addEventListener("scroll", hideContextMenu, true); } if (previewActions.length > 0) { previewActions.forEach((button) => { button.addEventListener("click", async (event) => { if (!event.ctrlKey && !ctrlCopyMode) { return; } event.preventDefault(); await copyPreviewLink(button); }); }); window.addEventListener("keydown", (event) => { if (event.key === "Control") { setPreviewCopyMode(true); } }); window.addEventListener("keyup", (event) => { if (event.key === "Control") { setPreviewCopyMode(false); } }); window.addEventListener("blur", () => { setPreviewCopyMode(false); }); } if (!form || !dropZone || !fileInput) { return; } let latestBoxURL = ""; let selectedFiles = []; ["dragenter", "dragover"].forEach((eventName) => { dropZone.addEventListener(eventName, (event) => { event.preventDefault(); dropZone.classList.add("is-dragging"); }); }); ["dragleave", "drop"].forEach((eventName) => { dropZone.addEventListener(eventName, (event) => { event.preventDefault(); dropZone.classList.remove("is-dragging"); }); }); dropZone.addEventListener("drop", (event) => { if (event.dataTransfer && event.dataTransfer.files.length > 0) { fileInput.files = event.dataTransfer.files; updateSelectedState(event.dataTransfer.files); } }); fileInput.addEventListener("change", () => { updateSelectedState(fileInput.files); }); form.addEventListener("submit", async (event) => { event.preventDefault(); if (!fileInput.files || fileInput.files.length === 0) { updateStatus("Choose at least one file first."); return; } const submit = form.querySelector("button[type='submit']"); const formData = new FormData(form); selectedFiles = Array.from(fileInput.files); renderQueue(selectedFiles, "queued"); setLoading(true, submit); try { const payload = await uploadWithProgress(form.action, formData, selectedFiles); renderResult(payload); form.reset(); updateSelectedState([]); } catch (error) { updateStatus(error.message || "Upload failed"); } finally { setLoading(false, submit); } }); if (copyURL) { copyURL.addEventListener("click", () => { copyText(latestBoxURL, copyURL, "Copied"); }); } function updateSelectedState(files) { selectedFiles = Array.from(files || []); const count = selectedFiles.length || 0; const title = dropZone.querySelector(".drop-title"); if (title) { title.textContent = count === 0 ? "Drop files to upload" : count === 1 ? "1 file selected" : `${count} files selected`; } if (fileSummary) { fileSummary.textContent = count === 0 ? "Choose one or more files to begin." : `${count} file${count === 1 ? "" : "s"} ready.`; } if (count > 0) { renderQueue(selectedFiles, "queued"); } else if (uploadQueue) { uploadQueue.hidden = true; uploadQueue.replaceChildren(); } } function setLoading(isLoading, submit) { if (progress) { progress.hidden = !isLoading; } if (submit) { submit.disabled = isLoading; submit.textContent = isLoading ? "Uploading..." : "Upload files"; } updateStatus(isLoading ? "Transferring files..." : ""); setTotalProgress(isLoading ? 0 : 100); } function updateStatus(message) { if (uploadStatus) { uploadStatus.textContent = message; } } function renderResult(payload) { if (!result || !resultList || !resultMeta || !openBox) { return; } latestBoxURL = payload.boxUrl; result.hidden = false; openBox.href = payload.boxUrl; resultMeta.textContent = `${payload.files.length} file${payload.files.length === 1 ? "" : "s"} · expires ${formatDate(payload.expiresAt)}`; if (manageLink) { const anchor = manageLink.querySelector("a"); manageLink.hidden = !payload.manageUrl; if (anchor && payload.manageUrl) { anchor.href = payload.manageUrl; } } resultList.replaceChildren(); payload.files.forEach((file) => { resultList.append(createFileRow({ name: file.name, meta: `${file.size} · ${file.url}`, progress: 100, status: "complete", })); }); } function uploadWithProgress(url, formData, files) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open("POST", url); request.setRequestHeader("Accept", "application/json"); request.upload.addEventListener("progress", (event) => { if (!event.lengthComputable) { updateStatus("Uploading..."); return; } const percent = Math.round((event.loaded / event.total) * 100); updateStatus(`${percent}%`); setTotalProgress(percent); setFileProgress(files, percent); }); request.addEventListener("load", () => { let payload = {}; try { payload = JSON.parse(request.responseText || "{}"); } catch (error) { reject(new Error("Upload response could not be read")); return; } if (request.status < 200 || request.status >= 300) { reject(new Error(payload.error || "Upload failed")); return; } setTotalProgress(100); setFileProgress(files, 100); resolve(payload); }); request.addEventListener("error", () => reject(new Error("Network error during upload"))); request.addEventListener("abort", () => reject(new Error("Upload aborted"))); request.send(formData); }); } function renderQueue(files, status) { if (!uploadQueue) { return; } uploadQueue.hidden = files.length === 0; uploadQueue.replaceChildren(); files.forEach((file) => { uploadQueue.append(createFileRow({ name: file.name, meta: formatBytes(file.size), progress: status === "queued" ? 0 : 100, status, })); }); } function createFileRow(file) { const row = document.createElement("div"); row.className = "result-item upload-file-row"; row.dataset.fileName = file.name; const body = document.createElement("span"); const name = document.createElement("strong"); name.className = "file-name"; name.textContent = file.name; name.title = file.name; const meta = document.createElement("code"); meta.textContent = file.meta; body.append(name, meta); const side = document.createElement("div"); side.className = "file-progress-side"; const percent = document.createElement("span"); percent.className = "file-progress-percent"; percent.textContent = `${file.progress}%`; const bar = document.createElement("div"); bar.className = "progress file-progress"; const fill = document.createElement("span"); fill.style.transform = `scaleX(${file.progress / 100})`; bar.append(fill); side.append(percent, bar); row.append(body, side); return row; } function setTotalProgress(percent) { if (totalProgressBar) { totalProgressBar.style.transform = `scaleX(${Math.max(0, Math.min(100, percent)) / 100})`; } } function setFileProgress(files, totalPercent) { if (!uploadQueue) { return; } const count = files.length || 1; const completedFloat = (Math.max(0, Math.min(100, totalPercent)) / 100) * count; uploadQueue.querySelectorAll(".upload-file-row").forEach((row, index) => { const progress = Math.max(0, Math.min(100, Math.round((completedFloat - index) * 100))); const percent = row.querySelector(".file-progress-percent"); const fill = row.querySelector(".file-progress span"); if (percent) { percent.textContent = `${progress}%`; } if (fill) { fill.style.transform = `scaleX(${progress / 100})`; } }); } async function copyText(text, button, copiedLabel) { if (!text) { return; } await writeClipboard(text); const previous = button.textContent; button.textContent = copiedLabel; setTimeout(() => { button.textContent = previous; }, 1400); } async function copyPreviewLink(button) { await writeClipboard(button.href); const label = button.querySelector("[data-preview-label]"); if (!label) { return; } label.textContent = "Copied"; setTimeout(() => { label.textContent = ctrlCopyMode ? button.dataset.copyLabel || "Copy link" : button.dataset.viewLabel || "View"; }, 1200); } function setPreviewCopyMode(enabled) { ctrlCopyMode = enabled; previewActions.forEach((button) => { const label = button.querySelector("[data-preview-label]"); const viewIcon = button.querySelector("[data-preview-view-icon]"); const copyIcon = button.querySelector("[data-preview-copy-icon]"); if (label) { label.textContent = enabled ? button.dataset.copyLabel || "Copy link" : button.dataset.viewLabel || "View"; } if (viewIcon) { viewIcon.hidden = enabled; } if (copyIcon) { copyIcon.hidden = !enabled; } }); } async function runContextAction(action, file) { if (action === "preview") { openInNewTab(file.previewURL); return true; } if (action === "view") { openInNewTab(file.viewURL); return true; } if (action === "copy-preview") { await writeClipboard(file.previewURL); return true; } if (action === "copy-download") { await writeClipboard(file.downloadURL); return true; } if (action === "download") { openInNewTab(file.downloadURL); } return true; } function showContextMenu(x, y) { fileContextMenu.hidden = false; fileContextMenu.style.left = "0px"; fileContextMenu.style.top = "0px"; const rect = fileContextMenu.getBoundingClientRect(); const margin = 8; const left = Math.min(x, window.innerWidth - rect.width - margin); const top = Math.min(y, window.innerHeight - rect.height - margin); fileContextMenu.style.left = `${Math.max(margin, left)}px`; fileContextMenu.style.top = `${Math.max(margin, top)}px`; } function hideContextMenu() { if (!fileContextMenu || fileContextMenu.hidden) { return; } fileContextMenu.hidden = true; contextFile = null; } function isPointerNearContextMenu(x, y) { const rect = fileContextMenu.getBoundingClientRect(); return x >= rect.left - contextMenuCloseDistance && x <= rect.right + contextMenuCloseDistance && y >= rect.top - contextMenuCloseDistance && y <= rect.bottom + contextMenuCloseDistance; } function openInNewTab(url) { window.open(url, "_blank", "noopener,noreferrer"); } async function writeClipboard(text) { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return; } const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.append(textarea); textarea.select(); document.execCommand("copy"); textarea.remove(); } function formatDate(value) { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", }); } function formatBytes(bytes) { if (bytes < 1024) { return `${bytes} B`; } const units = ["KiB", "MiB", "GiB", "TiB"]; let value = bytes / 1024; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } return `${value.toFixed(1)} ${units[unit]}`; } })();