/** * VideoClip - Компонент видео клипа */ import { Copy, Image, Scissors, Trash2, Video } from "lucide-react" import React, { memo, useCallback, useMemo } from "react" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { useTimeline } from "../../hooks/use-timeline" import { timelinePlayerSync } from "../../services/timeline-player-sync" import { TimelineClip, TimelineTrack } from "../../types" interface VideoClipProps { clip: TimelineClip track: TimelineTrack onUpdate?: (updates: Partial) => void onRemove?: () => void } // Мемоизируем компонент для предотвращения ненужных ререндеров export const VideoClip = memo( function VideoClip({ clip, track, onUpdate, onRemove }: VideoClipProps) { const [isHovered, setIsHovered] = React.useState(false) const timeline = useTimeline() // Мемоизируем обработчики для предотвращения создания новых функций при каждом рендере const handleSelect = useCallback(() => { const newIsSelected = !clip.isSelected onUpdate?.({ isSelected: newIsSelected }) // Синхронизируем с плеером при выборе if (newIsSelected) { void timelinePlayerSync.syncSelectedClip(clip) } }, [clip, onUpdate]) const handleCopy = useCallback( (e: React.MouseEvent) => { e.stopPropagation() // Выделяем клип если он не выделен if (!clip.isSelected && timeline) { timeline.selectClips([clip.id], false) } // Копируем выделенные клипы if (timeline) { timeline.copySelection() } }, [clip.id, clip.isSelected, timeline], ) const handleSplit = useCallback( (e: React.MouseEvent) => { e.stopPropagation() // Разделяем клип в середине const splitTime = clip.startTime + clip.duration / 2 if (timeline) { void timeline.splitClip(clip.id, splitTime) } }, [clip.id, clip.startTime, clip.duration, timeline], ) const handleRemove = useCallback( (e: React.MouseEvent) => { e.stopPropagation() onRemove?.() }, [onRemove], ) // Мемоизируем вычисляемые значения const clipColor = useMemo(() => (track.type === "video" ? "bg-blue-500" : "bg-purple-500"), [track.type]) const clipColorHover = useMemo(() => (track.type === "video" ? "bg-blue-600" : "bg-purple-600"), [track.type]) // Мемоизируем прогресс бар вычисления const progressBarStyle = useMemo( () => ({ marginLeft: `${(clip.mediaStartTime / (clip.mediaEndTime - clip.mediaStartTime + clip.duration)) * 100}%`, width: `${(clip.duration / (clip.mediaEndTime - clip.mediaStartTime + clip.duration)) * 100}%`, }), [clip.mediaStartTime, clip.mediaEndTime, clip.duration], ) return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onClick={handleSelect} > {/* Заголовок клипа */}
{track.type === "video" ? (
{/* Кнопки управления (показываются при наведении) */} {isHovered && !clip.isLocked && (
)}
{/* Содержимое клипа */}
{/* Превью видео/изображения */}
{/* TODO: Здесь будет превью кадра из видео */}
{Math.round(clip.duration)}s
{/* Индикаторы эффектов */} {clip.effects.length > 0 && (
)} {/* Индикаторы фильтров */} {clip.filters.length > 0 && (
)} {/* Индикаторы переходов */} {clip.transitions.length > 0 && (
)}
{/* Полоса прогресса (показывает обрезку) */}
{/* Ручки для изменения размера */} {isHovered && !clip.isLocked && ( <>
)}
) }, (prevProps, nextProps) => { // Кастомная функция сравнения для оптимизации ререндеров // Проверяем только те свойства, которые действительно влияют на отображение return ( prevProps.clip.id === nextProps.clip.id && prevProps.clip.name === nextProps.clip.name && prevProps.clip.startTime === nextProps.clip.startTime && prevProps.clip.duration === nextProps.clip.duration && prevProps.clip.isSelected === nextProps.clip.isSelected && prevProps.clip.isLocked === nextProps.clip.isLocked && prevProps.clip.effects.length === nextProps.clip.effects.length && prevProps.clip.filters.length === nextProps.clip.filters.length && prevProps.clip.transitions.length === nextProps.clip.transitions.length && prevProps.clip.mediaStartTime === nextProps.clip.mediaStartTime && prevProps.clip.mediaEndTime === nextProps.clip.mediaEndTime && prevProps.track.type === nextProps.track.type && prevProps.track.height === nextProps.track.height ) }, )