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(); }