feat: implement Phase 5 - undo/redo system with command pattern
Added comprehensive undo/redo functionality: - Command pattern interface and base classes - HistoryManager with 50-operation stack - EditCommand for all edit operations (cut, delete, paste, trim) - Full keyboard shortcuts (Ctrl+Z undo, Ctrl+Y/Ctrl+Shift+Z redo) - HistoryControls UI component with visual feedback - Integrated history system with all edit operations - Toast notifications for undo/redo actions - History state tracking and display New files: - lib/history/command.ts - Command interface and BaseCommand - lib/history/history-manager.ts - HistoryManager class - lib/history/commands/edit-command.ts - EditCommand and factory functions - lib/hooks/useHistory.ts - React hook for history management - components/editor/HistoryControls.tsx - History UI component Modified files: - components/editor/AudioEditor.tsx - Integrated history system - components/editor/EditControls.tsx - Updated keyboard shortcuts display 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,9 @@ import { Waveform } from './Waveform';
|
||||
import { PlaybackControls } from './PlaybackControls';
|
||||
import { ZoomControls } from './ZoomControls';
|
||||
import { EditControls } from './EditControls';
|
||||
import { HistoryControls } from './HistoryControls';
|
||||
import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';
|
||||
import { useHistory } from '@/lib/hooks/useHistory';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Slider } from '@/components/ui/Slider';
|
||||
@@ -19,6 +21,12 @@ import {
|
||||
insertBufferSegment,
|
||||
trimBuffer,
|
||||
} from '@/lib/audio/buffer-utils';
|
||||
import {
|
||||
createCutCommand,
|
||||
createDeleteCommand,
|
||||
createPasteCommand,
|
||||
createTrimCommand,
|
||||
} from '@/lib/history/commands/edit-command';
|
||||
|
||||
export function AudioEditor() {
|
||||
// Zoom and scroll state
|
||||
@@ -29,6 +37,7 @@ export function AudioEditor() {
|
||||
// Selection state
|
||||
const [selection, setSelection] = React.useState<Selection | null>(null);
|
||||
const [clipboard, setClipboard] = React.useState<ClipboardData | null>(null);
|
||||
|
||||
const {
|
||||
loadFile,
|
||||
loadBuffer,
|
||||
@@ -51,6 +60,7 @@ export function AudioEditor() {
|
||||
durationFormatted,
|
||||
} = useAudioPlayer();
|
||||
|
||||
const { execute, undo, redo, clear: clearHistory, state: historyState } = useHistory(50);
|
||||
const { addToast } = useToast();
|
||||
|
||||
const handleFileSelect = async (file: File) => {
|
||||
@@ -79,6 +89,7 @@ export function AudioEditor() {
|
||||
setAmplitudeScale(1);
|
||||
setSelection(null);
|
||||
setClipboard(null);
|
||||
clearHistory();
|
||||
addToast({
|
||||
title: 'Audio cleared',
|
||||
description: 'Audio file has been removed',
|
||||
@@ -101,9 +112,11 @@ export function AudioEditor() {
|
||||
duration: selection.end - selection.start,
|
||||
});
|
||||
|
||||
// Delete from buffer
|
||||
const newBuffer = deleteBufferSegment(audioBuffer, selection.start, selection.end);
|
||||
loadBuffer(newBuffer);
|
||||
// Create and execute cut command
|
||||
const command = createCutCommand(audioBuffer, selection, (buffer) => {
|
||||
loadBuffer(buffer);
|
||||
});
|
||||
execute(command);
|
||||
|
||||
setSelection(null);
|
||||
addToast({
|
||||
@@ -155,8 +168,12 @@ export function AudioEditor() {
|
||||
|
||||
try {
|
||||
const insertTime = currentTime;
|
||||
const newBuffer = insertBufferSegment(audioBuffer, clipboard.buffer, insertTime);
|
||||
loadBuffer(newBuffer);
|
||||
|
||||
// Create and execute paste command
|
||||
const command = createPasteCommand(audioBuffer, clipboard.buffer, insertTime, (buffer) => {
|
||||
loadBuffer(buffer);
|
||||
});
|
||||
execute(command);
|
||||
|
||||
addToast({
|
||||
title: 'Pasted',
|
||||
@@ -178,8 +195,11 @@ export function AudioEditor() {
|
||||
if (!selection || !audioBuffer) return;
|
||||
|
||||
try {
|
||||
const newBuffer = deleteBufferSegment(audioBuffer, selection.start, selection.end);
|
||||
loadBuffer(newBuffer);
|
||||
// Create and execute delete command
|
||||
const command = createDeleteCommand(audioBuffer, selection, (buffer) => {
|
||||
loadBuffer(buffer);
|
||||
});
|
||||
execute(command);
|
||||
|
||||
setSelection(null);
|
||||
addToast({
|
||||
@@ -202,8 +222,11 @@ export function AudioEditor() {
|
||||
if (!selection || !audioBuffer) return;
|
||||
|
||||
try {
|
||||
const newBuffer = trimBuffer(audioBuffer, selection.start, selection.end);
|
||||
loadBuffer(newBuffer);
|
||||
// Create and execute trim command
|
||||
const command = createTrimCommand(audioBuffer, selection, (buffer) => {
|
||||
loadBuffer(buffer);
|
||||
});
|
||||
execute(command);
|
||||
|
||||
setSelection(null);
|
||||
addToast({
|
||||
@@ -263,6 +286,32 @@ export function AudioEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Z: Undo
|
||||
if (e.ctrlKey && !e.shiftKey && e.key === 'z') {
|
||||
e.preventDefault();
|
||||
if (undo()) {
|
||||
addToast({
|
||||
title: 'Undo',
|
||||
description: 'Last action undone',
|
||||
variant: 'info',
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+Y or Ctrl+Shift+Z: Redo
|
||||
if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) {
|
||||
e.preventDefault();
|
||||
if (redo()) {
|
||||
addToast({
|
||||
title: 'Redo',
|
||||
description: 'Last action redone',
|
||||
variant: 'info',
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+A: Select all
|
||||
if (e.ctrlKey && e.key === 'a') {
|
||||
e.preventDefault();
|
||||
@@ -302,7 +351,7 @@ export function AudioEditor() {
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selection, clipboard, audioBuffer, currentTime]);
|
||||
}, [selection, clipboard, audioBuffer, currentTime, undo, redo, addToast]);
|
||||
|
||||
// Show error toast
|
||||
React.useEffect(() => {
|
||||
@@ -400,6 +449,21 @@ export function AudioEditor() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* History Controls */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<HistoryControls
|
||||
historyState={historyState}
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
onClear={clearHistory}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Zoom Controls */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user