fix(merge): Entfernt Konfliktmarker und Versionsanzeige im Nightly-Header
This commit is contained in:
commit
ef682e1827
4 changed files with 209 additions and 39 deletions
|
|
@ -2,6 +2,7 @@ import path from 'node:path';
|
|||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import express, { Request, Response } from 'express';
|
||||
// import multer from 'multer';
|
||||
import cors from 'cors';
|
||||
import crypto from 'node:crypto';
|
||||
import { Client, GatewayIntentBits, Partials, ChannelType, Events, type Message } from 'discord.js';
|
||||
|
|
@ -46,13 +47,14 @@ fs.mkdirSync(SOUNDS_DIR, { recursive: true });
|
|||
|
||||
// Persistenter Zustand: Lautstärke/Plays + Kategorien
|
||||
type Category = { id: string; name: string; color?: string; sort?: number };
|
||||
type PersistedState = {
|
||||
type PersistedState = {
|
||||
volumes: Record<string, number>;
|
||||
plays: Record<string, number>;
|
||||
totalPlays: number;
|
||||
categories?: Category[];
|
||||
fileCategories?: Record<string, string[]>; // relPath or fileName -> categoryIds[]
|
||||
fileBadges?: Record<string, string[]>; // relPath or fileName -> custom badges (emoji/text)
|
||||
selectedChannels?: Record<string, string>; // guildId -> channelId (serverweite Auswahl)
|
||||
};
|
||||
// Neuer, persistenter Speicherort direkt im Sounds-Volume
|
||||
const STATE_FILE_NEW = path.join(SOUNDS_DIR, 'state.json');
|
||||
|
|
@ -71,7 +73,8 @@ function readPersistedState(): PersistedState {
|
|||
totalPlays: parsed.totalPlays ?? 0,
|
||||
categories: Array.isArray(parsed.categories) ? parsed.categories : [],
|
||||
fileCategories: parsed.fileCategories ?? {},
|
||||
fileBadges: parsed.fileBadges ?? {}
|
||||
fileBadges: parsed.fileBadges ?? {},
|
||||
selectedChannels: parsed.selectedChannels ?? {}
|
||||
} as PersistedState;
|
||||
}
|
||||
// 2) Fallback: alten Speicherort lesen und sofort nach NEW migrieren
|
||||
|
|
@ -84,7 +87,8 @@ function readPersistedState(): PersistedState {
|
|||
totalPlays: parsed.totalPlays ?? 0,
|
||||
categories: Array.isArray(parsed.categories) ? parsed.categories : [],
|
||||
fileCategories: parsed.fileCategories ?? {},
|
||||
fileBadges: parsed.fileBadges ?? {}
|
||||
fileBadges: parsed.fileBadges ?? {},
|
||||
selectedChannels: parsed.selectedChannels ?? {}
|
||||
};
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(STATE_FILE_NEW), { recursive: true });
|
||||
|
|
@ -161,6 +165,23 @@ function sseBroadcast(payload: any) {
|
|||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktionen für serverweit ausgewählten Channel pro Guild
|
||||
function getSelectedChannelForGuild(guildId: string): string | undefined {
|
||||
const id = String(guildId || '');
|
||||
if (!id) return undefined;
|
||||
const sc = persistedState.selectedChannels ?? {};
|
||||
return sc[id];
|
||||
}
|
||||
function setSelectedChannelForGuild(guildId: string, channelId: string): void {
|
||||
const g = String(guildId || '');
|
||||
const c = String(channelId || '');
|
||||
if (!g || !c) return;
|
||||
if (!persistedState.selectedChannels) persistedState.selectedChannels = {};
|
||||
persistedState.selectedChannels[g] = c;
|
||||
writePersistedState(persistedState);
|
||||
sseBroadcast({ type: 'channel', guildId: g, channelId: c });
|
||||
}
|
||||
|
||||
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise<void> {
|
||||
const guild = client.guilds.cache.get(guildId);
|
||||
if (!guild) throw new Error('Guild nicht gefunden');
|
||||
|
|
@ -431,6 +452,8 @@ app.get('/api/sounds', (req: Request, res: Response) => {
|
|||
const q = String(req.query.q ?? '').toLowerCase();
|
||||
const folderFilter = typeof req.query.folder === 'string' ? (req.query.folder as string) : '__all__';
|
||||
const categoryFilter = typeof (req.query as any).categoryId === 'string' ? String((req.query as any).categoryId) : undefined;
|
||||
const fuzzyParam = String((req.query as any).fuzzy ?? '0');
|
||||
const useFuzzy = fuzzyParam === '1' || fuzzyParam === 'true';
|
||||
|
||||
const rootEntries = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true });
|
||||
const rootFiles = rootEntries
|
||||
|
|
@ -484,7 +507,54 @@ app.get('/api/sounds', (req: Request, res: Response) => {
|
|||
itemsByFolder = allItems.filter((it) => (folderFilter === '' ? it.folder === '' : it.folder === folderFilter));
|
||||
}
|
||||
}
|
||||
const filteredItems = itemsByFolder.filter((s) => (q ? s.name.toLowerCase().includes(q) : true));
|
||||
// Fuzzy-Score: bevorzugt Präfixe, zusammenhängende Treffer und frühe Positionen
|
||||
function fuzzyScore(text: string, pattern: string): number {
|
||||
if (!pattern) return 1;
|
||||
if (text === pattern) return 2000;
|
||||
const idx = text.indexOf(pattern);
|
||||
if (idx !== -1) {
|
||||
let base = 1000;
|
||||
if (idx === 0) base += 200; // Präfix-Bonus
|
||||
return base - idx * 2; // leichte Positionsstrafe
|
||||
}
|
||||
// subsequence Matching
|
||||
let textIndex = 0;
|
||||
let patIndex = 0;
|
||||
let score = 0;
|
||||
let lastMatch = -1;
|
||||
let gaps = 0;
|
||||
let firstMatchPos = -1;
|
||||
while (textIndex < text.length && patIndex < pattern.length) {
|
||||
if (text[textIndex] === pattern[patIndex]) {
|
||||
if (firstMatchPos === -1) firstMatchPos = textIndex;
|
||||
if (lastMatch === textIndex - 1) {
|
||||
score += 5; // zusammenhängende Treffer belohnen
|
||||
}
|
||||
lastMatch = textIndex;
|
||||
patIndex++;
|
||||
} else if (firstMatchPos !== -1) {
|
||||
gaps++;
|
||||
}
|
||||
textIndex++;
|
||||
}
|
||||
if (patIndex !== pattern.length) return 0; // nicht alle Pattern-Zeichen gefunden
|
||||
score += Math.max(0, 300 - firstMatchPos * 2); // frühe Starts belohnen
|
||||
score += Math.max(0, 100 - gaps * 10); // weniger Lücken belohnen
|
||||
return score;
|
||||
}
|
||||
|
||||
let filteredItems = itemsByFolder;
|
||||
if (q) {
|
||||
if (useFuzzy) {
|
||||
const scored = itemsByFolder
|
||||
.map((it) => ({ it, score: fuzzyScore(it.name.toLowerCase(), q) }))
|
||||
.filter((x) => x.score > 0)
|
||||
.sort((a, b) => (b.score - a.score) || a.it.name.localeCompare(b.it.name));
|
||||
filteredItems = scored.map((x) => x.it);
|
||||
} else {
|
||||
filteredItems = itemsByFolder.filter((s) => s.name.toLowerCase().includes(q));
|
||||
}
|
||||
}
|
||||
|
||||
const total = allItems.length;
|
||||
const recentCount = Math.min(10, total);
|
||||
|
|
@ -677,13 +747,14 @@ app.get('/api/channels', (_req: Request, res: Response) => {
|
|||
if (!client.isReady()) return res.status(503).json({ error: 'Bot noch nicht bereit' });
|
||||
|
||||
const allowed = new Set(ALLOWED_GUILD_IDS);
|
||||
const result: Array<{ guildId: string; guildName: string; channelId: string; channelName: string }> = [];
|
||||
const result: Array<{ guildId: string; guildName: string; channelId: string; channelName: string; selected?: boolean }> = [];
|
||||
for (const [, guild] of client.guilds.cache) {
|
||||
if (allowed.size > 0 && !allowed.has(guild.id)) continue;
|
||||
const channels = guild.channels.cache;
|
||||
for (const [, ch] of channels) {
|
||||
if (ch?.type === ChannelType.GuildVoice || ch?.type === ChannelType.GuildStageVoice) {
|
||||
result.push({ guildId: guild.id, guildName: guild.name, channelId: ch.id, channelName: ch.name });
|
||||
const sel = getSelectedChannelForGuild(guild.id);
|
||||
result.push({ guildId: guild.id, guildName: guild.name, channelId: ch.id, channelName: ch.name, selected: sel === ch.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -691,6 +762,36 @@ app.get('/api/channels', (_req: Request, res: Response) => {
|
|||
res.json(result);
|
||||
});
|
||||
|
||||
// Globale Channel-Auswahl: auslesen (komplettes Mapping)
|
||||
app.get('/api/selected-channels', (_req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({ selected: persistedState.selectedChannels ?? {} });
|
||||
} catch (e: any) {
|
||||
res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
// Globale Channel-Auswahl: setzen (validiert Channel-Typ)
|
||||
app.post('/api/selected-channel', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { guildId, channelId } = req.body as { guildId?: string; channelId?: string };
|
||||
const gid = String(guildId ?? '');
|
||||
const cid = String(channelId ?? '');
|
||||
if (!gid || !cid) return res.status(400).json({ error: 'guildId und channelId erforderlich' });
|
||||
const guild = client.guilds.cache.get(gid);
|
||||
if (!guild) return res.status(404).json({ error: 'Guild nicht gefunden' });
|
||||
const ch = guild.channels.cache.get(cid);
|
||||
if (!ch || (ch.type !== ChannelType.GuildVoice && ch.type !== ChannelType.GuildStageVoice)) {
|
||||
return res.status(400).json({ error: 'Ungültiger Voice-Channel' });
|
||||
}
|
||||
setSelectedChannelForGuild(gid, cid);
|
||||
return res.json({ ok: true });
|
||||
} catch (e: any) {
|
||||
console.error('selected-channel error', e);
|
||||
return res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/play', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { soundName, guildId, channelId, volume, folder, relativePath } = req.body as {
|
||||
|
|
@ -973,7 +1074,9 @@ app.get('/api/events', (req: Request, res: Response) => {
|
|||
res.flushHeaders?.();
|
||||
|
||||
// Snapshot senden
|
||||
try { res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive) })}\n\n`); } catch {}
|
||||
try {
|
||||
res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive), selected: persistedState.selectedChannels ?? {} })}\n\n`);
|
||||
} catch {}
|
||||
|
||||
// Ping, damit Proxies die Verbindung offen halten
|
||||
const ping = setInterval(() => { try { res.write(':\n\n'); } catch {} }, 15_000);
|
||||
|
|
@ -1028,6 +1131,8 @@ app.post('/api/play-url', async (req: Request, res: Response) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Upload endpoint removed (build reverted)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
108
web/src/App.tsx
108
web/src/App.tsx
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop, subscribeEvents } from './api';
|
||||
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';
|
||||
|
||||
|
|
@ -13,7 +13,9 @@ export default function App() {
|
|||
const [activeCategoryId, setActiveCategoryId] = useState<string>('');
|
||||
const [channels, setChannels] = useState<VoiceChannelInfo[]>([]);
|
||||
const [query, setQuery] = useState('');
|
||||
const [fuzzy, setFuzzy] = useState<boolean>(false);
|
||||
const [selected, setSelected] = useState<string>('');
|
||||
const selectedRef = useRef<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<string | null>(null);
|
||||
|
|
@ -54,18 +56,24 @@ export default function App() {
|
|||
const chaosTimeoutRef = useRef<number | null>(null);
|
||||
const chaosModeRef = useRef<boolean>(false);
|
||||
useEffect(() => { chaosModeRef.current = chaosMode; }, [chaosMode]);
|
||||
useEffect(() => { selectedRef.current = selected; }, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const c = await fetchChannels();
|
||||
const [c, selectedMap] = await Promise.all([fetchChannels(), getSelectedChannels()]);
|
||||
setChannels(c);
|
||||
const stored = localStorage.getItem('selectedChannel');
|
||||
if (stored && c.find(x => `${x.guildId}:${x.channelId}` === stored)) {
|
||||
setSelected(stored);
|
||||
} else if (c[0]) {
|
||||
setSelected(`${c[0].guildId}:${c[0].channelId}`);
|
||||
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) {
|
||||
setError(e?.message || 'Fehler beim Laden der Channels');
|
||||
}
|
||||
|
|
@ -89,6 +97,25 @@ export default function App() {
|
|||
});
|
||||
} 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 {}
|
||||
} 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 {}
|
||||
}
|
||||
});
|
||||
return () => { try { unsub(); } catch {} };
|
||||
|
|
@ -113,7 +140,7 @@ export default function App() {
|
|||
(async () => {
|
||||
try {
|
||||
const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder;
|
||||
const s = await fetchSounds(query, folderParam, activeCategoryId || undefined);
|
||||
const s = await fetchSounds(query, folderParam, activeCategoryId || undefined, fuzzy);
|
||||
setSounds(s.items);
|
||||
setTotal(s.total);
|
||||
setFolders(s.folders);
|
||||
|
|
@ -121,7 +148,7 @@ export default function App() {
|
|||
setError(e?.message || 'Fehler beim Laden der Sounds');
|
||||
}
|
||||
})();
|
||||
}, [activeFolder, query, activeCategoryId]);
|
||||
}, [activeFolder, query, activeCategoryId, fuzzy]);
|
||||
|
||||
// Favoriten aus Cookie laden
|
||||
useEffect(() => {
|
||||
|
|
@ -186,11 +213,8 @@ export default function App() {
|
|||
})();
|
||||
}, [selected]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return sounds;
|
||||
return sounds.filter((s) => s.name.toLowerCase().includes(q));
|
||||
}, [sounds, query]);
|
||||
// Server liefert bereits gefilterte (und ggf. fuzzy-sortierte) Ergebnisse
|
||||
const filtered = sounds;
|
||||
|
||||
const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]);
|
||||
|
||||
|
|
@ -303,10 +327,9 @@ export default function App() {
|
|||
<div className="flex items-center">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold">
|
||||
Jukebox 420
|
||||
{import.meta.env.VITE_BUILD_CHANNEL === 'nightly' && (
|
||||
Jukebox 420
|
||||
{import.meta.env.VITE_BUILD_CHANNEL === 'nightly' && (
|
||||
<div className="text-sm font-normal mt-1 opacity-70">
|
||||
v{import.meta.env.VITE_APP_VERSION || '1.1.0'}
|
||||
<span className="ml-2" style={{ color: '#ff4d4f' }}>• Nightly</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -344,12 +367,29 @@ export default function App() {
|
|||
|
||||
<div className="control-panel rounded-xl p-6 mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-center">
|
||||
<div className="relative">
|
||||
<input className="input-field pl-10 with-left-icon" placeholder="Nach Sounds suchen..." value={query} onChange={(e)=>setQuery(e.target.value)} />
|
||||
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>search</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className={`font-bold p-3 rounded-lg transition duration-300 ${fuzzy ? 'bg-green-600 hover:bg-green-500 text-white' : 'bg-gray-700 hover:bg-gray-600 text-white'}`}
|
||||
onClick={() => setFuzzy((v) => !v)}
|
||||
title="Fuzzy-Suche umschalten"
|
||||
aria-label="Fuzzy-Suche umschalten"
|
||||
aria-pressed={fuzzy}
|
||||
>
|
||||
<span className="material-icons" aria-hidden="true">blur_on</span>
|
||||
</button>
|
||||
<div className="relative flex-1">
|
||||
<input className="input-field pl-10 with-left-icon w-full" placeholder="Nach Sounds suchen..." value={query} onChange={(e)=>setQuery(e.target.value)} />
|
||||
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>search</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<CustomSelect channels={channels} value={selected} onChange={setSelected} />
|
||||
<CustomSelect channels={channels} value={selected} onChange={async (v)=>{
|
||||
setSelected(v);
|
||||
try {
|
||||
const [gid, cid] = v.split(':');
|
||||
await setSelectedChannel(gid, cid);
|
||||
} catch (e) { /* noop */ }
|
||||
}} />
|
||||
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>folder_special</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
|
|
@ -381,10 +421,10 @@ export default function App() {
|
|||
</div>
|
||||
<div className="flex items-center space-x-3 lg:col-span-2">
|
||||
<div className="relative flex-grow">
|
||||
<select className="input-field appearance-none pl-10" value={theme} onChange={(e)=>setTheme(e.target.value)}>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="rainbow">Rainbow</option>
|
||||
<option value="420">420</option>
|
||||
<select title="Theme Auswahl" className="input-field appearance-none pl-10" value={theme} onChange={(e)=>setTheme(e.target.value)}>
|
||||
<option value="dark">Theme: Dark</option>
|
||||
<option value="rainbow">Theme: Rainbow</option>
|
||||
<option value="420">Theme: 420</option>
|
||||
</select>
|
||||
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>palette</span>
|
||||
<span className="material-icons absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" style={{color:'var(--text-secondary)'}}>unfold_more</span>
|
||||
|
|
@ -443,7 +483,7 @@ export default function App() {
|
|||
const toDelete = Object.entries(selectedSet).filter(([,v])=>v).map(([k])=>k);
|
||||
await adminDelete(toDelete);
|
||||
clearSelection();
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder);
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined, fuzzy);
|
||||
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
|
||||
} catch (e:any) { setError(e?.message||'Löschen fehlgeschlagen'); }
|
||||
}}
|
||||
|
|
@ -458,7 +498,7 @@ export default function App() {
|
|||
try {
|
||||
await adminRename(from, newName);
|
||||
clearSelection();
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder);
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined, fuzzy);
|
||||
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
|
||||
} catch (e:any) { setError(e?.message||'Umbenennen fehlgeschlagen'); }
|
||||
}} />
|
||||
|
|
@ -478,7 +518,7 @@ export default function App() {
|
|||
if(!assignCategoryId){ setError('Bitte Kategorie wählen'); return; }
|
||||
await assignCategories(files, [assignCategoryId], []);
|
||||
setInfo('Kategorie zugewiesen'); setError(null);
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined);
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined, fuzzy);
|
||||
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
|
||||
}catch(e:any){ setError(e?.message||'Zuweisung fehlgeschlagen'); setInfo(null); }
|
||||
}}
|
||||
|
|
@ -493,7 +533,7 @@ export default function App() {
|
|||
const files = Object.entries(selectedSet).filter(([,v])=>v).map(([k])=>k);
|
||||
await clearBadges(files);
|
||||
setInfo('Alle Custom-Badges entfernt'); setError(null);
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined);
|
||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined, fuzzy);
|
||||
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
|
||||
}catch(err:any){ setError(err?.message||'Badge-Entfernung fehlgeschlagen'); setInfo(null); }
|
||||
}}
|
||||
|
|
@ -668,8 +708,14 @@ function CustomSelect({ channels, value, onChange }: SelectProps) {
|
|||
|
||||
return (
|
||||
<div className="control select custom-select" ref={ref}>
|
||||
<button ref={triggerRef} type="button" className="select-trigger" onClick={() => setOpen(v => !v)}>
|
||||
{current ? `${current.guildName} – ${current.channelName}` : 'Channel wählen'}
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
className="select-trigger"
|
||||
onClick={() => setOpen(v => !v)}
|
||||
title={current ? `Channel: ${current.guildName} – ${current.channelName}` : 'Channel wählen'}
|
||||
>
|
||||
{current ? `Channel: ${current.guildName} – ${current.channelName}` : 'Channel wählen'}
|
||||
<span className="chev">▾</span>
|
||||
</button>
|
||||
{open && typeof document !== 'undefined' && ReactDOM.createPortal(
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import type { Sound, SoundsResponse, VoiceChannelInfo } from './types';
|
|||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
|
||||
export async function fetchSounds(q?: string, folderKey?: string, categoryId?: string): Promise<SoundsResponse> {
|
||||
export async function fetchSounds(q?: string, folderKey?: string, categoryId?: string, fuzzy?: boolean): Promise<SoundsResponse> {
|
||||
const url = new URL(`${API_BASE}/sounds`, window.location.origin);
|
||||
if (q) url.searchParams.set('q', q);
|
||||
if (folderKey !== undefined) url.searchParams.set('folder', folderKey);
|
||||
if (categoryId) url.searchParams.set('categoryId', categoryId);
|
||||
if (typeof fuzzy === 'boolean') url.searchParams.set('fuzzy', fuzzy ? '1' : '0');
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) throw new Error('Fehler beim Laden der Sounds');
|
||||
return res.json();
|
||||
|
|
@ -78,6 +79,21 @@ export async function fetchChannels(): Promise<VoiceChannelInfo[]> {
|
|||
return res.json();
|
||||
}
|
||||
|
||||
export async function getSelectedChannels(): Promise<Record<string, string>> {
|
||||
const res = await fetch(`${API_BASE}/selected-channels`);
|
||||
if (!res.ok) throw new Error('Fehler beim Laden der Channel-Auswahl');
|
||||
const data = await res.json();
|
||||
return data?.selected || {};
|
||||
}
|
||||
|
||||
export async function setSelectedChannel(guildId: string, channelId: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/selected-channel`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ guildId, channelId })
|
||||
});
|
||||
if (!res.ok) throw new Error('Channel-Auswahl setzen fehlgeschlagen');
|
||||
}
|
||||
|
||||
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',
|
||||
|
|
@ -184,6 +200,8 @@ export async function playUrl(url: string, guildId: string, channelId: string, v
|
|||
}
|
||||
}
|
||||
|
||||
// uploadFile removed (build reverted)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export type VoiceChannelInfo = {
|
|||
guildName: string;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
export type Category = { id: string; name: string; color?: string; sort?: number };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue