diff --git a/server/src/index.ts b/server/src/index.ts index d59b241..a5cdcf0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -52,6 +52,7 @@ type PersistedState = { totalPlays: number; categories?: Category[]; fileCategories?: Record; // relPath or fileName -> categoryIds[] + fileBadges?: Record; // relPath or fileName -> custom badges (emoji/text) }; // Neuer, persistenter Speicherort direkt im Sounds-Volume const STATE_FILE_NEW = path.join(SOUNDS_DIR, 'state.json'); @@ -69,7 +70,8 @@ function readPersistedState(): PersistedState { plays: parsed.plays ?? {}, totalPlays: parsed.totalPlays ?? 0, categories: Array.isArray(parsed.categories) ? parsed.categories : [], - fileCategories: parsed.fileCategories ?? {} + fileCategories: parsed.fileCategories ?? {}, + fileBadges: parsed.fileBadges ?? {} } as PersistedState; } // 2) Fallback: alten Speicherort lesen und sofort nach NEW migrieren @@ -81,7 +83,8 @@ function readPersistedState(): PersistedState { plays: parsed.plays ?? {}, totalPlays: parsed.totalPlays ?? 0, categories: Array.isArray(parsed.categories) ? parsed.categories : [], - fileCategories: parsed.fileCategories ?? {} + fileCategories: parsed.fileCategories ?? {}, + fileBadges: parsed.fileBadges ?? {} }; try { fs.mkdirSync(path.dirname(STATE_FILE_NEW), { recursive: true }); @@ -507,10 +510,17 @@ app.get('/api/sounds', (req: Request, res: Response) => { result = allItems.filter(i => keys.has(i.relativePath ?? i.fileName)); } - const withRecentFlag = result.map((it) => ({ - ...it, - isRecent: recentTop5Set.has(it.relativePath ?? it.fileName) - })); + // Badges vorbereiten (Top3 = Rakete, Recent = New) + const top3Set = new Set(top3.map(t => t.key.split(':')[1])); + const customBadges = persistedState.fileBadges ?? {}; + const withRecentFlag = result.map((it) => { + const key = it.relativePath ?? it.fileName; + const badges: string[] = []; + if (recentTop5Set.has(key)) badges.push('new'); + if (top3Set.has(key)) badges.push('rocket'); + for (const b of (customBadges[key] ?? [])) badges.push(b); + return { ...it, isRecent: recentTop5Set.has(key), badges } as any; + }); res.json({ items: withRecentFlag, total, folders: foldersOut, categories: persistedState.categories ?? [], fileCategories: persistedState.fileCategories ?? {} }); }); @@ -622,6 +632,23 @@ app.post('/api/categories/assign', requireAdmin, (req: Request, res: Response) = res.json({ ok: true, fileCategories: fc }); }); +// Badges (custom) setzen/entfernen (Rakete/Neu kommen automatisch, hier nur freie Badges) +app.post('/api/badges/assign', requireAdmin, (req: Request, res: Response) => { + const { files, add, remove } = req.body as { files?: string[]; add?: string[]; remove?: string[] }; + if (!Array.isArray(files) || files.length === 0) return res.status(400).json({ error: 'files[] erforderlich' }); + const fb = persistedState.fileBadges ?? {}; + for (const rel of files) { + const key = rel; + const old = new Set(fb[key] ?? []); + for (const a of (add ?? [])) old.add(a); + for (const r of (remove ?? [])) old.delete(r); + fb[key] = Array.from(old); + } + persistedState.fileBadges = fb; + writePersistedState(persistedState); + res.json({ ok: true, fileBadges: fb }); +}); + app.get('/api/channels', (_req: Request, res: Response) => { if (!client.isReady()) return res.status(503).json({ error: 'Bot noch nicht bereit' }); diff --git a/web/src/App.tsx b/web/src/App.tsx index 97c82d5..6476596 100644 --- a/web/src/App.tsx +++ b/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 } from './api'; +import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, assignBadges } from './api'; import type { VoiceChannelInfo, Sound, Category } from './types'; import { getCookie, setCookie } from './cookies'; @@ -438,6 +438,21 @@ export default function App() { }catch(e:any){ setError(e?.message||'Zuweisung fehlgeschlagen'); setInfo(null); } }} >Zu Kategorie + + {/* Custom Badge setzen */} + )} @@ -510,7 +525,12 @@ export default function App() { /> )}
handlePlay(s.name, s.relativePath)}> - {s.name} + + {s.name} + {Array.isArray((s as any).badges) && (s as any).badges!.map((b:string, i:number)=> ( + {b==='new'?'🆕': b==='rocket'?'🚀': b} + ))} +
diff --git a/web/src/api.ts b/web/src/api.ts index b31a51f..e9968a7 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -37,6 +37,15 @@ export async function assignCategories(files: string[], add: string[], remove: s return res.json(); } +export async function assignBadges(files: string[], add: string[], remove: string[] = []) { + const res = await fetch(`${API_BASE}/badges/assign`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', + body: JSON.stringify({ files, add, remove }) + }); + if (!res.ok) throw new Error('Badges-Update fehlgeschlagen'); + return res.json(); +} + export async function fetchChannels(): Promise { const res = await fetch(`${API_BASE}/channels`); if (!res.ok) throw new Error('Fehler beim Laden der Channels'); diff --git a/web/src/types.ts b/web/src/types.ts index 757daac..40ab488 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -4,6 +4,7 @@ export type Sound = { folder?: string; relativePath?: string; isRecent?: boolean; + badges?: string[]; }; export type SoundsResponse = {