From 889b2b91aec1ce5a6f42ee0c13738b141a599d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Tue, 18 Nov 2025 07:06:46 +0100 Subject: [PATCH] feat: add click-to-load audio on empty track waveform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/tracks/Track.tsx | 55 ++++++++++++++++++++++++++++++--- components/tracks/TrackList.tsx | 3 ++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/components/tracks/Track.tsx b/components/tracks/Track.tsx index 3d8504f..476fa80 100644 --- a/components/tracks/Track.tsx +++ b/components/tracks/Track.tsx @@ -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(null); const containerRef = React.useRef(null); + const fileInputRef = React.useRef(null); const [isEditingName, setIsEditingName] = React.useState(false); const [nameInput, setNameInput] = React.useState(String(track.name || 'Untitled Track')); const inputRef = React.useRef(null); @@ -162,6 +165,33 @@ export function Track({ onSeek(clickTime); }; + const handleFileChange = async (e: React.ChangeEvent) => { + 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} /> ) : ( -
- No audio loaded -
+ <> +
{ + e.stopPropagation(); + handleLoadAudioClick(); + }} + > + +

Click to load audio file

+

or drag & drop (coming soon)

+
+ + )} )} diff --git a/components/tracks/TrackList.tsx b/components/tracks/TrackList.tsx index 7fec446..d879629 100644 --- a/components/tracks/TrackList.tsx +++ b/components/tracks/TrackList.tsx @@ -104,6 +104,9 @@ export function TrackList({ onUpdateTrack(track.id, { name }) } onSeek={onSeek} + onLoadAudio={(buffer) => + onUpdateTrack(track.id, { audioBuffer: buffer }) + } /> ))}