/** * AudioClip - Компонент аудио клипа */ import { Copy, Music, Scissors, Sparkles, Trash2, Volume2 } from "lucide-react" import React from "react" import { Button } from "@/components/ui/button" import { useModal } from "@/features/modals/services" import { convertToAssetUrl } from "@/lib/tauri-utils" import { cn } from "@/lib/utils" import { useClips } from "../../hooks" import { timelinePlayerSync } from "../../services/timeline-player-sync" import { AppliedEffect, TimelineClip, TimelineTrack } from "../../types" import Waveform from "../track/waveform" interface AudioClipProps { clip: TimelineClip track: TimelineTrack onUpdate?: (updates: Partial) => void onRemove?: () => void } /** * Renders an audio clip component with waveform visualization and control buttons. * * @param {AudioClipProps["clip"]} clip - The audio clip data, including id, name, duration, volume, and state. * @param {AudioClipProps["track"]} track - The track data used for styling based on type. * @param {(update: Partial) => void} [onUpdate] - Callback invoked when the clip is updated. * @param {() => void} [onRemove] - Callback invoked when the clip is removed. * @returns {JSX.Element} The AudioClip component. */ export function AudioClip({ clip, track, onUpdate, onRemove }: AudioClipProps) { const [isHovered, setIsHovered] = React.useState(false) const { updateClip } = useClips() const { openModal } = useModal() const handleSelect = () => { const newIsSelected = !clip.isSelected onUpdate?.({ isSelected: newIsSelected }) // Синхронизируем с плеером при выборе if (newIsSelected) { void timelinePlayerSync.syncSelectedClip(clip) } } const handleCopy = (e: React.MouseEvent) => { e.stopPropagation() console.log("Copy audio clip:", clip.id) } const handleSplit = (e: React.MouseEvent) => { e.stopPropagation() console.log("Split audio clip:", clip.id) } const handleRemove = (e: React.MouseEvent) => { e.stopPropagation() onRemove?.() } const handleEffects = (e: React.MouseEvent) => { e.stopPropagation() openModal("audio-effects", { clip, track, onApplyEffects: handleApplyEffects, }) } const handleApplyEffects = (effects: AppliedEffect[]) => { updateClip(clip.id, { effects: effects, }) } // Определяем цвет клипа в зависимости от типа аудио const getClipColor = () => { switch (track.type) { case "music": return "bg-pink-500" case "voiceover": return "bg-cyan-500" case "sfx": return "bg-red-500" case "ambient": return "bg-gray-500" default: return "bg-green-500" } } const getClipColorHover = () => { switch (track.type) { case "music": return "bg-pink-600" case "voiceover": return "bg-cyan-600" case "sfx": return "bg-red-600" case "ambient": return "bg-gray-600" default: return "bg-green-600" } } const clipColor = getClipColor() const clipColorHover = getClipColorHover() // Получаем URL аудио файла через Tauri API const audioUrl = React.useMemo(() => { if (!clip.mediaFile?.path) return null // Конвертируем локальный путь в asset URL для Tauri return convertToAssetUrl(clip.mediaFile.path) }, [clip.mediaFile?.path]) return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onClick={handleSelect} > {/* Заголовок клипа */}
{clip.name}
{/* Кнопки управления */} {isHovered && !clip.isLocked && (
)}
{/* Содержимое клипа - визуализация аудио */}
{/* Waveform компонент */} {audioUrl ? ( ) : ( // Fallback waveform visualization
{Array.from({ length: 20 }, (_, i) => (
))}
)} {/* Индикатор громкости */}
{Math.round(clip.volume * 100)}%
{/* Индикаторы эффектов */} {clip.effects.length > 0 && (
)} {/* Индикаторы фильтров */} {clip.filters.length > 0 && (
)}
{/* Информация о длительности */}
{Math.round(clip.duration)}s
{/* Ручки для изменения размера */} {isHovered && !clip.isLocked && ( <>
)}
) }