From 34f20f33afa423e6b9ca57fb6ef17adab378c527 Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 10 Aug 2025 20:07:14 +0200 Subject: [PATCH 1/5] feat: add sound addition functionality to PlaylistEditPage; implement drag-and-drop for adding available sounds --- src/pages/PlaylistEditPage.tsx | 382 +++++++++++++++++++++++++-------- 1 file changed, 292 insertions(+), 90 deletions(-) diff --git a/src/pages/PlaylistEditPage.tsx b/src/pages/PlaylistEditPage.tsx index 9891b5e..6724e69 100644 --- a/src/pages/PlaylistEditPage.tsx +++ b/src/pages/PlaylistEditPage.tsx @@ -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(null) const [saving, setSaving] = useState(false) const [isEditMode, setIsEditMode] = useState(false) + const [isAddSoundsMode, setIsAddSoundsMode] = useState(false) + const [availableSounds, setAvailableSounds] = useState([]) + 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 ( - Playlist Sounds ({sounds.length}) + {isAddSoundsMode ? 'Add Sounds to Playlist' : `Playlist Sounds (${sounds.length})`} - +
+ {isAddSoundsMode ? ( + + ) : ( + <> + + + + )} +
+ {isAddSoundsMode && ( +

+ 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. +

+ )} - {soundsLoading ? ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
- ) : sounds.length === 0 ? ( -
- -

No sounds in this playlist

-
- ) : ( -
- - - - - Name - Duration - Type - Plays - Actions - - - - {sounds.map((sound, index) => ( - - - {index + 1} - - -
- -
-
- {sound.name} + {isAddSoundsMode ? ( + /* Add Sounds Mode - Two Column Layout */ +
+ {/* Current Playlist Sounds - Left Column */} +
+

+ + Current Playlist ({sounds.length} sounds) +

+
+ {sounds.length === 0 ? ( +
+

Drag sounds here to add to playlist

+
+ ) : ( +
+ {/* Drop zone at the top */} +
+ + {sounds.map((sound, index) => ( +
+
+
+ {index + 1} +
+ +
+
{sound.name}
+
+ {formatDuration(sound.duration || 0)} +
+ + {/* Drop zone after each item */} +
- - {formatDuration(sound.duration || 0)} - - - {sound.type} - - - {sound.play_count} - -
- - - + ))} +
+ )} +
+
+ + {/* Available Sounds - Right Column */} +
+

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

+
+ {loadingAvailableSounds ? ( +
+ +
+ ) : availableSounds.length === 0 ? ( +
+

No available EXT sounds

+
+ ) : ( +
+ {availableSounds.map((sound) => ( +
handleDragStart(e, sound.id)} + onClick={() => handleAddSoundToPlaylist(sound.id)} + > + +
+
{sound.name}
+
+ {formatDuration(sound.duration || 0)} • {sound.type} +
+
+
- - - ))} - -
+ ))} +
+ )} + + + ) : ( + /* Normal Mode - Table View */ + soundsLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : sounds.length === 0 ? ( +
+ +

No sounds in this playlist

+
+ ) : ( +
+ + + + + Name + Duration + Type + Plays + Actions + + + + {sounds.map((sound, index) => ( + + + {index + 1} + + +
+ +
+
+ {sound.name} +
+
+
+
+ {formatDuration(sound.duration || 0)} + + + {sound.type} + + + {sound.play_count} + +
+ + + +
+
+
+ ))} +
+
+
+ ) )}
+
) } \ No newline at end of file From 0c7875cac597665a3fa1a052c48f02f1ade9f012 Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 10 Aug 2025 21:33:03 +0200 Subject: [PATCH 2/5] feat: implement drag-and-drop functionality for sound management in PlaylistEditPage; add sortable components for better user experience --- bun.lock | 11 + package.json | 3 + src/pages/PlaylistEditPage.tsx | 641 +++++++++++++++++++++++++-------- 3 files changed, 495 insertions(+), 160 deletions(-) diff --git a/bun.lock b/bun.lock index b83c29d..5279b69 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "": { "name": "frontend", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@number-flow/react": "^0.5.10", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", @@ -86,6 +89,14 @@ "@date-fns/tz": ["@date-fns/tz@1.2.0", "", {}, "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + + "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.8", "", { "os": "aix", "cpu": "ppc64" }, "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.8", "", { "os": "android", "cpu": "arm" }, "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw=="], diff --git a/package.json b/package.json index 9c001df..38eba39 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@number-flow/react": "^0.5.10", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", diff --git a/src/pages/PlaylistEditPage.tsx b/src/pages/PlaylistEditPage.tsx index 6724e69..33c53b3 100644 --- a/src/pages/PlaylistEditPage.tsx +++ b/src/pages/PlaylistEditPage.tsx @@ -1,5 +1,22 @@ import { useEffect, useState, useCallback } from 'react' import { useParams, useNavigate } from 'react-router' +import { + DndContext, + DragOverlay, + DragStartEvent, + DragEndEvent, + closestCenter, + PointerSensor, + useSensor, + useSensors, + useDroppable, +} from '@dnd-kit/core' +import { + SortableContext, + verticalListSortingStrategy, + useSortable, +} from '@dnd-kit/sortable' +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' @@ -15,6 +32,252 @@ import { AlertCircle, Save, Music, Clock, ChevronUp, ChevronDown, Trash2, Refres import { toast } from 'sonner' import { formatDuration } from '@/utils/format-duration' +// Sortable playlist item component for add sounds mode +interface SortablePlaylistItemProps { + sound: PlaylistSound + index: number +} + +function SortablePlaylistItem({ sound, index }: SortablePlaylistItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: `playlist-sound-${sound.id}` }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + } + + return ( +
+
+ {index + 1} +
+ +
+
{sound.name}
+
+ {formatDuration(sound.duration || 0)} +
+
+
+ ) +} + +// Sortable table row component for normal table view +interface SortableTableRowProps { + sound: PlaylistSound + index: number + onMoveSoundUp: (index: number) => void + onMoveSoundDown: (index: number) => void + onRemoveSound: (soundId: number) => void + totalSounds: number +} + +function SortableTableRow({ + sound, + index, + onMoveSoundUp, + onMoveSoundDown, + onRemoveSound, + totalSounds +}: SortableTableRowProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: `table-sound-${sound.id}` }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.8 : 1, + } + + return ( + + +
+
+
+
+
+
+ {index + 1} +
+
+ +
+ +
+
+ {sound.name} +
+
+
+
+ {formatDuration(sound.duration || 0)} + + + {sound.type} + + + {sound.play_count} + +
+ + + +
+
+
+ ) +} + +// Draggable available sound component +interface DraggableAvailableSoundProps { + sound: Sound + onAddSound: (soundId: number) => void +} + +function DraggableAvailableSound({ sound, onAddSound }: DraggableAvailableSoundProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: sound.id.toString() }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + } + + return ( +
onAddSound(sound.id)} + > + +
+
{sound.name}
+
+ {formatDuration(sound.duration || 0)} • {sound.type} +
+
+ +
+ ) +} + +// Drop zone component +interface DropZoneProps { + position: number + isActive?: boolean +} + +function DropZone({ position, isActive }: DropZoneProps) { + const { + setNodeRef, + isOver, + } = useDroppable({ + id: `playlist-position-${position}`, + data: { type: 'position', position } + }) + + return ( +
+ ) +} + +// Empty playlist drop zone +function EmptyPlaylistDropZone() { + const { + setNodeRef, + isOver, + } = useDroppable({ + id: 'playlist-position-0', + data: { type: 'position', position: 0 } + }) + + return ( +
+

