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:
JSC
2025-08-03 20:43:42 +02:00
parent b42b802c37
commit 6cbf0e5e6d
11 changed files with 1388 additions and 6 deletions

View 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>
)
}