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 { useToast } from '@/components/ui/Toast';
|
||||||
import { TrackList } from '@/components/tracks/TrackList';
|
import { TrackList } from '@/components/tracks/TrackList';
|
||||||
import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog';
|
import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog';
|
||||||
import { EffectsPanel } from '@/components/effects/EffectsPanel';
|
|
||||||
import { formatDuration } from '@/lib/audio/decoder';
|
import { formatDuration } from '@/lib/audio/decoder';
|
||||||
import { useHistory } from '@/lib/hooks/useHistory';
|
import { useHistory } from '@/lib/hooks/useHistory';
|
||||||
import { useRecording } from '@/lib/hooks/useRecording';
|
import { useRecording } from '@/lib/hooks/useRecording';
|
||||||
@@ -40,8 +39,6 @@ export function AudioEditor() {
|
|||||||
const [punchOutTime, setPunchOutTime] = React.useState(0);
|
const [punchOutTime, setPunchOutTime] = React.useState(0);
|
||||||
const [overdubEnabled, setOverdubEnabled] = React.useState(false);
|
const [overdubEnabled, setOverdubEnabled] = React.useState(false);
|
||||||
const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false);
|
const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false);
|
||||||
const [effectsPanelHeight, setEffectsPanelHeight] = React.useState(300);
|
|
||||||
const [effectsPanelVisible, setEffectsPanelVisible] = React.useState(false);
|
|
||||||
|
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
|
|
||||||
@@ -816,24 +813,6 @@ export function AudioEditor() {
|
|||||||
trackLevels={trackLevels}
|
trackLevels={trackLevels}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
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 type { Track as TrackType } from '@/types/track';
|
||||||
import { COLLAPSED_TRACK_HEIGHT, MIN_TRACK_HEIGHT, MAX_TRACK_HEIGHT } from '@/types/track';
|
import { COLLAPSED_TRACK_HEIGHT, MIN_TRACK_HEIGHT, MAX_TRACK_HEIGHT } from '@/types/track';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -13,6 +13,8 @@ import { CircularKnob } from '@/components/ui/CircularKnob';
|
|||||||
import { AutomationLane } from '@/components/automation/AutomationLane';
|
import { AutomationLane } from '@/components/automation/AutomationLane';
|
||||||
import type { AutomationLane as AutomationLaneType, AutomationPoint as AutomationPointType } from '@/types/automation';
|
import type { AutomationLane as AutomationLaneType, AutomationPoint as AutomationPointType } from '@/types/automation';
|
||||||
import { createAutomationPoint } from '@/lib/audio/automation/utils';
|
import { createAutomationPoint } from '@/lib/audio/automation/utils';
|
||||||
|
import { EffectDevice } from '@/components/effects/EffectDevice';
|
||||||
|
import { EffectBrowser } from '@/components/effects/EffectBrowser';
|
||||||
|
|
||||||
export interface TrackProps {
|
export interface TrackProps {
|
||||||
track: TrackType;
|
track: TrackType;
|
||||||
@@ -78,6 +80,7 @@ export function Track({
|
|||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
const [isResizing, setIsResizing] = React.useState(false);
|
const [isResizing, setIsResizing] = React.useState(false);
|
||||||
const resizeStartRef = React.useRef({ y: 0, height: 0 });
|
const resizeStartRef = React.useRef({ y: 0, height: 0 });
|
||||||
|
const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false);
|
||||||
|
|
||||||
// Selection state
|
// Selection state
|
||||||
const [isSelecting, setIsSelecting] = React.useState(false);
|
const [isSelecting, setIsSelecting] = React.useState(false);
|
||||||
@@ -617,6 +620,24 @@ export function Track({
|
|||||||
>
|
>
|
||||||
A
|
A
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -751,6 +772,62 @@ export function Track({
|
|||||||
</div>
|
</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 Height Resize Handle */}
|
||||||
{!track.collapsed && (
|
{!track.collapsed && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export function createTrack(name?: string, color?: TrackColor): Track {
|
|||||||
},
|
},
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
showEffects: false,
|
||||||
selection: null,
|
selection: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export function useMultiTrack() {
|
|||||||
effectChain: t.effectChain || createEffectChain(`${t.name} Effects`), // Restore effect chain or create new
|
effectChain: t.effectChain || createEffectChain(`${t.name} Effects`), // Restore effect chain or create new
|
||||||
automation: t.automation || { lanes: [], showAutomation: false }, // Restore automation or create new
|
automation: t.automation || { lanes: [], showAutomation: false }, // Restore automation or create new
|
||||||
selection: t.selection || null, // Initialize selection
|
selection: t.selection || null, // Initialize selection
|
||||||
|
showEffects: t.showEffects || false, // Restore showEffects state
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -64,6 +65,7 @@ export function useMultiTrack() {
|
|||||||
recordEnabled: track.recordEnabled,
|
recordEnabled: track.recordEnabled,
|
||||||
collapsed: track.collapsed,
|
collapsed: track.collapsed,
|
||||||
selected: track.selected,
|
selected: track.selected,
|
||||||
|
showEffects: track.showEffects, // Save effects panel state
|
||||||
effectChain: track.effectChain, // Save effect chain
|
effectChain: track.effectChain, // Save effect chain
|
||||||
automation: track.automation, // Save automation data
|
automation: track.automation, // Save automation data
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface Track {
|
|||||||
// UI state
|
// UI state
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
showEffects: boolean; // Show/hide per-track effects panel
|
||||||
|
|
||||||
// Selection (for editing operations)
|
// Selection (for editing operations)
|
||||||
selection: Selection | null;
|
selection: Selection | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user