feat(folders): Tabs für Unterordner + rekursive Sound-Liste; Play mit relativePath; UI-Tabs

This commit is contained in:
vibe-bot 2025-08-08 01:56:30 +02:00
parent 24de686a54
commit f9bec8b5a1
5 changed files with 85 additions and 16 deletions

View file

@ -5,6 +5,8 @@ import type { VoiceChannelInfo, Sound } from './types';
export default function App() {
const [sounds, setSounds] = useState<Sound[]>([]);
const [total, setTotal] = useState<number>(0);
const [folders, setFolders] = useState<Array<{ key: string; name: string; count: number }>>([]);
const [activeFolder, setActiveFolder] = useState<string>('__all__');
const [channels, setChannels] = useState<VoiceChannelInfo[]>([]);
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<string>('');
@ -18,6 +20,7 @@ export default function App() {
const [s, c] = await Promise.all([fetchSounds(), fetchChannels()]);
setSounds(s.items);
setTotal(s.total);
setFolders(s.folders);
setChannels(c);
if (c[0]) setSelected(`${c[0].guildId}:${c[0].channelId}`);
} catch (e: any) {
@ -30,6 +33,7 @@ export default function App() {
const s = await fetchSounds(query);
setSounds(s.items);
setTotal(s.total);
setFolders(s.folders);
} catch {}
}, 10000);
return () => clearInterval(interval);
@ -41,13 +45,13 @@ export default function App() {
return sounds.filter((s) => s.name.toLowerCase().includes(q));
}, [sounds, query]);
async function handlePlay(name: string) {
async function handlePlay(name: string, rel?: string) {
setError(null);
if (!selected) return setError('Bitte einen Voice-Channel auswählen');
const [guildId, channelId] = selected.split(':');
try {
setLoading(true);
await playSound(name, guildId, channelId, volume);
await playSound(name, guildId, channelId, volume, rel);
} catch (e: any) {
setError(e?.message || 'Play fehlgeschlagen');
} finally {
@ -101,11 +105,31 @@ export default function App() {
</div>
</section>
{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>
)}
{error && <div className="error">{error}</div>}
<section className="grid">
{filtered.map((s) => (
<button key={s.fileName} className="sound" onClick={() => handlePlay(s.name)} disabled={loading}>
<button key={`${s.fileName}-${s.name}`} className="sound" onClick={() => handlePlay(s.name, s.relativePath)} disabled={loading}>
{s.name}
</button>
))}
@ -116,6 +140,10 @@ export default function App() {
);
}
function handlePlayWithPathFactory(play: (name: string, rel?: string) => Promise<void>) {
return (s: Sound & { relativePath?: string }) => play(s.name, s.relativePath);
}

View file

@ -2,7 +2,7 @@ import type { Sound, SoundsResponse, VoiceChannelInfo } from './types';
const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api';
export async function fetchSounds(q?: string): Promise<SoundsResponse> {
export async function fetchSounds(q?: string, folderKey?: string): Promise<SoundsResponse> {
const url = new URL(`${API_BASE}/sounds`, window.location.origin);
if (q) url.searchParams.set('q', q);
const res = await fetch(url.toString());
@ -16,11 +16,11 @@ export async function fetchChannels(): Promise<VoiceChannelInfo[]> {
return res.json();
}
export async function playSound(soundName: string, guildId: string, channelId: string, volume: number): Promise<void> {
export async function playSound(soundName: string, guildId: string, channelId: string, volume: number, relativePath?: string): Promise<void> {
const res = await fetch(`${API_BASE}/play`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ soundName, guildId, channelId, volume })
body: JSON.stringify({ soundName, guildId, channelId, volume, relativePath })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));

View file

@ -55,6 +55,16 @@ header p { opacity: .8; }
font-size: 14px;
}
.tabs { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
.tab {
padding: 8px 12px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(18,24,48,.6);
color: #e7e7ee;
}
.tab.active { background: linear-gradient(135deg, rgba(88,28,135,.9), rgba(59,130,246,.9)); color: #fff; border-color: transparent; }

View file

@ -1,11 +1,14 @@
export type Sound = {
fileName: string;
name: string;
folder?: string;
relativePath?: string;
};
export type SoundsResponse = {
items: Sound[];
total: number;
folders: Array<{ key: string; name: string; count: number }>;
};
export type VoiceChannelInfo = {