Jetzt/Topbar und Material-Routing, Pause bei Filament-Ende, Druckbericht mit Stromanteilen, Design- und Kamera-Stream-Polish; Bedarf-Alert-Spam und Platte-belegt-Bugs behoben.
137 lines
4.6 KiB
HTML
137 lines
4.6 KiB
HTML
{% extends "layout.html" %}
|
|
{% block body %}
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>Kameras</h1>
|
|
<p class="tiny">Livebilder aller Drucker.</p>
|
|
</div>
|
|
<div class="page-head-side">
|
|
<label class="field cam-mode-field">
|
|
<span class="tiny">Bild</span>
|
|
<select id="cam-mode" title="Kamera-Modus">
|
|
<option value="snapshot">Snapshot</option>
|
|
<option value="stream">Stream</option>
|
|
</select>
|
|
</label>
|
|
<span id="when"></span>
|
|
</div>
|
|
</div>
|
|
<div class="camera-grid" id="grid"></div>
|
|
{% endblock %}
|
|
{% block script %}
|
|
<script>
|
|
const tiles = new Map();
|
|
|
|
(function initCamMode() {
|
|
const sel = document.getElementById("cam-mode");
|
|
if (!sel || typeof cameraMode !== "function") return;
|
|
sel.value = cameraMode();
|
|
sel.addEventListener("change", () => {
|
|
setCameraMode(sel.value);
|
|
for (const tile of tiles.values()) {
|
|
const img = tile.querySelector('[data-role="cam"]');
|
|
if (img) img.dataset.srcKey = "";
|
|
}
|
|
refresh().catch(() => {});
|
|
});
|
|
window.addEventListener("grotti-camera-mode", () => {
|
|
sel.value = cameraMode();
|
|
});
|
|
})();
|
|
|
|
function metaLine(printer) {
|
|
const state = shownState(printer);
|
|
const busy = state === "printing" || state === "paused";
|
|
const bits = [chipHtml(state, STATE_LABELS[state] || state)];
|
|
if (busy) {
|
|
const pct = Math.round((printer.progress || 0) * 100);
|
|
bits.push(pct + " %");
|
|
const eta = finishClock(printer.remain);
|
|
if (eta) bits.push("fertig " + eta);
|
|
else if (hours(printer.remain)) bits.push("Rest " + hours(printer.remain));
|
|
}
|
|
const job = (printer.filename || "").replace(/\.gcode$/i, "").replace(/^(\d+-)+/, "");
|
|
if (busy && job) bits.push(escapeHtml(job));
|
|
return bits.join(" · ");
|
|
}
|
|
|
|
function ensureTile(printer) {
|
|
let tile = tiles.get(printer.host);
|
|
if (tile) return tile;
|
|
tile = document.createElement("figure");
|
|
tile.className = "camera-tile";
|
|
tile.dataset.host = printer.host;
|
|
tile.innerHTML = `
|
|
<figcaption>
|
|
<span class="cam-name"></span>
|
|
<span class="host"></span>
|
|
</figcaption>
|
|
<p class="cam-meta tiny"></p>
|
|
<div class="cam-stage">
|
|
<img alt="" data-role="cam" hidden>
|
|
<p class="cam-empty" data-role="cam-empty">${icon("camera")}<span>Keine Kamera</span></p>
|
|
</div>
|
|
<a class="ghost cam-open" href="/#${encodeURIComponent(printer.host)}">Cockpit</a>`;
|
|
tiles.set(printer.host, tile);
|
|
document.getElementById("grid").appendChild(tile);
|
|
return tile;
|
|
}
|
|
|
|
async function refresh() {
|
|
const printers = await api("/drucker?live=1");
|
|
document.getElementById("when").textContent = new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
|
const grid = document.getElementById("grid");
|
|
if (!printers.length) {
|
|
tiles.clear();
|
|
grid.innerHTML = `<div class="empty-state">${icon("camera", "lg")}<p>Noch keine Drucker.</p><a href="/einstellungen">Zu den Einstellungen</a></div>`;
|
|
return;
|
|
}
|
|
if (grid.querySelector(".empty-state")) grid.innerHTML = "";
|
|
const streaming = cameraMode() === "stream";
|
|
const seen = new Set();
|
|
for (const printer of printers) {
|
|
seen.add(printer.host);
|
|
const tile = ensureTile(printer);
|
|
const state = shownState(printer);
|
|
tile.className = "camera-tile " + state + (streaming ? " stream" : "");
|
|
tile.querySelector(".cam-name").textContent = printer.name;
|
|
tile.querySelector(".host").textContent = printer.host;
|
|
tile.querySelector(".cam-meta").innerHTML = metaLine(printer);
|
|
tile.querySelector(".cam-open").href = "/#" + encodeURIComponent(printer.host);
|
|
const img = tile.querySelector('[data-role="cam"]');
|
|
const empty = tile.querySelector('[data-role="cam-empty"]');
|
|
if (!printer.camera || printer.has_camera === false) {
|
|
img.removeAttribute("src");
|
|
img.hidden = true;
|
|
empty.hidden = false;
|
|
empty.innerHTML = icon("camera") + "<span>Keine Kamera</span>";
|
|
img.dataset.srcKey = "";
|
|
continue;
|
|
}
|
|
img.alt = "Kamera " + printer.name;
|
|
const next = cameraUrl(printer, streaming ? 0 : 2000);
|
|
if (img.dataset.srcKey === next) continue;
|
|
img.dataset.srcKey = next;
|
|
img.onerror = () => {
|
|
img.hidden = true;
|
|
empty.hidden = false;
|
|
empty.innerHTML = icon("camera") + "<span>Keine Kamera</span>";
|
|
};
|
|
img.onload = () => {
|
|
img.hidden = false;
|
|
empty.hidden = true;
|
|
};
|
|
img.src = next;
|
|
}
|
|
for (const [host, tile] of [...tiles.entries()]) {
|
|
if (!seen.has(host)) {
|
|
tile.remove();
|
|
tiles.delete(host);
|
|
}
|
|
}
|
|
}
|
|
refresh().catch((error) => console.error(error));
|
|
setInterval(() => refresh().catch(() => {}), 5000);
|
|
</script>
|
|
{% endblock %}
|