173 lines
5.5 KiB
JavaScript
173 lines
5.5 KiB
JavaScript
const TOKEN = document.querySelector('meta[name="token"]')?.content || "";
|
|
|
|
const STATE_LABELS = {
|
|
printing: "druckt", paused: "pausiert", complete: "fertig", standby: "bereit",
|
|
heating: "heizt", error: "Fehler", cancelled: "abgebrochen", offline: "offline",
|
|
waiting: "wartet", running: "läuft",
|
|
};
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
const headers = new Headers(options.headers || {});
|
|
if (TOKEN) headers.set("Authorization", "Bearer " + TOKEN);
|
|
let body = options.body;
|
|
if (options.json !== undefined) {
|
|
headers.set("Content-Type", "application/json");
|
|
body = JSON.stringify(options.json);
|
|
}
|
|
const response = await fetch("/api/v1" + path, {
|
|
method: options.method || "GET",
|
|
headers,
|
|
body,
|
|
cache: "no-store",
|
|
});
|
|
if (!response.ok) {
|
|
let message = response.statusText;
|
|
try {
|
|
const payload = await response.json();
|
|
message = payload.error || message;
|
|
} catch (error) {
|
|
message = response.statusText;
|
|
}
|
|
throw new Error(message);
|
|
}
|
|
if (response.status === 204) return null;
|
|
const type = response.headers.get("content-type") || "";
|
|
if (type.includes("json")) return response.json();
|
|
return response;
|
|
}
|
|
|
|
function withToken(url) {
|
|
if (!url || !TOKEN) return url;
|
|
return url + (url.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(TOKEN);
|
|
}
|
|
|
|
function euro(value) {
|
|
return Number(value || 0).toLocaleString("de-DE", { style: "currency", currency: "EUR" });
|
|
}
|
|
|
|
function grams(value) {
|
|
return Number(value || 0).toLocaleString("de-DE", { maximumFractionDigits: 0 }) + " g";
|
|
}
|
|
|
|
function hours(seconds) {
|
|
seconds = Math.round(Number(seconds) || 0);
|
|
if (seconds <= 0) return "";
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
return h > 0 ? h + " h " + String(m).padStart(2, "0") + " min" : m + " min";
|
|
}
|
|
|
|
function finishClock(remainSeconds) {
|
|
const sec = Number(remainSeconds) || 0;
|
|
if (sec <= 0) return "";
|
|
const at = new Date(Date.now() + sec * 1000);
|
|
return at.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
|
|
function clockOf(iso) {
|
|
if (!iso) return "";
|
|
const at = new Date(iso);
|
|
if (Number.isNaN(at.getTime())) return "";
|
|
return at.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
|
|
function showNote(id, error, ok) {
|
|
const node = document.getElementById(id);
|
|
if (node) {
|
|
if (error) {
|
|
node.textContent = error.message || String(error);
|
|
node.className = "msg";
|
|
} else {
|
|
node.textContent = ok || "";
|
|
node.className = ok ? "ok" : "ok";
|
|
}
|
|
}
|
|
if (error) toast(error.message || String(error), "bad");
|
|
else if (ok) toast(ok, "good");
|
|
}
|
|
|
|
function icon(name, cls) {
|
|
return `<svg class="ico${cls ? " " + cls : ""}" aria-hidden="true"><use href="#i-${name}"/></svg>`;
|
|
}
|
|
|
|
function chipHtml(state, label, small) {
|
|
const text = label || STATE_LABELS[state] || state || "";
|
|
return `<span class="chip ${escapeHtml(state || "")}${small ? " sm" : ""}">${escapeHtml(text)}</span>`;
|
|
}
|
|
|
|
function stepsHtml(labels, index, compact) {
|
|
if (!labels || !labels.length) return "";
|
|
const parts = labels.map((label, i) => {
|
|
const cls = i < index ? "done" : i === index ? "now" : "";
|
|
return `<span class="step ${cls}">${escapeHtml(label)}</span>`;
|
|
});
|
|
return `<span class="steps${compact ? " compact" : ""}">${parts.join('<span class="step-sep"></span>')}</span>`;
|
|
}
|
|
|
|
function repeatText(job) {
|
|
const total = Math.max(1, Number(job.repeat_total) || 1);
|
|
if (total <= 1) return "";
|
|
return Math.min(total, (Number(job.repeat_done) || 0) + 1) + "/" + total;
|
|
}
|
|
|
|
function phaseText(job) {
|
|
const message = (job && job.message) || "";
|
|
if (!job || !job.is_ablauf || job.status !== "running") return message;
|
|
const parts = message.split(" · ");
|
|
return parts.length > 1 ? parts.slice(-1)[0] : message;
|
|
}
|
|
|
|
function jobLabel(filename) {
|
|
return (filename || "").replace(/\.gcode(\.3mf)?$/i, "").replace(/\.3mf$/i, "").replace(/^(\d+-)+/, "");
|
|
}
|
|
|
|
function toast(message, kind) {
|
|
let host = document.querySelector(".toast-host");
|
|
if (!host) {
|
|
host = document.createElement("div");
|
|
host.className = "toast-host";
|
|
host.setAttribute("role", "status");
|
|
host.setAttribute("aria-live", "polite");
|
|
document.body.appendChild(host);
|
|
}
|
|
const node = document.createElement("div");
|
|
node.className = "toast " + (kind || "good");
|
|
node.innerHTML = icon(kind === "bad" ? "alert" : "check", "lg") + "<div></div>";
|
|
node.querySelector("div").textContent = message;
|
|
host.appendChild(node);
|
|
const ttl = kind === "bad" ? 7000 : 4000;
|
|
setTimeout(() => {
|
|
node.classList.add("out");
|
|
setTimeout(() => node.remove(), 260);
|
|
}, ttl);
|
|
}
|
|
|
|
function armButton(button, armedText, onConfirm, timeoutMs) {
|
|
if (button.dataset.armed === "1") {
|
|
clearTimeout(Number(button.dataset.armTimer || 0));
|
|
button.dataset.armed = "";
|
|
button.classList.remove("confirm-armed");
|
|
if (button.dataset.idleHtml) button.innerHTML = button.dataset.idleHtml;
|
|
onConfirm();
|
|
return;
|
|
}
|
|
button.dataset.idleHtml = button.innerHTML;
|
|
button.dataset.armed = "1";
|
|
button.classList.add("confirm-armed");
|
|
button.textContent = armedText;
|
|
const timer = setTimeout(() => {
|
|
button.dataset.armed = "";
|
|
button.classList.remove("confirm-armed");
|
|
if (button.dataset.idleHtml) button.innerHTML = button.dataset.idleHtml;
|
|
}, timeoutMs || 5000);
|
|
button.dataset.armTimer = String(timer);
|
|
}
|