diff --git a/src/App.tsx b/src/App.tsx index 1eff415..a56f014 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,11 @@ import { AuthProvider } from '@/components/AuthProvider' import { AccountPage } from '@/pages/AccountPage' import { ActivityPage } from '@/pages/ActivityPage' import { AdminUsersPage } from '@/pages/AdminUsersPage' +import AdminSoundsPage from '@/pages/AdminSoundsPage' import { DashboardPage } from '@/pages/DashboardPage' import { LoginPage } from '@/pages/LoginPage' import { RegisterPage } from '@/pages/RegisterPage' +import SoundboardPage from '@/pages/SoundboardPage' import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router' import { ThemeProvider } from './components/ThemeProvider' @@ -60,6 +62,19 @@ function App() { } /> + + + + + + } + /> } /> + + + + + + } + /> } /> } /> diff --git a/src/components/sidebar/AppSidebar.tsx b/src/components/sidebar/AppSidebar.tsx index 6fe291d..583b669 100644 --- a/src/components/sidebar/AppSidebar.tsx +++ b/src/components/sidebar/AppSidebar.tsx @@ -9,7 +9,7 @@ import { useSidebar, } from '@/components/ui/sidebar' import { useAuth } from '@/hooks/use-auth' -import { Activity, Home, Users } from 'lucide-react' +import { Activity, Home, Users, Volume2, Settings } from 'lucide-react' import { Link, useLocation } from 'react-router' import { NavUser } from './NavUser' @@ -19,6 +19,11 @@ const navigationItems = [ href: '/dashboard', icon: Home, }, + { + title: 'Soundboard', + href: '/soundboard', + icon: Volume2, + }, { title: 'Activity', href: '/activity', @@ -32,6 +37,11 @@ const adminNavigationItems = [ href: '/admin/users', icon: Users, }, + { + title: 'Sounds', + href: '/admin/sounds', + icon: Settings, + }, ] export function AppSidebar() { diff --git a/src/pages/AdminSoundsPage.tsx b/src/pages/AdminSoundsPage.tsx new file mode 100644 index 0000000..dbde6c2 --- /dev/null +++ b/src/pages/AdminSoundsPage.tsx @@ -0,0 +1,388 @@ +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { + RefreshCw, + Volume2, + Trash2, + CheckCircle, + XCircle, + AlertCircle, + Database, + Zap +} from 'lucide-react'; +import { apiService } from '@/services/api'; + +interface Sound { + id: number; + name: string; + filename: string; + type: string; + duration: number; + size: number; + play_count: number; + is_normalized: boolean; + normalized_filename?: string; + original_exists: boolean; + normalized_exists: boolean; + created_at: string; +} + +interface ScanStats { + total_sounds: number; + soundboard_sounds: number; + music_sounds: number; + total_size_bytes: number; + total_duration: number; + total_plays: number; +} + +interface NormalizationStats { + total_sounds: number; + normalized_count: number; + normalization_percentage: number; + total_original_size: number; + total_normalized_size: number; + size_difference: number; +} + +export default function AdminSoundsPage() { + const [sounds, setSounds] = useState([]); + const [scanStats, setScanStats] = useState(null); + const [normalizationStats, setNormalizationStats] = useState(null); + const [loading, setLoading] = useState(false); + const [scanning, setScanning] = useState(false); + const [normalizing, setNormalizing] = useState(false); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + useEffect(() => { + fetchData(); + }, [page]); + + const fetchData = async () => { + await Promise.all([ + fetchSounds(), + fetchScanStats(), + fetchNormalizationStats() + ]); + }; + + const fetchSounds = async () => { + try { + setLoading(true); + const response = await apiService.get(`/api/admin/sounds/list?page=${page}&per_page=20`); + const data = await response.json(); + setSounds(data.sounds || []); + setTotalPages(data.pagination?.pages || 1); + } catch (err) { + setError('Failed to load sounds'); + console.error('Error fetching sounds:', err); + } finally { + setLoading(false); + } + }; + + const fetchScanStats = async () => { + try { + const response = await apiService.get('/api/admin/sounds/scan/status'); + const data = await response.json(); + setScanStats(data); + } catch (err) { + console.error('Error fetching scan stats:', err); + } + }; + + const fetchNormalizationStats = async () => { + try { + const response = await apiService.get('/api/admin/sounds/normalize/status'); + const data = await response.json(); + setNormalizationStats(data); + } catch (err) { + console.error('Error fetching normalization stats:', err); + } + }; + + const handleScanSounds = async () => { + try { + setScanning(true); + setError(null); + const response = await apiService.post('/api/admin/sounds/scan'); + const data = await response.json(); + + if (response.ok) { + alert(`Scan completed: ${data.files_added} new sounds added, ${data.files_skipped} skipped`); + await fetchData(); + } else { + setError(data.error || 'Scan failed'); + } + } catch (err) { + setError('Failed to scan sounds'); + console.error('Error scanning sounds:', err); + } finally { + setScanning(false); + } + }; + + const handleNormalizeAll = async () => { + try { + setNormalizing(true); + setError(null); + const response = await apiService.post('/api/admin/sounds/normalize', { + overwrite: false, + two_pass: true + }); + const data = await response.json(); + + if (response.ok) { + alert(`Normalization completed: ${data.successful} successful, ${data.failed} failed, ${data.skipped} skipped`); + await fetchData(); + } else { + setError(data.error || 'Normalization failed'); + } + } catch (err) { + setError('Failed to normalize sounds'); + console.error('Error normalizing sounds:', err); + } finally { + setNormalizing(false); + } + }; + + const handleNormalizeSound = async (soundId: number) => { + try { + const response = await apiService.post(`/api/admin/sounds/${soundId}/normalize`, { + overwrite: false, + two_pass: true + }); + const data = await response.json(); + + if (response.ok) { + alert(`Sound normalized successfully`); + await fetchData(); + } else { + alert(`Normalization failed: ${data.error}`); + } + } catch (err) { + alert('Failed to normalize sound'); + console.error('Error normalizing sound:', err); + } + }; + + const handleDeleteSound = async (soundId: number, soundName: string) => { + if (!confirm(`Are you sure you want to delete "${soundName}"?`)) { + return; + } + + try { + const response = await apiService.delete(`/api/admin/sounds/${soundId}`); + const data = await response.json(); + + if (response.ok) { + alert('Sound deleted successfully'); + await fetchData(); + } else { + alert(`Delete failed: ${data.error}`); + } + } catch (err) { + alert('Failed to delete sound'); + console.error('Error deleting sound:', err); + } + }; + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + }; + + const formatDuration = (ms: number) => { + const seconds = Math.floor(ms / 1000); + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + return ( +
+
+

