import React, { useEffect, useMemo, useRef, useState } from 'react'; import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop, subscribeEvents, getSelectedChannels, setSelectedChannel } from './api'; import type { VoiceChannelInfo, Sound, Category } from './types'; import { getCookie, setCookie } from './cookies'; export default function App() { const [sounds, setSounds] = useState([]); const [total, setTotal] = useState(0); const [folders, setFolders] = useState>([]); const [activeFolder, setActiveFolder] = useState('__all__'); const [categories, setCategories] = useState([]); const [activeCategoryId, setActiveCategoryId] = useState(''); const [channels, setChannels] = useState([]); const [query, setQuery] = useState(''); const [selected, setSelected] = useState(''); const selectedRef = useRef(''); const [loading, setLoading] = useState(false); const [notification, setNotification] = useState<{ msg: string, type: 'info' | 'error' } | null>(null); const [volume, setVolume] = useState(1); const [favs, setFavs] = useState>({}); const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark'); const [isAdmin, setIsAdmin] = useState(false); // Chaos Mode (Partymode) const [chaosMode, setChaosMode] = useState(false); const [partyActiveGuilds, setPartyActiveGuilds] = useState([]); const chaosTimeoutRef = useRef(null); const chaosModeRef = useRef(false); // Scrolled State for Header blur const [isScrolled, setIsScrolled] = useState(false); useEffect(() => { chaosModeRef.current = chaosMode; }, [chaosMode]); useEffect(() => { selectedRef.current = selected; }, [selected]); const showNotification = (msg: string, type: 'info' | 'error' = 'info') => { setNotification({ msg, type }); setTimeout(() => setNotification(null), 3000); }; // ---------------- Init Load ---------------- useEffect(() => { (async () => { try { const [c, selectedMap] = await Promise.all([fetchChannels(), getSelectedChannels()]); setChannels(c); let initial = ''; if (c.length > 0) { const firstGuild = c[0].guildId; const serverCid = selectedMap[firstGuild]; if (serverCid && c.find(x => x.guildId === firstGuild && x.channelId === serverCid)) { initial = `${firstGuild}:${serverCid}`; } else { initial = `${c[0].guildId}:${c[0].channelId}`; } } if (initial) setSelected(initial); } catch (e: any) { showNotification(e?.message || 'Fehler beim Laden der Channels', 'error'); } try { setIsAdmin(await adminStatus()); } catch { } try { const cats = await fetchCategories(); setCategories(cats.categories || []); } catch { } })(); }, []); // ---------------- Theme ---------------- useEffect(() => { document.body.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); }, [theme]); // ---------------- SSE Events ---------------- useEffect(() => { const unsub = subscribeEvents((msg) => { if (msg?.type === 'party') { setPartyActiveGuilds((prev) => { const s = new Set(prev); if (msg.active) s.add(msg.guildId); else s.delete(msg.guildId); return Array.from(s); }); } else if (msg?.type === 'snapshot') { setPartyActiveGuilds(Array.isArray(msg.party) ? msg.party : []); try { const sel = msg?.selected || {}; const currentSelected = selectedRef.current || ''; const gid = currentSelected ? currentSelected.split(':')[0] : ''; if (gid && sel[gid]) { const newVal = `${gid}:${sel[gid]}`; setSelected(newVal); } } catch { } try { const vols = msg?.volumes || {}; const cur = selectedRef.current || ''; const gid = cur ? cur.split(':')[0] : ''; if (gid && typeof vols[gid] === 'number') { setVolume(vols[gid]); } } catch { } } else if (msg?.type === 'channel') { try { const gid = msg.guildId; const cid = msg.channelId; if (gid && cid) { const currentSelected = selectedRef.current || ''; const curGid = currentSelected ? currentSelected.split(':')[0] : ''; if (curGid === gid) setSelected(`${gid}:${cid}`); } } catch { } } else if (msg?.type === 'volume') { try { const gid = msg.guildId; const v = msg.volume; const cur = selectedRef.current || ''; const curGid = cur ? cur.split(':')[0] : ''; if (gid && curGid === gid && typeof v === 'number') { setVolume(v); } } catch { } } }); return () => { try { unsub(); } catch { } }; }, []); useEffect(() => { const gid = selected ? selected.split(':')[0] : ''; setChaosMode(gid ? partyActiveGuilds.includes(gid) : false); }, [selected, partyActiveGuilds]); // ---------------- Data Fetching ---------------- useEffect(() => { (async () => { try { const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder; const s = await fetchSounds(query, folderParam, activeCategoryId || undefined, false); // Removed fuzzy param for cleaner UI setSounds(s.items); setTotal(s.total); setFolders(s.folders); } catch (e: any) { showNotification(e?.message || 'Fehler beim Laden der Sounds', 'error'); } })(); }, [activeFolder, query, activeCategoryId]); useEffect(() => { const c = getCookie('favs'); if (c) { try { setFavs(JSON.parse(c)); } catch { } } }, []); useEffect(() => { try { setCookie('favs', JSON.stringify(favs)); } catch { } }, [favs]); useEffect(() => { (async () => { if (selected) { localStorage.setItem('selectedChannel', selected); try { const [guildId] = selected.split(':'); const v = await getVolume(guildId); setVolume(v); } catch { } } })(); }, [selected]); // ---------------- Actions ---------------- async function handlePlay(name: string, rel?: string) { if (!selected) return showNotification('Bitte einen Voice-Channel auswählen', 'error'); const [guildId, channelId] = selected.split(':'); try { setLoading(true); await playSound(name, guildId, channelId, volume, rel); } catch (e: any) { showNotification(e?.message || 'Wiedergabe fehlgeschlagen', 'error'); } finally { setLoading(false); } } // Chaos Mode Logic const startChaosMode = async () => { if (!selected || !sounds.length) return; const playRandomSound = async () => { const pool = sounds; if (!pool.length || !selected) return; const randomSound = pool[Math.floor(Math.random() * pool.length)]; const [guildId, channelId] = selected.split(':'); try { await playSound(randomSound.name, guildId, channelId, volume, randomSound.relativePath); } catch (e: any) { console.error('Chaos sound play failed:', e); } }; const scheduleNextPlay = async () => { if (!chaosModeRef.current) return; await playRandomSound(); const delay = 30_000 + Math.floor(Math.random() * 60_000); // 30-90 Sekunden chaosTimeoutRef.current = window.setTimeout(scheduleNextPlay, delay); }; await playRandomSound(); const firstDelay = 30_000 + Math.floor(Math.random() * 60_000); chaosTimeoutRef.current = window.setTimeout(scheduleNextPlay, firstDelay); }; const stopChaosMode = async () => { if (chaosTimeoutRef.current) { clearTimeout(chaosTimeoutRef.current); chaosTimeoutRef.current = null; } if (selected) { const [guildId] = selected.split(':'); try { await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method: 'POST' }); } catch { } } }; const toggleChaosMode = async () => { if (chaosMode) { setChaosMode(false); await stopChaosMode(); if (selected) { const [guildId] = selected.split(':'); try { await partyStop(guildId); } catch { } } } else { setChaosMode(true); await startChaosMode(); if (selected) { const [guildId, channelId] = selected.split(':'); try { await partyStart(guildId, channelId); } catch { } } } }; // Filter Data const filtered = sounds; const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]); // Scroll Handler for Top Bar Blur const handleScroll = (e: React.UIEvent) => { setIsScrolled(e.currentTarget.scrollTop > 20); }; return (
{/* ---------------- Sidebar ---------------- */} {/* ---------------- Main Content ---------------- */}

{activeFolder === '__all__' ? 'All Sounds' : activeFolder === '__favs__' ? 'Favorites' : activeFolder === '__recent__' ? 'Recently Added' : folders.find(f => f.key === activeFolder)?.name.replace(/\s*\(\d+\)\s*$/, '') || 'Library'}

search setQuery(e.target.value)} />
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => { const key = `${s.relativePath ?? s.fileName}`; const isFav = !!favs[key]; return (
handlePlay(s.name, s.relativePath)}>
music_note
{s.name}
{Array.isArray((s as any).badges) && (s as any).badges!.includes('new') && (
NEW
)}
); })}
{/* ---------------- Bottom Control Bar ---------------- */}
{/* Target Channel */}
headset_mic
{/* Playback Controls */}
{/* Volume */}
volume_down { const v = parseFloat(e.target.value); setVolume(v); try { (e.target as HTMLInputElement).style.setProperty('--_fill', `${Math.round(v * 100)}%`); } catch { } if (selected) { const [guildId] = selected.split(':'); try { await setVolumeLive(guildId, v); } catch { } } }} style={{ ['--_fill' as any]: `${Math.round(volume * 100)}%` }} /> volume_up
{notification && (
{notification.type === 'error' ? 'error_outline' : 'check_circle'} {notification.msg}
)}
); }