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 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user