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; } 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>({}); const eventSourceRef = useRef(null); // Request notification permission useEffect(() => { if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } }, []); // 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 )} setShowVersionModal(true)} title="Versionsinformationen" > v{version}
{showVersionModal && (
setShowVersionModal(false)}>
e.stopPropagation()}>
Versionsinformationen
Version v{version}
Server {connected ? 'Verbunden' : 'Getrennt'}
)}
{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 (
); }) )}
); }