Files
livef12rocks/app/static/app.js
Frank Schwenk c03f17488c
CI / lint-and-test (push) Failing after 1m18s
feat: organizer Android app and token-gated upload API
Replace Syncthing as the product upload path with a share-target app
and POST /api/jobs. Web UI live-polls job progress.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 21:38:50 +02:00

161 lines
4.2 KiB
JavaScript

/* live.f12.rocks — live poll + lightbox gallery. No framework. */
(function () {
"use strict";
var POLL_MS = 4000;
function $(id) {
return document.getElementById(id);
}
/* --- live reload while jobs run --- */
function fingerprint(payload) {
if (!payload) return "";
if (payload.job_id) {
return [
payload.status,
payload.stage,
payload.updated_at,
payload.variant_count,
(payload.intermediates || []).length,
payload.error || "",
].join("|");
}
var jobs = payload.jobs || [];
return jobs
.map(function (j) {
return [j.job_id, j.status, j.updated_at, j.variant_count, j.stage].join(":");
})
.join(",");
}
function startLivePoll() {
if (!document.body.hasAttribute("data-live")) return;
var jobId = document.body.getAttribute("data-job-id");
var url = jobId ? "/api/jobs/" + encodeURIComponent(jobId) : "/api/jobs";
var last = "";
var first = true;
function tick() {
fetch(url, { headers: { Accept: "application/json" } })
.then(function (r) {
if (!r.ok) throw new Error("poll " + r.status);
return r.json();
})
.then(function (data) {
var next = fingerprint(data);
if (first) {
last = next;
first = false;
return;
}
if (next !== last) {
window.location.reload();
}
})
.catch(function () {
/* venue wifi: try again next tick */
});
}
tick();
setInterval(tick, POLL_MS);
}
/* --- lightbox with prev/next --- */
function startLightbox() {
var box = $("lightbox");
var img = $("lightbox-img");
var caption = $("lightbox-caption");
if (!box || !img) return;
var items = [];
var index = 0;
function collect() {
items = Array.prototype.slice
.call(document.querySelectorAll("img.lightboxable"))
.map(function (el) {
return {
src: el.getAttribute("data-full-src") || el.currentSrc || el.src,
alt: el.alt || "",
};
});
}
function show(i) {
if (!items.length) return;
index = (i + items.length) % items.length;
img.src = items[index].src;
img.alt = items[index].alt;
if (caption) caption.textContent = items[index].alt;
box.hidden = false;
document.body.classList.add("lightbox-open");
}
function close() {
box.hidden = true;
img.removeAttribute("src");
document.body.classList.remove("lightbox-open");
}
document.addEventListener("click", function (e) {
var target = e.target;
if (!(target instanceof HTMLImageElement)) return;
if (!target.classList.contains("lightboxable")) return;
e.preventDefault();
e.stopPropagation();
collect();
var full = target.getAttribute("data-full-src") || target.currentSrc || target.src;
var found = items.findIndex(function (it) {
return it.src === full;
});
show(found >= 0 ? found : 0);
});
box.addEventListener("click", function (e) {
if (e.target === box || (e.target && e.target.classList.contains("lightbox-close"))) {
close();
return;
}
if (e.target && e.target.classList.contains("lightbox-next")) {
show(index + 1);
}
if (e.target && e.target.classList.contains("lightbox-prev")) {
show(index - 1);
}
});
document.addEventListener("keydown", function (e) {
if (box.hidden) return;
if (e.key === "Escape") close();
if (e.key === "ArrowRight") show(index + 1);
if (e.key === "ArrowLeft") show(index - 1);
});
var startX = 0;
box.addEventListener(
"touchstart",
function (e) {
if (e.changedTouches && e.changedTouches[0]) startX = e.changedTouches[0].screenX;
},
{ passive: true }
);
box.addEventListener(
"touchend",
function (e) {
if (!e.changedTouches || !e.changedTouches[0]) return;
var dx = e.changedTouches[0].screenX - startX;
if (Math.abs(dx) < 40) return;
show(index + (dx < 0 ? 1 : -1));
},
{ passive: true }
);
}
startLivePoll();
startLightbox();
})();