import { useEffect, useMemo, useRef, useState } from "react" import { Skeleton } from "@/components/ui/skeleton" import { useSmartTimelinePreviews } from "@/features/video-compiler/hooks/use-frame-extraction" import { frameExtractionService } from "@/features/video-compiler/services/frame-extraction-service" import { cn } from "@/lib/utils" export interface TimelinePreviewStripProps { /** Путь к видео файлу */ videoPath: string | null /** Длительность видео в секундах */ duration: number /** Ширина контейнера */ containerWidth: number /** Масштаб timeline (пиксели на секунду) */ scale: number /** Смещение прокрутки */ scrollOffset?: number /** Высота полосы превью */ height?: number /** Класс для стилизации */ className?: string /** Обработчик клика на кадр */ onFrameClick?: (timestamp: number) => void /** Показывать ли временные метки */ showTimestamps?: boolean } export function TimelinePreviewStrip({ videoPath, duration, containerWidth, scale, scrollOffset = 0, height = 60, className, onFrameClick, showTimestamps = false, }: TimelinePreviewStripProps) { const containerRef = useRef(null) const [visibleRange, setVisibleRange] = useState({ start: 0, end: duration }) const { frames, isLoading, error, progress, frameWidth } = useSmartTimelinePreviews( videoPath, duration, containerWidth, { cacheResults: true, interval: 1.0, }, ) // Вычисляем видимый диапазон на основе прокрутки useEffect(() => { const startTime = scrollOffset / scale const endTime = startTime + containerWidth / scale setVisibleRange({ start: Math.max(0, startTime), end: Math.min(duration, endTime), }) }, [scrollOffset, containerWidth, scale, duration]) // Фильтруем только видимые кадры const visibleFrames = useMemo(() => { return frames.filter((frame) => frame.timestamp >= visibleRange.start && frame.timestamp <= visibleRange.end) }, [frames, visibleRange]) // Обработчик клика на кадр const handleFrameClick = (timestamp: number) => { onFrameClick?.(timestamp) } if (error) { return (
Ошибка загрузки превью
) } if (isLoading && frames.length === 0) { return (
{Array.from({ length: Math.min(10, Math.floor(containerWidth / frameWidth)) }).map((_, i) => ( ))}
) } return (
{/* Прогресс загрузки */} {isLoading && progress < 100 && (
)} {/* Контейнер для кадров */}
{frames.map((frame, index) => ( handleFrameClick(frame.timestamp)} /> ))}
{/* Оверлей для текущей позиции воспроизведения */} {/* TODO: Добавить индикатор текущей позиции */}
) } interface PreviewFrameProps { frame: { timestamp: number frameData: string isKeyframe: boolean } scale: number height: number showTimestamp: boolean isKeyframe: boolean onClick: () => void } function PreviewFrame({ frame, scale, height, showTimestamp, isKeyframe, onClick }: PreviewFrameProps) { const imgRef = useRef(null) const [isLoaded, setIsLoaded] = useState(false) // Позиция кадра на timeline const position = frame.timestamp * scale useEffect(() => { if (imgRef.current) { const img = frameExtractionService.createPreviewElement(frame.frameData, frame.timestamp) img.onload = () => setIsLoaded(true) img.className = "w-full h-full object-cover" // Заменяем содержимое if (imgRef.current) { imgRef.current.src = img.src } } }, [frame.frameData, frame.timestamp]) return (
{/* Изображение */}
{!isLoaded && } {} {`Frame
{/* Временная метка */} {showTimestamp && (
{formatTimestamp(frame.timestamp)}
)} {/* Индикатор ключевого кадра */} {isKeyframe && (
K
)}
) } /** * Форматировать временную метку */ function formatTimestamp(seconds: number): string { const minutes = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) const ms = Math.floor((seconds % 1) * 100) return `${minutes}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(2, "0")}` } /** * Hook для использования превью strip в timeline */ export function useTimelinePreviewStrip(_videoPath: string | null, _duration: number) { const [containerWidth, setContainerWidth] = useState(0) const containerRef = useRef(null) useEffect(() => { if (!containerRef.current) return const observer = new ResizeObserver((entries) => { const entry = entries[0] if (entry) { setContainerWidth(entry.contentRect.width) } }) observer.observe(containerRef.current) return () => observer.disconnect() }, []) return { containerRef, containerWidth, } }