Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein Build, kein Framework - OBS laedt die Dateien direkt, und jede Zwischenstufe waere ein Ding, das mitten im Stream kaputtgehen kann. Server-Sent Events statt WebSocket, entgegen der Skizze in chat-control.md: Node bringt einen WebSocket-*Client* mit, aber keinen Server, `EventSource` verbindet sich von allein neu, und die Richtung stimmt - das Overlay hoert nur zu. Gegen die tote Browser-Source nach einem Neustart stehen drei Dinge: der eingebaute Reconnect, ein Herzschlag als echtes Ereignis (ein SSE-Kommentar haelt die Verbindung offen, ist im Browser aber unsichtbar), und ein Zustands-Schnappschuss beim Verbinden. Abos, Raids, Bits und Loeschungen kommen ueber IRC-USERNOTICE und CLEARCHAT herein; das wurde bisher weggeworfen. Follows fehlen weiter, die gibt es nur ueber EventSub. GIFs im Chat: Twitch schickt dafuer einen Tag `gifs`, den wir nicht kannten. gifs=0-22|Pnd7qLFuGCOntZcNKB|https://media0.giphy.com/media/.../giphy.gif PRIVMSG #abdulive_ :[Idk GIF by Respective] Der Nachrichtentext ist nur ein Platzhalter, der Bereich markiert ihn. Ausgewertet wird er jetzt; ohne das stand woertlich "[Idk GIF by Respective]" im Bild. Gefunden wurde der Tag durch einen Rohmitschnitt - in 3094 Nachrichten aus acht grossen Kanaelen war kein einziges GIF, weil die Adresse eben nicht im Text steht. Zwei Fehler, die beim Durchklicken im Browser aufgefallen sind: - Die Aufraeumfunktion im Chat zaehlte Zeilen, die erst nach der Ausblend-Animation aus dem DOM verschwinden, und lief in eine Endlosschleife. In OBS waere die Quelle beim vierzehnten Chatbeitrag eingefroren. - Der Bereichsparser der GIFs war zu nachsichtig: `-1-4` zerfaellt beim Trennen in ['', '1', '4'], und `Number('')` ist 0 - der Bereich saehe gueltig aus und wuerde den falschen Textteil ersetzen. Auf der Uebersichtsseite steht fuer jede Einblendung, jede Chat-Variante und jeden Effekt ein Knopf. Ohne den liesse sich eine Raid-Einblendung nur pruefen, indem man auf einen echten Raid wartet. Was einen Zustand setzt, ist waehrend eines Streams gesperrt; "Stand zuruecksetzen" ausdruecklich nicht, sonst kaeme man aus einem erfundenen Zustand nicht mehr heraus. build-package.ts nimmt jetzt auch html/css/js mit - sonst liefert das Paket 404 statt Overlay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
142 lines
4.2 KiB
HTML
142 lines
4.2 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>abdulive - Statusleiste</title>
|
|
<link rel="stylesheet" href="/theme.css">
|
|
<style>
|
|
#leiste {
|
|
position: fixed;
|
|
left: 28px; right: 28px; top: 22px;
|
|
height: 92px;
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
align-items: center;
|
|
gap: 28px;
|
|
padding: 0 30px;
|
|
border-radius: 20px;
|
|
background: var(--tinte-klar);
|
|
backdrop-filter: blur(16px) saturate(150%);
|
|
border: 1px solid rgba(255, 255, 255, .1);
|
|
box-shadow: var(--schatten), var(--glanz);
|
|
/* Ein Farbverlauf an der Oberkante statt eines Rahmens: er bindet die
|
|
* Leiste an die Marke, ohne einen Kasten um alles zu ziehen. */
|
|
overflow: hidden;
|
|
}
|
|
#leiste::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0; right: 0; top: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--lila), var(--pink), var(--tuerkis), var(--lila));
|
|
background-size: 300% 100%;
|
|
animation: wandern 9s linear infinite;
|
|
}
|
|
@keyframes wandern { to { background-position: 300% 0; } }
|
|
|
|
.marke { display: flex; align-items: center; gap: 12px; }
|
|
.punkt {
|
|
width: 13px; height: 13px;
|
|
border-radius: 50%;
|
|
background: var(--rot);
|
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, .6);
|
|
}
|
|
body.live .punkt { background: var(--rot); animation: klopfen 2s ease-out infinite; }
|
|
body:not(.live) .punkt { background: #555; }
|
|
@keyframes klopfen {
|
|
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, .55); }
|
|
70% { box-shadow: 0 0 0 16px rgba(239, 68, 68, 0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
|
}
|
|
.kanal { font-size: 26px; font-weight: 700; letter-spacing: -.01em; }
|
|
|
|
.mitte { min-width: 0; }
|
|
#titel {
|
|
font-size: 25px; font-weight: 600;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
#kategorie { font-size: 17px; color: var(--schrift-leise); margin-top: 2px; }
|
|
|
|
.werte { display: flex; gap: 30px; }
|
|
.wert { text-align: right; min-width: 92px; }
|
|
.wert .zahl {
|
|
font-size: 30px; font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1.1;
|
|
display: block;
|
|
}
|
|
.wert .beschriftung {
|
|
font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
|
|
color: var(--schrift-leise);
|
|
}
|
|
/* Kurzes Aufleuchten, wenn sich eine Zahl aendert. Ohne das uebersieht man
|
|
* jede Veraenderung, weil die Leiste sonst voellig still steht. */
|
|
.zahl.neu { animation: aufleuchten .8s ease-out; }
|
|
@keyframes aufleuchten {
|
|
0% { color: var(--tuerkis); transform: translateY(-3px); }
|
|
100% { color: inherit; transform: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="leiste">
|
|
<div class="marke">
|
|
<span class="punkt"></span>
|
|
<span class="kanal kontur" id="kanal">abdulive_</span>
|
|
</div>
|
|
<div class="mitte">
|
|
<div id="titel" class="kontur">nicht live</div>
|
|
<div id="kategorie"></div>
|
|
</div>
|
|
<div class="werte">
|
|
<div class="wert"><span class="zahl" id="zuschauer">-</span>
|
|
<span class="beschriftung">Zuschauer</span></div>
|
|
<div class="wert"><span class="zahl" id="laufzeit">-</span>
|
|
<span class="beschriftung">live seit</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/client.js"></script>
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
var titel = document.getElementById('titel');
|
|
var kategorie = document.getElementById('kategorie');
|
|
var zuschauer = document.getElementById('zuschauer');
|
|
var laufzeit = document.getElementById('laufzeit');
|
|
var kanal = document.getElementById('kanal');
|
|
var seit = null;
|
|
|
|
kanal.textContent = Overlay.param('kanal', 'abdulive_');
|
|
|
|
function setze(el, wert) {
|
|
if (el.textContent === wert) return;
|
|
el.textContent = wert;
|
|
el.classList.remove('neu');
|
|
// Neuzeichnen erzwingen, sonst startet die Animation beim zweiten Mal nicht.
|
|
void el.offsetWidth;
|
|
el.classList.add('neu');
|
|
}
|
|
|
|
function stand(ev) {
|
|
document.body.classList.toggle('live', !!ev.live);
|
|
titel.textContent = ev.live ? (ev.title || 'ohne Titel') : 'nicht live';
|
|
kategorie.textContent = ev.live ? (ev.category || '') : '';
|
|
setze(zuschauer, ev.live ? Overlay.zahl(ev.viewers) : '-');
|
|
seit = ev.live ? ev.startedAt : null;
|
|
tick();
|
|
}
|
|
|
|
function tick() {
|
|
laufzeit.textContent = seit ? Overlay.laufzeit(seit) : '-';
|
|
}
|
|
setInterval(tick, 10000);
|
|
|
|
Overlay.on('stream', stand);
|
|
Overlay.on('state', function (ev) { stand(ev.stream); });
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|