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; let completedImpactKeys = new Set(); let overallImpactDone = false; 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") { window.WarpBoxUI.toast(message, type, { target: el.toast }); } function closeMenus() { document.querySelectorAll(".menu-item.is-open").forEach((node) => { node.classList.remove("is-open"); node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false"); }); } function disabledReasonFor(target) { const control = target.closest("[data-disabled-reason], button, input, select, textarea, .upload-dropzone, .option-check, .option-row"); if (!control) return ""; if (control.classList.contains("option-check") || control.classList.contains("option-row")) { const nested = control.querySelector("input, select, textarea"); if (nested?.disabled || nested?.readOnly || nested?.getAttribute("aria-disabled") === "true") { return nested.dataset.disabledReason || "This option is disabled right now."; } } if (control.classList.contains("upload-dropzone") && uploadLocked) { return control.dataset.disabledReason || "The current box is sealed after upload. Press Clear to start a new box."; } if (control.disabled || control.readOnly || control.getAttribute("aria-disabled") === "true") { return control.dataset.disabledReason || control.title || "This control is disabled right now."; } return ""; } function announceDisabledReason(event) { const reason = disabledReasonFor(event.target); if (!reason) return false; event.preventDefault(); event.stopPropagation(); closeMenus(); showToast(reason, "warning"); setStatus(reason); return true; } 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 = false; el.copyButton.setAttribute("aria-disabled", shareUrl ? "false" : "true"); el.copyButton.dataset.disabledReason = shareUrl ? "" : "There is no share URL yet. Start an upload first."; updateDisabledReasons(); 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 flashProgressBar(bar) { if (!bar) return; bar.classList.remove("just-completed"); void bar.offsetWidth; bar.classList.add("just-completed"); setTimeout(() => bar.classList.remove("just-completed"), 620); } 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); if (percent >= 100 && files.length && !overallImpactDone) { overallImpactDone = true; flashProgressBar(el.overallBar); } } 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 = false; remove.setAttribute("aria-disabled", uploadLocked ? "true" : "false"); remove.dataset.disabledReason = uploadLocked ? "Files cannot be removed after the box is created. Press Clear to start another upload." : ""; 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(""); showTemplatePopup("Duplicate file names", "duplicate", { list }) .then(() => document.querySelector("#duplicate-append")?.focus()); showToast("Duplicate names found. Choose skip or append numbers.", "warning"); } 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; completedImpactKeys = new Set(); overallImpactDone = 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; } showTemplatePopup("Clear WarpBox?", "clear") .then(() => document.querySelector("#confirm-clear-no")?.focus()); } 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 markCompletedImpact(item) { const key = item.boxFile?.id || item.displayName; if (completedImpactKeys.has(key)) return; completedImpactKeys.add(key); flashProgressBar(item.row?.querySelector(".upload-progress-bar")); } 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); markCompletedImpact(item); 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 = ""; }); completedImpactKeys = new Set(); overallImpactDone = false; 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 = false; el.startButton.setAttribute("aria-disabled", reason ? "true" : "false"); el.startButton.dataset.disabledReason = reason; el.startButton.title = reason; } if (el.fileInput) { el.fileInput.dataset.disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : ""); } if (el.dropzone) { el.dropzone.dataset.disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : ""); } document.querySelectorAll('[data-action="start-upload"]').forEach((button) => { const reason = el.startButton?.dataset.disabledReason || ""; button.setAttribute("aria-disabled", reason ? "true" : "false"); button.dataset.disabledReason = reason; }); document.querySelectorAll('[data-action="browse"]').forEach((button) => { const reason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : ""); button.setAttribute("aria-disabled", reason ? "true" : "false"); button.dataset.disabledReason = reason; }); document.querySelectorAll('[data-action="copy-link"]').forEach((button) => { button.setAttribute("aria-disabled", shareUrl ? "false" : "true"); button.dataset.disabledReason = shareUrl ? "" : "There is no share URL yet. Start an upload first."; }); } function saveSettings() { const apiKey = el.apiKeyMode?.checked && validApiKey(el.apiKeyInput?.value || "") ? el.apiKeyInput.value.trim() : ""; const settings = { maxViews: el.maxViews?.value || "", allowPreview: Boolean(el.allowPreview?.checked), keepFilenames: Boolean(el.keepFilenames?.checked), privateBox: Boolean(el.privateBox?.checked), apiKeyMode: Boolean(el.apiKeyMode?.checked), apiKey, }; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } function loadSettings() { let settings = {}; try { settings = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}"); } catch (_) {} if (el.maxViews) el.maxViews.value = settings.maxViews || ""; 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 = validApiKey(settings.apiKey || "") ? settings.apiKey : ""; syncZipForRetention(); syncApiKeyField(); saveSettings(); } function syncMenuChecks() { updateDisabledReasons(); } 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"; saveSettings(); 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; if (validApiKey(value)) { el.apiKeyState.textContent = "saved locally"; saveSettings(); } else { el.apiKeyInput.value = ""; el.apiKeyState.textContent = "invalid"; saveSettings(); showToast("Invalid API key removed. Paste a valid API key to save it.", "warning"); } }, 650); } function validApiKey(value) { return /^[A-Za-z0-9._-]{12,}$/.test(String(value || "").trim()); } function slugify(value) { return String(value || "") .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .slice(0, 32); } function sanitizeSlugInput(value) { return String(value || "") .toLowerCase() .replace(/[^a-z0-9-]/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 Disk", "Archive Box", "Packet Portal", "Upload Folder", "Cache Drive", "Release Bundle"]; 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) { const openLink = openUrl ? `Open` : ""; showTemplatePopup(`${kind} copy failed`, "copy-failed", { value: htmlEscape(value), openLink, }); } 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) { showTemplatePopup(title, "warning", { title: htmlEscape(title), content: quotaWarningHtml(message), }); } function openPopup(title, html, about = false) { window.WarpBoxUI.openPopup(title, html, { about, popup: el.docPopup, title: el.docPopupTitle, body: el.docPopupBody, backdrop: el.modalBackdrop, }); } function closeDoc() { window.WarpBoxUI.closePopup({ popup: el.docPopup, backdrop: el.modalBackdrop }); } async function showTemplatePopup(title, templateName, data = {}, about = false) { try { const html = await window.WBPopups.renderTemplate(templateName, data); openPopup(title, html, about); } catch (error) { showToast(error.message || `Could not load ${title}.`, "error"); } } function popupTemplateData(name) { const data = { origin: window.location.origin }; if (name !== "dailyQuota") return data; return { ...data, boxLimit: maxBoxBytes ? formatBytes(maxBoxBytes) : "No configured limit", boxPercent: maxBoxBytes ? Math.min(100, Math.round((totalBytes() / maxBoxBytes) * 100)) : 0, fileLimit: maxFileBytes ? formatBytes(maxFileBytes) : "No configured limit", filePercent: oversizedFiles().length ? 100 : 0, }; } async function openDoc(name) { try { const doc = await window.WBPopups.renderDoc(name, popupTemplateData(name)); if (!doc) return; openPopup(doc.title, doc.html, doc.about); setStatus(`${doc.title} opened`); } catch (error) { showToast(error.message || "Could not load help window.", "error"); } } document.addEventListener("click", (event) => { if (announceDisabledReason(event)) return; const menuButton = event.target.closest(".menu-button"); if (menuButton) { const item = menuButton.closest(".menu-item"); const isOpen = item.classList.contains("is-open"); closeMenus(); item.classList.toggle("is-open", !isOpen); menuButton.setAttribute("aria-expanded", String(!isOpen)); return; } const action = event.target.closest("[data-action]")?.dataset.action; if (action) { closeMenus(); 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 === "coming-soon") showToast("Coming Soon, not implemented just yet."); if (action === "fake-close") showToast("Close button denied. The upload window is staying open.", "warning"); if (action === "minimize") showToast("Minimize requested. WarpBox stays visible so your queue is safe."); if (action === "toggle-fit") { document.body.classList.toggle("fit-window"); showToast("Maximize requested. The pixel rectangle feels important now."); } if (action === "side-close") showToast("Box Options refuses to leave. Settings stay visible."); if (action === "side-help") showToast("Terminal help opened. Copy the command and feed it files."); if (action === "side-folder-close") showToast("The folder window saw that click and chose denial."); 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")) { closeMenus(); } }); document.addEventListener("mousedown", (event) => { announceDisabledReason(event); }, true); document.querySelectorAll(".menu-item").forEach((item) => { item.addEventListener("mouseenter", () => { if (!document.querySelector(".menu-item.is-open")) return; closeMenus(); item.classList.add("is-open"); item.querySelector(".menu-button")?.setAttribute("aria-expanded", "true"); }); }); 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.maxViews?.addEventListener("wheel", (event) => { if (el.maxViews.disabled || el.maxViews.readOnly) return; event.preventDefault(); const delta = event.deltaY < 0 ? 1 : -1; const modifier = event.ctrlKey && event.shiftKey ? 50 : event.shiftKey ? 15 : event.ctrlKey ? 5 : 1; const min = Number.parseInt(el.maxViews.min || "1", 10); const max = Number.parseInt(el.maxViews.max || "9999", 10); const current = Number.parseInt(el.maxViews.value || String(min), 10); el.maxViews.value = String(Math.max(min, Math.min(max, current + (delta * modifier)))); saveSettings(); updateTerminal(); }); el.apiKeyInput?.addEventListener("keydown", (event) => { const allowed = event.ctrlKey || event.metaKey || event.altKey || [ "Tab", "Shift", "Control", "Alt", "Meta", "Escape", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown", ].includes(event.key); if (allowed) return; event.preventDefault(); showToast("Only pasting the API key is supported.", "warning"); setStatus("Only pasting the API key is supported"); }); el.apiKeyInput?.addEventListener("paste", () => { setTimeout(validateApiKeyField, 0); }); [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) { const clean = sanitizeSlugInput(el.customSlug.value); if (el.customSlug.value !== clean) el.customSlug.value = clean; 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(); closeMenus(); } 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();