Files
audio-ui/lib/storage/projects.ts
Sebastian Krüger d3a5961131 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

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;
}