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
|
||||
|
||||
**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
|
||||
- ✅ **Phase 1**: Project Setup & Core Infrastructure (95% complete)
|
||||
@@ -761,33 +761,37 @@ audio-ui/
|
||||
- [x] Normalize on import option
|
||||
- [x] Import settings dialog component (ready for integration)
|
||||
|
||||
### Phase 12: Project Management
|
||||
### Phase 12: Project Management ✅
|
||||
|
||||
#### 12.1 Save/Load Projects
|
||||
- [ ] Save project to IndexedDB
|
||||
- [ ] Load project from IndexedDB
|
||||
- [ ] Project list UI
|
||||
- [ ] Auto-save functionality
|
||||
- [ ] Save-as functionality
|
||||
#### 12.1 Save/Load Projects ✅
|
||||
- [x] Save project to IndexedDB
|
||||
- [x] Load project from IndexedDB
|
||||
- [x] Project list UI (Projects dialog)
|
||||
- [x] Auto-save functionality (3-second debounce)
|
||||
- [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
|
||||
- [ ] JSON project format
|
||||
- [ ] Track information
|
||||
- [ ] Audio buffer references
|
||||
- [ ] Effect settings
|
||||
- [ ] Automation data
|
||||
- [ ] Region markers
|
||||
#### 12.2 Project Structure ✅
|
||||
- [x] IndexedDB storage with serialization
|
||||
- [x] Track information (name, color, volume, pan, mute, solo)
|
||||
- [x] Audio buffer serialization (Float32Array per channel)
|
||||
- [x] Effect settings (serialized effect chains)
|
||||
- [x] Automation data (deep cloned to remove functions)
|
||||
- [x] Project metadata (name, description, duration, track count)
|
||||
|
||||
#### 12.3 Project Export/Import
|
||||
- [ ] Export project as JSON (with audio files)
|
||||
- [ ] Import project from JSON
|
||||
- [ ] Project templates
|
||||
|
||||
#### 12.4 Project Settings
|
||||
- [ ] Sample rate
|
||||
- [ ] Bit depth
|
||||
- [ ] Default track count
|
||||
- [ ] Project name/description
|
||||
#### 12.4 Project Settings ✅
|
||||
- [x] Sample rate (stored per project)
|
||||
- [x] Zoom level (persisted)
|
||||
- [x] Current time (persisted)
|
||||
- [x] Project name/description
|
||||
- [x] Created/updated timestamps
|
||||
|
||||
### Phase 13: Keyboard Shortcuts
|
||||
|
||||
|
||||
@@ -881,7 +881,13 @@ export function AudioEditor() {
|
||||
}, [loadProjectsList]);
|
||||
|
||||
// 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;
|
||||
|
||||
try {
|
||||
@@ -904,7 +910,7 @@ export function AudioEditor() {
|
||||
tracks,
|
||||
{
|
||||
zoom,
|
||||
currentTime,
|
||||
currentTime: currentTimeRef.current, // Use ref value
|
||||
sampleRate: audioContext.sampleRate,
|
||||
}
|
||||
);
|
||||
@@ -914,14 +920,18 @@ export function AudioEditor() {
|
||||
// Save last project ID to localStorage for auto-load on next visit
|
||||
localStorage.setItem('audio-ui-last-project', projectId);
|
||||
|
||||
addToast({
|
||||
title: 'Project Saved',
|
||||
description: `"${currentProjectName}" saved successfully`,
|
||||
variant: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
// Only show toast for manual saves
|
||||
if (showToast) {
|
||||
addToast({
|
||||
title: 'Project Saved',
|
||||
description: `"${currentProjectName}" saved successfully`,
|
||||
variant: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save project:', error);
|
||||
// Always show error toasts
|
||||
addToast({
|
||||
title: 'Save Failed',
|
||||
description: 'Could not save project',
|
||||
@@ -929,17 +939,26 @@ export function AudioEditor() {
|
||||
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)
|
||||
React.useEffect(() => {
|
||||
if (tracks.length === 0) return;
|
||||
|
||||
console.log('[Auto-save] Scheduling auto-save in 3 seconds...', {
|
||||
trackCount: tracks.length,
|
||||
projectName: currentProjectName,
|
||||
});
|
||||
|
||||
const autoSaveTimer = setTimeout(() => {
|
||||
console.log('[Auto-save] Triggering auto-save now');
|
||||
handleSaveProject();
|
||||
}, 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]);
|
||||
|
||||
// Create new project
|
||||
@@ -1158,6 +1177,22 @@ export function AudioEditor() {
|
||||
category: 'playback',
|
||||
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
|
||||
{
|
||||
id: 'zoom-in',
|
||||
@@ -1204,7 +1239,7 @@ export function AudioEditor() {
|
||||
},
|
||||
];
|
||||
return actions;
|
||||
}, [play, pause, stop, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack]);
|
||||
}, [play, pause, stop, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
React.useEffect(() => {
|
||||
@@ -1260,6 +1295,13 @@ export function AudioEditor() {
|
||||
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
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
|
||||
e.preventDefault();
|
||||
@@ -1283,7 +1325,7 @@ export function AudioEditor() {
|
||||
|
||||
window.addEventListener('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 (
|
||||
<>
|
||||
|
||||
@@ -45,21 +45,26 @@ function serializeEffects(effects: any[]): any[] {
|
||||
* 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: serializeEffects(track.effectChain?.effects || []),
|
||||
automation: track.automation,
|
||||
recordEnabled: track.recordEnabled,
|
||||
}));
|
||||
return tracks.map(track => {
|
||||
// Serialize automation by deep cloning to remove any functions
|
||||
const automation = track.automation ? JSON.parse(JSON.stringify(track.automation)) : { lanes: [], showAutomation: false };
|
||||
|
||||
return {
|
||||
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: serializeEffects(track.effectChain?.effects || []),
|
||||
automation,
|
||||
recordEnabled: track.recordEnabled,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user