Integrated complete project management system with auto-save: **AudioEditor.tsx - Full Integration:** - Added "Projects" button in header toolbar (FolderOpen icon) - Project state management (currentProjectId, currentProjectName, projects list) - Comprehensive project handlers: - `handleOpenProjectsDialog` - Opens dialog and loads project list - `handleSaveProject` - Saves current project to IndexedDB - `handleNewProject` - Creates new project with confirmation - `handleLoadProject` - Loads project and restores all tracks/settings - `handleDeleteProject` - Deletes project with cleanup - `handleDuplicateProject` - Creates project copy - Auto-save effect: Saves project every 30 seconds when tracks exist - ProjectsDialog component integrated with all handlers - Toast notifications for all operations **lib/storage/projects.ts:** - Re-exported ProjectMetadata type for easier importing - Fixed type exports **Key Features:** - **Auto-save**: Automatically saves every 30 seconds - **Project persistence**: Full track state, audio buffers, effects, automation - **Smart loading**: Restores zoom, track order, and all track properties - **Safety confirmations**: Warns before creating new project with unsaved changes - **User feedback**: Toast messages for all operations (save, load, delete, duplicate) - **Seamless workflow**: Projects → Import → Export in logical toolbar order **User Flow:** 1. Click "Projects" to open project manager 2. Create new project or load existing 3. Work on tracks (auto-saves every 30s) 4. Switch between projects anytime 5. Duplicate projects for experimentation 6. Delete old projects to clean up 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
194 lines
4.3 KiB
TypeScript
194 lines
4.3 KiB
TypeScript
/**
|
|
* Project management service
|
|
*/
|
|
|
|
import type { Track } from '@/types/track';
|
|
import {
|
|
saveProject,
|
|
loadProject,
|
|
getAllProjects,
|
|
deleteProject,
|
|
serializeAudioBuffer,
|
|
deserializeAudioBuffer,
|
|
type ProjectData,
|
|
type SerializedTrack,
|
|
} from './db';
|
|
import type { ProjectMetadata } from './db';
|
|
import { getAudioContext } from '../audio/context';
|
|
import { generateId } from '../audio/effects/chain';
|
|
|
|
// Re-export ProjectMetadata for easier importing
|
|
export type { ProjectMetadata } from './db';
|
|
|
|
/**
|
|
* Generate unique project ID
|
|
*/
|
|
export function generateProjectId(): string {
|
|
return `project_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
/**
|
|
* Convert tracks to serialized format
|
|
*/
|
|
function serializeTracks(tracks: Track[]): SerializedTrack[] {
|
|
return tracks.map(track => ({
|
|
id: track.id,
|
|
name: track.name,
|
|
color: track.color,
|
|
volume: track.volume,
|
|
pan: track.pan,
|
|
muted: track.mute,
|
|
soloed: track.solo,
|
|
collapsed: track.collapsed,
|
|
height: track.height,
|
|
audioBuffer: track.audioBuffer ? serializeAudioBuffer(track.audioBuffer) : null,
|
|
effects: track.effectChain?.effects || [],
|
|
automation: track.automation,
|
|
recordEnabled: track.recordEnabled,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Convert serialized tracks back to Track format
|
|
*/
|
|
function deserializeTracks(serialized: SerializedTrack[]): Track[] {
|
|
const audioContext = getAudioContext();
|
|
|
|
return serialized.map(track => ({
|
|
id: track.id,
|
|
name: track.name,
|
|
color: track.color,
|
|
volume: track.volume,
|
|
pan: track.pan,
|
|
mute: track.muted,
|
|
solo: track.soloed,
|
|
collapsed: track.collapsed,
|
|
height: track.height,
|
|
audioBuffer: track.audioBuffer ? deserializeAudioBuffer(track.audioBuffer, audioContext) : null,
|
|
effectChain: {
|
|
id: generateId(),
|
|
name: `${track.name} FX`,
|
|
effects: track.effects,
|
|
},
|
|
automation: track.automation,
|
|
recordEnabled: track.recordEnabled,
|
|
selected: false,
|
|
showEffects: false,
|
|
selection: null, // Reset selection on load
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Calculate total project duration
|
|
*/
|
|
function calculateDuration(tracks: Track[]): number {
|
|
let maxDuration = 0;
|
|
|
|
for (const track of tracks) {
|
|
if (track.audioBuffer) {
|
|
maxDuration = Math.max(maxDuration, track.audioBuffer.duration);
|
|
}
|
|
}
|
|
|
|
return maxDuration;
|
|
}
|
|
|
|
/**
|
|
* Save current project state
|
|
*/
|
|
export async function saveCurrentProject(
|
|
projectId: string | null,
|
|
projectName: string,
|
|
tracks: Track[],
|
|
settings: {
|
|
zoom: number;
|
|
currentTime: number;
|
|
sampleRate: number;
|
|
},
|
|
description?: string
|
|
): Promise<string> {
|
|
const id = projectId || generateProjectId();
|
|
const now = Date.now();
|
|
|
|
const metadata: ProjectMetadata = {
|
|
id,
|
|
name: projectName,
|
|
description,
|
|
createdAt: projectId ? (await loadProject(id))?.metadata.createdAt || now : now,
|
|
updatedAt: now,
|
|
duration: calculateDuration(tracks),
|
|
sampleRate: settings.sampleRate,
|
|
trackCount: tracks.length,
|
|
};
|
|
|
|
const projectData: ProjectData = {
|
|
metadata,
|
|
tracks: serializeTracks(tracks),
|
|
settings,
|
|
};
|
|
|
|
await saveProject(projectData);
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Load project and restore state
|
|
*/
|
|
export async function loadProjectById(projectId: string): Promise<{
|
|
tracks: Track[];
|
|
settings: {
|
|
zoom: number;
|
|
currentTime: number;
|
|
sampleRate: number;
|
|
};
|
|
metadata: ProjectMetadata;
|
|
} | null> {
|
|
const project = await loadProject(projectId);
|
|
if (!project) return null;
|
|
|
|
return {
|
|
tracks: deserializeTracks(project.tracks),
|
|
settings: project.settings,
|
|
metadata: project.metadata,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get list of all projects
|
|
*/
|
|
export async function listProjects(): Promise<ProjectMetadata[]> {
|
|
return getAllProjects();
|
|
}
|
|
|
|
/**
|
|
* Delete a project
|
|
*/
|
|
export async function removeProject(projectId: string): Promise<void> {
|
|
return deleteProject(projectId);
|
|
}
|
|
|
|
/**
|
|
* Duplicate a project
|
|
*/
|
|
export async function duplicateProject(sourceProjectId: string, newName: string): Promise<string> {
|
|
const project = await loadProject(sourceProjectId);
|
|
if (!project) throw new Error('Project not found');
|
|
|
|
const newId = generateProjectId();
|
|
const now = Date.now();
|
|
|
|
const newProject: ProjectData = {
|
|
...project,
|
|
metadata: {
|
|
...project.metadata,
|
|
id: newId,
|
|
name: newName,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
};
|
|
|
|
await saveProject(newProject);
|
|
return newId;
|
|
}
|