Pult: Chat nur lesend, am Tablet Chat neben dem Reiter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c64309c51
commit
227ecfdcfe
6 files changed
+295
-4
No files matched your search
@@ -1,4 +1,5 @@
|
||||
/* Das Pult: Soundboard, Szenen, Liegestuetze, Vorlesen - am Handy.
|
||||
/* Das Pult: Soundboard, Szenen, Chat, Liegestuetze, Vorlesen - am Handy,
|
||||
* ab Tablet-Groesse mit dem Chat daneben.
|
||||
*
|
||||
* Zwei Wege zum Hub:
|
||||
* - der Ereignisstrom /events (wie bei den Browser-Sources), damit zwei
|
||||
@@ -12,6 +13,8 @@
|
||||
'use strict';
|
||||
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
/** Gleiche Grenze wie die Media Query in pult.css. */
|
||||
var geteilt = window.matchMedia('(min-width: 760px) and (min-height: 500px)');
|
||||
var FARBEN = ['lila', 'tuerkis', 'pink', 'gold', 'gruen', 'rot', 'blau', 'grau'];
|
||||
var SCHNELL = ['🔊', '😂', '💀', '👏', '🎺', '🥁', '💥', '🚨', '🤡', '😱', '🔥', '❤️', '👀', '🐐', '🫡', '🎉'];
|
||||
|
||||
@@ -86,6 +89,9 @@
|
||||
// ------------------------------------------------------------ Reiter
|
||||
|
||||
function zeigeReiter(name) {
|
||||
// Ab Tablet-Breite steht der Chat ohnehin daneben, links bleibt dann
|
||||
// Platz fuer das, womit man ihn zusammen braucht.
|
||||
if (name === 'chat' && geteilt.matches) name = 'szenen';
|
||||
var reiter = document.querySelectorAll('.reiter button');
|
||||
for (var i = 0; i < reiter.length; i++) {
|
||||
reiter[i].classList.toggle('an', reiter[i].dataset.tab === name);
|
||||
@@ -94,6 +100,7 @@
|
||||
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();
|
||||
if (name === 'chat') chatGesehen();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
document.querySelector('.reiter').addEventListener('click', function (e) {
|
||||
@@ -558,6 +565,173 @@
|
||||
rufen('/soundboard/stopp').then(function (r) { melden(r.ok ? 'Alles still.' : r.text, r.ok ? 'ok' : 'fehler'); });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Chat
|
||||
|
||||
// Nur lesend. Die Nachrichten kommen ueber denselben Ereignisstrom wie bei
|
||||
// der Chat-Browser-Source; das Pult schreibt nie in den Chat und braucht
|
||||
// dafuer weder Bot-Konto noch weitere Rechte bei Twitch.
|
||||
var CHAT_MAX = 250;
|
||||
var chatListe = $('chat-liste');
|
||||
/** Steht die Liste am Ende? Dann wandert sie mit, sonst nicht. */
|
||||
var chatKlebt = true;
|
||||
|
||||
/* Wie in chat.html: dunkle Twitch-Farben aufhellen statt verwerfen. */
|
||||
function lesbar(hex) {
|
||||
if (!hex || !/^#[0-9a-f]{6}$/i.test(hex)) return '#c9a7ff';
|
||||
var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16),
|
||||
b = parseInt(hex.slice(5, 7), 16);
|
||||
var hell = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
if (hell >= 0.5) return hex;
|
||||
var f = 0.5 / Math.max(hell, 0.08);
|
||||
return '#' + [r, g, b].map(function (v) {
|
||||
return Math.min(255, Math.round(v * f)).toString(16).padStart(2, '0');
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/** Text mit Emotes als Knoten. Ersetzt wird der Name als ganzes Wort,
|
||||
* wie in client.js - Twitch liefert keine Positionen. */
|
||||
function emoteKnoten(ziel, text, emotes) {
|
||||
var ids = {};
|
||||
(emotes || []).forEach(function (e) { if (e && e.id && e.name) ids[e.name] = e.id; });
|
||||
text.split(/(\s+)/).forEach(function (wort) {
|
||||
if (!wort) return;
|
||||
if (!Object.prototype.hasOwnProperty.call(ids, wort)) {
|
||||
ziel.appendChild(document.createTextNode(wort));
|
||||
return;
|
||||
}
|
||||
var img = el('img', 'emote');
|
||||
img.src = 'https://static-cdn.jtvnw.net/emoticons/v2/' +
|
||||
encodeURIComponent(ids[wort]) + '/default/dark/2.0';
|
||||
img.alt = wort;
|
||||
img.title = wort;
|
||||
ziel.appendChild(img);
|
||||
});
|
||||
}
|
||||
|
||||
/** GIFs kommen mit Zeichenbereich und ersetzen ihren Platzhalter im Text. */
|
||||
function textKnoten(ziel, text, gifs, emotes) {
|
||||
var zeichen = Array.from(text);
|
||||
var pos = 0;
|
||||
(gifs || []).slice().sort(function (a, b) { return a.from - b.from; }).forEach(function (g) {
|
||||
if (typeof g.url !== 'string' || g.url.slice(0, 8) !== 'https://') return;
|
||||
if (g.from > pos) emoteKnoten(ziel, zeichen.slice(pos, g.from).join(''), emotes);
|
||||
var img = el('img', 'gif');
|
||||
img.src = g.url;
|
||||
img.alt = 'GIF';
|
||||
img.onerror = function () { img.replaceWith(document.createTextNode('[GIF]')); };
|
||||
ziel.appendChild(img);
|
||||
pos = Math.max(pos, g.to + 1);
|
||||
});
|
||||
if (pos < zeichen.length) emoteKnoten(ziel, zeichen.slice(pos).join(''), emotes);
|
||||
}
|
||||
|
||||
function uhrzeit(ts) {
|
||||
var d = ts ? new Date(ts) : new Date();
|
||||
if (isNaN(d.getTime())) d = new Date();
|
||||
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function chatSichtbar() {
|
||||
return geteilt.matches || $('tab-chat').classList.contains('an');
|
||||
}
|
||||
|
||||
function nachUnten() {
|
||||
chatListe.scrollTop = chatListe.scrollHeight;
|
||||
chatKlebt = true;
|
||||
$('chat-runter').hidden = true;
|
||||
}
|
||||
|
||||
function chatGesehen() {
|
||||
$('nav-chat-punkt').hidden = true;
|
||||
nachUnten();
|
||||
}
|
||||
|
||||
function chatAnhaengen(zeile, still) {
|
||||
chatListe.appendChild(zeile);
|
||||
while (chatListe.children.length > CHAT_MAX) chatListe.firstChild.remove();
|
||||
$('chat-leer').hidden = true;
|
||||
if (chatKlebt) nachUnten();
|
||||
else $('chat-runter').hidden = false;
|
||||
if (!still && !chatSichtbar()) $('nav-chat-punkt').hidden = false;
|
||||
}
|
||||
|
||||
function chatNachricht(ev, still) {
|
||||
if (!ev || !ev.id) return;
|
||||
if (chatListe.querySelector('[data-id="' + CSS.escape(ev.id) + '"]')) return;
|
||||
var z = el('div', 'cz' + (ev.first ? ' erstmals' : '') + (ev.bits > 0 ? ' bits' : ''));
|
||||
z.dataset.id = ev.id;
|
||||
z.dataset.user = ev.userId || '';
|
||||
z.appendChild(el('span', 'zeit', uhrzeit(ev.ts)));
|
||||
(ev.badges || []).forEach(function (b) {
|
||||
// Nur Twitchs eigene Bildadressen, wie in chat.html.
|
||||
if (!b || !/^https:\/\/static-cdn\.jtvnw\.net\//.test(b.url)) return;
|
||||
var img = el('img', 'abz');
|
||||
img.src = b.url;
|
||||
img.alt = b.title || '';
|
||||
img.title = b.title || '';
|
||||
z.appendChild(img);
|
||||
});
|
||||
var wer = el('span', 'wer', ev.name || ev.login || '?');
|
||||
wer.style.color = lesbar(ev.color);
|
||||
z.appendChild(wer);
|
||||
z.appendChild(document.createTextNode(': '));
|
||||
var text = el('span', 'text');
|
||||
textKnoten(text, String(ev.text || ''), ev.gifs, ev.emotes);
|
||||
z.appendChild(text);
|
||||
chatAnhaengen(z, still);
|
||||
}
|
||||
|
||||
/** Was im Stream passiert, als eigene Zeile zwischen den Nachrichten.
|
||||
* Erstchatter und Bits fehlen bewusst: die zeigt schon die Nachricht. */
|
||||
function alarmText(a) {
|
||||
var n = a.name || 'Jemand';
|
||||
switch (a.type) {
|
||||
case 'sub': return n + ' hat abonniert' + (a.tier ? ' (Stufe ' + String(a.tier).charAt(0) + ')' : '');
|
||||
case 'resub': return n + ' abonniert weiter' + (a.months ? ', seit ' + a.months + ' Monaten' : '');
|
||||
case 'subgift': return n + ' verschenkt ein Abo' + (a.recipient ? ' an ' + a.recipient : '');
|
||||
case 'massgift': return n + ' verschenkt ' + (a.amount || 'mehrere') + ' Abos';
|
||||
case 'raid': return 'Raid von ' + n + (a.amount ? ' mit ' + a.amount + ' Zuschauern' : '');
|
||||
case 'follow': return n + ' folgt jetzt';
|
||||
case 'reward': return n + ' löst „' + (a.reward || 'Belohnung') + '“ ein';
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
function chatAlarm(a) {
|
||||
var text = alarmText(a);
|
||||
if (!text) return;
|
||||
var z = el('div', 'cz system');
|
||||
z.appendChild(el('span', 'zeit', uhrzeit()));
|
||||
z.appendChild(document.createTextNode(text + (a.message ? ': ' + a.message : '')));
|
||||
chatAnhaengen(z);
|
||||
}
|
||||
|
||||
/** Von einem Mod geloescht: durchstreichen statt verschwinden lassen,
|
||||
* damit man am Pult sieht, dass da etwas war. */
|
||||
function chatLoeschen(ev) {
|
||||
var zeilen = chatListe.querySelectorAll('.cz:not(.system)');
|
||||
for (var i = 0; i < zeilen.length; i++) {
|
||||
var z = zeilen[i];
|
||||
if ((!ev.messageId && !ev.userId) ||
|
||||
(ev.messageId && z.dataset.id === ev.messageId) ||
|
||||
(ev.userId && z.dataset.user === ev.userId)) z.classList.add('weg');
|
||||
}
|
||||
}
|
||||
|
||||
chatListe.addEventListener('scroll', function () {
|
||||
chatKlebt = chatListe.scrollHeight - chatListe.scrollTop - chatListe.clientHeight < 40;
|
||||
if (chatKlebt) $('chat-runter').hidden = true;
|
||||
}, { passive: true });
|
||||
// Emotes und GIFs laden nach und machen die Zeile hoeher. Ohne das
|
||||
// rutscht die neueste Nachricht unter den Rand.
|
||||
chatListe.addEventListener('load', function () { if (chatKlebt) nachUnten(); }, true);
|
||||
$('chat-runter').addEventListener('click', function () { tippen(); nachUnten(); });
|
||||
|
||||
geteilt.addEventListener('change', function () {
|
||||
if (geteilt.matches && $('tab-chat').classList.contains('an')) zeigeReiter('szenen');
|
||||
if (geteilt.matches) chatGesehen();
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Test
|
||||
|
||||
function baueTest(knoepfe, demoLaeuft) {
|
||||
@@ -628,7 +802,13 @@
|
||||
zeigeStream(ev.stream);
|
||||
if (ev.obs) zeigeObs(ev.obs);
|
||||
if (ev.liegestuetze && !$('lz-karte').hidden) zeigeLz(ev.liegestuetze);
|
||||
// Nach dem Neuverbinden: was fehlt, nachtragen. Schon Gezeigtes
|
||||
// bleibt stehen, sonst waere der Rueckblick bei jedem Abriss weg.
|
||||
(ev.chat || []).forEach(function (m) { chatNachricht(m, true); });
|
||||
break;
|
||||
case 'chat': chatNachricht(ev, false); break;
|
||||
case 'alert': chatAlarm(ev); break;
|
||||
case 'clear': chatLoeschen(ev); break;
|
||||
case 'stream': zeigeStream(ev); break;
|
||||
case 'obs': zeigeObs(ev); break;
|
||||
case 'liegestuetze': if (!$('lz-karte').hidden) zeigeLz(ev); break;
|
||||
@@ -684,6 +864,8 @@
|
||||
// dem zuletzt benutzten.
|
||||
var gemerkt = location.hash.slice(1) || null;
|
||||
if (!gemerkt) { try { gemerkt = localStorage.getItem('pult-reiter'); } catch (e) { /* privat */ } }
|
||||
// Am Tablet ist der naheliegende Anfang Szenen neben dem Chat.
|
||||
if (!gemerkt && geteilt.matches) gemerkt = 'szenen';
|
||||
if (gemerkt && $('tab-' + gemerkt)) zeigeReiter(gemerkt);
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
||||
Reference in new issue
Block a user