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,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core'
import {
SortableContext,
@@ -168,22 +169,11 @@ function SimpleSortableRow({ sound, index, onRemoveSound }: SimpleSortableRowPro
<div
ref={setNodeRef}
style={style}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 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"
className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 cursor-grab active:cursor-grabbing group"
{...attributes}
{...listeners}
>
<div className="flex flex-col gap-0.5">
<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">
<span className="text-sm font-mono text-muted-foreground min-w-[1.5rem] text-center flex-shrink-0">
{index + 1}
</span>
@@ -194,16 +184,18 @@ function SimpleSortableRow({ sound, index, onRemoveSound }: SimpleSortableRowPro
{sound.name}
</div>
</div>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => onRemoveSound(sound.id)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
onRemoveSound(sound.id)
}}
className="h-4 w-4 p-0 text-destructive hover:text-destructive flex-shrink-0"
title="Remove from playlist"
>
<X className="h-4 w-4" />
<X className="h-3 w-3" />
</Button>
</div>
)
@@ -212,9 +204,10 @@ function SimpleSortableRow({ sound, index, onRemoveSound }: SimpleSortableRowPro
// Available sound component for dragging
interface AvailableSoundProps {
sound: Sound
onAddToPlaylist: (soundId: number) => void
}
function AvailableSound({ sound }: AvailableSoundProps) {
function AvailableSound({ sound, onAddToPlaylist }: AvailableSoundProps) {
const {
attributes,
listeners,
@@ -234,7 +227,7 @@ function AvailableSound({ sound }: AvailableSoundProps) {
<div
ref={setNodeRef}
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}
{...listeners}
>
@@ -245,6 +238,19 @@ function AvailableSound({ sound }: AvailableSoundProps) {
{sound.name}
</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>
)
}
@@ -274,15 +280,7 @@ interface InlinePreviewProps {
function InlinePreview({ sound, position }: InlinePreviewProps) {
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 justify-center">
<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">
<span className="text-sm font-mono text-primary min-w-[1.5rem] text-center flex-shrink-0">
{position + 1}
</span>
@@ -292,8 +290,45 @@ function InlinePreview({ sound, position }: InlinePreviewProps) {
<div className="font-medium truncate text-primary">
{sound.name}
</div>
<div className="text-xs text-primary/70">
Will be added here
</div>
</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>
@@ -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) => {
try {
// 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" />
))}
</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 ? (
// Add Mode: Split layout with simplified playlist and available sounds
<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}`)}
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) => {
const adjustedIndex = dropPosition !== null && dropPosition <= index ? index + 1 : index
return (
@@ -984,6 +1052,8 @@ export function PlaylistEditPage() {
)}
{/* Invisible drop area at the end */}
<EndDropArea />
</>
)}
</div>
</SortableContext>
</div>
@@ -1015,6 +1085,7 @@ export function PlaylistEditPage() {
<AvailableSound
key={sound.id}
sound={sound}
onAddToPlaylist={(soundId) => handleAddSoundToPlaylist(soundId)}
/>
))}
</div>
@@ -1022,8 +1093,15 @@ export function PlaylistEditPage() {
)}
</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">
<SortableContext
items={sounds.map(sound => `table-sound-${sound.id}`)}
@@ -1065,11 +1143,21 @@ export function PlaylistEditPage() {
</Table>
</SortableContext>
</div>
)
)}
</CardContent>
</Card>
</div>
{/* Drag Overlay */}
<DragOverlay>
{draggedSound && (
<DragOverlayContent
sound={draggedSound}
position={draggedItem?.startsWith('available-sound-') ? (dropPosition ?? sounds.length) : undefined}
/>
)}
</DragOverlay>
</AppLayout>
</DndContext>
)