feat: add context menu to Playlist for adding tracks to play next queue
This commit is contained in:
@@ -3,11 +3,17 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from '@/components/ui/context-menu'
|
||||
import { filesService } from '@/lib/api/services/files'
|
||||
import { type PlayerPlaylist } from '@/lib/api/services/player'
|
||||
import { type PlayerPlaylist, playerService } from '@/lib/api/services/player'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { formatDuration } from '@/utils/format-duration'
|
||||
import { Music, Play, Search, X } from 'lucide-react'
|
||||
import { Music, Play, Search, X, ListPlus } from 'lucide-react'
|
||||
|
||||
interface PlaylistProps {
|
||||
playlist: PlayerPlaylist
|
||||
@@ -28,6 +34,14 @@ export function Playlist({
|
||||
sound.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
const handleAddToPlayNext = async (soundId: number) => {
|
||||
try {
|
||||
await playerService.addToPlayNext(soundId)
|
||||
} catch (error) {
|
||||
console.error('Failed to add track to play next:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Header */}
|
||||
@@ -68,64 +82,73 @@ export function Playlist({
|
||||
{filteredSounds.map((sound) => {
|
||||
const originalIndex = playlist.sounds.findIndex((s) => s.id === sound.id)
|
||||
return (
|
||||
<div
|
||||
key={sound.id}
|
||||
className={cn(
|
||||
'grid grid-cols-10 gap-2 items-center py-1.5 px-2 rounded hover:bg-muted/50 cursor-pointer text-xs',
|
||||
currentIndex === originalIndex && 'bg-primary/10 text-primary',
|
||||
)}
|
||||
onClick={() => onTrackSelect(originalIndex)}
|
||||
>
|
||||
{/* Track number/play icon - 1 column */}
|
||||
<div className="col-span-1 flex justify-center">
|
||||
{currentIndex === originalIndex ? (
|
||||
<Play className="h-3 w-3" />
|
||||
) : (
|
||||
<span className="text-muted-foreground">{originalIndex + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<ContextMenu key={sound.id}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'grid grid-cols-10 gap-2 items-center py-1.5 px-2 rounded hover:bg-muted/50 cursor-pointer text-xs',
|
||||
currentIndex === originalIndex && 'bg-primary/10 text-primary',
|
||||
)}
|
||||
onClick={() => onTrackSelect(originalIndex)}
|
||||
>
|
||||
{/* Track number/play icon - 1 column */}
|
||||
<div className="col-span-1 flex justify-center">
|
||||
{currentIndex === originalIndex ? (
|
||||
<Play className="h-3 w-3" />
|
||||
) : (
|
||||
<span className="text-muted-foreground">{originalIndex + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thumbnail - 1 column */}
|
||||
<div className="col-span-1">
|
||||
<div
|
||||
className={cn(
|
||||
'bg-muted rounded flex items-center justify-center overflow-hidden',
|
||||
variant === 'maximized' ? 'w-6 h-6' : 'w-5 h-5',
|
||||
)}
|
||||
>
|
||||
{sound.thumbnail ? (
|
||||
<img
|
||||
src={filesService.getThumbnailUrl(sound.id)}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Music className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Thumbnail - 1 column */}
|
||||
<div className="col-span-1">
|
||||
<div
|
||||
className={cn(
|
||||
'bg-muted rounded flex items-center justify-center overflow-hidden',
|
||||
variant === 'maximized' ? 'w-6 h-6' : 'w-5 h-5',
|
||||
)}
|
||||
>
|
||||
{sound.thumbnail ? (
|
||||
<img
|
||||
src={filesService.getThumbnailUrl(sound.id)}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Music className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Track name - 6 columns (takes most space) */}
|
||||
<div className="col-span-6">
|
||||
<span
|
||||
className={cn(
|
||||
'font-medium truncate block',
|
||||
variant === 'maximized' ? 'text-sm' : 'text-xs',
|
||||
currentIndex === originalIndex ? 'text-primary' : 'text-foreground',
|
||||
)}
|
||||
>
|
||||
{sound.name}
|
||||
</span>
|
||||
</div>
|
||||
{/* Track name - 6 columns (takes most space) */}
|
||||
<div className="col-span-6">
|
||||
<span
|
||||
className={cn(
|
||||
'font-medium truncate block',
|
||||
variant === 'maximized' ? 'text-sm' : 'text-xs',
|
||||
currentIndex === originalIndex ? 'text-primary' : 'text-foreground',
|
||||
)}
|
||||
>
|
||||
{sound.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Duration - 2 columns */}
|
||||
<div className="col-span-2 text-right">
|
||||
<span className="text-muted-foreground text-xs whitespace-nowrap">
|
||||
{formatDuration(sound.duration)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
{/* Duration - 2 columns */}
|
||||
<div className="col-span-2 text-right">
|
||||
<span className="text-muted-foreground text-xs whitespace-nowrap">
|
||||
{formatDuration(sound.duration)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onClick={() => handleAddToPlayNext(sound.id)}>
|
||||
<ListPlus className="mr-2 h-4 w-4" />
|
||||
Add to play next
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user