TonyD365's picture
Create operator.js
27eea41 verified
Raw
History Blame Contribute Delete
8.83 kB
/* 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) {}
}