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"); 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 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("span"); icon.className = "upload-file-icon"; 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({ 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 (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; } 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]; }); 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"); } }); }