Files
audio-ui/components/dialogs/ExportDialog.tsx
Sebastian Krüger 38a2b2962d feat: add export scope options (project/selection/tracks)
Implemented Phase 11.3 - Export Regions:
- Added export scope selector in ExportDialog
  - Entire Project: Mix all tracks into single file
  - Selected Region: Export only the selected region (disabled if no selection)
  - Individual Tracks: Export each track as separate file
- Updated ExportSettings interface with scope property
- Refactored handleExport to support all three export modes:
  - Project mode: Mix all tracks (existing behavior)
  - Selection mode: Extract selection from all tracks and mix
  - Tracks mode: Loop through tracks and export separately with sanitized filenames
- Added hasSelection prop to ExportDialog to enable/disable selection option
- Created helper function convertAndDownload to reduce code duplication
- Selection mode uses extractBufferSegment for precise region extraction
- Track names are sanitized for filenames (remove special characters)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 07:47:56 +01:00

238 lines
8.8 KiB
TypeScript

'use client';
import * as React from 'react';
import { X, Download } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
export interface ExportSettings {
format: 'wav' | 'mp3' | 'flac';
scope: 'project' | 'selection' | 'tracks'; // Export scope
bitDepth: 16 | 24 | 32;
bitrate: number; // For MP3: 128, 192, 256, 320 kbps
quality: number; // For FLAC: 0-9
normalize: boolean;
filename: string;
}
export interface ExportDialogProps {
open: boolean;
onClose: () => void;
onExport: (settings: ExportSettings) => void;
isExporting?: boolean;
hasSelection?: boolean; // Whether any track has a selection
}
export function ExportDialog({ open, onClose, onExport, isExporting, hasSelection }: ExportDialogProps) {
const [settings, setSettings] = React.useState<ExportSettings>({
format: 'wav',
scope: 'project',
bitDepth: 16,
bitrate: 192, // Default MP3 bitrate
quality: 6, // Default FLAC quality
normalize: true,
filename: 'mix',
});
const handleExport = () => {
onExport(settings);
};
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-md p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-foreground">Export Audio</h2>
<button
onClick={onClose}
className="text-muted-foreground hover:text-foreground transition-colors"
disabled={isExporting}
>
<X className="h-5 w-5" />
</button>
</div>
{/* Settings */}
<div className="space-y-4">
{/* Filename */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Filename
</label>
<input
type="text"
value={settings.filename}
onChange={(e) => setSettings({ ...settings, filename: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
disabled={isExporting}
/>
<p className="text-xs text-muted-foreground mt-1">
.{settings.format} will be added automatically
</p>
</div>
{/* Format */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Format
</label>
<select
value={settings.format}
onChange={(e) => setSettings({ ...settings, format: e.target.value as 'wav' | 'mp3' | 'flac' })}
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
disabled={isExporting}
>
<option value="wav">WAV (Uncompressed)</option>
<option value="mp3">MP3 (Lossy)</option>
<option value="flac">FLAC (Lossless)</option>
</select>
</div>
{/* Export Scope */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Export Scope
</label>
<select
value={settings.scope}
onChange={(e) => setSettings({ ...settings, scope: e.target.value as 'project' | 'selection' | 'tracks' })}
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
disabled={isExporting}
>
<option value="project">Entire Project (Mix All Tracks)</option>
<option value="selection" disabled={!hasSelection}>
Selected Region {!hasSelection && '(No selection)'}
</option>
<option value="tracks">Individual Tracks (Separate Files)</option>
</select>
<p className="text-xs text-muted-foreground mt-1">
{settings.scope === 'project' && 'Mix all tracks into a single file'}
{settings.scope === 'selection' && 'Export only the selected region'}
{settings.scope === 'tracks' && 'Export each track as a separate file'}
</p>
</div>
{/* Bit Depth (WAV and FLAC only) */}
{(settings.format === 'wav' || settings.format === 'flac') && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Bit Depth
</label>
<div className="flex gap-2">
{[16, 24, 32].map((depth) => (
<button
key={depth}
onClick={() => setSettings({ ...settings, bitDepth: depth as 16 | 24 | 32 })}
className={cn(
'flex-1 px-3 py-2 rounded text-sm font-medium transition-colors',
settings.bitDepth === depth
? 'bg-primary text-primary-foreground'
: 'bg-background border border-border text-foreground hover:bg-accent'
)}
disabled={isExporting}
>
{depth}-bit {depth === 32 && '(Float)'}
</button>
))}
</div>
</div>
)}
{/* MP3 Bitrate */}
{settings.format === 'mp3' && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Bitrate
</label>
<div className="flex gap-2">
{[128, 192, 256, 320].map((rate) => (
<button
key={rate}
onClick={() => setSettings({ ...settings, bitrate: rate })}
className={cn(
'flex-1 px-3 py-2 rounded text-sm font-medium transition-colors',
settings.bitrate === rate
? 'bg-primary text-primary-foreground'
: 'bg-background border border-border text-foreground hover:bg-accent'
)}
disabled={isExporting}
>
{rate} kbps
</button>
))}
</div>
</div>
)}
{/* FLAC Quality */}
{settings.format === 'flac' && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Compression Quality
</label>
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">Fast</span>
<input
type="range"
min="0"
max="9"
value={settings.quality}
onChange={(e) => setSettings({ ...settings, quality: parseInt(e.target.value) })}
className="flex-1"
disabled={isExporting}
/>
<span className="text-xs text-muted-foreground">Small</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
Level {settings.quality} (Higher = smaller file, slower encoding)
</p>
</div>
)}
{/* Normalize */}
<div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={settings.normalize}
onChange={(e) => setSettings({ ...settings, normalize: e.target.checked })}
className="w-4 h-4 rounded border-border text-primary focus:ring-primary"
disabled={isExporting}
/>
<span className="text-sm font-medium text-foreground">
Normalize audio
</span>
</label>
<p className="text-xs text-muted-foreground mt-1 ml-6">
Prevents clipping by adjusting peak levels
</p>
</div>
</div>
{/* Actions */}
<div className="flex gap-3 mt-6">
<Button
variant="outline"
onClick={onClose}
className="flex-1"
disabled={isExporting}
>
Cancel
</Button>
<Button
onClick={handleExport}
className="flex-1"
disabled={isExporting || !settings.filename.trim()}
>
<Download className="h-4 w-4 mr-2" />
{isExporting ? 'Exporting...' : 'Export'}
</Button>
</div>
</div>
</div>
);
}