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>
|
||||
|
||||
@@ -125,7 +125,7 @@ export function EditControls({
|
||||
|
||||
{/* Keyboard Shortcuts Info */}
|
||||
<div className="text-xs text-muted-foreground space-y-1 p-3 rounded-lg bg-muted/30">
|
||||
<p className="font-medium mb-2">Keyboard Shortcuts:</p>
|
||||
<p className="font-medium mb-2">Edit Shortcuts:</p>
|
||||
<p>• Shift+Drag: Select region</p>
|
||||
<p>• Ctrl+A: Select all</p>
|
||||
<p>• Ctrl+X: Cut</p>
|
||||
|
||||
107
components/editor/HistoryControls.tsx
Normal file
107
components/editor/HistoryControls.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Undo2, Redo2, History, Info } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import type { HistoryState } from '@/lib/history/history-manager';
|
||||
|
||||
export interface HistoryControlsProps {
|
||||
historyState: HistoryState;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onClear?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HistoryControls({
|
||||
historyState,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onClear,
|
||||
className,
|
||||
}: HistoryControlsProps) {
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* History Info */}
|
||||
{historyState.historySize > 0 && (
|
||||
<div className="rounded-lg border border-info bg-info/10 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="h-4 w-4 text-info-foreground mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0 text-sm">
|
||||
<p className="font-medium text-info-foreground">History Available</p>
|
||||
<p className="text-info-foreground/90 mt-1">
|
||||
{historyState.historySize} action{historyState.historySize !== 1 ? 's' : ''} in history
|
||||
</p>
|
||||
{historyState.undoDescription && (
|
||||
<p className="text-xs text-info-foreground/75 mt-1">
|
||||
Next undo: {historyState.undoDescription}
|
||||
</p>
|
||||
)}
|
||||
{historyState.redoDescription && (
|
||||
<p className="text-xs text-info-foreground/75 mt-1">
|
||||
Next redo: {historyState.redoDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Control Buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onUndo}
|
||||
disabled={!historyState.canUndo}
|
||||
title={
|
||||
historyState.canUndo
|
||||
? `Undo ${historyState.undoDescription} (Ctrl+Z)`
|
||||
: 'Nothing to undo (Ctrl+Z)'
|
||||
}
|
||||
className="justify-start"
|
||||
>
|
||||
<Undo2 className="h-4 w-4 mr-2" />
|
||||
Undo
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRedo}
|
||||
disabled={!historyState.canRedo}
|
||||
title={
|
||||
historyState.canRedo
|
||||
? `Redo ${historyState.redoDescription} (Ctrl+Y)`
|
||||
: 'Nothing to redo (Ctrl+Y)'
|
||||
}
|
||||
className="justify-start"
|
||||
>
|
||||
<Redo2 className="h-4 w-4 mr-2" />
|
||||
Redo
|
||||
</Button>
|
||||
|
||||
{onClear && historyState.historySize > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClear}
|
||||
title="Clear history"
|
||||
className="justify-start col-span-2"
|
||||
>
|
||||
<History className="h-4 w-4 mr-2" />
|
||||
Clear History ({historyState.historySize})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Keyboard Shortcuts Info */}
|
||||
<div className="text-xs text-muted-foreground space-y-1 p-3 rounded-lg bg-muted/30">
|
||||
<p className="font-medium mb-2">History Shortcuts:</p>
|
||||
<p>• Ctrl+Z: Undo last action</p>
|
||||
<p>• Ctrl+Y or Ctrl+Shift+Z: Redo last action</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground/75">
|
||||
History tracks up to 50 edit operations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
lib/history/command.ts
Normal file
38
lib/history/command.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Command Pattern for Undo/Redo System
|
||||
*/
|
||||
|
||||
export interface Command {
|
||||
/**
|
||||
* Execute the command
|
||||
*/
|
||||
execute(): void;
|
||||
|
||||
/**
|
||||
* Undo the command
|
||||
*/
|
||||
undo(): void;
|
||||
|
||||
/**
|
||||
* Redo the command (default: call execute again)
|
||||
*/
|
||||
redo(): void;
|
||||
|
||||
/**
|
||||
* Get a description of the command for UI display
|
||||
*/
|
||||
getDescription(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base command class with default redo implementation
|
||||
*/
|
||||
export abstract class BaseCommand implements Command {
|
||||
abstract execute(): void;
|
||||
abstract undo(): void;
|
||||
abstract getDescription(): string;
|
||||
|
||||
redo(): void {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
155
lib/history/commands/edit-command.ts
Normal file
155
lib/history/commands/edit-command.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Edit commands for audio buffer operations
|
||||
*/
|
||||
|
||||
import { BaseCommand } from '../command';
|
||||
import type { Selection } from '@/types/selection';
|
||||
import {
|
||||
extractBufferSegment,
|
||||
deleteBufferSegment,
|
||||
insertBufferSegment,
|
||||
trimBuffer,
|
||||
} from '@/lib/audio/buffer-utils';
|
||||
|
||||
export type EditCommandType = 'cut' | 'delete' | 'paste' | 'trim';
|
||||
|
||||
export interface EditCommandParams {
|
||||
type: EditCommandType;
|
||||
beforeBuffer: AudioBuffer;
|
||||
afterBuffer: AudioBuffer;
|
||||
selection?: Selection;
|
||||
clipboardData?: AudioBuffer;
|
||||
pastePosition?: number;
|
||||
onApply: (buffer: AudioBuffer) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command for edit operations (cut, delete, paste, trim)
|
||||
*/
|
||||
export class EditCommand extends BaseCommand {
|
||||
private type: EditCommandType;
|
||||
private beforeBuffer: AudioBuffer;
|
||||
private afterBuffer: AudioBuffer;
|
||||
private selection?: Selection;
|
||||
private clipboardData?: AudioBuffer;
|
||||
private pastePosition?: number;
|
||||
private onApply: (buffer: AudioBuffer) => void;
|
||||
|
||||
constructor(params: EditCommandParams) {
|
||||
super();
|
||||
this.type = params.type;
|
||||
this.beforeBuffer = params.beforeBuffer;
|
||||
this.afterBuffer = params.afterBuffer;
|
||||
this.selection = params.selection;
|
||||
this.clipboardData = params.clipboardData;
|
||||
this.pastePosition = params.pastePosition;
|
||||
this.onApply = params.onApply;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.onApply(this.afterBuffer);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
this.onApply(this.beforeBuffer);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
switch (this.type) {
|
||||
case 'cut':
|
||||
return 'Cut';
|
||||
case 'delete':
|
||||
return 'Delete';
|
||||
case 'paste':
|
||||
return 'Paste';
|
||||
case 'trim':
|
||||
return 'Trim';
|
||||
default:
|
||||
return 'Edit';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of edit operation
|
||||
*/
|
||||
getType(): EditCommandType {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selection that was affected
|
||||
*/
|
||||
getSelection(): Selection | undefined {
|
||||
return this.selection;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory functions to create edit commands
|
||||
*/
|
||||
|
||||
export function createCutCommand(
|
||||
buffer: AudioBuffer,
|
||||
selection: Selection,
|
||||
onApply: (buffer: AudioBuffer) => void
|
||||
): EditCommand {
|
||||
const afterBuffer = deleteBufferSegment(buffer, selection.start, selection.end);
|
||||
|
||||
return new EditCommand({
|
||||
type: 'cut',
|
||||
beforeBuffer: buffer,
|
||||
afterBuffer,
|
||||
selection,
|
||||
onApply,
|
||||
});
|
||||
}
|
||||
|
||||
export function createDeleteCommand(
|
||||
buffer: AudioBuffer,
|
||||
selection: Selection,
|
||||
onApply: (buffer: AudioBuffer) => void
|
||||
): EditCommand {
|
||||
const afterBuffer = deleteBufferSegment(buffer, selection.start, selection.end);
|
||||
|
||||
return new EditCommand({
|
||||
type: 'delete',
|
||||
beforeBuffer: buffer,
|
||||
afterBuffer,
|
||||
selection,
|
||||
onApply,
|
||||
});
|
||||
}
|
||||
|
||||
export function createPasteCommand(
|
||||
buffer: AudioBuffer,
|
||||
clipboardData: AudioBuffer,
|
||||
pastePosition: number,
|
||||
onApply: (buffer: AudioBuffer) => void
|
||||
): EditCommand {
|
||||
const afterBuffer = insertBufferSegment(buffer, clipboardData, pastePosition);
|
||||
|
||||
return new EditCommand({
|
||||
type: 'paste',
|
||||
beforeBuffer: buffer,
|
||||
afterBuffer,
|
||||
clipboardData,
|
||||
pastePosition,
|
||||
onApply,
|
||||
});
|
||||
}
|
||||
|
||||
export function createTrimCommand(
|
||||
buffer: AudioBuffer,
|
||||
selection: Selection,
|
||||
onApply: (buffer: AudioBuffer) => void
|
||||
): EditCommand {
|
||||
const afterBuffer = trimBuffer(buffer, selection.start, selection.end);
|
||||
|
||||
return new EditCommand({
|
||||
type: 'trim',
|
||||
beforeBuffer: buffer,
|
||||
afterBuffer,
|
||||
selection,
|
||||
onApply,
|
||||
});
|
||||
}
|
||||
156
lib/history/history-manager.ts
Normal file
156
lib/history/history-manager.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* History Manager for Undo/Redo functionality
|
||||
*/
|
||||
|
||||
import type { Command } from './command';
|
||||
|
||||
export interface HistoryState {
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
undoDescription: string | null;
|
||||
redoDescription: string | null;
|
||||
historySize: number;
|
||||
}
|
||||
|
||||
export class HistoryManager {
|
||||
private undoStack: Command[] = [];
|
||||
private redoStack: Command[] = [];
|
||||
private maxHistorySize: number;
|
||||
private listeners: Set<() => void> = new Set();
|
||||
|
||||
constructor(maxHistorySize: number = 50) {
|
||||
this.maxHistorySize = maxHistorySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command and add it to history
|
||||
*/
|
||||
execute(command: Command): void {
|
||||
command.execute();
|
||||
this.undoStack.push(command);
|
||||
|
||||
// Limit history size
|
||||
if (this.undoStack.length > this.maxHistorySize) {
|
||||
this.undoStack.shift();
|
||||
}
|
||||
|
||||
// Clear redo stack when new command is executed
|
||||
this.redoStack = [];
|
||||
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last command
|
||||
*/
|
||||
undo(): boolean {
|
||||
if (!this.canUndo()) return false;
|
||||
|
||||
const command = this.undoStack.pop()!;
|
||||
command.undo();
|
||||
this.redoStack.push(command);
|
||||
|
||||
this.notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last undone command
|
||||
*/
|
||||
redo(): boolean {
|
||||
if (!this.canRedo()) return false;
|
||||
|
||||
const command = this.redoStack.pop()!;
|
||||
command.redo();
|
||||
this.undoStack.push(command);
|
||||
|
||||
this.notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if undo is available
|
||||
*/
|
||||
canUndo(): boolean {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if redo is available
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current history state
|
||||
*/
|
||||
getState(): HistoryState {
|
||||
return {
|
||||
canUndo: this.canUndo(),
|
||||
canRedo: this.canRedo(),
|
||||
undoDescription: this.getUndoDescription(),
|
||||
redoDescription: this.getRedoDescription(),
|
||||
historySize: this.undoStack.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description of next undo action
|
||||
*/
|
||||
getUndoDescription(): string | null {
|
||||
if (!this.canUndo()) return null;
|
||||
return this.undoStack[this.undoStack.length - 1].getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description of next redo action
|
||||
*/
|
||||
getRedoDescription(): string | null {
|
||||
if (!this.canRedo()) return null;
|
||||
return this.redoStack[this.redoStack.length - 1].getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all history
|
||||
*/
|
||||
clear(): void {
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to history changes
|
||||
*/
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners of history changes
|
||||
*/
|
||||
private notifyListeners(): void {
|
||||
this.listeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current history size
|
||||
*/
|
||||
getHistorySize(): number {
|
||||
return this.undoStack.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum history size
|
||||
*/
|
||||
setMaxHistorySize(size: number): void {
|
||||
this.maxHistorySize = size;
|
||||
// Trim undo stack if needed
|
||||
while (this.undoStack.length > this.maxHistorySize) {
|
||||
this.undoStack.shift();
|
||||
}
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
54
lib/hooks/useHistory.ts
Normal file
54
lib/hooks/useHistory.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { HistoryManager } from '@/lib/history/history-manager';
|
||||
import type { HistoryState } from '@/lib/history/history-manager';
|
||||
import type { Command } from '@/lib/history/command';
|
||||
|
||||
export interface UseHistoryReturn {
|
||||
execute: (command: Command) => void;
|
||||
undo: () => boolean;
|
||||
redo: () => boolean;
|
||||
clear: () => void;
|
||||
state: HistoryState;
|
||||
}
|
||||
|
||||
export function useHistory(maxHistorySize: number = 50): UseHistoryReturn {
|
||||
const [manager] = React.useState(() => new HistoryManager(maxHistorySize));
|
||||
const [state, setState] = React.useState<HistoryState>(manager.getState());
|
||||
|
||||
React.useEffect(() => {
|
||||
const unsubscribe = manager.subscribe(() => {
|
||||
setState(manager.getState());
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [manager]);
|
||||
|
||||
const execute = React.useCallback(
|
||||
(command: Command) => {
|
||||
manager.execute(command);
|
||||
},
|
||||
[manager]
|
||||
);
|
||||
|
||||
const undo = React.useCallback(() => {
|
||||
return manager.undo();
|
||||
}, [manager]);
|
||||
|
||||
const redo = React.useCallback(() => {
|
||||
return manager.redo();
|
||||
}, [manager]);
|
||||
|
||||
const clear = React.useCallback(() => {
|
||||
manager.clear();
|
||||
}, [manager]);
|
||||
|
||||
return {
|
||||
execute,
|
||||
undo,
|
||||
redo,
|
||||
clear,
|
||||
state,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user