Sound Management

+
+ + +
+
+ + {error && ( +
+ {error} +
+ )} + + {/* Statistics Cards */} +
+ {scanStats && ( + + + Total Sounds + + + +
{scanStats.total_sounds}
+

+ {scanStats.soundboard_sounds} soundboard, {scanStats.music_sounds} music +

+
+
+ )} + + {normalizationStats && ( + + + Normalized + + + +
{normalizationStats.normalized_count}
+

+ {normalizationStats.normalization_percentage.toFixed(1)}% of total sounds +

+
+
+ )} + + {scanStats && ( + + + Total Size + + + +
{formatFileSize(scanStats.total_size_bytes)}
+

+ {scanStats.total_plays} total plays +

+
+
+ )} +
+ + {/* Sounds List */} + + + Sounds ({sounds.length}) + + + {loading ? ( +
+ +
+ ) : ( +
+ {sounds.map((sound) => ( +
+
+
+

{sound.name}

+
+ {sound.original_exists ? ( +
+ +
+ ) : ( +
+ +
+ )} + {sound.is_normalized ? ( + sound.normalized_exists ? ( +
+ +
+ ) : ( +
+ +
+ ) + ) : ( +
+ +
+ )} +
+
+
+ {sound.filename} • {formatFileSize(sound.size)} • {formatDuration(sound.duration)} • {sound.play_count} plays +
+
+
+ {!sound.is_normalized && ( + + )} + +
+
+ ))} +
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+ + + Page {page} of {totalPages} + + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/SoundboardPage.tsx b/src/pages/SoundboardPage.tsx new file mode 100644 index 0000000..708d6ca --- /dev/null +++ b/src/pages/SoundboardPage.tsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Play, Square, Volume2 } from 'lucide-react'; +import { apiService } from '@/services/api'; + +interface Sound { + id: number; + name: string; + filename: string; + type: string; + duration: number; + play_count: number; + is_normalized: boolean; + normalized_filename?: string; +} + +interface SoundCardProps { + sound: Sound; + onPlay: (soundId: number) => void; + isPlaying: boolean; +} + +const SoundCard: React.FC = ({ sound, onPlay, isPlaying }) => { + const handlePlay = () => { + onPlay(sound.id); + }; + + const formatDuration = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + return ( + + + + {sound.name} + + + +
+
+ + {formatDuration(sound.duration)} +
+
+ {sound.play_count} plays +
+
+ + {sound.is_normalized && ( +
+ Normalized +
+ )} +
+
+ ); +}; + +export default function SoundboardPage() { + const [sounds, setSounds] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [playingSound, setPlayingSound] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + fetchSounds(); + }, []); + + const fetchSounds = async () => { + try { + setLoading(true); + const response = await apiService.get('/api/soundboard/sounds?type=SDB'); + const data = await response.json(); + setSounds(data.sounds || []); + } catch (err) { + setError('Failed to load sounds'); + console.error('Error fetching sounds:', err); + } finally { + setLoading(false); + } + }; + + const handlePlaySound = async (soundId: number) => { + try { + setPlayingSound(soundId); + await apiService.post(`/api/soundboard/sounds/${soundId}/play`); + + // Reset playing state after a short delay + setTimeout(() => { + setPlayingSound(null); + }, 1000); + } catch (err) { + setError('Failed to play sound'); + console.error('Error playing sound:', err); + setPlayingSound(null); + } + }; + + const handleStopAll = async () => { + try { + await apiService.post('/api/soundboard/stop-all'); + setPlayingSound(null); + } catch (err) { + setError('Failed to stop sounds'); + console.error('Error stopping sounds:', err); + } + }; + + const handleForceStopAll = async () => { + try { + const response = await apiService.post('/api/soundboard/force-stop'); + const data = await response.json(); + setPlayingSound(null); + alert(`Force stopped ${data.stopped_count} sound instances`); + } catch (err) { + setError('Failed to force stop sounds'); + console.error('Error force stopping sounds:', err); + } + }; + + const filteredSounds = sounds.filter(sound => + sound.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + if (loading) { + return ( +
+
Loading sounds...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + return ( +
+
+

Soundboard

+
+ + +
+
+ +
+
+ setSearchTerm(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ {filteredSounds.length} of {sounds.length} sounds +
+
+ +
+ {filteredSounds.map((sound) => ( + + ))} +
+ + {filteredSounds.length === 0 && ( +
+
+ {searchTerm ? 'No sounds found matching your search.' : 'No sounds available.'} +
+
+ )} +
+ ); +} \ No newline at end of file