/* Operator Logic */ let state = { show_name: "", num_lights: 8, cues: [], current_cue_index: -1, }; let myLightId = window.INITIAL_LIGHT_ID; let ws = null; let reconnectTimer = null; function connectWS() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; ws = new WebSocket(`${protocol}//${window.location.host}/ws`); ws.onopen = () => { setStatus(true); if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } }; ws.onclose = () => { setStatus(false); reconnectTimer = setTimeout(connectWS, 2000); }; ws.onerror = () => { setStatus(false); }; ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.type === "state_update") { const prevIndex = state.current_cue_index; state = msg.data; render(); if (state.current_cue_index !== prevIndex && hasMyAction(state.current_cue_index)) { triggerAlert(); } } }; } function setStatus(connected) { const dot = document.getElementById("status-dot"); if (!dot) return; dot.className = connected ? "status-dot connected" : "status-dot disconnected"; } function hasMyAction(cueIdx) { if (cueIdx < 0 || cueIdx >= state.cues.length) return false; const cue = state.cues[cueIdx]; if (!cue.actions) return false; const action = cue.actions[myLightId - 1]; return action && action.enabled; } function getMyAction(cueIdx) { if (cueIdx < 0 || cueIdx >= state.cues.length) return null; const cue = state.cues[cueIdx]; if (!cue.actions) return null; return cue.actions[myLightId - 1]; } function findMyLastActionBefore(cueIdx) { for (let i = cueIdx - 1; i >= 0; i--) { if (hasMyAction(i)) return { cue: state.cues[i], index: i }; } return null; } function render() { if (myLightId === null || myLightId === undefined) { renderPicker(); return; } document.getElementById("light-picker").hidden = true; document.getElementById("operator-view").hidden = false; if (myLightId < 1 || myLightId > state.num_lights) { renderPicker(); return; } document.getElementById("my-light-id").textContent = myLightId; document.getElementById("footer-show").textContent = state.show_name || "—"; const curr = state.current_cue_index; const total = state.cues.length; if (curr < 0) { document.getElementById("footer-cue").textContent = total > 0 ? `— / ${total}` : "—"; } else { document.getElementById("footer-cue").textContent = `${curr + 1} / ${total}`; } const standby = document.getElementById("state-standby"); const action = document.getElementById("state-action"); const idle = document.getElementById("state-idle"); standby.hidden = true; action.hidden = true; idle.hidden = true; if (total === 0) { idle.hidden = false; document.getElementById("idle-label").textContent = "No cues programmed"; return; } if (curr < 0) { idle.hidden = false; document.getElementById("idle-label").textContent = "Show not started"; return; } if (hasMyAction(curr)) { action.hidden = false; renderAction(); } else { standby.hidden = false; renderStandby(); } } function renderStandby() { const lastAction = findMyLastActionBefore(state.current_cue_index); const lastEl = document.getElementById("last-action"); if (lastAction) { const a = lastAction.cue.actions[myLightId - 1]; lastEl.hidden = false; document.getElementById("last-action-detail").textContent = `Cue ${lastAction.index + 1}: ${formatActionSummary(a)}`; } else { lastEl.hidden = true; } } function renderAction() { const curr = state.current_cue_index; const cue = state.cues[curr]; const a = cue.actions[myLightId - 1]; document.getElementById("action-cue-num").textContent = `CUE ${curr + 1} — ${cue.name || "Untitled"}`; const modeCard = document.getElementById("mode-card"); const modeText = { "snap": "⚡ Snap (instant)", "fade": "🌫 Fade", "off": "⚫ Off", }[a.mode || "snap"] || "Snap"; modeCard.hidden = false; document.getElementById("detail-mode").textContent = modeText; const colorCard = document.getElementById("color-card"); if (a.mode === "off") { colorCard.hidden = true; } else { colorCard.hidden = false; const colorName = a.color_name || colorHexToName(a.color) || "Custom"; document.getElementById("detail-color").textContent = colorName; const swatch = document.getElementById("color-swatch"); swatch.style.backgroundColor = a.color || "#ffffff"; swatch.style.color = a.color || "#ffffff"; } const brightnessCard = document.getElementById("brightness-card"); if (a.mode === "off") { brightnessCard.hidden = true; } else { brightnessCard.hidden = false; const b = a.brightness ?? 100; document.getElementById("detail-brightness").textContent = `${b}%`; document.getElementById("brightness-fill").style.width = b + "%"; } const fadeCard = document.getElementById("fade-card"); if (a.mode === "fade" && (a.fade_seconds || 0) > 0) { fadeCard.hidden = false; document.getElementById("detail-fade").textContent = `${a.fade_seconds}s`; } else { fadeCard.hidden = true; } const notesCard = document.getElementById("notes-card"); if (a.notes && a.notes.trim()) { notesCard.hidden = false; document.getElementById("detail-notes").textContent = a.notes; } else { notesCard.hidden = true; } } function formatActionSummary(a) { if (!a) return "—"; if (a.mode === "off") return "Off"; const parts = []; if (a.color_name) parts.push(a.color_name); if (a.brightness != null) parts.push(`${a.brightness}%`); if (a.mode === "fade" && a.fade_seconds > 0) parts.push(`fade ${a.fade_seconds}s`); return parts.join(" · ") || "Active"; } function colorHexToName(hex) { if (!hex) return null; const h = hex.toLowerCase(); const map = { "#ffffff": "White", "#000000": "Black", "#ff0000": "Red", "#00ff00": "Green", "#0000ff": "Blue", "#ffff00": "Yellow", "#ff00ff": "Magenta", "#00ffff": "Cyan", "#ffa500": "Orange", "#ffc0cb": "Pink", "#800080": "Purple", }; return map[h] || null; } function triggerAlert() { if (navigator.vibrate) navigator.vibrate([200, 100, 200]); playBeep(); } function playBeep() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 880; osc.type = "sine"; gain.gain.setValueAtTime(0.0001, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.3, ctx.currentTime + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.35); osc.start(); osc.stop(ctx.currentTime + 0.4); } catch (e) {} } function renderPicker() { document.getElementById("light-picker").hidden = false; document.getElementById("operator-view").hidden = true; const btns = document.getElementById("light-buttons"); btns.innerHTML = ""; const n = state.num_lights || 8; for (let i = 1; i <= n; i++) { const a = document.createElement("a"); a.className = "light-btn"; a.href = `/operator/${i}`; a.textContent = i; btns.appendChild(a); } } document.addEventListener("DOMContentLoaded", () => { const doneBtn = document.getElementById("btn-done"); if (doneBtn) { doneBtn.addEventListener("click", () => { const action = document.getElementById("state-action"); const standby = document.getElementById("state-standby"); action.hidden = true; standby.hidden = false; renderStandby(); }); } document.body.addEventListener("touchstart", primeAudio, { once: true }); document.body.addEventListener("click", primeAudio, { once: true }); connectWS(); }); function primeAudio() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); gain.gain.value = 0; osc.start(); osc.stop(ctx.currentTime + 0.01); } catch (e) {} }