import React, { useEffect, useMemo, useState } from 'react'; import { Play, Pause } from 'lucide-react'; import { cn } from '../lib/utils'; import { useStore } from '../store/useStore'; export function FrameTimeline() { const frames = useStore((state) => state.frames); const currentProject = useStore((state) => state.currentProject); const currentFrameIndex = useStore((state) => state.currentFrameIndex); const masks = useStore((state) => state.masks); const setCurrentFrame = useStore((state) => state.setCurrentFrame); const [isPlaying, setIsPlaying] = useState(false); const totalFrames = frames.length; const currentFrame = totalFrames > 0 ? currentFrameIndex + 1 : 0; const playbackFps = useMemo(() => { const fps = currentProject?.parse_fps || currentProject?.original_fps || 12; return Math.min(Math.max(fps, 1), 30); }, [currentProject?.original_fps, currentProject?.parse_fps]); const timeBaseFps = useMemo(() => { const fps = currentProject?.parse_fps || currentProject?.original_fps || 12; return Math.max(fps, 1); }, [currentProject?.original_fps, currentProject?.parse_fps]); const currentSeconds = totalFrames > 0 ? currentFrameIndex / timeBaseFps : 0; const totalSeconds = totalFrames > 0 ? Math.max(totalFrames - 1, 0) / timeBaseFps : 0; const editedFrameMarkers = useMemo(() => { const frameIds = new Set(frames.map((frame) => frame.id)); const editedIds = new Set( masks .filter((mask) => frameIds.has(mask.frameId)) .map((mask) => mask.frameId), ); return frames .map((frame, index) => ({ frame, index })) .filter(({ frame }) => editedIds.has(frame.id)); }, [frames, masks]); const formatTime = (seconds: number) => { const safeSeconds = Math.max(0, seconds); const minutes = Math.floor(safeSeconds / 60); const wholeSeconds = Math.floor(safeSeconds % 60); const centiseconds = Math.floor((safeSeconds % 1) * 100); return `${minutes.toString().padStart(2, '0')}:${wholeSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`; }; useEffect(() => { if (!isPlaying || totalFrames <= 1) return; const timer = window.setTimeout(() => { if (currentFrameIndex >= totalFrames - 1) { setIsPlaying(false); return; } setCurrentFrame(currentFrameIndex + 1); }, 1000 / playbackFps); return () => window.clearTimeout(timer); }, [currentFrameIndex, isPlaying, playbackFps, setCurrentFrame, totalFrames]); useEffect(() => { if (totalFrames === 0) { setIsPlaying(false); } }, [totalFrames]); useEffect(() => { const isEditableTarget = (target: EventTarget | null) => { if (!(target instanceof HTMLElement)) return false; const tagName = target.tagName.toLowerCase(); return target.isContentEditable || ['input', 'textarea', 'select'].includes(tagName); }; const handleKeyDown = (event: KeyboardEvent) => { if (isEditableTarget(event.target) || totalFrames <= 1) return; if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; event.preventDefault(); setIsPlaying(false); const direction = event.key === 'ArrowRight' ? 1 : -1; const nextIndex = Math.min(Math.max(currentFrameIndex + direction, 0), totalFrames - 1); if (nextIndex !== currentFrameIndex) { setCurrentFrame(nextIndex); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [currentFrameIndex, setCurrentFrame, totalFrames]); // show frames around current frame const frameWindow = 20; const displayIndices = totalFrames > 0 ? Array.from({ length: 41 }, (_, i) => currentFrameIndex - frameWindow + i) : []; return (