fix: restore automation lanes and effects sections in two-column layout
Restored the automation lane and effects sections that were removed during the Track component refactoring. These sections now render as full-width rows below each track, spanning across both the controls and waveforms columns. Changes: - Created TrackExtensions component for effects section rendering - Added automation lane rendering in TrackList after each track waveform - Added placeholder spacers in left controls column to maintain alignment - Effects section shows collapsible device rack with mini preview when collapsed - Automation lanes render when track.automation.showAutomation is true - Import dialog moved to waveform-only rendering mode The automation and effects sections are now properly unfoldable again. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,13 @@ import * as React from 'react';
|
||||
import { Plus, Upload } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Track } from './Track';
|
||||
import { TrackExtensions } from './TrackExtensions';
|
||||
import { ImportTrackDialog } from './ImportTrackDialog';
|
||||
import type { Track as TrackType } from '@/types/track';
|
||||
import { createEffect, type EffectType, EFFECT_NAMES } from '@/lib/audio/effects/chain';
|
||||
import { AutomationLane } from '@/components/automation/AutomationLane';
|
||||
import type { AutomationPoint as AutomationPointType } from '@/types/automation';
|
||||
import { createAutomationPoint } from '@/lib/audio/automation/utils';
|
||||
|
||||
export interface TrackListProps {
|
||||
tracks: TrackType[];
|
||||
@@ -102,91 +106,103 @@ export function TrackList({
|
||||
{/* Left Column: Track Controls (Fixed Width, No Scroll - synced with waveforms) */}
|
||||
<div ref={controlsScrollRef} className="w-48 flex-shrink-0 overflow-hidden pb-3 border-r border-border">
|
||||
{tracks.map((track) => (
|
||||
<Track
|
||||
key={track.id}
|
||||
track={track}
|
||||
zoom={zoom}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isSelected={selectedTrackId === track.id}
|
||||
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
||||
onToggleMute={() =>
|
||||
onUpdateTrack(track.id, { mute: !track.mute })
|
||||
}
|
||||
onToggleSolo={() =>
|
||||
onUpdateTrack(track.id, { solo: !track.solo })
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
onUpdateTrack(track.id, { collapsed: !track.collapsed })
|
||||
}
|
||||
onVolumeChange={(volume) =>
|
||||
onUpdateTrack(track.id, { volume })
|
||||
}
|
||||
onPanChange={(pan) =>
|
||||
onUpdateTrack(track.id, { pan })
|
||||
}
|
||||
onRemove={() => onRemoveTrack(track.id)}
|
||||
onNameChange={(name) =>
|
||||
onUpdateTrack(track.id, { name })
|
||||
}
|
||||
onUpdateTrack={onUpdateTrack}
|
||||
onSeek={onSeek}
|
||||
onLoadAudio={(buffer) =>
|
||||
onUpdateTrack(track.id, { audioBuffer: buffer })
|
||||
}
|
||||
onToggleEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onRemoveEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onUpdateEffect={(effectId, parameters) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, parameters } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onAddEffect={(effectType) => {
|
||||
const newEffect = createEffect(
|
||||
effectType,
|
||||
EFFECT_NAMES[effectType]
|
||||
);
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: [...track.effectChain.effects, newEffect],
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onSelectionChange={
|
||||
onSelectionChange
|
||||
? (selection) => onSelectionChange(track.id, selection)
|
||||
: undefined
|
||||
}
|
||||
onToggleRecordEnable={
|
||||
onToggleRecordEnable
|
||||
? () => onToggleRecordEnable(track.id)
|
||||
: undefined
|
||||
}
|
||||
isRecording={recordingTrackId === track.id}
|
||||
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
|
||||
playbackLevel={trackLevels[track.id] || 0}
|
||||
onParameterTouched={onParameterTouched}
|
||||
isPlaying={isPlaying}
|
||||
renderControlsOnly={true}
|
||||
/>
|
||||
<React.Fragment key={track.id}>
|
||||
{/* Track Controls */}
|
||||
<Track
|
||||
track={track}
|
||||
zoom={zoom}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isSelected={selectedTrackId === track.id}
|
||||
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
||||
onToggleMute={() =>
|
||||
onUpdateTrack(track.id, { mute: !track.mute })
|
||||
}
|
||||
onToggleSolo={() =>
|
||||
onUpdateTrack(track.id, { solo: !track.solo })
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
onUpdateTrack(track.id, { collapsed: !track.collapsed })
|
||||
}
|
||||
onVolumeChange={(volume) =>
|
||||
onUpdateTrack(track.id, { volume })
|
||||
}
|
||||
onPanChange={(pan) =>
|
||||
onUpdateTrack(track.id, { pan })
|
||||
}
|
||||
onRemove={() => onRemoveTrack(track.id)}
|
||||
onNameChange={(name) =>
|
||||
onUpdateTrack(track.id, { name })
|
||||
}
|
||||
onUpdateTrack={onUpdateTrack}
|
||||
onSeek={onSeek}
|
||||
onLoadAudio={(buffer) =>
|
||||
onUpdateTrack(track.id, { audioBuffer: buffer })
|
||||
}
|
||||
onToggleEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onRemoveEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onUpdateEffect={(effectId, parameters) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, parameters } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onAddEffect={(effectType) => {
|
||||
const newEffect = createEffect(
|
||||
effectType,
|
||||
EFFECT_NAMES[effectType]
|
||||
);
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: [...track.effectChain.effects, newEffect],
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onSelectionChange={
|
||||
onSelectionChange
|
||||
? (selection) => onSelectionChange(track.id, selection)
|
||||
: undefined
|
||||
}
|
||||
onToggleRecordEnable={
|
||||
onToggleRecordEnable
|
||||
? () => onToggleRecordEnable(track.id)
|
||||
: undefined
|
||||
}
|
||||
isRecording={recordingTrackId === track.id}
|
||||
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
|
||||
playbackLevel={trackLevels[track.id] || 0}
|
||||
onParameterTouched={onParameterTouched}
|
||||
isPlaying={isPlaying}
|
||||
renderControlsOnly={true}
|
||||
/>
|
||||
|
||||
{/* Spacer for Automation Lane */}
|
||||
{!track.collapsed && track.automation?.showAutomation && (
|
||||
<div className="h-32 bg-muted/30 border-b border-border" />
|
||||
)}
|
||||
|
||||
{/* Spacer for Effects Section */}
|
||||
{!track.collapsed && (
|
||||
<div className={track.showEffects ? "h-64 bg-muted/50 border-b border-border/50" : "h-8 bg-muted/50 border-b border-border/50"} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -196,93 +212,203 @@ export function TrackList({
|
||||
onScroll={handleWaveformScroll}
|
||||
className="flex-1 overflow-auto custom-scrollbar"
|
||||
>
|
||||
{tracks.map((track) => (
|
||||
<Track
|
||||
key={track.id}
|
||||
track={track}
|
||||
zoom={zoom}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isSelected={selectedTrackId === track.id}
|
||||
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
||||
onToggleMute={() =>
|
||||
onUpdateTrack(track.id, { mute: !track.mute })
|
||||
}
|
||||
onToggleSolo={() =>
|
||||
onUpdateTrack(track.id, { solo: !track.solo })
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
onUpdateTrack(track.id, { collapsed: !track.collapsed })
|
||||
}
|
||||
onVolumeChange={(volume) =>
|
||||
onUpdateTrack(track.id, { volume })
|
||||
}
|
||||
onPanChange={(pan) =>
|
||||
onUpdateTrack(track.id, { pan })
|
||||
}
|
||||
onRemove={() => onRemoveTrack(track.id)}
|
||||
onNameChange={(name) =>
|
||||
onUpdateTrack(track.id, { name })
|
||||
}
|
||||
onUpdateTrack={onUpdateTrack}
|
||||
onSeek={onSeek}
|
||||
onLoadAudio={(buffer) =>
|
||||
onUpdateTrack(track.id, { audioBuffer: buffer })
|
||||
}
|
||||
onToggleEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onRemoveEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onUpdateEffect={(effectId, parameters) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, parameters } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onAddEffect={(effectType) => {
|
||||
const newEffect = createEffect(
|
||||
effectType,
|
||||
EFFECT_NAMES[effectType]
|
||||
);
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: [...track.effectChain.effects, newEffect],
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onSelectionChange={
|
||||
onSelectionChange
|
||||
? (selection) => onSelectionChange(track.id, selection)
|
||||
: undefined
|
||||
}
|
||||
onToggleRecordEnable={
|
||||
onToggleRecordEnable
|
||||
? () => onToggleRecordEnable(track.id)
|
||||
: undefined
|
||||
}
|
||||
isRecording={recordingTrackId === track.id}
|
||||
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
|
||||
playbackLevel={trackLevels[track.id] || 0}
|
||||
onParameterTouched={onParameterTouched}
|
||||
isPlaying={isPlaying}
|
||||
renderWaveformOnly={true}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col">
|
||||
{tracks.map((track) => (
|
||||
<React.Fragment key={track.id}>
|
||||
{/* Track Waveform Row */}
|
||||
<Track
|
||||
track={track}
|
||||
zoom={zoom}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isSelected={selectedTrackId === track.id}
|
||||
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
||||
onToggleMute={() =>
|
||||
onUpdateTrack(track.id, { mute: !track.mute })
|
||||
}
|
||||
onToggleSolo={() =>
|
||||
onUpdateTrack(track.id, { solo: !track.solo })
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
onUpdateTrack(track.id, { collapsed: !track.collapsed })
|
||||
}
|
||||
onVolumeChange={(volume) =>
|
||||
onUpdateTrack(track.id, { volume })
|
||||
}
|
||||
onPanChange={(pan) =>
|
||||
onUpdateTrack(track.id, { pan })
|
||||
}
|
||||
onRemove={() => onRemoveTrack(track.id)}
|
||||
onNameChange={(name) =>
|
||||
onUpdateTrack(track.id, { name })
|
||||
}
|
||||
onUpdateTrack={onUpdateTrack}
|
||||
onSeek={onSeek}
|
||||
onLoadAudio={(buffer) =>
|
||||
onUpdateTrack(track.id, { audioBuffer: buffer })
|
||||
}
|
||||
onToggleEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onRemoveEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onUpdateEffect={(effectId, parameters) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, parameters } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onAddEffect={(effectType) => {
|
||||
const newEffect = createEffect(
|
||||
effectType,
|
||||
EFFECT_NAMES[effectType]
|
||||
);
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: [...track.effectChain.effects, newEffect],
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onSelectionChange={
|
||||
onSelectionChange
|
||||
? (selection) => onSelectionChange(track.id, selection)
|
||||
: undefined
|
||||
}
|
||||
onToggleRecordEnable={
|
||||
onToggleRecordEnable
|
||||
? () => onToggleRecordEnable(track.id)
|
||||
: undefined
|
||||
}
|
||||
isRecording={recordingTrackId === track.id}
|
||||
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
|
||||
playbackLevel={trackLevels[track.id] || 0}
|
||||
onParameterTouched={onParameterTouched}
|
||||
isPlaying={isPlaying}
|
||||
renderWaveformOnly={true}
|
||||
/>
|
||||
|
||||
{/* Automation Lane Section */}
|
||||
{!track.collapsed && track.automation?.showAutomation && (
|
||||
<div className="bg-muted/30 border-b border-border">
|
||||
{track.automation.lanes
|
||||
.filter((lane) => lane.parameterId === track.automation.selectedParameterId)
|
||||
.map((lane) => (
|
||||
<AutomationLane
|
||||
key={lane.id}
|
||||
lane={lane}
|
||||
trackId={track.id}
|
||||
zoom={zoom}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isPlaying={isPlaying}
|
||||
onAddPoint={(time, value) => {
|
||||
const newPoint = createAutomationPoint(time, value);
|
||||
const updatedLanes = track.automation.lanes.map((l) =>
|
||||
l.id === lane.id
|
||||
? { ...l, points: [...l.points, newPoint].sort((a, b) => a.time - b.time) }
|
||||
: l
|
||||
);
|
||||
onUpdateTrack(track.id, {
|
||||
automation: { ...track.automation, lanes: updatedLanes },
|
||||
});
|
||||
}}
|
||||
onUpdatePoint={(pointId, updates) => {
|
||||
const updatedLanes = track.automation.lanes.map((l) =>
|
||||
l.id === lane.id
|
||||
? {
|
||||
...l,
|
||||
points: l.points.map((p) =>
|
||||
p.id === pointId ? { ...p, ...updates } : p
|
||||
),
|
||||
}
|
||||
: l
|
||||
);
|
||||
onUpdateTrack(track.id, {
|
||||
automation: { ...track.automation, lanes: updatedLanes },
|
||||
});
|
||||
}}
|
||||
onRemovePoint={(pointId) => {
|
||||
const updatedLanes = track.automation.lanes.map((l) =>
|
||||
l.id === lane.id
|
||||
? { ...l, points: l.points.filter((p) => p.id !== pointId) }
|
||||
: l
|
||||
);
|
||||
onUpdateTrack(track.id, {
|
||||
automation: { ...track.automation, lanes: updatedLanes },
|
||||
});
|
||||
}}
|
||||
onUpdateLane={(updates) => {
|
||||
const updatedLanes = track.automation.lanes.map((l) =>
|
||||
l.id === lane.id ? { ...l, ...updates } : l
|
||||
);
|
||||
onUpdateTrack(track.id, {
|
||||
automation: { ...track.automation, lanes: updatedLanes },
|
||||
});
|
||||
}}
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Effects Section */}
|
||||
<TrackExtensions
|
||||
track={track}
|
||||
onUpdateTrack={onUpdateTrack}
|
||||
onToggleEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onRemoveEffect={(effectId) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onUpdateEffect={(effectId, parameters) => {
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, parameters } : e
|
||||
),
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
onAddEffect={(effectType) => {
|
||||
const newEffect = createEffect(
|
||||
effectType,
|
||||
EFFECT_NAMES[effectType]
|
||||
);
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: [...track.effectChain.effects, newEffect],
|
||||
};
|
||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user