feat: add context menu to Playlist for adding tracks to play next queue

This commit is contained in:
JSC
2025-10-04 19:16:25 +02:00
parent 4352e4c792
commit 9a2a9343d2
2 changed files with 88 additions and 57 deletions

View File

@@ -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>

View File

@@ -38,6 +38,7 @@ export interface PlayerState {
index?: number
current_sound?: PlayerSound
playlist?: PlayerPlaylist
play_next_queue: PlayerSound[]
}
export interface PlayerSeekRequest {
@@ -147,6 +148,13 @@ export class PlayerService {
async getState(): Promise<PlayerState> {
return apiClient.get<PlayerState>('/api/v1/player/state')
}
/**
* Add a sound to the play next queue
*/
async addToPlayNext(soundId: number): Promise<MessageResponse> {
return apiClient.post<MessageResponse>(`/api/v1/player/play-next/${soundId}`)
}
}
export const playerService = new PlayerService()