Streamplan-Seite: interaktives Live-Radar statt schlichter Textkarten
Nutzer-Wunsch 20.08.2026: "diese seite vom streamplan soll viel geiler und spezieller sein, ueberrasch mich." Neues Herzstueck: ein 24-Stunden-Ziffernblatt (reines SVG, keine Bild-Assets), das den festen 20-Uhr-Termin als dauerhaft leuchtenden Fixpunkt zeigt (goldener Puls) und einen "Jetzt"-Zeiger in Echtzeit mit der tatsaechlichen Uhrzeit der besuchenden Person mitbewegt (Babyblau, DogFather-Markenfarbe). Nutzt denselben /live-status-Endpunkt wie die Startseite: - Offline: echter Sekunden-Countdown zum naechsten lokalen 20-Uhr-Termin. - Live: komplettes Radar schaltet auf Rot/Puls um, zeigt den Stream-Titel und einen "Jetzt anschauen"-Knopf direkt zu TikTok. Sanft rotierender Lichtschein fuer Atmosphaere, komplett augenschonend (gedeckte Farben, respektiert prefers-reduced-motion vollstaendig). Bugfix unterwegs gefunden (Playwright-Screenshot, dritte Wiederholung desselben Musters heute): der "Jetzt anschauen"-Knopf blieb trotz [hidden]-Attribut sichtbar, weil .btn selbst "display" setzt und damit gleiche Spezifitaet wie die eingebaute [hidden]-Regel hat -- explizite Regel ergaenzt. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -3571,3 +3571,99 @@ body.start-seite .music-toggle:focus-visible {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.todo-note strong { color: var(--accent-2); }
|
||||
|
||||
/* ===================================================================
|
||||
Streamplan — "Live-Radar" (20.08.2026, Nutzer-Wunsch: "soll viel
|
||||
geiler und spezieller sein, überrasch mich"). Ein 24-Stunden-Ziffernblatt
|
||||
als SVG: der feste 20-Uhr-Termin leuchtet dauerhaft als Fixpunkt, ein
|
||||
"Jetzt"-Zeiger bewegt sich in Echtzeit mit der tatsächlichen Uhrzeit,
|
||||
ein sanft rotierender Lichtschein sorgt für Atmosphäre. Schaltet live
|
||||
auf Rot/Puls um, sobald DogFather laut /live-status wirklich sendet —
|
||||
sonst zählt ein echter Sekunden-Countdown zum nächsten 20-Uhr-Termin
|
||||
runter. Bewusst augenschonend: gedeckte Farben, langsame, sanfte
|
||||
Animation, kompletter Stillstand bei prefers-reduced-motion. ================================================================= */
|
||||
.stream-radar-wrap { display: flex; flex-direction: column; align-items: center; margin: 1.4rem auto 0; }
|
||||
.stream-radar {
|
||||
position: relative;
|
||||
width: min(300px, 70vw);
|
||||
aspect-ratio: 1;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.stream-radar-sweep {
|
||||
position: absolute;
|
||||
inset: -6%;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(from 0deg,
|
||||
transparent 0deg, rgba(143, 217, 234, .16) 40deg, transparent 90deg,
|
||||
transparent 260deg, rgba(139, 92, 246, .14) 300deg, transparent 340deg, transparent 360deg);
|
||||
animation: stream-radar-rotate 16s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes stream-radar-rotate { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .stream-radar-sweep { animation: none; } }
|
||||
.stream-radar-dial { position: relative; width: 100%; height: 100%; display: block; }
|
||||
.radar-ring-outer { fill: none; stroke: rgba(255, 255, 255, .12); stroke-width: 1.5; }
|
||||
.radar-ring-inner { fill: rgba(255, 255, 255, .02); stroke: rgba(255, 255, 255, .07); stroke-width: 1; }
|
||||
.radar-tick { stroke: rgba(255, 255, 255, .22); stroke-width: 1.5; }
|
||||
.radar-tick-major { stroke: rgba(255, 255, 255, .5); stroke-width: 2.2; }
|
||||
.radar-tick-label { fill: var(--text-muted); font-size: 11px; font-family: var(--font-body, inherit); }
|
||||
.radar-fixmarker-dot {
|
||||
fill: #e8c465;
|
||||
filter: drop-shadow(0 0 6px rgba(232, 196, 101, .9)) drop-shadow(0 0 14px rgba(232, 196, 101, .5));
|
||||
animation: stream-radar-fixpuls 3.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes stream-radar-fixpuls { 0%, 100% { opacity: .75; } 50% { opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .radar-fixmarker-dot { animation: none; opacity: .9; } }
|
||||
.radar-now-hand {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2.5;
|
||||
stroke-linecap: round;
|
||||
filter: drop-shadow(0 0 4px var(--accent));
|
||||
transition: x2 .6s ease, y2 .6s ease;
|
||||
}
|
||||
.radar-now-dot {
|
||||
fill: var(--accent);
|
||||
filter: drop-shadow(0 0 6px var(--accent));
|
||||
transition: cx .6s ease, cy .6s ease;
|
||||
}
|
||||
.stream-radar.ist-live .radar-now-hand,
|
||||
.stream-radar.ist-live .radar-now-dot { stroke: #ff5c72; fill: #ff5c72; filter: drop-shadow(0 0 8px rgba(255, 92, 114, .8)); }
|
||||
.radar-center-dot { fill: var(--text); opacity: .8; }
|
||||
.stream-radar-center {
|
||||
position: absolute;
|
||||
inset: 24%;
|
||||
border-radius: 50%;
|
||||
background: rgba(8, 10, 14, .82);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: .5rem;
|
||||
gap: .25rem;
|
||||
}
|
||||
.stream-radar-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
margin-bottom: .2rem;
|
||||
}
|
||||
.stream-radar.ist-live .stream-radar-dot { background: #ff5c72; box-shadow: 0 0 10px rgba(255, 92, 114, .8); animation: stream-radar-live-puls 1.4s ease-in-out infinite; }
|
||||
@keyframes stream-radar-live-puls { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.35); opacity: .75; } }
|
||||
@media (prefers-reduced-motion: reduce) { .stream-radar.ist-live .stream-radar-dot { animation: none; } }
|
||||
.stream-radar-status { font-size: 1rem; font-weight: 800; color: var(--text); line-height: 1.25; }
|
||||
.stream-radar-countdown {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 1.55rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: .02em;
|
||||
color: var(--accent);
|
||||
}
|
||||
.stream-radar.ist-live .stream-radar-countdown { color: #ff5c72; }
|
||||
.stream-radar-sub { font-size: .74rem; color: var(--text-muted); }
|
||||
/* .btn setzt selbst "display", das schlägt bei gleicher Spezifität die
|
||||
eingebaute [hidden]-Regel des Browsers (identisches Muster wie bei
|
||||
.jahres-event-card weiter oben) — deshalb hier explizit. */
|
||||
.stream-radar-watch-btn[hidden] { display: none; }
|
||||
.stream-radar-watch-btn { margin-top: 1.1rem; }
|
||||
.stream-radar-hinweis { margin-top: 1rem; opacity: .8; }
|
||||
|
||||
@@ -20,6 +20,36 @@ window.I18N_PAGE = {
|
||||
de: "Zur Dogi TikTok-Seite", "de-CH": "Zur Dogi TikTok-Site",
|
||||
en: "Go to Dogi's TikTok page", fr: "Voir la page TikTok de Dogi", pt: "Ir para a página TikTok do Dogi",
|
||||
},
|
||||
|
||||
/* Live-Radar (20.08.2026) */
|
||||
sp_radar_loading: {
|
||||
de: "Lädt …", "de-CH": "Lädt …",
|
||||
en: "Loading …", fr: "Chargement …", pt: "Carregando …",
|
||||
},
|
||||
sp_radar_offline: {
|
||||
de: "Nächster Fix-Stream in", "de-CH": "Nächschte Fix-Stream in",
|
||||
en: "Next fixed stream in", fr: "Prochain live fixe dans", pt: "Próxima live fixa em",
|
||||
},
|
||||
sp_radar_countdown_label: {
|
||||
de: "täglich ab 20 Uhr", "de-CH": "täglich ab 20 Uhr",
|
||||
en: "daily from 8 PM", fr: "chaque jour à 20h", pt: "diariamente às 20h",
|
||||
},
|
||||
sp_radar_live: {
|
||||
de: "🔴 DogFather ist LIVE", "de-CH": "🔴 DogFather isch LIVE",
|
||||
en: "🔴 DogFather is LIVE", fr: "🔴 DogFather est EN DIRECT", pt: "🔴 DogFather está AO VIVO",
|
||||
},
|
||||
sp_radar_live_generic: {
|
||||
de: "Jetzt live auf TikTok", "de-CH": "Jetzt live uf TikTok",
|
||||
en: "Live now on TikTok", fr: "En direct maintenant sur TikTok", pt: "Ao vivo agora no TikTok",
|
||||
},
|
||||
sp_radar_watch_btn: {
|
||||
de: "🔴 Jetzt anschauen", "de-CH": "🔴 Jetzt aluege",
|
||||
en: "🔴 Watch now", fr: "🔴 Regarder maintenant", pt: "🔴 Assistir agora",
|
||||
},
|
||||
sp_radar_hinweis: {
|
||||
de: "Alle Zeiten in deiner Ortszeit.", "de-CH": "Alli Ziite in dinere Ortsziit.",
|
||||
en: "All times shown in your local time.", fr: "Tous les horaires affichés dans ton fuseau local.", pt: "Todos os horários na sua hora local.",
|
||||
},
|
||||
sp_card1_tag: {
|
||||
de: "Fixer Termin", "de-CH": "Fixe Termin",
|
||||
en: "Fixed Time", fr: "Rendez-vous fixe", pt: "Horário Fixo",
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/* =====================================================================
|
||||
streamplan.js — "Live-Radar" NUR für streamplan.html (20.08.2026,
|
||||
Nutzer-Wunsch: "soll viel geiler und spezieller sein, überrasch mich").
|
||||
|
||||
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. ===================================================================== */
|
||||
(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";
|
||||
|
||||
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) };
|
||||
}
|
||||
|
||||
// ---------- 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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
})();
|
||||
Reference in New Issue
Block a user