2025-08-07 23:24:56 +02:00
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
2025-08-08 01:51:36 +02:00
|
|
|
|
import { fetchChannels, fetchSounds, playSound, setVolumeLive } from './api';
|
2025-08-07 23:24:56 +02:00
|
|
|
|
import type { VoiceChannelInfo, Sound } from './types';
|
|
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
|
|
const [sounds, setSounds] = useState<Sound[]>([]);
|
2025-08-08 01:40:49 +02:00
|
|
|
|
const [total, setTotal] = useState<number>(0);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
const [folders, setFolders] = useState<Array<{ key: string; name: string; count: number }>>([]);
|
|
|
|
|
|
const [activeFolder, setActiveFolder] = useState<string>('__all__');
|
2025-08-07 23:24:56 +02:00
|
|
|
|
const [channels, setChannels] = useState<VoiceChannelInfo[]>([]);
|
|
|
|
|
|
const [query, setQuery] = useState('');
|
|
|
|
|
|
const [selected, setSelected] = useState<string>('');
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2025-08-08 01:23:52 +02:00
|
|
|
|
const [volume, setVolume] = useState<number>(1);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [s, c] = await Promise.all([fetchSounds(), fetchChannels()]);
|
2025-08-08 01:40:49 +02:00
|
|
|
|
setSounds(s.items);
|
|
|
|
|
|
setTotal(s.total);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
setFolders(s.folders);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
setChannels(c);
|
|
|
|
|
|
if (c[0]) setSelected(`${c[0].guildId}:${c[0].channelId}`);
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setError(e?.message || 'Fehler beim Laden');
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
const interval = setInterval(async () => {
|
|
|
|
|
|
try {
|
2025-08-08 02:14:46 +02:00
|
|
|
|
const s = await fetchSounds(query, activeFolder);
|
2025-08-08 01:40:49 +02:00
|
|
|
|
setSounds(s.items);
|
|
|
|
|
|
setTotal(s.total);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
setFolders(s.folders);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
} catch {}
|
|
|
|
|
|
}, 10000);
|
|
|
|
|
|
return () => clearInterval(interval);
|
2025-08-08 02:14:46 +02:00
|
|
|
|
}, [activeFolder]);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
|
|
|
|
|
const filtered = useMemo(() => {
|
|
|
|
|
|
const q = query.trim().toLowerCase();
|
|
|
|
|
|
if (!q) return sounds;
|
|
|
|
|
|
return sounds.filter((s) => s.name.toLowerCase().includes(q));
|
|
|
|
|
|
}, [sounds, query]);
|
|
|
|
|
|
|
2025-08-08 01:56:30 +02:00
|
|
|
|
async function handlePlay(name: string, rel?: string) {
|
2025-08-07 23:24:56 +02:00
|
|
|
|
setError(null);
|
|
|
|
|
|
if (!selected) return setError('Bitte einen Voice-Channel auswählen');
|
|
|
|
|
|
const [guildId, channelId] = selected.split(':');
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
await playSound(name, guildId, channelId, volume, rel);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setError(e?.message || 'Play fehlgeschlagen');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="container">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<h1>Discord Soundboard</h1>
|
|
|
|
|
|
<p>Schicke dem Bot per privater Nachricht eine .mp3 — neue Sounds erscheinen automatisch.</p>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="controls">
|
2025-08-08 01:23:52 +02:00
|
|
|
|
<div className="control search">
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={query}
|
|
|
|
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
|
|
|
|
placeholder="Nach Sounds suchen..."
|
|
|
|
|
|
aria-label="Suche"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="control select">
|
|
|
|
|
|
<select value={selected} onChange={(e) => setSelected(e.target.value)} aria-label="Voice-Channel">
|
|
|
|
|
|
{channels.map((c) => (
|
|
|
|
|
|
<option key={`${c.guildId}:${c.channelId}`} value={`${c.guildId}:${c.channelId}`}>
|
|
|
|
|
|
{c.guildName} – {c.channelName}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="control volume">
|
|
|
|
|
|
<label>🔊 {Math.round(volume * 100)}%</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
max={1}
|
|
|
|
|
|
step={0.01}
|
|
|
|
|
|
value={volume}
|
2025-08-08 01:51:36 +02:00
|
|
|
|
onChange={async (e) => {
|
|
|
|
|
|
const v = parseFloat(e.target.value);
|
|
|
|
|
|
setVolume(v);
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
const [guildId] = selected.split(':');
|
|
|
|
|
|
try { await setVolumeLive(guildId, v); } catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-08-08 01:23:52 +02:00
|
|
|
|
aria-label="Lautstärke"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-07 23:24:56 +02:00
|
|
|
|
</section>
|
|
|
|
|
|
|
2025-08-08 01:56:30 +02:00
|
|
|
|
{folders.length > 0 && (
|
|
|
|
|
|
<nav className="tabs">
|
|
|
|
|
|
{folders.map((f) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={f.key}
|
|
|
|
|
|
className={`tab ${activeFolder === f.key ? 'active' : ''}`}
|
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
|
setActiveFolder(f.key);
|
|
|
|
|
|
const resp = await fetchSounds(query, f.key);
|
|
|
|
|
|
setSounds(resp.items);
|
|
|
|
|
|
setTotal(resp.total);
|
|
|
|
|
|
setFolders(resp.folders);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{f.name} ({f.count})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-08-07 23:24:56 +02:00
|
|
|
|
{error && <div className="error">{error}</div>}
|
|
|
|
|
|
|
|
|
|
|
|
<section className="grid">
|
|
|
|
|
|
{filtered.map((s) => (
|
2025-08-08 01:56:30 +02:00
|
|
|
|
<button key={`${s.fileName}-${s.name}`} className="sound" onClick={() => handlePlay(s.name, s.relativePath)} disabled={loading}>
|
2025-08-07 23:24:56 +02:00
|
|
|
|
{s.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{filtered.length === 0 && <div className="hint">Keine Sounds gefunden.</div>}
|
|
|
|
|
|
</section>
|
2025-08-08 01:40:49 +02:00
|
|
|
|
<div className="footer-info">Geladene Sounds: {total}</div>
|
2025-08-07 23:24:56 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 01:56:30 +02:00
|
|
|
|
function handlePlayWithPathFactory(play: (name: string, rel?: string) => Promise<void>) {
|
|
|
|
|
|
return (s: Sound & { relativePath?: string }) => play(s.name, s.relativePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|