From fb9623af882a2cf6abf58d585107857f53899bc3 Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Thu, 30 Oct 2025 10:41:52 +0800 Subject: [PATCH] add useAudioPlayer2 --- src/app/memorize/Start.tsx | 2 - src/hooks/useAudioPlayer2.ts | 164 +++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useAudioPlayer2.ts diff --git a/src/app/memorize/Start.tsx b/src/app/memorize/Start.tsx index 6c0ef5b..635d55a 100644 --- a/src/app/memorize/Start.tsx +++ b/src/app/memorize/Start.tsx @@ -1,6 +1,4 @@ import LightButton from "@/components/buttons/LightButton"; -import ACard from "@/components/cards/ACard"; -import BCard from "@/components/cards/BCard"; import Window from "@/components/Window"; import { WordData } from "./page"; import { Dispatch, SetStateAction, useState } from "react"; diff --git a/src/hooks/useAudioPlayer2.ts b/src/hooks/useAudioPlayer2.ts new file mode 100644 index 0000000..b8f24d2 --- /dev/null +++ b/src/hooks/useAudioPlayer2.ts @@ -0,0 +1,164 @@ +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, + }; +}