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>
238 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|