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 (
|
return (
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Track List */}
|
{/* Track List - Two Column Layout */}
|
||||||
<div className="flex-1 overflow-auto custom-scrollbar">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{tracks.map((track) => (
|
{/* Left Column: Track Controls (Fixed Width, Vertical Scroll) */}
|
||||||
<Track
|
<div className="w-48 flex-shrink-0 overflow-y-auto custom-scrollbar">
|
||||||
key={track.id}
|
{tracks.map((track) => (
|
||||||
track={track}
|
<Track
|
||||||
zoom={zoom}
|
key={track.id}
|
||||||
currentTime={currentTime}
|
track={track}
|
||||||
duration={duration}
|
zoom={zoom}
|
||||||
isSelected={selectedTrackId === track.id}
|
currentTime={currentTime}
|
||||||
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
duration={duration}
|
||||||
onToggleMute={() =>
|
isSelected={selectedTrackId === track.id}
|
||||||
onUpdateTrack(track.id, { mute: !track.mute })
|
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
||||||
}
|
onToggleMute={() =>
|
||||||
onToggleSolo={() =>
|
onUpdateTrack(track.id, { mute: !track.mute })
|
||||||
onUpdateTrack(track.id, { solo: !track.solo })
|
}
|
||||||
}
|
onToggleSolo={() =>
|
||||||
onToggleCollapse={() =>
|
onUpdateTrack(track.id, { solo: !track.solo })
|
||||||
onUpdateTrack(track.id, { collapsed: !track.collapsed })
|
}
|
||||||
}
|
onToggleCollapse={() =>
|
||||||
onVolumeChange={(volume) =>
|
onUpdateTrack(track.id, { collapsed: !track.collapsed })
|
||||||
onUpdateTrack(track.id, { volume })
|
}
|
||||||
}
|
onVolumeChange={(volume) =>
|
||||||
onPanChange={(pan) =>
|
onUpdateTrack(track.id, { volume })
|
||||||
onUpdateTrack(track.id, { pan })
|
}
|
||||||
}
|
onPanChange={(pan) =>
|
||||||
onRemove={() => onRemoveTrack(track.id)}
|
onUpdateTrack(track.id, { pan })
|
||||||
onNameChange={(name) =>
|
}
|
||||||
onUpdateTrack(track.id, { name })
|
onRemove={() => onRemoveTrack(track.id)}
|
||||||
}
|
onNameChange={(name) =>
|
||||||
onUpdateTrack={onUpdateTrack}
|
onUpdateTrack(track.id, { name })
|
||||||
onSeek={onSeek}
|
}
|
||||||
onLoadAudio={(buffer) =>
|
onUpdateTrack={onUpdateTrack}
|
||||||
onUpdateTrack(track.id, { audioBuffer: buffer })
|
onSeek={onSeek}
|
||||||
}
|
onLoadAudio={(buffer) =>
|
||||||
onToggleEffect={(effectId) => {
|
onUpdateTrack(track.id, { audioBuffer: buffer })
|
||||||
const updatedChain = {
|
}
|
||||||
...track.effectChain,
|
onToggleEffect={(effectId) => {
|
||||||
effects: track.effectChain.effects.map((e) =>
|
const updatedChain = {
|
||||||
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
...track.effectChain,
|
||||||
),
|
effects: track.effectChain.effects.map((e) =>
|
||||||
};
|
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
||||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
),
|
||||||
}}
|
};
|
||||||
onRemoveEffect={(effectId) => {
|
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||||
const updatedChain = {
|
}}
|
||||||
...track.effectChain,
|
onRemoveEffect={(effectId) => {
|
||||||
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
const updatedChain = {
|
||||||
};
|
...track.effectChain,
|
||||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
||||||
}}
|
};
|
||||||
onUpdateEffect={(effectId, parameters) => {
|
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||||
const updatedChain = {
|
}}
|
||||||
...track.effectChain,
|
onUpdateEffect={(effectId, parameters) => {
|
||||||
effects: track.effectChain.effects.map((e) =>
|
const updatedChain = {
|
||||||
e.id === effectId ? { ...e, parameters } : e
|
...track.effectChain,
|
||||||
),
|
effects: track.effectChain.effects.map((e) =>
|
||||||
};
|
e.id === effectId ? { ...e, parameters } : e
|
||||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
),
|
||||||
}}
|
};
|
||||||
onAddEffect={(effectType) => {
|
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||||
const newEffect = createEffect(
|
}}
|
||||||
effectType,
|
onAddEffect={(effectType) => {
|
||||||
EFFECT_NAMES[effectType]
|
const newEffect = createEffect(
|
||||||
);
|
effectType,
|
||||||
const updatedChain = {
|
EFFECT_NAMES[effectType]
|
||||||
...track.effectChain,
|
);
|
||||||
effects: [...track.effectChain.effects, newEffect],
|
const updatedChain = {
|
||||||
};
|
...track.effectChain,
|
||||||
onUpdateTrack(track.id, { effectChain: updatedChain });
|
effects: [...track.effectChain.effects, newEffect],
|
||||||
}}
|
};
|
||||||
onSelectionChange={
|
onUpdateTrack(track.id, { effectChain: updatedChain });
|
||||||
onSelectionChange
|
}}
|
||||||
? (selection) => onSelectionChange(track.id, selection)
|
onSelectionChange={
|
||||||
: undefined
|
onSelectionChange
|
||||||
}
|
? (selection) => onSelectionChange(track.id, selection)
|
||||||
onToggleRecordEnable={
|
: undefined
|
||||||
onToggleRecordEnable
|
}
|
||||||
? () => onToggleRecordEnable(track.id)
|
onToggleRecordEnable={
|
||||||
: undefined
|
onToggleRecordEnable
|
||||||
}
|
? () => onToggleRecordEnable(track.id)
|
||||||
isRecording={recordingTrackId === track.id}
|
: undefined
|
||||||
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
|
}
|
||||||
playbackLevel={trackLevels[track.id] || 0}
|
isRecording={recordingTrackId === track.id}
|
||||||
onParameterTouched={onParameterTouched}
|
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
|
||||||
isPlaying={isPlaying}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Import Dialog */}
|
{/* Import Dialog */}
|
||||||
|
|||||||
Reference in New Issue
Block a user