Liegestuetze: pro Stream mitzaehlen, wie viele gemacht sind
Herunterzaehlen mit -1/-5/-10 zaehlt als gemacht, Setzen bleibt eine Korrektur und zaehlt nicht. Der Stand "gemacht" beginnt mit jedem neuen Stream bei 0 (nicht beim Neustart des Hubs mitten im Stream), steht im Overlay unter der offenen Zahl und laesst sich auf der Uebersicht korrigieren. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
eb9eab949e
commit
094a791da2
7 files changed
+213
-79
No files matched your search
+24
-13
@@ -99,7 +99,7 @@ export async function runHub(): Promise<void> {
|
|||||||
const liegestuetze = createLiegestuetze(db);
|
const liegestuetze = createLiegestuetze(db);
|
||||||
// Gleich zu Beginn in den Bus, sonst zeigt eine frisch verbundene
|
// Gleich zu Beginn in den Bus, sonst zeigt eine frisch verbundene
|
||||||
// Browser-Source 0, bis die naechste Einloesung kommt.
|
// Browser-Source 0, bis die naechste Einloesung kommt.
|
||||||
bus.publish({ kind: 'liegestuetze', offen: liegestuetze.stand() });
|
bus.publish({ kind: 'liegestuetze', ...liegestuetze.stand() });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eine Einloesung abschliessen (FULFILLED) oder die Punkte zurueckgeben
|
* Eine Einloesung abschliessen (FULFILLED) oder die Punkte zurueckgeben
|
||||||
@@ -196,8 +196,7 @@ export async function runHub(): Promise<void> {
|
|||||||
if (ent.aktion === 'emote-regen') bus.publish({ kind: 'hype', score: 5, rate: 40 });
|
if (ent.aktion === 'emote-regen') bus.publish({ kind: 'hype', score: 5, rate: 40 });
|
||||||
if (ent.aktion === 'liegestuetze') {
|
if (ent.aktion === 'liegestuetze') {
|
||||||
const dazu = ent.plan.anzahl ?? 5;
|
const dazu = ent.plan.anzahl ?? 5;
|
||||||
const offen = liegestuetze.aendern(dazu);
|
bus.publish({ kind: 'liegestuetze', ...liegestuetze.aendern(dazu), dazu, name: e.userName });
|
||||||
bus.publish({ kind: 'liegestuetze', offen, dazu, name: e.userName });
|
|
||||||
}
|
}
|
||||||
log.info(`${e.userName} hat "${e.rewardTitle}" eingeloest -> ${ent.aktion}`);
|
log.info(`${e.userName} hat "${e.rewardTitle}" eingeloest -> ${ent.aktion}`);
|
||||||
void einloesungSetzen(e.rewardId, e.id, 'FULFILLED');
|
void einloesungSetzen(e.rewardId, e.id, 'FULFILLED');
|
||||||
@@ -331,7 +330,7 @@ export async function runHub(): Promise<void> {
|
|||||||
goLive: announcer.lastResult() ?? 'noch nichts',
|
goLive: announcer.lastResult() ?? 'noch nichts',
|
||||||
overlayClients: bus.clients(),
|
overlayClients: bus.clients(),
|
||||||
ttsWartend: warteschlange.liste().length,
|
ttsWartend: warteschlange.liste().length,
|
||||||
liegestuetzeOffen: liegestuetze.stand(),
|
liegestuetze: liegestuetze.stand(),
|
||||||
ttsStimme: stimmeFehlt.length ? `fehlt: ${stimmeFehlt.join(', ')}` : 'bereit',
|
ttsStimme: stimmeFehlt.length ? `fehlt: ${stimmeFehlt.join(', ')}` : 'bereit',
|
||||||
spikeArmed: spikes.isArmed(),
|
spikeArmed: spikes.isArmed(),
|
||||||
note: state.chatAnonymous
|
note: state.chatAnonymous
|
||||||
@@ -374,23 +373,35 @@ export async function runHub(): Promise<void> {
|
|||||||
liegestuetze: {
|
liegestuetze: {
|
||||||
stand: () => liegestuetze.stand(),
|
stand: () => liegestuetze.stand(),
|
||||||
aendern: (n) => {
|
aendern: (n) => {
|
||||||
const offen = liegestuetze.aendern(n);
|
const neu = liegestuetze.aendern(n);
|
||||||
bus.publish({ kind: 'liegestuetze', offen });
|
bus.publish({ kind: 'liegestuetze', ...neu });
|
||||||
log.info(`Liegestuetze von Hand ${n > 0 ? '+' : ''}${n} -> ${offen} offen`);
|
log.info(`Liegestuetze von Hand ${n > 0 ? '+' : ''}${n} -> ` +
|
||||||
return offen;
|
`${neu.offen} offen, ${neu.gemacht} gemacht`);
|
||||||
|
return neu;
|
||||||
},
|
},
|
||||||
setzen: (n) => {
|
setzen: (n) => {
|
||||||
const offen = liegestuetze.setzen(n);
|
const neu = liegestuetze.setzen(n);
|
||||||
bus.publish({ kind: 'liegestuetze', offen });
|
bus.publish({ kind: 'liegestuetze', ...neu });
|
||||||
log.info(`Liegestuetze von Hand gesetzt -> ${offen} offen`);
|
log.info(`Liegestuetze von Hand gesetzt -> ${neu.offen} offen`);
|
||||||
return offen;
|
return neu;
|
||||||
|
},
|
||||||
|
gemachtSetzen: (n) => {
|
||||||
|
const neu = liegestuetze.gemachtSetzen(n);
|
||||||
|
bus.publish({ kind: 'liegestuetze', ...neu });
|
||||||
|
log.info(`Liegestuetze gemacht von Hand gesetzt -> ${neu.gemacht}`);
|
||||||
|
return neu;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
void server;
|
void server;
|
||||||
|
|
||||||
// ---- Zuschauerzahlen und Stream-Sitzungen
|
// ---- Zuschauerzahlen und Stream-Sitzungen
|
||||||
void pollStream(cfg, repo, state, bus, abort.signal, (stream) => announcer.announce(stream));
|
void pollStream(cfg, repo, state, bus, abort.signal, (stream) => {
|
||||||
|
announcer.announce(stream);
|
||||||
|
// Nur bei einem wirklich neuen Stream, nicht beim Neustart des Hubs
|
||||||
|
// mitten im Stream: dann wird die Sitzung uebernommen, nicht gestartet.
|
||||||
|
bus.publish({ kind: 'liegestuetze', ...liegestuetze.neuerStream() });
|
||||||
|
});
|
||||||
|
|
||||||
// ---- EventSub erst, wenn ein Konto verbunden ist. Ohne Nutzer-Token
|
// ---- EventSub erst, wenn ein Konto verbunden ist. Ohne Nutzer-Token
|
||||||
// wuerde der Verbindungsversuch im Minutentakt scheitern und nichts als
|
// wuerde der Verbindungsversuch im Minutentakt scheitern und nichts als
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Der Zaehler haelt fest, was Zuschauer mit Kanalpunkten bezahlt haben. Er
|
// Der Zaehler haelt fest, was Zuschauer mit Kanalpunkten bezahlt haben und
|
||||||
// darf weder beim Neustart vergessen noch ins Negative laufen.
|
// was Abdu davon geschafft hat. Er darf weder beim Neustart vergessen noch
|
||||||
|
// ins Negative laufen, und eine Korrektur ist keine Liegestuetze.
|
||||||
|
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -25,23 +26,43 @@ function oeffnen(file: string): DatabaseSync {
|
|||||||
|
|
||||||
test('Liegestuetze: zaehlt hoch und herunter, nie unter 0', () => {
|
test('Liegestuetze: zaehlt hoch und herunter, nie unter 0', () => {
|
||||||
const lz = createLiegestuetze(oeffnen(datei()));
|
const lz = createLiegestuetze(oeffnen(datei()));
|
||||||
assert.equal(lz.stand(), 0);
|
assert.deepEqual(lz.stand(), { offen: 0, gemacht: 0 });
|
||||||
assert.equal(lz.aendern(10), 10);
|
assert.equal(lz.aendern(10).offen, 10);
|
||||||
assert.equal(lz.aendern(10), 20);
|
assert.equal(lz.aendern(10).offen, 20);
|
||||||
assert.equal(lz.aendern(-5), 15);
|
assert.equal(lz.aendern(-5).offen, 15);
|
||||||
assert.equal(lz.aendern(-100), 0);
|
assert.equal(lz.aendern(-100).offen, 0);
|
||||||
assert.equal(lz.setzen(42), 42);
|
assert.equal(lz.setzen(42).offen, 42);
|
||||||
assert.equal(lz.setzen(-3), 0);
|
assert.equal(lz.setzen(-3).offen, 0);
|
||||||
assert.equal(lz.setzen(10_000_000), HOECHSTENS);
|
assert.equal(lz.setzen(10_000_000).offen, HOECHSTENS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Liegestuetze: Herunterzaehlen ist gemacht, Setzen nicht', () => {
|
||||||
|
const lz = createLiegestuetze(oeffnen(datei()));
|
||||||
|
lz.aendern(20);
|
||||||
|
assert.deepEqual(lz.aendern(-5), { offen: 15, gemacht: 5 });
|
||||||
|
// Nur was wirklich offen war, kann gemacht sein.
|
||||||
|
assert.deepEqual(lz.aendern(-100), { offen: 0, gemacht: 20 });
|
||||||
|
lz.aendern(10);
|
||||||
|
assert.deepEqual(lz.setzen(3), { offen: 3, gemacht: 20 }, 'Korrektur zaehlt nicht');
|
||||||
|
assert.deepEqual(lz.gemachtSetzen(7), { offen: 3, gemacht: 7 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Liegestuetze: neuer Stream setzt gemacht zurueck, offen bleibt', () => {
|
||||||
|
const lz = createLiegestuetze(oeffnen(datei()));
|
||||||
|
lz.aendern(30);
|
||||||
|
lz.aendern(-10);
|
||||||
|
assert.deepEqual(lz.neuerStream(), { offen: 20, gemacht: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Liegestuetze: der Stand ueberlebt einen Neustart', () => {
|
test('Liegestuetze: der Stand ueberlebt einen Neustart', () => {
|
||||||
const file = datei();
|
const file = datei();
|
||||||
const erst = oeffnen(file);
|
const erst = oeffnen(file);
|
||||||
createLiegestuetze(erst).aendern(30);
|
const lz = createLiegestuetze(erst);
|
||||||
|
lz.aendern(30);
|
||||||
|
lz.aendern(-5);
|
||||||
erst.close();
|
erst.close();
|
||||||
|
|
||||||
assert.equal(createLiegestuetze(oeffnen(file)).stand(), 30);
|
assert.deepEqual(createLiegestuetze(oeffnen(file)).stand(), { offen: 25, gemacht: 5 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Belohnung mit Aktion liegestuetze wird ausgefuehrt und traegt die Anzahl', () => {
|
test('Belohnung mit Aktion liegestuetze wird ausgefuehrt und traegt die Anzahl', () => {
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
// Offene Liegestuetze aus Kanalpunkten.
|
// Liegestuetze aus Kanalpunkten: was offen ist und was in diesem Stream
|
||||||
|
// schon gemacht wurde.
|
||||||
//
|
//
|
||||||
// Eine Einloesung zaehlt hoch, Abdu zaehlt auf der Uebersichtsseite wieder
|
// Eine Einloesung zaehlt `offen` hoch. Abdu zaehlt auf der Uebersichtsseite
|
||||||
// herunter, wenn er sie gemacht hat - oder korrigiert, wenn etwas nicht
|
// herunter, wenn er sie gemacht hat - das wandert nach `gemacht`. Setzen ist
|
||||||
// stimmt. Der Stand liegt in der Datenbank: was die Zuschauer bezahlt haben,
|
// dagegen eine Korrektur und zaehlt nicht als gemacht: wer einen Tippfehler
|
||||||
// darf ein Neustart nicht loeschen.
|
// behebt, hat dabei keine Liegestuetze gemacht.
|
||||||
|
//
|
||||||
|
// Beides liegt in der Datenbank: was die Zuschauer bezahlt haben, darf ein
|
||||||
|
// Neustart nicht loeschen. `gemacht` beginnt mit jedem neuen Stream bei 0.
|
||||||
|
|
||||||
import type { Db } from '#db/index.ts';
|
import type { Db } from '#db/index.ts';
|
||||||
|
|
||||||
/** Mehr glaubt niemand, und ein Tippfehler mit sechs Nullen soll nicht im Bild stehen. */
|
/** Mehr glaubt niemand, und ein Tippfehler mit sechs Nullen soll nicht im Bild stehen. */
|
||||||
export const HOECHSTENS = 99_999;
|
export const HOECHSTENS = 99_999;
|
||||||
|
|
||||||
|
export type LiegestuetzStand = { offen: number; gemacht: number };
|
||||||
|
|
||||||
export type Liegestuetze = {
|
export type Liegestuetze = {
|
||||||
stand: () => number;
|
stand: () => LiegestuetzStand;
|
||||||
/** Zaehlt um `n` hoch oder (negativ) herunter. Nie unter 0. Gibt den neuen Stand zurueck. */
|
/**
|
||||||
aendern: (n: number) => number;
|
* Zaehlt `offen` um `n` hoch oder (negativ) herunter, nie unter 0. Was
|
||||||
/** Setzt den Stand fest. Gibt den neuen Stand zurueck. */
|
* dabei wirklich abgezogen wurde, kommt zu `gemacht`.
|
||||||
setzen: (n: number) => number;
|
*/
|
||||||
|
aendern: (n: number) => LiegestuetzStand;
|
||||||
|
/** Setzt `offen` fest. Korrektur, zaehlt nicht als gemacht. */
|
||||||
|
setzen: (n: number) => LiegestuetzStand;
|
||||||
|
/** Setzt `gemacht` fest. Korrektur. */
|
||||||
|
gemachtSetzen: (n: number) => LiegestuetzStand;
|
||||||
|
/** Neuer Stream: `gemacht` auf 0, `offen` bleibt. */
|
||||||
|
neuerStream: () => LiegestuetzStand;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createLiegestuetze(db: Db, name = 'liegestuetze'): Liegestuetze {
|
export function createLiegestuetze(db: Db, name = 'liegestuetze'): Liegestuetze {
|
||||||
@@ -24,13 +37,27 @@ export function createLiegestuetze(db: Db, name = 'liegestuetze'): Liegestuetze
|
|||||||
INSERT INTO zaehler (name, wert, geaendert) VALUES (?, ?, ?)
|
INSERT INTO zaehler (name, wert, geaendert) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(name) DO UPDATE SET wert = excluded.wert, geaendert = excluded.geaendert
|
ON CONFLICT(name) DO UPDATE SET wert = excluded.wert, geaendert = excluded.geaendert
|
||||||
`);
|
`);
|
||||||
|
const OFFEN = name;
|
||||||
|
const GEMACHT = `${name}-gemacht`;
|
||||||
|
|
||||||
const stand = (): number => (lesen.get(name) as { wert: number } | undefined)?.wert ?? 0;
|
const wert = (n: string): number => (lesen.get(n) as { wert: number } | undefined)?.wert ?? 0;
|
||||||
const setzen = (n: number): number => {
|
const schreibe = (n: string, w: number): number => {
|
||||||
const wert = Math.min(HOECHSTENS, Math.max(0, Math.trunc(n)));
|
const sauber = Math.min(HOECHSTENS, Math.max(0, Math.trunc(w)));
|
||||||
schreiben.run(name, wert, new Date().toISOString());
|
schreiben.run(n, sauber, new Date().toISOString());
|
||||||
return wert;
|
return sauber;
|
||||||
};
|
};
|
||||||
|
const stand = (): LiegestuetzStand => ({ offen: wert(OFFEN), gemacht: wert(GEMACHT) });
|
||||||
|
|
||||||
return { stand, setzen, aendern: (n) => setzen(stand() + n) };
|
return {
|
||||||
|
stand,
|
||||||
|
aendern(n) {
|
||||||
|
const vorher = wert(OFFEN);
|
||||||
|
const nachher = schreibe(OFFEN, vorher + n);
|
||||||
|
if (nachher < vorher) schreibe(GEMACHT, wert(GEMACHT) + (vorher - nachher));
|
||||||
|
return stand();
|
||||||
|
},
|
||||||
|
setzen(n) { schreibe(OFFEN, n); return stand(); },
|
||||||
|
gemachtSetzen(n) { schreibe(GEMACHT, n); return stand(); },
|
||||||
|
neuerStream() { schreibe(GEMACHT, 0); return stand(); },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -111,8 +111,8 @@ test('abgemeldete Zuhoerer bekommen nichts mehr', () => {
|
|||||||
|
|
||||||
test('der Schnappschuss traegt den letzten Stand der Liegestuetze', () => {
|
test('der Schnappschuss traegt den letzten Stand der Liegestuetze', () => {
|
||||||
const bus = createBus();
|
const bus = createBus();
|
||||||
assert.equal(schnappschuss(bus).liegestuetze, 0);
|
assert.deepEqual(schnappschuss(bus).liegestuetze, { offen: 0, gemacht: 0 });
|
||||||
bus.publish({ kind: 'liegestuetze', offen: 20, dazu: 10, name: 'Kevin' });
|
bus.publish({ kind: 'liegestuetze', offen: 20, gemacht: 0, dazu: 10, name: 'Kevin' });
|
||||||
bus.publish({ kind: 'liegestuetze', offen: 15 });
|
bus.publish({ kind: 'liegestuetze', offen: 15, gemacht: 5 });
|
||||||
assert.equal(schnappschuss(bus).liegestuetze, 15);
|
assert.deepEqual(schnappschuss(bus).liegestuetze, { offen: 15, gemacht: 5 });
|
||||||
});
|
});
|
||||||
@@ -66,12 +66,14 @@ export type StreamEvent = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Neuer Stand der offenen Liegestuetze. `dazu` und `name` nur bei einer
|
* Neuer Stand der Liegestuetze: offen und in diesem Stream gemacht. `dazu`
|
||||||
* Einloesung - eine Korrektur von Hand soll nicht aussehen wie eine.
|
* und `name` nur bei einer Einloesung - eine Korrektur von Hand soll nicht
|
||||||
|
* aussehen wie eine.
|
||||||
*/
|
*/
|
||||||
export type LiegestuetzeEvent = {
|
export type LiegestuetzeEvent = {
|
||||||
kind: 'liegestuetze';
|
kind: 'liegestuetze';
|
||||||
offen: number;
|
offen: number;
|
||||||
|
gemacht: number;
|
||||||
dazu?: number;
|
dazu?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
@@ -105,8 +107,8 @@ export type OverlayEvent =
|
|||||||
| { kind: 'beat'; t: number }
|
| { kind: 'beat'; t: number }
|
||||||
| {
|
| {
|
||||||
kind: 'state'; stream: StreamEvent; chat: ChatEvent[]; alerts: AlertEvent[];
|
kind: 'state'; stream: StreamEvent; chat: ChatEvent[]; alerts: AlertEvent[];
|
||||||
/** Offene Liegestuetze, damit der Zaehler nach dem Neuverbinden stimmt. */
|
/** Liegestuetze, damit der Zaehler nach dem Neuverbinden stimmt. */
|
||||||
liegestuetze: number;
|
liegestuetze: { offen: number; gemacht: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,7 +136,7 @@ export function createBus(): Bus {
|
|||||||
let stream: StreamEvent = {
|
let stream: StreamEvent = {
|
||||||
kind: 'stream', live: false, title: '', category: '', viewers: 0, startedAt: null,
|
kind: 'stream', live: false, title: '', category: '', viewers: 0, startedAt: null,
|
||||||
};
|
};
|
||||||
let liegestuetze = 0;
|
let liegestuetze = { offen: 0, gemacht: 0 };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publish(ev) {
|
publish(ev) {
|
||||||
@@ -149,7 +151,7 @@ export function createBus(): Bus {
|
|||||||
} else if (ev.kind === 'stream') {
|
} else if (ev.kind === 'stream') {
|
||||||
stream = ev;
|
stream = ev;
|
||||||
} else if (ev.kind === 'liegestuetze') {
|
} else if (ev.kind === 'liegestuetze') {
|
||||||
liegestuetze = ev.offen;
|
liegestuetze = { offen: ev.offen, gemacht: ev.gemacht };
|
||||||
} else if (ev.kind === 'clear') {
|
} else if (ev.kind === 'clear') {
|
||||||
// Geloeschtes verschwindet auch aus dem Rueckblick, sonst holt der
|
// Geloeschtes verschwindet auch aus dem Rueckblick, sonst holt der
|
||||||
// naechste Neustart der Browser-Source es zurueck ins Bild.
|
// naechste Neustart der Browser-Source es zurueck ins Bild.
|
||||||
|
|||||||
@@ -65,9 +65,11 @@ export const PAGES = [
|
|||||||
{ path: '/tts', file: 'tts.html', w: 600, h: 200,
|
{ path: '/tts', file: 'tts.html', w: 600, h: 200,
|
||||||
title: 'Vorlesen', hint: 'Liest freigegebene Nachrichten vor. Braucht Ton in OBS.' },
|
title: 'Vorlesen', hint: 'Liest freigegebene Nachrichten vor. Braucht Ton in OBS.' },
|
||||||
{ path: '/liegestuetze', file: 'liegestuetze.html', w: 420, h: 200,
|
{ path: '/liegestuetze', file: 'liegestuetze.html', w: 420, h: 200,
|
||||||
title: 'Liegestütze', hint: 'Offene Liegestütze aus Kanalpunkten. Zählt bei Einlösung hoch, korrigieren unten auf dieser Seite.' },
|
title: 'Liegestütze', hint: 'Offene und in diesem Stream gemachte Liegestütze. Zählt bei Einlösung hoch, bedienen unten auf dieser Seite.' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
type LiegestuetzStand = { offen: number; gemacht: number };
|
||||||
|
|
||||||
export type OverlayServer = {
|
export type OverlayServer = {
|
||||||
bus: Bus;
|
bus: Bus;
|
||||||
zugang: Zugang;
|
zugang: Zugang;
|
||||||
@@ -100,9 +102,12 @@ export type OverlayOptions = {
|
|||||||
};
|
};
|
||||||
/** Der Liegestuetz-Zaehler. Ohne ihn fehlt der Abschnitt auf der Seite. */
|
/** Der Liegestuetz-Zaehler. Ohne ihn fehlt der Abschnitt auf der Seite. */
|
||||||
liegestuetze?: {
|
liegestuetze?: {
|
||||||
stand: () => number;
|
stand: () => LiegestuetzStand;
|
||||||
aendern: (n: number) => number;
|
/** Negativ heisst gemacht: das Abgezogene zaehlt fuer diesen Stream. */
|
||||||
setzen: (n: number) => number;
|
aendern: (n: number) => LiegestuetzStand;
|
||||||
|
/** Korrekturen. Zaehlen nicht als gemacht. */
|
||||||
|
setzen: (n: number) => LiegestuetzStand;
|
||||||
|
gemachtSetzen: (n: number) => LiegestuetzStand;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,16 +300,21 @@ async function handle(
|
|||||||
const n = zahlAus(form.get('wert'));
|
const n = zahlAus(form.get('wert'));
|
||||||
const antwort = (text: string, code = 200): void =>
|
const antwort = (text: string, code = 200): void =>
|
||||||
send(res, code, 'application/json; charset=utf-8',
|
send(res, code, 'application/json; charset=utf-8',
|
||||||
JSON.stringify({ text, offen: opts.liegestuetze!.stand() }));
|
JSON.stringify({ text, ...opts.liegestuetze!.stand() }));
|
||||||
|
|
||||||
if (n === null) return antwort('Keine ganze Zahl.', 400);
|
if (n === null) return antwort('Keine ganze Zahl.', 400);
|
||||||
if (was === 'aendern') {
|
if (was === 'aendern') {
|
||||||
const offen = opts.liegestuetze.aendern(n);
|
const neu = opts.liegestuetze.aendern(n);
|
||||||
return antwort(`${n > 0 ? '+' : ''}${n}, jetzt ${offen} offen.`);
|
return antwort(n < 0
|
||||||
|
? `${-n} gemacht. Noch ${neu.offen} offen, ${neu.gemacht} in diesem Stream.`
|
||||||
|
: `+${n}, jetzt ${neu.offen} offen.`);
|
||||||
}
|
}
|
||||||
|
if (n < 0) return antwort('Weniger als 0 geht nicht.', 400);
|
||||||
if (was === 'setzen') {
|
if (was === 'setzen') {
|
||||||
if (n < 0) return antwort('Weniger als 0 geht nicht.', 400);
|
return antwort(`Offen auf ${opts.liegestuetze.setzen(n).offen} gesetzt.`);
|
||||||
return antwort(`Auf ${opts.liegestuetze.setzen(n)} gesetzt.`);
|
}
|
||||||
|
if (was === 'gemacht-setzen') {
|
||||||
|
return antwort(`Gemacht auf ${opts.liegestuetze.gemachtSetzen(n).gemacht} gesetzt.`);
|
||||||
}
|
}
|
||||||
return antwort('Unbekannt.', 400);
|
return antwort('Unbekannt.', 400);
|
||||||
}
|
}
|
||||||
@@ -469,7 +479,8 @@ const SEITEN_CSS = `
|
|||||||
.wartend .hint { max-width: 60ch; word-break: break-word; }
|
.wartend .hint { max-width: 60ch; word-break: break-word; }
|
||||||
.lz-stand { font: 700 3rem/1 "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
|
.lz-stand { font: 700 3rem/1 "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
|
||||||
font-variant-numeric: tabular-nums; margin: .4rem 0 1rem; }
|
font-variant-numeric: tabular-nums; margin: .4rem 0 1rem; }
|
||||||
.lz-stand small { font-size: 1rem; font-weight: 400; color: #8b84a3; margin-left: .5rem; }
|
.lz-stand small { font-size: 1rem; font-weight: 400; color: #8b84a3; margin: 0 2rem 0 .5rem; }
|
||||||
|
.lz-stand .gemacht { color: #4ade80; }
|
||||||
button.lz { margin-left: 0; min-width: 3.2rem; }
|
button.lz { margin-left: 0; min-width: 3.2rem; }
|
||||||
input.lz { width: 6.5rem; padding: .2rem .6rem; font-size: .85rem; }
|
input.lz { width: 6.5rem; padding: .2rem .6rem; font-size: .85rem; }
|
||||||
button.tts { margin-left: .4rem; }
|
button.tts { margin-left: .4rem; }
|
||||||
@@ -513,9 +524,10 @@ Kanalpunkte zurück, sofern das verbundene Konto das darf.</p>
|
|||||||
<p id="tts-meldung" class="meldung"> </p>` : '';
|
<p id="tts-meldung" class="meldung"> </p>` : '';
|
||||||
const lzFeld = opts.liegestuetze ? `
|
const lzFeld = opts.liegestuetze ? `
|
||||||
<h2>Liegestütze</h2>
|
<h2>Liegestütze</h2>
|
||||||
<p class="lead">Jede Einlösung zählt hoch. Hier herunterzählen, wenn sie gemacht
|
<p class="lead">Jede Einlösung zählt hoch. Mit −1, −5, −10 herunterzählen, wenn
|
||||||
sind, oder den Stand korrigieren. Die Zuschauer sehen jede Änderung.</p>
|
sie gemacht sind: das zählt für diesen Stream als gemacht. „Setzen“ ist nur
|
||||||
<div class="lz-stand"><span id="lz-offen">${opts.liegestuetze.stand()}</span><small>offen</small></div>
|
für Korrekturen und zählt nicht. Die Zuschauer sehen jede Änderung.</p>
|
||||||
|
<div class="lz-stand"><span id="lz-offen">${opts.liegestuetze.stand().offen}</span><small>offen</small><span id="lz-gemacht" class="gemacht">${opts.liegestuetze.stand().gemacht}</span><small>gemacht in diesem Stream</small></div>
|
||||||
<div class="gruppe">
|
<div class="gruppe">
|
||||||
<button class="lz" data-um="-10">−10</button>
|
<button class="lz" data-um="-10">−10</button>
|
||||||
<button class="lz" data-um="-5">−5</button>
|
<button class="lz" data-um="-5">−5</button>
|
||||||
@@ -526,8 +538,13 @@ sind, oder den Stand korrigieren. Die Zuschauer sehen jede Änderung.</p>
|
|||||||
</div>
|
</div>
|
||||||
<form class="gruppe" id="lz-form">
|
<form class="gruppe" id="lz-form">
|
||||||
<input class="lz" id="lz-wert" type="number" min="0" step="1" inputmode="numeric" placeholder="Anzahl">
|
<input class="lz" id="lz-wert" type="number" min="0" step="1" inputmode="numeric" placeholder="Anzahl">
|
||||||
<button type="submit" class="lz">setzen</button>
|
<button type="submit" class="lz">offen setzen</button>
|
||||||
<button type="button" class="lz" id="lz-null">auf 0</button>
|
<button type="button" class="lz" id="lz-null">offen auf 0</button>
|
||||||
|
</form>
|
||||||
|
<form class="gruppe" id="lz-gemacht-form">
|
||||||
|
<input class="lz" id="lz-gemacht-wert" type="number" min="0" step="1" inputmode="numeric" placeholder="Anzahl">
|
||||||
|
<button type="submit" class="lz">gemacht setzen</button>
|
||||||
|
<span class="hint">Beginnt bei jedem neuen Stream bei 0.</span>
|
||||||
</form>
|
</form>
|
||||||
<p id="lz-meldung" class="meldung"> </p>` : '';
|
<p id="lz-meldung" class="meldung"> </p>` : '';
|
||||||
const base = opts.publicUrl || `http://127.0.0.1:${opts.port}`;
|
const base = opts.publicUrl || `http://127.0.0.1:${opts.port}`;
|
||||||
@@ -631,19 +648,21 @@ ${lzFeld}
|
|||||||
? 'Stream läuft: ' + (ev.stream.title || 'ohne Titel')
|
? 'Stream läuft: ' + (ev.stream.title || 'ohne Titel')
|
||||||
: 'verbunden - kein Stream';
|
: 'verbunden - kein Stream';
|
||||||
setzeLive(ev.stream.live);
|
setzeLive(ev.stream.live);
|
||||||
zeigeLiegestuetze(ev.liegestuetze);
|
if (ev.liegestuetze) zeigeLiegestuetze(ev.liegestuetze);
|
||||||
} else if (ev.kind === 'stream') {
|
} else if (ev.kind === 'stream') {
|
||||||
setzeLive(ev.live);
|
setzeLive(ev.live);
|
||||||
} else if (ev.kind === 'liegestuetze') {
|
} else if (ev.kind === 'liegestuetze') {
|
||||||
zeigeLiegestuetze(ev.offen);
|
zeigeLiegestuetze(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Der Stand kommt über den Ereignisstrom, damit eine Einlösung auch hier
|
// Der Stand kommt über den Ereignisstrom, damit eine Einlösung auch hier
|
||||||
// sofort sichtbar ist und zwei offene Fenster nicht auseinanderlaufen.
|
// sofort sichtbar ist und zwei offene Fenster nicht auseinanderlaufen.
|
||||||
function zeigeLiegestuetze(n) {
|
function zeigeLiegestuetze(st) {
|
||||||
const el = document.getElementById('lz-offen');
|
for (const [id, n] of [['lz-offen', st.offen], ['lz-gemacht', st.gemacht]]) {
|
||||||
if (el && typeof n === 'number') el.textContent = n.toLocaleString('de-DE');
|
const el = document.getElementById(id);
|
||||||
|
if (el && typeof n === 'number') el.textContent = n.toLocaleString('de-DE');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const lzMeldung = document.getElementById('lz-meldung');
|
const lzMeldung = document.getElementById('lz-meldung');
|
||||||
async function lzRuf(was, wert) {
|
async function lzRuf(was, wert) {
|
||||||
@@ -653,7 +672,7 @@ ${lzFeld}
|
|||||||
body: new URLSearchParams({ was, wert: String(wert) }),
|
body: new URLSearchParams({ was, wert: String(wert) }),
|
||||||
});
|
});
|
||||||
const body = await res.json().catch(() => ({ text: 'Fehler ' + res.status }));
|
const body = await res.json().catch(() => ({ text: 'Fehler ' + res.status }));
|
||||||
zeigeLiegestuetze(body.offen);
|
zeigeLiegestuetze(body);
|
||||||
lzMeldung.textContent = body.text || '';
|
lzMeldung.textContent = body.text || '';
|
||||||
lzMeldung.className = 'meldung' + (res.ok ? ' ok' : ' fehler');
|
lzMeldung.className = 'meldung' + (res.ok ? ' ok' : ' fehler');
|
||||||
setTimeout(() => { lzMeldung.innerHTML = ' '; lzMeldung.className = 'meldung'; }, 4000);
|
setTimeout(() => { lzMeldung.innerHTML = ' '; lzMeldung.className = 'meldung'; }, 4000);
|
||||||
@@ -671,6 +690,13 @@ ${lzFeld}
|
|||||||
feld.value = '';
|
feld.value = '';
|
||||||
});
|
});
|
||||||
document.getElementById('lz-null').addEventListener('click', () => lzRuf('setzen', 0));
|
document.getElementById('lz-null').addEventListener('click', () => lzRuf('setzen', 0));
|
||||||
|
document.getElementById('lz-gemacht-form').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const feld = document.getElementById('lz-gemacht-wert');
|
||||||
|
if (feld.value === '') return;
|
||||||
|
lzRuf('gemacht-setzen', feld.value);
|
||||||
|
feld.value = '';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let sagAb;
|
let sagAb;
|
||||||
|
|||||||
@@ -5,14 +5,16 @@
|
|||||||
<title>abdulive - Liegestütze</title>
|
<title>abdulive - Liegestütze</title>
|
||||||
<link rel="stylesheet" href="/theme.css">
|
<link rel="stylesheet" href="/theme.css">
|
||||||
<style>
|
<style>
|
||||||
/* Offene Liegestütze aus Kanalpunkten.
|
/* Liegestütze aus Kanalpunkten: groß die offenen, darunter die in diesem
|
||||||
|
* Stream schon gemachten.
|
||||||
*
|
*
|
||||||
* Eine Einlösung zählt hoch und zeigt kurz, wer es war. Wenn Abdu auf der
|
* Eine Einlösung zählt hoch und zeigt kurz, wer es war. Wenn Abdu auf der
|
||||||
* Übersichtsseite herunterzählt, blinkt die Zahl grün - ohne Namen, damit
|
* Übersichtsseite herunterzählt, blinkt die Zahl grün und "gemacht" wächst -
|
||||||
* eine Korrektur nicht wie eine Einlösung aussieht.
|
* ohne Namen, damit das nicht wie eine Einlösung aussieht.
|
||||||
*
|
*
|
||||||
* In OBS: Browser-Quelle 420x200. Einstellungen in der Adresse:
|
* In OBS: Browser-Quelle 420x200. Einstellungen in der Adresse:
|
||||||
* ?verstecken=1 bei 0 ausblenden statt "erledigt" zeigen
|
* ?verstecken=1 ausblenden, solange nichts offen und nichts gemacht ist
|
||||||
|
* ?gemacht=0 die Zeile "gemacht" weglassen
|
||||||
* ?titel=... andere Beschriftung als "Liegestütze" */
|
* ?titel=... andere Beschriftung als "Liegestütze" */
|
||||||
#karte {
|
#karte {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -48,6 +50,22 @@
|
|||||||
}
|
}
|
||||||
#zahl.erledigt { font-size: 30px; color: var(--gruen); padding: 8px 0 6px; }
|
#zahl.erledigt { font-size: 30px; color: var(--gruen); padding: 8px 0 6px; }
|
||||||
|
|
||||||
|
#gemacht {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
margin-top: 7px;
|
||||||
|
font: 600 16px/1 var(--font-zahl);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--schrift-leise);
|
||||||
|
}
|
||||||
|
#gemacht b { color: var(--gruen); font-weight: 800; }
|
||||||
|
#gemacht svg { width: 15px; height: 15px; color: var(--gruen); }
|
||||||
|
#gemacht.aus { display: none; }
|
||||||
|
#gemacht.mehr b { animation: gemacht-mehr .9s ease-out; display: inline-block; }
|
||||||
|
@keyframes gemacht-mehr {
|
||||||
|
0% { transform: scale(1.35); }
|
||||||
|
100% { transform: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Einlösung: Karte glüht gold, Zahl springt. */
|
/* Einlösung: Karte glüht gold, Zahl springt. */
|
||||||
#karte.mehr { border-color: var(--gold); box-shadow: var(--schatten), 0 0 28px rgba(255, 197, 61, .55); }
|
#karte.mehr { border-color: var(--gold); box-shadow: var(--schatten), 0 0 28px rgba(255, 197, 61, .55); }
|
||||||
#zahl.springen { animation: springen .6s cubic-bezier(.3, 1.6, .5, 1); }
|
#zahl.springen { animation: springen .6s cubic-bezier(.3, 1.6, .5, 1); }
|
||||||
@@ -67,7 +85,7 @@
|
|||||||
/* "+5 von Name", steigt über der Karte auf. */
|
/* "+5 von Name", steigt über der Karte auf. */
|
||||||
#plus {
|
#plus {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px; bottom: 112px;
|
left: 12px; bottom: 140px;
|
||||||
max-width: 396px;
|
max-width: 396px;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
font: 700 20px/1.1 var(--font);
|
font: 700 20px/1.1 var(--font);
|
||||||
@@ -101,6 +119,13 @@
|
|||||||
<div class="text">
|
<div class="text">
|
||||||
<div id="titel" class="kontur">Liegestütze</div>
|
<div id="titel" class="kontur">Liegestütze</div>
|
||||||
<span id="zahl" class="kontur">0</span>
|
<span id="zahl" class="kontur">0</span>
|
||||||
|
<div id="gemacht" class="kontur">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M5 12.5l4.5 4.5L19 7.5"/>
|
||||||
|
</svg>
|
||||||
|
<span><b>0</b> gemacht</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,10 +137,14 @@
|
|||||||
var karte = document.getElementById('karte');
|
var karte = document.getElementById('karte');
|
||||||
var zahl = document.getElementById('zahl');
|
var zahl = document.getElementById('zahl');
|
||||||
var plus = document.getElementById('plus');
|
var plus = document.getElementById('plus');
|
||||||
|
var gemacht = document.getElementById('gemacht');
|
||||||
|
var gemachtZahl = gemacht.querySelector('b');
|
||||||
|
if (!Overlay.param('gemacht', true)) gemacht.classList.add('aus');
|
||||||
var verstecken = Overlay.param('verstecken', false);
|
var verstecken = Overlay.param('verstecken', false);
|
||||||
document.getElementById('titel').textContent = Overlay.param('titel', 'Liegestütze');
|
document.getElementById('titel').textContent = Overlay.param('titel', 'Liegestütze');
|
||||||
|
|
||||||
var angezeigt = null; // was gerade im Bild steht
|
var angezeigt = null; // was gerade im Bild steht
|
||||||
|
var gemachtBisher = null;
|
||||||
var lauf = null; // laufendes Hochzählen
|
var lauf = null; // laufendes Hochzählen
|
||||||
var glimmen = null;
|
var glimmen = null;
|
||||||
|
|
||||||
@@ -127,7 +156,18 @@
|
|||||||
zahl.textContent = Overlay.zahl(n);
|
zahl.textContent = Overlay.zahl(n);
|
||||||
zahl.classList.remove('erledigt');
|
zahl.classList.remove('erledigt');
|
||||||
}
|
}
|
||||||
karte.classList.toggle('weg', verstecken && n === 0);
|
}
|
||||||
|
|
||||||
|
function sichtbarkeit() {
|
||||||
|
karte.classList.toggle('weg', verstecken && angezeigt === 0 && !gemachtBisher);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeigeGemacht(g) {
|
||||||
|
if (typeof g !== 'number') return;
|
||||||
|
var alt = gemachtBisher;
|
||||||
|
gemachtBisher = g;
|
||||||
|
gemachtZahl.textContent = Overlay.zahl(g);
|
||||||
|
if (alt !== null && g > alt) nochmal(gemacht, 'mehr');
|
||||||
}
|
}
|
||||||
|
|
||||||
function nochmal(el, klasse) {
|
function nochmal(el, klasse) {
|
||||||
@@ -176,9 +216,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Overlay.on('state', function (ev) { stand(ev.liegestuetze); });
|
Overlay.on('state', function (ev) {
|
||||||
|
if (!ev.liegestuetze) return;
|
||||||
|
stand(ev.liegestuetze.offen);
|
||||||
|
zeigeGemacht(ev.liegestuetze.gemacht);
|
||||||
|
sichtbarkeit();
|
||||||
|
});
|
||||||
Overlay.on('liegestuetze', function (ev) {
|
Overlay.on('liegestuetze', function (ev) {
|
||||||
stand(ev.offen, ev.dazu ? { dazu: ev.dazu, name: ev.name } : null);
|
stand(ev.offen, ev.dazu ? { dazu: ev.dazu, name: ev.name } : null);
|
||||||
|
zeigeGemacht(ev.gemacht);
|
||||||
|
sichtbarkeit();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in new issue
Block a user