const SETTINGS_KEY = "warpbox.upload.settings.v1"; const el = { form: document.querySelector("#upload-form"), fileInput: document.querySelector("#file-upload"), dropSurface: document.querySelector("#drop-surface"), dropzone: document.querySelector("#dropzone"), fileList: document.querySelector("#file-list"), queueLabel: document.querySelector("#queue-label"), queueSize: document.querySelector("#queue-size"), limitHint: document.querySelector("#limit-hint"), boxSpaceText: document.querySelector("#box-space-text"), boxSpaceBar: document.querySelector("#box-space-bar"), overallBar: document.querySelector("#overall-bar"), overallPercent: document.querySelector("#overall-percent"), shareLink: document.querySelector("#share-link"), copyButton: document.querySelector("#copy-button"), startButton: document.querySelector("#start-button"), statusText: document.querySelector("#status-text"), toast: document.querySelector("#toast"), terminal: document.querySelector("#terminal-box"), copyCurlButton: document.querySelector("#copy-curl-button"), docPopup: document.querySelector("#doc-popup"), modalBackdrop: document.querySelector("#modal-backdrop"), docPopupTitle: document.querySelector("#doc-popup-title"), docPopupBody: document.querySelector("#doc-popup-body"), docPopupClose: document.querySelector("#doc-popup-close"), expiry: document.querySelector("#expiry-select"), password: document.querySelector("#password-input"), optionsForm: document.querySelector("#box-options-form"), maxViews: document.querySelector("#max-views"), boxName: document.querySelector("#box-name"), customSlug: document.querySelector("#custom-slug"), downloadPage: document.querySelector("#download-page"), allowZip: document.querySelector("#allow-zip"), allowPreview: document.querySelector("#allow-preview"), keepFilenames: document.querySelector("#keep-filenames"), privateBox: document.querySelector("#private-box"), apiKeyMode: document.querySelector("#api-key-mode"), apiKeyInput: document.querySelector("#api-key-input"), apiKeyRow: document.querySelector("#api-key-row"), apiKeyState: document.querySelector("#api-key-state"), }; const uploadsEnabled = el.form?.dataset.uploadsEnabled === "true"; const defaultRetention = el.form?.dataset.defaultRetention || "10s"; const maxFileBytes = numberFromDataset(el.form?.dataset.maxFileBytes); const maxBoxBytes = numberFromDataset(el.form?.dataset.maxBoxBytes); const oneTimeRetentionKey = "one-time"; let files = []; let shareUrl = ""; let uploadLocked = false; let statusTimer = null; let pendingDuplicateFiles = []; let apiKeyTimer = null; function numberFromDataset(value) { const number = Number.parseInt(value || "0", 10); return Number.isFinite(number) && number > 0 ? number : 0; } function formatBytes(bytes) { if (!bytes) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`; } function htmlEscape(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function shellQuote(value) { return `'${String(value).replaceAll("'", "'\\''")}'`; } function totalBytes() { return files.reduce((sum, item) => sum + item.file.size, 0); } function uploadedBytes() { return files.reduce((sum, item) => sum + item.loaded, 0); } function overallProgress() { const total = totalBytes(); return total ? Math.round((uploadedBytes() / total) * 100) : 0; } function oversizedFiles() { return maxFileBytes ? files.filter((item) => item.file.size > maxFileBytes) : []; } function isOverBoxQuota() { return maxBoxBytes ? totalBytes() > maxBoxBytes : false; } function hasQuotaError() { return isOverBoxQuota() || oversizedFiles().length > 0; } function normalizedFileName(name) { return String(name || "").trim().toLowerCase(); } function splitNameForIncrement(name) { const value = String(name || "file"); const dot = value.lastIndexOf("."); if (dot > 0 && dot < value.length - 1) return [value.slice(0, dot), value.slice(dot)]; return [value, ""]; } function nextIncrementedFileName(name, usedNames) { const [base, ext] = splitNameForIncrement(name); let index = 2; let candidate = `${base} (${index})${ext}`; while (usedNames.has(normalizedFileName(candidate))) { index += 1; candidate = `${base} (${index})${ext}`; } usedNames.add(normalizedFileName(candidate)); return candidate; } function makeQueuedFile(file, displayName = file.name) { return { file, displayName, loaded: 0, uploaded: false, failed: false, error: "", row: null, boxID: "", boxFile: null, previewURL: file.type?.startsWith("image/") ? URL.createObjectURL(file) : "", }; } 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 setStatus(message) { if (el.statusText) el.statusText.textContent = message; } function showToast(message, type = "info") { if (!el.toast) return; el.toast.textContent = message; el.toast.classList.remove("toast-info", "toast-warning", "toast-error", "is-visible"); el.toast.classList.add(`toast-${type}`, "is-visible"); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => el.toast.classList.remove("is-visible"), 2600); } function stopStatusAnimation() { if (statusTimer) { clearInterval(statusTimer); statusTimer = null; } } function animateUploadStatus(getPrefix) { let dotCount = 0; stopStatusAnimation(); statusTimer = setInterval(() => { dotCount = (dotCount % 3) + 1; setStatus(`${getPrefix()} Uploading${".".repeat(dotCount)}`); }, 350); } function setShareUrl(url) { shareUrl = url ? new URL(url, window.location.origin).toString() : ""; if (!el.shareLink || !el.copyButton) return; el.shareLink.textContent = shareUrl || "Not created yet"; el.shareLink.href = shareUrl || "#"; el.shareLink.title = shareUrl; el.shareLink.classList.toggle("is-empty", !shareUrl); el.shareLink.setAttribute("aria-disabled", shareUrl ? "false" : "true"); el.copyButton.disabled = !shareUrl; el.copyButton.dataset.disabledReason = shareUrl ? "" : "There is no share URL yet. Start an upload first."; updateTerminal(); updateCurrentStep(); } function setOverallProgress(percent) { const clamped = Math.max(0, Math.min(100, percent)); const display = `${Math.round(clamped)}%`; if (el.overallBar) el.overallBar.style.width = display; if (el.overallPercent) el.overallPercent.textContent = display; } function setRowProgress(item, percent) { const bar = item.row?.querySelector(".upload-progress-bar"); if (bar) bar.style.width = `${Math.max(0, Math.min(100, percent))}%`; } function updateCurrentStep() { const hasFiles = files.length > 0; const allDone = hasFiles && files.every((item) => item.uploaded); el.dropzone?.classList.toggle("is-current-step", uploadsEnabled && !hasFiles && !uploadLocked); el.startButton?.classList.toggle("is-current-step", uploadsEnabled && hasFiles && !allDone && !uploadLocked && !hasQuotaError()); document.querySelector(".upload-result")?.classList.toggle("is-current-step", allDone && Boolean(shareUrl)); } function quotaWarningMessage(incoming = []) { const combined = [...files, ...incoming]; const tooBig = maxFileBytes ? combined.filter((item) => item.file.size > maxFileBytes) : []; const total = combined.reduce((sum, item) => sum + item.file.size, 0); if (tooBig.length) { const list = tooBig.slice(0, 4).map((item) => `${item.displayName} (${formatBytes(item.file.size)})`).join(", "); const more = tooBig.length > 4 ? ` and ${tooBig.length - 4} more` : ""; return `These files are over the single-file limit of ${formatBytes(maxFileBytes)}: ${list}${more}. Remove them before uploading.`; } if (maxBoxBytes && total > maxBoxBytes) { return `This box is ${formatBytes(total - maxBoxBytes)} over the ${formatBytes(maxBoxBytes)} limit. Remove some files before uploading.`; } return ""; } function updateLimitHint() { if (!el.limitHint) return; const parts = []; if (maxBoxBytes) parts.push(`Max box: ${formatBytes(maxBoxBytes)}`); if (maxFileBytes) parts.push(`max file: ${formatBytes(maxFileBytes)}`); parts.push("links expire automatically"); el.limitHint.textContent = parts.join(" · "); } function updateQuota() { const used = totalBytes(); const limitText = maxBoxBytes ? ` / ${formatBytes(maxBoxBytes)}` : ""; const overQuota = isOverBoxQuota(); const overFile = oversizedFiles().length > 0; const percent = maxBoxBytes ? Math.min(100, Math.round((used / maxBoxBytes) * 100)) : 0; document.querySelector(".upload-quota")?.classList.toggle("is-quota-warning", overQuota || overFile); if (el.boxSpaceText) el.boxSpaceText.textContent = `${formatBytes(used)}${limitText}${overQuota ? " - over quota" : ""}`; if (el.boxSpaceBar) { el.boxSpaceBar.style.width = `${percent}%`; el.boxSpaceBar.classList.toggle("is-over-quota", overQuota || overFile); } } function updateQueueSummary() { const count = files.length; if (el.queueLabel) el.queueLabel.textContent = count ? `${count} file${count === 1 ? "" : "s"} selected` : "No files selected"; if (el.queueSize) el.queueSize.textContent = `${formatBytes(totalBytes())} total`; } function updateOverallProgress() { const uploadedCount = files.filter((item) => item.uploaded).length; const percent = overallProgress(); setOverallProgress(percent >= 100 && uploadedCount < files.length ? 99 : percent); } function createFileRow(item, index) { const row = document.createElement("div"); row.className = "upload-file-row"; row.dataset.index = String(index); row.classList.toggle("has-thumbnail", Boolean(item.previewURL)); row.classList.toggle("is-too-large", maxFileBytes > 0 && item.file.size > maxFileBytes); row.classList.toggle("is-working", item.loaded > 0 && !item.uploaded && !item.failed); row.classList.toggle("is-uploaded", item.uploaded); row.classList.toggle("is-failed", item.failed); row.title = item.error || ""; const icon = document.createElement("img"); icon.className = "upload-file-icon"; icon.src = item.previewURL || iconForFile(item.file); icon.alt = ""; icon.setAttribute("aria-hidden", "true"); const name = document.createElement("span"); name.className = "upload-file-name"; name.textContent = item.displayName; name.title = item.displayName; const size = document.createElement("span"); size.className = "upload-file-size"; size.textContent = formatBytes(item.file.size); const remove = document.createElement("button"); remove.className = "win98-button upload-file-remove"; remove.type = "button"; remove.textContent = "×"; remove.dataset.remove = String(index); remove.title = uploadLocked ? "This file cannot be removed because this upload box was already created." : "Remove file"; remove.disabled = uploadLocked; const progress = document.createElement("span"); progress.className = "upload-progress"; progress.setAttribute("aria-label", `Upload progress ${Math.round(item.file.size ? (item.loaded / item.file.size) * 100 : 0)} percent`); const progressBar = document.createElement("span"); progressBar.className = "upload-progress-bar"; progressBar.style.width = `${item.uploaded ? 100 : item.failed ? 100 : Math.max(0, Math.min(100, item.file.size ? (item.loaded / item.file.size) * 100 : 0))}%`; progress.append(progressBar); row.append(icon, name, size, remove, progress); item.row = row; return row; } function renderFiles() { if (!el.fileList) return; el.fileList.replaceChildren(); if (!files.length) { const empty = document.createElement("p"); empty.className = "upload-empty-state"; empty.textContent = uploadsEnabled ? "No files in the box yet. Drop files here, use File > Add files, or click the dropzone." : "Guest uploads are disabled."; el.fileList.append(empty); } else { const fragment = document.createDocumentFragment(); files.forEach((item, index) => fragment.append(createFileRow(item, index))); el.fileList.append(fragment); } updateQueueSummary(); updateQuota(); updateOverallProgress(); updateTerminal(); updateDisabledReasons(); updateCurrentStep(); } function duplicateFileReport(incoming = []) { const used = new Set(files.map((item) => normalizedFileName(item.displayName))); const duplicates = []; const unique = []; incoming.forEach((item) => { const key = normalizedFileName(item.displayName); if (used.has(key)) { duplicates.push(item); return; } used.add(key); unique.push(item); }); return { unique, duplicates }; } function addFiles(fileList) { if (!uploadsEnabled) { showToast("Guest uploads are disabled.", "warning"); return; } if (uploadLocked) { showToast("This box is sealed. Clear it to create a fresh upload.", "warning"); return; } const incoming = Array.from(fileList || []).map((file) => makeQueuedFile(file)); if (!incoming.length) return; const { unique, duplicates } = duplicateFileReport(incoming); if (unique.length) { files.push(...unique); setShareUrl(""); renderFiles(); const warning = quotaWarningMessage(); if (warning) showWarningDialog("Quota warning", warning); } if (duplicates.length) showDuplicateDialog(duplicates); if (unique.length) setStatus(`${unique.length} file${unique.length === 1 ? "" : "s"} added to queue`); if (duplicates.length && !unique.length) setStatus(`${duplicates.length} duplicate file${duplicates.length === 1 ? "" : "s"} need your choice`); } function showDuplicateDialog(duplicates) { pendingDuplicateFiles = duplicates; const list = duplicates.map((item) => `
  • ${htmlEscape(item.displayName)} ${formatBytes(item.file.size)}
  • `).join(""); openPopup("Duplicate file names", `

    Duplicate file names detected

    These files have the same names as files already in the queue.

      ${list}

    Skip them, or append numbers so they become names like file (2).zip.

    `); showToast("Duplicate names found. Choose skip or append numbers.", "warning"); setTimeout(() => document.querySelector("#duplicate-append")?.focus(), 0); } function appendPendingDuplicates() { if (!pendingDuplicateFiles.length) return; const used = new Set(files.map((item) => normalizedFileName(item.displayName))); pendingDuplicateFiles.forEach((item) => { item.displayName = nextIncrementedFileName(item.displayName, used); files.push(item); }); const count = pendingDuplicateFiles.length; pendingDuplicateFiles = []; closeDoc(); setShareUrl(""); renderFiles(); showToast("Duplicate files added with numbered names.", "info"); setStatus(`${count} duplicate file${count === 1 ? "" : "s"} added with numbered names`); } function removeFile(index) { if (uploadLocked) { showToast("Box already created. Clear it before editing the queue.", "warning"); return; } const [removed] = files.splice(index, 1); if (removed?.previewURL) URL.revokeObjectURL(removed.previewURL); setShareUrl(""); renderFiles(); setStatus("File removed from queue"); } function clearQueue() { files.forEach((item) => { if (item.previewURL) URL.revokeObjectURL(item.previewURL); }); files = []; pendingDuplicateFiles = []; uploadLocked = false; stopStatusAnimation(); setBoxOptionsLocked(false); setShareUrl(""); if (el.fileInput) { el.fileInput.value = ""; el.fileInput.disabled = !uploadsEnabled; } el.dropzone?.classList.remove("is-locked"); renderFiles(); setStatus(uploadsEnabled ? "Queue cleared" : "Guest uploads are disabled"); showToast("Queue cleared."); } function confirmClearQueue() { if (!files.length && !shareUrl) { showToast("Nothing to clear."); return; } openPopup("Clear WarpBox?", `

    Confirm clear

    This removes the current queue, resets progress, and unlocks the Start upload button.

    `); setTimeout(() => document.querySelector("#confirm-clear-no")?.focus(), 0); } async function createBox() { const response = await fetch("/box", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ retention_key: el.expiry?.value || defaultRetention, password: el.password?.value || "", allow_zip: isOneTimeDownloadSelected() || !el.allowZip || el.allowZip.checked, files: files.map((item) => ({ name: item.displayName, size: item.file.size })), }), }); const result = await readJSON(response); if (!response.ok) throw new Error(result.error || "Could not create upload box"); return result; } async function readJSON(response) { try { return await response.json(); } catch (_) { return {}; } } async function markFileStatus(item, status) { if (!item.boxID || !item.boxFile) return; try { await fetch(`/box/${item.boxID}/files/${item.boxFile.id}/status`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }); } catch (_) { // Best effort only. The upload endpoint also marks hard failures. } } function setFileFailed(item, message) { item.failed = true; item.uploaded = false; item.error = message || "Failed to upload"; item.loaded = item.file.size; item.row?.classList.remove("is-working", "is-uploaded"); item.row?.classList.add("is-failed"); if (item.row) item.row.title = item.error; setRowProgress(item, 100); updateOverallProgress(); } function uploadFile(item, onComplete) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append("file", item.file, item.displayName); xhr.open("POST", item.boxFile.upload_path); xhr.upload.addEventListener("loadstart", () => { item.loaded = 0; item.failed = false; item.uploaded = false; item.row?.classList.remove("is-failed", "is-uploaded"); item.row?.classList.add("is-working"); setRowProgress(item, 2); updateOverallProgress(); }); xhr.upload.addEventListener("progress", (event) => { if (!event.lengthComputable) return; item.loaded = Math.min(event.loaded, item.file.size); const percent = (event.loaded / event.total) * 100; setRowProgress(item, percent >= 100 ? 99 : percent); updateOverallProgress(); }); xhr.addEventListener("load", async () => { if (xhr.status < 200 || xhr.status >= 300) { let message = "Upload failed"; try { message = JSON.parse(xhr.responseText).error || message; } catch (_) {} setFileFailed(item, message); await markFileStatus(item, "failed"); reject(new Error(message)); return; } item.uploaded = true; item.failed = false; item.loaded = item.file.size; item.row?.classList.remove("is-working", "is-failed"); item.row?.classList.add("is-uploaded"); if (item.row) item.row.title = "Uploaded"; setRowProgress(item, 100); try { const result = JSON.parse(xhr.responseText); if (result.file) { item.boxFile = result.file; const icon = item.row?.querySelector(".upload-file-icon"); if (icon && result.file.thumbnail_path) { item.row.classList.add("has-thumbnail"); icon.src = result.file.thumbnail_path; } else if (icon && result.file.icon_path && !item.previewURL) { icon.src = result.file.icon_path; } } } catch (_) {} updateOverallProgress(); onComplete(); resolve(); }); xhr.addEventListener("error", async () => { setFileFailed(item, "Network error while uploading"); await markFileStatus(item, "failed"); reject(new Error("Network error while uploading")); }); xhr.addEventListener("abort", async () => { setFileFailed(item, "Upload cancelled"); await markFileStatus(item, "failed"); reject(new Error("Upload cancelled")); }); markFileStatus(item, "uploading"); xhr.send(formData); }); } async function startUpload() { if (!uploadsEnabled) { showToast("Guest uploads are disabled.", "warning"); return; } if (uploadLocked) { showToast("Upload already started. Press Clear to create another box.", "warning"); return; } if (!files.length) { showWarningDialog("No files selected", "There are no files selected. Please select files to upload."); showToast("No files selected. Please select files to upload.", "warning"); setStatus("No files selected"); return; } if (hasQuotaError()) { showWarningDialog("Over maximum upload size", quotaWarningMessage() || "Over maximum upload size."); showToast("Over maximum upload size.", "error"); return; } uploadLocked = true; setBoxOptionsLocked(true); if (el.fileInput) el.fileInput.disabled = true; el.dropzone?.classList.add("is-locked"); setShareUrl(""); files.forEach((item) => { item.loaded = 0; item.uploaded = false; item.failed = false; item.error = ""; }); renderFiles(); let completedCount = 0; const totalCount = files.length; const statusPrefix = () => `${completedCount}/${totalCount}`; setStatus(`${statusPrefix()} Uploading.`); animateUploadStatus(statusPrefix); try { const box = await createBox(); setShareUrl(box.box_url); files.forEach((item, index) => { item.boxID = box.box_id; item.boxFile = box.files[index]; item.displayName = item.boxFile?.name || item.displayName; const icon = item.row?.querySelector(".upload-file-icon"); if (icon && item.boxFile?.thumbnail_path) { item.row.classList.add("has-thumbnail"); icon.src = item.boxFile.thumbnail_path; } else if (icon && item.boxFile?.icon_path && !item.previewURL) { icon.src = item.boxFile.icon_path; } }); const results = await Promise.allSettled(files.map((item) => uploadFile(item, () => { completedCount += 1; }))); stopStatusAnimation(); const failedCount = results.filter((result) => result.status === "rejected").length; if (failedCount > 0) { setStatus(`${completedCount}/${totalCount} uploaded, ${failedCount} failed`); showToast(`${failedCount} file${failedCount === 1 ? "" : "s"} failed. The share URL contains the successful files.`, "error"); renderFiles(); return; } setOverallProgress(100); setStatus(`${completedCount}/${totalCount} uploaded. Share URL created. Press Clear to start another upload.`); showToast("Upload complete. Share URL created."); renderFiles(); } catch (error) { stopStatusAnimation(); uploadLocked = false; setBoxOptionsLocked(false); if (el.fileInput) el.fileInput.disabled = !uploadsEnabled; el.dropzone?.classList.remove("is-locked"); setShareUrl(""); setStatus(error.message || "Upload failed"); showToast(error.message || "Upload failed", "error"); renderFiles(); } } function isOneTimeDownloadSelected() { return el.expiry?.value === oneTimeRetentionKey; } function syncZipForRetention() { if (!el.allowZip) return; if (isOneTimeDownloadSelected()) { el.allowZip.checked = true; el.allowZip.disabled = true; } else if (!uploadLocked) { el.allowZip.disabled = false; } } function setBoxOptionsLocked(locked) { const controls = [el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.allowZip, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean); el.optionsForm?.classList.toggle("is-locked", locked); controls.forEach((control) => { control.dataset.disabledReason = locked ? "Box Options are locked because this box was already created. Press Clear to start another upload." : ""; if (control.tagName === "INPUT" && !["checkbox", "radio", "file"].includes(control.type)) { control.readOnly = locked; } else { control.disabled = locked; } }); if (el.password) el.password.type = locked ? "password" : "text"; if (!locked) { syncZipForRetention(); syncApiKeyField(); } updateDisabledReasons(); } function updateDisabledReasons() { if (el.startButton) { let reason = ""; if (!uploadsEnabled) reason = "Guest uploads are disabled."; else if (uploadLocked) reason = "This upload already started. Press Clear to create another box."; else if (hasQuotaError()) reason = "Over maximum upload size. Remove highlighted files or clear some files."; else if (!files.length) reason = "There are no files selected. Please select files to upload."; el.startButton.disabled = !uploadsEnabled || uploadLocked || hasQuotaError(); el.startButton.dataset.disabledReason = reason; el.startButton.title = reason; } } function saveSettings() { const settings = { expiry: el.expiry?.value || defaultRetention, password: el.password?.value || "", maxViews: el.maxViews?.value || "", boxName: el.boxName?.value || "", customSlug: el.customSlug?.value || "", downloadPage: Boolean(el.downloadPage?.checked), allowZip: Boolean(el.allowZip?.checked), allowPreview: Boolean(el.allowPreview?.checked), keepFilenames: Boolean(el.keepFilenames?.checked), privateBox: Boolean(el.privateBox?.checked), apiKeyMode: Boolean(el.apiKeyMode?.checked), apiKey: el.apiKeyInput?.value || "", }; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } function loadSettings() { let settings = {}; try { settings = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}"); } catch (_) {} if (settings.expiry && Array.from(el.expiry?.options || []).some((option) => option.value === settings.expiry)) el.expiry.value = settings.expiry; if (el.password) el.password.value = settings.password || ""; if (el.maxViews) el.maxViews.value = settings.maxViews || ""; if (el.boxName) el.boxName.value = settings.boxName || ""; if (el.customSlug) el.customSlug.value = settings.customSlug || ""; if (el.downloadPage) el.downloadPage.checked = settings.downloadPage !== false; if (el.allowZip) el.allowZip.checked = settings.allowZip !== false; if (el.allowPreview) el.allowPreview.checked = settings.allowPreview !== false; if (el.keepFilenames) el.keepFilenames.checked = settings.keepFilenames !== false; if (el.privateBox) el.privateBox.checked = Boolean(settings.privateBox); if (el.apiKeyMode) el.apiKeyMode.checked = Boolean(settings.apiKeyMode); if (el.apiKeyInput) el.apiKeyInput.value = settings.apiKey || ""; syncZipForRetention(); syncApiKeyField(); } function syncMenuChecks() { document.querySelectorAll("[data-expiry-check]").forEach((node) => { node.textContent = node.dataset.expiryCheck === el.expiry?.value ? "✓" : ""; }); const downloadCheck = document.querySelector("[data-download-page-check]"); if (downloadCheck) downloadCheck.textContent = el.downloadPage?.checked ? "✓" : ""; } function syncApiKeyField() { const enabled = Boolean(el.apiKeyMode?.checked) && !uploadLocked; el.apiKeyRow?.classList.toggle("is-visible", Boolean(el.apiKeyMode?.checked)); if (el.apiKeyInput) { el.apiKeyInput.disabled = !enabled; el.apiKeyInput.dataset.disabledReason = enabled ? "" : "Enable Use API key for larger quota before typing an API key."; } validateApiKeyField(); } function validateApiKeyField() { if (!el.apiKeyInput || !el.apiKeyState) return; clearTimeout(apiKeyTimer); const wrapper = el.apiKeyInput.closest(".api-key-field"); wrapper?.classList.remove("is-checking"); if (!el.apiKeyMode?.checked) { el.apiKeyState.textContent = ""; return; } const value = el.apiKeyInput.value.trim(); if (!value) { el.apiKeyState.textContent = "waiting"; return; } el.apiKeyInput.disabled = true; wrapper?.classList.add("is-checking"); el.apiKeyState.textContent = "checking"; apiKeyTimer = setTimeout(() => { wrapper?.classList.remove("is-checking"); el.apiKeyInput.disabled = uploadLocked; el.apiKeyState.textContent = value.length >= 12 ? "saved locally" : "too short"; if (value.length < 12) showToast("API key looks too short. It was saved locally, but not sent during browser uploads.", "warning"); }, 650); } function slugify(value) { return String(value || "") .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .slice(0, 32); } function syncSlugFromName(force = false) { if (!el.customSlug || !el.boxName) return; if (force || !el.customSlug.value || el.customSlug.dataset.auto === "true") { el.customSlug.value = slugify(el.boxName.value); el.customSlug.dataset.auto = "true"; } saveSettings(); updateTerminal(); } function randomPassword() { if (!el.password || uploadLocked) return; el.password.value = `${Math.random().toString(36).slice(2, 8)}-${Math.random().toString(36).slice(2, 6)}`; saveSettings(); updateTerminal(); setStatus("Generated a password"); } function randomBoxName() { if (!el.boxName || uploadLocked) return; const adjectives = ["neon", "turbo", "quiet", "cosmic", "lucky", "midnight", "pixel", "rapid"]; const nouns = ["floppy", "archive", "packet", "portal", "folder", "upload", "cache", "drive"]; el.boxName.value = `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}`; syncSlugFromName(true); setStatus("Generated a local box name"); } function getCurlCommand({ full = true } = {}) { const args = []; const selectedFiles = files.length ? files : [{ displayName: "build.zip" }]; const previewLimit = full ? selectedFiles.length : 4; selectedFiles.slice(0, previewLimit).forEach((item) => args.push(` -F ${shellQuote(`files=@${item.displayName}`)}`)); const hiddenFileCount = !full && selectedFiles.length > previewLimit ? selectedFiles.length - previewLimit : 0; args.push(` -F ${shellQuote(`retention=${el.expiry?.value || defaultRetention}`)}`); if (el.password?.value) args.push(` -F ${shellQuote("password=YOUR_PASSWORD")}`); if (el.allowZip && !el.allowZip.checked) args.push(` -F ${shellQuote("allow_zip=false")}`); const commandLines = ["curl"]; if (el.apiKeyMode?.checked) commandLines.push(` -H ${shellQuote("Authorization: Bearer YOUR_API_KEY")}`); commandLines.push(...args, ` ${window.location.origin}/upload`); const command = commandLines.join(" \\\n"); return hiddenFileCount ? `${command}\n# and ${hiddenFileCount} other files included when copying` : command; } function updateTerminal() { if (!el.terminal) return; const command = getCurlCommand({ full: false }); el.terminal.innerHTML = `warpbox@cli:~$ ${htmlEscape(command)}`; } async function copyText(kind, value, openUrl = "") { if (!value) { showToast(`No ${kind.toLowerCase()} yet.`, "warning"); return; } try { await navigator.clipboard.writeText(value); showToast(`${kind} copied to clipboard.`); setStatus(`Copied ${kind.toLowerCase()}`); } catch (_) { showCopyFallback(kind, value, openUrl); } } function showCopyFallback(kind, value, openUrl) { openPopup(`${kind} copy failed`, `

    Clipboard access failed

    The browser refused clipboard access. Copy it manually from the field below.

    ${openUrl ? `Open` : ""}
    `); } function quotaWarningHtml(message) { const tooLarge = oversizedFiles(); const parts = []; if (tooLarge.length) { parts.push("

    Single-file limit exceeded. Remove these files before uploading.

    "); parts.push(`
      ${tooLarge.map((item) => `
    1. ${htmlEscape(item.displayName)} ${formatBytes(item.file.size)} / max ${formatBytes(maxFileBytes)}
    2. `).join("")}
    `); } if (isOverBoxQuota()) { parts.push(`

    Box quota exceeded. Current total is ${formatBytes(totalBytes())}. The limit is ${formatBytes(maxBoxBytes)}. Remove ${formatBytes(totalBytes() - maxBoxBytes)} or more.

    `); } if (!parts.length) parts.push(`

    ${htmlEscape(message)}

    `); return parts.join(""); } function showWarningDialog(title, message) { openPopup(title, `

    ${htmlEscape(title)}

    ${quotaWarningHtml(message)}
    `); } function openPopup(title, html, about = false) { if (!el.docPopup || !el.docPopupTitle || !el.docPopupBody) return; el.docPopupTitle.textContent = title; el.docPopupBody.innerHTML = html; el.docPopup.classList.toggle("is-about-popup", about); el.docPopup.classList.add("is-visible"); el.modalBackdrop?.classList.add("is-visible"); } function closeDoc() { el.docPopup?.classList.remove("is-visible", "is-about-popup"); el.modalBackdrop?.classList.remove("is-visible"); } const docs = { cli: { title: "CLI Guide", html: `

    Upload with cURL

    WarpBox accepts normal multipart form uploads through the compatibility endpoint:

    curl \\
      -F 'files=@./my-file.zip' \\
      -F 'retention=1h' \\
      ${window.location.origin}/upload

    Browser flow

    The browser uses the manifest API: it creates a box, uploads each file, and marks failed uploads so the download page does not wait forever.

    `, }, faq: { title: "Help & FAQ", html: `

    Help & FAQ

    Keyboard shortcuts

    Can I password protect uploads?

    Yes. Set a password in Box Options before starting the upload.

    What happens if one file fails?

    The failed row stays red, successful files remain available, and WarpBox marks the failed file in the manifest.

    Are all options server-backed?

    Expiry, password, ZIP download, and one-time download are sent to the backend. Notes like box name, custom slug, and API key mode are saved locally until backend support exists.

    `, }, dailyQuota: { title: "Upload limits", html: `

    Upload limits

    Box size${maxBoxBytes ? formatBytes(maxBoxBytes) : "No configured limit"}
    Single file${maxFileBytes ? formatBytes(maxFileBytes) : "No configured limit"}

    These values come from the running WarpBox configuration.

    `, }, about: { title: "About WarpBox", about: true, html: `

    WarpBox

    WarpBox was made by Daniel Legt.

    Temporary file boxes, terminal-friendly uploads, and old-web UI charm.

    `, }, examples: { title: "Examples", html: `

    Upload examples

    Basic CLI upload

    curl \\
      -F 'files=@./photo.png' \\
      -F 'retention=24h' \\
      ${window.location.origin}/upload

    Multiple files with password

    curl \\
      -F 'files=@./one.png' \\
      -F 'files=@./two.zip' \\
      -F 'retention=1h' \\
      -F 'password=secret-pass' \\
      ${window.location.origin}/upload
    `, }, }; function openDoc(name) { const doc = docs[name]; if (!doc) return; openPopup(doc.title, doc.html, doc.about); setStatus(`${doc.title} opened`); } document.addEventListener("click", (event) => { const menuButton = event.target.closest(".menu-button"); if (menuButton) { const item = menuButton.closest(".menu-item"); const isOpen = item.classList.contains("is-open"); document.querySelectorAll(".menu-item.is-open").forEach((node) => { node.classList.remove("is-open"); node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false"); }); item.classList.toggle("is-open", !isOpen); menuButton.setAttribute("aria-expanded", String(!isOpen)); return; } const action = event.target.closest("[data-action]")?.dataset.action; if (action) { document.querySelectorAll(".menu-item.is-open").forEach((node) => node.classList.remove("is-open")); if (action === "browse") el.fileInput?.click(); if (action === "start-upload") startUpload(); if (action === "copy-link") copyText("Share URL", shareUrl, shareUrl); if (action === "clear") confirmClearQueue(); if (action === "toggle-delete-once" && el.expiry?.querySelector(`option[value="${oneTimeRetentionKey}"]`)) { el.expiry.value = isOneTimeDownloadSelected() ? defaultRetention : oneTimeRetentionKey; syncZipForRetention(); saveSettings(); syncMenuChecks(); updateTerminal(); } if (action === "random-password") randomPassword(); if (action === "random-box-name") randomBoxName(); if (action === "clear-password" && el.password && !uploadLocked) { el.password.value = ""; saveSettings(); updateTerminal(); } if (action === "toggle-page" && el.downloadPage && !uploadLocked) { el.downloadPage.checked = !el.downloadPage.checked; saveSettings(); syncMenuChecks(); } if (action === "help" || action === "side-help") openDoc("faq"); if (action === "terminal-help") el.terminal?.focus(); if (action === "coming-soon") showToast("That shortcut is decorative for now."); if (action === "side-close" || action === "side-folder-close" || action === "fake-close" || action === "minimize" || action === "toggle-fit") showToast("Window controls are decorative on this page."); return; } const expiry = event.target.closest("[data-expiry]")?.dataset.expiry; if (expiry && el.expiry) { el.expiry.value = expiry; syncZipForRetention(); saveSettings(); syncMenuChecks(); updateTerminal(); setStatus(`Expiry set to ${event.target.textContent.trim()}`); return; } const doc = event.target.closest("[data-doc]")?.dataset.doc; if (doc) { openDoc(doc); return; } const remove = event.target.closest("[data-remove]"); if (remove) { removeFile(Number(remove.dataset.remove)); return; } if (event.target.id === "duplicate-append") appendPendingDuplicates(); if (event.target.id === "duplicate-skip") { pendingDuplicateFiles = []; closeDoc(); showToast("Duplicate files skipped."); } if (event.target.id === "confirm-clear-yes") { closeDoc(); clearQueue(); } if (event.target.id === "confirm-clear-no" || event.target.id === "fallback-close") closeDoc(); if (!event.target.closest(".menu-item")) { document.querySelectorAll(".menu-item.is-open").forEach((node) => { node.classList.remove("is-open"); node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false"); }); } }); el.fileInput?.addEventListener("change", () => addFiles(el.fileInput.files)); [el.dropSurface, el.dropzone].filter(Boolean).forEach((target) => { target.addEventListener("dragover", (event) => { event.preventDefault(); el.dropzone?.classList.add("is-dragging"); }); target.addEventListener("dragleave", () => el.dropzone?.classList.remove("is-dragging")); target.addEventListener("drop", (event) => { event.preventDefault(); el.dropzone?.classList.remove("is-dragging"); addFiles(event.dataTransfer.files); }); }); el.dropzone?.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); el.fileInput?.click(); } }); el.form?.addEventListener("submit", (event) => { event.preventDefault(); startUpload(); }); el.copyButton?.addEventListener("click", () => copyText("Share URL", shareUrl, shareUrl)); el.copyCurlButton?.addEventListener("click", () => copyText("cURL command", getCurlCommand({ full: true }))); el.docPopupClose?.addEventListener("click", closeDoc); el.modalBackdrop?.addEventListener("click", closeDoc); [el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.allowZip, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean).forEach((control) => { control.addEventListener("input", () => { if (control === el.boxName) syncSlugFromName(); if (control === el.customSlug) el.customSlug.dataset.auto = "false"; if (control === el.apiKeyInput) validateApiKeyField(); saveSettings(); updateTerminal(); }); control.addEventListener("change", () => { if (control === el.expiry) syncZipForRetention(); if (control === el.apiKeyMode) syncApiKeyField(); saveSettings(); syncMenuChecks(); updateTerminal(); }); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeDoc(); document.querySelectorAll(".menu-item.is-open").forEach((node) => node.classList.remove("is-open")); } if (event.key === "F1") { event.preventDefault(); openDoc("faq"); } if (event.ctrlKey && !event.shiftKey && !event.altKey) { const key = event.key.toLowerCase(); if (key === "o") { event.preventDefault(); el.fileInput?.click(); } if (key === "u") { event.preventDefault(); startUpload(); } if (key === "k") { event.preventDefault(); copyText("cURL command", getCurlCommand({ full: true })); } if (key === "l") { event.preventDefault(); copyText("Share URL", shareUrl, shareUrl); } } }); window.addEventListener("beforeunload", () => { files.forEach((item) => { if (item.previewURL) URL.revokeObjectURL(item.previewURL); }); }); loadSettings(); updateLimitHint(); syncMenuChecks(); renderFiles(); updateTerminal();