Drag sounds here to add to playlist

+
+ ) +} + export function PlaylistEditPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() @@ -31,6 +294,18 @@ export function PlaylistEditPage() { const [availableSounds, setAvailableSounds] = useState([]) const [loadingAvailableSounds, setLoadingAvailableSounds] = useState(false) + // dnd-kit state + const [draggedSound, setDraggedSound] = useState(null) + + // dnd-kit sensors + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }) + ) + // Form state const [formData, setFormData] = useState({ name: '', @@ -236,41 +511,128 @@ export function PlaylistEditPage() { } const handleAddSoundToPlaylist = async (soundId: number, position?: number) => { + // Find the sound being added for potential rollback + const soundToAdd = availableSounds.find(sound => sound.id === soundId) + try { + // Optimistically remove the sound from available sounds for instant feedback + setAvailableSounds(prev => prev.filter(sound => sound.id !== soundId)) + await playlistsService.addSoundToPlaylist(playlistId, soundId, position) toast.success('Sound added to playlist') - // Refresh sounds and available sounds + // Refresh playlist sounds to show the new addition await fetchSounds() - await fetchAvailableSounds() } catch (err) { + // Rollback: add the sound back to available sounds if the API call failed + if (soundToAdd) { + setAvailableSounds(prev => [...prev, soundToAdd]) + } 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' + // dnd-kit drag handlers + const handleDragStart = (event: DragStartEvent) => { + const { active } = event + + // Extract sound ID from different ID formats + const activeId = active.id as string + const getSoundId = (id: string) => { + if (id.startsWith('table-sound-')) { + return parseInt(id.replace('table-sound-', ''), 10) + } else if (id.startsWith('playlist-sound-')) { + return parseInt(id.replace('playlist-sound-', ''), 10) + } else { + return parseInt(id, 10) + } + } + + const soundId = getSoundId(activeId) + const availableSound = availableSounds.find(s => s.id === soundId) + const playlistSound = sounds.find(s => s.id === soundId) + + setDraggedSound(availableSound || playlistSound || null) } - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - e.dataTransfer.dropEffect = 'copy' + const handleDragOver = () => { + // Handle drag over logic here if needed for visual feedback } - const handleDrop = async (e: React.DragEvent) => { - e.preventDefault() - const soundIdStr = e.dataTransfer.getData('text/plain') - const soundId = parseInt(soundIdStr, 10) + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event - // Get the position from the drop target - const target = e.currentTarget as HTMLElement - const position = parseInt(target.dataset.position || '0', 10) + setDraggedSound(null) - if (!isNaN(soundId) && !isNaN(position)) { - await handleAddSoundToPlaylist(soundId, position) + if (!over) return + + const activeId = active.id as string + const overId = over.id as string + + // Extract sound ID from different ID formats + const getDraggedSoundId = (id: string) => { + if (id.startsWith('table-sound-')) { + return parseInt(id.replace('table-sound-', ''), 10) + } else if (id.startsWith('playlist-sound-')) { + return parseInt(id.replace('playlist-sound-', ''), 10) + } else { + return parseInt(id, 10) + } + } + + const getTargetSoundId = (id: string) => { + if (id.startsWith('table-sound-')) { + return parseInt(id.replace('table-sound-', ''), 10) + } else if (id.startsWith('playlist-sound-')) { + return parseInt(id.replace('playlist-sound-', ''), 10) + } else { + return parseInt(id, 10) + } + } + + const draggedSoundId = getDraggedSoundId(activeId) + + // Check if dragging from available sounds (adding new sound) + const isFromAvailable = availableSounds.some(s => s.id === draggedSoundId) + + // Handle dropping onto playlist positions (inserting new sound in add sounds mode) + if (overId.startsWith('playlist-position-') && isFromAvailable) { + const position = parseInt(overId.replace('playlist-position-', ''), 10) + await handleAddSoundToPlaylist(draggedSoundId, position) + return + } + + // Handle reordering within playlist (both add sounds mode and table mode) + if (overId.startsWith('playlist-sound-') || overId.startsWith('table-sound-')) { + const targetSoundId = getTargetSoundId(overId) + const draggedIndex = sounds.findIndex(s => s.id === draggedSoundId) + const targetIndex = sounds.findIndex(s => s.id === targetSoundId) + + // Only allow reordering if both sounds are in the playlist + if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { + // Reorder sounds in playlist + const newSounds = [...sounds] + const [draggedSound] = newSounds.splice(draggedIndex, 1) + newSounds.splice(targetIndex, 0, draggedSound) + + // Create sound positions for API + const soundPositions: Array<[number, number]> = newSounds.map((sound, idx) => [sound.id, idx]) + + try { + await playlistsService.reorderPlaylistSounds(playlistId, soundPositions) + setSounds(newSounds) + toast.success('Playlist reordered') + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to reorder playlist' + toast.error(errorMessage) + } + } + // Handle dropping from available sounds onto existing playlist item (insert before) + else if (isFromAvailable && targetIndex !== -1) { + await handleAddSoundToPlaylist(draggedSoundId, targetIndex) + return + } } } @@ -325,15 +687,22 @@ export function PlaylistEditPage() { } return ( - +
@@ -574,51 +943,26 @@ export function PlaylistEditPage() { Current Playlist ({sounds.length} sounds)
- {sounds.length === 0 ? ( -
-

Drag sounds here to add to playlist

-
- ) : ( -
- {/* Drop zone at the top */} -
- - {sounds.map((sound, index) => ( -
-
-
- {index + 1} -
- -
-
{sound.name}
-
- {formatDuration(sound.duration || 0)} -
-
+ `playlist-sound-${sound.id}`)} + strategy={verticalListSortingStrategy} + > + {sounds.length === 0 ? ( + + ) : ( +
+ {/* Drop zone at the top */} + + + {sounds.map((sound, index) => ( +
+ +
- - {/* Drop zone after each item */} -
-
- ))} -
- )} + ))} +
+ )} +
@@ -638,26 +982,20 @@ export function PlaylistEditPage() {

No available EXT sounds

) : ( -
- {availableSounds.map((sound) => ( -
handleDragStart(e, sound.id)} - onClick={() => handleAddSoundToPlaylist(sound.id)} - > - -
-
{sound.name}
-
- {formatDuration(sound.duration || 0)} • {sound.type} -
-
- -
- ))} -
+ sound.id.toString())} + strategy={verticalListSortingStrategy} + > +
+ {availableSounds.map((sound) => ( + + ))} +
+
)}
@@ -677,84 +1015,67 @@ export function PlaylistEditPage() {
) : (
- - - - - Name - Duration - Type - Plays - Actions - - - - {sounds.map((sound, index) => ( - - - {index + 1} - - -
- -
-
- {sound.name} -
+ `table-sound-${sound.id}`)} + strategy={verticalListSortingStrategy} + > +
+ + + +
+
+
+
+
+ #
- - {formatDuration(sound.duration || 0)} - - - {sound.type} - - - {sound.play_count} - -
- - - -
-
+
+ Name + Duration + Type + Plays + Actions
- ))} - -
+ + + {sounds.map((sound, index) => ( + + ))} + + +
) )} -
+
- + {/* Drag Overlay */} + + {draggedSound && ( +
+ +
+
{draggedSound.name}
+
+ {formatDuration(draggedSound.duration || 0)} +
+
+
+ )} +
+ + ) } \ No newline at end of file From d80d8588f6ab8b793dbab3c2c440818e1aa4451c Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 10 Aug 2025 21:55:20 +0200 Subject: [PATCH 3/5] fix: update previous_volume to 80 in CompactPlayer and Player components for consistency --- src/components/player/CompactPlayer.tsx | 2 +- src/components/player/Player.tsx | 7 +- src/pages/PlaylistEditPage.tsx | 513 +++--------------------- 3 files changed, 68 insertions(+), 454 deletions(-) diff --git a/src/components/player/CompactPlayer.tsx b/src/components/player/CompactPlayer.tsx index f66b79f..56735db 100644 --- a/src/components/player/CompactPlayer.tsx +++ b/src/components/player/CompactPlayer.tsx @@ -27,7 +27,7 @@ export function CompactPlayer({ className }: CompactPlayerProps) { status: 'stopped', mode: 'continuous', volume: 80, - previous_volume: 50, + previous_volume: 80, position: 0 }) const [isLoading, setIsLoading] = useState(false) diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 49ee910..3e41019 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -46,7 +46,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) { status: 'stopped', mode: 'continuous', volume: 80, - previous_volume: 50, + previous_volume: 80, position: 0 }) const [displayMode, setDisplayMode] = useState(() => { @@ -566,11 +566,6 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) { )} - {state.playlist && ( -

- {state.playlist.name} -

- )} {/* Progress Bar */} diff --git a/src/pages/PlaylistEditPage.tsx b/src/pages/PlaylistEditPage.tsx index 33c53b3..e44b87c 100644 --- a/src/pages/PlaylistEditPage.tsx +++ b/src/pages/PlaylistEditPage.tsx @@ -2,14 +2,11 @@ import { useEffect, useState, useCallback } from 'react' import { useParams, useNavigate } from 'react-router' import { DndContext, - DragOverlay, - DragStartEvent, DragEndEvent, closestCenter, PointerSensor, useSensor, useSensors, - useDroppable, } from '@dnd-kit/core' import { SortableContext, @@ -19,7 +16,6 @@ import { 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' @@ -28,53 +24,10 @@ 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, Plus } from 'lucide-react' +import { AlertCircle, Save, Music, Clock, ChevronUp, ChevronDown, Trash2, RefreshCw, Edit, X, ArrowLeft } from 'lucide-react' import { toast } from 'sonner' import { formatDuration } from '@/utils/format-duration' -// Sortable playlist item component for add sounds mode -interface SortablePlaylistItemProps { - sound: PlaylistSound - index: number -} - -function SortablePlaylistItem({ sound, index }: SortablePlaylistItemProps) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: `playlist-sound-${sound.id}` }) - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - } - - return ( -
-
- {index + 1} -
- -
-
{sound.name}
-
- {formatDuration(sound.duration || 0)} -
-
-
- ) -} // Sortable table row component for normal table view interface SortableTableRowProps { @@ -184,99 +137,6 @@ function SortableTableRow({ ) } -// Draggable available sound component -interface DraggableAvailableSoundProps { - sound: Sound - onAddSound: (soundId: number) => void -} - -function DraggableAvailableSound({ sound, onAddSound }: DraggableAvailableSoundProps) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: sound.id.toString() }) - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - } - - return ( -
onAddSound(sound.id)} - > - -
-
{sound.name}
-
- {formatDuration(sound.duration || 0)} • {sound.type} -
-
- -
- ) -} - -// Drop zone component -interface DropZoneProps { - position: number - isActive?: boolean -} - -function DropZone({ position, isActive }: DropZoneProps) { - const { - setNodeRef, - isOver, - } = useDroppable({ - id: `playlist-position-${position}`, - data: { type: 'position', position } - }) - - return ( -
- ) -} - -// Empty playlist drop zone -function EmptyPlaylistDropZone() { - const { - setNodeRef, - isOver, - } = useDroppable({ - id: 'playlist-position-0', - data: { type: 'position', position: 0 } - }) - - return ( -
-

