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:
2025-11-17 20:27:08 +01:00
parent bc4e75167f
commit 0986896756
7 changed files with 903 additions and 11 deletions

View File

@@ -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">