feat: enhance PlaylistEditPage with drag-and-drop functionality for adding sounds and improved UI elements

This commit is contained in:
JSC
2025-08-11 20:56:13 +02:00
parent fc9cdf1065
commit 25fd92e0da

View File

@@ -9,6 +9,7 @@ import {
PointerSensor, PointerSensor,
useSensor, useSensor,
useSensors, useSensors,
DragOverlay,
} from '@dnd-kit/core' } from '@dnd-kit/core'
import { import {
SortableContext, SortableContext,
@@ -168,22 +169,11 @@ function SimpleSortableRow({ sound, index, onRemoveSound }: SimpleSortableRowPro
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 group" className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 cursor-grab active:cursor-grabbing group"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div
className="flex items-center justify-center cursor-grab active:cursor-grabbing"
{...attributes} {...attributes}
{...listeners} {...listeners}
> >
<div className="flex flex-col gap-0.5"> <span className="text-sm font-mono text-muted-foreground min-w-[1.5rem] text-center flex-shrink-0">
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full"></div>
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full"></div>
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full"></div>
</div>
</div>
<span className="text-sm font-mono text-muted-foreground w-6 text-center">
{index + 1} {index + 1}
</span> </span>
@@ -194,16 +184,18 @@ function SimpleSortableRow({ sound, index, onRemoveSound }: SimpleSortableRowPro
{sound.name} {sound.name}
</div> </div>
</div> </div>
</div>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => onRemoveSound(sound.id)} onClick={(e) => {
className="h-8 w-8 p-0 text-destructive hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity" e.stopPropagation()
onRemoveSound(sound.id)
}}
className="h-4 w-4 p-0 text-destructive hover:text-destructive flex-shrink-0"
title="Remove from playlist" title="Remove from playlist"
> >
<X className="h-4 w-4" /> <X className="h-3 w-3" />
</Button> </Button>
</div> </div>
) )
@@ -212,9 +204,10 @@ function SimpleSortableRow({ sound, index, onRemoveSound }: SimpleSortableRowPro
// Available sound component for dragging // Available sound component for dragging
interface AvailableSoundProps { interface AvailableSoundProps {
sound: Sound sound: Sound
onAddToPlaylist: (soundId: number) => void
} }
function AvailableSound({ sound }: AvailableSoundProps) { function AvailableSound({ sound, onAddToPlaylist }: AvailableSoundProps) {
const { const {
attributes, attributes,
listeners, listeners,
@@ -234,7 +227,7 @@ function AvailableSound({ sound }: AvailableSoundProps) {
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 cursor-grab active:cursor-grabbing" className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 cursor-grab active:cursor-grabbing group"
{...attributes} {...attributes}
{...listeners} {...listeners}
> >
@@ -245,6 +238,19 @@ function AvailableSound({ sound }: AvailableSoundProps) {
{sound.name} {sound.name}
</div> </div>
</div> </div>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
onAddToPlaylist(sound.id)
}}
className="h-4 w-4 p-0 text-primary hover:text-primary flex-shrink-0"
title="Add to playlist"
>
<Plus className="h-3 w-3" />
</Button>
</div> </div>
) )
} }
@@ -274,15 +280,7 @@ interface InlinePreviewProps {
function InlinePreview({ sound, position }: InlinePreviewProps) { function InlinePreview({ sound, position }: InlinePreviewProps) {
return ( return (
<div className="flex items-center gap-3 p-3 border-2 border-dashed border-primary rounded-lg bg-primary/10 animate-pulse"> <div className="flex items-center gap-3 p-3 border-2 border-dashed border-primary rounded-lg bg-primary/10 animate-pulse">
<div className="flex items-center justify-center"> <span className="text-sm font-mono text-primary min-w-[1.5rem] text-center flex-shrink-0">
<div className="flex flex-col gap-0.5">
<div className="w-1 h-1 bg-primary/60 rounded-full"></div>
<div className="w-1 h-1 bg-primary/60 rounded-full"></div>
<div className="w-1 h-1 bg-primary/60 rounded-full"></div>
</div>
</div>
<span className="text-sm font-mono text-primary w-6 text-center">
{position + 1} {position + 1}
</span> </span>
@@ -292,8 +290,45 @@ function InlinePreview({ sound, position }: InlinePreviewProps) {
<div className="font-medium truncate text-primary"> <div className="font-medium truncate text-primary">
{sound.name} {sound.name}
</div> </div>
<div className="text-xs text-primary/70"> </div>
Will be added here </div>
)
}
// Drag overlay component that shows the dragged item
interface DragOverlayContentProps {
sound: Sound | PlaylistSound
position?: number
}
function DragOverlayContent({ sound, position }: DragOverlayContentProps) {
// If position is provided, show as current playlist style
if (position !== undefined) {
return (
<div className="flex items-center gap-3 p-3 border rounded-lg bg-background shadow-lg">
<span className="text-sm font-mono text-muted-foreground min-w-[1.5rem] text-center flex-shrink-0">
{position + 1}
</span>
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="font-medium truncate">
{sound.name}
</div>
</div>
</div>
)
}
// Default available sound style
return (
<div className="flex items-center gap-3 p-3 border rounded-lg bg-background shadow-lg">
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="font-medium truncate">
{sound.name}
</div> </div>
</div> </div>
</div> </div>
@@ -517,6 +552,28 @@ export function PlaylistEditPage() {
} }
} }
const handleAddSoundToPlaylist = async (soundId: number) => {
try {
// Add at the end - backend should handle position gaps automatically
const position = sounds.length
await playlistsService.addSoundToPlaylist(playlistId, soundId, position)
toast.success('Sound added to playlist')
// Refresh playlist sounds
await fetchSounds()
// Remove the added sound from available sounds
setAvailableSounds(prev => prev.filter(s => s.id !== soundId))
// Refresh playlist data to update counts
await fetchPlaylist()
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to add sound to playlist'
toast.error(errorMessage)
}
}
const handleRemoveSound = async (soundId: number) => { const handleRemoveSound = async (soundId: number) => {
try { try {
// Find the sound being removed to check if it's EXT type // Find the sound being removed to check if it's EXT type
@@ -946,11 +1003,6 @@ export function PlaylistEditPage() {
<Skeleton key={i} className="h-12 w-full" /> <Skeleton key={i} className="h-12 w-full" />
))} ))}
</div> </div>
) : sounds.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Music className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>No sounds in this playlist</p>
</div>
) : isAddMode ? ( ) : isAddMode ? (
// Add Mode: Split layout with simplified playlist and available sounds // Add Mode: Split layout with simplified playlist and available sounds
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
@@ -961,7 +1013,23 @@ export function PlaylistEditPage() {
items={sounds.map(sound => `playlist-sound-${sound.id}`)} items={sounds.map(sound => `playlist-sound-${sound.id}`)}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-2"> <div className="space-y-2 max-h-96 overflow-y-auto">
{sounds.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Music className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No sounds in playlist</p>
<p className="text-xs mt-1">Drag sounds here or use + button</p>
{/* Show inline preview at position 0 if dragging */}
{dropPosition === 0 && draggedSound && draggedItem?.startsWith('available-sound-') && (
<div className="mt-4">
<InlinePreview sound={draggedSound} position={0} />
</div>
)}
{/* Invisible drop area */}
<EndDropArea />
</div>
) : (
<>
{sounds.map((sound, index) => { {sounds.map((sound, index) => {
const adjustedIndex = dropPosition !== null && dropPosition <= index ? index + 1 : index const adjustedIndex = dropPosition !== null && dropPosition <= index ? index + 1 : index
return ( return (
@@ -984,6 +1052,8 @@ export function PlaylistEditPage() {
)} )}
{/* Invisible drop area at the end */} {/* Invisible drop area at the end */}
<EndDropArea /> <EndDropArea />
</>
)}
</div> </div>
</SortableContext> </SortableContext>
</div> </div>
@@ -1015,6 +1085,7 @@ export function PlaylistEditPage() {
<AvailableSound <AvailableSound
key={sound.id} key={sound.id}
sound={sound} sound={sound}
onAddToPlaylist={(soundId) => handleAddSoundToPlaylist(soundId)}
/> />
))} ))}
</div> </div>
@@ -1022,8 +1093,15 @@ export function PlaylistEditPage() {
)} )}
</div> </div>
</div> </div>
) : sounds.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Music className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>No sounds in this playlist</p>
<p className="text-xs mt-1">Click the + button to enter add mode</p>
</div>
) : ( ) : (
// Normal Mode: Full table view // Normal Mode: Full table view (only show table if there are sounds)
sounds.length > 0 && (
<div className="rounded-md border"> <div className="rounded-md border">
<SortableContext <SortableContext
items={sounds.map(sound => `table-sound-${sound.id}`)} items={sounds.map(sound => `table-sound-${sound.id}`)}
@@ -1065,11 +1143,21 @@ export function PlaylistEditPage() {
</Table> </Table>
</SortableContext> </SortableContext>
</div> </div>
)
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Drag Overlay */}
<DragOverlay>
{draggedSound && (
<DragOverlayContent
sound={draggedSound}
position={draggedItem?.startsWith('available-sound-') ? (dropPosition ?? sounds.length) : undefined}
/>
)}
</DragOverlay>
</AppLayout> </AppLayout>
</DndContext> </DndContext>
) )