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:
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user