diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index fefbf26..ca9fd88 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -95,6 +95,21 @@ const WUNSCH_STATUS = [ // Alles mit Editor, Vorschau oder Liste bleibt einspaltig. const SENDER_LEER = { id: null, name: '', url: '', emoji: '', sort: 0 }; +/** + * 3:07 statt 187. Ohne bekannte Dauer steht „live" da und nicht „0:00" — + * ein Livestream hat keine Laenge, und eine Null waere eine Behauptung. + */ +function dauerText(sekunden) { + if (sekunden === null || sekunden === undefined) return 'live'; + const t = Math.max(0, Math.round(sekunden)); + const h = Math.floor(t / 3600); + const m = Math.floor((t % 3600) / 60); + const r = t % 60; + return h > 0 + ? `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}` + : `${m}:${String(r).padStart(2, '0')}`; +} + const DENSE_TABS = new Set(['feeds', 'community', 'support', 'system']); // Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner) @@ -432,6 +447,14 @@ export default function Settings({ me }) { const [senderDraft, setSenderDraft] = useState({ id: null, name: '', url: '', emoji: '', sort: 0 }); // Ergebnis der Sender-Probe: { erreichbar, sender, titel } oder 'laeuft' const [senderProbe, setSenderProbe] = useState(null); + // Fernbedienung: der Live-Stand vom Bot (null = noch nichts geholt) + const [radio, setRadio] = useState(null); + const [radioKanal, setRadioKanal] = useState(''); + const [radioEingabe, setRadioEingabe] = useState(''); + const [radioTreffer, setRadioTreffer] = useState(null); // Auswahl nach einer Suche + const [radioLaedt, setRadioLaedt] = useState(false); + const [radioFehler, setRadioFehler] = useState(''); + const [ytCookies, setYtCookies] = useState(''); const [owner, setOwner] = useState(null); const [auditAlle, setAuditAlle] = useState(false); // Module (An/Aus für alle Funktionen) @@ -472,6 +495,23 @@ export default function Settings({ me }) { apiGet('/api/radio').then((d) => setSender(d.sender ?? [])).catch(() => {}); }, []); + // Der Live-Stand der Fernbedienung. + // + // Nur solange der Community-Reiter offen ist: sonst liefe im Hintergrund + // alle fuenf Sekunden eine Anfrage gegen eine Ansicht, die niemand sieht. + // Fuenf Sekunden, weil der Fortschrittsbalken sonst ruckelt und ein Titel + // selten kuerzer als zwei Minuten ist. + useEffect(() => { + if (tab !== 'community') return undefined; + let lebt = true; + const holen = () => apiGet('/api/radio/status') + .then((d) => { if (lebt) setRadio(d); }) + .catch(() => {}); + holen(); + const timer = setInterval(holen, 5000); + return () => { lebt = false; clearInterval(timer); }; + }, [tab]); + // Portal-Dienste (Kacheln auf der Startseite) const emptyService = { id: null, name: '', url: '', icon: '', description: '', sort: 0 }; const [services, setServices] = useState([]); @@ -2151,6 +2191,323 @@ export default function Settings({ me }) { ohne Haken antwortet /radio nur mit einem Hinweis +
+ +
+ + {/* ── Fernbedienung ────────────────────────────────────── + Steht ueber der Senderliste, weil man oefter etwas + abspielt als die Liste pflegt. */} + {radio?.mehrdeutig && ( +

+ Der Bot steht in mehreren Servern und es ist keiner eingestellt — + unter System die Server-ID eintragen, sonst weiss + das Panel nicht, welchen es steuern soll. +

+ )} + + {radio && !radio.mehrdeutig && ( +
+
+ {radio.laeuft ? ( + <> +
+ + {radio.laeuft.art === 'youtube' ? '▶️' : '📻'} + +
+ + {radio.laeuft.art === 'youtube' + ? radio.laeuft.titel + : radio.laeuft.sender} + + + {radio.laeuft.art === 'youtube' + ? [radio.laeuft.kanal, radio.laeuft.heimStation + && `danach zurück auf ${radio.laeuft.heimStation.name}`] + .filter(Boolean).join(' · ') + : (radio.laeuft.titel ?? 'Titel wird abgefragt …')} + + + 🔊 {radio.laeuft.kanalName ?? radio.laeuft.kanalId} + +
+
+ {/* Der Balken nur, wo er etwas aussagen kann: ohne + bekannte Dauer gaebe er einen Stand vor, den + niemand kennt. */} + {radio.laeuft.dauer ? ( +
+
+ +
+ + {dauerText(radio.laeuft.verstrichen)} / {dauerText(radio.laeuft.dauer)} + +
+ ) : radio.laeuft.art === 'youtube' && ( +

🔴 Livestream

+ )} + {radio.laeuft.tonLaeuft === false && ( +

+ ⚠️ Es kommt kein Ton an. + {radio.werkzeuge.ffmpeg === false + && ' Auf dem Server fehlt ffmpeg.'} +

+ )} + + ) : ( +

+ Nichts läuft gerade. + {radio.stand.n > 0 + && ` ${radio.stand.n} Titel warten — Sprachkanal wählen und abspielen.`} +

+ )} +
+ +
+
+ + +
+
+ + {/* onMouseUp/onChange statt live bei jedem Pixel: sonst + geht je Mausbewegung eine Anfrage raus. */} + setRadio({ ...radio, lautstaerke: Number(e.target.value) })} + onMouseUp={(e) => radioSteuern('laut', { wert: Number(e.target.value) })} + onTouchEnd={(e) => radioSteuern('laut', { wert: Number(e.target.value) })} /> +
+
+ +
+ + + + +
+ + {/* ── Einreihen ── */} +
+ +
+ { setRadioEingabe(e.target.value); setRadioTreffer(null); }} + onKeyDown={(e) => e.key === 'Enter' && radioEinreihen()} /> + +
+ {!radio.werkzeuge.youtubeAn && ( + + YouTube ist ausgeschaltet — Haken oben setzen und speichern. + + )} +
+ + {/* Bei einer Suche wird nicht geraten, welcher gemeint war. */} + {radioTreffer && ( +
+ {radioTreffer.map((t) => ( +
+ ▶️ +
+ {t.titel} + + {[t.kanal, dauerText(t.dauer)].filter(Boolean).join(' · ')} + +
+
+ +
+
+ ))} +
+ )} + + {/* ── Warteschlange ── */} + {radio.warteschlange.length > 0 && ( + <> + +
+ {radio.warteschlange.map((e, i) => ( +
+ {i + 1}. +
+ {e.titel} + + {[e.kanal, dauerText(e.dauer), + e.wunsch_von && `von ${e.wunsch_von}`] + .filter(Boolean).join(' · ')} + +
+
+ + + +
+
+ ))} +
+
+ +
+ + )} + + {radioFehler &&

❌ {radioFehler}

} + + {/* ── yt-dlp ────────────────────────────────────── + Steht sichtbar hier und nicht in einer Fussnote: + wenn YouTube klemmt, ist das fast immer die + Antwort, und Stille erklaert sich nicht selbst. */} +
+ + yt-dlp — {radio.werkzeuge.ytdlp === false + ? '❌ fehlt' + : radio.werkzeuge.ytdlp === null + ? '… noch nicht geprüft' + : `✅ ${radio.werkzeuge.ytdlpFassung ?? 'da'}`} + {radio.werkzeuge.cookies ? ' · Cookies gesetzt' : ''} + + + {radio.werkzeuge.letzterFehler && ( + <> + +
{radio.werkzeuge.letzterFehler.text}
+ + )} + +

+ YouTube sperrt Server-Adressen inzwischen häufig + („Sign in to confirm you're not a bot"). Zwei Griffe helfen, + beide ohne neues Image: eine cookies.txt aus dem + Browser eines angemeldeten Kontos, und eigene Argumente + (Feld weiter unten in dieser Sektion). +

+ +
+ +