Add MP3 URL import and analytics widgets

This commit is contained in:
Bot 2026-03-01 18:56:37 +01:00
parent e200087a73
commit 5a41b6a622
5 changed files with 380 additions and 19 deletions

View file

@ -1,11 +1,11 @@
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import {
fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume,
fetchChannels, fetchSounds, fetchAnalytics, playSound, playUrl, setVolumeLive, getVolume,
adminStatus, adminLogin, adminLogout, adminDelete, adminRename,
fetchCategories, partyStart, partyStop, subscribeEvents,
getSelectedChannels, setSelectedChannel,
} from './api';
import type { VoiceChannelInfo, Sound, Category } from './types';
import type { VoiceChannelInfo, Sound, Category, AnalyticsResponse } from './types';
import { getCookie, setCookie } from './cookies';
const THEMES = [
@ -30,11 +30,18 @@ export default function App() {
const [total, setTotal] = useState(0);
const [folders, setFolders] = useState<Array<{ key: string; name: string; count: number }>>([]);
const [categories, setCategories] = useState<Category[]>([]);
const [analytics, setAnalytics] = useState<AnalyticsResponse>({
totalSounds: 0,
totalPlays: 0,
mostPlayed: [],
});
/* ── Navigation ── */
const [activeTab, setActiveTab] = useState<Tab>('all');
const [activeFolder, setActiveFolder] = useState('');
const [query, setQuery] = useState('');
const [importUrl, setImportUrl] = useState('');
const [importBusy, setImportBusy] = useState(false);
/* ── Channels ── */
const [channels, setChannels] = useState<VoiceChannelInfo[]>([]);
@ -83,6 +90,14 @@ export default function App() {
setTimeout(() => setNotification(null), 3000);
}, []);
const soundKey = useCallback((s: Sound) => s.relativePath ?? s.fileName, []);
const isMp3Url = useCallback((value: string) => {
try {
const parsed = new URL(value.trim());
return parsed.pathname.toLowerCase().endsWith('.mp3');
} catch {
return false;
}
}, []);
const guildId = selected ? selected.split(':')[0] : '';
const channelId = selected ? selected.split(':')[1] : '';
@ -199,6 +214,10 @@ export default function App() {
})();
}, [activeTab, activeFolder, query, refreshKey]);
useEffect(() => {
void loadAnalytics();
}, [refreshKey]);
/* ── Favs persistence ── */
useEffect(() => {
const c = getCookie('favs');
@ -232,14 +251,41 @@ export default function App() {
}, [showAdmin, isAdmin]);
/* ── Actions ── */
async function loadAnalytics() {
try {
const data = await fetchAnalytics();
setAnalytics(data);
} catch { }
}
async function handlePlay(s: Sound) {
if (!selected) return notify('Bitte einen Voice-Channel auswählen', 'error');
try {
await playSound(s.name, guildId, channelId, volume, s.relativePath);
setLastPlayed(s.name);
void loadAnalytics();
} catch (e: any) { notify(e?.message || 'Play fehlgeschlagen', 'error'); }
}
async function handleUrlImport() {
const trimmed = importUrl.trim();
if (!trimmed) return notify('Bitte einen MP3-Link eingeben', 'error');
if (!selected) return notify('Bitte einen Voice-Channel auswählen', 'error');
if (!isMp3Url(trimmed)) return notify('Nur direkte MP3-Links sind erlaubt', 'error');
setImportBusy(true);
try {
await playUrl(trimmed, guildId, channelId, volume);
setImportUrl('');
notify('MP3 importiert und abgespielt');
setRefreshKey(k => k + 1);
await loadAnalytics();
} catch (e: any) {
notify(e?.message || 'URL-Import fehlgeschlagen', 'error');
} finally {
setImportBusy(false);
}
}
async function handleStop() {
if (!selected) return;
setLastPlayed('');
@ -410,6 +456,8 @@ export default function App() {
[adminFilteredSounds, adminSelection, soundKey]);
const allVisibleSelected = adminFilteredSounds.length > 0 && selectedVisibleCount === adminFilteredSounds.length;
const analyticsTop = analytics.mostPlayed.slice(0, 3);
const totalSoundsDisplay = analytics.totalSounds || total;
const clockMain = clock.slice(0, 5);
const clockSec = clock.slice(5);
@ -538,6 +586,26 @@ export default function App() {
)}
</div>
<div className="url-import-wrap">
<span className="material-icons url-import-icon">link</span>
<input
className="url-import-input"
type="url"
placeholder="MP3-URL einfügen..."
value={importUrl}
onChange={e => setImportUrl(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') void handleUrlImport(); }}
/>
<button
className="url-import-btn"
onClick={() => { void handleUrlImport(); }}
disabled={importBusy}
title="MP3 importieren"
>
{importBusy ? 'Lädt...' : 'Download'}
</button>
</div>
<div className="toolbar-spacer" />
<div className="volume-control">
@ -612,6 +680,34 @@ export default function App() {
</div>
</div>
<div className="analytics-strip">
<div className="analytics-card">
<span className="material-icons analytics-icon">library_music</span>
<div className="analytics-copy">
<span className="analytics-label">Sounds gesamt</span>
<strong className="analytics-value">{totalSoundsDisplay}</strong>
</div>
</div>
<div className="analytics-card analytics-wide">
<span className="material-icons analytics-icon">leaderboard</span>
<div className="analytics-copy">
<span className="analytics-label">Most Played</span>
<div className="analytics-top-list">
{analyticsTop.length === 0 ? (
<span className="analytics-muted">Noch keine Plays</span>
) : (
analyticsTop.map((item, idx) => (
<span className="analytics-chip" key={item.relativePath}>
{idx + 1}. {item.name} ({item.count})
</span>
))
)}
</div>
</div>
</div>
</div>
{/* ═══ FOLDER CHIPS ═══ */}
{activeTab === 'all' && visibleFolders.length > 0 && (
<div className="category-strip">