import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ipcChannels } from '../../shared/ipc/contracts'; import { clampSceneAudioVolume, type AssetId, type MediaAsset, type SceneAudioRef, } from '../../shared/types'; import { getDndApi } from '../shared/dndApi'; import { useEditorI18n } from './i18n/EditorI18nContext'; import { registerEditorAudioStopper } from '../shared/audio/editorAudioStop'; import styles from './EditorApp.module.css'; const AUDIO_REF_DND = 'application/x-dnd-scene-audio-ref'; function reorderRefs(refs: SceneAudioRef[], draggedId: AssetId, targetId: AssetId): SceneAudioRef[] { if (draggedId === targetId) return refs; const from = refs.findIndex((r) => r.assetId === draggedId); const to = refs.findIndex((r) => r.assetId === targetId); if (from < 0 || to < 0) return refs; const next = [...refs]; const [item] = next.splice(from, 1); if (!item) return refs; next.splice(to, 0, item); return next; } /** Локальный draft — не пишет в проект на каждый tick ползунка (иначе мигает граф). */ function AudioVolumeSlider({ assetId, volume, isPlaying, previewElRef, label, onCommit, }: { assetId: AssetId; volume: number; isPlaying: boolean; previewElRef: React.MutableRefObject; label: string; onCommit: (volume: number) => void; }) { const [draft, setDraft] = useState(() => clampSceneAudioVolume(volume)); const draftRef = useRef(draft); draftRef.current = draft; useEffect(() => { setDraft(clampSceneAudioVolume(volume)); }, [assetId, volume]); const applyLocal = (v: number) => { const next = clampSceneAudioVolume(v); setDraft(next); if (isPlaying && previewElRef.current) previewElRef.current.volume = next; }; const commit = () => { const next = clampSceneAudioVolume(draftRef.current); if (next === clampSceneAudioVolume(volume)) return; onCommit(next); }; return ( ); } export type EditorAudioTrackListProps = { audioRefs: SceneAudioRef[]; mediaAssets: MediaAsset[]; removeTitle: string; onAudioRefsChange: (next: SceneAudioRef[]) => void; }; export function EditorAudioTrackList({ audioRefs, mediaAssets, removeTitle, onAudioRefsChange, }: EditorAudioTrackListProps) { const { t } = useEditorI18n(); const assetById = useMemo(() => new Map(mediaAssets.map((a) => [a.id, a])), [mediaAssets]); const [playingId, setPlayingId] = useState(null); const previewElRef = useRef(null); useEffect(() => { return () => { const el = previewElRef.current; if (el) { el.pause(); el.src = ''; previewElRef.current = null; } }; }, []); useEffect(() => { return registerEditorAudioStopper(() => { const el = previewElRef.current; if (el) { el.pause(); el.onended = null; el.src = ''; } previewElRef.current = null; setPlayingId(null); }); }, []); const stopPreview = () => { const el = previewElRef.current; if (el) { el.pause(); el.onended = null; el.src = ''; } previewElRef.current = null; setPlayingId(null); }; const togglePreview = async (assetId: AssetId, volume: number) => { if (playingId === assetId) { stopPreview(); return; } stopPreview(); try { const r = await getDndApi().invoke(ipcChannels.project.assetFileUrl, { assetId }); const url = r.url; if (!url) return; const el = new Audio(url); el.volume = clampSceneAudioVolume(volume); el.onended = () => { previewElRef.current = null; setPlayingId(null); }; previewElRef.current = el; setPlayingId(assetId); await el.play(); } catch { stopPreview(); } }; if (audioRefs.length === 0) { return (
{t('campaign.noFiles')}
); } return (
{audioRefs.map((ref) => { const asset = assetById.get(ref.assetId); const name = asset?.originalName ?? ref.assetId; const volume = clampSceneAudioVolume(ref.volume); const isPlaying = playingId === ref.assetId; return (
{ if (e.dataTransfer.types.includes(AUDIO_REF_DND)) e.preventDefault(); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); const dragged = e.dataTransfer.getData(AUDIO_REF_DND) as AssetId; if (!dragged) return; const next = reorderRefs(audioRefs, dragged, ref.assetId); if (next !== audioRefs) onAudioRefsChange(next); }} >
{ e.dataTransfer.setData(AUDIO_REF_DND, ref.assetId); e.dataTransfer.effectAllowed = 'move'; }} > ⋮⋮ {name}
{ const next = audioRefs.map((x) => x.assetId === ref.assetId ? { ...x, volume: v } : x, ); onAudioRefsChange(next); }} />
); })}
); }