feat: add Phase 6.6 Effect Chain Management system
Effect Chain System: - Create comprehensive effect chain types and state management - Implement EffectRack component with drag-and-drop reordering - Add enable/disable toggle for individual effects - Build PresetManager for save/load/import/export functionality - Create useEffectChain hook with localStorage persistence UI Integration: - Add Chain tab to SidePanel with effect rack - Integrate preset manager dialog - Add chain management controls (clear, presets) - Update SidePanel with chain props and handlers Features: - Drag-and-drop effect reordering with visual feedback - Effect bypass/enable toggle with power icons - Save effect chains as presets with descriptions - Import/export presets as JSON files - localStorage persistence for chains and presets - Visual status indicators for enabled/disabled effects - Preset timestamp and effect count display Components Created: - /lib/audio/effects/chain.ts - Effect chain types and utilities - /components/effects/EffectRack.tsx - Visual effect chain component - /components/effects/PresetManager.tsx - Preset management dialog - /lib/hooks/useEffectChain.ts - Effect chain state hook Updated PLAN.md: - Mark Phase 6.6 as complete - Update current status to Phase 6.6 Complete - Add effect chain features to working features list - Update Next Steps to show Phase 6 complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,18 @@ import {
|
||||
Download,
|
||||
X,
|
||||
Sparkles,
|
||||
Link2,
|
||||
FolderOpen,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { formatDuration } from '@/lib/audio/decoder';
|
||||
import type { Selection } from '@/types/selection';
|
||||
import type { HistoryState } from '@/lib/history/history-manager';
|
||||
import type { EffectChain, ChainEffect, EffectPreset } from '@/lib/audio/effects/chain';
|
||||
import { EffectRack } from '@/components/effects/EffectRack';
|
||||
import { PresetManager } from '@/components/effects/PresetManager';
|
||||
|
||||
export interface SidePanelProps {
|
||||
// File info
|
||||
@@ -31,6 +37,17 @@ export interface SidePanelProps {
|
||||
// History info
|
||||
historyState: HistoryState;
|
||||
|
||||
// Effect chain
|
||||
effectChain: EffectChain;
|
||||
effectPresets: EffectPreset[];
|
||||
onToggleEffect: (effectId: string) => void;
|
||||
onRemoveEffect: (effectId: string) => void;
|
||||
onReorderEffects: (fromIndex: number, toIndex: number) => void;
|
||||
onSavePreset: (preset: EffectPreset) => void;
|
||||
onLoadPreset: (preset: EffectPreset) => void;
|
||||
onDeletePreset: (presetId: string) => void;
|
||||
onClearChain: () => void;
|
||||
|
||||
// Effects handlers
|
||||
onNormalize: () => void;
|
||||
onFadeIn: () => void;
|
||||
@@ -62,6 +79,15 @@ export function SidePanel({
|
||||
onClear,
|
||||
selection,
|
||||
historyState,
|
||||
effectChain,
|
||||
effectPresets,
|
||||
onToggleEffect,
|
||||
onRemoveEffect,
|
||||
onReorderEffects,
|
||||
onSavePreset,
|
||||
onLoadPreset,
|
||||
onDeletePreset,
|
||||
onClearChain,
|
||||
onNormalize,
|
||||
onFadeIn,
|
||||
onFadeOut,
|
||||
@@ -84,7 +110,8 @@ export function SidePanel({
|
||||
className,
|
||||
}: SidePanelProps) {
|
||||
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
||||
const [activeTab, setActiveTab] = React.useState<'file' | 'history' | 'info' | 'effects'>('file');
|
||||
const [activeTab, setActiveTab] = React.useState<'file' | 'chain' | 'history' | 'info' | 'effects'>('file');
|
||||
const [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileClick = () => {
|
||||
@@ -131,6 +158,14 @@ export function SidePanel({
|
||||
>
|
||||
<FileAudio className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'chain' ? 'secondary' : 'ghost'}
|
||||
size="icon-sm"
|
||||
onClick={() => setActiveTab('chain')}
|
||||
title="Effect Chain"
|
||||
>
|
||||
<Link2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'effects' ? 'secondary' : 'ghost'}
|
||||
size="icon-sm"
|
||||
@@ -227,6 +262,53 @@ export function SidePanel({
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'chain' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Effect Chain
|
||||
</h3>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setPresetDialogOpen(true)}
|
||||
title="Manage presets"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
{effectChain.effects.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onClearChain}
|
||||
title="Clear all effects"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EffectRack
|
||||
chain={effectChain}
|
||||
onToggleEffect={onToggleEffect}
|
||||
onRemoveEffect={onRemoveEffect}
|
||||
onReorderEffects={onReorderEffects}
|
||||
/>
|
||||
<PresetManager
|
||||
open={presetDialogOpen}
|
||||
onClose={() => setPresetDialogOpen(false)}
|
||||
currentChain={effectChain}
|
||||
presets={effectPresets}
|
||||
onSavePreset={onSavePreset}
|
||||
onLoadPreset={onLoadPreset}
|
||||
onDeletePreset={onDeletePreset}
|
||||
onExportPreset={() => {}}
|
||||
onImportPreset={(preset) => onSavePreset(preset)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
|
||||
Reference in New Issue
Block a user