feat: enhance SoundboardPage with dynamic color themes and improved sound card display
Some checks failed
Frontend CI / lint (push) Failing after 5m9s
Frontend CI / build (push) Has been skipped

This commit is contained in:
JSC
2025-07-08 16:34:37 +02:00
parent ed767485f2
commit 9396510075

View File

@@ -1,12 +1,46 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Play, Square, Volume2, Plus } from 'lucide-react'; import { Play, Square, Volume2, Plus, Clock, Weight } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { apiService } from '@/services/api'; import { apiService } from '@/services/api';
import { AddUrlDialog } from '@/components/AddUrlDialog'; import { AddUrlDialog } from '@/components/AddUrlDialog';
import { useAddUrlShortcut } from '@/hooks/use-keyboard-shortcuts'; import { useAddUrlShortcut } from '@/hooks/use-keyboard-shortcuts';
import { formatDuration } from '@/lib/format-duration'; import { formatDuration } from '@/lib/format-duration';
import { cn } from '@/lib/utils';
import NumberFlow from '@number-flow/react';
import { formatSize } from '@/lib/format-size';
import { useTheme } from 'next-themes';
const lightModeColors = [
'bg-red-600/30 hover:bg-red-600/40 text-red-900 border-red-600/20',
'bg-blue-700/30 hover:bg-blue-700/40 text-blue-900 border-blue-700/20',
'bg-yellow-400/30 hover:bg-yellow-400/40 text-yellow-800 border-yellow-400/20',
'bg-purple-700/30 hover:bg-purple-700/40 text-purple-900 border-purple-700/20',
'bg-green-600/30 hover:bg-green-600/40 text-green-900 border-green-600/20',
'bg-pink-500/30 hover:bg-pink-500/40 text-pink-900 border-pink-500/20',
'bg-cyan-500/30 hover:bg-cyan-500/40 text-cyan-900 border-cyan-500/20',
'bg-amber-500/30 hover:bg-amber-500/40 text-amber-900 border-amber-500/20',
'bg-indigo-800/30 hover:bg-indigo-800/40 text-indigo-900 border-indigo-800/20',
'bg-lime-500/30 hover:bg-lime-500/40 text-lime-900 border-lime-500/20',
'bg-fuchsia-600/30 hover:bg-fuchsia-600/40 text-fuchsia-900 border-fuchsia-600/20',
'bg-orange-600/30 hover:bg-orange-600/40 text-orange-900 border-orange-600/20',
]
const darkModeColors = [
'bg-red-700/40 hover:bg-red-700/50 text-red-100 border border-red-500/50',
'bg-blue-800/40 hover:bg-blue-800/50 text-blue-100 border border-blue-600/50',
'bg-yellow-600/40 hover:bg-yellow-600/50 text-yellow-100 border border-yellow-400/50',
'bg-purple-800/40 hover:bg-purple-800/50 text-purple-100 border border-purple-600/50',
'bg-green-700/40 hover:bg-green-700/50 text-green-100 border border-green-500/50',
'bg-pink-700/40 hover:bg-pink-700/50 text-pink-100 border border-pink-500/50',
'bg-cyan-700/40 hover:bg-cyan-700/50 text-cyan-100 border border-cyan-500/50',
'bg-amber-700/40 hover:bg-amber-700/50 text-amber-100 border border-amber-500/50',
'bg-indigo-900/40 hover:bg-indigo-900/50 text-indigo-100 border border-indigo-700/50',
'bg-lime-700/40 hover:bg-lime-700/50 text-lime-100 border border-lime-500/50',
'bg-fuchsia-800/40 hover:bg-fuchsia-800/50 text-fuchsia-100 border border-fuchsia-600/50',
'bg-orange-700/40 hover:bg-orange-700/50 text-orange-100 border border-orange-500/50',
]
interface Sound { interface Sound {
id: number; id: number;
@@ -14,6 +48,7 @@ interface Sound {
filename: string; filename: string;
type: string; type: string;
duration: number; duration: number;
size: number;
play_count: number; play_count: number;
is_normalized: boolean; is_normalized: boolean;
normalized_filename?: string; normalized_filename?: string;
@@ -22,45 +57,38 @@ interface Sound {
interface SoundCardProps { interface SoundCardProps {
sound: Sound; sound: Sound;
onPlay: (soundId: number) => void; onPlay: (soundId: number) => void;
isPlaying: boolean; colorClasses: string;
} }
const SoundCard: React.FC<SoundCardProps> = ({ sound, onPlay, isPlaying }) => { const SoundCard: React.FC<SoundCardProps> = ({ sound, onPlay, colorClasses }) => {
const handlePlay = () => { const handlePlay = () => {
onPlay(sound.id); onPlay(sound.id);
}; };
return ( return (
<Card className="transition-all duration-200 hover:shadow-lg cursor-pointer group"> <Card
<CardHeader className="pb-3"> onClick={handlePlay}
<CardTitle className="text-sm font-medium truncate" title={sound.name}> className={cn(
{sound.name} 'py-2 transition-all duration-100 border-0 shadow-sm cursor-pointer active:scale-95',
</CardTitle> colorClasses,
</CardHeader> )}
<CardContent className="pt-0"> >
<div className="flex items-center justify-between mb-2"> <CardContent className="grid grid-cols-1 pl-3 pr-3 gap-1">
<div className="flex items-center gap-2 text-xs text-muted-foreground"> <h3 className="font-medium text-s truncate">{sound.name}</h3>
<Volume2 size={12} /> <div className="grid grid-cols-3 gap-1 text-xs text-muted-foreground">
<div className="flex">
<Clock className="h-3.5 w-3.5 mr-0.5" />
<span>{formatDuration(sound.duration)}</span> <span>{formatDuration(sound.duration)}</span>
</div> </div>
<div className="text-xs text-muted-foreground"> <div className="flex justify-center">
{sound.play_count} plays <Weight className="h-3.5 w-3.5 mr-0.5" />
<span>{formatSize(sound.size)}</span>
</div>
<div className="flex justify-end items-center">
<Play className="h-3.5 w-3.5 mr-0.5" />
<NumberFlow value={sound.play_count} />
</div> </div>
</div> </div>
<Button
onClick={handlePlay}
className="w-full"
variant={isPlaying ? "secondary" : "default"}
size="sm"
>
<Play size={16} className="mr-2" />
{isPlaying ? 'Playing...' : 'Play'}
</Button>
{sound.is_normalized && (
<div className="mt-2 text-xs text-green-600 text-center">
Normalized
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -70,9 +98,9 @@ export function SoundboardPage() {
const [sounds, setSounds] = useState<Sound[]>([]); const [sounds, setSounds] = useState<Sound[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [playingSound, setPlayingSound] = useState<number | null>(null);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [addUrlDialogOpen, setAddUrlDialogOpen] = useState(false); const [addUrlDialogOpen, setAddUrlDialogOpen] = useState(false);
const [currentColors, setCurrentColors] = useState<string[]>(lightModeColors)
// Setup keyboard shortcut for CTRL+U // Setup keyboard shortcut for CTRL+U
useAddUrlShortcut(() => setAddUrlDialogOpen(true)); useAddUrlShortcut(() => setAddUrlDialogOpen(true));
@@ -81,6 +109,21 @@ export function SoundboardPage() {
fetchSounds(); fetchSounds();
}, []); }, []);
const { resolvedTheme } = useTheme()
useEffect(() => {
if (resolvedTheme === 'dark') {
setCurrentColors(darkModeColors)
} else {
setCurrentColors(lightModeColors)
}
}, [resolvedTheme])
const getSoundColor = (soundIdx: number) => {
const index = soundIdx % currentColors.length
return currentColors[index]
}
const fetchSounds = async () => { const fetchSounds = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -90,7 +133,6 @@ export function SoundboardPage() {
} catch (err) { } catch (err) {
setError('Failed to load sounds'); setError('Failed to load sounds');
toast.error('Failed to load sounds'); toast.error('Failed to load sounds');
console.error('Error fetching sounds:', err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -98,30 +140,20 @@ export function SoundboardPage() {
const handlePlaySound = async (soundId: number) => { const handlePlaySound = async (soundId: number) => {
try { try {
setPlayingSound(soundId);
await apiService.post(`/api/soundboard/sounds/${soundId}/play`); await apiService.post(`/api/soundboard/sounds/${soundId}/play`);
// Reset playing state after a short delay
setTimeout(() => {
setPlayingSound(null);
}, 1000);
} catch (err) { } catch (err) {
setError('Failed to play sound'); setError('Failed to play sound');
toast.error('Failed to play sound'); toast.error('Failed to play sound');
console.error('Error playing sound:', err);
setPlayingSound(null);
} }
}; };
const handleStopAll = async () => { const handleStopAll = async () => {
try { try {
await apiService.post('/api/soundboard/stop-all'); await apiService.post('/api/soundboard/stop-all');
setPlayingSound(null);
toast.success('All sounds stopped'); toast.success('All sounds stopped');
} catch (err) { } catch (err) {
setError('Failed to stop sounds'); setError('Failed to stop sounds');
toast.error('Failed to stop sounds'); toast.error('Failed to stop sounds');
console.error('Error stopping sounds:', err);
} }
}; };
@@ -129,12 +161,10 @@ export function SoundboardPage() {
try { try {
const response = await apiService.post('/api/soundboard/force-stop'); const response = await apiService.post('/api/soundboard/force-stop');
const data = await response.json(); const data = await response.json();
setPlayingSound(null);
toast.success(`Force stopped ${data.stopped_count} sound instances`); toast.success(`Force stopped ${data.stopped_count} sound instances`);
} catch (err) { } catch (err) {
setError('Failed to force stop sounds'); setError('Failed to force stop sounds');
toast.error('Failed to force stop sounds'); toast.error('Failed to force stop sounds');
console.error('Error force stopping sounds:', err);
} }
}; };
@@ -199,12 +229,12 @@ export function SoundboardPage() {
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{filteredSounds.map((sound) => ( {filteredSounds.map((sound, idx) => (
<SoundCard <SoundCard
key={sound.id} key={sound.id}
sound={sound} sound={sound}
onPlay={handlePlaySound} onPlay={handlePlaySound}
isPlaying={playingSound === sound.id} colorClasses={getSoundColor(idx)}
/> />
))} ))}
</div> </div>