Improves sound placement and preview logic
Refines the sound placement logic in the sequencer to ensure sounds are placed correctly within track boundaries. It restricts sound placement to the track duration, preventing sounds from being placed out of bounds. Enhances the drag preview by visually indicating invalid placement positions with a red border and "Invalid" label. Also extracts duration and size formatting into separate utility functions for better code organization.
This commit is contained in:
@@ -157,26 +157,38 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Precise drag preview */}
|
{/* Precise drag preview */}
|
||||||
{draggedItem && dragOverInfo && dragOverInfo.trackId === track.id && (
|
{draggedItem && dragOverInfo && dragOverInfo.trackId === track.id && (() => {
|
||||||
<div
|
const soundDuration = draggedItem.type === 'sound'
|
||||||
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"
|
? draggedItem.sound.duration / 1000 // Convert ms to seconds
|
||||||
style={{
|
: draggedItem.duration
|
||||||
left: `${dragOverInfo.x}px`,
|
const startTime = dragOverInfo.x / zoom
|
||||||
width: `${Math.max(60,
|
const endTime = startTime + soundDuration
|
||||||
draggedItem.type === 'sound'
|
const isValidPosition = startTime >= 0 && endTime <= duration
|
||||||
? (draggedItem.sound.duration / 1000) * zoom
|
|
||||||
: draggedItem.duration * zoom
|
return (
|
||||||
)}px`,
|
<div
|
||||||
}}
|
className={`absolute top-2 bottom-2 border-2 border-dashed rounded pointer-events-none z-10 flex items-center px-2 ${
|
||||||
>
|
isValidPosition
|
||||||
<div className="text-xs text-primary/80 truncate font-medium">
|
? 'border-primary/60 bg-primary/10'
|
||||||
{draggedItem.type === 'sound'
|
: 'border-red-500/60 bg-red-500/10'
|
||||||
? (draggedItem.sound.name || draggedItem.sound.filename)
|
}`}
|
||||||
: draggedItem.name
|
style={{
|
||||||
}
|
left: `${Math.max(0, dragOverInfo.x)}px`,
|
||||||
|
width: `${Math.max(60, soundDuration * zoom)}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={`text-xs truncate font-medium ${
|
||||||
|
isValidPosition ? 'text-primary/80' : 'text-red-500/80'
|
||||||
|
}`}>
|
||||||
|
{draggedItem.type === 'sound'
|
||||||
|
? (draggedItem.sound.name || draggedItem.sound.filename)
|
||||||
|
: draggedItem.name
|
||||||
|
}
|
||||||
|
{!isValidPosition && ' (Invalid)'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
})()}
|
||||||
|
|
||||||
{/* Playhead */}
|
{/* Playhead */}
|
||||||
{isPlaying && (
|
{isPlaying && (
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { formatDuration } from '@/utils/format-duration'
|
||||||
|
import { formatSize } from '@/utils/format-size'
|
||||||
|
|
||||||
interface DraggableSoundProps {
|
interface DraggableSoundProps {
|
||||||
sound: Sound
|
sound: Sound
|
||||||
@@ -43,18 +45,6 @@ function DraggableSound({ sound }: DraggableSoundProps) {
|
|||||||
// Don't apply transform to prevent layout shift - DragOverlay handles the visual feedback
|
// Don't apply transform to prevent layout shift - DragOverlay handles the visual feedback
|
||||||
const style = undefined
|
const style = undefined
|
||||||
|
|
||||||
const formatDuration = (ms: number): string => {
|
|
||||||
const seconds = Math.floor(ms / 1000)
|
|
||||||
const mins = Math.floor(seconds / 60)
|
|
||||||
const secs = seconds % 60
|
|
||||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
|
||||||
const mb = bytes / (1024 * 1024)
|
|
||||||
return `${mb.toFixed(1)}MB`
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
@@ -81,7 +71,7 @@ function DraggableSound({ sound }: DraggableSoundProps) {
|
|||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
|
||||||
<span>{formatDuration(sound.duration)}</span>
|
<span>{formatDuration(sound.duration)}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{formatFileSize(sound.size)}</span>
|
<span>{formatSize(sound.size)}</span>
|
||||||
{sound.play_count > 0 && (
|
{sound.play_count > 0 && (
|
||||||
<>
|
<>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
|
|||||||
@@ -119,85 +119,89 @@ export function SequencerPage() {
|
|||||||
startTime = Math.max(0, dragOverInfo.x / state.zoom)
|
startTime = Math.max(0, dragOverInfo.x / state.zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPlacedSound: PlacedSound = {
|
const soundDuration = dragData.sound.duration / 1000 // Convert from ms to seconds
|
||||||
id: `placed-${Date.now()}-${Math.random()}`,
|
|
||||||
soundId: dragData.sound.id,
|
// Restrict placement to within track duration
|
||||||
name: dragData.sound.name || dragData.sound.filename,
|
const maxStartTime = Math.max(0, state.duration - soundDuration)
|
||||||
duration: dragData.sound.duration / 1000, // Convert from ms to seconds
|
startTime = Math.min(startTime, maxStartTime)
|
||||||
startTime,
|
|
||||||
trackId: overData.trackId,
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(prev => ({
|
// Only proceed if the sound can fit within the track
|
||||||
...prev,
|
if (startTime >= 0 && startTime + soundDuration <= state.duration) {
|
||||||
tracks: prev.tracks.map(track =>
|
const newPlacedSound: PlacedSound = {
|
||||||
track.id === overData.trackId
|
id: `placed-${Date.now()}-${Math.random()}`,
|
||||||
? { ...track, sounds: [...track.sounds, newPlacedSound] }
|
soundId: dragData.sound.id,
|
||||||
: track
|
name: dragData.sound.name || dragData.sound.filename,
|
||||||
),
|
duration: soundDuration,
|
||||||
}))
|
startTime,
|
||||||
|
trackId: overData.trackId,
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
tracks: prev.tracks.map(track =>
|
||||||
|
track.id === overData.trackId
|
||||||
|
? { ...track, sounds: [...track.sounds, newPlacedSound] }
|
||||||
|
: track
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle moving placed sounds within tracks
|
// Handle moving placed sounds within tracks
|
||||||
if (dragData?.type === 'placed-sound' && overData?.type === 'track') {
|
if (dragData?.type === 'placed-sound' && overData?.type === 'track') {
|
||||||
console.log('Moving placed sound:', dragData, 'to track:', overData.trackId)
|
|
||||||
|
|
||||||
// Use precise drop position if available
|
// Use precise drop position if available
|
||||||
let startTime = dragData.startTime || 0
|
let startTime = dragData.startTime || 0
|
||||||
if (dragOverInfo && dragOverInfo.trackId === overData.trackId) {
|
if (dragOverInfo && dragOverInfo.trackId === overData.trackId) {
|
||||||
startTime = Math.max(0, dragOverInfo.x / state.zoom)
|
startTime = Math.max(0, dragOverInfo.x / state.zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceTrackId = dragData.trackId
|
// Restrict placement to within track duration
|
||||||
const targetTrackId = overData.trackId
|
const maxStartTime = Math.max(0, state.duration - dragData.duration)
|
||||||
|
startTime = Math.min(startTime, maxStartTime)
|
||||||
console.log('Source track:', sourceTrackId, 'Target track:', targetTrackId, 'New start time:', startTime)
|
|
||||||
|
|
||||||
setState(prev => ({
|
// Only proceed if the sound can fit within the track
|
||||||
...prev,
|
if (startTime >= 0 && startTime + dragData.duration <= state.duration) {
|
||||||
tracks: prev.tracks.map(track => {
|
const sourceTrackId = dragData.trackId
|
||||||
if (track.id === sourceTrackId && sourceTrackId === targetTrackId) {
|
const targetTrackId = overData.trackId
|
||||||
// Moving within the same track - just update position
|
|
||||||
console.log('Moving within same track')
|
setState(prev => ({
|
||||||
const updatedSound: PlacedSound = {
|
...prev,
|
||||||
...dragData,
|
tracks: prev.tracks.map(track => {
|
||||||
startTime,
|
if (track.id === sourceTrackId && sourceTrackId === targetTrackId) {
|
||||||
trackId: targetTrackId,
|
// Moving within the same track - just update position
|
||||||
|
const updatedSound: PlacedSound = {
|
||||||
|
...dragData,
|
||||||
|
startTime,
|
||||||
|
trackId: targetTrackId,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
sounds: track.sounds.map(s =>
|
||||||
|
s.id === dragData.id ? updatedSound : s
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else if (track.id === sourceTrackId) {
|
||||||
|
// Remove from source track (different track move)
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
sounds: track.sounds.filter(s => s.id !== dragData.id),
|
||||||
|
}
|
||||||
|
} else if (track.id === targetTrackId) {
|
||||||
|
// Add to target track (different track move)
|
||||||
|
const updatedSound: PlacedSound = {
|
||||||
|
...dragData,
|
||||||
|
startTime,
|
||||||
|
trackId: targetTrackId,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
sounds: [...track.sounds, updatedSound],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return track
|
||||||
...track,
|
}),
|
||||||
sounds: track.sounds.map(s =>
|
}))
|
||||||
s.id === dragData.id ? updatedSound : s
|
}
|
||||||
),
|
|
||||||
}
|
|
||||||
} else if (track.id === sourceTrackId) {
|
|
||||||
// Remove from source track (different track move)
|
|
||||||
console.log('Removing sound from source track:', track.id, 'sounds before:', track.sounds.length)
|
|
||||||
const filtered = track.sounds.filter(s => s.id !== dragData.id)
|
|
||||||
console.log('Sounds after removal:', filtered.length)
|
|
||||||
return {
|
|
||||||
...track,
|
|
||||||
sounds: filtered,
|
|
||||||
}
|
|
||||||
} else if (track.id === targetTrackId) {
|
|
||||||
// Add to target track (different track move)
|
|
||||||
console.log('Adding sound to target track:', track.id, 'sounds before:', track.sounds.length)
|
|
||||||
const updatedSound: PlacedSound = {
|
|
||||||
...dragData,
|
|
||||||
startTime,
|
|
||||||
trackId: targetTrackId,
|
|
||||||
}
|
|
||||||
console.log('Updated sound:', updatedSound)
|
|
||||||
const newSounds = [...track.sounds, updatedSound]
|
|
||||||
console.log('Sounds after addition:', newSounds.length)
|
|
||||||
return {
|
|
||||||
...track,
|
|
||||||
sounds: newSounds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return track
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear state
|
// Clear state
|
||||||
|
|||||||
Reference in New Issue
Block a user