Feature: Live Stream-Liste + Toast Notifications

- stream_available/stream_ended WS-Events verarbeiten
- WS sofort beim Mount verbinden (nicht nur beim Broadcasting)
- WS reconnect immer aktiv (nicht nur bei aktivem Stream)
- Toast Notifications: neuer Stream, Update verfügbar/bereit
- Notification Permission beim App-Start anfragen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-07 15:05:41 +01:00
parent 939137cc77
commit 3455e20a96
2 changed files with 51 additions and 8 deletions

View file

@ -37,6 +37,13 @@ export default function App() {
const [pluginData, setPluginData] = useState<Record<string, any>>({}); const [pluginData, setPluginData] = useState<Record<string, any>>({});
const eventSourceRef = useRef<EventSource | null>(null); const eventSourceRef = useRef<EventSource | null>(null);
// Request notification permission
useEffect(() => {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
}, []);
// Fetch plugin list // Fetch plugin list
useEffect(() => { useEffect(() => {
fetch('/api/plugins') fetch('/api/plugins')
@ -98,8 +105,18 @@ export default function App() {
useEffect(() => { useEffect(() => {
const api = (window as any).electronAPI; const api = (window as any).electronAPI;
if (!api?.onUpdateAvailable) return; if (!api?.onUpdateAvailable) return;
api.onUpdateAvailable(() => setUpdateState('downloading')); api.onUpdateAvailable(() => {
api.onUpdateReady(() => setUpdateState('ready')); setUpdateState('downloading');
if (Notification.permission === 'granted') {
new Notification('Gaming Hub Update', { body: 'Ein Update wird heruntergeladen...' });
}
});
api.onUpdateReady(() => {
setUpdateState('ready');
if (Notification.permission === 'granted') {
new Notification('Gaming Hub Update bereit', { body: 'Klicke OK um das Update zu installieren.' });
}
});
api.onUpdateNotAvailable?.(() => setUpdateState('uptodate')); api.onUpdateNotAvailable?.(() => setUpdateState('uptodate'));
api.onUpdateError?.(() => setUpdateState('error')); api.onUpdateError?.(() => setUpdateState('error'));
}, []); }, []);

View file

@ -165,9 +165,28 @@ export default function StreamingTab({ data }: { data: any }) {
break; break;
case 'stream_available': case 'stream_available':
setStreams(prev => {
if (prev.some(s => s.id === msg.streamId)) return prev;
return [...prev, {
id: msg.streamId,
broadcasterName: msg.broadcasterName,
title: msg.title,
startedAt: new Date().toISOString(),
viewerCount: 0,
hasPassword: true,
}];
});
// Toast notification for new stream
if (Notification.permission === 'granted') {
new Notification('Neuer Stream', {
body: `${msg.broadcasterName} streamt: ${msg.title}`,
icon: '/assets/icon.png',
});
}
break; break;
case 'stream_ended': case 'stream_ended':
setStreams(prev => prev.filter(s => s.id !== msg.streamId));
if (viewingRef.current?.streamId === msg.streamId) { if (viewingRef.current?.streamId === msg.streamId) {
cleanupViewer(); cleanupViewer();
setViewing(null); setViewing(null);
@ -303,17 +322,24 @@ export default function StreamingTab({ data }: { data: any }) {
ws.onclose = () => { ws.onclose = () => {
wsRef.current = null; wsRef.current = null;
if (isBroadcastingRef.current || viewingRef.current) { // Always reconnect to keep stream list in sync
reconnectTimerRef.current = setTimeout(() => { reconnectTimerRef.current = setTimeout(() => {
reconnectDelayRef.current = Math.min(reconnectDelayRef.current * 2, 10000); reconnectDelayRef.current = Math.min(reconnectDelayRef.current * 2, 10000);
connectWs(); connectWs();
}, reconnectDelayRef.current); }, reconnectDelayRef.current);
}
}; };
ws.onerror = () => { ws.close(); }; ws.onerror = () => { ws.close(); };
}, []); }, []);
// ── Connect WS on mount for live stream updates ──
useEffect(() => {
connectWs();
return () => {
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
};
}, [connectWs]);
// ── Start broadcasting ── // ── Start broadcasting ──
const startBroadcast = useCallback(async () => { const startBroadcast = useCallback(async () => {
if (!userName.trim()) { setError('Bitte gib einen Namen ein.'); return; } if (!userName.trim()) { setError('Bitte gib einen Namen ein.'); return; }