Diese Änderungen liefen bereits live auf dem Server, lagen dort aber ausschließlich als nicht eingecheckte Arbeitskopie — bei jedem Deploy (stash/pull/pop) und bei jedem Serverproblem wären sie verloren gewesen. Deshalb hier unverändert in git übernommen, bevor darauf aufgebaut wird. Enthalten (nicht von mir gebaut, nur gesichert): - Supporter: eigener fester Zugangscode statt Einmalcode-Login (Migration 0009, lib/crypto.js scrypt-Hash, routes/supporter.js, abonnieren.html, supporter.html, i18n-abonnieren/-supporter) - Stimmen: Profilbilder (Migration 0010, routes/testimonials.js) - Event-Bild-Upload: voller Pfad statt relativem (routes/events.js) - Neue/überarbeitete Hintergrundbilder für viele Seiten - Teilen-Funktion (streamplan.js, i18n-index/-streamplan, main.css) Co-Authored-By: Claude Opus 5 <[email protected]>
252 lines
11 KiB
JavaScript
252 lines
11 KiB
JavaScript
/* =====================================================================
|
|
streamplan.js — "Live-Radar" NUR für streamplan.html (20.08.2026,
|
|
Nutzer-Wunsch: "soll viel geiler und spezieller sein, überrasch mich",
|
|
ergänzt um eine zweite Uhr für ein "spezielles Live-Event": "sehr
|
|
wichtig ist dass ich auch spezielle live events da auch eintragen kann
|
|
in der verwaltungsseite so dass da eine zweite uhr erscheint wo die
|
|
zeit dann für dieses spezielle event läuft").
|
|
|
|
Zwei 24-Stunden-Ziffernblätter (reines SVG, keine Bild-Assets nötig):
|
|
- Uhr 1 (immer sichtbar): der feste 20-Uhr-Termin leuchtet dauerhaft als
|
|
Fixpunkt, ein "Jetzt"-Zeiger bewegt sich mit der echten Uhrzeit der
|
|
besuchenden Person mit. Live laut /live-status -> Rot/Puls +
|
|
"Jetzt anschauen".
|
|
- Uhr 2 (nur wenn in der Verwaltung aktiviert): zählt zu einem exakten
|
|
Zieldatum+Uhrzeit runter (Tage möglich, nicht nur Stunden), Magenta/
|
|
Violett statt Cyan/Gold, damit beide Uhren auf einen Blick
|
|
unterscheidbar sind ("bissl Farbe"). ===================================================================== */
|
|
(function () {
|
|
const API_BASE_URL = "https://postfach.dogfather-universe.com";
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
const CX = 150, CY = 150, R_OUT = 130, R_TICK_IN = 116, R_TICK_MAJOR_IN = 106, R_HAND = 100;
|
|
|
|
function polar(r, stundenAnteil) {
|
|
const winkel = (stundenAnteil / 24) * 360 - 90; // 0 Uhr oben, im Uhrzeigersinn
|
|
const rad = (winkel * Math.PI) / 180;
|
|
return { x: CX + r * Math.cos(rad), y: CY + r * Math.sin(rad) };
|
|
}
|
|
|
|
/** Zeichnet die 24 Stundenstriche + einen Fixpunkt-Leuchtpunkt bei
|
|
"fixStunde" (kann eine Dezimalzahl sein, z.B. 19.5 = 19:30) in die
|
|
übergebenen <g>-Container. Läuft einmalig pro Uhr. */
|
|
function zeichneZiffernblatt(ticksHost, fixHost, fixStunde) {
|
|
ticksHost.innerHTML = "";
|
|
for (let h = 0; h < 24; h++) {
|
|
const major = h % 6 === 0;
|
|
const p1 = polar(R_OUT, h);
|
|
const p2 = polar(major ? R_TICK_MAJOR_IN : R_TICK_IN, h);
|
|
const line = document.createElementNS(SVG_NS, "line");
|
|
line.setAttribute("x1", p1.x); line.setAttribute("y1", p1.y);
|
|
line.setAttribute("x2", p2.x); line.setAttribute("y2", p2.y);
|
|
line.setAttribute("class", major ? "radar-tick radar-tick-major" : "radar-tick");
|
|
ticksHost.appendChild(line);
|
|
}
|
|
fixHost.innerHTML = "";
|
|
if (fixStunde != null) {
|
|
const fixPos = polar(R_OUT + 11, fixStunde);
|
|
const fixDot = document.createElementNS(SVG_NS, "circle");
|
|
fixDot.setAttribute("cx", fixPos.x); fixDot.setAttribute("cy", fixPos.y);
|
|
fixDot.setAttribute("r", 6.5);
|
|
fixDot.setAttribute("class", "radar-fixmarker-dot");
|
|
fixHost.appendChild(fixDot);
|
|
}
|
|
}
|
|
|
|
function zeigerAktualisieren(handEl, dotEl, stundenAnteil) {
|
|
const spitze = polar(R_HAND, stundenAnteil);
|
|
handEl.setAttribute("x2", spitze.x); handEl.setAttribute("y2", spitze.y);
|
|
dotEl.setAttribute("cx", spitze.x); dotEl.setAttribute("cy", spitze.y);
|
|
}
|
|
|
|
function zweistellig(n) { return String(n).padStart(2, "0"); }
|
|
|
|
/* ===================================================================
|
|
Uhr 1: täglicher 20-Uhr-Fixtermin (bereits bestehend, unverändert im
|
|
Verhalten) ================================================================= */
|
|
const radar = document.getElementById("stream-radar");
|
|
if (radar) {
|
|
const FIX_STUNDE = 20;
|
|
zeichneZiffernblatt(document.getElementById("radar-ticks"), document.getElementById("radar-fixmarker"), FIX_STUNDE);
|
|
|
|
const nowHand = document.getElementById("radar-now-hand");
|
|
const nowDot = document.getElementById("radar-now-dot");
|
|
function zeigerTick() {
|
|
const jetzt = new Date();
|
|
zeigerAktualisieren(nowHand, nowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
|
|
}
|
|
zeigerTick();
|
|
setInterval(zeigerTick, 15000);
|
|
|
|
function naechsterFixTermin() {
|
|
const jetzt = new Date();
|
|
const ziel = new Date(jetzt);
|
|
ziel.setHours(FIX_STUNDE, 0, 0, 0);
|
|
if (ziel <= jetzt) ziel.setDate(ziel.getDate() + 1);
|
|
return ziel;
|
|
}
|
|
|
|
const countdownEl = document.getElementById("radar-countdown");
|
|
const statusText = document.getElementById("radar-status-text");
|
|
const statusSub = document.getElementById("radar-status-sub");
|
|
const watchBtn = document.getElementById("radar-watch-btn");
|
|
|
|
let istLive = false;
|
|
let letzterTitel = null;
|
|
|
|
function countdownTick() {
|
|
if (istLive) return;
|
|
const diffMs = naechsterFixTermin() - new Date();
|
|
const gesamtSek = Math.max(0, Math.floor(diffMs / 1000));
|
|
const std = Math.floor(gesamtSek / 3600);
|
|
const min = Math.floor((gesamtSek % 3600) / 60);
|
|
const sek = gesamtSek % 60;
|
|
countdownEl.textContent = `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
|
|
}
|
|
setInterval(countdownTick, 1000);
|
|
|
|
function texteNeuSetzen() {
|
|
const t = window.dogiUebersetzen || ((k) => k);
|
|
if (istLive) {
|
|
statusText.textContent = t("sp_radar_live");
|
|
countdownEl.textContent = letzterTitel || t("sp_radar_live_generic");
|
|
statusSub.textContent = "";
|
|
watchBtn.hidden = false;
|
|
} else {
|
|
statusText.textContent = t("sp_radar_offline");
|
|
statusSub.textContent = t("sp_radar_countdown_label");
|
|
watchBtn.hidden = true;
|
|
countdownTick();
|
|
}
|
|
}
|
|
|
|
function liveStatusAnwenden(data) {
|
|
istLive = !!(data && data.live);
|
|
letzterTitel = data && data.title ? data.title : null;
|
|
radar.classList.toggle("ist-live", istLive);
|
|
texteNeuSetzen();
|
|
}
|
|
|
|
function liveStatusLaden() {
|
|
return fetch(`${API_BASE_URL}/live-status`, { cache: "no-store" })
|
|
.then((res) => res.json())
|
|
.then(liveStatusAnwenden)
|
|
.catch(() => {
|
|
istLive = false;
|
|
radar.classList.remove("ist-live");
|
|
texteNeuSetzen();
|
|
});
|
|
}
|
|
|
|
liveStatusLaden();
|
|
setInterval(liveStatusLaden, 30000);
|
|
document.addEventListener("visibilitychange", () => { if (!document.hidden) liveStatusLaden(); });
|
|
document.addEventListener("dogi-sprache-geaendert", texteNeuSetzen);
|
|
}
|
|
|
|
/* ===================================================================
|
|
Uhr 2: "spezielles Live-Event" aus der Verwaltung (20.08.2026, neu).
|
|
Nur sichtbar, wenn /special-event ein aktives, noch bevorstehendes
|
|
Event liefert. Zieluhrzeit kann Tage entfernt sein -> Countdown zeigt
|
|
bei Bedarf "Xd HH:MM:SS" statt nur HH:MM:SS. ================================================================= */
|
|
const seWrap = document.getElementById("se-radar-wrap");
|
|
if (seWrap) {
|
|
const seRadar = document.getElementById("se-radar");
|
|
const seNowHand = document.getElementById("se-radar-now-hand");
|
|
const seNowDot = document.getElementById("se-radar-now-dot");
|
|
const seCountdown = document.getElementById("se-radar-countdown");
|
|
const seTitel = document.getElementById("se-radar-titel");
|
|
const seLink = document.getElementById("se-radar-link");
|
|
const seTeilenBtn = document.getElementById("se-radar-teilen");
|
|
let zielDatum = null;
|
|
let aktuellesEvent = null; // für dogiTeilen() beim Klick (21.08.2026)
|
|
let countdownIntervalId = null;
|
|
let zeigerIntervalId = null;
|
|
|
|
function seCountdownTick() {
|
|
if (!zielDatum) return;
|
|
const diffMs = zielDatum - new Date();
|
|
if (diffMs <= 0) {
|
|
seRadar.classList.add("ist-live");
|
|
seCountdown.textContent = window.dogiUebersetzen ? window.dogiUebersetzen("sp_radar_live_generic") : "Live!";
|
|
clearInterval(countdownIntervalId);
|
|
return;
|
|
}
|
|
const gesamtSek = Math.floor(diffMs / 1000);
|
|
const tage = Math.floor(gesamtSek / 86400);
|
|
const std = Math.floor((gesamtSek % 86400) / 3600);
|
|
const min = Math.floor((gesamtSek % 3600) / 60);
|
|
const sek = gesamtSek % 60;
|
|
seCountdown.textContent = tage > 0
|
|
? `${tage}T ${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`
|
|
: `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
|
|
}
|
|
|
|
// Der "Jetzt"-Zeiger zeigt wie bei Uhr 1 die ECHTE aktuelle Uhrzeit --
|
|
// der magentafarbene Fixpunkt markiert dagegen die Tageszeit, zu der
|
|
// das spezielle Event stattfindet. So bleibt die Bildsprache zwischen
|
|
// beiden Uhren konsistent (nur der Fixpunkt unterscheidet sich).
|
|
function seZeigerTick() {
|
|
const jetzt = new Date();
|
|
zeigerAktualisieren(seNowHand, seNowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
|
|
}
|
|
|
|
function specialEventLaden() {
|
|
return fetch(`${API_BASE_URL}/special-event`, { cache: "no-store" })
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
const event = data && data.ok ? data.event : null;
|
|
if (countdownIntervalId) clearInterval(countdownIntervalId);
|
|
if (zeigerIntervalId) clearInterval(zeigerIntervalId);
|
|
if (!event) {
|
|
seWrap.hidden = true;
|
|
zielDatum = null;
|
|
return;
|
|
}
|
|
zielDatum = new Date(event.zieldatum);
|
|
aktuellesEvent = event;
|
|
seWrap.hidden = false;
|
|
seRadar.classList.remove("ist-live");
|
|
seTitel.textContent = window.dogiFeld ? window.dogiFeld(event.titel) : event.titel?.de || "";
|
|
zeichneZiffernblatt(document.getElementById("se-radar-ticks"), document.getElementById("se-radar-fixmarker"), zielDatum.getHours() + zielDatum.getMinutes() / 60);
|
|
seZeigerTick();
|
|
seCountdownTick();
|
|
zeigerIntervalId = setInterval(seZeigerTick, 15000);
|
|
countdownIntervalId = setInterval(seCountdownTick, 1000);
|
|
if (event.link) {
|
|
seLink.href = event.link;
|
|
seLink.hidden = false;
|
|
} else {
|
|
seLink.hidden = true;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
seWrap.hidden = true;
|
|
});
|
|
}
|
|
|
|
specialEventLaden();
|
|
setInterval(specialEventLaden, 60000); // reicht -- Aenderungen in der Verwaltung muessen nicht sekundengenau ankommen
|
|
document.addEventListener("dogi-sprache-geaendert", () => {
|
|
if (zielDatum) specialEventLaden();
|
|
});
|
|
|
|
// Teilen (21.08.2026, Nutzer-Wunsch: "... speziellen live events soll man
|
|
// auch teilen können") -- teilt die Streamplan-Seite selbst (dort steht
|
|
// ja bereits automatisch das aktuell aktive Event), zusammen mit Titel
|
|
// im Text, damit klar ist, worum es geht. Nur EIN spezielles Event kann
|
|
// je aktiv sein, deshalb reicht ein einziger Knopf ohne Slot-Bezug.
|
|
seTeilenBtn?.addEventListener("click", () => {
|
|
if (!aktuellesEvent) return;
|
|
const titel = window.dogiFeld ? window.dogiFeld(aktuellesEvent.titel) : aktuellesEvent.titel?.de || "";
|
|
window.dogiTeilen?.(
|
|
{
|
|
title: titel,
|
|
text: `🔴 ${titel} — spezielles Live-Event bei DogFather!`,
|
|
url: `${location.origin}/streamplan.html`,
|
|
},
|
|
seTeilenBtn
|
|
);
|
|
});
|
|
}
|
|
})();
|