Files
audio-ui/components/effects/EffectRack.tsx
Sebastian Krüger 0986896756 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>
2025-11-17 20:27:08 +01:00

145 lines
4.4 KiB
TypeScript

'use client';
import * as React from 'react';
import { GripVertical, Power, PowerOff, Trash2, Settings } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import type { ChainEffect, EffectChain } from '@/lib/audio/effects/chain';
import { EFFECT_NAMES } from '@/lib/audio/effects/chain';
export interface EffectRackProps {
chain: EffectChain;
onToggleEffect: (effectId: string) => void;
onRemoveEffect: (effectId: string) => void;
onReorderEffects: (fromIndex: number, toIndex: number) => void;
onEditEffect?: (effect: ChainEffect) => void;
className?: string;
}
export function EffectRack({
chain,
onToggleEffect,
onRemoveEffect,
onReorderEffects,
onEditEffect,
className,
}: EffectRackProps) {
const [draggedIndex, setDraggedIndex] = React.useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = React.useState<number | null>(null);
const handleDragStart = (e: React.DragEvent, index: number) => {
setDraggedIndex(index);
e.dataTransfer.effectAllowed = 'move';
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggedIndex === null || draggedIndex === index) return;
setDragOverIndex(index);
};
const handleDrop = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggedIndex === null || draggedIndex === index) return;
onReorderEffects(draggedIndex, index);
setDraggedIndex(null);
setDragOverIndex(null);
};
const handleDragEnd = () => {
setDraggedIndex(null);
setDragOverIndex(null);
};
if (chain.effects.length === 0) {
return (
<div className={cn('p-4 text-center', className)}>
<p className="text-sm text-muted-foreground">
No effects in chain. Add effects from the side panel to get started.
</p>
</div>
);
}
return (
<div className={cn('space-y-2', className)}>
{chain.effects.map((effect, index) => (
<div
key={effect.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
onDragEnd={handleDragEnd}
className={cn(
'flex items-center gap-2 p-3 rounded-lg border transition-all',
effect.enabled
? 'bg-card border-border'
: 'bg-muted/50 border-border/50 opacity-60',
draggedIndex === index && 'opacity-50',
dragOverIndex === index && 'border-primary'
)}
>
{/* Drag Handle */}
<div
className="cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground"
title="Drag to reorder"
>
<GripVertical className="h-4 w-4" />
</div>
{/* Effect Info */}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground truncate">
{effect.name}
</div>
<div className="text-xs text-muted-foreground">
{EFFECT_NAMES[effect.type]}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-1">
{/* Edit Button (if edit handler provided) */}
{onEditEffect && (
<Button
variant="ghost"
size="icon-sm"
onClick={() => onEditEffect(effect)}
title="Edit parameters"
>
<Settings className="h-4 w-4" />
</Button>
)}
{/* Toggle Enable/Disable */}
<Button
variant="ghost"
size="icon-sm"
onClick={() => onToggleEffect(effect.id)}
title={effect.enabled ? 'Disable effect' : 'Enable effect'}
>
{effect.enabled ? (
<Power className="h-4 w-4 text-success" />
) : (
<PowerOff className="h-4 w-4 text-muted-foreground" />
)}
</Button>
{/* Remove */}
<Button
variant="ghost"
size="icon-sm"
onClick={() => onRemoveEffect(effect.id)}
title="Remove effect"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
))}
</div>
);
}