Files
audio-ui/components/effects/PresetManager.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

251 lines
8.2 KiB
TypeScript

'use client';
import * as React from 'react';
import { Save, FolderOpen, Trash2, Download, Upload } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import type { EffectChain, EffectPreset } from '@/lib/audio/effects/chain';
import { createPreset, loadPreset } from '@/lib/audio/effects/chain';
export interface PresetManagerProps {
open: boolean;
onClose: () => void;
currentChain: EffectChain;
presets: EffectPreset[];
onSavePreset: (preset: EffectPreset) => void;
onLoadPreset: (preset: EffectPreset) => void;
onDeletePreset: (presetId: string) => void;
onExportPreset?: (preset: EffectPreset) => void;
onImportPreset?: (preset: EffectPreset) => void;
}
export function PresetManager({
open,
onClose,
currentChain,
presets,
onSavePreset,
onLoadPreset,
onDeletePreset,
onExportPreset,
onImportPreset,
}: PresetManagerProps) {
const [presetName, setPresetName] = React.useState('');
const [presetDescription, setPresetDescription] = React.useState('');
const [mode, setMode] = React.useState<'list' | 'create'>('list');
const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleSave = () => {
if (!presetName.trim()) return;
const preset = createPreset(currentChain, presetName.trim(), presetDescription.trim());
onSavePreset(preset);
setPresetName('');
setPresetDescription('');
setMode('list');
};
const handleLoad = (preset: EffectPreset) => {
onLoadPreset(preset);
onClose();
};
const handleExport = (preset: EffectPreset) => {
if (!onExportPreset) return;
const data = JSON.stringify(preset, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${preset.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
onExportPreset(preset);
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !onImportPreset) return;
try {
const text = await file.text();
const preset: EffectPreset = JSON.parse(text);
// Validate preset structure
if (!preset.id || !preset.name || !preset.chain) {
throw new Error('Invalid preset format');
}
onImportPreset(preset);
} catch (error) {
console.error('Failed to import preset:', error);
alert('Failed to import preset. Please check the file format.');
}
// Reset input
e.target.value = '';
};
return (
<Modal
open={open}
onClose={onClose}
title="Effect Presets"
className="max-w-2xl"
>
<div className="space-y-4">
{/* Mode Toggle */}
<div className="flex items-center gap-2 border-b border-border pb-3">
<Button
variant={mode === 'list' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setMode('list')}
>
<FolderOpen className="h-4 w-4 mr-2" />
Load Preset
</Button>
<Button
variant={mode === 'create' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setMode('create')}
>
<Save className="h-4 w-4 mr-2" />
Save Current
</Button>
{onImportPreset && (
<>
<Button variant="ghost" size="sm" onClick={handleImportClick}>
<Upload className="h-4 w-4 mr-2" />
Import
</Button>
<input
ref={fileInputRef}
type="file"
accept=".json"
onChange={handleFileChange}
className="hidden"
/>
</>
)}
</div>
{/* Create Mode */}
{mode === 'create' && (
<div className="space-y-3">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Preset Name
</label>
<input
type="text"
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
placeholder="My Awesome Preset"
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Description (optional)
</label>
<textarea
value={presetDescription}
onChange={(e) => setPresetDescription(e.target.value)}
placeholder="What does this preset do?"
rows={3}
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm resize-none"
/>
</div>
<div className="text-xs text-muted-foreground">
Current chain has {currentChain.effects.length} effect
{currentChain.effects.length !== 1 ? 's' : ''}
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setMode('list')}>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={!presetName.trim()}
>
<Save className="h-4 w-4 mr-2" />
Save Preset
</Button>
</div>
</div>
)}
{/* List Mode */}
{mode === 'list' && (
<div className="space-y-2 max-h-96 overflow-y-auto custom-scrollbar">
{presets.length === 0 ? (
<div className="text-center py-8 text-sm text-muted-foreground">
No presets saved yet. Create one to get started!
</div>
) : (
presets.map((preset) => (
<div
key={preset.id}
className="flex items-start gap-3 p-3 rounded-lg border border-border bg-card hover:bg-accent/50 transition-colors"
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground">
{preset.name}
</div>
{preset.description && (
<div className="text-xs text-muted-foreground mt-1">
{preset.description}
</div>
)}
<div className="text-xs text-muted-foreground mt-1">
{preset.chain.effects.length} effect
{preset.chain.effects.length !== 1 ? 's' : ''} {' '}
{new Date(preset.createdAt).toLocaleDateString()}
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon-sm"
onClick={() => handleLoad(preset)}
title="Load preset"
>
<FolderOpen className="h-4 w-4" />
</Button>
{onExportPreset && (
<Button
variant="ghost"
size="icon-sm"
onClick={() => handleExport(preset)}
title="Export preset"
>
<Download className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon-sm"
onClick={() => onDeletePreset(preset.id)}
title="Delete preset"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
))
)}
</div>
)}
</div>
</Modal>
);
}