feat: enhance SoundboardPage with dynamic color themes and improved sound card display
This commit is contained in:
@@ -1,12 +1,46 @@
|
||||
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, Plus } from 'lucide-react';
|
||||
import { Play, Square, Volume2, Plus, Clock, Weight } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { apiService } from '@/services/api';
|
||||
import { AddUrlDialog } from '@/components/AddUrlDialog';
|
||||
import { useAddUrlShortcut } from '@/hooks/use-keyboard-shortcuts';
|
||||
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 {
|
||||
id: number;
|
||||
@@ -14,6 +48,7 @@ interface Sound {
|
||||
filename: string;
|
||||
type: string;
|
||||
duration: number;
|
||||
size: number;
|
||||
play_count: number;
|
||||
is_normalized: boolean;
|
||||
normalized_filename?: string;
|
||||
@@ -22,45 +57,38 @@ interface Sound {
|
||||
interface SoundCardProps {
|
||||
sound: Sound;
|
||||
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 = () => {
|
||||
onPlay(sound.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="transition-all duration-200 hover:shadow-lg cursor-pointer group">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium truncate" title={sound.name}>
|
||||
{sound.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Volume2 size={12} />
|
||||
<Card
|
||||
onClick={handlePlay}
|
||||
className={cn(
|
||||
'py-2 transition-all duration-100 border-0 shadow-sm cursor-pointer active:scale-95',
|
||||
colorClasses,
|
||||
)}
|
||||
>
|
||||
<CardContent className="grid grid-cols-1 pl-3 pr-3 gap-1">
|
||||
<h3 className="font-medium text-s truncate">{sound.name}</h3>
|
||||
<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>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{sound.play_count} plays
|
||||
<div className="flex justify-center">
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
@@ -70,9 +98,9 @@ export function SoundboardPage() {
|
||||
const [sounds, setSounds] = useState<Sound[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [playingSound, setPlayingSound] = useState<number | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [addUrlDialogOpen, setAddUrlDialogOpen] = useState(false);
|
||||
const [currentColors, setCurrentColors] = useState<string[]>(lightModeColors)
|
||||
|
||||
// Setup keyboard shortcut for CTRL+U
|
||||
useAddUrlShortcut(() => setAddUrlDialogOpen(true));
|
||||
@@ -81,6 +109,21 @@ export function SoundboardPage() {
|
||||
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 () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -90,7 +133,6 @@ export function SoundboardPage() {
|
||||
} catch (err) {
|
||||
setError('Failed to load sounds');
|
||||
toast.error('Failed to load sounds');
|
||||
console.error('Error fetching sounds:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -98,30 +140,20 @@ export function SoundboardPage() {
|
||||
|
||||
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');
|
||||
toast.error('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);
|
||||
toast.success('All sounds stopped');
|
||||
} catch (err) {
|
||||
setError('Failed to stop sounds');
|
||||
toast.error('Failed to stop sounds');
|
||||
console.error('Error stopping sounds:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,12 +161,10 @@ export function SoundboardPage() {
|
||||
try {
|
||||
const response = await apiService.post('/api/soundboard/force-stop');
|
||||
const data = await response.json();
|
||||
setPlayingSound(null);
|
||||
toast.success(`Force stopped ${data.stopped_count} sound instances`);
|
||||
} catch (err) {
|
||||
setError('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 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
|
||||
key={sound.id}
|
||||
sound={sound}
|
||||
onPlay={handlePlaySound}
|
||||
isPlaying={playingSound === sound.id}
|
||||
colorClasses={getSoundColor(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user