import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import "./index.scss"; function formatAudioTime(seconds: number): string { const safe = Number.isFinite(seconds) && seconds > 0 ? seconds : 0; const total = Math.floor(safe); const mins = Math.floor(total / 60); const secs = total % 60; return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; } export type AssetAudioPlayerHandle = { togglePlay: () => void; }; export interface AssetAudioPlayerProps { src: string; resetKey?: string | number; autoPlay?: boolean; autoAdvanceOnEnded?: boolean; className?: string; onPlayingChange?: (playing: boolean) => void; canGoPrevious?: boolean; canGoNext?: boolean; onPrevious?: () => void; onNext?: () => void; } export const AssetAudioPlayer = forwardRef(function AssetAudioPlayer( { src, resetKey, autoPlay = false, autoAdvanceOnEnded = false, className, onPlayingChange, canGoPrevious = false, canGoNext = false, onPrevious, onNext }, forwardedRef, ) { const { t } = useTranslation(); const audioRef = useRef(null); const progressTrackRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const voiceSrc = String(src ?? "").trim(); useEffect(() => { setIsPlaying(false); setCurrentTime(0); setDuration(0); const audio = audioRef.current; if (audio) { audio.pause(); audio.currentTime = 0; } }, [voiceSrc, resetKey]); useEffect(() => { if (!autoPlay || !voiceSrc) return; const audio = audioRef.current; if (!audio) return; void audio.play().catch(() => { }); }, [autoPlay, voiceSrc, resetKey]); useEffect(() => { const audio = audioRef.current; if (!audio || !voiceSrc) return; const syncDuration = () => { setDuration(Number.isFinite(audio.duration) ? audio.duration : 0); }; const onTimeUpdate = () => setCurrentTime(audio.currentTime); const onEnded = () => { setIsPlaying(false); onPlayingChange?.(false); if (autoAdvanceOnEnded && canGoNext && onNext) { onNext(); } }; const onPlay = () => { setIsPlaying(true); onPlayingChange?.(true); }; const onPause = () => { setIsPlaying(false); onPlayingChange?.(false); }; audio.addEventListener("timeupdate", onTimeUpdate); audio.addEventListener("loadedmetadata", syncDuration); audio.addEventListener("durationchange", syncDuration); audio.addEventListener("ended", onEnded); audio.addEventListener("play", onPlay); audio.addEventListener("pause", onPause); return () => { audio.removeEventListener("timeupdate", onTimeUpdate); audio.removeEventListener("loadedmetadata", syncDuration); audio.removeEventListener("durationchange", syncDuration); audio.removeEventListener("ended", onEnded); audio.removeEventListener("play", onPlay); audio.removeEventListener("pause", onPause); audio.pause(); }; }, [autoAdvanceOnEnded, canGoNext, onNext, onPlayingChange, voiceSrc]); const togglePlay = useCallback(() => { const audio = audioRef.current; if (!audio) return; if (audio.paused) { void audio.play().catch(() => { }); return; } audio.pause(); }, []); useImperativeHandle(forwardedRef, () => ({ togglePlay }), [togglePlay]); const seekByClientX = useCallback((clientX: number) => { const audio = audioRef.current; const track = progressTrackRef.current; if (!audio || !track || !Number.isFinite(audio.duration) || audio.duration <= 0) return; const rect = track.getBoundingClientRect(); if (rect.width <= 0) return; const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width)); audio.currentTime = ratio * audio.duration; }, []); const seekWithinTrack = useCallback((delta: number) => { const audio = audioRef.current; if (!audio) return; const max = Number.isFinite(audio.duration) ? audio.duration : 0; const next = audio.currentTime + delta; audio.currentTime = max > 0 ? Math.min(Math.max(0, next), max) : Math.max(0, next); }, []); const handlePreviousTrack = useCallback(() => { if (!canGoPrevious || !onPrevious) return; onPrevious(); }, [canGoPrevious, onPrevious]); const handleNextTrack = useCallback(() => { if (!canGoNext || !onNext) return; onNext(); }, [canGoNext, onNext]); if (!voiceSrc) return null; const progressPercent = duration > 0 ? Math.min(100, (currentTime / duration) * 100) : 0; return (