feat: add audio extraction management interface and services
- Implemented ExtractionsPage component for managing audio extractions. - Added ExtractionsService for handling extraction API calls. - Created Playlist component for displaying audio tracks. - Introduced ScrollArea component for better UI scrolling experience. - Developed FilesService for file download and thumbnail management. - Added PlayerService for controlling audio playback and state. - Updated API services index to include new services.
This commit is contained in:
134
src/components/player/Playlist.tsx
Normal file
134
src/components/player/Playlist.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Music, Play } from 'lucide-react'
|
||||
import { type PlayerPlaylist } from '@/lib/api/services/player'
|
||||
import { filesService } from '@/lib/api/services/files'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { formatDuration } from '@/utils/format-duration'
|
||||
|
||||
interface PlaylistProps {
|
||||
playlist: PlayerPlaylist
|
||||
currentIndex?: number
|
||||
onTrackSelect: (index: number) => void
|
||||
variant?: 'normal' | 'maximized'
|
||||
}
|
||||
|
||||
export function Playlist({
|
||||
playlist,
|
||||
currentIndex,
|
||||
onTrackSelect,
|
||||
variant = 'normal'
|
||||
}: PlaylistProps) {
|
||||
const maxHeight = variant === 'maximized' ? 'max-h-[calc(100vh-200px)]' : 'max-h-60'
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-sm">
|
||||
{playlist.name}
|
||||
</h4>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{playlist.sounds.length} tracks
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<ScrollArea className={cn('w-full', maxHeight)}>
|
||||
<div className="space-y-1">
|
||||
{playlist.sounds.map((sound, index) => (
|
||||
<div
|
||||
key={sound.id}
|
||||
className={cn(
|
||||
'flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-colors',
|
||||
'hover:bg-muted/50',
|
||||
currentIndex === index && 'bg-primary/10 border border-primary/20'
|
||||
)}
|
||||
onClick={() => onTrackSelect(index)}
|
||||
>
|
||||
{/* Track Number or Play Icon */}
|
||||
<div className="w-6 h-6 flex items-center justify-center flex-shrink-0">
|
||||
{currentIndex === index ? (
|
||||
<Play className="h-3 w-3 text-primary" />
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thumbnail */}
|
||||
<div className={cn(
|
||||
'flex-shrink-0 bg-muted rounded flex items-center justify-center overflow-hidden',
|
||||
variant === 'maximized' ? 'w-10 h-10' : 'w-8 h-8'
|
||||
)}>
|
||||
{sound.thumbnail ? (
|
||||
<img
|
||||
src={filesService.getThumbnailUrl(sound.id)}
|
||||
alt={sound.name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
// Hide image and show music icon if thumbnail fails to load
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
const musicIcon = target.nextElementSibling as HTMLElement
|
||||
if (musicIcon) musicIcon.style.display = 'block'
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Music
|
||||
className={cn(
|
||||
'text-muted-foreground',
|
||||
variant === 'maximized' ? 'h-5 w-5' : 'h-4 w-4',
|
||||
sound.thumbnail ? 'hidden' : 'block'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Track Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={cn(
|
||||
'font-medium truncate',
|
||||
variant === 'maximized' ? 'text-sm' : 'text-xs',
|
||||
currentIndex === index && 'text-primary'
|
||||
)}>
|
||||
{sound.name}
|
||||
</p>
|
||||
<p className={cn(
|
||||
'text-muted-foreground truncate',
|
||||
variant === 'maximized' ? 'text-xs' : 'text-[10px]'
|
||||
)}>
|
||||
{sound.filename}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Duration and Type */}
|
||||
<div className="flex flex-col items-end gap-1 flex-shrink-0">
|
||||
<span className={cn(
|
||||
'text-muted-foreground',
|
||||
variant === 'maximized' ? 'text-xs' : 'text-[10px]'
|
||||
)}>
|
||||
{formatDuration(sound.duration)}
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
variant === 'maximized' ? 'text-[10px] px-1' : 'text-[8px] px-1 py-0'
|
||||
)}
|
||||
>
|
||||
{sound.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Playlist Stats */}
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>{playlist.sounds.length} tracks</span>
|
||||
<span>{formatDuration(playlist.duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user