Skip to content

Commit 28ffef6

Browse files
committed
UPDATE: import/export function for midi mapping
1 parent 0b97755 commit 28ffef6

2 files changed

Lines changed: 93 additions & 13 deletions

File tree

CODEMAP.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ Re-generate: `./scripts/generate-codemap.sh > CODEMAP.md`
253253
- 12:export function CreateDummySongDialog({ onClose }: CreateDummySongDialogProps)
254254
- **deps**: ../../stores/useSongStore,../../stores/useTabStore,../../stores/useToastStore
255255

256-
### MidiSettingsDialog.tsx (181 lines)
257-
- 35:export function MidiSettingsDialog({ onClose }: MidiSettingsDialogProps)
258-
- **deps**: ../../services/midiService,../../stores/useMidiStore
256+
### MidiSettingsDialog.tsx (261 lines)
257+
- 37:export function MidiSettingsDialog({ onClose }: MidiSettingsDialogProps)
258+
- **deps**: ../../services/midiService,../../stores/useMidiStore,../../stores/useToastStore
259259

260260
### Sidebar.tsx (645 lines)
261261
- 19:export function Sidebar({ onSeekTo, duration, currentTime, isViewer = false, collapsed = false, onToggleCollapse, o...

src/components/Layout/MidiSettingsDialog.tsx

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// MIDI controller settings dialog.
22
// Shows connected devices, command mappings with MIDI Learn, and reset option.
33

4+
import { useRef } from 'react';
45
import { useMidiStore } from '../../stores/useMidiStore';
5-
import type { MidiCommand, MidiMessageType } from '../../services/midiService';
6+
import { useToastStore } from '../../stores/useToastStore';
7+
import type { MidiCommand, MidiMapping, MidiMessageType } from '../../services/midiService';
68

79
interface MidiSettingsDialogProps {
810
onClose: () => void;
@@ -38,6 +40,9 @@ export function MidiSettingsDialog({ onClose }: MidiSettingsDialogProps) {
3840
const learnTarget = useMidiStore((s) => s.learnTarget);
3941
const setLearnTarget = useMidiStore((s) => s.setLearnTarget);
4042
const resetMappings = useMidiStore((s) => s.resetMappings);
43+
const setMappings = useMidiStore((s) => s.setMappings);
44+
const addToast = useToastStore((s) => s.addToast);
45+
const fileInputRef = useRef<HTMLInputElement>(null);
4146

4247
const handleKeyDown = (e: React.KeyboardEvent) => {
4348
if (e.key === 'Escape') {
@@ -57,6 +62,55 @@ export function MidiSettingsDialog({ onClose }: MidiSettingsDialogProps) {
5762
await resetMappings();
5863
};
5964

65+
const handleExport = () => {
66+
const blob = new Blob(
67+
[JSON.stringify(mappings, null, 2)],
68+
{ type: 'application/json' },
69+
);
70+
const url = URL.createObjectURL(blob);
71+
const a = document.createElement('a');
72+
a.href = url;
73+
a.download = 'midi-mappings.songlab.json';
74+
a.click();
75+
URL.revokeObjectURL(url);
76+
};
77+
78+
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
79+
const file = e.target.files?.[0];
80+
if (!file) return;
81+
82+
try {
83+
const text = await file.text();
84+
const data = JSON.parse(text) as MidiMapping[];
85+
86+
// Validate structure
87+
if (!Array.isArray(data) || data.length === 0) {
88+
addToast('Invalid mapping file: expected a non-empty array', 'error');
89+
return;
90+
}
91+
92+
const isValid = data.every(
93+
(m) => m.command && m.type && typeof m.note === 'number',
94+
);
95+
if (!isValid) {
96+
addToast('Invalid mapping file: entries are missing required fields', 'error');
97+
return;
98+
}
99+
100+
setMappings(data);
101+
await import('../../services/db').then(
102+
(db) => db.setConfig('midiMappings', data),
103+
);
104+
addToast('MIDI mappings imported', 'success');
105+
} catch (error) {
106+
console.error('Failed to import MIDI mappings:', error);
107+
addToast('Failed to import MIDI mappings', 'error');
108+
}
109+
110+
// Reset file input so the same file can be re-imported
111+
if (fileInputRef.current) fileInputRef.current.value = '';
112+
};
113+
60114
return (
61115
<div
62116
className='fixed inset-0 z-50 flex items-center justify-center bg-black/50'
@@ -156,15 +210,41 @@ export function MidiSettingsDialog({ onClose }: MidiSettingsDialogProps) {
156210
</div>
157211

158212
{/* Buttons */}
159-
<div className='flex justify-between mt-2'>
160-
<button
161-
onClick={handleReset}
162-
className='px-3 py-1.5 text-xs font-mono text-slate-500 hover:text-slate-300
163-
transition-colors'
164-
aria-label='Reset MIDI mappings to defaults'
165-
>
166-
Reset defaults
167-
</button>
213+
<div className='flex items-center justify-between mt-2'>
214+
<div className='flex gap-2'>
215+
<button
216+
onClick={handleReset}
217+
className='px-4 py-1.5 text-xs font-mono bg-slate-700 hover:bg-slate-600
218+
text-slate-200 rounded-lg transition-colors'
219+
aria-label='Reset MIDI mappings to defaults'
220+
>
221+
Reset
222+
</button>
223+
<button
224+
onClick={handleExport}
225+
className='px-4 py-1.5 text-xs font-mono bg-slate-700 hover:bg-slate-600
226+
text-slate-200 rounded-lg transition-colors'
227+
aria-label='Export MIDI mappings'
228+
>
229+
Export
230+
</button>
231+
<button
232+
onClick={() => fileInputRef.current?.click()}
233+
className='px-4 py-1.5 text-xs font-mono bg-slate-700 hover:bg-slate-600
234+
text-slate-200 rounded-lg transition-colors'
235+
aria-label='Import MIDI mappings'
236+
>
237+
Import
238+
</button>
239+
<input
240+
ref={fileInputRef}
241+
type='file'
242+
accept='.json'
243+
onChange={handleImport}
244+
className='hidden'
245+
aria-hidden='true'
246+
/>
247+
</div>
168248
<button
169249
onClick={() => {
170250
setLearnTarget(null);

0 commit comments

Comments
 (0)