/** * Clip - Основной компонент клипа на Timeline */ import { memo, useCallback, useMemo, useState } from "react" import { Dialog, DialogContent } from "@/components/ui/dialog" import { cn } from "@/lib/utils" import { useClipEditing } from "../../hooks/use-clip-editing" import { useClipGroups } from "../../hooks/use-clip-groups" import { useEditModeContext } from "../../hooks/use-edit-mode" import { useJLCuts } from "../../hooks/use-jl-cuts" import { useSpeedRamping } from "../../hooks/use-speed-ramping" import { useTimelinePersons } from "../../hooks/use-timeline-persons" import { isSubtitleClip, TimelineClip, TimelineTrack } from "../../types" import { EDIT_MODES } from "../../types/edit-modes" import { getCutType } from "../../types/jl-cuts" import { ClipAIIndicator } from "../ai-analysis/clip-ai-indicator" import { ClipContextMenu } from "../clip-context-menu" import { ClipEffectsPanel } from "../clip-effects-panel" import { GroupIndicator } from "../clip-groups/group-indicator" import { RateStretchHandle } from "../edit-tools/rate-stretch-handle" import { SlipSlideHandles } from "../edit-tools/slip-slide-handles" import { JLCutDragHandle, JLCutIndicator, JLCutTool, LinkedClipIndicator } from "../jl-cuts" import { PersonIndicator } from "../person-indicators" import { SpeedCurveEditor } from "../speed-ramping/speed-curve-editor" import { AudioClip } from "./audio-clip" import { ClipTrimHandles } from "./clip-trim-handles" import { SubtitleClip } from "./subtitle-clip" import { VideoClip } from "./video-clip" interface ClipProps { clip: TimelineClip track: TimelineTrack timeScale: number // Пикселей на секунду onUpdate?: (updates: Partial) => void onRemove?: () => void className?: string } export const Clip = memo(function Clip({ clip, track, timeScale, onUpdate, onRemove, className }: ClipProps) { const { editMode } = useEditModeContext() const [isHovered, setIsHovered] = useState(false) const [showSpeedCurve, setShowSpeedCurve] = useState(false) const [showEffectsPanel, setShowEffectsPanel] = useState(false) const { getGroupByClip, toggleCollapse, lockGroup } = useClipGroups() const { getLinkedClip } = useJLCuts() const { getConfig } = useSpeedRamping() const { getPersonsForClip, getAppearancesForClip, showPersonDetail } = useTimelinePersons() const { isEditing, preview, handleTrimStart, handleTrimMove, handleTrimEnd } = useClipEditing(clip.id) // Получаем группу, если клип в ней находится const group = getGroupByClip(clip.id) // Получаем связанный клип для J/L cut const linkedClip = getLinkedClip(clip.id) // Получаем конфигурацию speed ramping const speedRampingConfig = getConfig(clip.id) // Получаем данные о персонах для клипа const clipPersons = getPersonsForClip(clip.id) const clipAppearances = getAppearancesForClip(clip.id) // Мемоизируем вычисления позиции и размеров const { left, width } = useMemo( () => ({ left: (preview?.startTime ?? clip.startTime) * timeScale, width: Math.max((preview?.duration ?? clip.duration) * timeScale, 20), // Минимальная ширина 20px }), [clip.startTime, clip.duration, timeScale, preview], ) // Выбираем специализированный компонент в зависимости от типа трека const renderClipContent = () => { switch (track.type) { case "video": case "image": return case "audio": case "music": case "voiceover": case "sfx": case "ambient": return case "subtitle": case "title": if (isSubtitleClip(clip)) { return } return (
Invalid subtitle clip
) default: return (
{track.type}
) } } // Handle slip/slide start const handleSlipSlideStart = useCallback( (mouseX: number) => { if (editMode === EDIT_MODES.SLIP || editMode === EDIT_MODES.SLIDE) { handleTrimStart(editMode === EDIT_MODES.SLIP ? "start" : "end", mouseX) } }, [editMode, handleTrimStart], ) return ( <> setShowEffectsPanel(true)} onShowTransitions={() => { /* TODO: Implement transitions panel */ }} onShowFilters={() => { /* TODO: Implement filters panel */ }} >
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {renderClipContent()} {/* Linked clip indicator */} {/* J/L Cut indicator */} {linkedClip && clip.audioOffset !== undefined && clip.audioOffset !== 0 && ( <> {/* J/L Cut drag handle */} )} {/* J/L Cut tool */} {isHovered && linkedClip && (
)} {/* Group indicator */} {group && !group.collapsed && (
toggleCollapse(group.id)} onToggleLock={() => lockGroup(group.id, !group.locked)} className="scale-75 origin-top-left" />
)} {/* AI Analysis indicator */} {/* Person indicators для видео клипов */} {(track.type === "video" || track.type === "image") && clipPersons.length > 0 && (
)} {/* Trim handles for regular trim/ripple modes */} {(editMode === EDIT_MODES.TRIM || editMode === EDIT_MODES.RIPPLE) && ( )} {/* Slip/Slide handles */} {/* Rate Stretch handles */} {/* Speed ramping indicator */} {speedRampingConfig && speedRampingConfig.enabled && (
)} {/* Speed curve editor */} {showSpeedCurve && speedRampingConfig && speedRampingConfig.enabled && (
setShowSpeedCurve(false)} className="shadow-lg" />
)}
{/* Диалог панели эффектов */} setShowEffectsPanel(false)} /> ) })