close sh;able to save/load projects
This commit is contained in:
@ -42,6 +42,10 @@ export default function WaveformTimeline() {
|
||||
const zoomRef = useRef(1); // 1 = show all, >1 = zoomed in
|
||||
const scrollSecsRef = useRef(0); // seconds scrolled from left
|
||||
const rafRef = useRef(0);
|
||||
// Ref so the RAF loop can call drawStaticWaveform without a stale closure
|
||||
const drawStaticWaveformRef = useRef<() => void>(() => {});
|
||||
const isDraggingRef = useRef(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoUrl || !videoPath) return;
|
||||
@ -226,6 +230,11 @@ export default function WaveformTimeline() {
|
||||
ctx.stroke();
|
||||
}, [deletedRanges]);
|
||||
|
||||
// Keep the ref in sync with the latest drawStaticWaveform closure
|
||||
useEffect(() => {
|
||||
drawStaticWaveformRef.current = drawStaticWaveform;
|
||||
}, [drawStaticWaveform]);
|
||||
|
||||
// Redraw static layer when deletedRanges change
|
||||
useEffect(() => {
|
||||
drawStaticWaveform();
|
||||
@ -260,15 +269,24 @@ export default function WaveformTimeline() {
|
||||
|
||||
if (dur > 0 && video) {
|
||||
const pxPerSec = (width * zoomRef.current) / dur;
|
||||
const px = (video.currentTime - scrollSecsRef.current) * pxPerSec;
|
||||
if (px >= 0 && px <= width) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#6366f1';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.moveTo(px, 0);
|
||||
ctx.lineTo(px, height);
|
||||
ctx.stroke();
|
||||
let px = (video.currentTime - scrollSecsRef.current) * pxPerSec;
|
||||
|
||||
// If the playhead is off-screen (e.g. after a seek from the transcript),
|
||||
// scroll so it's centered and redraw the static waveform layer.
|
||||
if (px < 0 || px > width) {
|
||||
const visibleSecs = width / pxPerSec;
|
||||
const maxScroll = Math.max(0, dur - visibleSecs);
|
||||
scrollSecsRef.current = Math.max(0, Math.min(maxScroll, video.currentTime - visibleSecs / 2));
|
||||
drawStaticWaveformRef.current();
|
||||
px = (video.currentTime - scrollSecsRef.current) * pxPerSec;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#6366f1';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.moveTo(px, 0);
|
||||
ctx.lineTo(px, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
@ -316,20 +334,40 @@ export default function WaveformTimeline() {
|
||||
drawStaticWaveform();
|
||||
}, [drawStaticWaveform]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
const seekToClientX = useCallback((clientX: number) => {
|
||||
const buffer = audioBufferRef.current;
|
||||
const canvas = headCanvasRef.current;
|
||||
if (!canvas || !buffer) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
const pxPerSec = (rect.width * zoomRef.current) / buffer.duration;
|
||||
const newTime = Math.max(0, Math.min(buffer.duration, scrollSecsRef.current + x / pxPerSec));
|
||||
setCurrentTime(newTime);
|
||||
const video = document.querySelector('video') as HTMLVideoElement | null;
|
||||
if (video) video.currentTime = newTime;
|
||||
}, [setCurrentTime]);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const buffer = audioBufferRef.current;
|
||||
const canvas = headCanvasRef.current;
|
||||
if (!canvas || !buffer) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const pxPerSec = (rect.width * zoomRef.current) / buffer.duration;
|
||||
const newTime = Math.max(0, Math.min(buffer.duration, scrollSecsRef.current + x / pxPerSec));
|
||||
setCurrentTime(newTime);
|
||||
const video = document.querySelector('video');
|
||||
if (video) video.currentTime = newTime;
|
||||
e.preventDefault();
|
||||
isDraggingRef.current = true;
|
||||
setIsDragging(true);
|
||||
seekToClientX(e.clientX);
|
||||
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!isDraggingRef.current) return;
|
||||
seekToClientX(ev.clientX);
|
||||
};
|
||||
const onUp = () => {
|
||||
isDraggingRef.current = false;
|
||||
setIsDragging(false);
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
},
|
||||
[setCurrentTime],
|
||||
[seekToClientX],
|
||||
);
|
||||
|
||||
if (!videoUrl) {
|
||||
@ -360,8 +398,8 @@ export default function WaveformTimeline() {
|
||||
<canvas ref={waveCanvasRef} className="absolute inset-0 w-full h-full" />
|
||||
<canvas
|
||||
ref={headCanvasRef}
|
||||
className="absolute inset-0 w-full h-full cursor-crosshair"
|
||||
onClick={handleClick}
|
||||
className={`absolute inset-0 w-full h-full ${isDragging ? 'cursor-grabbing' : 'cursor-crosshair'}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
onWheel={handleWheel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user