2025-11-17 20:03:40 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import * as React from 'react';
|
|
|
|
|
import {
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
Upload,
|
2025-11-17 22:17:09 +01:00
|
|
|
Plus,
|
|
|
|
|
Trash2,
|
2025-11-17 20:27:08 +01:00
|
|
|
Link2,
|
|
|
|
|
FolderOpen,
|
2025-11-17 22:17:09 +01:00
|
|
|
Music2,
|
2025-11-17 20:03:40 +01:00
|
|
|
} from 'lucide-react';
|
|
|
|
|
import { Button } from '@/components/ui/Button';
|
|
|
|
|
import { cn } from '@/lib/utils/cn';
|
2025-11-17 22:17:09 +01:00
|
|
|
import type { Track } from '@/types/track';
|
|
|
|
|
import type { EffectChain, EffectPreset } from '@/lib/audio/effects/chain';
|
2025-11-17 20:27:08 +01:00
|
|
|
import { EffectRack } from '@/components/effects/EffectRack';
|
|
|
|
|
import { PresetManager } from '@/components/effects/PresetManager';
|
2025-11-17 20:03:40 +01:00
|
|
|
|
|
|
|
|
export interface SidePanelProps {
|
2025-11-17 22:17:09 +01:00
|
|
|
tracks: Track[];
|
|
|
|
|
selectedTrackId: string | null;
|
|
|
|
|
onSelectTrack: (trackId: string | null) => void;
|
|
|
|
|
onAddTrack: () => void;
|
|
|
|
|
onImportTracks: () => void;
|
|
|
|
|
onUpdateTrack: (trackId: string, updates: Partial<Track>) => void;
|
|
|
|
|
onRemoveTrack: (trackId: string) => void;
|
|
|
|
|
onClearTracks: () => void;
|
2025-11-17 20:03:40 +01:00
|
|
|
|
2025-11-18 07:30:46 +01:00
|
|
|
// Track effect chain (for selected track)
|
|
|
|
|
trackEffectChain: EffectChain | null;
|
|
|
|
|
onToggleTrackEffect: (effectId: string) => void;
|
|
|
|
|
onRemoveTrackEffect: (effectId: string) => void;
|
|
|
|
|
onReorderTrackEffects: (fromIndex: number, toIndex: number) => void;
|
|
|
|
|
onClearTrackChain: () => void;
|
|
|
|
|
|
|
|
|
|
// Master effect chain
|
|
|
|
|
masterEffectChain: EffectChain;
|
|
|
|
|
masterEffectPresets: EffectPreset[];
|
|
|
|
|
onToggleMasterEffect: (effectId: string) => void;
|
|
|
|
|
onRemoveMasterEffect: (effectId: string) => void;
|
|
|
|
|
onReorderMasterEffects: (fromIndex: number, toIndex: number) => void;
|
|
|
|
|
onSaveMasterPreset: (preset: EffectPreset) => void;
|
|
|
|
|
onLoadMasterPreset: (preset: EffectPreset) => void;
|
|
|
|
|
onDeleteMasterPreset: (presetId: string) => void;
|
|
|
|
|
onClearMasterChain: () => void;
|
2025-11-17 20:27:08 +01:00
|
|
|
|
2025-11-17 20:03:40 +01:00
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SidePanel({
|
2025-11-17 22:17:09 +01:00
|
|
|
tracks,
|
|
|
|
|
selectedTrackId,
|
|
|
|
|
onSelectTrack,
|
|
|
|
|
onAddTrack,
|
|
|
|
|
onImportTracks,
|
|
|
|
|
onUpdateTrack,
|
|
|
|
|
onRemoveTrack,
|
|
|
|
|
onClearTracks,
|
2025-11-18 07:30:46 +01:00
|
|
|
trackEffectChain,
|
|
|
|
|
onToggleTrackEffect,
|
|
|
|
|
onRemoveTrackEffect,
|
|
|
|
|
onReorderTrackEffects,
|
|
|
|
|
onClearTrackChain,
|
|
|
|
|
masterEffectChain,
|
|
|
|
|
masterEffectPresets,
|
|
|
|
|
onToggleMasterEffect,
|
|
|
|
|
onRemoveMasterEffect,
|
|
|
|
|
onReorderMasterEffects,
|
|
|
|
|
onSaveMasterPreset,
|
|
|
|
|
onLoadMasterPreset,
|
|
|
|
|
onDeleteMasterPreset,
|
|
|
|
|
onClearMasterChain,
|
2025-11-17 20:03:40 +01:00
|
|
|
className,
|
|
|
|
|
}: SidePanelProps) {
|
|
|
|
|
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
2025-11-18 07:41:16 +01:00
|
|
|
const [activeTab, setActiveTab] = React.useState<'tracks' | 'master'>('tracks');
|
2025-11-17 20:27:08 +01:00
|
|
|
const [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
|
2025-11-17 20:03:40 +01:00
|
|
|
|
2025-11-17 22:17:09 +01:00
|
|
|
const selectedTrack = tracks.find((t) => t.id === selectedTrackId);
|
2025-11-17 20:03:40 +01:00
|
|
|
|
|
|
|
|
if (isCollapsed) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'w-12 bg-card border-r border-border flex flex-col items-center py-2',
|
|
|
|
|
className
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
onClick={() => setIsCollapsed(false)}
|
|
|
|
|
title="Expand Side Panel"
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-17 22:17:09 +01:00
|
|
|
<div className={cn('w-80 bg-card border-r border-border flex flex-col', className)}>
|
2025-11-17 20:03:40 +01:00
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between p-2 border-b border-border">
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<Button
|
2025-11-17 22:17:09 +01:00
|
|
|
variant={activeTab === 'tracks' ? 'secondary' : 'ghost'}
|
2025-11-18 07:41:16 +01:00
|
|
|
size="sm"
|
2025-11-17 22:17:09 +01:00
|
|
|
onClick={() => setActiveTab('tracks')}
|
|
|
|
|
title="Tracks"
|
2025-11-17 20:03:40 +01:00
|
|
|
>
|
2025-11-18 07:41:16 +01:00
|
|
|
<Music2 className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Tracks
|
2025-11-17 20:03:40 +01:00
|
|
|
</Button>
|
2025-11-17 20:27:08 +01:00
|
|
|
<Button
|
2025-11-18 07:41:16 +01:00
|
|
|
variant={activeTab === 'master' ? 'secondary' : 'ghost'}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setActiveTab('master')}
|
|
|
|
|
title="Master"
|
2025-11-17 20:27:08 +01:00
|
|
|
>
|
2025-11-18 07:41:16 +01:00
|
|
|
<Link2 className="h-4 w-4 mr-1.5 text-primary" />
|
|
|
|
|
Master
|
2025-11-18 07:30:46 +01:00
|
|
|
</Button>
|
2025-11-17 20:03:40 +01:00
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
onClick={() => setIsCollapsed(true)}
|
|
|
|
|
title="Collapse Side Panel"
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeft className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar">
|
2025-11-17 22:17:09 +01:00
|
|
|
{activeTab === 'tracks' && (
|
2025-11-17 20:03:40 +01:00
|
|
|
<>
|
2025-11-17 22:17:09 +01:00
|
|
|
{/* Track Actions */}
|
2025-11-17 20:03:40 +01:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
2025-11-18 07:41:16 +01:00
|
|
|
Track Management
|
2025-11-17 20:03:40 +01:00
|
|
|
</h3>
|
2025-11-17 22:17:09 +01:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onAddTrack}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
|
|
|
|
Add Track
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onImportTracks}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
|
|
|
|
<Upload className="h-3.5 w-3.5 mr-1.5" />
|
|
|
|
|
Import
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
{tracks.length > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onClearTracks}
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5 mr-1.5 text-destructive" />
|
|
|
|
|
Clear All Tracks
|
|
|
|
|
</Button>
|
2025-11-17 20:03:40 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-11-17 22:17:09 +01:00
|
|
|
|
2025-11-18 07:41:16 +01:00
|
|
|
{/* Track List Summary */}
|
2025-11-17 22:17:09 +01:00
|
|
|
{tracks.length > 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
2025-11-18 07:41:16 +01:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
|
|
|
|
Tracks ({tracks.length})
|
|
|
|
|
</h3>
|
|
|
|
|
{selectedTrack && (
|
|
|
|
|
<span className="text-xs text-primary">
|
|
|
|
|
{String(selectedTrack.name || 'Untitled Track')} selected
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-11-18 06:41:22 +01:00
|
|
|
<div className="text-xs text-muted-foreground">
|
2025-11-18 07:41:16 +01:00
|
|
|
<p>
|
|
|
|
|
{selectedTrack
|
|
|
|
|
? 'Track controls are on the left of each track. Effects for the selected track are shown below.'
|
|
|
|
|
: 'Click a track\'s waveform to select it and edit its effects below.'}
|
2025-11-18 06:41:22 +01:00
|
|
|
</p>
|
2025-11-17 22:17:09 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-center py-8">
|
|
|
|
|
<Music2 className="h-12 w-12 mx-auto text-muted-foreground/50 mb-2" />
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
|
|
|
No tracks yet. Add or import audio files to get started.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-18 07:41:16 +01:00
|
|
|
|
|
|
|
|
{/* Selected Track Effects */}
|
|
|
|
|
{selectedTrack && (
|
|
|
|
|
<div className="space-y-2 pt-3 border-t border-border">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
|
|
|
|
Track Effects
|
|
|
|
|
</h3>
|
|
|
|
|
{trackEffectChain && trackEffectChain.effects.length > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
onClick={onClearTrackChain}
|
|
|
|
|
title="Clear all effects"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<EffectRack
|
|
|
|
|
chain={trackEffectChain!}
|
|
|
|
|
onToggleEffect={onToggleTrackEffect}
|
|
|
|
|
onRemoveEffect={onRemoveTrackEffect}
|
|
|
|
|
onReorderEffects={onReorderTrackEffects}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-17 20:03:40 +01:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-11-18 07:41:16 +01:00
|
|
|
{activeTab === 'master' && (
|
|
|
|
|
<>
|
|
|
|
|
{/* Master Channel Info */}
|
|
|
|
|
<div className="space-y-2">
|
2025-11-17 20:27:08 +01:00
|
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
2025-11-18 07:41:16 +01:00
|
|
|
Master Channel
|
2025-11-17 20:27:08 +01:00
|
|
|
</h3>
|
2025-11-18 07:41:16 +01:00
|
|
|
<div className="text-xs text-muted-foreground">
|
|
|
|
|
<p>
|
|
|
|
|
Master effects are applied to the final mix of all tracks.
|
2025-11-17 22:17:09 +01:00
|
|
|
</p>
|
2025-11-17 20:03:40 +01:00
|
|
|
</div>
|
2025-11-18 07:41:16 +01:00
|
|
|
</div>
|
2025-11-18 07:30:46 +01:00
|
|
|
|
2025-11-18 07:41:16 +01:00
|
|
|
{/* Master Effects */}
|
|
|
|
|
<div className="space-y-2 pt-3 border-t border-border">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
|
|
|
|
Master Effects
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="flex gap-1">
|
2025-11-18 07:30:46 +01:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
2025-11-18 07:41:16 +01:00
|
|
|
onClick={() => setPresetDialogOpen(true)}
|
|
|
|
|
title="Manage presets"
|
2025-11-18 07:30:46 +01:00
|
|
|
>
|
2025-11-18 07:41:16 +01:00
|
|
|
<FolderOpen className="h-4 w-4" />
|
2025-11-18 07:30:46 +01:00
|
|
|
</Button>
|
2025-11-18 07:41:16 +01:00
|
|
|
{masterEffectChain.effects.length > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
onClick={onClearMasterChain}
|
|
|
|
|
title="Clear all effects"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-11-18 07:30:46 +01:00
|
|
|
</div>
|
|
|
|
|
|
2025-11-18 07:41:16 +01:00
|
|
|
<EffectRack
|
|
|
|
|
chain={masterEffectChain}
|
|
|
|
|
onToggleEffect={onToggleMasterEffect}
|
|
|
|
|
onRemoveEffect={onRemoveMasterEffect}
|
|
|
|
|
onReorderEffects={onReorderMasterEffects}
|
|
|
|
|
/>
|
|
|
|
|
<PresetManager
|
|
|
|
|
open={presetDialogOpen}
|
|
|
|
|
onClose={() => setPresetDialogOpen(false)}
|
|
|
|
|
currentChain={masterEffectChain}
|
|
|
|
|
presets={masterEffectPresets}
|
|
|
|
|
onSavePreset={onSaveMasterPreset}
|
|
|
|
|
onLoadPreset={onLoadMasterPreset}
|
|
|
|
|
onDeletePreset={onDeleteMasterPreset}
|
|
|
|
|
onExportPreset={() => {}}
|
|
|
|
|
onImportPreset={(preset) => onSaveMasterPreset(preset)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
2025-11-18 07:30:46 +01:00
|
|
|
)}
|
2025-11-17 20:03:40 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|