233 lines
9.5 KiB
JavaScript
233 lines
9.5 KiB
JavaScript
|
|
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();
|
|||
|
|
}
|