From 34f20f33afa423e6b9ca57fb6ef17adab378c527 Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 10 Aug 2025 20:07:14 +0200 Subject: [PATCH] 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