license and some functions

This commit is contained in:
Mateusz Gruszczyński
2026-07-29 14:03:46 +02:00
parent 0655cfe48c
commit ef8dfc67c1
56 changed files with 2586 additions and 547 deletions
+155 -5
View File
@@ -1,10 +1,160 @@
/*
* 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;
export function toast(message, options = {}) {
const element = document.querySelector(options.selector || "#toast");
if (!element) return;
element.textContent = String(message ?? "");
function toastElement(selector = "#toast") {
return document.querySelector(selector);
}
function show(element, duration = null) {
element.classList.add("visible");
clearTimeout(hideTimer);
hideTimer = setTimeout(() => element.classList.remove("visible"), options.duration ?? 1800);
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 = `
<div class="upload-toast__header">
<div class="upload-toast__heading">
<strong class="upload-toast__title">Uploading file</strong>
<span class="upload-toast__filename"></span>
</div>
</div>
<div class="upload-toast__progress" role="progressbar" aria-label="File upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span></span></div>
<div class="upload-toast__meta"><span data-upload-amount>Preparing…</span><span data-upload-speed>—</span></div>
<p class="upload-toast__error" data-upload-error hidden></p>
<div class="upload-toast__actions" data-upload-actions hidden>
<button type="button" class="upload-toast__retry">Retry</button>
<button type="button" class="upload-toast__dismiss">Dismiss</button>
</div>`;
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 };
}