Grotti 3.3.0: Projekte mit Ordner, Datei-Löschen/Verschieben, Material-Archiv, UI-Polish.
This commit is contained in:
1 parent
4ddffbbeb1
commit
67a89d1794
45 files changed
+2856
-320
No files matched your search
+121
-32
@@ -340,6 +340,14 @@ function flowHtml(printer) {
|
||||
return bits.join("");
|
||||
}
|
||||
|
||||
function filamentPauseHint(printer) {
|
||||
if (!printer) return "";
|
||||
if (printer.filament_empty) return printer.pause_reason || printer.message || "Filament leer";
|
||||
const text = String(printer.pause_reason || printer.message || "");
|
||||
if (/filament|spule|material.?leer|gerissen|runout|10107/i.test(text)) return text;
|
||||
return "";
|
||||
}
|
||||
|
||||
function stripsHtml(printer, state) {
|
||||
const bits = [];
|
||||
const busy = state === "printing" || state === "paused" || state === "heating";
|
||||
@@ -348,6 +356,14 @@ function stripsHtml(printer, state) {
|
||||
if (state === "error") {
|
||||
bits.push(`<div class="strip bad">${icon("alert")}<span class="grow">${escapeHtml(printer.message || "Drucker meldet einen Fehler.")}</span><button type="button" class="warn" data-cmd="clear_error">Fehler löschen</button></div>`);
|
||||
}
|
||||
const filamentHint = state === "paused" ? filamentPauseHint(printer) : "";
|
||||
if (filamentHint) {
|
||||
bits.push(`<div class="strip warn filament-swap">
|
||||
<div class="strip-head">${icon("alert")}<span class="grow"><b>Neue Rolle in Material dem Drucker zuweisen</b></span></div>
|
||||
<span class="strip-msg">${escapeHtml(filamentHint)} — sonst zählt der Verbrauch weiter zur alten Spule. Ort = dieser Drucker, speichern, dann Weiter.</span>
|
||||
<div class="strip-actions"><a class="ghost" href="/material">Zu Material</a></div>
|
||||
</div>`);
|
||||
}
|
||||
const failed = !queueBusy && printer.queue_failed;
|
||||
if (failed) {
|
||||
const step = failed.step_label_current || "Schritt " + ((Number(failed.step_index) || 0) + 1);
|
||||
@@ -417,7 +433,11 @@ function aceSlotLabel(slot) {
|
||||
|
||||
function aceSteerHtml(printer) {
|
||||
const dryers = printer.dryers || [];
|
||||
if (!dryers.length) return "";
|
||||
const loaded = wallSpools
|
||||
.filter((s) => s.printer_host === printer.host && s.ace_slot)
|
||||
.slice()
|
||||
.sort((a, b) => Number(a.ace_slot) - Number(b.ace_slot));
|
||||
if (!dryers.length && !loaded.length) return "";
|
||||
const bits = [];
|
||||
dryers.forEach((dryer) => {
|
||||
const id = Number(dryer.id);
|
||||
@@ -443,10 +463,6 @@ function aceSteerHtml(printer) {
|
||||
</div>
|
||||
</div>`);
|
||||
});
|
||||
const loaded = wallSpools
|
||||
.filter((s) => s.printer_host === printer.host && s.ace_slot)
|
||||
.slice()
|
||||
.sort((a, b) => Number(a.ace_slot) - Number(b.ace_slot));
|
||||
if (loaded.length) {
|
||||
bits.push(`<div class="steer-ace-slots"><div class="steer-ace-head"><b>Plätze</b><span class="tiny">Sorte und Farbe aus Grotti</span></div>`);
|
||||
loaded.forEach((spool) => {
|
||||
@@ -472,16 +488,22 @@ function steerPanelHtml(printer) {
|
||||
const fan = typeof printer.fan === "number" ? Math.round(printer.fan * 100) : 0;
|
||||
const aceKey = aceSteerKey(printer);
|
||||
return `<div class="steer" data-steer-host="${escapeHtml(printer.host)}" data-ace-key="${escapeHtml(aceKey)}">
|
||||
<div class="steer-row">
|
||||
<label>${icon("nozzle")} Düse <input type="number" data-steer="nozzle" data-steer-action="nozzle" min="0" max="350" value="${escapeHtml(String(n))}"></label>
|
||||
<label>${icon("bed")} Bett <input type="number" data-steer="bed" data-steer-action="bed" min="0" max="150" value="${escapeHtml(String(b))}"></label>
|
||||
<button type="button" data-steer-go="temps">Temp</button>
|
||||
<button type="button" class="ghost" data-steer-go="heaters_off" title="Heizung und Lüfter aus">Aus</button>
|
||||
<div class="steer-group">
|
||||
<div class="steer-group-label">Heizung</div>
|
||||
<div class="steer-row">
|
||||
<label>${icon("nozzle")} Düse <input type="number" data-steer="nozzle" data-steer-action="nozzle" min="0" max="350" value="${escapeHtml(String(n))}"></label>
|
||||
<label>${icon("bed")} Bett <input type="number" data-steer="bed" data-steer-action="bed" min="0" max="150" value="${escapeHtml(String(b))}"></label>
|
||||
<button type="button" data-steer-go="temps">Temp</button>
|
||||
<button type="button" class="ghost" data-steer-go="heaters_off" title="Heizung und Lüfter aus">Aus</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="steer-row">
|
||||
<label>Speed % <input type="number" data-steer="speed" data-steer-action="speed" min="10" max="200" value="${escapeHtml(String(sp))}"></label>
|
||||
<label>Flow % <input type="number" data-steer="flow" data-steer-action="flow" min="50" max="150" value="${escapeHtml(String(flow))}"></label>
|
||||
<label>${icon("fan")} Fan % <input type="number" data-steer="fan" data-steer-action="fan" min="0" max="100" value="${escapeHtml(String(fan))}"></label>
|
||||
<div class="steer-group">
|
||||
<div class="steer-group-label">Druck</div>
|
||||
<div class="steer-row">
|
||||
<label>Speed % <input type="number" data-steer="speed" data-steer-action="speed" min="10" max="200" value="${escapeHtml(String(sp))}"></label>
|
||||
<label>Flow % <input type="number" data-steer="flow" data-steer-action="flow" min="50" max="150" value="${escapeHtml(String(flow))}"></label>
|
||||
<label>${icon("fan")} Fan % <input type="number" data-steer="fan" data-steer-action="fan" min="0" max="100" value="${escapeHtml(String(fan))}"></label>
|
||||
</div>
|
||||
</div>
|
||||
${aceSteerHtml(printer)}
|
||||
<p class="tiny">Enter im Feld setzt nur diesen Wert.</p>
|
||||
@@ -564,19 +586,24 @@ function factsHtml(printer) {
|
||||
`;
|
||||
}
|
||||
|
||||
function factChip(label, value, ico) {
|
||||
if (!value && value !== 0) return "";
|
||||
return `<span class="fact-chip">${ico || ""}<span class="fact-chip-label">${escapeHtml(label)}</span><b>${escapeHtml(String(value))}</b></span>`;
|
||||
}
|
||||
|
||||
function techFactsHtml(printer) {
|
||||
const part = fanPct(printer, "part") || fanPct(printer);
|
||||
const chamberFan = fanPct(printer, "chamber");
|
||||
const auxFan = fanPct(printer, "aux");
|
||||
const speed = speedPct(printer);
|
||||
return `
|
||||
${part ? "<span class=\"fact-ico\">" + icon("fan") + " Teil <b>" + part + "</b></span>" : ""}
|
||||
${chamberFan ? "<span class=\"fact-ico\">" + icon("fan-chamber") + " Bauraum <b>" + chamberFan + "</b></span>" : ""}
|
||||
${auxFan ? "<span class=\"fact-ico\">" + icon("fan-aux") + " Aux <b>" + auxFan + "</b></span>" : ""}
|
||||
${speed ? "<span>Speed <b>" + speed + "</b></span>" : ""}
|
||||
${anycubicTaskLine(printer)}
|
||||
<span>Host <b>${escapeHtml(printer.host)}</b></span>
|
||||
`;
|
||||
return [
|
||||
part ? factChip("Teil", part, icon("fan")) : "",
|
||||
chamberFan ? factChip("Bauraum", chamberFan, icon("fan-chamber")) : "",
|
||||
auxFan ? factChip("Aux", auxFan, icon("fan-aux")) : "",
|
||||
speed ? factChip("Speed", speed) : "",
|
||||
anycubicTaskLine(printer),
|
||||
factChip("Host", printer.host),
|
||||
].filter(Boolean).join("");
|
||||
}
|
||||
|
||||
function anycubicTaskLine(printer) {
|
||||
@@ -584,13 +611,11 @@ function anycubicTaskLine(printer) {
|
||||
if (!task || typeof task !== "object") return "";
|
||||
const state = String(task.state || "").trim();
|
||||
const tid = task.task_id != null && String(task.task_id).trim() ? String(task.task_id) : "";
|
||||
const file = task.filename ? String(task.filename) : "";
|
||||
if (!state && !tid && !file) return "";
|
||||
if (!state && !tid) return "";
|
||||
const bits = [];
|
||||
if (state) bits.push(state);
|
||||
if (tid) bits.push("task " + tid);
|
||||
if (file) bits.push(file);
|
||||
return "<span>Anycubic <b>" + escapeHtml(bits.join(" · ")) + "</b></span>";
|
||||
if (tid) bits.push("#" + tid);
|
||||
return factChip("Anycubic", bits.join(" · "));
|
||||
}
|
||||
|
||||
function jobLineVisible(printer, state, filename) {
|
||||
@@ -626,12 +651,15 @@ function buildCard(printer) {
|
||||
article.dataset.needsClear = needsClear(printer) ? "1" : "";
|
||||
article.innerHTML = `
|
||||
<div class="machine-head">
|
||||
<div class="machine-title">
|
||||
<h2></h2>
|
||||
${badge}
|
||||
<span data-role="chip"></span>
|
||||
<div class="machine-head-main">
|
||||
<div class="machine-title">
|
||||
<h2></h2>
|
||||
${badge}
|
||||
<span data-role="chip"></span>
|
||||
</div>
|
||||
<div data-role="hero"></div>
|
||||
</div>
|
||||
<div data-role="hero"></div>
|
||||
<img class="wall-cam-thumb" data-role="wall-cam" alt="" hidden>
|
||||
</div>
|
||||
<div class="machine-body">
|
||||
<div data-role="bar"></div>
|
||||
@@ -655,6 +683,59 @@ function buildCard(printer) {
|
||||
return article;
|
||||
}
|
||||
|
||||
/** Wand-Thumb immer Snapshot — nie Stream, unabhängig von der Browser-Einstellung. */
|
||||
function wallCamSnapshotUrl(printer) {
|
||||
if (!printer || !printer.camera || printer.has_camera === false) return "";
|
||||
return withToken(printer.camera) + "&t=" + Math.floor(Date.now() / 3000);
|
||||
}
|
||||
|
||||
function patchWallCam(article, printer) {
|
||||
const img = article.querySelector('[data-role="wall-cam"]');
|
||||
const head = article.querySelector(".machine-head");
|
||||
if (!img) return;
|
||||
const next = wallCamSnapshotUrl(printer);
|
||||
if (!next) {
|
||||
img.hidden = true;
|
||||
img.removeAttribute("src");
|
||||
img.dataset.srcKey = "";
|
||||
img.dataset.pending = "";
|
||||
if (head) head.classList.remove("has-cam");
|
||||
return;
|
||||
}
|
||||
img.alt = "Kamera " + (printer.name || printer.host || "");
|
||||
if (img.dataset.srcKey === next) {
|
||||
img.hidden = false;
|
||||
if (head) head.classList.add("has-cam");
|
||||
return;
|
||||
}
|
||||
img.dataset.pending = next;
|
||||
const probe = new Image();
|
||||
probe.onload = () => {
|
||||
if (img.dataset.pending !== next) return;
|
||||
img.dataset.srcKey = next;
|
||||
img.dataset.pending = "";
|
||||
img.src = next;
|
||||
img.hidden = false;
|
||||
if (head) head.classList.add("has-cam");
|
||||
};
|
||||
probe.onerror = () => {
|
||||
if (img.dataset.pending !== next) return;
|
||||
img.dataset.pending = "";
|
||||
img.hidden = true;
|
||||
img.removeAttribute("src");
|
||||
img.dataset.srcKey = "";
|
||||
if (head) head.classList.remove("has-cam");
|
||||
};
|
||||
probe.src = next;
|
||||
}
|
||||
|
||||
function refreshWallCams() {
|
||||
for (const [host, article] of cardNodes.entries()) {
|
||||
const printer = printerCache.get(host);
|
||||
if (printer) patchWallCam(article, printer);
|
||||
}
|
||||
}
|
||||
|
||||
function steerHasFocus(root) {
|
||||
const active = document.activeElement;
|
||||
return !!(active && root.contains(active) && active.matches("input, select, textarea, button"));
|
||||
@@ -772,6 +853,7 @@ function patchCard(article, printer) {
|
||||
setHtml(role("flow"), flowHtml(printer));
|
||||
setHtml(role("strips"), stripsHtml(printer, state));
|
||||
setHtml(role("controls"), actionsHtml(state, printer));
|
||||
patchWallCam(article, printer);
|
||||
}
|
||||
|
||||
function refreshDeskCamera(printer) {
|
||||
@@ -1618,6 +1700,12 @@ async function handleWallClick(event) {
|
||||
}
|
||||
button.disabled = true;
|
||||
try {
|
||||
if (button.dataset.cmd === "fortsetzen") {
|
||||
const card = printerCache.get(host);
|
||||
if (card && filamentPauseHint(card)) {
|
||||
toast("Neue Spule unter Material dem Drucker zuweisen — sonst bleibt der Abzug bei der alten Rolle.");
|
||||
}
|
||||
}
|
||||
await api("/drucker/" + encodeURIComponent(host) + "/" + button.dataset.cmd, { method: "POST", json: {} });
|
||||
showNote("note", null);
|
||||
refresh();
|
||||
@@ -1665,6 +1753,7 @@ deskEl.addEventListener("keydown", async (event) => {
|
||||
|
||||
refresh().catch((error) => showNote("note", error));
|
||||
createPoller(() => refresh().catch((error) => showNote("note", error)), 12000).start();
|
||||
setInterval(refreshWallCams, 3000);
|
||||
setInterval(() => {
|
||||
api("/dateien").then((rows) => { libraryFiles = rows || []; }).catch(() => {});
|
||||
api("/spulen").then((rows) => { wallSpools = rows || []; }).catch(() => {});
|
||||
|
||||
Reference in new issue
Block a user