fix: prevent multiple ImportDialog instances from appearing
Fixed issue where ImportDialog was being rendered for each track in waveform mode, causing multiple unclosable dialogs to appear on page load. Changes: - Moved ImportDialog to renderControlsOnly mode only - Each track now has exactly one ImportDialog (rendered in controls column) - Removed duplicate ImportDialog from renderWaveformOnly mode - Removed ImportDialog from fallback return statement This ensures only one dialog appears per track, making it properly closable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -682,226 +682,137 @@ export function Track({
|
|||||||
|
|
||||||
// Render only controls
|
// Render only controls
|
||||||
if (renderControlsOnly) {
|
if (renderControlsOnly) {
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"w-48 flex-shrink-0 border-b border-r-4 p-2 flex flex-col gap-2 min-h-0 transition-all duration-200 cursor-pointer border-border",
|
|
||||||
isSelected
|
|
||||||
? "bg-primary/10 border-r-primary"
|
|
||||||
: "bg-card border-r-transparent hover:bg-accent/30",
|
|
||||||
)}
|
|
||||||
style={{ height: trackHeight }}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onSelect) onSelect();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Track Name Row - Integrated collapse (DAW style) */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"group flex items-center gap-1.5 px-1 py-0.5 rounded cursor-pointer transition-colors",
|
|
||||||
isSelected ? "bg-primary/10" : "hover:bg-accent/50",
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (!isEditingName) {
|
|
||||||
e.stopPropagation();
|
|
||||||
onToggleCollapse();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={track.collapsed ? "Expand track" : "Collapse track"}
|
|
||||||
>
|
|
||||||
{/* Small triangle indicator */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex-shrink-0 transition-colors",
|
|
||||||
isSelected
|
|
||||||
? "text-primary"
|
|
||||||
: "text-muted-foreground group-hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{track.collapsed ? (
|
|
||||||
<ChevronRight className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Color stripe (thicker when selected) */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-5 rounded-full flex-shrink-0 transition-all",
|
|
||||||
isSelected ? "w-1" : "w-0.5",
|
|
||||||
)}
|
|
||||||
style={{ backgroundColor: track.color }}
|
|
||||||
></div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
{isEditingName ? (
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={nameInput}
|
|
||||||
onChange={(e) => setNameInput(e.target.value)}
|
|
||||||
onBlur={handleNameBlur}
|
|
||||||
onKeyDown={handleNameKeyDown}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="w-full px-1 py-0.5 text-xs font-semibold bg-background border border-border rounded"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleNameClick();
|
|
||||||
}}
|
|
||||||
className="px-1 py-0.5 text-xs font-semibold text-foreground truncate"
|
|
||||||
title={String(track.name || "Untitled Track")}
|
|
||||||
>
|
|
||||||
{String(track.name || "Untitled Track")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Track Controls - Only show when not collapsed */}
|
|
||||||
{!track.collapsed && (
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-between min-h-0 overflow-hidden">
|
|
||||||
{/* Integrated Track Controls (Pan + Fader + Buttons) */}
|
|
||||||
<TrackControls
|
|
||||||
volume={track.volume}
|
|
||||||
pan={track.pan}
|
|
||||||
peakLevel={
|
|
||||||
track.recordEnabled || isRecording
|
|
||||||
? recordingLevel
|
|
||||||
: playbackLevel
|
|
||||||
}
|
|
||||||
rmsLevel={
|
|
||||||
track.recordEnabled || isRecording
|
|
||||||
? recordingLevel * 0.7
|
|
||||||
: playbackLevel * 0.7
|
|
||||||
}
|
|
||||||
isMuted={track.mute}
|
|
||||||
isSolo={track.solo}
|
|
||||||
isRecordEnabled={track.recordEnabled}
|
|
||||||
showAutomation={track.automation?.showAutomation}
|
|
||||||
showEffects={track.showEffects}
|
|
||||||
isRecording={isRecording}
|
|
||||||
onVolumeChange={onVolumeChange}
|
|
||||||
onPanChange={onPanChange}
|
|
||||||
onMuteToggle={onToggleMute}
|
|
||||||
onSoloToggle={onToggleSolo}
|
|
||||||
onRecordToggle={onToggleRecordEnable}
|
|
||||||
onAutomationToggle={() => {
|
|
||||||
onUpdateTrack(track.id, {
|
|
||||||
automation: {
|
|
||||||
...track.automation,
|
|
||||||
showAutomation: !track.automation?.showAutomation,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onEffectsClick={() => {
|
|
||||||
onUpdateTrack(track.id, {
|
|
||||||
showEffects: !track.showEffects,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onVolumeTouchStart={handleVolumeTouchStart}
|
|
||||||
onVolumeTouchEnd={handleVolumeTouchEnd}
|
|
||||||
onPanTouchStart={handlePanTouchStart}
|
|
||||||
onPanTouchEnd={handlePanTouchEnd}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render only waveform
|
|
||||||
if (renderWaveformOnly) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative bg-waveform-bg border-b transition-all duration-200",
|
"w-48 flex-shrink-0 border-b border-r-4 p-2 flex flex-col gap-2 min-h-0 transition-all duration-200 cursor-pointer border-border",
|
||||||
isSelected && "bg-primary/5",
|
isSelected
|
||||||
|
? "bg-primary/10 border-r-primary"
|
||||||
|
: "bg-card border-r-transparent hover:bg-accent/30",
|
||||||
)}
|
)}
|
||||||
style={{ height: trackHeight }}
|
style={{ height: trackHeight }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onSelect) onSelect();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Inner container with dynamic width */}
|
{/* Track Name Row - Integrated collapse (DAW style) */}
|
||||||
<div
|
<div
|
||||||
className="relative h-full"
|
className={cn(
|
||||||
style={{
|
"group flex items-center gap-1.5 px-1 py-0.5 rounded cursor-pointer transition-colors",
|
||||||
minWidth:
|
isSelected ? "bg-primary/10" : "hover:bg-accent/50",
|
||||||
track.audioBuffer && zoom > 1
|
|
||||||
? `${duration * zoom * 100}px`
|
|
||||||
: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Delete Button - Top Right Overlay */}
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRemove();
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"absolute top-2 right-2 z-20 h-6 w-6 rounded flex items-center justify-center transition-all",
|
|
||||||
"bg-card/80 hover:bg-destructive/90 text-muted-foreground hover:text-white",
|
|
||||||
"border border-border/50 hover:border-destructive",
|
|
||||||
"backdrop-blur-sm shadow-sm hover:shadow-md",
|
|
||||||
)}
|
|
||||||
title="Remove track"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{track.audioBuffer ? (
|
|
||||||
<>
|
|
||||||
{/* Waveform Canvas */}
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
className="absolute inset-0 w-full h-full cursor-pointer"
|
|
||||||
onMouseDown={handleCanvasMouseDown}
|
|
||||||
onMouseMove={handleCanvasMouseMove}
|
|
||||||
onMouseUp={handleCanvasMouseUp}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
!track.collapsed && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-0 flex flex-col items-center justify-center text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer group",
|
|
||||||
isDragging
|
|
||||||
? "bg-primary/20 text-primary border-2 border-primary border-dashed"
|
|
||||||
: "hover:bg-accent/50",
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleLoadAudioClick();
|
|
||||||
}}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
>
|
|
||||||
<Upload className="h-6 w-6 mb-2 opacity-50 group-hover:opacity-100" />
|
|
||||||
<p>
|
|
||||||
{isDragging
|
|
||||||
? "Drop audio file here"
|
|
||||||
: "Click to load audio file"}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs opacity-75 mt-1">or drag & drop</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="audio/*"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!isEditingName) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggleCollapse();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={track.collapsed ? "Expand track" : "Collapse track"}
|
||||||
|
>
|
||||||
|
{/* Small triangle indicator */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-shrink-0 transition-colors",
|
||||||
|
isSelected
|
||||||
|
? "text-primary"
|
||||||
|
: "text-muted-foreground group-hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{track.collapsed ? (
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Color stripe (thicker when selected) */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-5 rounded-full flex-shrink-0 transition-all",
|
||||||
|
isSelected ? "w-1" : "w-0.5",
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: track.color }}
|
||||||
|
></div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{isEditingName ? (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={nameInput}
|
||||||
|
onChange={(e) => setNameInput(e.target.value)}
|
||||||
|
onBlur={handleNameBlur}
|
||||||
|
onKeyDown={handleNameKeyDown}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="w-full px-1 py-0.5 text-xs font-semibold bg-background border border-border rounded"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleNameClick();
|
||||||
|
}}
|
||||||
|
className="px-1 py-0.5 text-xs font-semibold text-foreground truncate"
|
||||||
|
title={String(track.name || "Untitled Track")}
|
||||||
|
>
|
||||||
|
{String(track.name || "Untitled Track")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Track Controls - Only show when not collapsed */}
|
||||||
|
{!track.collapsed && (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-between min-h-0 overflow-hidden">
|
||||||
|
{/* Integrated Track Controls (Pan + Fader + Buttons) */}
|
||||||
|
<TrackControls
|
||||||
|
volume={track.volume}
|
||||||
|
pan={track.pan}
|
||||||
|
peakLevel={
|
||||||
|
track.recordEnabled || isRecording
|
||||||
|
? recordingLevel
|
||||||
|
: playbackLevel
|
||||||
|
}
|
||||||
|
rmsLevel={
|
||||||
|
track.recordEnabled || isRecording
|
||||||
|
? recordingLevel * 0.7
|
||||||
|
: playbackLevel * 0.7
|
||||||
|
}
|
||||||
|
isMuted={track.mute}
|
||||||
|
isSolo={track.solo}
|
||||||
|
isRecordEnabled={track.recordEnabled}
|
||||||
|
showAutomation={track.automation?.showAutomation}
|
||||||
|
showEffects={track.showEffects}
|
||||||
|
isRecording={isRecording}
|
||||||
|
onVolumeChange={onVolumeChange}
|
||||||
|
onPanChange={onPanChange}
|
||||||
|
onMuteToggle={onToggleMute}
|
||||||
|
onSoloToggle={onToggleSolo}
|
||||||
|
onRecordToggle={onToggleRecordEnable}
|
||||||
|
onAutomationToggle={() => {
|
||||||
|
onUpdateTrack(track.id, {
|
||||||
|
automation: {
|
||||||
|
...track.automation,
|
||||||
|
showAutomation: !track.automation?.showAutomation,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onEffectsClick={() => {
|
||||||
|
onUpdateTrack(track.id, {
|
||||||
|
showEffects: !track.showEffects,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onVolumeTouchStart={handleVolumeTouchStart}
|
||||||
|
onVolumeTouchEnd={handleVolumeTouchEnd}
|
||||||
|
onPanTouchStart={handlePanTouchStart}
|
||||||
|
onPanTouchEnd={handlePanTouchEnd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Import Dialog */}
|
{/* Import Dialog - Only render in controls mode to avoid duplicates */}
|
||||||
<ImportDialog
|
<ImportDialog
|
||||||
open={showImportDialog}
|
open={showImportDialog}
|
||||||
onClose={handleImportCancel}
|
onClose={handleImportCancel}
|
||||||
@@ -914,7 +825,97 @@ export function Track({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render only waveform
|
||||||
|
if (renderWaveformOnly) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative bg-waveform-bg border-b transition-all duration-200",
|
||||||
|
isSelected && "bg-primary/5",
|
||||||
|
)}
|
||||||
|
style={{ height: trackHeight }}
|
||||||
|
>
|
||||||
|
{/* Inner container with dynamic width */}
|
||||||
|
<div
|
||||||
|
className="relative h-full"
|
||||||
|
style={{
|
||||||
|
minWidth:
|
||||||
|
track.audioBuffer && zoom > 1
|
||||||
|
? `${duration * zoom * 100}px`
|
||||||
|
: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Delete Button - Top Right Overlay */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove();
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"absolute top-2 right-2 z-20 h-6 w-6 rounded flex items-center justify-center transition-all",
|
||||||
|
"bg-card/80 hover:bg-destructive/90 text-muted-foreground hover:text-white",
|
||||||
|
"border border-border/50 hover:border-destructive",
|
||||||
|
"backdrop-blur-sm shadow-sm hover:shadow-md",
|
||||||
|
)}
|
||||||
|
title="Remove track"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{track.audioBuffer ? (
|
||||||
|
<>
|
||||||
|
{/* Waveform Canvas */}
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="absolute inset-0 w-full h-full cursor-pointer"
|
||||||
|
onMouseDown={handleCanvasMouseDown}
|
||||||
|
onMouseMove={handleCanvasMouseMove}
|
||||||
|
onMouseUp={handleCanvasMouseUp}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
!track.collapsed && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 flex flex-col items-center justify-center text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer group",
|
||||||
|
isDragging
|
||||||
|
? "bg-primary/20 text-primary border-2 border-primary border-dashed"
|
||||||
|
: "hover:bg-accent/50",
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleLoadAudioClick();
|
||||||
|
}}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<Upload className="h-6 w-6 mb-2 opacity-50 group-hover:opacity-100" />
|
||||||
|
<p>
|
||||||
|
{isDragging
|
||||||
|
? "Drop audio file here"
|
||||||
|
: "Click to load audio file"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs opacity-75 mt-1">or drag & drop</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="audio/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Render full track (both controls and waveform side by side)
|
// Render full track (both controls and waveform side by side)
|
||||||
|
// This mode is no longer used - tracks are rendered separately with renderControlsOnly and renderWaveformOnly
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user