import { useState, useRef, useCallback, useEffect } from 'react'; import type { LineSegment } from '../App.tsx'; import styles from './PlaybackBar.module.css'; import { Button } from '@/components/ui/button.tsx'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select.tsx'; import { cn } from '@/utils.ts'; const SPEEDS = [ { value: 0.125, label: '⅛×' }, { value: 0.25, label: '¼×' }, { value: 0.5, label: '½×' }, { value: 1, label: '1×' }, { value: 2, label: '2×' }, { value: 4, label: '4×' }, ]; interface Props { total: number; scrubPos: number; onScrubChange: (v: number) => void; activeLine: number | null; lineSegments: LineSegment[]; highlightLines: number[]; } export default function PlaybackBar({ total, scrubPos, onScrubChange, activeLine, lineSegments, highlightLines, }: Props) { const [playingForTotal, setPlayingForTotal] = useState(null); const playing = playingForTotal === total; const playReqRef = useRef(null); const [speed, setSpeed] = useState(1); const speedRef = useRef(speed); useEffect(() => { speedRef.current = speed; }, [speed]); const stopPlay = useCallback(() => { setPlayingForTotal(null); if (playReqRef.current !== null) { cancelAnimationFrame(playReqRef.current); playReqRef.current = null; } }, []); const cancelFrame = useCallback(() => { if (playReqRef.current !== null) { cancelAnimationFrame(playReqRef.current); playReqRef.current = null; } }, []); useEffect(() => { return cancelFrame; }, [total, cancelFrame]); const startPlay = useCallback(() => { if (total === 0) return; let pos = scrubPos >= total ? 0 : scrubPos; setPlayingForTotal(total); function step() { const perFrame = Math.max(1, Math.round((total / 420) * speedRef.current)); pos += perFrame; if (pos >= total) { onScrubChange(total); setPlayingForTotal(null); playReqRef.current = null; return; } onScrubChange(pos); playReqRef.current = requestAnimationFrame(step); } playReqRef.current = requestAnimationFrame(step); }, [total, scrubPos, onScrubChange]); const handlePlayClick = useCallback(() => { if (playing) stopPlay(); else startPlay(); }, [playing, stopPlay, startPlay]); const handleScrub = useCallback( (e: React.ChangeEvent) => { stopPlay(); onScrubChange(Number(e.target.value)); }, [stopPlay, onScrubChange], ); const handleSpeedChange = useCallback((value: string | null) => { if (value) setSpeed(Number(value)); }, []); let activeSegIdx = -1; if (scrubPos > 0 && total > 0) { for (let i = lineSegments.length - 1; i >= 0; i--) { if (lineSegments[i].start < scrubPos) { activeSegIdx = i; break; } } } const hi = highlightLines.length ? new Set(highlightLines) : null; const totalStr = total.toLocaleString(); const paddedPos = scrubPos.toLocaleString().padStart(totalStr.length, '\u2007'); const paddedLine = (activeLine ?? 0).toString().padStart(4, '\u2007'); return (
{/* Play / Pause */} {/* Speed selector */} {/* Scrub range + code timeline (unchanged — custom unit) */}
{paddedPos} / {totalStr} stitches {' · line '} {paddedLine}
); }