feat: add copy/paste automation data functionality

Implemented automation clipboard system:
- Added separate automationClipboard state for automation points
- Created handleCopyAutomation function to copy automation lane points
- Created handlePasteAutomation function to paste at current time with time offset
- Added Copy and Clipboard icon buttons to AutomationHeader component
- Automation points preserve curve type and value when copied/pasted
- Points are sorted by time after pasting
- Toast notifications for user feedback
- Ready for integration when automation lanes are actively used

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 07:51:24 +01:00
parent 08b33aacb5
commit 25ddac349b
2 changed files with 119 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Eye, EyeOff, ChevronDown, ChevronUp } from 'lucide-react';
import { Eye, EyeOff, ChevronDown, ChevronUp, Copy, Clipboard } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import type { AutomationMode } from '@/types/automation';
@@ -21,6 +21,9 @@ export interface AutomationHeaderProps {
availableParameters?: Array<{ id: string; name: string }>;
selectedParameterId?: string;
onParameterChange?: (parameterId: string) => void;
// Copy/Paste automation
onCopyAutomation?: () => void;
onPasteAutomation?: () => void;
}
const MODE_LABELS: Record<AutomationMode, string> = {
@@ -51,6 +54,8 @@ export function AutomationHeader({
availableParameters,
selectedParameterId,
onParameterChange,
onCopyAutomation,
onPasteAutomation,
}: AutomationHeaderProps) {
const modes: AutomationMode[] = ['read', 'write', 'touch', 'latch'];
const currentModeIndex = modes.indexOf(mode);
@@ -145,6 +150,34 @@ export function AutomationHeader({
</div>
)}
{/* Copy/Paste automation controls */}
{(onCopyAutomation || onPasteAutomation) && (
<div className="flex gap-1 flex-shrink-0 ml-auto mr-8">
{onCopyAutomation && (
<Button
variant="ghost"
size="icon-sm"
onClick={onCopyAutomation}
title="Copy automation data"
className="h-5 w-5"
>
<Copy className="h-3 w-3" />
</Button>
)}
{onPasteAutomation && (
<Button
variant="ghost"
size="icon-sm"
onClick={onPasteAutomation}
title="Paste automation data"
className="h-5 w-5"
>
<Clipboard className="h-3 w-3" />
</Button>
)}
</div>
)}
{/* Show/hide toggle - Positioned absolutely on the right */}
<Button
variant="ghost"

View File

@@ -77,6 +77,7 @@ export function AudioEditor() {
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
const [masterControlsCollapsed, setMasterControlsCollapsed] = React.useState(false);
const [clipboard, setClipboard] = React.useState<AudioBuffer | null>(null);
const [automationClipboard, setAutomationClipboard] = React.useState<{ points: Array<{ time: number; value: number; curve: string }> } | null>(null);
const [recordingTrackId, setRecordingTrackId] = React.useState<string | null>(null);
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
const [punchInTime, setPunchInTime] = React.useState(0);
@@ -863,6 +864,90 @@ export function AudioEditor() {
});
}, [selectedTrackId, tracks, currentTime, addTrackFromBuffer, removeTrack, addToast]);
const handleCopyAutomation = React.useCallback((trackId: string, laneId: string) => {
const track = tracks.find((t) => t.id === trackId);
if (!track) return;
const lane = track.automation.lanes.find((l) => l.id === laneId);
if (!lane || lane.points.length === 0) {
addToast({
title: 'No Automation Data',
description: 'Selected automation lane has no points',
variant: 'error',
duration: 2000,
});
return;
}
// Copy the automation points
setAutomationClipboard({
points: lane.points.map(p => ({
time: p.time,
value: p.value,
curve: p.curve,
})),
});
addToast({
title: 'Automation Copied',
description: `Copied ${lane.points.length} automation point(s)`,
variant: 'success',
duration: 2000,
});
}, [tracks, addToast]);
const handlePasteAutomation = React.useCallback((trackId: string, laneId: string) => {
if (!automationClipboard) {
addToast({
title: 'Nothing to Paste',
description: 'No automation data in clipboard',
variant: 'error',
duration: 2000,
});
return;
}
const track = tracks.find((t) => t.id === trackId);
if (!track) return;
const lane = track.automation.lanes.find((l) => l.id === laneId);
if (!lane) return;
// Generate new IDs for pasted points and shift them to current time
const timeOffset = currentTime;
const newPoints = automationClipboard.points.map((p) => ({
id: `point-${Date.now()}-${Math.random()}`,
time: p.time + timeOffset,
value: p.value,
curve: p.curve as 'linear' | 'bezier' | 'step',
}));
// Merge with existing points and sort by time
const updatedLanes = track.automation.lanes.map((l) => {
if (l.id === laneId) {
return {
...l,
points: [...l.points, ...newPoints].sort((a, b) => a.time - b.time),
};
}
return l;
});
updateTrack(trackId, {
automation: {
...track.automation,
lanes: updatedLanes,
},
});
addToast({
title: 'Automation Pasted',
description: `Pasted ${newPoints.length} automation point(s) at ${formatDuration(currentTime)}`,
variant: 'success',
duration: 2000,
});
}, [automationClipboard, tracks, currentTime, updateTrack, addToast]);
// Export handler
const handleExport = React.useCallback(async (settings: ExportSettings) => {
if (tracks.length === 0) {