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';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
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 { Button } from '@/components/ui/Button';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import type { AutomationMode } from '@/types/automation';
|
import type { AutomationMode } from '@/types/automation';
|
||||||
@@ -21,6 +21,9 @@ export interface AutomationHeaderProps {
|
|||||||
availableParameters?: Array<{ id: string; name: string }>;
|
availableParameters?: Array<{ id: string; name: string }>;
|
||||||
selectedParameterId?: string;
|
selectedParameterId?: string;
|
||||||
onParameterChange?: (parameterId: string) => void;
|
onParameterChange?: (parameterId: string) => void;
|
||||||
|
// Copy/Paste automation
|
||||||
|
onCopyAutomation?: () => void;
|
||||||
|
onPasteAutomation?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODE_LABELS: Record<AutomationMode, string> = {
|
const MODE_LABELS: Record<AutomationMode, string> = {
|
||||||
@@ -51,6 +54,8 @@ export function AutomationHeader({
|
|||||||
availableParameters,
|
availableParameters,
|
||||||
selectedParameterId,
|
selectedParameterId,
|
||||||
onParameterChange,
|
onParameterChange,
|
||||||
|
onCopyAutomation,
|
||||||
|
onPasteAutomation,
|
||||||
}: AutomationHeaderProps) {
|
}: AutomationHeaderProps) {
|
||||||
const modes: AutomationMode[] = ['read', 'write', 'touch', 'latch'];
|
const modes: AutomationMode[] = ['read', 'write', 'touch', 'latch'];
|
||||||
const currentModeIndex = modes.indexOf(mode);
|
const currentModeIndex = modes.indexOf(mode);
|
||||||
@@ -145,6 +150,34 @@ export function AutomationHeader({
|
|||||||
</div>
|
</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 */}
|
{/* Show/hide toggle - Positioned absolutely on the right */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export function AudioEditor() {
|
|||||||
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
|
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
|
||||||
const [masterControlsCollapsed, setMasterControlsCollapsed] = React.useState(false);
|
const [masterControlsCollapsed, setMasterControlsCollapsed] = React.useState(false);
|
||||||
const [clipboard, setClipboard] = React.useState<AudioBuffer | null>(null);
|
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 [recordingTrackId, setRecordingTrackId] = React.useState<string | null>(null);
|
||||||
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
|
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
|
||||||
const [punchInTime, setPunchInTime] = React.useState(0);
|
const [punchInTime, setPunchInTime] = React.useState(0);
|
||||||
@@ -863,6 +864,90 @@ export function AudioEditor() {
|
|||||||
});
|
});
|
||||||
}, [selectedTrackId, tracks, currentTime, addTrackFromBuffer, removeTrack, addToast]);
|
}, [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
|
// Export handler
|
||||||
const handleExport = React.useCallback(async (settings: ExportSettings) => {
|
const handleExport = React.useCallback(async (settings: ExportSettings) => {
|
||||||
if (tracks.length === 0) {
|
if (tracks.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user