feat: implement synchronized horizontal scrolling for track waveforms
Refactored track layout to use a two-column design where: - Left column: All track control panels (fixed, vertical scroll) - Right column: All waveforms (shared horizontal scroll container) This ensures all track waveforms scroll together horizontally when zoomed in, providing a more cohesive DAW-like experience. Changes: - Added renderControlsOnly and renderWaveformOnly props to Track component - Track component now supports conditional rendering of just controls or just waveform - TrackList renders each track twice: once for controls, once for waveforms - Waveforms share a common scrollable container for synchronized scrolling - Track controls stay fixed while waveforms scroll horizontally together 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -86,96 +86,196 @@ export function TrackList({
|
||||
);
|
||||
}
|
||||
|
||||
const waveformScrollRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Track List */}
|
||||
<div 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}
|
||||
/>
|
||||
))}
|
||||
{/* Track List - Two Column Layout */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left Column: Track Controls (Fixed Width, Vertical Scroll) */}
|
||||
<div className="w-48 flex-shrink-0 overflow-y-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}
|
||||
renderControlsOnly={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right Column: Waveforms (Flexible Width, Shared Horizontal Scroll) */}
|
||||
<div
|
||||
ref={waveformScrollRef}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Import Dialog */}
|
||||
|
||||
Reference in New Issue
Block a user