const fileInput = document.querySelector("#file-upload"); const fileCount = document.querySelector("#upload-file-count"); const fileList = document.querySelector(".upload-file-list"); const dropzone = document.querySelector(".upload-dropzone"); const uploadForm = document.querySelector(".upload-form"); const uploadStatus = document.querySelector(".upload-statusbar span:first-child"); const boxStatus = document.querySelector(".upload-statusbar span:last-child"); const uploadResult = document.querySelector(".upload-result"); const boxLink = document.querySelector("#upload-box-link"); const shareButton = document.querySelector("#upload-share-button"); const overallProgressBar = document.querySelector(".upload-overall-bar"); const overallProgressPercent = document.querySelector(".upload-overall-percent"); const retentionSelect = document.querySelector("#upload-retention"); const passwordEnabled = document.querySelector("#upload-password-enabled"); const passwordInput = document.querySelector("#upload-password"); const zipEnabled = document.querySelector("#upload-zip-enabled"); let selectedFiles = []; let statusTimer = null; let shareURL = ""; function formatBytes(bytes) { const units = ["B", "KB", "MB", "GB"]; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex += 1; } if (unitIndex === 0) { return `${size} ${units[unitIndex]}`; } return `${size.toFixed(1)} ${units[unitIndex]}`; } function iconForFile(file) { const filename = file.name || ""; const mimeType = file.type || ""; const extension = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : ""; if (extension === ".exe") { return "/static/img/icons/Program Files Icons - PNG/MSONSEXT.DLL_14_6-0.png"; } if (mimeType.startsWith("image/")) { return "/static/img/sprites/bitmap.png"; } if (mimeType.startsWith("video/") || mimeType.startsWith("audio/")) { return "/static/img/icons/netshow_notransm-1.png"; } if (mimeType.startsWith("text/") || extension === ".md") { return "/static/img/sprites/notepad_file-1.png"; } if ( mimeType.includes("zip") || mimeType.includes("compressed") || [".rar", ".7z", ".tar", ".gz"].includes(extension) ) { return "/static/img/icons/Windows Icons - PNG/zipfldr.dll_14_101-0.png"; } if ([".ttf", ".otf", ".woff", ".woff2"].includes(extension)) { return "/static/img/sprites/font.png"; } if (extension === ".pdf") { return "/static/img/sprites/journal.png"; } if ([".html", ".css", ".js"].includes(extension)) { return "/static/img/sprites/frame_web-0.png"; } return "/static/img/icons/Windows Icons - PNG/ole2.dll_14_DEFICON.png"; } function updateStatus(message) { if (uploadStatus) { uploadStatus.textContent = message; } } function stopStatusAnimation() { if (statusTimer) { clearInterval(statusTimer); statusTimer = null; } } function animateUploadStatus(getPrefix) { let dotCount = 0; stopStatusAnimation(); statusTimer = setInterval(() => { dotCount = (dotCount % 3) + 1; updateStatus(`${getPrefix()} Uploading${".".repeat(dotCount)}`); }, 350); } function setBoxStatus(message) { if (boxStatus) { boxStatus.textContent = message; boxStatus.title = message; } } function setBoxLink(path) { shareURL = path ? new URL(path, window.location.origin).toString() : ""; if (uploadResult) { uploadResult.classList.toggle("is-hidden", !shareURL); } if (boxLink) { boxLink.href = shareURL || "#"; boxLink.textContent = shareURL || "Waiting for upload"; boxLink.title = shareURL; boxLink.classList.toggle("is-empty", !shareURL); boxLink.setAttribute("aria-disabled", shareURL ? "false" : "true"); } if (shareButton) { shareButton.disabled = !shareURL; } } function setOverallProgress(percent) { const clampedPercent = Math.max(0, Math.min(100, percent)); const displayPercent = `${Math.round(clampedPercent)}%`; if (overallProgressBar) { overallProgressBar.style.width = displayPercent; } if (overallProgressPercent) { overallProgressPercent.textContent = displayPercent; } } function updateOverallProgress() { const totalBytes = selectedFiles.reduce((total, selectedFile) => total + selectedFile.file.size, 0); const loadedBytes = selectedFiles.reduce((total, selectedFile) => total + selectedFile.loaded, 0); const uploadedCount = selectedFiles.filter((selectedFile) => selectedFile.uploaded).length; const percent = totalBytes > 0 ? (loadedBytes / totalBytes) * 100 : 0; setOverallProgress(percent >= 100 && uploadedCount < selectedFiles.length ? 99 : percent); } function updateFileCount() { if (fileCount) { fileCount.textContent = `${selectedFiles.length} ${selectedFiles.length === 1 ? "file" : "files"}`; } } function setRowProgress(row, percent) { const progressBar = row.querySelector(".upload-progress-bar"); if (progressBar) { progressBar.style.width = `${Math.max(0, Math.min(100, percent))}%`; } } function createFileRow(selectedFile) { const row = document.createElement("div"); row.className = "upload-file-row"; const icon = document.createElement("img"); icon.className = "upload-file-icon"; icon.src = iconForFile(selectedFile.file); icon.alt = ""; icon.setAttribute("aria-hidden", "true"); const name = document.createElement("span"); name.className = "upload-file-name"; name.textContent = selectedFile.file.name; name.title = selectedFile.file.name; const size = document.createElement("span"); size.className = "upload-file-size"; size.textContent = formatBytes(selectedFile.file.size); const progress = document.createElement("span"); progress.className = "upload-progress"; progress.setAttribute("aria-hidden", "true"); const progressBar = document.createElement("span"); progressBar.className = "upload-progress-bar"; progress.append(progressBar); row.append(icon, name, size, progress); selectedFile.row = row; return row; } function updateSelectedFiles(files) { selectedFiles = Array.from(files || []).map((file) => ({ file, loaded: 0, row: null, uploaded: false, failed: false, })); updateFileCount(); if (!fileList) { return; } fileList.replaceChildren(); if (!selectedFiles.length) { const emptyState = document.createElement("p"); emptyState.className = "upload-empty-state"; emptyState.textContent = "No files selected"; fileList.append(emptyState); updateStatus("Ready"); setBoxStatus("WarpBox"); setBoxLink(""); setOverallProgress(0); return; } const fragment = document.createDocumentFragment(); selectedFiles.forEach((selectedFile) => { fragment.append(createFileRow(selectedFile)); }); fileList.append(fragment); updateStatus("Files selected"); setBoxStatus("WarpBox"); setBoxLink(""); setOverallProgress(0); } async function createBox() { const response = await fetch("/box", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ retention_key: retentionSelect ? retentionSelect.value : "10s", password: passwordEnabled && passwordEnabled.checked && passwordInput ? passwordInput.value : "", allow_zip: !zipEnabled || zipEnabled.checked, files: selectedFiles.map((selectedFile) => ({ name: selectedFile.file.name, size: selectedFile.file.size, })), }), }); if (!response.ok) { throw new Error("Could not create upload box"); } return response.json(); } async function markFileStatus(selectedFile, status) { if (!selectedFile.boxFile) { return; } await fetch(`/box/${selectedFile.boxID}/files/${selectedFile.boxFile.id}/status`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ status }), }); } function uploadFile(boxID, selectedFile, onComplete) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append("file", selectedFile.file); xhr.open("POST", selectedFile.boxFile.upload_path); xhr.upload.addEventListener("loadstart", () => { selectedFile.loaded = 0; selectedFile.row.classList.add("is-uploading"); selectedFile.row.title = "Loading"; updateOverallProgress(); setRowProgress(selectedFile.row, 2); }); xhr.upload.addEventListener("progress", (event) => { if (!event.lengthComputable) { return; } selectedFile.loaded = Math.min(event.loaded, selectedFile.file.size); updateOverallProgress(); const percent = (event.loaded / event.total) * 100; if (percent >= 100) { selectedFile.row.classList.add("is-processing"); selectedFile.row.title = "Loading"; setRowProgress(selectedFile.row, 99); return; } setRowProgress(selectedFile.row, percent); }); xhr.addEventListener("load", () => { if (xhr.status < 200 || xhr.status >= 300) { selectedFile.failed = true; selectedFile.row.classList.remove("is-uploading", "is-processing"); selectedFile.row.classList.add("is-failed"); selectedFile.row.title = "Failed to upload"; markFileStatus(selectedFile, "failed"); reject(new Error("Upload failed")); return; } selectedFile.uploaded = true; selectedFile.loaded = selectedFile.file.size; selectedFile.row.classList.remove("is-uploading", "is-processing"); selectedFile.row.classList.add("is-uploaded"); selectedFile.row.title = "Uploaded"; updateOverallProgress(); setRowProgress(selectedFile.row, 100); onComplete(); resolve(); }); xhr.addEventListener("error", () => { selectedFile.failed = true; selectedFile.row.classList.remove("is-uploading", "is-processing"); selectedFile.row.classList.add("is-failed"); selectedFile.row.title = "Failed to upload"; markFileStatus(selectedFile, "failed"); reject(new Error("Upload failed")); }); xhr.addEventListener("abort", () => { selectedFile.failed = true; selectedFile.row.classList.remove("is-uploading", "is-processing"); selectedFile.row.classList.add("is-failed"); selectedFile.row.title = "Failed to upload"; markFileStatus(selectedFile, "failed"); reject(new Error("Upload cancelled")); }); markFileStatus(selectedFile, "uploading"); xhr.send(formData); }); } if (fileInput) { fileInput.addEventListener("change", () => { stopStatusAnimation(); updateSelectedFiles(fileInput.files); }); } if (passwordEnabled && passwordInput) { passwordEnabled.addEventListener("change", () => { passwordInput.disabled = !passwordEnabled.checked; if (!passwordEnabled.checked) { passwordInput.value = ""; return; } passwordInput.focus(); }); } if (fileInput && dropzone) { dropzone.addEventListener("dragover", (event) => { event.preventDefault(); dropzone.classList.add("is-dragging"); }); dropzone.addEventListener("dragleave", () => { dropzone.classList.remove("is-dragging"); }); dropzone.addEventListener("drop", (event) => { event.preventDefault(); dropzone.classList.remove("is-dragging"); if (!event.dataTransfer.files.length) { return; } fileInput.files = event.dataTransfer.files; stopStatusAnimation(); updateSelectedFiles(fileInput.files); }); } if (uploadForm) { uploadForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!selectedFiles.length) { updateStatus("Choose files first"); return; } if (passwordEnabled && passwordEnabled.checked && passwordInput && !passwordInput.value.trim()) { updateStatus("Enter password"); passwordInput.focus(); return; } let completedCount = 0; const totalCount = selectedFiles.length; const statusPrefix = () => `${completedCount}/${totalCount}`; selectedFiles.forEach((selectedFile) => { selectedFile.uploaded = false; selectedFile.failed = false; selectedFile.loaded = 0; selectedFile.row.classList.remove("is-uploaded", "is-failed", "is-uploading", "is-processing"); selectedFile.row.title = ""; setRowProgress(selectedFile.row, 0); }); setBoxLink(""); setOverallProgress(0); updateStatus(`${statusPrefix()} Uploading.`); animateUploadStatus(statusPrefix); try { const box = await createBox(); setBoxStatus(box.box_url); setBoxLink(box.box_url); selectedFiles.forEach((selectedFile, index) => { selectedFile.boxID = box.box_id; selectedFile.boxFile = box.files[index]; const icon = selectedFile.row.querySelector(".upload-file-icon"); if (icon && selectedFile.boxFile.icon_path) { icon.src = selectedFile.boxFile.icon_path; } }); await Promise.allSettled(selectedFiles.map((selectedFile) => { return uploadFile(box.box_id, selectedFile, () => { completedCount += 1; }); })); stopStatusAnimation(); const failedCount = selectedFiles.filter((selectedFile) => selectedFile.failed).length; if (failedCount > 0) { updateStatus(`${completedCount}/${totalCount} Uploaded, ${failedCount} failed`); return; } setOverallProgress(100); updateStatus(`${completedCount}/${totalCount} Uploaded`); } catch (error) { stopStatusAnimation(); setBoxLink(""); updateStatus("Upload failed"); } }); } if (shareButton) { shareButton.addEventListener("click", async () => { if (!shareURL) { return; } try { if (navigator.share) { await navigator.share({ title: "WarpBox download", text: "Download these files from WarpBox", url: shareURL, }); return; } await navigator.clipboard.writeText(shareURL); updateStatus("Link copied"); } catch (error) { updateStatus("Share cancelled"); } }); }