Drag sounds here to add to playlist

-
- ) -} export function PlaylistEditPage() { const { id } = useParams<{ id: string }>() @@ -290,12 +150,7 @@ export function PlaylistEditPage() { const [error, setError] = useState(null) const [saving, setSaving] = useState(false) const [isEditMode, setIsEditMode] = useState(false) - const [isAddSoundsMode, setIsAddSoundsMode] = useState(false) - const [availableSounds, setAvailableSounds] = useState([]) - const [loadingAvailableSounds, setLoadingAvailableSounds] = useState(false) - // dnd-kit state - const [draggedSound, setDraggedSound] = useState(null) // dnd-kit sensors const sensors = useSensors( @@ -481,135 +336,24 @@ 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) => { - // Find the sound being added for potential rollback - const soundToAdd = availableSounds.find(sound => sound.id === soundId) - - try { - // Optimistically remove the sound from available sounds for instant feedback - setAvailableSounds(prev => prev.filter(sound => sound.id !== soundId)) - - await playlistsService.addSoundToPlaylist(playlistId, soundId, position) - toast.success('Sound added to playlist') - - // Refresh playlist sounds to show the new addition - await fetchSounds() - } catch (err) { - // Rollback: add the sound back to available sounds if the API call failed - if (soundToAdd) { - setAvailableSounds(prev => [...prev, soundToAdd]) - } - const errorMessage = err instanceof Error ? err.message : 'Failed to add sound to playlist' - toast.error(errorMessage) - } - } - - // dnd-kit drag handlers - const handleDragStart = (event: DragStartEvent) => { - const { active } = event - - // Extract sound ID from different ID formats - const activeId = active.id as string - const getSoundId = (id: string) => { - if (id.startsWith('table-sound-')) { - return parseInt(id.replace('table-sound-', ''), 10) - } else if (id.startsWith('playlist-sound-')) { - return parseInt(id.replace('playlist-sound-', ''), 10) - } else { - return parseInt(id, 10) - } - } - - const soundId = getSoundId(activeId) - const availableSound = availableSounds.find(s => s.id === soundId) - const playlistSound = sounds.find(s => s.id === soundId) - - setDraggedSound(availableSound || playlistSound || null) - } - - const handleDragOver = () => { - // Handle drag over logic here if needed for visual feedback - } + // dnd-kit drag handler const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event - setDraggedSound(null) - if (!over) return const activeId = active.id as string const overId = over.id as string - // Extract sound ID from different ID formats - const getDraggedSoundId = (id: string) => { - if (id.startsWith('table-sound-')) { - return parseInt(id.replace('table-sound-', ''), 10) - } else if (id.startsWith('playlist-sound-')) { - return parseInt(id.replace('playlist-sound-', ''), 10) - } else { - return parseInt(id, 10) - } - } - - const getTargetSoundId = (id: string) => { - if (id.startsWith('table-sound-')) { - return parseInt(id.replace('table-sound-', ''), 10) - } else if (id.startsWith('playlist-sound-')) { - return parseInt(id.replace('playlist-sound-', ''), 10) - } else { - return parseInt(id, 10) - } - } - - const draggedSoundId = getDraggedSoundId(activeId) - - // Check if dragging from available sounds (adding new sound) - const isFromAvailable = availableSounds.some(s => s.id === draggedSoundId) - - // Handle dropping onto playlist positions (inserting new sound in add sounds mode) - if (overId.startsWith('playlist-position-') && isFromAvailable) { - const position = parseInt(overId.replace('playlist-position-', ''), 10) - await handleAddSoundToPlaylist(draggedSoundId, position) - return - } - - // Handle reordering within playlist (both add sounds mode and table mode) - if (overId.startsWith('playlist-sound-') || overId.startsWith('table-sound-')) { - const targetSoundId = getTargetSoundId(overId) + // 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) + const draggedIndex = sounds.findIndex(s => s.id === draggedSoundId) const targetIndex = sounds.findIndex(s => s.id === targetSoundId) - // Only allow reordering if both sounds are in the playlist if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { // Reorder sounds in playlist const newSounds = [...sounds] @@ -628,11 +372,6 @@ export function PlaylistEditPage() { toast.error(errorMessage) } } - // Handle dropping from available sounds onto existing playlist item (insert before) - else if (isFromAvailable && targetIndex !== -1) { - await handleAddSoundToPlaylist(draggedSoundId, targetIndex) - return - } } } @@ -690,8 +429,6 @@ export function PlaylistEditPage() {
-

{playlist.name}

@@ -892,189 +621,79 @@ export function PlaylistEditPage() {

- {isAddSoundsMode ? 'Add Sounds to Playlist' : `Playlist Sounds (${sounds.length})`} + Playlist Sounds ({sounds.length})
- {isAddSoundsMode ? ( - - ) : ( - <> - - - - )} +
- {isAddSoundsMode && ( -

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

- )} - {isAddSoundsMode ? ( - /* Add Sounds Mode - Two Column Layout */ -
- {/* Current Playlist Sounds - Left Column */} -
-

- - Current Playlist ({sounds.length} sounds) -

-
- `playlist-sound-${sound.id}`)} - strategy={verticalListSortingStrategy} - > - {sounds.length === 0 ? ( - - ) : ( -
- {/* Drop zone at the top */} - - - {sounds.map((sound, index) => ( -
- - -
- ))} -
- )} -
-
-
- - {/* Available Sounds - Right Column */} -
-

- - Available EXT Sounds ({availableSounds.length} available) -

-
- {loadingAvailableSounds ? ( -
- -
- ) : availableSounds.length === 0 ? ( -
-

No available EXT sounds

-
- ) : ( - sound.id.toString())} - strategy={verticalListSortingStrategy} - > -
- {availableSounds.map((sound) => ( - - ))} -
-
- )} -
-
+ {soundsLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : sounds.length === 0 ? ( +
+ +

No sounds in this playlist

) : ( - /* Normal Mode - Table View */ - soundsLoading ? ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
- ) : sounds.length === 0 ? ( -
- -

No sounds in this playlist

-
- ) : ( -
- `table-sound-${sound.id}`)} - strategy={verticalListSortingStrategy} - > - - - - -
-
-
-
-
-
- # +
+ `table-sound-${sound.id}`)} + strategy={verticalListSortingStrategy} + > +
+ + + +
+
+
+
+
- - Name - Duration - Type - Plays - Actions - - - - {sounds.map((sound, index) => ( - - ))} - -
-
-
- ) + # +
+ + Name + Duration + Type + Plays + Actions + + + + {sounds.map((sound, index) => ( + + ))} + + + +
)}
- {/* Drag Overlay */} - - {draggedSound && ( -
- -
-
{draggedSound.name}
-
- {formatDuration(draggedSound.duration || 0)} -
-
-
- )} -
) From 490221ffddcf4c1ab3d572100ab2ed382b49b651 Mon Sep 17 00:00:00 2001 From: JSC Date: Mon, 11 Aug 2025 01:00:26 +0200 Subject: [PATCH 4/5] feat: implement add mode in PlaylistEditPage; add drag-and-drop functionality for managing available sounds --- src/pages/PlaylistEditPage.tsx | 399 ++++++++++++++++++++++++++++++++- 1 file changed, 392 insertions(+), 7 deletions(-) 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}`)} From 5182ed36c34c573e63ea6dea751914abbc04e2af Mon Sep 17 00:00:00 2001 From: JSC Date: Mon, 11 Aug 2025 09:39:50 +0200 Subject: [PATCH 5/5] feat: replace DropZone with EndDropArea for improved sound insertion in PlaylistEditPage --- src/pages/PlaylistEditPage.tsx | 39 +++++++++++++--------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/pages/PlaylistEditPage.tsx b/src/pages/PlaylistEditPage.tsx index af733e8..2e68326 100644 --- a/src/pages/PlaylistEditPage.tsx +++ b/src/pages/PlaylistEditPage.tsx @@ -249,26 +249,18 @@ function AvailableSound({ sound }: AvailableSoundProps) { ) } -// 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 } +// Simple drop area for the end of the playlist +function EndDropArea() { + const { setNodeRef } = useDroppable({ + id: 'playlist-end', + data: { type: 'playlist-end' } }) return (
) } @@ -543,7 +535,7 @@ export function PlaylistEditPage() { if (soundToAddBack) { setAvailableSounds(prev => [...prev, soundToAddBack].sort((a, b) => a.name.localeCompare(b.name))) } - } catch (err) { + } catch { // If we can't fetch the sound data, just refresh the available sounds await fetchAvailableSounds() } @@ -589,7 +581,7 @@ export function PlaylistEditPage() { const overId = over.id as string // Handle adding sound from available list to playlist - if (activeId.startsWith('available-sound-') && (overId.startsWith('playlist-sound-') || overId.startsWith('drop-zone-'))) { + if (activeId.startsWith('available-sound-') && (overId.startsWith('playlist-sound-') || overId === 'playlist-end')) { const soundId = parseInt(activeId.replace('available-sound-', ''), 10) let position = sounds.length // Default to end @@ -598,8 +590,8 @@ export function PlaylistEditPage() { 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) + } else if (overId === 'playlist-end') { + position = sounds.length } try { @@ -670,9 +662,8 @@ export function PlaylistEditPage() { 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 if (overId === 'playlist-end') { + setDropPosition(sounds.length) } else { setDropPosition(null) } @@ -970,8 +961,7 @@ export function PlaylistEditPage() { items={sounds.map(sound => `playlist-sound-${sound.id}`)} strategy={verticalListSortingStrategy} > -
- +
{sounds.map((sound, index) => { const adjustedIndex = dropPosition !== null && dropPosition <= index ? index + 1 : index return ( @@ -985,7 +975,6 @@ export function PlaylistEditPage() { index={adjustedIndex} onRemoveSound={handleRemoveSound} /> -
) })} @@ -993,6 +982,8 @@ export function PlaylistEditPage() { {dropPosition === sounds.length && draggedSound && draggedItem?.startsWith('available-sound-') && ( )} + {/* Invisible drop area at the end */} +