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.
This commit is contained in:
2026-04-29 02:29:49 +03:00
parent a8c0666b5a
commit e330fb04b3
15 changed files with 3309 additions and 189 deletions

View File

@@ -171,17 +171,25 @@ function setStatus(message) {
}
function showToast(message, type = "info") {
if (!el.toast) return;
el.toast.textContent = message;
el.toast.classList.remove("toast-info", "toast-warning", "toast-error", "is-visible");
el.toast.classList.add(`toast-${type}`, "is-visible");
clearTimeout(showToast.timer);
showToast.timer = setTimeout(() => el.toast.classList.remove("is-visible"), 2600);
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");
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.";
}
@@ -196,6 +204,7 @@ function announceDisabledReason(event) {
if (!reason) return false;
event.preventDefault();
event.stopPropagation();
closeMenus();
showToast(reason, "warning");
setStatus(reason);
return true;
@@ -225,8 +234,10 @@ function setShareUrl(url) {
el.shareLink.title = shareUrl;
el.shareLink.classList.toggle("is-empty", !shareUrl);
el.shareLink.setAttribute("aria-disabled", shareUrl ? "false" : "true");
el.copyButton.disabled = !shareUrl;
el.copyButton.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();
}
@@ -345,7 +356,8 @@ function createFileRow(item, index) {
remove.textContent = "×";
remove.dataset.remove = String(index);
remove.title = uploadLocked ? "This file cannot be removed because this upload box was already created." : "Remove file";
remove.disabled = uploadLocked;
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");
@@ -762,7 +774,8 @@ function updateDisabledReasons() {
else if (uploadLocked) reason = "This upload already started. Press Clear to create another box.";
else if (hasQuotaError()) reason = "Over maximum upload size. Remove highlighted files or clear some files.";
else if (!files.length) reason = "There are no files selected. Please select files to upload.";
el.startButton.disabled = !uploadsEnabled || uploadLocked || hasQuotaError();
el.startButton.disabled = false;
el.startButton.setAttribute("aria-disabled", reason ? "true" : "false");
el.startButton.dataset.disabledReason = reason;
el.startButton.title = reason;
}
@@ -772,22 +785,31 @@ function updateDisabledReasons() {
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 = {
expiry: el.expiry?.value || defaultRetention,
password: el.password?.value || "",
maxViews: el.maxViews?.value || "",
boxName: el.boxName?.value || "",
customSlug: el.customSlug?.value || "",
downloadPage: Boolean(el.downloadPage?.checked),
allowZip: Boolean(el.allowZip?.checked),
allowPreview: Boolean(el.allowPreview?.checked),
keepFilenames: Boolean(el.keepFilenames?.checked),
privateBox: Boolean(el.privateBox?.checked),
apiKeyMode: Boolean(el.apiKeyMode?.checked),
apiKey: el.apiKeyInput?.value || "",
apiKey,
};
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
}
@@ -797,25 +819,19 @@ function loadSettings() {
try {
settings = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}");
} catch (_) {}
if (settings.expiry && Array.from(el.expiry?.options || []).some((option) => option.value === settings.expiry)) el.expiry.value = settings.expiry;
if (el.password) el.password.value = settings.password || "";
if (el.maxViews) el.maxViews.value = settings.maxViews || "";
if (el.boxName) el.boxName.value = settings.boxName || "";
if (el.customSlug) el.customSlug.value = settings.customSlug || "";
if (el.downloadPage) el.downloadPage.checked = settings.downloadPage !== false;
if (el.allowZip) el.allowZip.checked = settings.allowZip !== false;
if (el.allowPreview) el.allowPreview.checked = settings.allowPreview !== false;
if (el.keepFilenames) el.keepFilenames.checked = settings.keepFilenames !== false;
if (el.privateBox) el.privateBox.checked = Boolean(settings.privateBox);
if (el.apiKeyMode) el.apiKeyMode.checked = Boolean(settings.apiKeyMode);
if (el.apiKeyInput) el.apiKeyInput.value = settings.apiKey || "";
if (el.apiKeyInput) el.apiKeyInput.value = validApiKey(settings.apiKey || "") ? settings.apiKey : "";
syncZipForRetention();
syncApiKeyField();
saveSettings();
}
function syncMenuChecks() {
const downloadCheck = document.querySelector("[data-download-page-check]");
if (downloadCheck) downloadCheck.textContent = el.downloadPage?.checked ? "✓" : "";
updateDisabledReasons();
}
function syncApiKeyField() {
@@ -841,6 +857,7 @@ function validateApiKeyField() {
const value = el.apiKeyInput.value.trim();
if (!value) {
el.apiKeyState.textContent = "waiting";
saveSettings();
return;
}
@@ -850,11 +867,22 @@ function validateApiKeyField() {
apiKeyTimer = setTimeout(() => {
wrapper?.classList.remove("is-checking");
el.apiKeyInput.disabled = uploadLocked;
el.apiKeyState.textContent = value.length >= 12 ? "saved locally" : "too short";
if (value.length < 12) showToast("API key looks too short. It was saved locally, but not sent during browser uploads.", "warning");
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()
@@ -966,17 +994,17 @@ function showWarningDialog(title, message) {
}
function openPopup(title, html, about = false) {
if (!el.docPopup || !el.docPopupTitle || !el.docPopupBody) return;
el.docPopupTitle.textContent = title;
el.docPopupBody.innerHTML = html;
el.docPopup.classList.toggle("is-about-popup", about);
el.docPopup.classList.add("is-visible");
el.modalBackdrop?.classList.add("is-visible");
window.WarpBoxUI.openPopup(title, html, {
about,
popup: el.docPopup,
title: el.docPopupTitle,
body: el.docPopupBody,
backdrop: el.modalBackdrop,
});
}
function closeDoc() {
el.docPopup?.classList.remove("is-visible", "is-about-popup");
el.modalBackdrop?.classList.remove("is-visible");
window.WarpBoxUI.closePopup({ popup: el.docPopup, backdrop: el.modalBackdrop });
}
async function showTemplatePopup(title, templateName, data = {}, about = false) {
@@ -1018,10 +1046,7 @@ document.addEventListener("click", (event) => {
if (menuButton) {
const item = menuButton.closest(".menu-item");
const isOpen = item.classList.contains("is-open");
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
node.classList.remove("is-open");
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
});
closeMenus();
item.classList.toggle("is-open", !isOpen);
menuButton.setAttribute("aria-expanded", String(!isOpen));
return;
@@ -1029,7 +1054,7 @@ document.addEventListener("click", (event) => {
const action = event.target.closest("[data-action]")?.dataset.action;
if (action) {
document.querySelectorAll(".menu-item.is-open").forEach((node) => node.classList.remove("is-open"));
closeMenus();
if (action === "browse") el.fileInput?.click();
if (action === "start-upload") startUpload();
if (action === "copy-link") copyText("Share URL", shareUrl, shareUrl);
@@ -1054,7 +1079,6 @@ document.addEventListener("click", (event) => {
syncMenuChecks();
}
if (action === "help" || action === "side-help") openDoc("faq");
if (action === "terminal-help") el.terminal?.focus();
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.");
@@ -1093,10 +1117,7 @@ document.addEventListener("click", (event) => {
if (event.target.id === "confirm-clear-no" || event.target.id === "fallback-close") closeDoc();
if (!event.target.closest(".menu-item")) {
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
node.classList.remove("is-open");
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
});
closeMenus();
}
});
@@ -1107,10 +1128,7 @@ document.addEventListener("mousedown", (event) => {
document.querySelectorAll(".menu-item").forEach((item) => {
item.addEventListener("mouseenter", () => {
if (!document.querySelector(".menu-item.is-open")) return;
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
node.classList.remove("is-open");
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
});
closeMenus();
item.classList.add("is-open");
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "true");
});
@@ -1148,6 +1166,46 @@ el.copyCurlButton?.addEventListener("click", () => copyText("cURL command", getC
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();
@@ -1172,7 +1230,7 @@ el.modalBackdrop?.addEventListener("click", closeDoc);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeDoc();
document.querySelectorAll(".menu-item.is-open").forEach((node) => node.classList.remove("is-open"));
closeMenus();
}
if (event.key === "F1") {
event.preventDefault();

View File

@@ -1,20 +1,182 @@
const boxPanel = document.querySelector(".box-panel[data-box-id]");
const boxStatus = document.querySelector(".box-statusbar span:first-child");
const boxAddress = document.querySelector("#box-address");
const boxExpiryMeta = document.querySelector(".box-meta[data-expires-at]");
const boxExpiryText = document.querySelector("#box-expiry-text");
const contextMenu = document.querySelector("#box-context-menu");
const docPopup = document.querySelector("#doc-popup");
const docPopupTitle = document.querySelector("#doc-popup-title");
const docPopupBody = document.querySelector("#doc-popup-body");
const docPopupClose = document.querySelector("#doc-popup-close");
const modalBackdrop = document.querySelector("#modal-backdrop");
const toast = document.querySelector("#toast");
const zipOnly = boxPanel && boxPanel.dataset.zipOnly === "true";
document.querySelectorAll('.box-file[aria-disabled="true"]').forEach((item) => {
item.addEventListener("click", (event) => {
if (item.getAttribute("aria-disabled") === "true") {
event.preventDefault();
}
let contextFile = null;
function htmlEscape(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function showToast(message, type = "info") {
window.WarpBoxUI.toast(message, type, { target: toast });
}
function openPopup(title, html, options = {}) {
window.WarpBoxUI.openPopup(title, html, {
...options,
popup: docPopup,
title: docPopupTitle,
body: docPopupBody,
backdrop: modalBackdrop,
});
});
}
function closePopup() {
window.WarpBoxUI.closePopup({ popup: docPopup, backdrop: modalBackdrop });
}
function currentExpiryDate() {
const value = boxExpiryMeta?.dataset.expiresAt || "";
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function formatDuration(ms) {
if (ms <= 0) return "expired";
const totalSeconds = Math.ceil(ms / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (days) return `${days}d ${hours}h ${minutes}m`;
if (hours) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
}
function updateExpiryCountdown() {
if (!boxExpiryText || !boxExpiryMeta) return;
const expiry = currentExpiryDate();
if (!expiry) {
boxExpiryText.textContent = "Expires after one-time download";
return;
}
boxExpiryText.textContent = `Expires in ${formatDuration(expiry.getTime() - Date.now())}`;
boxExpiryText.title = `Expires at ${expiry.toLocaleString()}`;
}
function closeContextMenu() {
contextMenu?.classList.remove("is-visible");
contextMenu?.setAttribute("aria-hidden", "true");
}
function showContextMenu(file, x, y) {
if (!contextMenu) return;
contextFile = file;
contextMenu.style.left = `${Math.min(x, window.innerWidth - 190)}px`;
contextMenu.style.top = `${Math.min(y, window.innerHeight - 98)}px`;
contextMenu.classList.add("is-visible");
contextMenu.setAttribute("aria-hidden", "false");
}
function fileData(item) {
return {
id: item.dataset.fileId || "",
name: item.dataset.name || item.querySelector(".box-file-name")?.textContent || "",
size: item.dataset.size || "",
mime: item.dataset.mime || "",
status: item.dataset.status || "",
statusLabel: item.querySelector(".box-file-meta")?.textContent || "",
downloadPath: item.dataset.downloadPath || item.getAttribute("href") || "",
thumbnail: item.dataset.thumbnail || "",
canDownload: item.getAttribute("aria-disabled") !== "true" && item.getAttribute("href") !== "#",
};
}
function downloadFile(item) {
const data = fileData(item);
if (!data.canDownload) {
showToast(zipOnly ? "Individual file downloads are disabled for one-time boxes. Use Download Zip." : "This file is not ready for download yet.", "warning");
return;
}
window.location.href = data.downloadPath;
setTimeout(refreshBoxStatus, 900);
}
function previewURL(data) {
return data.canDownload ? data.downloadPath : "";
}
async function previewFile(item) {
const data = fileData(item);
if (zipOnly) {
showToast("Previews are disabled for one-time boxes. Use Download Zip.", "warning");
return;
}
const url = previewURL(data);
if (!url) {
showToast("This file is not ready to preview yet.", "warning");
return;
}
const mime = data.mime.toLowerCase();
const name = htmlEscape(data.name);
if (mime.startsWith("image/")) {
openPopup(`${data.name} preview`, `<img class="preview-frame" src="${htmlEscape(url)}" alt="${name}">`, { preview: true });
return;
}
if (mime.startsWith("video/")) {
openPopup(`${data.name} preview`, `<video class="preview-frame" src="${htmlEscape(url)}" controls></video>`, { preview: true });
return;
}
if (mime.startsWith("audio/")) {
openPopup(`${data.name} preview`, `<audio class="preview-frame" src="${htmlEscape(url)}" controls></audio>`, { preview: true });
return;
}
if (mime === "application/pdf") {
openPopup(`${data.name} preview`, `<iframe class="preview-frame" src="${htmlEscape(url)}" title="${name}"></iframe>`, { preview: true });
return;
}
if (mime.startsWith("text/") || /\.(txt|md|json|csv|log|html|css|js)$/i.test(data.name)) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error("Preview failed");
const text = await response.text();
openPopup(`${data.name} preview`, `<code class="code-block preview-frame is-text">${htmlEscape(text.slice(0, 120000))}</code>`, { preview: true });
} catch (_) {
showToast("The browser could not load a text preview.", "error");
}
return;
}
showToast("This file type cannot be previewed in the browser.", "warning");
}
function showProperties(item) {
const data = fileData(item);
const url = data.downloadPath ? new URL(data.downloadPath, window.location.origin).toString() : "Not ready";
openPopup(`${data.name} Properties`, `
<h3>${htmlEscape(data.name)}</h3>
<dl class="properties-grid">
<dt>Name</dt><dd>${htmlEscape(data.name)}</dd>
<dt>Size</dt><dd>${htmlEscape(data.size || "Unknown")}</dd>
<dt>Type</dt><dd>${htmlEscape(data.mime || "Unknown")}</dd>
<dt>Status</dt><dd>${htmlEscape(data.statusLabel || data.status || "Unknown")}</dd>
<dt>File ID</dt><dd>${htmlEscape(data.id)}</dd>
<dt>Location</dt><dd>${htmlEscape(url)}</dd>
</dl>
`, { properties: true });
}
function updateBoxFile(file) {
const item = document.querySelector(`.box-file[data-file-id="${file.id}"]`);
if (!item) {
return;
}
if (!item) return;
const meta = item.querySelector(".box-file-meta");
const icon = item.querySelector(".box-file-icon");
@@ -26,6 +188,11 @@ function updateBoxFile(file) {
item.classList.toggle("is-loading", !isComplete && !isFailed);
item.classList.toggle("has-thumbnail", Boolean(file.thumbnail_path));
item.dataset.status = file.status;
item.dataset.name = file.name || item.dataset.name || "";
item.dataset.size = file.size_label || item.dataset.size || "";
item.dataset.mime = file.mime_type || item.dataset.mime || "";
item.dataset.downloadPath = file.download_path || item.dataset.downloadPath || "";
item.dataset.thumbnail = file.thumbnail_path || "";
item.title = file.title;
if (isComplete && !zipOnly) {
@@ -38,27 +205,22 @@ function updateBoxFile(file) {
item.setAttribute("aria-disabled", "true");
}
if (meta) {
meta.textContent = `${file.status_label} · ${file.size_label}`;
}
if (icon) {
icon.src = file.thumbnail_path || file.icon_path;
}
if (meta) meta.textContent = `${file.status_label} · ${file.size_label}`;
if (icon) icon.src = file.thumbnail_path || file.icon_path;
}
async function refreshBoxStatus() {
if (!boxPanel) {
return false;
}
if (!boxPanel) return false;
const boxID = boxPanel.dataset.boxId;
const response = await fetch(`/box/${boxID}/status`);
if (!response.ok) {
return true;
}
if (!response.ok) return true;
const result = await response.json();
if (boxExpiryMeta && typeof result.expires_at === "string") {
boxExpiryMeta.dataset.expiresAt = result.expires_at;
updateExpiryCountdown();
}
result.files.forEach(updateBoxFile);
if (boxStatus) {
@@ -73,17 +235,73 @@ async function refreshBoxStatus() {
});
}
document.addEventListener("click", (event) => {
const action = event.target.closest("[data-action]")?.dataset.action;
if (action === "fake-close") showToast("Close clicked. The download window is emotionally attached.", "warning");
if (action === "minimize") showToast("Minimize clicked. WarpBox refuses to disappear quietly.");
if (action === "toggle-fit") {
document.body.classList.toggle("fit-window");
showToast("Maximize clicked. The window is doing its best.");
}
const contextAction = event.target.closest("[data-context-action]")?.dataset.contextAction;
if (contextAction && contextFile) {
event.preventDefault();
const item = contextFile;
closeContextMenu();
if (contextAction === "preview") previewFile(item);
if (contextAction === "download") downloadFile(item);
if (contextAction === "properties") showProperties(item);
return;
}
if (!event.target.closest("#box-context-menu")) closeContextMenu();
});
document.querySelectorAll(".box-file").forEach((item) => {
item.addEventListener("click", (event) => {
if (item.getAttribute("aria-disabled") === "true") {
event.preventDefault();
showToast(zipOnly ? "Individual file downloads are disabled for one-time boxes. Use Download Zip." : "This file is not ready yet.", "warning");
return;
}
setTimeout(refreshBoxStatus, 900);
});
item.addEventListener("contextmenu", (event) => {
event.preventDefault();
showContextMenu(item, event.clientX, event.clientY);
});
});
boxAddress?.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(window.location.href);
showToast("Current box URL copied.");
} catch (_) {
openPopup("Copy box URL", `<p>Clipboard access failed. Copy this URL manually.</p><textarea class="copy-fallback-text" readonly>${htmlEscape(window.location.href)}</textarea>`);
}
});
docPopupClose?.addEventListener("click", closePopup);
modalBackdrop?.addEventListener("click", closePopup);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closePopup();
closeContextMenu();
}
});
updateExpiryCountdown();
setInterval(updateExpiryCountdown, 1000);
if (boxPanel) {
const pollMS = Number.parseInt(boxPanel.dataset.pollMs, 10) || 5000;
const timer = setInterval(async () => {
try {
const hasLoadingFiles = await refreshBoxStatus();
if (!hasLoadingFiles) {
clearInterval(timer);
}
} catch (error) {
// Keep polling through temporary network/server hiccups; otherwise
// an in-progress file can appear stuck forever after one bad poll.
if (!hasLoadingFiles) clearInterval(timer);
} catch (_) {
// Keep polling through temporary network/server hiccups.
}
}, pollMS);
}

View File

@@ -1,8 +1,10 @@
window.WBUtils = (() => {
function renderTemplate(template, data = {}) {
return String(template).replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
return Object.prototype.hasOwnProperty.call(data, key) ? String(data[key]) : "";
});
return window.WarpBoxUI?.renderTemplate
? window.WarpBoxUI.renderTemplate(template, data)
: String(template).replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
return Object.prototype.hasOwnProperty.call(data, key) ? String(data[key]) : "";
});
}
return { renderTemplate };

48
static/js/warpbox-ui.js Normal file
View File

@@ -0,0 +1,48 @@
window.WarpBoxUI = (() => {
let toastTimer = null;
function toast(message, type = "info", options = {}) {
const target = options.target || document.querySelector("#toast");
if (!target) return;
target.textContent = message;
target.classList.remove("toast-info", "toast-warning", "toast-error", "is-visible");
target.classList.add(`toast-${type}`, "is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => target.classList.remove("is-visible"), options.duration || 2600);
}
function popupElements(options = {}) {
return {
popup: options.popup || document.querySelector("#doc-popup"),
title: options.title || document.querySelector("#doc-popup-title"),
body: options.body || document.querySelector("#doc-popup-body"),
backdrop: options.backdrop || document.querySelector("#modal-backdrop"),
};
}
function openPopup(titleText, html, options = {}) {
const parts = popupElements(options);
if (!parts.popup || !parts.title || !parts.body) return;
parts.title.textContent = titleText;
parts.body.innerHTML = html;
parts.popup.classList.toggle("is-about-popup", Boolean(options.about));
parts.popup.classList.toggle("is-properties-popup", Boolean(options.properties));
parts.popup.classList.toggle("is-preview-popup", Boolean(options.preview));
parts.popup.classList.add("is-visible");
parts.backdrop?.classList.add("is-visible");
}
function closePopup(options = {}) {
const parts = popupElements(options);
parts.popup?.classList.remove("is-visible", "is-about-popup", "is-properties-popup", "is-preview-popup");
parts.backdrop?.classList.remove("is-visible");
}
function renderTemplate(template, data = {}) {
return String(template).replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
return Object.prototype.hasOwnProperty.call(data, key) ? String(data[key]) : "";
});
}
return { toast, openPopup, closePopup, renderTemplate };
})();