import { readFile } from "@tauri-apps/plugin-fs" import { Music } from "lucide-react" import { memo, useCallback, useEffect, useRef, useState } from "react" import { LiveAudioVisualizer } from "react-audio-visualize" import { MediaFile } from "@/features/media/types/media" import { TimelineResource } from "@/features/resources/types" import { usePlayer } from "@/features/video-player" import { convertToAssetUrl } from "@/lib/tauri-utils" import { AddMediaButton } from "../layout/add-media-button" import { FavoriteButton } from "../layout/favorite-button" interface AudioPreviewProps { file: MediaFile size?: number showFileName?: boolean dimensions?: [number, number] } /** * Предварительный просмотр аудиофайла * * Функционал: * - Отображает превью аудиофайла с поддержкой ленивой загрузки * - Адаптивный размер контейнера с соотношением сторон 16:9 * - Поддерживает два размера UI (стандартный и большой при size > 100) * - Опциональное отображение имени файла * - Кнопка добавления с состояниями (добавлено/не добавлено) * - Темная тема для UI элементов * * @param file - Объект файла с путем и метаданными * @param size - Размер превью в пикселях (по умолчанию 60) * @param showFileName - Флаг для отображения имени файла * @param dimensions - Соотношение сторон контейнера [ширина, высота], по умолчанию [16, 9] */ export const AudioPreview = memo(function AudioPreview({ file, size = 60, showFileName = false, dimensions = [16, 9], }: AudioPreviewProps) { const [isPlaying, setIsPlaying] = useState(false) const [hoverTime, setHoverTime] = useState(null) const [isLoaded, setIsLoaded] = useState(false) const audioRef = useRef(null) const audioContextRef = useRef(null) const sourceRef = useRef(null) const [mediaRecorder, setMediaRecorder] = useState(null) const { playerSetSource, playerSetMedia } = usePlayer() const handleMouseMove = useCallback( (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect() const x = e.clientX - rect.left const percentage = x / rect.width const newTime = percentage * (file.duration ?? 0) setHoverTime(newTime) if (audioRef.current) { audioRef.current.currentTime = newTime } }, [file.duration], ) const handlePlayPause = useCallback( async (e: React.MouseEvent) => { e.preventDefault() try { // Отправляем аудио в главный плеер через backend await playerSetSource("browser") await playerSetMedia(file.id, hoverTime || 0) console.log(`[AudioPreview] Audio sent to main player: ${file.name} at time ${hoverTime || 0}`) } catch (error) { console.error("[AudioPreview] Failed to send audio to main player:", error) // Fallback: локальное воспроизведение в превью if (!audioRef.current) return if (isPlaying) { audioRef.current.pause() } else { if (hoverTime !== null) { audioRef.current.currentTime = hoverTime } void audioRef.current.play() } setIsPlaying(!isPlaying) console.log(`[AudioPreview] Fallback: Audio ${!isPlaying ? "playing" : "paused"} in preview`) } }, [hoverTime, file, playerSetSource, playerSetMedia, isPlaying], ) const handleMouseLeave = useCallback(() => { setHoverTime(null) if (audioRef.current && isPlaying) { audioRef.current.pause() setIsPlaying(false) } }, [isPlaying]) // Состояние для хранения объекта URL const [audioUrl, setAudioUrl] = useState("") // Функция для чтения файла и создания объекта URL const loadAudioFile = useCallback(async (path: string) => { try { console.log("[AudioPreview] Чтение файла через readFile:", path) const fileData = await readFile(path) const blob = new Blob([fileData], { type: "audio/mp3" }) // Можно определить тип по расширению файла const url = URL.createObjectURL(blob) console.log("[AudioPreview] Создан объект URL:", url) return url } catch (error) { console.error("[AudioPreview] Ошибка при загрузке аудио:", error) // В случае ошибки используем convertToAssetUrl const assetUrl = convertToAssetUrl(path) console.log("[AudioPreview] Используем asset URL:", assetUrl) return assetUrl } }, []) // Эффект для загрузки аудио при монтировании компонента useEffect(() => { let isMounted = true void loadAudioFile(file.path).then((url) => { if (isMounted) { setAudioUrl(url) } }) // Очистка объекта URL при размонтировании компонента return () => { isMounted = false if (audioUrl?.startsWith("blob:")) { URL.revokeObjectURL(audioUrl) } } }, [file.path, loadAudioFile]) // Убираем audioUrl из зависимостей useEffect(() => { const audioElement = audioRef.current if (!audioElement) return const initAudioContext = () => { try { audioContextRef.current ??= new AudioContext() const audioContext = audioContextRef.current sourceRef.current ??= audioContext.createMediaElementSource(audioElement) const destination = audioContext.createMediaStreamDestination() sourceRef.current.connect(destination) sourceRef.current.connect(audioContext.destination) const recorder = new MediaRecorder(destination.stream) setMediaRecorder(recorder) recorder.start() } catch (error) { console.error("Error initializing audio context:", error) } } setTimeout(initAudioContext, 100) return () => { if (mediaRecorder) { mediaRecorder.stop() } if (sourceRef.current) { sourceRef.current.disconnect() } if (audioContextRef.current) { void audioContextRef.current.close() } } }, [mediaRecorder]) return (
) })