635 lines
30 KiB
JavaScript
635 lines
30 KiB
JavaScript
(() => {
|
||
async function jsonRequest(url, options = {}) {
|
||
const response = await fetch(url, {
|
||
credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
|
||
...options
|
||
});
|
||
const payload = await response.json().catch(() => ({ ok: false, error: "Lumi returned an unexpected reply." }));
|
||
if (!response.ok) throw new Error(payload.error || "The request did not work.");
|
||
return payload;
|
||
}
|
||
|
||
document.querySelectorAll("[data-copy-value]").forEach((button) => {
|
||
button.addEventListener("click", async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(button.dataset.copyValue || "");
|
||
const original = button.textContent;
|
||
button.textContent = "Copied";
|
||
setTimeout(() => { button.textContent = original; }, 1400);
|
||
} catch {
|
||
button.closest(".overlay-url-row")?.querySelector("input")?.select();
|
||
}
|
||
});
|
||
});
|
||
|
||
function wireModal(modalSelector, openSelector, closeSelector) {
|
||
const modal = document.querySelector(modalSelector);
|
||
if (!modal) return;
|
||
const open = () => {
|
||
modal.classList.add("is-open");
|
||
modal.setAttribute("aria-hidden", "false");
|
||
modal.querySelector("input")?.focus();
|
||
};
|
||
const close = () => {
|
||
modal.classList.remove("is-open");
|
||
modal.setAttribute("aria-hidden", "true");
|
||
};
|
||
document.querySelectorAll(openSelector).forEach((button) => button.addEventListener("click", open));
|
||
modal.querySelectorAll(closeSelector).forEach((button) => button.addEventListener("click", close));
|
||
modal.addEventListener("click", (event) => { if (event.target === modal) close(); });
|
||
document.addEventListener("keydown", (event) => { if (event.key === "Escape") close(); });
|
||
}
|
||
|
||
wireModal("[data-overlay-create-modal]", "[data-overlay-create-open]", "[data-overlay-create-close]");
|
||
wireModal("[data-scene-create-modal]", "[data-scene-create-open]", "[data-scene-create-close]");
|
||
|
||
const settingsColumn = document.querySelector(".overlay-settings-column");
|
||
if (settingsColumn) {
|
||
let layoutFrame = null;
|
||
const syncSettingsHeight = () => {
|
||
layoutFrame = null;
|
||
if (!window.matchMedia("(min-width: 1180px)").matches) {
|
||
settingsColumn.style.removeProperty("--overlay-settings-max-height");
|
||
return;
|
||
}
|
||
const gap = 24;
|
||
const top = Math.max(gap, settingsColumn.getBoundingClientRect().top);
|
||
settingsColumn.style.setProperty("--overlay-settings-max-height", `${Math.max(240, Math.floor(window.innerHeight - top - gap))}px`);
|
||
};
|
||
const scheduleSettingsHeight = () => {
|
||
if (layoutFrame === null) layoutFrame = window.requestAnimationFrame(syncSettingsHeight);
|
||
};
|
||
window.addEventListener("resize", scheduleSettingsHeight);
|
||
window.addEventListener("scroll", scheduleSettingsHeight, { passive: true });
|
||
syncSettingsHeight();
|
||
}
|
||
|
||
const supportedHexColor = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
||
const pickerValue = (value) => {
|
||
if (!supportedHexColor.test(value)) return null;
|
||
const digits = value.slice(1);
|
||
return digits.length <= 4
|
||
? `#${digits.slice(0, 3).split("").map((digit) => digit + digit).join("")}`
|
||
: `#${digits.slice(0, 6)}`;
|
||
};
|
||
const alphaValue = (value) => {
|
||
if (!supportedHexColor.test(value)) return "";
|
||
const digits = value.slice(1);
|
||
if (digits.length === 4) return digits[3] + digits[3];
|
||
if (digits.length === 8) return digits.slice(6);
|
||
return "";
|
||
};
|
||
|
||
document.querySelectorAll("[data-color-field]").forEach((field) => {
|
||
const textInput = field.querySelector("[data-color-text]");
|
||
const picker = field.querySelector("[data-color-picker]");
|
||
if (!textInput || !picker) return;
|
||
textInput.addEventListener("input", () => {
|
||
const nextPickerValue = pickerValue(textInput.value.trim());
|
||
if (nextPickerValue) picker.value = nextPickerValue;
|
||
});
|
||
picker.addEventListener("input", () => {
|
||
textInput.value = `${picker.value}${alphaValue(textInput.value.trim())}`;
|
||
textInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll("[data-reorder-form]").forEach((form) => {
|
||
const list = form.querySelector("[data-reorder-list]");
|
||
const value = form.querySelector("[data-reorder-value]");
|
||
if (!list || !value) return;
|
||
const sync = () => {
|
||
value.value = JSON.stringify(Array.from(list.querySelectorAll("[data-reorder-id]")).map((item) => item.dataset.reorderId));
|
||
};
|
||
list.addEventListener("click", (event) => {
|
||
const item = event.target.closest("[data-reorder-id]");
|
||
if (!item) return;
|
||
const next = item.nextElementSibling;
|
||
if (next) next.after(item);
|
||
else list.prepend(item);
|
||
sync();
|
||
});
|
||
form.addEventListener("submit", sync);
|
||
sync();
|
||
});
|
||
|
||
function syncModuleFields(form) {
|
||
const select = form.querySelector("[data-module-type-select]");
|
||
if (!select) return;
|
||
form.querySelectorAll("[data-module-fields]").forEach((group) => {
|
||
const visible = group.dataset.moduleFields === select.value;
|
||
group.hidden = !visible;
|
||
group.querySelectorAll("input, select, textarea, button").forEach((field) => { field.disabled = !visible; });
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll("[data-module-editor]").forEach((form) => {
|
||
const select = form.querySelector("[data-module-type-select]");
|
||
select?.addEventListener("change", () => syncModuleFields(form));
|
||
syncModuleFields(form);
|
||
});
|
||
|
||
const editorRoot = document.querySelector("[data-overlay-editor]");
|
||
const editorData = document.querySelector("[data-overlay-editor-state]");
|
||
const renderer = window.LumiOverlayRenderer;
|
||
let editorApi = null;
|
||
|
||
if (editorRoot && editorData && renderer) {
|
||
const state = JSON.parse(editorData.textContent || "{}");
|
||
const savedModules = new Map(state.scenes.flatMap((scene) => scene.modules).map((module) => [module.id, JSON.parse(JSON.stringify(module))]));
|
||
const canvas = editorRoot.querySelector("[data-overlay-canvas]");
|
||
const layer = editorRoot.querySelector("[data-overlay-canvas-layer]");
|
||
const sceneSelect = editorRoot.querySelector("[data-editor-scene]");
|
||
const snapToggle = editorRoot.querySelector("[data-editor-snap]");
|
||
const snapHint = editorRoot.querySelector("[data-snap-hint]");
|
||
const nameReadout = editorRoot.querySelector("[data-selected-name]");
|
||
const positionReadout = editorRoot.querySelector("[data-selected-position]");
|
||
const saveState = editorRoot.querySelector("[data-editor-save-state]");
|
||
const sourceList = editorRoot.querySelector("[data-canvas-source-list]");
|
||
const guideX = editorRoot.querySelector("[data-guide-x]");
|
||
const guideY = editorRoot.querySelector("[data-guide-y]");
|
||
const anchorFactors = {
|
||
"top-left": [0, 0], "top-center": [0.5, 0], "top-right": [1, 0],
|
||
"center-left": [0, 0.5], center: [0.5, 0.5], "center-right": [1, 0.5],
|
||
"bottom-left": [0, 1], "bottom-center": [0.5, 1], "bottom-right": [1, 1]
|
||
};
|
||
let selectedId = null;
|
||
let drag = null;
|
||
let elements = new Map();
|
||
const previewRequests = new Map();
|
||
|
||
const currentScene = () => state.scenes.find((scene) => scene.id === sceneSelect.value) || state.scenes[0];
|
||
const selectedModule = () => currentScene()?.modules.find((module) => module.id === selectedId) || null;
|
||
const round = (number) => Math.round(number * 100) / 100;
|
||
const clamp = (number, min, max) => Math.min(max, Math.max(min, number));
|
||
|
||
function edges(config) {
|
||
const [ax, ay] = anchorFactors[config.anchor] || anchorFactors["top-left"];
|
||
const width = Number(config.width || 0);
|
||
const height = Number(config.height || 0);
|
||
const left = Number(config.x || 0) - ax * width;
|
||
const top = Number(config.y || 0) - ay * height;
|
||
return { left, top, right: left + width, bottom: top + height, width, height, ax, ay };
|
||
}
|
||
|
||
function configFromEdges(box, anchor) {
|
||
const [ax, ay] = anchorFactors[anchor] || anchorFactors["top-left"];
|
||
const width = clamp(box.right - box.left, 0.5, 100);
|
||
const height = clamp(box.bottom - box.top, 0.5, 100);
|
||
return {
|
||
x: clamp(box.left + ax * width, 0, 100),
|
||
y: clamp(box.top + ay * height, 0, 100),
|
||
width,
|
||
height
|
||
};
|
||
}
|
||
|
||
function updateReadout() {
|
||
const module = selectedModule();
|
||
if (!module) {
|
||
nameReadout.textContent = "No source selected";
|
||
positionReadout.textContent = "";
|
||
return;
|
||
}
|
||
const config = module.config;
|
||
nameReadout.textContent = module.name;
|
||
positionReadout.textContent = `X ${round(config.x)}% · Y ${round(config.y)}% · ${round(config.width)} × ${round(config.height)}%`;
|
||
}
|
||
|
||
function syncForm(module) {
|
||
const form = document.querySelector(`[data-module-editor][data-module-id="${CSS.escape(module.id)}"]`);
|
||
if (!form) return;
|
||
for (const [name, key] of [["x", "x"], ["y", "y"], ["width", "width"], ["height", "height"], ["anchor", "anchor"]]) {
|
||
const field = form.elements[name];
|
||
if (field) field.value = module.config[key];
|
||
}
|
||
}
|
||
|
||
function formBody(form) {
|
||
const body = {};
|
||
for (const [key, value] of new FormData(form).entries()) body[key] = value;
|
||
return body;
|
||
}
|
||
|
||
function setPreviewStatus(form, message, stateName = "") {
|
||
const status = form.querySelector("[data-source-preview-status]");
|
||
if (status) status.textContent = message;
|
||
form.closest("[data-module-card]")?.classList.toggle("is-dirty", stateName === "dirty");
|
||
if (stateName === "dirty") saveState.textContent = "Unsaved preview";
|
||
}
|
||
|
||
function moduleForId(moduleId) {
|
||
return state.scenes.flatMap((scene) => scene.modules).find((module) => module.id === moduleId) || null;
|
||
}
|
||
|
||
function previewForm(form) {
|
||
const moduleId = form.dataset.moduleId;
|
||
const module = moduleForId(moduleId);
|
||
if (!module) return;
|
||
clearTimeout(previewRequests.get(moduleId)?.timer);
|
||
previewRequests.get(moduleId)?.controller?.abort();
|
||
setPreviewStatus(form, "Previewing unsaved changes…", "dirty");
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(async () => {
|
||
try {
|
||
const payload = await jsonRequest(`/api/admin/overlays/${state.overlayId}/modules/${moduleId}/preview`, {
|
||
method: "POST",
|
||
body: JSON.stringify(formBody(form)),
|
||
signal: controller.signal
|
||
});
|
||
Object.assign(module, payload.module);
|
||
selectedId = moduleId;
|
||
renderCanvas();
|
||
setPreviewStatus(form, "Live preview · not saved", "dirty");
|
||
} catch (error) {
|
||
if (error.name !== "AbortError") setPreviewStatus(form, `Preview problem: ${error.message}`, "dirty");
|
||
}
|
||
}, 280);
|
||
previewRequests.set(moduleId, { controller, timer });
|
||
}
|
||
|
||
document.querySelectorAll("[data-module-editor][data-module-id]").forEach((form) => {
|
||
const update = () => previewForm(form);
|
||
form.addEventListener("input", update);
|
||
form.addEventListener("change", update);
|
||
form.addEventListener("submit", () => {
|
||
setPreviewStatus(form, "Saving…");
|
||
saveState.textContent = "Saving…";
|
||
});
|
||
form.querySelector("[data-source-revert]")?.addEventListener("click", () => {
|
||
const saved = savedModules.get(form.dataset.moduleId);
|
||
const module = moduleForId(form.dataset.moduleId);
|
||
if (!saved || !module) return;
|
||
previewRequests.get(module.id)?.controller?.abort();
|
||
clearTimeout(previewRequests.get(module.id)?.timer);
|
||
form.reset();
|
||
syncModuleFields(form);
|
||
Object.assign(module, JSON.parse(JSON.stringify(saved)));
|
||
selectedId = module.id;
|
||
renderCanvas();
|
||
setPreviewStatus(form, "Saved settings restored");
|
||
saveState.textContent = "Ready";
|
||
});
|
||
});
|
||
|
||
function selectSource(moduleId, { openSettings = false } = {}) {
|
||
selectedId = moduleId;
|
||
layer.querySelectorAll(".lumi-overlay-module").forEach((element) => element.classList.toggle("is-selected", element.dataset.moduleId === moduleId));
|
||
sourceList.querySelectorAll("[data-canvas-select]").forEach((button) => button.classList.toggle("is-selected", button.dataset.canvasSelect === moduleId));
|
||
updateReadout();
|
||
if (openSettings) {
|
||
const card = document.querySelector(`[data-module-card="${CSS.escape(moduleId)}"]`);
|
||
if (card) card.open = true;
|
||
}
|
||
canvas.focus({ preventScroll: true });
|
||
}
|
||
|
||
function renderCanvas() {
|
||
const scene = currentScene();
|
||
elements = renderer.render(layer, { enabled: true, canvas: state.canvas, scene }, { editor: true, canvas: state.canvas });
|
||
sourceList.replaceChildren();
|
||
for (const module of scene?.modules || []) {
|
||
const button = document.createElement("button");
|
||
button.type = "button";
|
||
button.className = "button subtle";
|
||
button.dataset.canvasSelect = module.id;
|
||
button.textContent = `${module.name}${module.enabled ? "" : " (hidden)"}`;
|
||
button.disabled = !module.enabled;
|
||
button.addEventListener("click", () => selectSource(module.id, { openSettings: true }));
|
||
sourceList.appendChild(button);
|
||
}
|
||
if (!elements.has(selectedId)) selectedId = elements.keys().next().value || null;
|
||
if (selectedId) selectSource(selectedId);
|
||
else updateReadout();
|
||
}
|
||
|
||
function showGuide(axis, value) {
|
||
const guide = axis === "x" ? guideX : guideY;
|
||
guide.style[axis === "x" ? "left" : "top"] = `${value}%`;
|
||
guide.classList.add("is-visible");
|
||
}
|
||
|
||
function clearGuides() {
|
||
guideX.classList.remove("is-visible");
|
||
guideY.classList.remove("is-visible");
|
||
}
|
||
|
||
function snapTargets(moduleId, axis) {
|
||
const result = [0, 50, 100];
|
||
for (const module of currentScene()?.modules || []) {
|
||
if (module.id === moduleId || !module.enabled) continue;
|
||
const box = edges(module.config);
|
||
result.push(axis === "x" ? box.left : box.top, axis === "x" ? (box.left + box.right) / 2 : (box.top + box.bottom) / 2, axis === "x" ? box.right : box.bottom);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function snapAdjustment(points, targets, threshold) {
|
||
let best = null;
|
||
for (const point of points) for (const target of targets) {
|
||
const difference = target - point;
|
||
if (Math.abs(difference) <= threshold && (!best || Math.abs(difference) < Math.abs(best.difference))) best = { difference, target };
|
||
}
|
||
return best;
|
||
}
|
||
|
||
function snappingActive(event) {
|
||
return Boolean(snapToggle.checked) !== Boolean(event.ctrlKey || event.metaKey);
|
||
}
|
||
|
||
function applyDrag(event) {
|
||
if (!drag) return;
|
||
const module = selectedModule();
|
||
if (!module) return;
|
||
const dx = ((event.clientX - drag.startX) / drag.rect.width) * 100;
|
||
const dy = ((event.clientY - drag.startY) / drag.rect.height) * 100;
|
||
const startBox = edges(drag.config);
|
||
let box = { ...startBox };
|
||
if (drag.handle) {
|
||
if (drag.handle.includes("w")) box.left += dx;
|
||
if (drag.handle.includes("e")) box.right += dx;
|
||
if (drag.handle.includes("n")) box.top += dy;
|
||
if (drag.handle.includes("s")) box.bottom += dy;
|
||
if (box.right - box.left < 0.5) box.right = box.left + 0.5;
|
||
if (box.bottom - box.top < 0.5) box.bottom = box.top + 0.5;
|
||
} else {
|
||
box.left += dx; box.right += dx; box.top += dy; box.bottom += dy;
|
||
}
|
||
clearGuides();
|
||
if (snappingActive(event)) {
|
||
const thresholdX = (7 / drag.rect.width) * 100;
|
||
const thresholdY = (7 / drag.rect.height) * 100;
|
||
const xPoints = drag.handle
|
||
? [drag.handle.includes("w") ? box.left : drag.handle.includes("e") ? box.right : (box.left + box.right) / 2]
|
||
: [box.left, (box.left + box.right) / 2, box.right];
|
||
const yPoints = drag.handle
|
||
? [drag.handle.includes("n") ? box.top : drag.handle.includes("s") ? box.bottom : (box.top + box.bottom) / 2]
|
||
: [box.top, (box.top + box.bottom) / 2, box.bottom];
|
||
const snapX = snapAdjustment(xPoints, snapTargets(module.id, "x"), thresholdX);
|
||
const snapY = snapAdjustment(yPoints, snapTargets(module.id, "y"), thresholdY);
|
||
if (snapX) {
|
||
if (drag.handle?.includes("w")) box.left += snapX.difference;
|
||
else if (drag.handle?.includes("e")) box.right += snapX.difference;
|
||
else { box.left += snapX.difference; box.right += snapX.difference; }
|
||
showGuide("x", snapX.target);
|
||
}
|
||
if (snapY) {
|
||
if (drag.handle?.includes("n")) box.top += snapY.difference;
|
||
else if (drag.handle?.includes("s")) box.bottom += snapY.difference;
|
||
else { box.top += snapY.difference; box.bottom += snapY.difference; }
|
||
showGuide("y", snapY.target);
|
||
}
|
||
}
|
||
Object.assign(module.config, configFromEdges(box, module.config.anchor || "top-left"));
|
||
renderer.applyBox(elements.get(module.id), module.config);
|
||
syncForm(module);
|
||
updateReadout();
|
||
}
|
||
|
||
async function saveTransform(module) {
|
||
if (!module) return;
|
||
saveState.textContent = "Saving…";
|
||
try {
|
||
const payload = await jsonRequest(`/api/admin/overlays/${state.overlayId}/modules/${module.id}/transform`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
x: module.config.x, y: module.config.y,
|
||
width: module.config.width, height: module.config.height,
|
||
anchor: module.config.anchor || "top-left"
|
||
})
|
||
});
|
||
Object.assign(module.config, payload.module.config);
|
||
syncForm(module);
|
||
saveState.textContent = "Saved";
|
||
setTimeout(() => { saveState.textContent = "Ready"; }, 1300);
|
||
} catch (error) {
|
||
saveState.textContent = "Save failed";
|
||
saveState.title = error.message;
|
||
}
|
||
}
|
||
|
||
layer.addEventListener("pointerdown", (event) => {
|
||
const element = event.target.closest(".lumi-overlay-module");
|
||
if (!element) return;
|
||
event.preventDefault();
|
||
selectSource(element.dataset.moduleId);
|
||
const module = selectedModule();
|
||
drag = {
|
||
startX: event.clientX,
|
||
startY: event.clientY,
|
||
rect: canvas.getBoundingClientRect(),
|
||
config: { ...module.config },
|
||
handle: event.target.closest("[data-resize-handle]")?.dataset.resizeHandle || null
|
||
};
|
||
});
|
||
window.addEventListener("pointermove", applyDrag);
|
||
window.addEventListener("pointerup", () => {
|
||
if (!drag) return;
|
||
drag = null;
|
||
clearGuides();
|
||
saveTransform(selectedModule());
|
||
});
|
||
|
||
canvas.addEventListener("keydown", (event) => {
|
||
if (!selectedModule() || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
||
event.preventDefault();
|
||
const amount = event.shiftKey ? 1 : 0.1;
|
||
const module = selectedModule();
|
||
if (event.key === "ArrowLeft") module.config.x = clamp(Number(module.config.x) - amount, 0, 100);
|
||
if (event.key === "ArrowRight") module.config.x = clamp(Number(module.config.x) + amount, 0, 100);
|
||
if (event.key === "ArrowUp") module.config.y = clamp(Number(module.config.y) - amount, 0, 100);
|
||
if (event.key === "ArrowDown") module.config.y = clamp(Number(module.config.y) + amount, 0, 100);
|
||
renderer.applyBox(elements.get(module.id), module.config);
|
||
syncForm(module);
|
||
updateReadout();
|
||
clearTimeout(canvas._saveTimer);
|
||
canvas._saveTimer = setTimeout(() => saveTransform(module), 300);
|
||
});
|
||
|
||
function transformSelected(changes) {
|
||
const module = selectedModule();
|
||
if (!module) return;
|
||
Object.assign(module.config, changes);
|
||
renderer.applyBox(elements.get(module.id), module.config);
|
||
syncForm(module);
|
||
updateReadout();
|
||
saveTransform(module);
|
||
}
|
||
|
||
editorRoot.querySelector("[data-editor-center]")?.addEventListener("click", () => transformSelected({ x: 50, y: 50, anchor: "center" }));
|
||
editorRoot.querySelector("[data-editor-fit]")?.addEventListener("click", () => transformSelected({ x: 0, y: 0, width: 100, height: 100, anchor: "top-left" }));
|
||
editorRoot.querySelector("[data-editor-reset]")?.addEventListener("click", () => transformSelected({ x: 0, y: 0, width: 50, height: 25, anchor: "top-left" }));
|
||
const previewFrame = editorRoot.querySelector("[data-preview-frame]");
|
||
editorRoot.querySelectorAll("[data-preview-background]").forEach((button) => button.addEventListener("click", () => {
|
||
const background = button.dataset.previewBackground;
|
||
previewFrame?.classList.toggle("preview-bg-dark", background === "dark");
|
||
previewFrame?.classList.toggle("preview-bg-light", background === "light");
|
||
button.parentElement?.querySelectorAll("[data-preview-background]").forEach((item) => item.setAttribute("aria-pressed", item === button ? "true" : "false"));
|
||
}));
|
||
editorRoot.querySelector("[data-preview-grid]")?.addEventListener("click", (event) => {
|
||
const active = !previewFrame?.classList.contains("show-grid");
|
||
previewFrame?.classList.toggle("show-grid", active);
|
||
event.currentTarget.setAttribute("aria-pressed", active ? "true" : "false");
|
||
});
|
||
snapToggle.addEventListener("change", () => { snapHint.textContent = snapToggle.checked ? "off" : "on"; });
|
||
sceneSelect.addEventListener("change", () => { selectedId = null; renderCanvas(); });
|
||
window.addEventListener("resize", () => renderCanvas());
|
||
|
||
document.querySelectorAll("[data-edit-scene]").forEach((button) => button.addEventListener("click", () => {
|
||
sceneSelect.value = button.dataset.editScene;
|
||
selectedId = null;
|
||
renderCanvas();
|
||
editorRoot.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}));
|
||
document.querySelectorAll("[data-select-source]").forEach((button) => button.addEventListener("click", () => {
|
||
const scene = state.scenes.find((item) => item.modules.some((module) => module.id === button.dataset.selectSource));
|
||
if (scene) sceneSelect.value = scene.id;
|
||
selectedId = button.dataset.selectSource;
|
||
renderCanvas();
|
||
editorRoot.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}));
|
||
|
||
editorApi = {
|
||
reload(moduleId) {
|
||
const frame = renderer.findSourceContent(layer, moduleId, ".lumi-overlay-frame");
|
||
if (frame) {
|
||
const replacement = frame.cloneNode(false);
|
||
replacement.src = frame.dataset.originalUrl || frame.src;
|
||
frame.replaceWith(replacement);
|
||
return;
|
||
}
|
||
renderer.restartMediaElement(renderer.findSourceContent(layer, moduleId, "video, audio"));
|
||
}
|
||
};
|
||
renderCanvas();
|
||
}
|
||
|
||
document.querySelectorAll("[data-refresh-source]").forEach((button) => button.addEventListener("click", async () => {
|
||
const original = button.textContent;
|
||
button.disabled = true;
|
||
button.textContent = "Refreshing…";
|
||
try {
|
||
editorApi?.reload(button.dataset.refreshSource);
|
||
const overlayId = document.querySelector("[data-overlay-editor-state]")
|
||
? JSON.parse(document.querySelector("[data-overlay-editor-state]").textContent || "{}").overlayId
|
||
: null;
|
||
await jsonRequest(`/api/admin/overlays/${overlayId}/modules/${button.dataset.refreshSource}/refresh`, { method: "POST", body: "{}" });
|
||
button.textContent = "Refreshed";
|
||
} catch (error) {
|
||
button.textContent = "Refresh failed";
|
||
button.title = error.message;
|
||
} finally {
|
||
setTimeout(() => { button.disabled = false; button.textContent = original; }, 1600);
|
||
}
|
||
}));
|
||
|
||
document.querySelectorAll("[data-check-source]").forEach((button) => button.addEventListener("click", async () => {
|
||
const output = document.querySelector(`[data-source-health="${CSS.escape(button.dataset.checkSource)}"]`);
|
||
const stateData = document.querySelector("[data-overlay-editor-state]");
|
||
const overlayId = stateData ? JSON.parse(stateData.textContent || "{}").overlayId : null;
|
||
button.disabled = true;
|
||
if (output) output.textContent = "Checking the website…";
|
||
try {
|
||
const result = await jsonRequest(`/api/admin/overlays/${overlayId}/modules/${button.dataset.checkSource}/health`);
|
||
if (output) output.textContent = result.skipped ? result.reason : result.healthy ? `Website replied normally${result.status ? ` (HTTP ${result.status})` : ""}.` : result.reason;
|
||
} catch (error) {
|
||
if (output) output.textContent = error.message;
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
}));
|
||
|
||
const connector = document.querySelector("[data-obs-connector]");
|
||
if (!connector) return;
|
||
const overlayId = connector.dataset.overlayId;
|
||
const settingsForm = connector.querySelector("[data-obs-settings-form]");
|
||
const sceneForm = connector.querySelector("[data-obs-scene-form]");
|
||
const statusState = connector.querySelector("[data-obs-state]");
|
||
const current = connector.querySelector("[data-obs-current]");
|
||
const error = connector.querySelector("[data-obs-error]");
|
||
const sceneSelect = connector.querySelector("[data-obs-scenes]");
|
||
const providerSelect = connector.querySelector("[data-obs-provider]");
|
||
const permission = connector.querySelector("[data-obs-permission]");
|
||
const transition = connector.querySelector("[data-obs-transition]");
|
||
const canvasSize = connector.querySelector("[data-obs-canvas]");
|
||
const profile = connector.querySelector("[data-obs-profile]");
|
||
const collection = connector.querySelector("[data-obs-collection]");
|
||
const sourceStatus = connector.querySelector("[data-obs-source]");
|
||
const outputs = connector.querySelector("[data-obs-outputs]");
|
||
const importResult = connector.querySelector("[data-obs-import-result]");
|
||
|
||
function syncProviderFields() {
|
||
const browserBridge = providerSelect?.value === "obs_browser_bridge";
|
||
connector.querySelectorAll("[data-obs-websocket-field]").forEach((field) => {
|
||
field.hidden = browserBridge;
|
||
field.querySelectorAll("input, select").forEach((input) => { input.disabled = browserBridge; });
|
||
});
|
||
const help = connector.querySelector("[data-obs-browser-help]");
|
||
if (help) help.hidden = !browserBridge;
|
||
}
|
||
|
||
function renderStatus(payload) {
|
||
if (statusState) statusState.textContent = payload.state || (payload.ok ? "connected" : "error");
|
||
if (current) current.textContent = payload.current_scene || "Unknown";
|
||
if (error) error.textContent = payload.error || "None";
|
||
if (permission) permission.textContent = payload.control_level_label || "Not reported";
|
||
if (transition) transition.textContent = payload.current_transition || "Unknown";
|
||
if (canvasSize) canvasSize.textContent = payload.canvas_width && payload.canvas_height ? `${payload.canvas_width} × ${payload.canvas_height}` : "Unknown";
|
||
if (profile) profile.textContent = payload.profiles_supported === false ? "Not exposed by Browser Bridge" : (payload.current_profile || "Unknown");
|
||
if (collection) collection.textContent = payload.scene_collections_supported === false ? "Not exposed by Browser Bridge" : (payload.current_scene_collection || "Unknown");
|
||
if (sourceStatus) sourceStatus.textContent = payload.source_active === true ? "Active" : payload.source_visible === true ? "Visible" : payload.source_active === false ? "Inactive" : "Unknown";
|
||
if (outputs) {
|
||
const activeOutputs = [];
|
||
if (payload.outputs?.streaming) activeOutputs.push("Streaming");
|
||
if (payload.outputs?.recording) activeOutputs.push(payload.outputs.recording_paused ? "Recording paused" : "Recording");
|
||
if (payload.outputs?.replay_buffer) activeOutputs.push("Replay buffer");
|
||
if (payload.outputs?.virtual_camera) activeOutputs.push("Virtual camera");
|
||
outputs.textContent = activeOutputs.join(", ") || "Idle or unavailable";
|
||
}
|
||
if (sceneSelect && Array.isArray(payload.scenes)) {
|
||
const selected = sceneSelect.value;
|
||
sceneSelect.replaceChildren(new Option("Choose an OBS scene", ""));
|
||
payload.scenes.forEach((name) => sceneSelect.add(new Option(name, name)));
|
||
if (payload.scenes.includes(selected)) sceneSelect.value = selected;
|
||
}
|
||
}
|
||
|
||
providerSelect?.addEventListener("change", syncProviderFields);
|
||
syncProviderFields();
|
||
|
||
const refreshObsStatus = async () => {
|
||
try { renderStatus(await jsonRequest(`/api/admin/overlays/${overlayId}/obs/status`)); }
|
||
catch (requestError) { renderStatus({ state: "error", error: requestError.message }); }
|
||
};
|
||
connector.querySelector("[data-obs-refresh]")?.addEventListener("click", refreshObsStatus);
|
||
window.setInterval(refreshObsStatus, 5000);
|
||
connector.querySelector("[data-obs-test]")?.addEventListener("click", async () => {
|
||
const data = new FormData(settingsForm);
|
||
renderStatus({ state: "testing", error: null });
|
||
try {
|
||
renderStatus(await jsonRequest(`/api/admin/overlays/${overlayId}/obs/test`, { method: "POST", body: JSON.stringify({ provider: data.get("provider"), endpoint: data.get("endpoint"), password: data.get("password") }) }));
|
||
} catch (requestError) { renderStatus({ state: "error", error: requestError.message }); }
|
||
});
|
||
connector.querySelector("[data-obs-import]")?.addEventListener("click", async (event) => {
|
||
const button = event.currentTarget;
|
||
button.disabled = true;
|
||
if (importResult) importResult.textContent = "Applying the detected OBS setup…";
|
||
try {
|
||
const result = await jsonRequest(`/api/admin/overlays/${overlayId}/obs/import`, { method: "POST", body: "{}" });
|
||
if (importResult) importResult.textContent = `${result.message}${result.canvas_updated ? " Canvas size updated." : ""} Reloading…`;
|
||
window.setTimeout(() => window.location.reload(), 700);
|
||
} catch (requestError) {
|
||
if (importResult) importResult.textContent = requestError.message;
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
sceneForm?.addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
const sceneName = new FormData(sceneForm).get("scene_name");
|
||
if (!sceneName) return;
|
||
try { renderStatus(await jsonRequest(`/api/admin/overlays/${overlayId}/obs/scene`, { method: "POST", body: JSON.stringify({ scene_name: sceneName }) })); }
|
||
catch (requestError) { renderStatus({ state: "error", error: requestError.message }); }
|
||
});
|
||
})();
|