Files
WarpBox/static/js/app.js
Daniel Legt e330fb04b3 feat(ui): add clear queue flow and expose ISO expiry
- Add `formatBrowserTime()` and include ISO-8601 `expires_at` in box status JSON and `ExpiresAtISO` in the box view for browser-friendly rendering.
- Refresh UI styling (switch to MonoCraft/PixelOperatorMono, tweak base font size) and treat `aria-disabled="true"` like `disabled` for consistent button states.
- Introduce a clear-queue action with confirmation to reset upload state, unlock controls, and provide user feedback.feat(ui): add clear queue flow and expose ISO expiry

- Add `formatBrowserTime()` and include ISO-8601 `expires_at` in box status JSON and `ExpiresAtISO` in the box view for browser-friendly rendering.
- Refresh UI styling (switch to MonoCraft/PixelOperatorMono, tweak base font size) and treat `aria-disabled="true"` like `disabled` for consistent button states.
- Introduce a clear-queue action with confirmation to reset upload state, unlock controls, and provide user feedback.
2026-04-29 02:29:49 +03:00

1271 lines
48 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
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) => `<li><strong>${htmlEscape(item.displayName)}</strong> <span>${formatBytes(item.file.size)}</span></li>`).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 = `<span class="terminal-muted">warpbox@cli</span>:~$ ${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 ? `<a class="win98-button" href="${htmlEscape(openUrl)}" target="_blank" rel="noreferrer">Open</a>` : "";
showTemplatePopup(`${kind} copy failed`, "copy-failed", {
value: htmlEscape(value),
openLink,
});
}
function quotaWarningHtml(message) {
const tooLarge = oversizedFiles();
const parts = [];
if (tooLarge.length) {
parts.push("<p class=\"quota-dialog-summary\"><strong>Single-file limit exceeded.</strong> Remove these files before uploading.</p>");
parts.push(`<ol class="quota-dialog-list">${tooLarge.map((item) => `<li><strong>${htmlEscape(item.displayName)}</strong> <span>${formatBytes(item.file.size)} / max ${formatBytes(maxFileBytes)}</span></li>`).join("")}</ol>`);
}
if (isOverBoxQuota()) {
parts.push(`<p class="quota-dialog-summary"><strong>Box quota exceeded.</strong> Current total is ${formatBytes(totalBytes())}. The limit is ${formatBytes(maxBoxBytes)}. Remove ${formatBytes(totalBytes() - maxBoxBytes)} or more.</p>`);
}
if (!parts.length) parts.push(`<p>${htmlEscape(message)}</p>`);
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();