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:
JSC
2025-09-03 17:17:19 +02:00
parent dba08e2ec0
commit 1ba6f23999
3 changed files with 104 additions and 98 deletions

View File

@@ -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 && (

View File

@@ -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>

View File

@@ -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,
name: dragData.sound.name || dragData.sound.filename,
duration: dragData.sound.duration / 1000, // Convert from ms to seconds
startTime,
trackId: overData.trackId,
}
setState(prev => ({ // Restrict placement to within track duration
...prev, const maxStartTime = Math.max(0, state.duration - soundDuration)
tracks: prev.tracks.map(track => startTime = Math.min(startTime, maxStartTime)
track.id === overData.trackId
? { ...track, sounds: [...track.sounds, newPlacedSound] } // Only proceed if the sound can fit within the track
: track if (startTime >= 0 && startTime + soundDuration <= state.duration) {
), const newPlacedSound: PlacedSound = {
})) id: `placed-${Date.now()}-${Math.random()}`,
soundId: dragData.sound.id,
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) // Only proceed if the sound can fit within the track
if (startTime >= 0 && startTime + dragData.duration <= state.duration) {
const sourceTrackId = dragData.trackId
const targetTrackId = overData.trackId
setState(prev => ({ setState(prev => ({
...prev, ...prev,
tracks: prev.tracks.map(track => { tracks: prev.tracks.map(track => {
if (track.id === sourceTrackId && sourceTrackId === targetTrackId) { if (track.id === sourceTrackId && sourceTrackId === targetTrackId) {
// Moving within the same track - just update position // Moving within the same track - just update position
console.log('Moving within same track') const updatedSound: PlacedSound = {
const updatedSound: PlacedSound = { ...dragData,
...dragData, startTime,
startTime, trackId: targetTrackId,
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