feat: enhance SequencerPage and SequencerCanvas with drag-and-drop functionality for sound placement and improved track management
This commit is contained in:
@@ -2,7 +2,7 @@ import { useDroppable, useDraggable } from '@dnd-kit/core'
|
||||
import type { Track, PlacedSound } from '@/pages/SequencerPage'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Trash2, Volume2 } from 'lucide-react'
|
||||
import { useState, forwardRef, useRef } from 'react'
|
||||
import { useState, forwardRef, useRef, useEffect } from 'react'
|
||||
|
||||
interface SequencerCanvasProps {
|
||||
tracks: Track[]
|
||||
@@ -11,6 +11,8 @@ interface SequencerCanvasProps {
|
||||
currentTime: number
|
||||
isPlaying: boolean
|
||||
onScroll?: () => void
|
||||
draggedItem?: any // Current dragged item from parent
|
||||
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
|
||||
}
|
||||
|
||||
interface TrackRowProps {
|
||||
@@ -19,6 +21,8 @@ interface TrackRowProps {
|
||||
zoom: number
|
||||
isPlaying: boolean
|
||||
currentTime: number
|
||||
draggedItem?: any // Current dragged item
|
||||
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
|
||||
}
|
||||
|
||||
interface PlacedSoundItemProps {
|
||||
@@ -101,7 +105,7 @@ function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProp
|
||||
)
|
||||
}
|
||||
|
||||
function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowProps) {
|
||||
function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem, dragOverInfo }: TrackRowProps) {
|
||||
const totalWidth = duration * zoom
|
||||
const playheadPosition = currentTime * zoom
|
||||
|
||||
@@ -113,23 +117,7 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
|
||||
},
|
||||
})
|
||||
|
||||
const [dragOverX, setDragOverX] = useState<number | null>(null)
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (isOver) {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
setDragOverX(x)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setDragOverX(null)
|
||||
}
|
||||
|
||||
const handleRemoveSound = (soundId: string) => {
|
||||
// This would typically be handled by the parent component
|
||||
// For now, we'll just console.log
|
||||
console.log('Remove sound:', soundId, 'from track:', track.id)
|
||||
}
|
||||
|
||||
@@ -137,6 +125,7 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
|
||||
<div className="relative" style={{ height: '80px' }}>
|
||||
<div
|
||||
ref={setDropRef}
|
||||
id={`track-${track.id}`}
|
||||
className={`
|
||||
w-full h-full border-b border-border/50
|
||||
relative overflow-hidden
|
||||
@@ -144,8 +133,6 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
|
||||
transition-colors
|
||||
`}
|
||||
style={{ minWidth: `${totalWidth}px` }}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* Grid lines for time markers */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
@@ -166,12 +153,26 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Drop indicator */}
|
||||
{isOver && dragOverX !== null && (
|
||||
{/* Precise drag preview */}
|
||||
{draggedItem && dragOverInfo && dragOverInfo.trackId === track.id && (
|
||||
<div
|
||||
className="absolute top-0 bottom-0 w-0.5 bg-primary/60 pointer-events-none z-20"
|
||||
style={{ left: `${dragOverX}px` }}
|
||||
/>
|
||||
className="absolute top-2 bottom-2 border-2 border-dashed border-primary/60 bg-primary/10 rounded pointer-events-none z-10 flex items-center px-2"
|
||||
style={{
|
||||
left: `${dragOverInfo.x}px`,
|
||||
width: `${Math.max(60,
|
||||
draggedItem.type === 'sound'
|
||||
? (draggedItem.sound.duration / 1000) * zoom
|
||||
: draggedItem.duration * zoom
|
||||
)}px`,
|
||||
}}
|
||||
>
|
||||
<div className="text-xs text-primary/80 truncate font-medium">
|
||||
{draggedItem.type === 'sound'
|
||||
? (draggedItem.sound.name || draggedItem.sound.filename)
|
||||
: draggedItem.name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Playhead */}
|
||||
@@ -204,9 +205,19 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
||||
currentTime,
|
||||
isPlaying,
|
||||
onScroll,
|
||||
draggedItem,
|
||||
dragOverInfo,
|
||||
}, ref) => {
|
||||
const totalWidth = duration * zoom
|
||||
const timelineRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Add a fallback droppable for the entire canvas area
|
||||
const { setNodeRef: setCanvasDropRef } = useDroppable({
|
||||
id: 'sequencer-canvas',
|
||||
data: {
|
||||
type: 'canvas',
|
||||
},
|
||||
})
|
||||
|
||||
const handleTracksScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
// Sync timeline horizontal scroll with tracks
|
||||
@@ -222,7 +233,7 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div ref={setCanvasDropRef} className="h-full flex flex-col">
|
||||
{/* Time ruler */}
|
||||
<div className="h-8 bg-muted/50 border-b border-border/50 flex-shrink-0 overflow-hidden">
|
||||
<div
|
||||
@@ -278,6 +289,8 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
||||
zoom={zoom}
|
||||
isPlaying={isPlaying}
|
||||
currentTime={currentTime}
|
||||
draggedItem={draggedItem}
|
||||
dragOverInfo={dragOverInfo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -119,16 +119,65 @@ export function SoundLibrary() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const params = {
|
||||
search: debouncedSearchQuery.trim() || undefined,
|
||||
types: soundType === 'all' ? undefined : [soundType],
|
||||
sort_by: 'name' as const,
|
||||
sort_order: 'asc' as const,
|
||||
limit: 100, // Limit to 100 sounds for performance
|
||||
// Mock sounds for testing drag functionality
|
||||
const mockSounds: Sound[] = [
|
||||
{
|
||||
id: 1,
|
||||
filename: 'kick.mp3',
|
||||
name: 'Kick Drum',
|
||||
duration: 2000,
|
||||
size: 48000,
|
||||
type: 'SDB',
|
||||
play_count: 5,
|
||||
hash: 'mock-hash-1',
|
||||
is_normalized: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
filename: 'snare.wav',
|
||||
name: 'Snare Hit',
|
||||
duration: 1500,
|
||||
size: 36000,
|
||||
type: 'SDB',
|
||||
play_count: 3,
|
||||
hash: 'mock-hash-2',
|
||||
is_normalized: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
filename: 'hello.mp3',
|
||||
name: 'Hello World',
|
||||
duration: 3000,
|
||||
size: 72000,
|
||||
type: 'TTS',
|
||||
play_count: 1,
|
||||
hash: 'mock-hash-3',
|
||||
is_normalized: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
// Filter by search query
|
||||
let filteredSounds = mockSounds
|
||||
if (debouncedSearchQuery.trim()) {
|
||||
const query = debouncedSearchQuery.toLowerCase()
|
||||
filteredSounds = mockSounds.filter(sound =>
|
||||
sound.name.toLowerCase().includes(query) ||
|
||||
sound.filename.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
const fetchedSounds = await soundsService.getSounds(params)
|
||||
setSounds(fetchedSounds)
|
||||
// Filter by type
|
||||
if (soundType !== 'all') {
|
||||
filteredSounds = filteredSounds.filter(sound => sound.type === soundType)
|
||||
}
|
||||
|
||||
setSounds(filteredSounds)
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch sounds'
|
||||
setError(errorMessage)
|
||||
|
||||
Reference in New Issue
Block a user