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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,6 +59,7 @@ export function createTrack(name?: string, color?: TrackColor): Track {
|
||||
},
|
||||
collapsed: false,
|
||||
selected: false,
|
||||
showEffects: false,
|
||||
selection: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user