feat(channel): Serverweite Channel-Auswahl mit Persistenz und SSE-Broadcast; Frontend passt Auswahl global an
This commit is contained in:
parent
fdf0dea3e6
commit
e83954624c
4 changed files with 111 additions and 14 deletions
|
|
@ -54,6 +54,7 @@ type PersistedState = {
|
||||||
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)
|
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
|
// 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');
|
||||||
|
|
@ -72,7 +73,8 @@ function readPersistedState(): PersistedState {
|
||||||
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 ?? {}
|
fileBadges: parsed.fileBadges ?? {},
|
||||||
|
selectedChannels: parsed.selectedChannels ?? {}
|
||||||
} as PersistedState;
|
} as PersistedState;
|
||||||
}
|
}
|
||||||
// 2) Fallback: alten Speicherort lesen und sofort nach NEW migrieren
|
// 2) Fallback: alten Speicherort lesen und sofort nach NEW migrieren
|
||||||
|
|
@ -85,7 +87,8 @@ function readPersistedState(): PersistedState {
|
||||||
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 ?? {}
|
fileBadges: parsed.fileBadges ?? {},
|
||||||
|
selectedChannels: parsed.selectedChannels ?? {}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(STATE_FILE_NEW), { recursive: true });
|
fs.mkdirSync(path.dirname(STATE_FILE_NEW), { recursive: true });
|
||||||
|
|
@ -162,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> {
|
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise<void> {
|
||||||
const guild = client.guilds.cache.get(guildId);
|
const guild = client.guilds.cache.get(guildId);
|
||||||
if (!guild) throw new Error('Guild nicht gefunden');
|
if (!guild) throw new Error('Guild nicht gefunden');
|
||||||
|
|
@ -727,13 +747,14 @@ 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' });
|
||||||
|
|
||||||
const allowed = new Set(ALLOWED_GUILD_IDS);
|
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) {
|
for (const [, guild] of client.guilds.cache) {
|
||||||
if (allowed.size > 0 && !allowed.has(guild.id)) continue;
|
if (allowed.size > 0 && !allowed.has(guild.id)) continue;
|
||||||
const channels = guild.channels.cache;
|
const channels = guild.channels.cache;
|
||||||
for (const [, ch] of channels) {
|
for (const [, ch] of channels) {
|
||||||
if (ch?.type === ChannelType.GuildVoice || ch?.type === ChannelType.GuildStageVoice) {
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -741,6 +762,36 @@ app.get('/api/channels', (_req: Request, res: Response) => {
|
||||||
res.json(result);
|
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) => {
|
app.post('/api/play', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { soundName, guildId, channelId, volume, folder, relativePath } = req.body as {
|
const { soundName, guildId, channelId, volume, folder, relativePath } = req.body as {
|
||||||
|
|
@ -1023,7 +1074,9 @@ app.get('/api/events', (req: Request, res: Response) => {
|
||||||
res.flushHeaders?.();
|
res.flushHeaders?.();
|
||||||
|
|
||||||
// Snapshot senden
|
// 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
|
// Ping, damit Proxies die Verbindung offen halten
|
||||||
const ping = setInterval(() => { try { res.write(':\n\n'); } catch {} }, 15_000);
|
const ping = setInterval(() => { try { res.write(':\n\n'); } catch {} }, 15_000);
|
||||||
|
|
|
||||||
|
|
@ -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, 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 type { VoiceChannelInfo, Sound, Category } from './types';
|
||||||
import { getCookie, setCookie } from './cookies';
|
import { getCookie, setCookie } from './cookies';
|
||||||
|
|
||||||
|
|
@ -59,14 +59,19 @@ export default function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const c = await fetchChannels();
|
const [c, selectedMap] = await Promise.all([fetchChannels(), getSelectedChannels()]);
|
||||||
setChannels(c);
|
setChannels(c);
|
||||||
const stored = localStorage.getItem('selectedChannel');
|
let initial = '';
|
||||||
if (stored && c.find(x => `${x.guildId}:${x.channelId}` === stored)) {
|
if (c.length > 0) {
|
||||||
setSelected(stored);
|
const firstGuild = c[0].guildId;
|
||||||
} else if (c[0]) {
|
const serverCid = selectedMap[firstGuild];
|
||||||
setSelected(`${c[0].guildId}:${c[0].channelId}`);
|
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) {
|
} catch (e: any) {
|
||||||
setError(e?.message || 'Fehler beim Laden der Channels');
|
setError(e?.message || 'Fehler beim Laden der Channels');
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +95,23 @@ export default function App() {
|
||||||
});
|
});
|
||||||
} else if (msg?.type === 'snapshot') {
|
} else if (msg?.type === 'snapshot') {
|
||||||
setPartyActiveGuilds(Array.isArray(msg.party) ? msg.party : []);
|
setPartyActiveGuilds(Array.isArray(msg.party) ? msg.party : []);
|
||||||
|
try {
|
||||||
|
const sel = msg?.selected || {};
|
||||||
|
const gid = selected ? selected.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 curGid = selected ? selected.split(':')[0] : '';
|
||||||
|
if (curGid === gid) setSelected(`${gid}:${cid}`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => { try { unsub(); } catch {} };
|
return () => { try { unsub(); } catch {} };
|
||||||
|
|
@ -358,7 +380,13 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<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>
|
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>folder_special</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,21 @@ export async function fetchChannels(): Promise<VoiceChannelInfo[]> {
|
||||||
return res.json();
|
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> {
|
export async function playSound(soundName: string, guildId: string, channelId: string, volume: number, relativePath?: string): Promise<void> {
|
||||||
const res = await fetch(`${API_BASE}/play`, {
|
const res = await fetch(`${API_BASE}/play`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export type VoiceChannelInfo = {
|
||||||
guildName: string;
|
guildName: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
channelName: string;
|
channelName: string;
|
||||||
|
selected?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Category = { id: string; name: string; color?: string; sort?: number };
|
export type Category = { id: string; name: string; color?: string; sort?: number };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue