Added full export/import functionality for projects:
Export Features:
- Export button for each project in Projects dialog
- Downloads project as JSON file with all data
- Includes tracks, audio buffers, effects, automation, settings
- Filename format: {project_name}_{timestamp}.json
Import Features:
- Import button in Projects dialog header
- File picker for .json files
- Automatically generates new project ID to avoid conflicts
- Appends "(Imported)" to project name
- Preserves all project data
This enables:
- Backup of projects outside the browser
- Sharing projects with collaborators
- Migration between computers/browsers
- Version snapshots at different stages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
163 lines
6.0 KiB
TypeScript
163 lines
6.0 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { X, Plus, Trash2, Copy, FolderOpen, Download, Upload } from 'lucide-react';
|
|
import { Button } from '@/components/ui/Button';
|
|
import type { ProjectMetadata } from '@/lib/storage/db';
|
|
import { formatDuration } from '@/lib/audio/decoder';
|
|
|
|
export interface ProjectsDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
projects: ProjectMetadata[];
|
|
onNewProject: () => void;
|
|
onLoadProject: (projectId: string) => void;
|
|
onDeleteProject: (projectId: string) => void;
|
|
onDuplicateProject: (projectId: string) => void;
|
|
onExportProject: (projectId: string) => void;
|
|
onImportProject: () => void;
|
|
}
|
|
|
|
export function ProjectsDialog({
|
|
open,
|
|
onClose,
|
|
projects,
|
|
onNewProject,
|
|
onLoadProject,
|
|
onDeleteProject,
|
|
onDuplicateProject,
|
|
onExportProject,
|
|
onImportProject,
|
|
}: ProjectsDialogProps) {
|
|
if (!open) return null;
|
|
|
|
const formatDate = (timestamp: number) => {
|
|
return new Date(timestamp).toLocaleString(undefined, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
<div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-3xl max-h-[80vh] flex flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-6 border-b border-border">
|
|
<h2 className="text-lg font-semibold text-foreground">Projects</h2>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
onClick={onImportProject}
|
|
variant="outline"
|
|
size="sm"
|
|
className="gap-2"
|
|
>
|
|
<Upload className="h-4 w-4" />
|
|
Import
|
|
</Button>
|
|
<Button
|
|
onClick={onNewProject}
|
|
variant="default"
|
|
size="sm"
|
|
className="gap-2"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
New Project
|
|
</Button>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Projects List */}
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
{projects.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<FolderOpen className="h-16 w-16 text-muted-foreground mb-4" />
|
|
<h3 className="text-lg font-medium text-foreground mb-2">
|
|
No projects yet
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Create your first project to get started
|
|
</p>
|
|
<Button onClick={onNewProject} variant="default">
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Create Project
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{projects.map((project) => (
|
|
<div
|
|
key={project.id}
|
|
className="border border-border rounded-lg p-4 hover:bg-accent/50 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<h3 className="font-medium text-foreground mb-1">
|
|
{project.name}
|
|
</h3>
|
|
{project.description && (
|
|
<p className="text-sm text-muted-foreground mb-2">
|
|
{project.description}
|
|
</p>
|
|
)}
|
|
<div className="flex flex-wrap gap-4 text-xs text-muted-foreground">
|
|
<span>{project.trackCount} tracks</span>
|
|
<span>{formatDuration(project.duration)}</span>
|
|
<span>{project.sampleRate / 1000}kHz</span>
|
|
<span>Updated {formatDate(project.updatedAt)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 ml-4">
|
|
<button
|
|
onClick={() => onLoadProject(project.id)}
|
|
className="px-3 py-1.5 text-sm font-medium text-primary hover:bg-primary/10 rounded transition-colors"
|
|
title="Open project"
|
|
>
|
|
Open
|
|
</button>
|
|
<button
|
|
onClick={() => onExportProject(project.id)}
|
|
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded transition-colors"
|
|
title="Export project"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => onDuplicateProject(project.id)}
|
|
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded transition-colors"
|
|
title="Duplicate project"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (confirm(`Delete "${project.name}"? This cannot be undone.`)) {
|
|
onDeleteProject(project.id);
|
|
}
|
|
}}
|
|
className="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded transition-colors"
|
|
title="Delete project"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|