2025-08-11 00:15:58 +02:00
|
|
|
/** biome-ignore-all lint/correctness/noUnusedVariables: utility */
|
|
|
|
|
|
|
|
function storeValue(key, value = false) {
|
|
|
|
if (value === null || value === undefined || value === false) {
|
2025-08-09 21:16:24 +02:00
|
|
|
localStorage.removeItem(key);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
localStorage.setItem(key, JSON.stringify(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadValue(key, fallback = false) {
|
|
|
|
const raw = localStorage.getItem(key);
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
if (raw === null) {
|
2025-08-09 21:16:24 +02:00
|
|
|
return fallback;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const value = JSON.parse(raw);
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
if (value === null) {
|
2025-08-09 21:16:24 +02:00
|
|
|
throw new Error("no value");
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
return fallback;
|
|
|
|
}
|
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
function schedule(cb) {
|
|
|
|
if (document.visibilityState === "visible") {
|
|
|
|
requestAnimationFrame(cb);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setTimeout(cb, 80);
|
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
function uid() {
|
|
|
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function make(tag, ...classes) {
|
|
|
|
const el = document.createElement(tag);
|
|
|
|
|
2025-08-10 16:38:02 +02:00
|
|
|
if (classes.length) {
|
|
|
|
el.classList.add(...classes);
|
|
|
|
}
|
2025-08-09 21:16:24 +02:00
|
|
|
|
|
|
|
return el;
|
|
|
|
}
|
|
|
|
|
|
|
|
function escapeHtml(text) {
|
|
|
|
return text
|
|
|
|
.replace(/&/g, "&")
|
|
|
|
.replace(/</g, "<")
|
|
|
|
.replace(/>/g, ">");
|
|
|
|
}
|
2025-08-11 15:43:00 +02:00
|
|
|
|
|
|
|
function formatMilliseconds(ms) {
|
|
|
|
if (ms < 1000) {
|
|
|
|
return `${ms}ms`;
|
|
|
|
} else if (ms < 10000) {
|
|
|
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return `${Math.round(ms / 1000)}s`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function fixed(num, decimals = 0) {
|
|
|
|
return num.toFixed(decimals).replace(/\.?0+$/m, "");
|
|
|
|
}
|
2025-08-16 14:54:27 +02:00
|
|
|
|
|
|
|
function download(name, type, data) {
|
|
|
|
let blob;
|
|
|
|
|
|
|
|
if (data instanceof Blob) {
|
|
|
|
blob = data;
|
|
|
|
} else {
|
|
|
|
blob = new Blob([data], {
|
|
|
|
type: type,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const a = document.createElement("a"),
|
|
|
|
url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
a.setAttribute("download", name);
|
|
|
|
a.style.display = "none";
|
|
|
|
a.href = url;
|
|
|
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectFile(accept) {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
const input = make("input");
|
|
|
|
|
|
|
|
input.type = "file";
|
|
|
|
input.accept = accept;
|
|
|
|
|
|
|
|
input.onchange = () => {
|
|
|
|
const file = input.files[0];
|
|
|
|
|
|
|
|
if (!file) {
|
|
|
|
resolve(false);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
reader.onload = () => {
|
|
|
|
try {
|
|
|
|
const data = JSON.parse(reader.result);
|
|
|
|
|
|
|
|
resolve(data);
|
|
|
|
} catch {
|
|
|
|
resolve(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
reader.onerror = () => resolve(false);
|
|
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
};
|
|
|
|
|
|
|
|
input.click();
|
|
|
|
});
|
|
|
|
}
|