Zweite Live-Uhr fuer "spezielles Live-Event" + mehr Farbe im Radar
Nutzer-Wunsch 20.08.2026: "wuensch mir nur noch bissl farbe und 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 fuer dieses spezielle event laeuft." - Neue Verwaltungs-Sektion "Spezielles Live-Event": Titel (Deutsch, wird automatisch uebersetzt), echtes Datum+Uhrzeit, optionaler Link, Aktiv- Schalter -- nur mit Titel + Zukunftsdatum aktivierbar. - Zweite Uhr auf streamplan.html (Magenta/Violett statt Cyan/Gold, direkt neben der bestehenden), nur sichtbar wenn ein Event aktiviert ist und das Zieldatum noch nicht vorbei ist. Echter Countdown inkl. Tage (z.B. "2T 03:14:59"), "Jetzt"-Zeiger zeigt wie bei Uhr 1 die tatsaechliche Uhrzeit, der magentafarbene Fixpunkt markiert die Tageszeit des Events. Zieldatum wird als UTC gespeichert -- jede besuchende Person sieht den exakt richtigen Countdown in ihrer eigenen Zeitzone. - "Bissl Farbe": bunter Farbverlauf (Cyan/Violett/Gold) auf dem aeusseren Ring statt reinem Grauton, kraeftigerer Sweep-Farbschein. - Backend: neue routes/special-event.js (getSpecialEventPublic nur bei aktiv+zukuenftig, getSpecialEventAdmin fuer Entwuerfe, saveSpecialEvent mit Validierung), 13 automatisierte Tests gegen eine Fake-DB bestanden. - Bugfix unterwegs gefunden (Playwright-Screenshot, wiederholtes Muster auf dieser Seite): .stream-radar-wrap blieb trotz [hidden]-Attribut sichtbar (display:block ueberschreibt die eingebaute [hidden]-Regel bei gleicher Spezifitaet) -- explizite Regel ergaenzt, per Playwright erneut verifiziert (display:none bestaetigt). Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen, sonst bleibt die zweite Uhr unsichtbar. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+209
-110
@@ -1,25 +1,24 @@
|
||||
/* =====================================================================
|
||||
streamplan.js — "Live-Radar" NUR für streamplan.html (20.08.2026,
|
||||
Nutzer-Wunsch: "soll viel geiler und spezieller sein, überrasch mich").
|
||||
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").
|
||||
|
||||
Ein 24-Stunden-Ziffernblatt (reines SVG, keine Bild-Assets nötig): der
|
||||
feste 20-Uhr-Termin leuchtet dauerhaft als Fixpunkt, ein "Jetzt"-Zeiger
|
||||
bewegt sich mit der echten Uhrzeit der besuchenden Person mit. Nutzt
|
||||
denselben /live-status-Endpunkt wie die Startseite (main.js lädt
|
||||
window.dogiUebersetzen/dogiSprache VOR dieser Datei, siehe <script>-
|
||||
Reihenfolge in streamplan.html) — läuft DogFather laut TikTok gerade
|
||||
wirklich, schaltet das ganze Radar auf Rot/Puls um und zeigt einen
|
||||
"Jetzt anschauen"-Knopf, sonst zählt ein echter Sekunden-Countdown zum
|
||||
nächsten lokalen 20-Uhr-Termin runter. ===================================================================== */
|
||||
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 FIX_STUNDE = 20; // täglicher Fix-Termin, in der Ortszeit der besuchenden Person
|
||||
|
||||
const radar = document.getElementById("stream-radar");
|
||||
if (!radar) return; // Datei ist nur auf streamplan.html eingebunden
|
||||
|
||||
const CX = 150, CY = 150, R_OUT = 130, R_TICK_IN = 116, R_TICK_MAJOR_IN = 106, R_HAND = 100;
|
||||
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
|
||||
@@ -27,105 +26,205 @@
|
||||
return { x: CX + r * Math.cos(rad), y: CY + r * Math.sin(rad) };
|
||||
}
|
||||
|
||||
// ---------- 24 Stundenstriche (alle 6 Std. etwas länger/heller) ----------
|
||||
const ticksHost = document.getElementById("radar-ticks");
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------- Fixmarkierung beim täglichen 20-Uhr-Termin ----------
|
||||
const fixHost = document.getElementById("radar-fixmarker");
|
||||
const fixPos = polar(R_OUT + 11, FIX_STUNDE);
|
||||
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);
|
||||
|
||||
// ---------- "Jetzt"-Zeiger: bewegt sich mit der echten Uhrzeit mit ----------
|
||||
const nowHand = document.getElementById("radar-now-hand");
|
||||
const nowDot = document.getElementById("radar-now-dot");
|
||||
function zeigerAktualisieren() {
|
||||
const jetzt = new Date();
|
||||
const stundenAnteil = jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600;
|
||||
const spitze = polar(R_HAND, stundenAnteil);
|
||||
nowHand.setAttribute("x2", spitze.x); nowHand.setAttribute("y2", spitze.y);
|
||||
nowDot.setAttribute("cx", spitze.x); nowDot.setAttribute("cy", spitze.y);
|
||||
}
|
||||
zeigerAktualisieren();
|
||||
setInterval(zeigerAktualisieren, 15000); // alle 15s reicht für eine sichtbare, ruhige Bewegung
|
||||
|
||||
// ---------- Countdown zum nächsten lokalen 20-Uhr-Termin ----------
|
||||
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;
|
||||
}
|
||||
function zweistellig(n) { return String(n).padStart(2, "0"); }
|
||||
|
||||
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; // im Live-Zustand zeigt der Countdown-Platz die Uhrzeit/den Titel, siehe unten
|
||||
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();
|
||||
/** 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 liveStatusAnwenden(data) {
|
||||
istLive = !!(data && data.live);
|
||||
letzterTitel = data && data.title ? data.title : null;
|
||||
radar.classList.toggle("ist-live", istLive);
|
||||
texteNeuSetzen();
|
||||
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 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();
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
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");
|
||||
let zielDatum = null;
|
||||
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);
|
||||
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();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user