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'; import AdminPanel from './AdminPanel'; import LoginModal from './LoginModal'; import UserSettings from './UserSettings'; interface PluginInfo { name: string; version: string; description: string; } interface AuthUser { authenticated: boolean; provider?: 'discord' | 'steam' | 'admin'; discordId?: string; steamId?: string; username?: string; avatar?: string | null; globalName?: string | null; isAdmin?: boolean; } interface AuthProviders { discord: boolean; steam: boolean; admin: boolean; } // 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; isAdmin?: boolean }>) { 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>({}); // ── Unified Auth State ── const [user, setUser] = useState({ authenticated: false }); const [providers, setProviders] = useState({ discord: false, steam: false, admin: false }); const [showLoginModal, setShowLoginModal] = useState(false); const [showUserSettings, setShowUserSettings] = useState(false); const [showAdminPanel, setShowAdminPanel] = useState(false); // Derived state const isAdmin = user.authenticated && (user.provider === 'admin' || user.isAdmin === true); const isDiscordUser = user.authenticated && user.provider === 'discord'; const isSteamUser = user.authenticated && user.provider === 'steam'; const isRegularUser = isDiscordUser || isSteamUser; // 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(); } }, []); // Check auth status + providers on mount useEffect(() => { fetch('/api/auth/me', { credentials: 'include' }) .then(r => r.json()) .then((data: AuthUser) => setUser(data)) .catch(() => {}); fetch('/api/auth/providers') .then(r => r.json()) .then((data: AuthProviders) => setProviders(data)) .catch(() => {}); // Also check legacy admin cookie (backward compat) fetch('/api/soundboard/admin/status', { credentials: 'include' }) .then(r => r.json()) .then(d => { if (d.authenticated) { setUser(prev => prev.authenticated ? prev : { authenticated: true, provider: 'admin', username: 'Admin', isAdmin: true }); } }) .catch(() => {}); }, []); // Admin login handler (for LoginModal) async function handleAdminLogin(password: string): Promise { try { const resp = await fetch('/api/auth/admin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }), credentials: 'include', }); if (resp.ok) { setUser({ authenticated: true, provider: 'admin', username: 'Admin', isAdmin: true }); return true; } return false; } catch { return false; } } // Unified logout async function handleLogout() { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); setUser({ authenticated: false }); setShowUserSettings(false); setShowAdminPanel(false); } // 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}', }; // What happens when the user button is clicked function handleUserButtonClick() { if (!user.authenticated) { setShowLoginModal(true); } else if (isAdmin) { setShowAdminPanel(true); } else if (isRegularUser) { setShowUserSettings(true); } } return (
{'\u{1F3AE}'} Gaming Hub
{!(window as any).electronAPI && ( {'\u2B07\uFE0F'} Desktop App )} {/* Unified Login / User button */} { // 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}
)}
)}
)} {/* Login Modal */} {showLoginModal && ( setShowLoginModal(false)} onAdminLogin={handleAdminLogin} providers={providers} /> )} {/* User Settings (Discord + Steam users) */} {showUserSettings && isRegularUser && ( setShowUserSettings(false)} onLogout={handleLogout} /> )} {/* Admin Panel */} {showAdminPanel && isAdmin && ( setShowAdminPanel(false)} onLogout={() => { handleLogout(); setShowAdminPanel(false); }} /> )}
{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 (
); }) )}
); }