'use client'; import * as React from 'react'; import { FileUpload } from './FileUpload'; import { AudioInfo } from './AudioInfo'; import { Waveform } from './Waveform'; import { PlaybackControls } from './PlaybackControls'; import { ZoomControls } from './ZoomControls'; import { EditControls } from './EditControls'; import { HistoryControls } from './HistoryControls'; import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer'; import { useHistory } from '@/lib/hooks/useHistory'; import { useToast } from '@/components/ui/Toast'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; import { Slider } from '@/components/ui/Slider'; import { Loader2 } from 'lucide-react'; import type { Selection, ClipboardData } from '@/types/selection'; import { extractBufferSegment, deleteBufferSegment, insertBufferSegment, trimBuffer, } from '@/lib/audio/buffer-utils'; import { createCutCommand, createDeleteCommand, createPasteCommand, createTrimCommand, } from '@/lib/history/commands/edit-command'; export function AudioEditor() { // Zoom and scroll state const [zoom, setZoom] = React.useState(1); const [scrollOffset, setScrollOffset] = React.useState(0); const [amplitudeScale, setAmplitudeScale] = React.useState(1); // Selection state const [selection, setSelection] = React.useState(null); const [clipboard, setClipboard] = React.useState(null); const { loadFile, loadBuffer, clearFile, play, pause, stop, seek, setVolume, isPlaying, isPaused, currentTime, duration, volume, audioBuffer, fileName, isLoading, error, currentTimeFormatted, durationFormatted, } = useAudioPlayer(); const { execute, undo, redo, clear: clearHistory, state: historyState } = useHistory(50); const { addToast } = useToast(); const handleFileSelect = async (file: File) => { try { await loadFile(file); addToast({ title: 'File loaded', description: `Successfully loaded ${file.name}`, variant: 'success', duration: 3000, }); } catch (err) { addToast({ title: 'Error loading file', description: err instanceof Error ? err.message : 'Unknown error', variant: 'error', duration: 5000, }); } }; const handleClear = () => { clearFile(); setZoom(1); setScrollOffset(0); setAmplitudeScale(1); setSelection(null); setClipboard(null); clearHistory(); addToast({ title: 'Audio cleared', description: 'Audio file has been removed', variant: 'info', duration: 2000, }); }; // Edit operations const handleCut = () => { if (!selection || !audioBuffer) return; try { // Copy to clipboard const clipData = extractBufferSegment(audioBuffer, selection.start, selection.end); setClipboard({ buffer: clipData, start: selection.start, end: selection.end, duration: selection.end - selection.start, }); // Create and execute cut command const command = createCutCommand(audioBuffer, selection, (buffer) => { loadBuffer(buffer); }); execute(command); setSelection(null); addToast({ title: 'Cut', description: 'Selection cut to clipboard', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to cut selection', variant: 'error', duration: 3000, }); } }; const handleCopy = () => { if (!selection || !audioBuffer) return; try { const clipData = extractBufferSegment(audioBuffer, selection.start, selection.end); setClipboard({ buffer: clipData, start: selection.start, end: selection.end, duration: selection.end - selection.start, }); addToast({ title: 'Copied', description: 'Selection copied to clipboard', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to copy selection', variant: 'error', duration: 3000, }); } }; const handlePaste = () => { if (!clipboard || !audioBuffer) return; try { const insertTime = currentTime; // Create and execute paste command const command = createPasteCommand(audioBuffer, clipboard.buffer, insertTime, (buffer) => { loadBuffer(buffer); }); execute(command); addToast({ title: 'Pasted', description: 'Clipboard pasted at current position', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to paste clipboard', variant: 'error', duration: 3000, }); } }; const handleDelete = () => { if (!selection || !audioBuffer) return; try { // Create and execute delete command const command = createDeleteCommand(audioBuffer, selection, (buffer) => { loadBuffer(buffer); }); execute(command); setSelection(null); addToast({ title: 'Deleted', description: 'Selection deleted', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to delete selection', variant: 'error', duration: 3000, }); } }; const handleTrim = () => { if (!selection || !audioBuffer) return; try { // Create and execute trim command const command = createTrimCommand(audioBuffer, selection, (buffer) => { loadBuffer(buffer); }); execute(command); setSelection(null); addToast({ title: 'Trimmed', description: 'Audio trimmed to selection', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to trim audio', variant: 'error', duration: 3000, }); } }; const handleSelectAll = () => { if (!audioBuffer) return; setSelection({ start: 0, end: duration }); }; const handleClearSelection = () => { setSelection(null); }; // Zoom controls const handleZoomIn = () => { setZoom((prev) => Math.min(20, prev + 1)); }; const handleZoomOut = () => { setZoom((prev) => Math.max(1, prev - 1)); }; const handleFitToView = () => { setZoom(1); setScrollOffset(0); }; // Auto-adjust scroll when zoom changes React.useEffect(() => { if (!audioBuffer) return; // Reset scroll if zoomed out completely if (zoom === 1) { setScrollOffset(0); } }, [zoom, audioBuffer]); // Keyboard shortcuts React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Prevent shortcuts if typing in an input if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } // Ctrl+Z: Undo if (e.ctrlKey && !e.shiftKey && e.key === 'z') { e.preventDefault(); if (undo()) { addToast({ title: 'Undo', description: 'Last action undone', variant: 'info', duration: 1500, }); } } // Ctrl+Y or Ctrl+Shift+Z: Redo if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) { e.preventDefault(); if (redo()) { addToast({ title: 'Redo', description: 'Last action redone', variant: 'info', duration: 1500, }); } } // Ctrl+A: Select all if (e.ctrlKey && e.key === 'a') { e.preventDefault(); handleSelectAll(); } // Ctrl+X: Cut if (e.ctrlKey && e.key === 'x') { e.preventDefault(); handleCut(); } // Ctrl+C: Copy if (e.ctrlKey && e.key === 'c') { e.preventDefault(); handleCopy(); } // Ctrl+V: Paste if (e.ctrlKey && e.key === 'v') { e.preventDefault(); handlePaste(); } // Delete: Delete selection if (e.key === 'Delete') { e.preventDefault(); handleDelete(); } // Escape: Clear selection if (e.key === 'Escape') { e.preventDefault(); handleClearSelection(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [selection, clipboard, audioBuffer, currentTime, undo, redo, addToast]); // Show error toast React.useEffect(() => { if (error) { addToast({ title: 'Error', description: error, variant: 'error', duration: 5000, }); } }, [error, addToast]); return (
{/* File Upload or Audio Info */} {!audioBuffer ? ( ) : ( )} {/* Loading State */} {isLoading && (

Loading audio file...

)} {/* Waveform and Controls */} {audioBuffer && !isLoading && ( <> {/* Waveform */} Waveform {/* Horizontal scroll for zoomed waveform */} {zoom > 1 && (
)}
{/* Edit Controls */} Edit {/* History Controls */} History {/* Zoom Controls */} Zoom & View {/* Playback Controls */} Playback )}
); }