import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { ApplyButton } from "@/features" import { AddMediaButton } from "@/features/browser/components/layout/add-media-button" import { FavoriteButton } from "@/features/browser/components/layout/favorite-button" import { BaseEffect, VideoEffect } from "@/features/effects/types" import { useResources } from "@/features/resources" import { EffectResource, TimelineResource } from "@/features/resources/types" import { usePlayer, useVideoSelection } from "@/features/video-player" import { generateCSSFilterForEffect, getPlaybackRate } from "../utils/css-effects" import { getEffectPreview } from "../utils/effect-previews" import { EffectIndicators } from "./effect-indicators" // Получаем путь к превью видео для конкретного эффекта const getPreviewPath = (effect: BaseEffect) => { // Используем превью из эффекта или дефолтное if (effect.preview) { return effect.preview } // Fallback на старую систему const preview = getEffectPreview(effect.id) return preview.videoPath } /** * Интерфейс пропсов для компонента EffectPreview */ interface EffectPreviewProps { effect: BaseEffect onClick: () => void size: number width?: number // Ширина превью (опционально, по умолчанию равна size) height?: number // Высота превью (опционально, по умолчанию равна size) customParams?: Record // Пользовательские параметры для эффекта } /** * Компонент для отображения превью видеоэффекта * Показывает видео с применённым эффектом и позволяет добавить эффект в проект */ export function EffectPreview({ effect, onClick, size, width = size, // По умолчанию ширина равна size (квадратное превью) height = size, // По умолчанию высота равна size (квадратное превью) customParams, // Пользовательские параметры для эффекта }: EffectPreviewProps) { const { i18n } = useTranslation() // Хук для интернационализации const { isEffectAdded } = useResources() // Получаем методы для работы с ресурсами const [isHovering, setIsHovering] = useState(false) // Состояние наведения мыши const [videoSrc, setVideoSrc] = useState(null) // Путь к видео (для ленивой загрузки) const videoRef = useRef(null) // Ссылка на элемент видео const timeoutRef = useRef(null) // Ссылка на таймер для воспроизведения видео const { applyEffect } = usePlayer() // Получаем метод для применения эффекта const { getCurrentVideo } = useVideoSelection() // Получаем текущее видео для применения эффекта // Создаем эффект с пользовательскими параметрами, если они переданы const processedEffect = useMemo(() => { if (!effect) return null if (customParams && Object.keys(customParams).length > 0) { // Создаем объект с параметрами на основе структуры эффекта const params: Record = {} // Собираем текущие значения параметров if (effect.parameters) { effect.parameters.forEach((param) => { params[param.id] = param.currentValue ?? param.defaultValue }) } return { ...effect, parameters: effect.parameters?.map((param) => ({ ...param, currentValue: customParams[param.id] ?? param.currentValue ?? param.defaultValue, })) || [], // Для обратной совместимости создаем params объект params: { ...params, ...customParams, }, } } return effect }, [effect, customParams]) // Проверяем, добавлен ли эффект уже в хранилище ресурсов // Мемоизируем результат для оптимизации const isAdded = useMemo(() => { return processedEffect ? isEffectAdded(processedEffect) : false }, [processedEffect, isEffectAdded]) // Обработчик применения эффекта const handleApplyEffect = useCallback( (_resource: TimelineResource, _type: string) => { if (!processedEffect) return // Получаем имя эффекта для текущего языка const effectName = typeof processedEffect.name === "object" ? processedEffect.name[i18n.language] || processedEffect.name.en || processedEffect.id : processedEffect.name || processedEffect.id console.log("[EffectPreview] Applying effect:", effectName) // Собираем параметры из новой структуры const params: Record = {} if (processedEffect.parameters) { processedEffect.parameters.forEach((param) => { params[param.id] = param.currentValue ?? param.defaultValue }) } applyEffect({ id: processedEffect.id, name: effectName, params: { ...params, ...((processedEffect as any).params || {}), // Fallback для старой структуры }, }) }, [processedEffect, applyEffect, i18n.language], ) // Ленивая загрузка видео при наведении useEffect(() => { if (isHovering && !videoSrc) { setVideoSrc(getPreviewPath(effect)) } }, [isHovering, effect, videoSrc]) /** * Эффект для управления воспроизведением видео и применением эффектов * Запускает видео при наведении и применяет соответствующий эффект */ useEffect(() => { if (!effect) return if (!videoSrc || !videoRef.current) return const videoElement = videoRef.current /** * Применяет эффект к видео и запускает его воспроизведение * Устанавливает таймер для повторного воспроизведения */ const applyEffect = () => { videoElement.currentTime = 0 // Сбрасываем время видео на начало videoElement.style.filter = "" // Сбрасываем предыдущие фильтры videoElement.style.boxShadow = "" // Сбрасываем дополнительные эффекты videoElement.playbackRate = 1 // Сбрасываем скорость воспроизведения // Применяем CSS-фильтр на основе параметров эффекта // Собираем параметры для CSS фильтра const effectParams: Record = {} if (processedEffect?.parameters) { processedEffect.parameters.forEach((param) => { effectParams[param.id] = param.currentValue ?? param.defaultValue }) } // Добавляем пользовательские параметры Object.assign(effectParams, customParams) const cssFilter = generateCSSFilterForEffect(processedEffect || effect, effectParams) if (cssFilter) { videoElement.style.filter = cssFilter } // Специальные эффекты, требующие дополнительных CSS-стилей if ( processedEffect?.id === "vignette" || (processedEffect?.category === "lighting" && processedEffect?.id.includes("vignette")) ) { // Создаем эффект виньетки через box-shadow const intensity = customParams?.intensity ?? effectParams?.intensity ?? 0.3 const radius = customParams?.radius ?? effectParams?.radius ?? 0.8 const shadowSize = Math.round(Math.min(width, height) * (1 - radius) * 0.5) const shadowBlur = Math.round(shadowSize * intensity * 2) videoElement.style.boxShadow = `inset 0 0 ${shadowBlur}px ${shadowSize}px rgba(0,0,0,${intensity})` } else { videoElement.style.boxShadow = "" } // Устанавливаем скорость воспроизведения const playbackRate = getPlaybackRate(processedEffect) videoElement.playbackRate = playbackRate // Запускаем воспроизведение видео videoElement.play().catch((err: unknown) => { console.log("Autoplay prevented:", err) }) // Устанавливаем таймер для повторного воспроизведения через 2 секунды timeoutRef.current = setTimeout(() => { if (isHovering) { applyEffect() } }, 2000) } // Если курсор наведен на превью - применяем эффект и запускаем видео if (isHovering) { applyEffect() } else { // Если курсор не наведен - останавливаем видео и сбрасываем эффекты videoElement.pause() videoElement.currentTime = 0 videoElement.style.filter = "" videoElement.style.boxShadow = "" videoElement.playbackRate = 1 if (timeoutRef.current) clearTimeout(timeoutRef.current) } // Очищаем таймер при размонтировании компонента return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current) } }, [isHovering, processedEffect, width, height, customParams, videoSrc]) return (
{/* Контейнер превью эффекта */}
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} onClick={onClick} > {/* Видео для демонстрации эффекта */} {videoSrc && (
) }