diff --git a/src/pages/PlaylistEditPage.tsx b/src/pages/PlaylistEditPage.tsx index e44b87c..af733e8 100644 --- a/src/pages/PlaylistEditPage.tsx +++ b/src/pages/PlaylistEditPage.tsx @@ -2,7 +2,9 @@ import { useEffect, useState, useCallback } from 'react' import { useParams, useNavigate } from 'react-router' import { DndContext, - DragEndEvent, + type DragEndEvent, + type DragStartEvent, + type DragOverEvent, closestCenter, PointerSensor, useSensor, @@ -13,9 +15,11 @@ import { verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable' +import { useDroppable } from '@dnd-kit/core' import { CSS } from '@dnd-kit/utilities' 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' @@ -24,7 +28,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, Plus, Minus } from 'lucide-react' import { toast } from 'sonner' import { formatDuration } from '@/utils/format-duration' @@ -137,6 +141,173 @@ function SortableTableRow({ ) } +// Simplified sortable row component for add mode +interface SimpleSortableRowProps { + sound: PlaylistSound + index: number + onRemoveSound: (soundId: number) => void +} + +function SimpleSortableRow({ sound, index, onRemoveSound }: SimpleSortableRowProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: `playlist-sound-${sound.id}` }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.8 : 1, + } + + return ( +
+
+
+
+
+
+
+
+
+ + + {index + 1} + + + + +
+
+ {sound.name} +
+
+
+ + +
+ ) +} + +// Available sound component for dragging +interface AvailableSoundProps { + sound: Sound +} + +function AvailableSound({ sound }: AvailableSoundProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: `available-sound-${sound.id}` }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.8 : 1, + } + + return ( +
+ + +
+
+ {sound.name} +
+
+
+ ) +} + +// Drop zone component for inserting sounds at specific positions +interface DropZoneProps { + index: number + isActive: boolean +} + +function DropZone({ index, isActive }: DropZoneProps) { + const { isOver, setNodeRef } = useDroppable({ + id: `drop-zone-${index}`, + data: { type: 'dropzone', index } + }) + + return ( +
+ ) +} + +// Inline preview component that shows where the sound will be dropped +interface InlinePreviewProps { + sound: Sound | PlaylistSound + position: number +} + +function InlinePreview({ sound, position }: InlinePreviewProps) { + return ( +
+
+
+
+
+
+
+
+ + + {position + 1} + + + + +
+
+ {sound.name} +
+
+ Will be added here +
+
+
+ ) +} + export function PlaylistEditPage() { const { id } = useParams<{ id: string }>() @@ -151,6 +322,14 @@ export function PlaylistEditPage() { const [saving, setSaving] = useState(false) const [isEditMode, setIsEditMode] = useState(false) + // Add mode state + const [isAddMode, setIsAddMode] = useState(false) + const [availableSounds, setAvailableSounds] = useState([]) + const [availableSoundsLoading, setAvailableSoundsLoading] = useState(false) + const [draggedItem, setDraggedItem] = useState(null) + const [draggedSound, setDraggedSound] = useState(null) + const [dropPosition, setDropPosition] = useState(null) + // dnd-kit sensors const sensors = useSensors( @@ -204,6 +383,30 @@ export function PlaylistEditPage() { } }, [playlistId]) + const fetchAvailableSounds = useCallback(async () => { + try { + setAvailableSoundsLoading(true) + const soundsData = await soundsService.getSoundsByType('EXT') + // Filter out sounds that are already in the playlist + const playlistSoundIds = new Set(sounds.map(s => s.id)) + const filteredSounds = soundsData.filter(sound => !playlistSoundIds.has(sound.id)) + setAvailableSounds(filteredSounds) + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch available sounds' + toast.error(errorMessage) + } finally { + setAvailableSoundsLoading(false) + } + }, [sounds]) + + const toggleAddMode = async () => { + if (!isAddMode) { + // Entering add mode - fetch available sounds + await fetchAvailableSounds() + } + setIsAddMode(!isAddMode) + } + useEffect(() => { if (!isNaN(playlistId)) { fetchPlaylist() @@ -324,10 +527,28 @@ export function PlaylistEditPage() { const handleRemoveSound = async (soundId: number) => { try { + // Find the sound being removed to check if it's EXT type + const removedSound = sounds.find(s => s.id === soundId) + await playlistsService.removeSoundFromPlaylist(playlistId, soundId) setSounds(prev => prev.filter(sound => sound.id !== soundId)) toast.success('Sound removed from playlist') + // If it's an EXT sound and we're in add mode, add it back to available sounds + if (isAddMode && removedSound && removedSound.type === 'EXT') { + // Get the full sound data from the API to add back to available sounds + try { + const allExtSounds = await soundsService.getSoundsByType('EXT') + const soundToAddBack = allExtSounds.find(s => s.id === soundId) + if (soundToAddBack) { + setAvailableSounds(prev => [...prev, soundToAddBack].sort((a, b) => a.name.localeCompare(b.name))) + } + } catch (err) { + // If we can't fetch the sound data, just refresh the available sounds + await fetchAvailableSounds() + } + } + // Refresh playlist data to update counts await fetchPlaylist() } catch (err) { @@ -337,19 +558,78 @@ export function PlaylistEditPage() { } - // dnd-kit drag handler + const handleDragStart = (event: DragStartEvent) => { + const draggedId = event.active.id.toString() + setDraggedItem(draggedId) + setDropPosition(null) // Clear any previous drop position + + // Find the sound being dragged + if (draggedId.startsWith('available-sound-')) { + const soundId = parseInt(draggedId.replace('available-sound-', ''), 10) + const sound = availableSounds.find(s => s.id === soundId) + setDraggedSound(sound || null) + } else if (draggedId.startsWith('playlist-sound-') || draggedId.startsWith('table-sound-')) { + const soundId = parseInt( + draggedId.replace('playlist-sound-', '').replace('table-sound-', ''), 10 + ) + const sound = sounds.find(s => s.id === soundId) + setDraggedSound(sound || null) + } + } + const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event + setDraggedItem(null) + setDraggedSound(null) + setDropPosition(null) if (!over) return const activeId = active.id as string const overId = over.id as string - // Only handle table-sound reordering - if (overId.startsWith('table-sound-')) { - const draggedSoundId = parseInt(activeId.replace('table-sound-', ''), 10) - const targetSoundId = parseInt(overId.replace('table-sound-', ''), 10) + // Handle adding sound from available list to playlist + if (activeId.startsWith('available-sound-') && (overId.startsWith('playlist-sound-') || overId.startsWith('drop-zone-'))) { + const soundId = parseInt(activeId.replace('available-sound-', ''), 10) + + let position = sounds.length // Default to end + + if (overId.startsWith('playlist-sound-')) { + const targetSoundId = parseInt(overId.replace('playlist-sound-', ''), 10) + const targetIndex = sounds.findIndex(s => s.id === targetSoundId) + position = targetIndex + } else if (overId.startsWith('drop-zone-')) { + position = parseInt(overId.replace('drop-zone-', ''), 10) + } + + try { + await playlistsService.addSoundToPlaylist(playlistId, soundId, position) + toast.success('Sound added to playlist') + + // Refresh playlist sounds first + await fetchSounds() + + // Immediately remove the added sound from available sounds + if (isAddMode) { + setAvailableSounds(prev => prev.filter(s => s.id !== soundId)) + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to add sound to playlist' + toast.error(errorMessage) + } + return + } + + // Handle reordering sounds within playlist (both table and simple view) + if ((activeId.startsWith('table-sound-') && overId.startsWith('table-sound-')) || + (activeId.startsWith('playlist-sound-') && overId.startsWith('playlist-sound-'))) { + + const draggedSoundId = parseInt( + activeId.replace('table-sound-', '').replace('playlist-sound-', ''), 10 + ) + const targetSoundId = parseInt( + overId.replace('table-sound-', '').replace('playlist-sound-', ''), 10 + ) const draggedIndex = sounds.findIndex(s => s.id === draggedSoundId) const targetIndex = sounds.findIndex(s => s.id === targetSoundId) @@ -375,6 +655,29 @@ export function PlaylistEditPage() { } } + const handleDragOver = (event: DragOverEvent) => { + const { active, over } = event + + // Only show preview when dragging available sounds + if (!active.id.toString().startsWith('available-sound-') || !over) { + setDropPosition(null) + return + } + + const overId = over.id.toString() + + if (overId.startsWith('playlist-sound-')) { + const targetSoundId = parseInt(overId.replace('playlist-sound-', ''), 10) + const targetIndex = sounds.findIndex(s => s.id === targetSoundId) + setDropPosition(targetIndex) + } else if (overId.startsWith('drop-zone-')) { + const position = parseInt(overId.replace('drop-zone-', ''), 10) + setDropPosition(position) + } else { + setDropPosition(null) + } + } + if (loading) { return (
+
+ ) : isAddMode ? ( + // Add Mode: Split layout with simplified playlist and available sounds +
+ {/* Current Playlist Sounds - Simplified */} +
+

Current Playlist

+ `playlist-sound-${sound.id}`)} + strategy={verticalListSortingStrategy} + > +
+ + {sounds.map((sound, index) => { + const adjustedIndex = dropPosition !== null && dropPosition <= index ? index + 1 : index + return ( +
+ {/* Show inline preview if this is the drop position */} + {dropPosition === index && draggedSound && draggedItem?.startsWith('available-sound-') && ( + + )} + + +
+ ) + })} + {/* Show inline preview at the end if that's the drop position */} + {dropPosition === sounds.length && draggedSound && draggedItem?.startsWith('available-sound-') && ( + + )} +
+
+
+ + {/* Available Sounds */} +
+

+ Available EXT Sounds ({availableSounds.length}) +

+ {availableSoundsLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : availableSounds.length === 0 ? ( +
+ +

No EXT sounds available

+

All EXT sounds are already in this playlist

+
+ ) : ( + `available-sound-${sound.id}`)} + strategy={verticalListSortingStrategy} + > +
+ {availableSounds.map((sound) => ( + + ))} +
+
+ )} +
+
) : ( + // Normal Mode: Full table view
`table-sound-${sound.id}`)}