feat: add sound addition functionality to PlaylistEditPage; implement drag-and-drop for adding available sounds

This commit is contained in:
JSC
2025-08-10 20:07:14 +02:00
parent 9c01cd538e
commit 34f20f33af

View File

@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router'
import { AppLayout } from '@/components/AppLayout'
import { playlistsService, type Playlist, type PlaylistSound } from '@/lib/api/services/playlists'
import { soundsService, type Sound } from '@/lib/api/services/sounds'
import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
@@ -10,7 +11,7 @@ import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { AlertCircle, Save, Music, Clock, ChevronUp, ChevronDown, Trash2, RefreshCw, Edit, X, ArrowLeft } from 'lucide-react'
import { AlertCircle, Save, Music, Clock, ChevronUp, ChevronDown, Trash2, RefreshCw, Edit, X, ArrowLeft, Plus } from 'lucide-react'
import { toast } from 'sonner'
import { formatDuration } from '@/utils/format-duration'
@@ -26,6 +27,9 @@ export function PlaylistEditPage() {
const [error, setError] = useState<string | null>(null)
const [saving, setSaving] = useState(false)
const [isEditMode, setIsEditMode] = useState(false)
const [isAddSoundsMode, setIsAddSoundsMode] = useState(false)
const [availableSounds, setAvailableSounds] = useState<Sound[]>([])
const [loadingAvailableSounds, setLoadingAvailableSounds] = useState(false)
// Form state
const [formData, setFormData] = useState({
@@ -202,6 +206,74 @@ export function PlaylistEditPage() {
}
}
const fetchAvailableSounds = useCallback(async () => {
try {
setLoadingAvailableSounds(true)
// Get all EXT sounds
const allExtSounds = await soundsService.getSoundsByType('EXT')
// Filter out sounds that are already in the current playlist
const currentSoundIds = sounds.map(sound => sound.id)
const available = allExtSounds.filter(sound => !currentSoundIds.includes(sound.id))
setAvailableSounds(available)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch available sounds'
toast.error(errorMessage)
} finally {
setLoadingAvailableSounds(false)
}
}, [sounds])
const handleOpenAddSounds = async () => {
setIsAddSoundsMode(true)
await fetchAvailableSounds()
}
const handleCloseAddSounds = () => {
setIsAddSoundsMode(false)
setAvailableSounds([])
}
const handleAddSoundToPlaylist = async (soundId: number, position?: number) => {
try {
await playlistsService.addSoundToPlaylist(playlistId, soundId, position)
toast.success('Sound added to playlist')
// Refresh sounds and available sounds
await fetchSounds()
await fetchAvailableSounds()
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to add sound to playlist'
toast.error(errorMessage)
}
}
// Drag and drop handlers
const handleDragStart = (e: React.DragEvent, soundId: number) => {
e.dataTransfer.setData('text/plain', soundId.toString())
e.dataTransfer.effectAllowed = 'copy'
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
}
const handleDrop = async (e: React.DragEvent) => {
e.preventDefault()
const soundIdStr = e.dataTransfer.getData('text/plain')
const soundId = parseInt(soundIdStr, 10)
// Get the position from the drop target
const target = e.currentTarget as HTMLElement
const position = parseInt(target.dataset.position || '0', 10)
if (!isNaN(soundId) && !isNaN(position)) {
await handleAddSoundToPlaylist(soundId, position)
}
}
if (loading) {
return (
<AppLayout
@@ -451,8 +523,28 @@ export function PlaylistEditPage() {
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Music className="h-5 w-5" />
Playlist Sounds ({sounds.length})
{isAddSoundsMode ? 'Add Sounds to Playlist' : `Playlist Sounds (${sounds.length})`}
</CardTitle>
<div className="flex items-center gap-2">
{isAddSoundsMode ? (
<Button
variant="outline"
size="sm"
onClick={handleCloseAddSounds}
>
<X className="h-4 w-4 mr-2" />
Cancel
</Button>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={handleOpenAddSounds}
title="Add sounds to playlist"
>
<Plus className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
@@ -461,10 +553,118 @@ export function PlaylistEditPage() {
>
<RefreshCw className={`h-4 w-4 ${soundsLoading ? 'animate-spin' : ''}`} />
</Button>
</>
)}
</div>
</div>
{isAddSoundsMode && (
<p className="text-sm text-muted-foreground mt-2">
Drag sounds from the available list (right) to add them to your playlist (left) at specific positions, or click a sound to add it to the end.
</p>
)}
</CardHeader>
<CardContent>
{soundsLoading ? (
{isAddSoundsMode ? (
/* Add Sounds Mode - Two Column Layout */
<div className="grid grid-cols-2 gap-6 min-h-[600px]">
{/* Current Playlist Sounds - Left Column */}
<div className="flex flex-col">
<h3 className="font-semibold mb-4 flex items-center gap-2">
<Music className="h-4 w-4" />
Current Playlist ({sounds.length} sounds)
</h3>
<div className="flex-1 border rounded-lg overflow-hidden">
{sounds.length === 0 ? (
<div
className="flex items-center justify-center h-full text-muted-foreground border-2 border-dashed border-muted hover:border-primary/50 transition-colors cursor-copy"
data-position="0"
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<p>Drag sounds here to add to playlist</p>
</div>
) : (
<div className="h-full overflow-y-auto">
{/* Drop zone at the top */}
<div
className="h-2 border-2 border-dashed border-transparent hover:border-primary/50 transition-colors cursor-copy"
data-position="0"
onDragOver={handleDragOver}
onDrop={handleDrop}
/>
{sounds.map((sound, index) => (
<div key={sound.id}>
<div className="flex items-center gap-3 p-3 border-b hover:bg-muted/50">
<div className="text-sm text-muted-foreground font-mono w-8">
{index + 1}
</div>
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{sound.name}</div>
<div className="text-sm text-muted-foreground">
{formatDuration(sound.duration || 0)}
</div>
</div>
</div>
{/* Drop zone after each item */}
<div
className="h-2 border-2 border-dashed border-transparent hover:border-primary/50 transition-colors cursor-copy"
data-position={index + 1}
onDragOver={handleDragOver}
onDrop={handleDrop}
/>
</div>
))}
</div>
)}
</div>
</div>
{/* Available Sounds - Right Column */}
<div className="flex flex-col">
<h3 className="font-semibold mb-4 flex items-center gap-2">
<Music className="h-4 w-4" />
Available EXT Sounds ({availableSounds.length} available)
</h3>
<div className="flex-1 border rounded-lg overflow-hidden">
{loadingAvailableSounds ? (
<div className="flex items-center justify-center h-full">
<RefreshCw className="h-6 w-6 animate-spin" />
</div>
) : availableSounds.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<p>No available EXT sounds</p>
</div>
) : (
<div className="h-full overflow-y-auto">
{availableSounds.map((sound) => (
<div
key={sound.id}
className="flex items-center gap-3 p-3 border-b hover:bg-muted/50 cursor-grab active:cursor-grabbing select-none"
draggable
onDragStart={(e) => handleDragStart(e, sound.id)}
onClick={() => handleAddSoundToPlaylist(sound.id)}
>
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{sound.name}</div>
<div className="text-sm text-muted-foreground">
{formatDuration(sound.duration || 0)} {sound.type}
</div>
</div>
<Plus className="h-4 w-4 text-primary" />
</div>
))}
</div>
)}
</div>
</div>
</div>
) : (
/* Normal Mode - Table View */
soundsLoading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
@@ -490,7 +690,7 @@ export function PlaylistEditPage() {
</TableHeader>
<TableBody>
{sounds.map((sound, index) => (
<TableRow key={sound.id}>
<TableRow key={sound.id} className="hover:bg-muted/50">
<TableCell className="text-center text-muted-foreground font-mono text-sm">
{index + 1}
</TableCell>
@@ -549,10 +749,12 @@ export function PlaylistEditPage() {
</TableBody>
</Table>
</div>
)
)}
</CardContent>
</Card>
</div>
</AppLayout>
)
}