Lumi/src/web/public/overlay-management.js
2026-07-19 12:15:43 +02:00

635 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
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 }); }
});
})();