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:
@@ -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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -104,6 +104,9 @@ export function TrackList({
|
||||
onUpdateTrack(track.id, { name })
|
||||
}
|
||||
onSeek={onSeek}
|
||||
onLoadAudio={(buffer) =>
|
||||
onUpdateTrack(track.id, { audioBuffer: buffer })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user