refactor: move effects panel from global to per-track

- Added showEffects property to Track type
- Added "E" button with Sparkles icon to toggle per-track effects
- Effects panel now appears below each track when toggled
- Removed global EffectsPanel from AudioEditor
- Updated useMultiTrack to persist showEffects state
- Streamlined workflow: both automation and effects are now per-track

This aligns the UX with professional DAWs like Ableton Live, where
effects and automation are track-scoped rather than global.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 18:44:06 +01:00
parent 03a7e2e485
commit d2709b8fc2
5 changed files with 82 additions and 22 deletions

View File

@@ -14,7 +14,6 @@ import { useEffectChain } from '@/lib/hooks/useEffectChain';
import { useToast } from '@/components/ui/Toast';
import { TrackList } from '@/components/tracks/TrackList';
import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog';
import { EffectsPanel } from '@/components/effects/EffectsPanel';
import { formatDuration } from '@/lib/audio/decoder';
import { useHistory } from '@/lib/hooks/useHistory';
import { useRecording } from '@/lib/hooks/useRecording';
@@ -40,8 +39,6 @@ export function AudioEditor() {
const [punchOutTime, setPunchOutTime] = React.useState(0);
const [overdubEnabled, setOverdubEnabled] = React.useState(false);
const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false);
const [effectsPanelHeight, setEffectsPanelHeight] = React.useState(300);
const [effectsPanelVisible, setEffectsPanelVisible] = React.useState(false);
const { addToast } = useToast();
@@ -816,24 +813,6 @@ export function AudioEditor() {
trackLevels={trackLevels}
/>
</div>
{/* Effects Panel - Global Folding State, Collapsed when no track */}
<EffectsPanel
track={selectedTrack || null}
visible={selectedTrack ? effectsPanelVisible : false}
height={effectsPanelHeight}
onToggleVisible={() => {
if (selectedTrack) {
setEffectsPanelVisible(!effectsPanelVisible);
}
}}
onResizeHeight={setEffectsPanelHeight}
onAddEffect={handleAddEffect}
onToggleEffect={handleToggleEffect}
onRemoveEffect={handleRemoveEffect}
onUpdateEffect={handleUpdateEffect}
onToggleEffectExpanded={handleToggleEffectExpanded}
/>
</main>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, UnfoldHorizontal, Upload, Mic, Gauge, Circle } from 'lucide-react';
import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, UnfoldHorizontal, Upload, Mic, Gauge, Circle, Sparkles } from 'lucide-react';
import type { Track as TrackType } from '@/types/track';
import { COLLAPSED_TRACK_HEIGHT, MIN_TRACK_HEIGHT, MAX_TRACK_HEIGHT } from '@/types/track';
import { Button } from '@/components/ui/Button';
@@ -13,6 +13,8 @@ import { CircularKnob } from '@/components/ui/CircularKnob';
import { AutomationLane } from '@/components/automation/AutomationLane';
import type { AutomationLane as AutomationLaneType, AutomationPoint as AutomationPointType } from '@/types/automation';
import { createAutomationPoint } from '@/lib/audio/automation/utils';
import { EffectDevice } from '@/components/effects/EffectDevice';
import { EffectBrowser } from '@/components/effects/EffectBrowser';
export interface TrackProps {
track: TrackType;
@@ -78,6 +80,7 @@ export function Track({
const inputRef = React.useRef<HTMLInputElement>(null);
const [isResizing, setIsResizing] = React.useState(false);
const resizeStartRef = React.useRef({ y: 0, height: 0 });
const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false);
// Selection state
const [isSelecting, setIsSelecting] = React.useState(false);
@@ -617,6 +620,24 @@ export function Track({
>
A
</button>
{/* Effects Toggle */}
<button
onClick={() => {
onUpdateTrack(track.id, {
showEffects: !track.showEffects,
});
}}
className={cn(
'h-6 w-6 rounded flex items-center justify-center transition-all',
track.showEffects
? 'bg-primary text-primary-foreground shadow-md shadow-primary/30'
: 'bg-card hover:bg-accent text-muted-foreground border border-border/50'
)}
title={track.showEffects ? 'Hide effects' : 'Show effects'}
>
<Sparkles className="h-3 w-3" />
</button>
</div>
</div>
</div>
@@ -751,6 +772,62 @@ export function Track({
</div>
)}
{/* Per-Track Effects Panel */}
{!track.collapsed && track.showEffects && (
<div className="bg-background/30 border-t border-border/50 p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-foreground">
{track.name} - Effects
</span>
<Button
variant="ghost"
size="sm"
onClick={() => setEffectBrowserOpen(true)}
className="h-7 px-2 text-xs"
>
<Sparkles className="h-3 w-3 mr-1" />
Add Effect
</Button>
</div>
{track.effectChain.effects.length === 0 ? (
<div className="text-center py-8 text-sm text-muted-foreground">
No effects on this track
</div>
) : (
<div className="flex gap-3 overflow-x-auto custom-scrollbar">
{track.effectChain.effects.map((effect) => (
<EffectDevice
key={effect.id}
effect={effect}
onToggleEnabled={() => onToggleEffect?.(effect.id)}
onRemove={() => onRemoveEffect?.(effect.id)}
onUpdateParameters={(params) => onUpdateEffect?.(effect.id, params)}
onToggleExpanded={() => {
const updatedEffects = track.effectChain.effects.map((e) =>
e.id === effect.id ? { ...e, expanded: !e.expanded } : e
);
onUpdateTrack(track.id, {
effectChain: { ...track.effectChain, effects: updatedEffects },
});
}}
/>
))}
</div>
)}
{/* Effect Browser Dialog */}
<EffectBrowser
open={effectBrowserOpen}
onClose={() => setEffectBrowserOpen(false)}
onSelectEffect={(effectType) => {
onAddEffect?.(effectType);
setEffectBrowserOpen(false);
}}
/>
</div>
)}
{/* Track Height Resize Handle */}
{!track.collapsed && (
<div

View File

@@ -59,6 +59,7 @@ export function createTrack(name?: string, color?: TrackColor): Track {
},
collapsed: false,
selected: false,
showEffects: false,
selection: null,
};
}

View File

@@ -35,6 +35,7 @@ export function useMultiTrack() {
effectChain: t.effectChain || createEffectChain(`${t.name} Effects`), // Restore effect chain or create new
automation: t.automation || { lanes: [], showAutomation: false }, // Restore automation or create new
selection: t.selection || null, // Initialize selection
showEffects: t.showEffects || false, // Restore showEffects state
}));
}
} catch (error) {
@@ -64,6 +65,7 @@ export function useMultiTrack() {
recordEnabled: track.recordEnabled,
collapsed: track.collapsed,
selected: track.selected,
showEffects: track.showEffects, // Save effects panel state
effectChain: track.effectChain, // Save effect chain
automation: track.automation, // Save automation data
}));

View File

@@ -32,6 +32,7 @@ export interface Track {
// UI state
collapsed: boolean;
selected: boolean;
showEffects: boolean; // Show/hide per-track effects panel
// Selection (for editing operations)
selection: Selection | null;