/** * ClipEffectsPanel - Панель управления эффектами клипа * Позволяет просматривать, добавлять и настраивать эффекты для выбранного клипа */ import { GripVertical, Plus, Trash2 } from "lucide-react" import { useCallback, useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { EffectManagerPanel } from "@/features/effects/components/effect-manager-panel" import { EffectParameterControls } from "@/features/effects/components/effect-parameter-controls" import { useEffects } from "@/features/effects/hooks/use-effects" import { BaseEffect } from "@/features/effects/types" import { useTimeline } from "../hooks/use-timeline" import { AppliedEffect, TimelineClip } from "../types" interface ClipEffectsPanelProps { clip: TimelineClip | null onClose?: () => void } /** * Компонент панели эффектов для клипа */ export function ClipEffectsPanel({ clip, onClose }: ClipEffectsPanelProps) { const { t, i18n } = useTranslation() const { effects: availableEffects } = useEffects() const { send } = useTimeline() const [showEffectSelector, setShowEffectSelector] = useState(false) const [selectedEffectId, setSelectedEffectId] = useState(null) const [expandedEffects, setExpandedEffects] = useState>(new Set()) // Получаем эффекты клипа с полной информацией const clipEffects = useMemo(() => { if (!clip) return [] return clip.effects .map((appliedEffect) => { const baseEffect = availableEffects.find((e) => e.id === appliedEffect.effectId) return { applied: appliedEffect, base: baseEffect, } }) .filter((e) => e.base) // Фильтруем эффекты, которые не найдены в библиотеке }, [clip, availableEffects]) // Выбранный эффект для настройки параметров const selectedEffect = useMemo(() => { if (!selectedEffectId) return null return clipEffects.find((e) => e.applied.id === selectedEffectId) }, [selectedEffectId, clipEffects]) // Обработчик добавления эффекта const handleAddEffect = useCallback( (effect: BaseEffect, _preset?: string, customParams?: Record) => { if (!clip) return const appliedEffect: AppliedEffect = { id: `applied_${effect.id}_${Date.now()}`, effectId: effect.id, customParams: customParams || {}, enabled: true, order: clip.effects.length, } send({ type: "ADD_EFFECT_TO_CLIP", clipId: clip.id, effect: appliedEffect, }) setShowEffectSelector(false) }, [clip, send], ) // Обработчик удаления эффекта const handleRemoveEffect = useCallback( (appliedEffectId: string) => { if (!clip) return send({ type: "REMOVE_EFFECT_FROM_CLIP", clipId: clip.id, effectId: appliedEffectId, }) if (selectedEffectId === appliedEffectId) { setSelectedEffectId(null) } }, [clip, send, selectedEffectId], ) // Обработчик переключения состояния эффекта const handleToggleEffect = useCallback( (appliedEffectId: string, enabled: boolean) => { if (!clip) return send({ type: "UPDATE_CLIP_EFFECT", clipId: clip.id, effectId: appliedEffectId, updates: { isEnabled: enabled }, }) }, [clip, send], ) // Обработчик изменения параметров эффекта const handleParametersChange = useCallback( (appliedEffectId: string, params: Record) => { if (!clip) return send({ type: "UPDATE_CLIP_EFFECT", clipId: clip.id, effectId: appliedEffectId, updates: { customParams: params }, }) }, [clip, send], ) // Обработчик изменения порядка эффектов const handleReorderEffects = useCallback( (fromIndex: number, toIndex: number) => { if (!clip) return send({ type: "REORDER_CLIP_EFFECTS", clipId: clip.id, fromIndex, toIndex, }) }, [clip, send], ) // Разворачивание/сворачивание эффекта const toggleEffectExpanded = useCallback((effectId: string) => { setExpandedEffects((prev) => { const next = new Set(prev) if (next.has(effectId)) { next.delete(effectId) } else { next.add(effectId) } return next }) }, []) // Автоматически выбираем первый эффект при смене клипа useEffect(() => { if (clipEffects.length > 0 && !selectedEffectId) { setSelectedEffectId(clipEffects[0].applied.id) } }, [clipEffects, selectedEffectId]) if (!clip) { return ( {t("timeline.effects.title", "Эффекты клипа")}

{t("timeline.effects.noClipSelected", "Выберите клип для управления эффектами")}

) } return (
{t("timeline.effects.title", "Эффекты клипа")}
{t("timeline.effects.selectEffect", "Выберите эффект")} {t("timeline.effects.selectEffectDescription", "Выберите эффект для добавления к клипу")}
{onClose && ( )}

{clip.name}

{/* Список эффектов */}
{clipEffects.length === 0 ? (

{t("timeline.effects.noEffects", "Нет эффектов")}

) : ( clipEffects.map((effect, _index) => (
setSelectedEffectId(effect.applied.id)} > handleToggleEffect(effect.applied.id, checked)} onClick={(e) => e.stopPropagation()} />

{effect.base?.name[i18n.language] || effect.base?.name.en}

{effect.base?.category &&

{effect.base.category}

}
)) )}
{/* Параметры выбранного эффекта */}
{selectedEffect ? ( <>

{selectedEffect.base?.name[i18n.language] || selectedEffect.base?.name.en}

{selectedEffect.base?.description && (

{selectedEffect.base.description[i18n.language] || selectedEffect.base.description.en}

)} {selectedEffect.base && ( handleParametersChange(selectedEffect.applied.id, params)} /> )} ) : (

{t("timeline.effects.selectEffectToEdit", "Выберите эффект для редактирования")}

)}
) }