/* 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 ? '' : ''; 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); }); } })();