feat: enhance PlaylistEditPage with drag-and-drop functionality for adding sounds and improved UI elements
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user