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