Nightly: Badges für Sounds (auto /, plus Custom via Admin), Kategorien-UI erweitertet (anlegen/zuweisen)
This commit is contained in:
parent
56351fd547
commit
8795657f69
4 changed files with 65 additions and 8 deletions
|
|
@ -52,6 +52,7 @@ type PersistedState = {
|
||||||
totalPlays: number;
|
totalPlays: number;
|
||||||
categories?: Category[];
|
categories?: Category[];
|
||||||
fileCategories?: Record<string, string[]>; // relPath or fileName -> categoryIds[]
|
fileCategories?: Record<string, string[]>; // relPath or fileName -> categoryIds[]
|
||||||
|
fileBadges?: Record<string, string[]>; // relPath or fileName -> custom badges (emoji/text)
|
||||||
};
|
};
|
||||||
// Neuer, persistenter Speicherort direkt im Sounds-Volume
|
// Neuer, persistenter Speicherort direkt im Sounds-Volume
|
||||||
const STATE_FILE_NEW = path.join(SOUNDS_DIR, 'state.json');
|
const STATE_FILE_NEW = path.join(SOUNDS_DIR, 'state.json');
|
||||||
|
|
@ -69,7 +70,8 @@ function readPersistedState(): PersistedState {
|
||||||
plays: parsed.plays ?? {},
|
plays: parsed.plays ?? {},
|
||||||
totalPlays: parsed.totalPlays ?? 0,
|
totalPlays: parsed.totalPlays ?? 0,
|
||||||
categories: Array.isArray(parsed.categories) ? parsed.categories : [],
|
categories: Array.isArray(parsed.categories) ? parsed.categories : [],
|
||||||
fileCategories: parsed.fileCategories ?? {}
|
fileCategories: parsed.fileCategories ?? {},
|
||||||
|
fileBadges: parsed.fileBadges ?? {}
|
||||||
} as PersistedState;
|
} as PersistedState;
|
||||||
}
|
}
|
||||||
// 2) Fallback: alten Speicherort lesen und sofort nach NEW migrieren
|
// 2) Fallback: alten Speicherort lesen und sofort nach NEW migrieren
|
||||||
|
|
@ -81,7 +83,8 @@ function readPersistedState(): PersistedState {
|
||||||
plays: parsed.plays ?? {},
|
plays: parsed.plays ?? {},
|
||||||
totalPlays: parsed.totalPlays ?? 0,
|
totalPlays: parsed.totalPlays ?? 0,
|
||||||
categories: Array.isArray(parsed.categories) ? parsed.categories : [],
|
categories: Array.isArray(parsed.categories) ? parsed.categories : [],
|
||||||
fileCategories: parsed.fileCategories ?? {}
|
fileCategories: parsed.fileCategories ?? {},
|
||||||
|
fileBadges: parsed.fileBadges ?? {}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(STATE_FILE_NEW), { recursive: true });
|
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));
|
result = allItems.filter(i => keys.has(i.relativePath ?? i.fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
const withRecentFlag = result.map((it) => ({
|
// Badges vorbereiten (Top3 = Rakete, Recent = New)
|
||||||
...it,
|
const top3Set = new Set(top3.map(t => t.key.split(':')[1]));
|
||||||
isRecent: recentTop5Set.has(it.relativePath ?? it.fileName)
|
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 ?? {} });
|
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 });
|
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) => {
|
app.get('/api/channels', (_req: Request, res: Response) => {
|
||||||
if (!client.isReady()) return res.status(503).json({ error: 'Bot noch nicht bereit' });
|
if (!client.isReady()) return res.status(503).json({ error: 'Bot noch nicht bereit' });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
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 type { VoiceChannelInfo, Sound, Category } from './types';
|
||||||
import { getCookie, setCookie } from './cookies';
|
import { getCookie, setCookie } from './cookies';
|
||||||
|
|
||||||
|
|
@ -438,6 +438,21 @@ export default function App() {
|
||||||
}catch(e:any){ setError(e?.message||'Zuweisung fehlgeschlagen'); setInfo(null); }
|
}catch(e:any){ setError(e?.message||'Zuweisung fehlgeschlagen'); setInfo(null); }
|
||||||
}}
|
}}
|
||||||
>Zu Kategorie</button>
|
>Zu Kategorie</button>
|
||||||
|
|
||||||
|
{/* Custom Badge setzen */}
|
||||||
|
<button
|
||||||
|
className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300"
|
||||||
|
onClick={async ()=>{
|
||||||
|
try{
|
||||||
|
const files = Object.entries(selectedSet).filter(([,v])=>v).map(([k])=>k);
|
||||||
|
// Beispiel: Herz-Emoji als Badge; später UI-Eingabe möglich
|
||||||
|
await assignBadges(files, ['❤'], []);
|
||||||
|
setInfo('Badge gesetzt'); setError(null);
|
||||||
|
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined);
|
||||||
|
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
|
||||||
|
}catch(e:any){ setError(e?.message||'Badge-Update fehlgeschlagen'); setInfo(null); }
|
||||||
|
}}
|
||||||
|
>Badge ❤</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -510,7 +525,12 @@ export default function App() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="sound-btn group rounded-xl flex items-center justify-between p-3 cursor-pointer" onClick={()=>handlePlay(s.name, s.relativePath)}>
|
<div className="sound-btn group rounded-xl flex items-center justify-between p-3 cursor-pointer" onClick={()=>handlePlay(s.name, s.relativePath)}>
|
||||||
<span className="text-sm font-medium truncate pr-2">{s.name}</span>
|
<span className="text-sm font-medium truncate pr-2">
|
||||||
|
{s.name}
|
||||||
|
{Array.isArray((s as any).badges) && (s as any).badges!.map((b:string, i:number)=> (
|
||||||
|
<span key={i} style={{ marginLeft: 6, opacity:.9 }}>{b==='new'?'🆕': b==='rocket'?'🚀': b}</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button className="text-gray-400 hover:text-[var(--accent-blue)]" onClick={(e)=>{e.stopPropagation(); setFavs(prev=>({ ...prev, [key]: !prev[key] }));}}><span className="material-icons text-xl">{isFav?'star':'star_border'}</span></button>
|
<button className="text-gray-400 hover:text-[var(--accent-blue)]" onClick={(e)=>{e.stopPropagation(); setFavs(prev=>({ ...prev, [key]: !prev[key] }));}}><span className="material-icons text-xl">{isFav?'star':'star_border'}</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,15 @@ export async function assignCategories(files: string[], add: string[], remove: s
|
||||||
return res.json();
|
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<VoiceChannelInfo[]> {
|
export async function fetchChannels(): Promise<VoiceChannelInfo[]> {
|
||||||
const res = await fetch(`${API_BASE}/channels`);
|
const res = await fetch(`${API_BASE}/channels`);
|
||||||
if (!res.ok) throw new Error('Fehler beim Laden der Channels');
|
if (!res.ok) throw new Error('Fehler beim Laden der Channels');
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export type Sound = {
|
||||||
folder?: string;
|
folder?: string;
|
||||||
relativePath?: string;
|
relativePath?: string;
|
||||||
isRecent?: boolean;
|
isRecent?: boolean;
|
||||||
|
badges?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SoundsResponse = {
|
export type SoundsResponse = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue