全新popiart网站 react项目重构
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

206 lines
7.3 KiB

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<AssetAudioPlayerHandle, AssetAudioPlayerProps>(function AssetAudioPlayer(
{ src, resetKey, autoPlay = false, autoAdvanceOnEnded = false, className, onPlayingChange, canGoPrevious = false, canGoNext = false, onPrevious, onNext },
forwardedRef,
) {
const { t } = useTranslation();
const audioRef = useRef<HTMLAudioElement>(null);
const progressTrackRef = useRef<HTMLDivElement>(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 (
<div className={classNames("assetDetailsAudioPlayer", className)}>
<audio ref={audioRef} className="assetDetailsAudioPlayer__engine" src={voiceSrc} preload="metadata" />
<div className="assetDetailsAudioPlayer__controls">
<button
type="button"
className="assetDetailsAudioPlayer__skipBtn assetDetailsAudioPlayer__skipBtn_rewind"
onClick={handlePreviousTrack}
disabled={!canGoPrevious || !onPrevious}
aria-label={t("pages.assetDetails.audioPreviousTrack")}
>
<img src={new URL("/src/assets/images/ic_audio_forward.png", import.meta.url).href} alt="" />
</button>
<button type="button" className="assetDetailsAudioPlayer__playBtn" onClick={togglePlay} aria-label={isPlaying ? t("pages.assetDetails.audioPause") : t("pages.assetDetails.audioPlay")}>
{isPlaying ? <img src={new URL("/src/assets/images/ic_pause_gray.png", import.meta.url).href} alt="" /> : <img src={new URL("/src/assets/images/ic_play_gray.png", import.meta.url).href} alt="" />}
</button>
<button
type="button"
className="assetDetailsAudioPlayer__skipBtn assetDetailsAudioPlayer__skipBtn_forward"
onClick={handleNextTrack}
disabled={!canGoNext || !onNext}
aria-label={t("pages.assetDetails.audioNextTrack")}
>
<img src={new URL("/src/assets/images/ic_audio_forward.png", import.meta.url).href} alt="" />
</button>
</div>
<div className="assetDetailsAudioPlayer__progressRow">
<span className="assetDetailsAudioPlayer__time" aria-hidden>
{formatAudioTime(currentTime)}
</span>
<div
ref={progressTrackRef}
className="assetDetailsAudioPlayer__track"
role="slider"
aria-label={t("pages.assetDetails.audioProgress")}
aria-valuemin={0}
aria-valuemax={Math.floor(duration)}
aria-valuenow={Math.floor(currentTime)}
tabIndex={0}
onClick={(event) => seekByClientX(event.clientX)}
onKeyDown={(event) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
seekWithinTrack(-5);
}
if (event.key === "ArrowRight") {
event.preventDefault();
seekWithinTrack(5);
}
}}
>
<div className="assetDetailsAudioPlayer__trackFill" style={{ width: `${progressPercent}%` }} />
</div>
<span className="assetDetailsAudioPlayer__time" aria-hidden>
{formatAudioTime(duration)}
</span>
</div>
</div>
);
});