import { useState, useEffect, useRef } from 'react'; import RadioTab from './plugins/radio/RadioTab'; import SoundboardTab from './plugins/soundboard/SoundboardTab'; import LolstatsTab from './plugins/lolstats/LolstatsTab'; import StreamingTab from './plugins/streaming/StreamingTab'; import WatchTogetherTab from './plugins/watch-together/WatchTogetherTab'; import GameLibraryTab from './plugins/game-library/GameLibraryTab'; interface PluginInfo { name: string; version: string; description: string; } // Plugin tab components const tabComponents: Record> = { radio: RadioTab, soundboard: SoundboardTab, lolstats: LolstatsTab, streaming: StreamingTab, 'watch-together': WatchTogetherTab, 'game-library': GameLibraryTab, }; export function registerTab(pluginName: string, component: React.FC<{ data: any }>) { tabComponents[pluginName] = component; } type UpdateStatus = 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'upToDate' | 'error'; export default function App() { const [connected, setConnected] = useState(false); const [plugins, setPlugins] = useState([]); const [activeTab, setActiveTabRaw] = useState(() => localStorage.getItem('hub_activeTab') ?? ''); const setActiveTab = (tab: string) => { setActiveTabRaw(tab); localStorage.setItem('hub_activeTab', tab); }; const [showVersionModal, setShowVersionModal] = useState(false); const [pluginData, setPluginData] = useState>({}); // Electron auto-update state const isElectron = !!(window as any).electronAPI?.isElectron; const electronVersion = isElectron ? (window as any).electronAPI.version : null; const [updateStatus, setUpdateStatus] = useState('idle'); const [updateError, setUpdateError] = useState(''); const eventSourceRef = useRef(null); // Request notification permission useEffect(() => { if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } }, []); // Electron auto-update listeners useEffect(() => { if (!isElectron) return; const api = (window as any).electronAPI; api.onUpdateAvailable(() => setUpdateStatus('downloading')); api.onUpdateReady(() => setUpdateStatus('ready')); api.onUpdateNotAvailable(() => setUpdateStatus('upToDate')); api.onUpdateError((msg: string) => { setUpdateStatus('error'); setUpdateError(msg || 'Unbekannter Fehler'); }); // Sync initial status (Hintergrund-Download könnte bereits laufen) const currentState = api.getUpdateStatus?.(); if (currentState === 'downloading') setUpdateStatus('downloading'); else if (currentState === 'ready') setUpdateStatus('ready'); else if (currentState === 'checking') setUpdateStatus('checking'); }, [isElectron]); // Fetch plugin list useEffect(() => { fetch('/api/plugins') .then(r => r.json()) .then((list: PluginInfo[]) => { setPlugins(list); // If ?viewStream= is in URL, force switch to streaming tab const urlParams = new URLSearchParams(location.search); if (urlParams.has('viewStream') && list.some(p => p.name === 'streaming')) { setActiveTab('streaming'); return; } const saved = localStorage.getItem('hub_activeTab'); const valid = list.some(p => p.name === saved); if (list.length > 0 && !valid) setActiveTab(list[0].name); }) .catch(() => {}); }, []); // SSE connection useEffect(() => { let es: EventSource | null = null; let retryTimer: ReturnType; function connect() { es = new EventSource('/api/events'); eventSourceRef.current = es; es.onopen = () => setConnected(true); es.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === 'snapshot') { setPluginData(prev => ({ ...prev, ...msg })); } else if (msg.plugin) { setPluginData(prev => ({ ...prev, [msg.plugin]: { ...(prev[msg.plugin] || {}), ...msg }, })); } } catch {} }; es.onerror = () => { setConnected(false); es?.close(); retryTimer = setTimeout(connect, 3000); }; } connect(); return () => { es?.close(); clearTimeout(retryTimer); }; }, []); const version = (import.meta as any).env?.VITE_APP_VERSION ?? 'dev'; // Close version modal on Escape useEffect(() => { if (!showVersionModal) return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setShowVersionModal(false); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [showVersionModal]); // Tab icon mapping const tabIcons: Record = { radio: '\u{1F30D}', soundboard: '\u{1F3B5}', lolstats: '\u{2694}\uFE0F', stats: '\u{1F4CA}', events: '\u{1F4C5}', games: '\u{1F3B2}', gamevote: '\u{1F3AE}', streaming: '\u{1F4FA}', 'watch-together': '\u{1F3AC}', 'game-library': '\u{1F3AE}', }; return (
{'\u{1F3AE}'} Gaming Hub
{!(window as any).electronAPI && ( {'\u2B07\uFE0F'} Desktop App )} { // Status vom Main-Prozess synchronisieren bevor Modal öffnet if (isElectron) { const api = (window as any).electronAPI; const s = api.getUpdateStatus?.(); if (s === 'downloading') setUpdateStatus('downloading'); else if (s === 'ready') setUpdateStatus('ready'); else if (s === 'checking') setUpdateStatus('checking'); } setShowVersionModal(true); }} title="Versionsinformationen" > v{version}
{showVersionModal && (
setShowVersionModal(false)}>
e.stopPropagation()}>
Versionsinformationen
Hub-Version v{version}
{isElectron && (
Desktop-App v{electronVersion}
)}
Server {connected ? 'Verbunden' : 'Getrennt'}
{isElectron && (
{updateStatus === 'idle' && ( )} {updateStatus === 'checking' && (
Suche nach Updates…
)} {updateStatus === 'downloading' && (
Update wird heruntergeladen…
)} {updateStatus === 'ready' && ( )} {updateStatus === 'upToDate' && (
{'\u2705'} App ist aktuell
)} {updateStatus === 'error' && (
{'\u274C'} {updateError}
)}
)}
)}
{plugins.length === 0 ? (
{'\u{1F4E6}'}

Keine Plugins geladen

Plugins werden im Server konfiguriert.

) : ( /* Render ALL tabs, hide inactive ones to preserve state. Active tab gets full dimensions; hidden tabs stay in DOM but invisible. */ plugins.map(p => { const Comp = tabComponents[p.name]; if (!Comp) return null; const isActive = activeTab === p.name; return (
); }) )}
); }