From d2f9a58cca21f4a8237fd3095ec9e80ff5d37c96 Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Thu, 30 Oct 2025 11:19:00 +0800 Subject: [PATCH] refactored to useAudioPlayer2, change useAudioPlayer2 into useAudioPlayer --- src/app/text-speaker/SaveList.tsx | 4 +- src/app/text-speaker/page.tsx | 21 ++-- src/app/translator/page.tsx | 16 ++- src/hooks/useAudioPlayer.ts | 171 ++++++++++++++++++++++++++---- src/hooks/useAudioPlayer2.ts | 164 ---------------------------- src/utils.ts | 1 + 6 files changed, 177 insertions(+), 200 deletions(-) delete mode 100644 src/hooks/useAudioPlayer2.ts diff --git a/src/app/text-speaker/SaveList.tsx b/src/app/text-speaker/SaveList.tsx index 0a81426..3be2e4c 100644 --- a/src/app/text-speaker/SaveList.tsx +++ b/src/app/text-speaker/SaveList.tsx @@ -20,7 +20,7 @@ function TextCard({ item, handleUse, handleDel }: TextCardProps) { handleDel(item); }; return ( -
+
{item.text} @@ -70,7 +70,7 @@ export default function SaveList({ show = false, handleUse }: SaveListProps) { if (show) return (
diff --git a/src/app/text-speaker/page.tsx b/src/app/text-speaker/page.tsx index 68c5865..4e18efc 100644 --- a/src/app/text-speaker/page.tsx +++ b/src/app/text-speaker/page.tsx @@ -30,7 +30,7 @@ export default function TextSpeaker() { const [ipa, setIPA] = useState(""); const objurlRef = useRef(null); const [processing, setProcessing] = useState(false); - const { playAudio, stopAudio, audioRef } = useAudioPlayer(); + const { play, stop, load, audioRef } = useAudioPlayer(); useEffect(() => { const audio = audioRef.current; if (!audio) return; @@ -39,7 +39,8 @@ export default function TextSpeaker() { if (autopause) { setPause(true); } else { - playAudio(objurlRef.current!); + load(objurlRef.current!); + play(); } }; audio.addEventListener("ended", handleEnded); @@ -77,7 +78,8 @@ export default function TextSpeaker() { if (objurlRef.current) { // 之前有播放 - playAudio(objurlRef.current); + load(objurlRef.current); + play(); } else { // 第一次播放 try { @@ -112,7 +114,8 @@ export default function TextSpeaker() { }; })(), ); - playAudio(objurlRef.current); + load(objurlRef.current); + play(); } catch (e) { console.error(e); @@ -126,7 +129,7 @@ export default function TextSpeaker() { } else { // 如果在读就暂停 setPause(true); - stopAudio(); + stop(); } setProcessing(false); @@ -138,7 +141,7 @@ export default function TextSpeaker() { setIPA(""); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; - stopAudio(); + stop(); setPause(true); }; @@ -147,7 +150,7 @@ export default function TextSpeaker() { setSpeed(new_speed); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; - stopAudio(); + stop(); setPause(true); }; }; @@ -159,7 +162,7 @@ export default function TextSpeaker() { setIPA(item.ipa || ""); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; - stopAudio(); + stop(); setPause(true); }; @@ -291,7 +294,7 @@ export default function TextSpeaker() { onClick={() => { setAutopause(!autopause); if (objurlRef) { - stopAudio(); + stop(); } setPause(true); }} diff --git a/src/app/translator/page.tsx b/src/app/translator/page.tsx index 6f1c34e..d2f08e1 100644 --- a/src/app/translator/page.tsx +++ b/src/app/translator/page.tsx @@ -20,7 +20,7 @@ export default function Translator() { const [sourceLocale, setSourceLocale] = useState(null); const [targetLocale, setTargetLocale] = useState(null); const [translating, setTranslating] = useState(false); - const { playAudio } = useAudioPlayer(); + const { play, load } = useAudioPlayer(); const tl = ["Chinese", "English", "Italian"]; @@ -132,7 +132,8 @@ export default function Translator() { } const url = await getTTSAudioUrl(sourceText, voice.short_name); - await playAudio(url); + await load(url); + await play(); URL.revokeObjectURL(url); } catch (e) { console.error(e); @@ -146,7 +147,8 @@ export default function Translator() { } const url = await getTTSAudioUrl(sourceText, voice.short_name); - await playAudio(url); + await load(url); + await play(); URL.revokeObjectURL(url); } }; @@ -173,7 +175,8 @@ export default function Translator() { if (!voice) return; const url = await getTTSAudioUrl(targetText, voice.short_name); - await playAudio(url); + await load(url); + await play(); URL.revokeObjectURL(url); }; @@ -264,7 +267,10 @@ export default function Translator() { > Italian - + {"Other" + (tl.includes(targetLang) ? "" : ": " + targetLang)}
diff --git a/src/hooks/useAudioPlayer.ts b/src/hooks/useAudioPlayer.ts index e6954df..87695f9 100644 --- a/src/hooks/useAudioPlayer.ts +++ b/src/hooks/useAudioPlayer.ts @@ -1,33 +1,164 @@ -import { useRef, useEffect } from "react"; +import { useRef, useEffect, useState, useCallback } from "react"; + +type AudioPlayerError = Error | null; export function useAudioPlayer() { const audioRef = useRef(null); + const [state, setState] = useState({ + isPlaying: false, + isLoading: false, + duration: 0, + currentTime: 0, + volume: 1, + }); + const [error, setError] = useState(null); + + // Initialize audio element useEffect(() => { - audioRef.current = new Audio(); + const audio = new Audio(); + audio.preload = "metadata"; + audioRef.current = audio; + + // Event listeners + const handleLoadStart = () => + setState((prev) => ({ ...prev, isLoading: true })); + const handleCanPlay = () => + setState((prev) => ({ ...prev, isLoading: false })); + const handleLoadedMetadata = () => + setState((prev) => ({ ...prev, duration: audio.duration })); + const handleTimeUpdate = () => + setState((prev) => ({ ...prev, currentTime: audio.currentTime })); + const handleEnded = () => + setState((prev) => ({ ...prev, isPlaying: false, currentTime: 0 })); + const handleError = (e: Event) => { + const target = e.target as HTMLAudioElement; + setError(new Error(target.error?.message || "Audio playback error")); + setState((prev) => ({ ...prev, isLoading: false, isPlaying: false })); + }; + + audio.addEventListener("loadstart", handleLoadStart); + audio.addEventListener("canplay", handleCanPlay); + audio.addEventListener("loadedmetadata", handleLoadedMetadata); + audio.addEventListener("timeupdate", handleTimeUpdate); + audio.addEventListener("ended", handleEnded); + audio.addEventListener("error", handleError); + return () => { - audioRef.current!.pause(); - audioRef.current = null; + audio.removeEventListener("loadstart", handleLoadStart); + audio.removeEventListener("canplay", handleCanPlay); + audio.removeEventListener("loadedmetadata", handleLoadedMetadata); + audio.removeEventListener("timeupdate", handleTimeUpdate); + audio.removeEventListener("ended", handleEnded); + audio.removeEventListener("error", handleError); + + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ""; + audioRef.current = null; + } }; }, []); - const playAudio = async (audioUrl: string) => { - audioRef.current!.src = audioUrl; + + const play = useCallback(async () => { + if (!audioRef.current) return; + try { - await audioRef.current!.play(); - } catch (e) { - return e; + setError(null); + await audioRef.current.play(); + setState((prev) => ({ ...prev, isPlaying: true })); + } catch (err) { + const error = + err instanceof Error ? err : new Error("Failed to play audio"); + setError(error); + setState((prev) => ({ ...prev, isPlaying: false })); + throw error; } - }; - const pauseAudio = () => { - audioRef.current!.pause(); - }; - const stopAudio = () => { - audioRef.current!.pause(); - audioRef.current!.currentTime = 0; - }; + }, []); + + const pause = useCallback(() => { + if (audioRef.current) { + audioRef.current.pause(); + setState((prev) => ({ ...prev, isPlaying: false })); + } + }, []); + + const stop = useCallback(() => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + setState((prev) => ({ ...prev, isPlaying: false, currentTime: 0 })); + } + }, []); + + const setVolume = useCallback((volume: number) => { + if (audioRef.current) { + const clampedVolume = Math.max(0, Math.min(1, volume)); + audioRef.current.volume = clampedVolume; + setState((prev) => ({ ...prev, volume: clampedVolume })); + } + }, []); + + const seek = useCallback((time: number) => { + if (audioRef.current) { + const clampedTime = Math.max( + 0, + Math.min(audioRef.current.duration, time), + ); + audioRef.current.currentTime = clampedTime; + setState((prev) => ({ ...prev, currentTime: clampedTime })); + } + }, []); + + const load = useCallback(async (audioUrl: string) => { + if (!audioRef.current) return; + + try { + setError(null); + setState((prev) => ({ ...prev, isLoading: true })); + + // Only load if URL is different + if (audioRef.current.src !== audioUrl) { + audioRef.current.src = audioUrl; + await new Promise((resolve, reject) => { + if (!audioRef.current) + return reject(new Error("Audio element not found")); + + const handleCanPlay = () => { + audioRef.current?.removeEventListener("canplay", handleCanPlay); + audioRef.current?.removeEventListener("error", handleError); + resolve(void 0); + }; + + const handleError = () => { + audioRef.current?.removeEventListener("canplay", handleCanPlay); + audioRef.current?.removeEventListener("error", handleError); + reject(new Error("Failed to load audio")); + }; + + audioRef.current.addEventListener("canplay", handleCanPlay); + audioRef.current.addEventListener("error", handleError); + }); + } + + setState((prev) => ({ ...prev, isLoading: false })); + } catch (err) { + const error = + err instanceof Error ? err : new Error("Failed to load audio"); + setError(error); + setState((prev) => ({ ...prev, isLoading: false })); + throw error; + } + }, []); + return { - playAudio, - pauseAudio, - stopAudio, + ...state, + play, + pause, + stop, + setVolume, + seek, + load, + error, audioRef, }; } diff --git a/src/hooks/useAudioPlayer2.ts b/src/hooks/useAudioPlayer2.ts deleted file mode 100644 index b8f24d2..0000000 --- a/src/hooks/useAudioPlayer2.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { useRef, useEffect, useState, useCallback } from "react"; - -type AudioPlayerError = Error | null; - -export function useAudioPlayer2() { - const audioRef = useRef(null); - const [state, setState] = useState({ - isPlaying: false, - isLoading: false, - duration: 0, - currentTime: 0, - volume: 1, - }); - const [error, setError] = useState(null); - - // Initialize audio element - useEffect(() => { - const audio = new Audio(); - audio.preload = "metadata"; - audioRef.current = audio; - - // Event listeners - const handleLoadStart = () => - setState((prev) => ({ ...prev, isLoading: true })); - const handleCanPlay = () => - setState((prev) => ({ ...prev, isLoading: false })); - const handleLoadedMetadata = () => - setState((prev) => ({ ...prev, duration: audio.duration })); - const handleTimeUpdate = () => - setState((prev) => ({ ...prev, currentTime: audio.currentTime })); - const handleEnded = () => - setState((prev) => ({ ...prev, isPlaying: false, currentTime: 0 })); - const handleError = (e: Event) => { - const target = e.target as HTMLAudioElement; - setError(new Error(target.error?.message || "Audio playback error")); - setState((prev) => ({ ...prev, isLoading: false, isPlaying: false })); - }; - - audio.addEventListener("loadstart", handleLoadStart); - audio.addEventListener("canplay", handleCanPlay); - audio.addEventListener("loadedmetadata", handleLoadedMetadata); - audio.addEventListener("timeupdate", handleTimeUpdate); - audio.addEventListener("ended", handleEnded); - audio.addEventListener("error", handleError); - - return () => { - audio.removeEventListener("loadstart", handleLoadStart); - audio.removeEventListener("canplay", handleCanPlay); - audio.removeEventListener("loadedmetadata", handleLoadedMetadata); - audio.removeEventListener("timeupdate", handleTimeUpdate); - audio.removeEventListener("ended", handleEnded); - audio.removeEventListener("error", handleError); - - if (audioRef.current) { - audioRef.current.pause(); - audioRef.current.src = ""; - audioRef.current = null; - } - }; - }, []); - - const play = useCallback(async () => { - if (!audioRef.current) return; - - try { - setError(null); - await audioRef.current.play(); - setState((prev) => ({ ...prev, isPlaying: true })); - } catch (err) { - const error = - err instanceof Error ? err : new Error("Failed to play audio"); - setError(error); - setState((prev) => ({ ...prev, isPlaying: false })); - throw error; - } - }, []); - - const pause = useCallback(() => { - if (audioRef.current) { - audioRef.current.pause(); - setState((prev) => ({ ...prev, isPlaying: false })); - } - }, []); - - const stop = useCallback(() => { - if (audioRef.current) { - audioRef.current.pause(); - audioRef.current.currentTime = 0; - setState((prev) => ({ ...prev, isPlaying: false, currentTime: 0 })); - } - }, []); - - const setVolume = useCallback((volume: number) => { - if (audioRef.current) { - const clampedVolume = Math.max(0, Math.min(1, volume)); - audioRef.current.volume = clampedVolume; - setState((prev) => ({ ...prev, volume: clampedVolume })); - } - }, []); - - const seek = useCallback((time: number) => { - if (audioRef.current) { - const clampedTime = Math.max( - 0, - Math.min(audioRef.current.duration, time), - ); - audioRef.current.currentTime = clampedTime; - setState((prev) => ({ ...prev, currentTime: clampedTime })); - } - }, []); - - const load = useCallback(async (audioUrl: string) => { - if (!audioRef.current) return; - - try { - setError(null); - setState((prev) => ({ ...prev, isLoading: true })); - - // Only load if URL is different - if (audioRef.current.src !== audioUrl) { - audioRef.current.src = audioUrl; - await new Promise((resolve, reject) => { - if (!audioRef.current) - return reject(new Error("Audio element not found")); - - const handleCanPlay = () => { - audioRef.current?.removeEventListener("canplay", handleCanPlay); - audioRef.current?.removeEventListener("error", handleError); - resolve(void 0); - }; - - const handleError = () => { - audioRef.current?.removeEventListener("canplay", handleCanPlay); - audioRef.current?.removeEventListener("error", handleError); - reject(new Error("Failed to load audio")); - }; - - audioRef.current.addEventListener("canplay", handleCanPlay); - audioRef.current.addEventListener("error", handleError); - }); - } - - setState((prev) => ({ ...prev, isLoading: false })); - } catch (err) { - const error = - err instanceof Error ? err : new Error("Failed to load audio"); - setError(error); - setState((prev) => ({ ...prev, isLoading: false })); - throw error; - } - }, []); - - return { - ...state, - play, - pause, - stop, - setVolume, - seek, - load, - error, - audioRef, - }; -} diff --git a/src/utils.ts b/src/utils.ts index 60fae59..6081059 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -62,6 +62,7 @@ export async function getTTSAudioUrl( } export const getTextSpeakerData = () => { try { + if (!localStorage) return []; const item = localStorage.getItem("text-speaker"); if (!item) return [];