Skip to content

Commit 917e30e

Browse files
committed
feat: Add JSON project export/import and MIDI import
- Add project-io.ts with full JSON export (includes audio as base64) - Add MIDI import with track detection and drum channel support - Create ImportModal with drag & drop file picker and preview - Update ExportModal with card-based UI for WAV/MIDI/JSON options - Add Import button to Transport toolbar - Add i18n translations for import/export UI (en, es) Closes #1
1 parent dc26f68 commit 917e30e

8 files changed

Lines changed: 1116 additions & 65 deletions

File tree

components/compose/ExportModal.tsx

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// ============================================
22
// ComposeYogi — Export Modal
3-
// Progress UI for audio export
3+
// Progress UI for audio export + JSON export
44
// ============================================
55

66
'use client';
77

88
import { useEffect, useState, useCallback } from 'react';
9-
import { Download, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
9+
import { Download, CheckCircle, AlertCircle, Loader2, FileJson, Music, FileAudio } from 'lucide-react';
1010
import { Button } from '@/components/ui/button';
1111
import { Progress } from '@/components/ui/progress';
1212
import {
@@ -18,6 +18,8 @@ import {
1818
} from '@/components/ui/dialog';
1919
import { useProjectStore } from '@/lib/store';
2020
import { downloadProjectAsWav } from '@/lib/audio/offline-renderer';
21+
import { downloadProjectAsMidi } from '@/lib/audio/export';
22+
import { downloadProjectAsJSON } from '@/lib/audio/project-io';
2123

2224
// ============================================
2325
// Types
@@ -39,20 +41,23 @@ export function ExportModal({ isOpen, onClose }: ExportModalProps) {
3941
const [exportState, setExportState] = useState<ExportState>('idle');
4042
const [progress, setProgress] = useState(0);
4143
const [errorMessage, setErrorMessage] = useState<string | null>(null);
44+
const [exportType, setExportType] = useState<'wav' | 'midi' | 'json'>('wav');
4245

4346
// Reset state when modal opens
4447
useEffect(() => {
4548
if (isOpen) {
4649
setExportState('idle');
4750
setProgress(0);
4851
setErrorMessage(null);
52+
setExportType('wav');
4953
}
5054
}, [isOpen]);
5155

52-
const handleExport = useCallback(async () => {
56+
const handleExportWav = useCallback(async () => {
5357
if (!project) return;
5458

5559
setExportState('exporting');
60+
setExportType('wav');
5661
setProgress(0);
5762
setErrorMessage(null);
5863

@@ -62,7 +67,33 @@ export function ExportModal({ isOpen, onClose }: ExportModalProps) {
6267
});
6368
setExportState('complete');
6469
} catch (error) {
65-
console.error('[ExportModal] Export failed:', error);
70+
console.error('[ExportModal] WAV export failed:', error);
71+
setErrorMessage(error instanceof Error ? error.message : 'Export failed');
72+
setExportState('error');
73+
}
74+
}, [project]);
75+
76+
const handleExportMidi = useCallback(() => {
77+
if (!project) return;
78+
try {
79+
downloadProjectAsMidi(project);
80+
setExportState('complete');
81+
setExportType('midi');
82+
} catch (error) {
83+
console.error('[ExportModal] MIDI export failed:', error);
84+
setErrorMessage(error instanceof Error ? error.message : 'Export failed');
85+
setExportState('error');
86+
}
87+
}, [project]);
88+
89+
const handleExportJSON = useCallback(() => {
90+
if (!project) return;
91+
try {
92+
downloadProjectAsJSON(project, undefined, false);
93+
setExportState('complete');
94+
setExportType('json');
95+
} catch (error) {
96+
console.error('[ExportModal] JSON export failed:', error);
6697
setErrorMessage(error instanceof Error ? error.message : 'Export failed');
6798
setExportState('error');
6899
}
@@ -82,46 +113,69 @@ export function ExportModal({ isOpen, onClose }: ExportModalProps) {
82113
<DialogHeader>
83114
<DialogTitle className="flex items-center gap-2">
84115
<Download className="h-5 w-5" />
85-
Export Audio
116+
Export Project
86117
</DialogTitle>
87118
<DialogDescription>
88-
Export &quot;{project.name}&quot; as a WAV file
119+
Export &quot;{project.name}&quot; in your preferred format
89120
</DialogDescription>
90121
</DialogHeader>
91122

92123
<div className="py-4 space-y-4">
93124
{/* Idle State */}
94125
{exportState === 'idle' && (
95-
<div className="space-y-4">
96-
<div className="rounded-lg bg-muted/50 p-4 space-y-2">
97-
<div className="flex justify-between text-sm">
98-
<span className="text-muted-foreground">Format</span>
99-
<span className="font-medium">WAV (16-bit PCM)</span>
126+
<div className="space-y-3">
127+
{/* WAV Export */}
128+
<button
129+
onClick={handleExportWav}
130+
className="w-full flex items-center gap-4 p-4 rounded-lg border border-border hover:bg-muted/50 transition-colors text-left"
131+
>
132+
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center">
133+
<FileAudio className="h-5 w-5 text-green-500" />
100134
</div>
101-
<div className="flex justify-between text-sm">
102-
<span className="text-muted-foreground">Sample Rate</span>
103-
<span className="font-medium">44.1 kHz</span>
135+
<div className="flex-1 min-w-0">
136+
<div className="font-medium">WAV Audio</div>
137+
<div className="text-sm text-muted-foreground">High-quality 16-bit PCM, 44.1 kHz stereo</div>
104138
</div>
105-
<div className="flex justify-between text-sm">
106-
<span className="text-muted-foreground">Channels</span>
107-
<span className="font-medium">Stereo</span>
139+
</button>
140+
141+
{/* MIDI Export */}
142+
<button
143+
onClick={handleExportMidi}
144+
className="w-full flex items-center gap-4 p-4 rounded-lg border border-border hover:bg-muted/50 transition-colors text-left"
145+
>
146+
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center">
147+
<Music className="h-5 w-5 text-blue-500" />
108148
</div>
109-
</div>
149+
<div className="flex-1 min-w-0">
150+
<div className="font-medium">MIDI File</div>
151+
<div className="text-sm text-muted-foreground">Standard MIDI for use in other DAWs</div>
152+
</div>
153+
</button>
154+
155+
{/* JSON Export */}
156+
<button
157+
onClick={handleExportJSON}
158+
className="w-full flex items-center gap-4 p-4 rounded-lg border border-border hover:bg-muted/50 transition-colors text-left"
159+
>
160+
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center">
161+
<FileJson className="h-5 w-5 text-purple-500" />
162+
</div>
163+
<div className="flex-1 min-w-0">
164+
<div className="font-medium">Project File (.cyp)</div>
165+
<div className="text-sm text-muted-foreground">Full project backup, can be re-imported</div>
166+
</div>
167+
</button>
110168

111-
<div className="flex justify-end gap-2">
169+
<div className="flex justify-end pt-2">
112170
<Button variant="outline" onClick={handleClose}>
113171
Cancel
114172
</Button>
115-
<Button onClick={handleExport}>
116-
<Download className="mr-2 h-4 w-4" />
117-
Export WAV
118-
</Button>
119173
</div>
120174
</div>
121175
)}
122176

123-
{/* Exporting State */}
124-
{exportState === 'exporting' && (
177+
{/* Exporting State (WAV only) */}
178+
{exportState === 'exporting' && exportType === 'wav' && (
125179
<div className="space-y-4">
126180
<div className="flex items-center gap-3">
127181
<Loader2 className="h-5 w-5 animate-spin text-accent" />
@@ -177,7 +231,7 @@ export function ExportModal({ isOpen, onClose }: ExportModalProps) {
177231
<Button variant="outline" onClick={handleClose}>
178232
Close
179233
</Button>
180-
<Button onClick={handleExport}>
234+
<Button onClick={() => setExportState('idle')}>
181235
Try Again
182236
</Button>
183237
</div>

0 commit comments

Comments
 (0)