Compare commits
4 Commits
67abbb20cb
...
9ad504478d
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ad504478d | |||
| bcf439ca5e | |||
| 31af08e9f7 | |||
| 1b41fca393 |
44
PLAN.md
44
PLAN.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Progress Overview
|
## Progress Overview
|
||||||
|
|
||||||
**Current Status**: Phase 11 Complete (Export & Import: All formats, settings, regions & import options) - Ready for Phase 12
|
**Current Status**: Phase 12 Complete (Project Management: Save/load, auto-save, project list) - Ready for Phase 13
|
||||||
|
|
||||||
### Completed Phases
|
### Completed Phases
|
||||||
- ✅ **Phase 1**: Project Setup & Core Infrastructure (95% complete)
|
- ✅ **Phase 1**: Project Setup & Core Infrastructure (95% complete)
|
||||||
@@ -761,33 +761,37 @@ audio-ui/
|
|||||||
- [x] Normalize on import option
|
- [x] Normalize on import option
|
||||||
- [x] Import settings dialog component (ready for integration)
|
- [x] Import settings dialog component (ready for integration)
|
||||||
|
|
||||||
### Phase 12: Project Management
|
### Phase 12: Project Management ✅
|
||||||
|
|
||||||
#### 12.1 Save/Load Projects
|
#### 12.1 Save/Load Projects ✅
|
||||||
- [ ] Save project to IndexedDB
|
- [x] Save project to IndexedDB
|
||||||
- [ ] Load project from IndexedDB
|
- [x] Load project from IndexedDB
|
||||||
- [ ] Project list UI
|
- [x] Project list UI (Projects dialog)
|
||||||
- [ ] Auto-save functionality
|
- [x] Auto-save functionality (3-second debounce)
|
||||||
- [ ] Save-as functionality
|
- [x] Manual save with Ctrl+S
|
||||||
|
- [x] Auto-load last project on startup
|
||||||
|
- [x] Editable project name in header
|
||||||
|
- [x] Delete and duplicate projects
|
||||||
|
|
||||||
#### 12.2 Project Structure
|
#### 12.2 Project Structure ✅
|
||||||
- [ ] JSON project format
|
- [x] IndexedDB storage with serialization
|
||||||
- [ ] Track information
|
- [x] Track information (name, color, volume, pan, mute, solo)
|
||||||
- [ ] Audio buffer references
|
- [x] Audio buffer serialization (Float32Array per channel)
|
||||||
- [ ] Effect settings
|
- [x] Effect settings (serialized effect chains)
|
||||||
- [ ] Automation data
|
- [x] Automation data (deep cloned to remove functions)
|
||||||
- [ ] Region markers
|
- [x] Project metadata (name, description, duration, track count)
|
||||||
|
|
||||||
#### 12.3 Project Export/Import
|
#### 12.3 Project Export/Import
|
||||||
- [ ] Export project as JSON (with audio files)
|
- [ ] Export project as JSON (with audio files)
|
||||||
- [ ] Import project from JSON
|
- [ ] Import project from JSON
|
||||||
- [ ] Project templates
|
- [ ] Project templates
|
||||||
|
|
||||||
#### 12.4 Project Settings
|
#### 12.4 Project Settings ✅
|
||||||
- [ ] Sample rate
|
- [x] Sample rate (stored per project)
|
||||||
- [ ] Bit depth
|
- [x] Zoom level (persisted)
|
||||||
- [ ] Default track count
|
- [x] Current time (persisted)
|
||||||
- [ ] Project name/description
|
- [x] Project name/description
|
||||||
|
- [x] Created/updated timestamps
|
||||||
|
|
||||||
### Phase 13: Keyboard Shortcuts
|
### Phase 13: Keyboard Shortcuts
|
||||||
|
|
||||||
|
|||||||
@@ -881,7 +881,13 @@ export function AudioEditor() {
|
|||||||
}, [loadProjectsList]);
|
}, [loadProjectsList]);
|
||||||
|
|
||||||
// Save current project
|
// Save current project
|
||||||
const handleSaveProject = React.useCallback(async () => {
|
// Use ref to capture latest currentTime without triggering callback recreation
|
||||||
|
const currentTimeRef = React.useRef(currentTime);
|
||||||
|
React.useEffect(() => {
|
||||||
|
currentTimeRef.current = currentTime;
|
||||||
|
}, [currentTime]);
|
||||||
|
|
||||||
|
const handleSaveProject = React.useCallback(async (showToast = false) => {
|
||||||
if (tracks.length === 0) return;
|
if (tracks.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -904,7 +910,7 @@ export function AudioEditor() {
|
|||||||
tracks,
|
tracks,
|
||||||
{
|
{
|
||||||
zoom,
|
zoom,
|
||||||
currentTime,
|
currentTime: currentTimeRef.current, // Use ref value
|
||||||
sampleRate: audioContext.sampleRate,
|
sampleRate: audioContext.sampleRate,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -914,14 +920,18 @@ export function AudioEditor() {
|
|||||||
// Save last project ID to localStorage for auto-load on next visit
|
// Save last project ID to localStorage for auto-load on next visit
|
||||||
localStorage.setItem('audio-ui-last-project', projectId);
|
localStorage.setItem('audio-ui-last-project', projectId);
|
||||||
|
|
||||||
addToast({
|
// Only show toast for manual saves
|
||||||
title: 'Project Saved',
|
if (showToast) {
|
||||||
description: `"${currentProjectName}" saved successfully`,
|
addToast({
|
||||||
variant: 'success',
|
title: 'Project Saved',
|
||||||
duration: 2000,
|
description: `"${currentProjectName}" saved successfully`,
|
||||||
});
|
variant: 'success',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save project:', error);
|
console.error('Failed to save project:', error);
|
||||||
|
// Always show error toasts
|
||||||
addToast({
|
addToast({
|
||||||
title: 'Save Failed',
|
title: 'Save Failed',
|
||||||
description: 'Could not save project',
|
description: 'Could not save project',
|
||||||
@@ -929,17 +939,26 @@ export function AudioEditor() {
|
|||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [tracks, currentProjectId, currentProjectName, zoom, currentTime, addToast]);
|
}, [tracks, currentProjectId, currentProjectName, zoom, addToast]); // Removed currentTime from deps
|
||||||
|
|
||||||
// Auto-save effect (saves on track or name changes after 3 seconds of no changes)
|
// Auto-save effect (saves on track or name changes after 3 seconds of no changes)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (tracks.length === 0) return;
|
if (tracks.length === 0) return;
|
||||||
|
|
||||||
|
console.log('[Auto-save] Scheduling auto-save in 3 seconds...', {
|
||||||
|
trackCount: tracks.length,
|
||||||
|
projectName: currentProjectName,
|
||||||
|
});
|
||||||
|
|
||||||
const autoSaveTimer = setTimeout(() => {
|
const autoSaveTimer = setTimeout(() => {
|
||||||
|
console.log('[Auto-save] Triggering auto-save now');
|
||||||
handleSaveProject();
|
handleSaveProject();
|
||||||
}, 3000); // Auto-save after 3 seconds of no changes
|
}, 3000); // Auto-save after 3 seconds of no changes
|
||||||
|
|
||||||
return () => clearTimeout(autoSaveTimer);
|
return () => {
|
||||||
|
console.log('[Auto-save] Clearing auto-save timer');
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
};
|
||||||
}, [tracks, currentProjectName, handleSaveProject]);
|
}, [tracks, currentProjectName, handleSaveProject]);
|
||||||
|
|
||||||
// Create new project
|
// Create new project
|
||||||
@@ -1158,6 +1177,22 @@ export function AudioEditor() {
|
|||||||
category: 'playback',
|
category: 'playback',
|
||||||
action: stop,
|
action: stop,
|
||||||
},
|
},
|
||||||
|
// Project
|
||||||
|
{
|
||||||
|
id: 'save-project',
|
||||||
|
label: 'Save Project',
|
||||||
|
description: 'Save current project',
|
||||||
|
shortcut: 'Ctrl+S',
|
||||||
|
category: 'file',
|
||||||
|
action: () => handleSaveProject(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-projects',
|
||||||
|
label: 'Open Projects',
|
||||||
|
description: 'Open projects dialog',
|
||||||
|
category: 'file',
|
||||||
|
action: handleOpenProjectsDialog,
|
||||||
|
},
|
||||||
// View
|
// View
|
||||||
{
|
{
|
||||||
id: 'zoom-in',
|
id: 'zoom-in',
|
||||||
@@ -1204,7 +1239,7 @@ export function AudioEditor() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return actions;
|
return actions;
|
||||||
}, [play, pause, stop, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack]);
|
}, [play, pause, stop, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack]);
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -1260,6 +1295,13 @@ export function AudioEditor() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl/Cmd+S: Save project (manual save with toast)
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSaveProject(true); // Show toast for manual saves
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ctrl/Cmd+D: Duplicate
|
// Ctrl/Cmd+D: Duplicate
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1283,7 +1325,7 @@ export function AudioEditor() {
|
|||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate]);
|
}, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -45,21 +45,26 @@ function serializeEffects(effects: any[]): any[] {
|
|||||||
* Convert tracks to serialized format
|
* Convert tracks to serialized format
|
||||||
*/
|
*/
|
||||||
function serializeTracks(tracks: Track[]): SerializedTrack[] {
|
function serializeTracks(tracks: Track[]): SerializedTrack[] {
|
||||||
return tracks.map(track => ({
|
return tracks.map(track => {
|
||||||
id: track.id,
|
// Serialize automation by deep cloning to remove any functions
|
||||||
name: track.name,
|
const automation = track.automation ? JSON.parse(JSON.stringify(track.automation)) : { lanes: [], showAutomation: false };
|
||||||
color: track.color,
|
|
||||||
volume: track.volume,
|
return {
|
||||||
pan: track.pan,
|
id: track.id,
|
||||||
muted: track.mute,
|
name: track.name,
|
||||||
soloed: track.solo,
|
color: track.color,
|
||||||
collapsed: track.collapsed,
|
volume: track.volume,
|
||||||
height: track.height,
|
pan: track.pan,
|
||||||
audioBuffer: track.audioBuffer ? serializeAudioBuffer(track.audioBuffer) : null,
|
muted: track.mute,
|
||||||
effects: serializeEffects(track.effectChain?.effects || []),
|
soloed: track.solo,
|
||||||
automation: track.automation,
|
collapsed: track.collapsed,
|
||||||
recordEnabled: track.recordEnabled,
|
height: track.height,
|
||||||
}));
|
audioBuffer: track.audioBuffer ? serializeAudioBuffer(track.audioBuffer) : null,
|
||||||
|
effects: serializeEffects(track.effectChain?.effects || []),
|
||||||
|
automation,
|
||||||
|
recordEnabled: track.recordEnabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user