Pult: Handy als Stream Deck mit Soundboard und Szenenwechsel
Paket bauen und veroeffentlichen / paket (push) Failing after 1m21s
Paket bauen und veroeffentlichen / paket (push) Failing after 1m21s
- /pult als installierbare Web-App (Manifest, Service Worker, Symbole) - Soundboard: Sounds vom Handy hochladen, bearbeiten, im Stream spielen ueber die neue Browser-Source /sounds - Szenen, Clip und Stummschalter: der Stream-PC waehlt sich mit eigenem Schluessel beim Hub ein (fern:verbinden), Befehle warten auf OBS - Browser-Sources melden, welche Seite sie sind Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70448967f5
commit
8be195d938
29 files changed
+2935
-28
No files matched your search
@@ -0,0 +1,638 @@
|
||||
/* Das Pult: Soundboard, Szenen, Liegestuetze, Vorlesen - am Handy.
|
||||
*
|
||||
* Zwei Wege zum Hub:
|
||||
* - der Ereignisstrom /events (wie bei den Browser-Sources), damit zwei
|
||||
* offene Pulte und die Uebersichtsseite nicht auseinanderlaufen;
|
||||
* - einfache POSTs fuer jeden Druck. Jede Antwort sagt, ob es geklappt
|
||||
* hat, und genau das steht danach an der Taste.
|
||||
*
|
||||
* Fremder Text (Soundnamen, Szenen, Chat) kommt nur ueber textContent ins
|
||||
* DOM, nie ueber innerHTML. */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
var FARBEN = ['lila', 'tuerkis', 'pink', 'gold', 'gruen', 'rot', 'blau', 'grau'];
|
||||
var SCHNELL = ['🔊', '😂', '💀', '👏', '🎺', '🥁', '💥', '🚨', '🤡', '😱', '🔥', '❤️', '👀', '🐐', '🫡', '🎉'];
|
||||
|
||||
var zustand = {
|
||||
sounds: [],
|
||||
obs: { pc: false, obs: false, szenen: [], aktuell: null, ton: [], puffer: false },
|
||||
live: false,
|
||||
bearbeiten: false,
|
||||
ttsAn: false,
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------ Hilfen
|
||||
|
||||
function el(tag, klasse, text) {
|
||||
var e = document.createElement(tag);
|
||||
if (klasse) e.className = klasse;
|
||||
if (text !== undefined) e.textContent = text;
|
||||
return e;
|
||||
}
|
||||
|
||||
function tippen() {
|
||||
if (navigator.vibrate) { try { navigator.vibrate(12); } catch (e) { /* egal */ } }
|
||||
}
|
||||
|
||||
var meldungTimer;
|
||||
function melden(text, art) {
|
||||
var m = $('meldung');
|
||||
if (!text) return;
|
||||
m.textContent = text;
|
||||
m.className = 'meldung an' + (art ? ' ' + art : '');
|
||||
clearTimeout(meldungTimer);
|
||||
meldungTimer = setTimeout(function () { m.className = 'meldung'; }, art === 'fehler' ? 4500 : 2200);
|
||||
}
|
||||
|
||||
/** POST mit Formular, Antwort als JSON. Wirft nie; Fehler stehen in `ok`. */
|
||||
function rufen(pfad, felder, roh) {
|
||||
var opts = { method: 'POST', credentials: 'same-origin' };
|
||||
if (roh) {
|
||||
opts.body = roh;
|
||||
} else {
|
||||
opts.headers = { 'content-type': 'application/x-www-form-urlencoded' };
|
||||
opts.body = new URLSearchParams(felder || {});
|
||||
}
|
||||
return fetch(pfad, opts).then(function (res) {
|
||||
return res.json().catch(function () { return { text: 'Fehler ' + res.status }; })
|
||||
.then(function (body) {
|
||||
if (res.status === 401) abgemeldet();
|
||||
body.ok = res.ok && body.ok !== false;
|
||||
return body;
|
||||
});
|
||||
}, function () {
|
||||
return { ok: false, text: 'Keine Verbindung zum Hub.' };
|
||||
});
|
||||
}
|
||||
|
||||
function holen(pfad) {
|
||||
return fetch(pfad, { credentials: 'same-origin' }).then(function (res) {
|
||||
if (res.status === 401) { abgemeldet(); throw new Error('abgemeldet'); }
|
||||
if (!res.ok) throw new Error('Fehler ' + res.status);
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
|
||||
function abgemeldet() { $('abgemeldet').hidden = false; }
|
||||
|
||||
/** Taste waehrend eines Rufs sperren, damit ein Doppeltipp nicht doppelt schaltet. */
|
||||
function mitWarten(knopf, versprechen) {
|
||||
knopf.classList.add('wartet');
|
||||
return versprechen.then(function (r) { knopf.classList.remove('wartet'); return r; });
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Reiter
|
||||
|
||||
function zeigeReiter(name) {
|
||||
var reiter = document.querySelectorAll('.reiter button');
|
||||
for (var i = 0; i < reiter.length; i++) {
|
||||
reiter[i].classList.toggle('an', reiter[i].dataset.tab === name);
|
||||
}
|
||||
var tabs = document.querySelectorAll('.tab');
|
||||
for (var j = 0; j < tabs.length; j++) tabs[j].classList.toggle('an', tabs[j].id === 'tab-' + name);
|
||||
try { localStorage.setItem('pult-reiter', name); } catch (e) { /* privat */ }
|
||||
if (name === 'stream') ladeWartend();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
document.querySelector('.reiter').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('button[data-tab]');
|
||||
if (b) { tippen(); zeigeReiter(b.dataset.tab); }
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Kopf
|
||||
|
||||
var laufzeitTimer;
|
||||
function zeigeStream(s) {
|
||||
zustand.live = !!s.live;
|
||||
var live = $('live');
|
||||
live.classList.toggle('aus', !s.live);
|
||||
clearInterval(laufzeitTimer);
|
||||
var schreiben = function () {
|
||||
if (!s.live) { $('live-text').textContent = 'offline'; return; }
|
||||
var teile = ['LIVE'];
|
||||
if (s.startedAt) {
|
||||
var min = Math.max(0, Math.floor((Date.now() - Date.parse(s.startedAt)) / 60000));
|
||||
teile.push(Math.floor(min / 60) + ':' + String(min % 60).padStart(2, '0'));
|
||||
}
|
||||
teile.push((s.viewers || 0).toLocaleString('de-DE') + ' 👁');
|
||||
$('live-text').textContent = teile.join(' · ');
|
||||
};
|
||||
schreiben();
|
||||
if (s.live) laufzeitTimer = setInterval(schreiben, 30000);
|
||||
$('test-live').hidden = !s.live;
|
||||
var gesperrt = document.querySelectorAll('#test-gruppen [data-offline]');
|
||||
for (var i = 0; i < gesperrt.length; i++) gesperrt[i].disabled = !!s.live;
|
||||
var demo = $('demo');
|
||||
if (demo.dataset.art === 'demo-an') demo.disabled = !!s.live;
|
||||
}
|
||||
|
||||
// Bildschirm anlassen. Ein Pult, das nach 30 s schwarz wird, ist mitten
|
||||
// im Stream keins. Der Wunsch wird gemerkt; das Handy gibt die Sperre bei
|
||||
// jedem Wegwischen frei, also wird sie beim Zurueckkommen neu geholt.
|
||||
var sperre = null;
|
||||
var wachGewuenscht = false;
|
||||
try { wachGewuenscht = localStorage.getItem('pult-wach') === '1'; } catch (e) { /* privat */ }
|
||||
function wachHolen() {
|
||||
if (!wachGewuenscht || !('wakeLock' in navigator) || document.visibilityState !== 'visible') {
|
||||
$('wach').setAttribute('aria-pressed', String(!!sperre));
|
||||
return;
|
||||
}
|
||||
navigator.wakeLock.request('screen').then(function (s) {
|
||||
sperre = s;
|
||||
$('wach').setAttribute('aria-pressed', 'true');
|
||||
s.addEventListener('release', function () {
|
||||
sperre = null;
|
||||
$('wach').setAttribute('aria-pressed', 'false');
|
||||
});
|
||||
}, function () { $('wach').setAttribute('aria-pressed', 'false'); });
|
||||
}
|
||||
$('wach').addEventListener('click', function () {
|
||||
tippen();
|
||||
if (!('wakeLock' in navigator)) { melden('Dieser Browser kann den Bildschirm nicht wach halten.', 'fehler'); return; }
|
||||
wachGewuenscht = !sperre;
|
||||
try { localStorage.setItem('pult-wach', wachGewuenscht ? '1' : '0'); } catch (e) { /* privat */ }
|
||||
if (sperre) { sperre.release(); sperre = null; melden('Bildschirm darf wieder ausgehen.'); }
|
||||
else { wachHolen(); melden('Bildschirm bleibt an.', 'ok'); }
|
||||
});
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') { wachHolen(); ladeSounds(); }
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Sounds
|
||||
|
||||
function ladeSounds() {
|
||||
return holen('/soundboard/liste').then(zeigeSounds, function () { /* Meldung kommt woanders */ });
|
||||
}
|
||||
|
||||
function zeigeSounds(body) {
|
||||
if (body.sounds) zustand.sounds = body.sounds;
|
||||
if (typeof body.quellen === 'number') $('quelle-fehlt').hidden = body.quellen > 0;
|
||||
var raster = $('raster');
|
||||
raster.textContent = '';
|
||||
raster.classList.toggle('bearbeiten', zustand.bearbeiten);
|
||||
$('sounds-leer').hidden = zustand.sounds.length > 0;
|
||||
$('bearbeiten').hidden = zustand.sounds.length === 0;
|
||||
if (!zustand.sounds.length && zustand.bearbeiten) setzeBearbeiten(false);
|
||||
|
||||
zustand.sounds.forEach(function (s) {
|
||||
var k = el('button', 'kachel' + (s.emoji ? '' : ' ohne-emoji'));
|
||||
k.type = 'button';
|
||||
k.dataset.id = s.id;
|
||||
k.dataset.farbe = FARBEN.indexOf(s.farbe) !== -1 ? s.farbe : 'lila';
|
||||
if (s.emoji) k.appendChild(el('span', 'emoji', s.emoji));
|
||||
k.appendChild(el('span', 'name', s.name));
|
||||
k.setAttribute('aria-label', (zustand.bearbeiten ? 'Bearbeiten: ' : 'Abspielen: ') + s.name);
|
||||
raster.appendChild(k);
|
||||
});
|
||||
}
|
||||
|
||||
function knall(id) {
|
||||
var k = document.querySelector('.kachel[data-id="' + CSS.escape(id) + '"]');
|
||||
if (!k) return;
|
||||
k.classList.remove('knall');
|
||||
void k.offsetWidth; // Animation neu starten, auch beim Doppeltipp
|
||||
k.classList.add('knall');
|
||||
}
|
||||
|
||||
$('raster').addEventListener('click', function (e) {
|
||||
var k = e.target.closest('.kachel');
|
||||
if (!k) return;
|
||||
tippen();
|
||||
var id = k.dataset.id;
|
||||
if (zustand.bearbeiten) { oeffneBlatt(id); return; }
|
||||
knall(id);
|
||||
rufen('/soundboard/spielen', { id: id }).then(function (r) {
|
||||
if (!r.ok) { melden(r.text, 'fehler'); return; }
|
||||
$('quelle-fehlt').hidden = r.quellen > 0;
|
||||
if (!r.quellen) melden(r.text, 'fehler');
|
||||
});
|
||||
});
|
||||
|
||||
$('stopp').addEventListener('click', function () {
|
||||
tippen();
|
||||
rufen('/soundboard/stopp').then(function (r) { melden(r.text, r.ok ? 'ok' : 'fehler'); });
|
||||
});
|
||||
|
||||
function setzeBearbeiten(an) {
|
||||
zustand.bearbeiten = an;
|
||||
$('bearbeiten').setAttribute('aria-pressed', String(an));
|
||||
$('bearbeiten').textContent = an ? 'Fertig' : 'Bearbeiten';
|
||||
$('raster').classList.toggle('bearbeiten', an);
|
||||
var kacheln = document.querySelectorAll('.kachel');
|
||||
for (var i = 0; i < kacheln.length; i++) {
|
||||
var s = zustand.sounds[i];
|
||||
if (s) kacheln[i].setAttribute('aria-label', (an ? 'Bearbeiten: ' : 'Abspielen: ') + s.name);
|
||||
}
|
||||
}
|
||||
$('bearbeiten').addEventListener('click', function () { tippen(); setzeBearbeiten(!zustand.bearbeiten); });
|
||||
|
||||
// Hochladen: roh im Koerper, eine Datei nach der anderen. Parallel waere
|
||||
// schneller, aber dann kommt die Reihenfolge durcheinander.
|
||||
$('datei').addEventListener('change', function (e) {
|
||||
var dateien = Array.prototype.slice.call(e.target.files || []);
|
||||
e.target.value = '';
|
||||
var weiter = Promise.resolve();
|
||||
dateien.forEach(function (d, i) {
|
||||
weiter = weiter.then(function () {
|
||||
melden((dateien.length > 1 ? (i + 1) + '/' + dateien.length + ' · ' : '') + d.name + ' wird hochgeladen …');
|
||||
return rufen('/soundboard/neu?name=' + encodeURIComponent(d.name), null, d).then(function (r) {
|
||||
melden(r.text, r.ok ? 'ok' : 'fehler');
|
||||
if (r.sounds) zeigeSounds(r);
|
||||
// Der erste Sound geht gleich ins Bearbeiten: ein Emoji macht aus
|
||||
// einer Liste von Dateinamen erst ein Pult.
|
||||
if (r.ok && r.sound && dateien.length === 1) oeffneBlatt(r.sound.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Blatt
|
||||
|
||||
var blatt = $('blatt');
|
||||
var offenId = null;
|
||||
var probe = null;
|
||||
|
||||
(function baueBlatt() {
|
||||
var schnell = $('b-schnell');
|
||||
SCHNELL.forEach(function (em) {
|
||||
var b = el('button', '', em);
|
||||
b.type = 'button';
|
||||
b.addEventListener('click', function () { $('b-emoji').value = em; tippen(); });
|
||||
schnell.appendChild(b);
|
||||
});
|
||||
var farben = $('b-farben');
|
||||
FARBEN.forEach(function (f) {
|
||||
var b = el('button', 'farbe');
|
||||
b.type = 'button';
|
||||
b.dataset.farbe = f;
|
||||
b.setAttribute('role', 'radio');
|
||||
b.setAttribute('aria-label', f);
|
||||
b.addEventListener('click', function () { waehleFarbe(f); tippen(); });
|
||||
farben.appendChild(b);
|
||||
});
|
||||
})();
|
||||
|
||||
function waehleFarbe(f) {
|
||||
var alle = $('b-farben').children;
|
||||
for (var i = 0; i < alle.length; i++) alle[i].setAttribute('aria-checked', String(alle[i].dataset.farbe === f));
|
||||
blatt.dataset.farbe = f;
|
||||
}
|
||||
|
||||
function oeffneBlatt(id) {
|
||||
var s = zustand.sounds.find(function (x) { return x.id === id; });
|
||||
if (!s) return;
|
||||
offenId = id;
|
||||
$('b-name').value = s.name;
|
||||
$('b-emoji').value = s.emoji || '';
|
||||
$('b-laut').value = Math.round((s.laut ?? 0.8) * 100);
|
||||
$('b-laut-zahl').textContent = $('b-laut').value + ' %';
|
||||
waehleFarbe(s.farbe);
|
||||
$('b-loeschen').textContent = 'Löschen';
|
||||
$('b-loeschen').dataset.sicher = '';
|
||||
if (typeof blatt.showModal === 'function') blatt.showModal(); else blatt.setAttribute('open', '');
|
||||
}
|
||||
|
||||
function schliesseBlatt() {
|
||||
if (probe) { probe.pause(); probe = null; }
|
||||
offenId = null;
|
||||
if (blatt.open) blatt.close();
|
||||
}
|
||||
|
||||
$('b-laut').addEventListener('input', function () {
|
||||
$('b-laut-zahl').textContent = $('b-laut').value + ' %';
|
||||
if (probe) probe.volume = Number($('b-laut').value) / 100;
|
||||
});
|
||||
|
||||
$('b-hoeren').addEventListener('click', function () {
|
||||
if (!offenId) return;
|
||||
if (probe) { probe.pause(); probe = null; $('b-hoeren').textContent = 'Am Handy anhören'; return; }
|
||||
probe = new Audio('/soundboard/datei/' + encodeURIComponent(offenId));
|
||||
probe.volume = Number($('b-laut').value) / 100;
|
||||
$('b-hoeren').textContent = 'Anhören stoppen';
|
||||
probe.onended = function () { probe = null; $('b-hoeren').textContent = 'Am Handy anhören'; };
|
||||
probe.play().catch(function () { melden('Abspielen am Handy ging nicht.', 'fehler'); });
|
||||
});
|
||||
|
||||
function verschieben(um) {
|
||||
if (!offenId) return;
|
||||
tippen();
|
||||
rufen('/soundboard/verschieben', { id: offenId, um: String(um) }).then(function (r) {
|
||||
if (r.sounds) zeigeSounds(r);
|
||||
});
|
||||
}
|
||||
$('b-vor').addEventListener('click', function () { verschieben(-1); });
|
||||
$('b-zurueck').addEventListener('click', function () { verschieben(1); });
|
||||
|
||||
$('b-loeschen').addEventListener('click', function () {
|
||||
var b = $('b-loeschen');
|
||||
// Zweimal tippen statt confirm(): ein Dialog des Browsers sieht am Handy
|
||||
// aus wie eine Fehlermeldung, und er haelt die Seite an.
|
||||
if (!b.dataset.sicher) { b.dataset.sicher = '1'; b.textContent = 'Wirklich löschen?'; tippen(); return; }
|
||||
var id = offenId;
|
||||
schliesseBlatt();
|
||||
rufen('/soundboard/loeschen', { id: id }).then(function (r) {
|
||||
melden(r.text, r.ok ? 'ok' : 'fehler');
|
||||
if (r.sounds) zeigeSounds(r);
|
||||
});
|
||||
});
|
||||
|
||||
$('b-abbrechen').addEventListener('click', schliesseBlatt);
|
||||
blatt.addEventListener('close', function () { if (probe) { probe.pause(); probe = null; } });
|
||||
blatt.addEventListener('click', function (e) { if (e.target === blatt) schliesseBlatt(); });
|
||||
|
||||
$('blatt-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var id = offenId;
|
||||
if (!id) return;
|
||||
var felder = {
|
||||
id: id,
|
||||
name: $('b-name').value,
|
||||
emoji: $('b-emoji').value,
|
||||
farbe: blatt.dataset.farbe || 'lila',
|
||||
laut: String(Number($('b-laut').value) / 100),
|
||||
};
|
||||
schliesseBlatt();
|
||||
rufen('/soundboard/aendern', felder).then(function (r) {
|
||||
if (!r.ok) melden(r.text, 'fehler');
|
||||
if (r.sounds) zeigeSounds(r);
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ OBS
|
||||
|
||||
function zeigeObs(o) {
|
||||
zustand.obs = o;
|
||||
var fehlt = !o.pc || !o.obs;
|
||||
$('obs-fehlt').hidden = !fehlt;
|
||||
$('obs-da').hidden = fehlt;
|
||||
if (!o.pc) {
|
||||
$('obs-fehlt-titel').textContent = 'Stream-PC nicht verbunden';
|
||||
$('obs-fehlt-text').textContent = 'Auf dem Stream-PC muss abdulive ingest laufen und einmal mit ' +
|
||||
'„abdulive fern:verbinden …“ eingerichtet sein. Der Befehl steht auf der Übersicht unter „Stream-PC verbinden“.';
|
||||
} else if (!o.obs) {
|
||||
$('obs-fehlt-titel').textContent = 'OBS ist nicht erreichbar';
|
||||
$('obs-fehlt-text').textContent = 'Der Stream-PC ist da, aber OBS nicht. Läuft OBS, und ist ' +
|
||||
'obs-websocket an (Werkzeuge → WebSocket-Servereinstellungen)?';
|
||||
}
|
||||
if (fehlt) return;
|
||||
|
||||
var szenen = $('szenen');
|
||||
szenen.textContent = '';
|
||||
o.szenen.forEach(function (name) {
|
||||
var b = el('button', 'szene' + (name === o.aktuell ? ' aktiv' : ''));
|
||||
b.type = 'button';
|
||||
b.dataset.szene = name;
|
||||
b.appendChild(el('span', 'szene-name', name));
|
||||
b.appendChild(el('span', 'marke-an', 'Im Bild'));
|
||||
szenen.appendChild(b);
|
||||
});
|
||||
if (!o.szenen.length) szenen.appendChild(el('p', 'klein', 'OBS meldet keine Szenen.'));
|
||||
|
||||
$('clip').disabled = !o.puffer;
|
||||
$('clip-hinweis').textContent = o.puffer
|
||||
? 'Sichert die letzten Sekunden aus dem Wiedergabepuffer, wie !clip im Chat – ohne Abklingzeit.'
|
||||
: 'Der Wiedergabepuffer läuft nicht. In OBS unten rechts „Wiedergabepuffer starten“.';
|
||||
|
||||
var ton = $('ton');
|
||||
ton.textContent = '';
|
||||
o.ton.forEach(function (t) {
|
||||
var b = el('button', 'ton-zeile' + (t.stumm ? ' stumm' : ''));
|
||||
b.type = 'button';
|
||||
b.dataset.quelle = t.name;
|
||||
b.dataset.stumm = t.stumm ? '1' : '0';
|
||||
b.innerHTML = t.stumm
|
||||
? '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 5 6 9H3v6h3l5 4zM16 9l5 6M21 9l-5 6"/></svg>'
|
||||
: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 5 6 9H3v6h3l5 4zM15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg>';
|
||||
b.appendChild(el('span', 'ton-name', t.name));
|
||||
b.appendChild(el('span', 'ton-stand', t.stumm ? 'stumm' : 'an'));
|
||||
ton.appendChild(b);
|
||||
});
|
||||
if (!o.ton.length) ton.appendChild(el('p', 'klein', 'Keine Toneingänge gefunden.'));
|
||||
}
|
||||
|
||||
function obsRuf(knopf, felder) {
|
||||
tippen();
|
||||
return mitWarten(knopf, rufen('/pult/obs', felder)).then(function (r) {
|
||||
melden(r.text, r.ok ? 'ok' : 'fehler');
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
$('szenen').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('.szene');
|
||||
if (!b || b.classList.contains('aktiv')) return;
|
||||
obsRuf(b, { was: 'szene', szene: b.dataset.szene }).then(function (r) {
|
||||
// Sofort umfaerben, nicht erst auf den naechsten Stand warten. Der
|
||||
// Stand vom PC kommt hinterher und rueckt es gerade, falls noetig.
|
||||
if (r.ok) zeigeObs(Object.assign({}, zustand.obs, { aktuell: b.dataset.szene }));
|
||||
});
|
||||
});
|
||||
$('clip').addEventListener('click', function () { obsRuf($('clip'), { was: 'clip' }); });
|
||||
$('ton').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('.ton-zeile');
|
||||
if (!b) return;
|
||||
obsRuf(b, { was: 'stumm', quelle: b.dataset.quelle, stumm: b.dataset.stumm === '1' ? '0' : '1' });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Liegestuetze
|
||||
|
||||
function zeigeLz(st) {
|
||||
if (!st) return;
|
||||
$('lz-karte').hidden = false;
|
||||
if (typeof st.offen === 'number') $('lz-offen').textContent = st.offen.toLocaleString('de-DE');
|
||||
if (typeof st.gemacht === 'number') $('lz-gemacht').textContent = st.gemacht.toLocaleString('de-DE');
|
||||
}
|
||||
document.querySelector('.lz-knoepfe').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('button[data-um]');
|
||||
if (!b) return;
|
||||
tippen();
|
||||
mitWarten(b, rufen('/liegestuetze', { was: 'aendern', wert: b.dataset.um })).then(function (r) {
|
||||
zeigeLz(r);
|
||||
if (!r.ok) melden(r.text, 'fehler');
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Vorlesen
|
||||
|
||||
function zeigeWartend(liste) {
|
||||
var box = $('wartend');
|
||||
box.textContent = '';
|
||||
$('tts-anzahl').hidden = !liste.length;
|
||||
$('tts-anzahl').textContent = String(liste.length);
|
||||
$('nav-tts').hidden = !liste.length;
|
||||
$('alle-ablehnen').hidden = liste.length < 2;
|
||||
if (!liste.length) { box.appendChild(el('div', 'leer-zeile', 'Nichts wartet auf Freigabe.')); return; }
|
||||
liste.forEach(function (w) {
|
||||
var n = el('div', 'nachricht');
|
||||
n.appendChild(el('div', 'wer', w.name));
|
||||
n.appendChild(el('div', 'was', w.text));
|
||||
var reihe = el('div', 'reihe');
|
||||
var ja = el('button', 'knopf primaer', 'Vorlesen');
|
||||
ja.type = 'button'; ja.dataset.was = 'freigeben'; ja.dataset.id = w.id;
|
||||
var nein = el('button', 'knopf', 'Ablehnen');
|
||||
nein.type = 'button'; nein.dataset.was = 'ablehnen'; nein.dataset.id = w.id;
|
||||
reihe.appendChild(ja); reihe.appendChild(nein);
|
||||
n.appendChild(reihe);
|
||||
box.appendChild(n);
|
||||
});
|
||||
}
|
||||
|
||||
function ladeWartend() {
|
||||
if (!zustand.ttsAn) return;
|
||||
holen('/tts/wartend').then(function (b) { zeigeWartend(b.wartend || []); }, function () { /* still */ });
|
||||
}
|
||||
|
||||
function ttsRuf(was, id) {
|
||||
tippen();
|
||||
return rufen('/tts', { was: was, id: id || '' }).then(function (r) {
|
||||
melden(r.text, r.ok ? 'ok' : 'fehler');
|
||||
if (r.wartend) zeigeWartend(r.wartend);
|
||||
});
|
||||
}
|
||||
$('wartend').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('button[data-was]');
|
||||
if (b) ttsRuf(b.dataset.was, b.dataset.id);
|
||||
});
|
||||
$('alle-ablehnen').addEventListener('click', function () { ttsRuf('alle-ablehnen'); });
|
||||
|
||||
// Der Panikschalter. Ohne Vorlesen haelt er wenigstens die Sounds an.
|
||||
$('still').addEventListener('click', function () {
|
||||
tippen();
|
||||
if (zustand.ttsAn) { ttsRuf('still'); return; }
|
||||
rufen('/soundboard/stopp').then(function (r) { melden(r.ok ? 'Alles still.' : r.text, r.ok ? 'ok' : 'fehler'); });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Test
|
||||
|
||||
function baueTest(knoepfe, demoLaeuft) {
|
||||
var gruppen = $('test-gruppen');
|
||||
gruppen.textContent = '';
|
||||
var namen = [];
|
||||
knoepfe.forEach(function (k) { if (namen.indexOf(k.gruppe) === -1) namen.push(k.gruppe); });
|
||||
namen.forEach(function (g) {
|
||||
var box = el('div', 'gruppe');
|
||||
box.appendChild(el('h3', 'abschnitt', g));
|
||||
var reihe = el('div', 'knoepfe');
|
||||
knoepfe.filter(function (k) { return k.gruppe === g; }).forEach(function (k) {
|
||||
var b = el('button', 'knopf', k.label);
|
||||
b.type = 'button';
|
||||
b.dataset.art = k.art;
|
||||
if (k.nurOffline) b.dataset.offline = '1';
|
||||
reihe.appendChild(b);
|
||||
});
|
||||
box.appendChild(reihe);
|
||||
gruppen.appendChild(box);
|
||||
});
|
||||
setzeDemo(demoLaeuft);
|
||||
}
|
||||
|
||||
function setzeDemo(laeuft) {
|
||||
var d = $('demo');
|
||||
d.dataset.art = laeuft ? 'demo-aus' : 'demo-an';
|
||||
d.textContent = laeuft ? 'Vorführung stoppen' : 'Vorführung starten';
|
||||
d.disabled = !laeuft && zustand.live;
|
||||
}
|
||||
|
||||
function testRuf(knopf) {
|
||||
tippen();
|
||||
mitWarten(knopf, rufen('/test', { art: knopf.dataset.art })).then(function (r) {
|
||||
melden(r.text, r.ok ? 'ok' : 'fehler');
|
||||
if (typeof r.demo === 'boolean') setzeDemo(r.demo);
|
||||
});
|
||||
}
|
||||
$('test-gruppen').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('button[data-art]');
|
||||
if (b && !b.disabled) testRuf(b);
|
||||
});
|
||||
$('demo').addEventListener('click', function () { testRuf($('demo')); });
|
||||
|
||||
// ------------------------------------------------------------ Strom
|
||||
|
||||
var quelle = null;
|
||||
var zuletzt = Date.now();
|
||||
|
||||
function verbinde() {
|
||||
if (quelle) { try { quelle.close(); } catch (e) { /* zu */ } }
|
||||
quelle = new EventSource('/events?quelle=pult');
|
||||
zuletzt = Date.now();
|
||||
quelle.onopen = function () { setzeNetz(true); };
|
||||
quelle.onerror = function () {
|
||||
setzeNetz(false);
|
||||
// EventSource sagt nicht, ob es an der Anmeldung liegt. Eine kurze
|
||||
// Frage klaert das, sonst stuende "keine Verbindung" fuer immer da.
|
||||
holen('/soundboard/liste').then(function () { /* nur Netz */ }, function () { /* abgemeldet() hat schon */ });
|
||||
};
|
||||
quelle.onmessage = function (e) {
|
||||
zuletzt = Date.now();
|
||||
setzeNetz(true);
|
||||
var ev;
|
||||
try { ev = JSON.parse(e.data); } catch (err) { return; }
|
||||
switch (ev.kind) {
|
||||
case 'state':
|
||||
zeigeStream(ev.stream);
|
||||
if (ev.obs) zeigeObs(ev.obs);
|
||||
if (ev.liegestuetze && !$('lz-karte').hidden) zeigeLz(ev.liegestuetze);
|
||||
break;
|
||||
case 'stream': zeigeStream(ev); break;
|
||||
case 'obs': zeigeObs(ev); break;
|
||||
case 'liegestuetze': if (!$('lz-karte').hidden) zeigeLz(ev); break;
|
||||
case 'sound': knall(ev.id); break;
|
||||
case 'sounds-geaendert': ladeSounds(); break;
|
||||
case 'tts': ladeWartend(); break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setzeNetz(an) {
|
||||
$('netz').classList.toggle('an', an);
|
||||
var banner = $('banner');
|
||||
banner.hidden = an;
|
||||
if (!an) banner.textContent = 'Keine Verbindung zum Hub – verbinde neu …';
|
||||
}
|
||||
|
||||
// Wachhund wie in client.js: der Hub schickt alle 15 s einen Herzschlag.
|
||||
// Am Handy reisst die Leitung beim Wegwischen ab, ohne dass es jemand merkt.
|
||||
setInterval(function () {
|
||||
if (Date.now() - zuletzt > 45000) verbinde();
|
||||
}, 10000);
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible' && Date.now() - zuletzt > 20000) verbinde();
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Start
|
||||
|
||||
holen('/pult/knoepfe').then(function (b) {
|
||||
baueTest(b.knoepfe || [], !!b.demo);
|
||||
zustand.ttsAn = !!b.tts;
|
||||
$('tts-karte').hidden = !b.tts;
|
||||
if (b.liegestuetze) zeigeLz(b.liegestuetze);
|
||||
ladeWartend();
|
||||
}, function () { /* abgemeldet oder weg - zeigt sich oben */ });
|
||||
ladeSounds();
|
||||
verbinde();
|
||||
wachHolen();
|
||||
|
||||
// Die Liste der Wartenden kommt nicht ueber den Strom (dort steht nur, was
|
||||
// vorgelesen wird). Also nachsehen, solange das Pult offen ist.
|
||||
setInterval(function () {
|
||||
if (document.visibilityState === 'visible') ladeWartend();
|
||||
}, 4000);
|
||||
// Ob die Sound-Quelle in OBS offen ist, aendert sich ohne Ereignis.
|
||||
setInterval(function () {
|
||||
if (document.visibilityState === 'visible') ladeSounds();
|
||||
}, 20000);
|
||||
|
||||
// Ein Reiter in der Adresse (Verknuepfung "Szenen" am App-Symbol) geht vor
|
||||
// dem zuletzt benutzten.
|
||||
var gemerkt = location.hash.slice(1) || null;
|
||||
if (!gemerkt) { try { gemerkt = localStorage.getItem('pult-reiter'); } catch (e) { /* privat */ } }
|
||||
if (gemerkt && $('tab-' + gemerkt)) zeigeReiter(gemerkt);
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').catch(function (e) {
|
||||
console.warn('[pult] Service Worker nicht registriert', e);
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user