diff options
| -rw-r--r-- | static/deck.css | 13 | ||||
| -rw-r--r-- | static/modules/obs.js | 104 | ||||
| -rw-r--r-- | tests/test_frontend.py | 7 |
3 files changed, 124 insertions, 0 deletions
diff --git a/static/deck.css b/static/deck.css index 9bbd806..c2512d5 100644 --- a/static/deck.css +++ b/static/deck.css @@ -64,6 +64,19 @@ button:active { background: var(--udt-surface2); } button.on { background: var(--udt-accent); color: var(--udt-crust); border-color: var(--udt-accent); } +.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; } +.obs-scenes, .obs-sources { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; } +.obs-audio .row span { flex: 1; min-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +select { + font: inherit; + color: inherit; + background: var(--udt-surface1); + border: 1px solid var(--udt-surface2); + border-radius: 10px; + padding: 8px; +} + .good { color: var(--udt-green); } .bad { color: var(--udt-red); } .warn { color: var(--udt-yellow); } diff --git a/static/modules/obs.js b/static/modules/obs.js new file mode 100644 index 0000000..c6a3ebc --- /dev/null +++ b/static/modules/obs.js @@ -0,0 +1,104 @@ +const clamp = (v) => Math.max(0, Math.min(1, v)); + +async function post(action, body = {}) { + try { + await fetch(`/api/obs/${action}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + } catch { + // the next poll shows the real state + } +} + +function button(label) { + const b = document.createElement("button"); + b.textContent = label; + return b; +} + +export default { + title: "OBS", + interval: 1, + + mount(el) { + const wrap = document.createElement("div"); + wrap.className = "obs"; + wrap.innerHTML = ` + <div class="row obs-controls"></div> + <div class="obs-scenes"></div> + <div class="obs-audio"></div> + <div class="obs-sources"></div>`; + el.append(wrap); + }, + + update(state, el) { + const controls = el.querySelector(".obs-controls"); + const scenes = el.querySelector(".obs-scenes"); + const audio = el.querySelector(".obs-audio"); + const sources = el.querySelector(".obs-sources"); + + if (!state.connected) { + controls.textContent = "OBS offline"; + scenes.textContent = audio.textContent = sources.textContent = ""; + return; + } + + controls.textContent = ""; + const rec = button(state.recording ? "Stop recording" : "Record"); + rec.classList.toggle("on", state.recording); + rec.onclick = () => post("record", { on: !state.recording }); + const stream = button(state.streaming ? "Stop stream" : "Stream"); + stream.classList.toggle("on", state.streaming); + stream.onclick = () => post("stream", { on: !state.streaming }); + controls.append(rec, stream); + + if (state.transitions?.length) { + const sel = document.createElement("select"); + for (const t of state.transitions) { + const o = document.createElement("option"); + o.value = o.textContent = t; + if (t === state.transition) o.selected = true; + sel.append(o); + } + sel.onchange = () => post("transition", { name: sel.value }); + const trigger = button("Transition"); + trigger.onclick = () => post("trigger"); + controls.append(sel, trigger); + } + + scenes.textContent = ""; + for (const name of state.scenes ?? []) { + const b = button(name); + b.classList.toggle("on", name === state.currentScene); + b.onclick = () => post("scene", { scene: name }); + scenes.append(b); + } + + audio.textContent = ""; + for (const inp of state.inputs ?? []) { + const row = document.createElement("div"); + row.className = "row"; + const label = document.createElement("span"); + label.textContent = inp.name; + const mute = button(inp.muted ? "Unmute" : "Mute"); + mute.classList.toggle("on", inp.muted); + mute.onclick = () => post("mute", { input: inp.name, muted: !inp.muted }); + const down = button("−"); + down.onclick = () => post("volume", { input: inp.name, level: clamp(inp.volume - 0.1) }); + const up = button("+"); + up.onclick = () => post("volume", { input: inp.name, level: clamp(inp.volume + 0.1) }); + row.append(label, mute, down, up); + audio.append(row); + } + + sources.textContent = ""; + for (const s of state.sources ?? []) { + const b = button(s.name); + b.classList.toggle("on", s.on); + b.onclick = () => post("source", { scene: state.currentScene, id: s.id, on: !s.on }); + sources.append(b); + } + }, +}; diff --git a/tests/test_frontend.py b/tests/test_frontend.py index d90bf48..94ea88f 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -60,6 +60,13 @@ class Frontend(unittest.TestCase): self.assertEqual(status, 200) self.assertIn(b"--udt-", body) + def test_obs_panel_served(self): + status, body = self.get("/modules/obs.js") + self.assertEqual(status, 200) + text = body.decode() + for part in ("export default", "mount", "update"): + self.assertIn(part, text) + if __name__ == "__main__": unittest.main() |
