Files
audio-ui/lib/storage/projects.ts

194 lines
4.3 KiB
TypeScript
Raw Normal View History

/**
* Project management service
*/
import type { Track } from '@/types/track';
import {
saveProject,
loadProject,
getAllProjects,
deleteProject,
serializeAudioBuffer,
deserializeAudioBuffer,
type ProjectData,
type SerializedTrack,
} from './db';
feat: implement Phase 12.2 - Project Management UI Integration 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>
2025-11-19 09:26:57 +01:00
import type { ProjectMetadata } from './db';
import { getAudioContext } from '../audio/context';
import { generateId } from '../audio/effects/chain';
feat: implement Phase 12.2 - Project Management UI Integration 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>
2025-11-19 09:26:57 +01:00
// 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;
}