feat: add click-to-load audio on empty track waveform

Changes:
- Empty tracks now show upload icon and "Click to load audio file" message
- Clicking the placeholder opens native file dialog
- Automatically decodes audio file and updates track with AudioBuffer
- Auto-renames track to filename if track name is still default
- Hover effect with background color change for better UX
- Message about drag & drop coming soon

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 07:06:46 +01:00
parent 8ffa8e8b81
commit 889b2b91ae
2 changed files with 54 additions and 4 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, CircleArrowOutUpRight } from 'lucide-react';
import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, CircleArrowOutUpRight, Upload } from 'lucide-react';
import type { Track as TrackType } from '@/types/track';
import { Button } from '@/components/ui/Button';
import { Slider } from '@/components/ui/Slider';
@@ -22,6 +22,7 @@ export interface TrackProps {
onRemove: () => void;
onNameChange: (name: string) => void;
onSeek?: (time: number) => void;
onLoadAudio?: (buffer: AudioBuffer) => void;
}
export function Track({
@@ -39,9 +40,11 @@ export function Track({
onRemove,
onNameChange,
onSeek,
onLoadAudio,
}: TrackProps) {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const [isEditingName, setIsEditingName] = React.useState(false);
const [nameInput, setNameInput] = React.useState(String(track.name || 'Untitled Track'));
const inputRef = React.useRef<HTMLInputElement>(null);
@@ -162,6 +165,33 @@ export function Track({
onSeek(clickTime);
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !onLoadAudio) return;
try {
const arrayBuffer = await file.arrayBuffer();
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
onLoadAudio(audioBuffer);
// Update track name to filename if it's still default
if (track.name === 'New Track' || track.name === 'Untitled Track') {
const fileName = file.name.replace(/\.[^/.]+$/, '');
onNameChange(fileName);
}
} catch (error) {
console.error('Failed to load audio file:', error);
}
// Reset input
e.target.value = '';
};
const handleLoadAudioClick = () => {
fileInputRef.current?.click();
};
const trackHeight = track.collapsed ? 48 : track.height;
return (
@@ -322,9 +352,26 @@ export function Track({
onClick={handleCanvasClick}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
No audio loaded
</div>
<>
<div
className="absolute inset-0 flex flex-col items-center justify-center text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors cursor-pointer group"
onClick={(e) => {
e.stopPropagation();
handleLoadAudioClick();
}}
>
<Upload className="h-8 w-8 mb-2 opacity-50 group-hover:opacity-100" />
<p>Click to load audio file</p>
<p className="text-xs opacity-75 mt-1">or drag & drop (coming soon)</p>
</div>
<input
ref={fileInputRef}
type="file"
accept="audio/*"
onChange={handleFileChange}
className="hidden"
/>
</>
)}
</>
)}

View File

@@ -104,6 +104,9 @@ export function TrackList({
onUpdateTrack(track.id, { name })
}
onSeek={onSeek}
onLoadAudio={(buffer) =>
onUpdateTrack(track.id, { audioBuffer: buffer })
}
/>
))}
</div>