diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx index 4e75f39..bdb017b 100644 --- a/components/editor/AudioEditor.tsx +++ b/components/editor/AudioEditor.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { Music, Plus, Upload, Trash2, Settings, Download } from 'lucide-react'; +import { Music, Plus, Upload, Trash2, Settings, Download, FolderOpen } from 'lucide-react'; import { PlaybackControls } from './PlaybackControls'; import { MasterControls } from '@/components/controls/MasterControls'; import { FrequencyAnalyzer } from '@/components/analysis/FrequencyAnalyzer'; @@ -13,6 +13,7 @@ import { ThemeToggle } from '@/components/layout/ThemeToggle'; import { CommandPalette } from '@/components/ui/CommandPalette'; import { GlobalSettingsDialog } from '@/components/settings/GlobalSettingsDialog'; import { ExportDialog, type ExportSettings } from '@/components/dialogs/ExportDialog'; +import { ProjectsDialog } from '@/components/dialogs/ProjectsDialog'; import { Button } from '@/components/ui/Button'; import type { CommandAction } from '@/components/ui/CommandPalette'; import { useMultiTrack } from '@/lib/hooks/useMultiTrack'; @@ -35,6 +36,15 @@ import { import { extractBufferSegment } from '@/lib/audio/buffer-utils'; import { mixTracks, getMaxTrackDuration } from '@/lib/audio/track-utils'; import { audioBufferToWav, audioBufferToMp3, downloadArrayBuffer } from '@/lib/audio/export'; +import { + saveCurrentProject, + loadProjectById, + listProjects, + removeProject, + duplicateProject, + type ProjectMetadata, +} from '@/lib/storage/projects'; +import { getAudioContext } from '@/lib/audio/context'; export function AudioEditor() { const [importDialogOpen, setImportDialogOpen] = React.useState(false); @@ -53,6 +63,10 @@ export function AudioEditor() { const [exportDialogOpen, setExportDialogOpen] = React.useState(false); const [isExporting, setIsExporting] = React.useState(false); const [analyzerView, setAnalyzerView] = React.useState<'frequency' | 'spectrogram' | 'phase' | 'lufs' | 'stats'>('frequency'); + const [projectsDialogOpen, setProjectsDialogOpen] = React.useState(false); + const [projects, setProjects] = React.useState([]); + const [currentProjectId, setCurrentProjectId] = React.useState(null); + const [currentProjectName, setCurrentProjectName] = React.useState('Untitled Project'); const { addToast } = useToast(); @@ -843,6 +857,196 @@ export function AudioEditor() { } }, [tracks, addToast]); + // Load projects list when dialog opens + const loadProjectsList = React.useCallback(async () => { + try { + const projectsList = await listProjects(); + setProjects(projectsList); + } catch (error) { + console.error('Failed to load projects:', error); + addToast({ + title: 'Failed to Load Projects', + description: 'Could not retrieve project list', + variant: 'error', + duration: 3000, + }); + } + }, [addToast]); + + // Open projects dialog + const handleOpenProjectsDialog = React.useCallback(async () => { + await loadProjectsList(); + setProjectsDialogOpen(true); + }, [loadProjectsList]); + + // Save current project + const handleSaveProject = React.useCallback(async () => { + if (tracks.length === 0) return; + + try { + const audioContext = getAudioContext(); + const projectId = await saveCurrentProject( + currentProjectId, + currentProjectName, + tracks, + { + zoom, + currentTime, + sampleRate: audioContext.sampleRate, + } + ); + + setCurrentProjectId(projectId); + + addToast({ + title: 'Project Saved', + description: `"${currentProjectName}" saved successfully`, + variant: 'success', + duration: 2000, + }); + } catch (error) { + console.error('Failed to save project:', error); + addToast({ + title: 'Save Failed', + description: 'Could not save project', + variant: 'error', + duration: 3000, + }); + } + }, [tracks, currentProjectId, currentProjectName, zoom, currentTime, addToast]); + + // Auto-save effect + React.useEffect(() => { + if (tracks.length === 0) return; + + const autoSaveTimer = setTimeout(() => { + handleSaveProject(); + }, 30000); // Auto-save every 30 seconds + + return () => clearTimeout(autoSaveTimer); + }, [tracks, handleSaveProject]); + + // Create new project + const handleNewProject = React.useCallback(() => { + if (tracks.length > 0) { + if (!confirm('Create new project? Unsaved changes will be lost.')) { + return; + } + } + + clearTracks(); + setCurrentProjectId(null); + setCurrentProjectName('Untitled Project'); + setProjectsDialogOpen(false); + + addToast({ + title: 'New Project', + description: 'Started new project', + variant: 'success', + duration: 2000, + }); + }, [tracks, clearTracks, addToast]); + + // Load project + const handleLoadProject = React.useCallback(async (projectId: string) => { + try { + const projectData = await loadProjectById(projectId); + if (!projectData) { + throw new Error('Project not found'); + } + + // Clear current state + clearTracks(); + + // Load tracks + for (const track of projectData.tracks) { + if (track.audioBuffer) { + addTrackFromBufferOriginal(track.audioBuffer, track.name); + } + } + + // Restore settings + setZoom(projectData.settings.zoom); + // Note: currentTime is managed by player, will start at 0 + + // Set project metadata + setCurrentProjectId(projectData.metadata.id); + setCurrentProjectName(projectData.metadata.name); + setProjectsDialogOpen(false); + + addToast({ + title: 'Project Loaded', + description: `"${projectData.metadata.name}" loaded successfully`, + variant: 'success', + duration: 2000, + }); + } catch (error) { + console.error('Failed to load project:', error); + addToast({ + title: 'Load Failed', + description: 'Could not load project', + variant: 'error', + duration: 3000, + }); + } + }, [clearTracks, addTrackFromBufferOriginal, addToast]); + + // Delete project + const handleDeleteProject = React.useCallback(async (projectId: string) => { + try { + await removeProject(projectId); + await loadProjectsList(); + + // If deleted current project, reset + if (projectId === currentProjectId) { + setCurrentProjectId(null); + setCurrentProjectName('Untitled Project'); + } + + addToast({ + title: 'Project Deleted', + description: 'Project deleted successfully', + variant: 'success', + duration: 2000, + }); + } catch (error) { + console.error('Failed to delete project:', error); + addToast({ + title: 'Delete Failed', + description: 'Could not delete project', + variant: 'error', + duration: 3000, + }); + } + }, [currentProjectId, loadProjectsList, addToast]); + + // Duplicate project + const handleDuplicateProject = React.useCallback(async (projectId: string) => { + try { + const sourceProject = projects.find(p => p.id === projectId); + if (!sourceProject) return; + + const newName = `${sourceProject.name} (Copy)`; + await duplicateProject(projectId, newName); + await loadProjectsList(); + + addToast({ + title: 'Project Duplicated', + description: `"${newName}" created successfully`, + variant: 'success', + duration: 2000, + }); + } catch (error) { + console.error('Failed to duplicate project:', error); + addToast({ + title: 'Duplicate Failed', + description: 'Could not duplicate project', + variant: 'error', + duration: 3000, + }); + } + }, [projects, loadProjectsList, addToast]); + // Zoom controls const handleZoomIn = () => { setZoom((prev) => Math.min(20, prev + 1)); @@ -1050,6 +1254,10 @@ export function AudioEditor() { {/* Track Actions */}
+