/* * Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Free for non-commercial and evaluation use under terms of BSL/GPLv3. * Commercial or production use requires a valid paid license. * See LICENSE file in repository root for details. */ let hideTimer; function toastElement(selector = "#toast") { return document.querySelector(selector); } function show(element, duration = null) { element.classList.add("visible"); clearTimeout(hideTimer); if (duration != null) hideTimer = setTimeout(() => element.classList.remove("visible"), duration); } function reset(element) { element.className = "toast"; element.removeAttribute("aria-busy"); element.removeAttribute("aria-label"); } function formatBytes(value) { const bytes = Math.max(0, Number(value) || 0); if (bytes < 1024) return `${Math.round(bytes)} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes >= 10240 ? 0 : 1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(bytes >= 10 * 1024 * 1024 ? 1 : 2)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } export function toast(message, options = {}) { const element = toastElement(options.selector); if (!element) return; reset(element); element.textContent = String(message ?? ""); show(element, options.duration ?? 1800); } export function createUploadToast(filename, options = {}) { const element = toastElement(options.selector); if (!element) { return { start() { }, update() { }, fail() { }, success() { }, dismiss() { } }; } reset(element); element.classList.add("toast--upload", "toast--interactive"); element.setAttribute("aria-live", "polite"); element.innerHTML = `
Uploading file
Preparing…
`; const title = element.querySelector(".upload-toast__title"); const filenameElement = element.querySelector(".upload-toast__filename"); const progress = element.querySelector(".upload-toast__progress"); const progressFill = progress.querySelector("span"); const amount = element.querySelector("[data-upload-amount]"); const speedElement = element.querySelector("[data-upload-speed]"); const errorElement = element.querySelector("[data-upload-error]"); const actions = element.querySelector("[data-upload-actions]"); const retryButton = element.querySelector(".upload-toast__retry"); const dismissButton = element.querySelector(".upload-toast__dismiss"); let retryHandler = null; filenameElement.textContent = String(filename || "file"); function dismiss() { clearTimeout(hideTimer); element.classList.remove("visible"); } function start() { reset(element); element.classList.add("toast--upload", "toast--interactive", "visible"); element.setAttribute("aria-live", "polite"); element.setAttribute("aria-busy", "true"); title.textContent = "Uploading file"; filenameElement.textContent = String(filename || "file"); progress.classList.remove("is-error", "is-complete"); progress.setAttribute("aria-valuenow", "0"); progressFill.style.width = "0%"; amount.textContent = "Preparing…"; speedElement.textContent = "—"; errorElement.hidden = true; errorElement.textContent = ""; actions.hidden = true; retryButton.disabled = false; clearTimeout(hideTimer); } function update({ loaded = 0, total = 0, speed = 0, percent = null, phase = "uploading" } = {}) { const normalizedPercent = Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : total > 0 ? Math.max(0, Math.min(100, (loaded / total) * 100)) : null; if (normalizedPercent != null) { progress.setAttribute("aria-valuenow", String(Math.round(normalizedPercent))); progressFill.style.width = `${normalizedPercent}%`; } else { progress.removeAttribute("aria-valuenow"); progressFill.style.width = "24%"; progress.classList.add("is-indeterminate"); } amount.textContent = total > 0 ? `${Math.round(normalizedPercent || 0)}% · ${formatBytes(loaded)} / ${formatBytes(total)}` : formatBytes(loaded); speedElement.textContent = phase === "processing" ? "Processing…" : speed > 0 ? `${formatBytes(speed)}/s` : "Starting…"; } function fail(message, { retryable = true, onRetry = null } = {}) { element.removeAttribute("aria-busy"); title.textContent = "Upload failed"; progress.classList.remove("is-indeterminate", "is-complete"); progress.classList.add("is-error"); errorElement.textContent = String(message || "Upload failed."); errorElement.hidden = false; actions.hidden = false; retryButton.hidden = !retryable; retryHandler = typeof onRetry === "function" ? onRetry : null; show(element); } function success(message = "File uploaded") { element.removeAttribute("aria-busy"); title.textContent = message; progress.classList.remove("is-error", "is-indeterminate"); progress.classList.add("is-complete"); progress.setAttribute("aria-valuenow", "100"); progressFill.style.width = "100%"; amount.textContent = "100%"; speedElement.textContent = "Complete"; errorElement.hidden = true; actions.hidden = true; show(element, 1800); } retryButton.addEventListener("click", async () => { if (!retryHandler) return; retryButton.disabled = true; await retryHandler(); }); dismissButton.addEventListener("click", dismiss); start(); return { start, update, fail, success, dismiss }; }