feat: refine UI with effects panel improvements and visual polish
Major improvements: - Fixed multi-file import (FileList to Array conversion) - Auto-select first track when adding to empty project - Global effects panel folding state (independent of track selection) - Effects panel collapsed/disabled when no track selected - Effect device expansion state persisted per-device - Effect browser with searchable descriptions Visual refinements: - Removed center dot from pan knob for cleaner look - Simplified fader: removed volume fill overlay, dynamic level meter visible through semi-transparent handle - Level meter capped at fader position (realistic mixer behavior) - Solid background instead of gradient for fader track - Subtle volume overlay up to fader handle - Fixed track control width flickering (consistent 4px border) - Effect devices: removed shadows/rounded corners for flatter DAW-style look, added consistent border-radius - Added border between track control and waveform area 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ 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';
|
||||
@@ -38,6 +39,8 @@ 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();
|
||||
|
||||
@@ -61,13 +64,46 @@ export function AudioEditor() {
|
||||
// Multi-track hooks
|
||||
const {
|
||||
tracks,
|
||||
addTrack,
|
||||
addTrackFromBuffer,
|
||||
addTrack: addTrackOriginal,
|
||||
addTrackFromBuffer: addTrackFromBufferOriginal,
|
||||
removeTrack,
|
||||
updateTrack,
|
||||
clearTracks,
|
||||
} = useMultiTrack();
|
||||
|
||||
// Track whether we should auto-select on next add (when project is empty)
|
||||
const shouldAutoSelectRef = React.useRef(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Update auto-select flag based on track count
|
||||
shouldAutoSelectRef.current = tracks.length === 0;
|
||||
}, [tracks.length]);
|
||||
|
||||
// Wrap addTrack to auto-select first track when adding to empty project
|
||||
const addTrack = React.useCallback((name?: string) => {
|
||||
const shouldAutoSelect = shouldAutoSelectRef.current;
|
||||
const track = addTrackOriginal(name);
|
||||
if (shouldAutoSelect) {
|
||||
setSelectedTrackId(track.id);
|
||||
shouldAutoSelectRef.current = false; // Only auto-select once
|
||||
}
|
||||
return track;
|
||||
}, [addTrackOriginal]);
|
||||
|
||||
// Wrap addTrackFromBuffer to auto-select first track when adding to empty project
|
||||
const addTrackFromBuffer = React.useCallback((buffer: AudioBuffer, name?: string) => {
|
||||
console.log(`[AudioEditor] addTrackFromBuffer wrapper called: ${name}, shouldAutoSelect: ${shouldAutoSelectRef.current}`);
|
||||
const shouldAutoSelect = shouldAutoSelectRef.current;
|
||||
const track = addTrackFromBufferOriginal(buffer, name);
|
||||
console.log(`[AudioEditor] Track created: ${track.name} (${track.id})`);
|
||||
if (shouldAutoSelect) {
|
||||
console.log(`[AudioEditor] Auto-selecting track: ${track.id}`);
|
||||
setSelectedTrackId(track.id);
|
||||
shouldAutoSelectRef.current = false; // Only auto-select once
|
||||
}
|
||||
return track;
|
||||
}, [addTrackFromBufferOriginal]);
|
||||
|
||||
// Log tracks to see if they update
|
||||
React.useEffect(() => {
|
||||
console.log('[AudioEditor] Tracks updated:', tracks.map(t => ({
|
||||
@@ -108,6 +144,7 @@ export function AudioEditor() {
|
||||
};
|
||||
|
||||
const handleImportTrack = (buffer: AudioBuffer, name: string) => {
|
||||
console.log(`[AudioEditor] handleImportTrack called: ${name}`);
|
||||
addTrackFromBuffer(buffer, name);
|
||||
};
|
||||
|
||||
@@ -171,6 +208,79 @@ export function AudioEditor() {
|
||||
updateTrack(selectedTrack.id, { effectChain: updatedChain });
|
||||
};
|
||||
|
||||
// Effects Panel handlers
|
||||
const handleAddEffect = React.useCallback((effectType: any) => {
|
||||
if (!selectedTrackId) return;
|
||||
const track = tracks.find((t) => t.id === selectedTrackId);
|
||||
if (!track) return;
|
||||
|
||||
// Import createEffect and EFFECT_NAMES dynamically
|
||||
import('@/lib/audio/effects/chain').then(({ createEffect, EFFECT_NAMES }) => {
|
||||
const newEffect = createEffect(effectType, EFFECT_NAMES[effectType]);
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: [...track.effectChain.effects, newEffect],
|
||||
};
|
||||
updateTrack(selectedTrackId, { effectChain: updatedChain });
|
||||
});
|
||||
}, [selectedTrackId, tracks, updateTrack]);
|
||||
|
||||
const handleToggleEffect = React.useCallback((effectId: string) => {
|
||||
if (!selectedTrackId) return;
|
||||
const track = tracks.find((t) => t.id === selectedTrackId);
|
||||
if (!track) return;
|
||||
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, enabled: !e.enabled } : e
|
||||
),
|
||||
};
|
||||
updateTrack(selectedTrackId, { effectChain: updatedChain });
|
||||
}, [selectedTrackId, tracks, updateTrack]);
|
||||
|
||||
const handleRemoveEffect = React.useCallback((effectId: string) => {
|
||||
if (!selectedTrackId) return;
|
||||
const track = tracks.find((t) => t.id === selectedTrackId);
|
||||
if (!track) return;
|
||||
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
|
||||
};
|
||||
updateTrack(selectedTrackId, { effectChain: updatedChain });
|
||||
}, [selectedTrackId, tracks, updateTrack]);
|
||||
|
||||
const handleUpdateEffect = React.useCallback((effectId: string, parameters: any) => {
|
||||
if (!selectedTrackId) return;
|
||||
const track = tracks.find((t) => t.id === selectedTrackId);
|
||||
if (!track) return;
|
||||
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, parameters } : e
|
||||
),
|
||||
};
|
||||
updateTrack(selectedTrackId, { effectChain: updatedChain });
|
||||
}, [selectedTrackId, tracks, updateTrack]);
|
||||
|
||||
const handleToggleEffectExpanded = React.useCallback((effectId: string) => {
|
||||
if (!selectedTrackId) return;
|
||||
const track = tracks.find((t) => t.id === selectedTrackId);
|
||||
if (!track) return;
|
||||
|
||||
const updatedChain = {
|
||||
...track.effectChain,
|
||||
effects: track.effectChain.effects.map((e) =>
|
||||
e.id === effectId ? { ...e, expanded: !e.expanded } : e
|
||||
),
|
||||
};
|
||||
updateTrack(selectedTrackId, { effectChain: updatedChain });
|
||||
}, [selectedTrackId, tracks, updateTrack]);
|
||||
|
||||
// Preserve effects panel state - don't auto-open/close on track selection
|
||||
|
||||
// Selection handler
|
||||
const handleSelectionChange = (trackId: string, selection: { start: number; end: number } | null) => {
|
||||
updateTrack(trackId, { selection });
|
||||
@@ -706,6 +816,23 @@ export function AudioEditor() {
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user