Mercurial
diff react_games/src/API_works/music_player.tsx @ 37:fb9bcd3145cb
[ReactGames] Few games I made using react just to practice few things.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 01 Dec 2025 20:22:47 -0800 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/react_games/src/API_works/music_player.tsx Mon Dec 01 20:22:47 2025 -0800 @@ -0,0 +1,168 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +const CHAPTERS: string[] = [ + "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + "https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3", + "https://download.samplelib.com/mp3/sample-3s.mp3", +]; + +type AudioComponentProp = { + srcUrl: string; + onNext?: () => void; + onPrev?: () => void; +}; + +function fmt(t: number) { + if (!isFinite(t)) return "0:00"; + const m = Math.floor(t / 60); + const s = Math.floor(t % 60); + return `${m}:${s.toString().padStart(2, "0")}`; +} + +const AudioComponent = ({ srcUrl, onNext, onPrev }: AudioComponentProp) => { + const audioRef = useRef<HTMLAudioElement>(null); + + const [isPlaying, setIsPlaying] = useState(false); + const [current, setCurrent] = useState(0); + const [duration, setDuration] = useState(0); + + // If true, auto-play after src changes and can play. + const autoplayNextRef = useRef(false); + + // Wire up element events once. + useEffect(() => { + const el = audioRef.current; + if (!el) return; + + const onPlay = () => setIsPlaying(true); + const onPause = () => setIsPlaying(false); + const onTime = () => setCurrent(el.currentTime || 0); + const onMeta = () => setDuration(isFinite(el.duration) ? el.duration : 0); + const onEnded = () => { + if (!onNext) return; + autoplayNextRef.current = true; // keep playing the next track + onNext(); + }; + + el.addEventListener("play", onPlay); + el.addEventListener("pause", onPause); + el.addEventListener("timeupdate", onTime); + el.addEventListener("loadedmetadata", onMeta); + el.addEventListener("ended", onEnded); + + // Prime initial state + onMeta(); + onTime(); + setIsPlaying(!el.paused && !el.ended); + + return () => { + el.removeEventListener("play", onPlay); + el.removeEventListener("pause", onPause); + el.removeEventListener("timeupdate", onTime); + el.removeEventListener("loadedmetadata", onMeta); + el.removeEventListener("ended", onEnded); + }; + }, [onNext]); + + // When src changes, load and maybe auto-play when ready. + useEffect(() => { + const el = audioRef.current; + if (!el) return; + + el.load(); // pick up new src + + const tryPlay = () => { + if (autoplayNextRef.current) { + autoplayNextRef.current = false; + el.play().catch(() => { + // Autoplay might be blocked; leave paused state as-is. + }); + } + }; + + // Play once ready (covers both cached and freshly loaded cases) + el.addEventListener("canplay", tryPlay, { once: true }); + // In case it's already buffered: + tryPlay(); + + // Reset progress immediately on new track + setCurrent(0); + }, [srcUrl]); + + const toggle = useCallback(() => { + const el = audioRef.current; + if (!el) return; + if (el.paused) el.play().catch(() => {}); + else el.pause(); + }, []); + + const seek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { + const el = audioRef.current; + if (!el) return; + el.currentTime = Number(e.target.value); + }, []); + + // Button handlers: set the autoplay flag so next track starts immediately. + const handleNext = useCallback(() => { + if (!onNext) return; + autoplayNextRef.current = true; // user gesture → autoplay allowed + onNext(); + }, [onNext]); + + const handlePrev = useCallback(() => { + if (!onPrev) return; + autoplayNextRef.current = true; + onPrev(); + }, [onPrev]); + + const pct = duration > 0 ? (current / duration) * 100 : 0; + + return ( + <> + {/* remove "controls" if you want only custom UI */} + <audio ref={audioRef} src={srcUrl} preload="metadata" /> + <div style={{ display: "flex", gap: 8, alignItems: "center", margin: "8px 0" }}> + <button onClick={toggle}>{isPlaying ? "Pause" : "Play"}</button> + <button onClick={handlePrev} disabled={!onPrev}>Prev</button> + <button onClick={handleNext} disabled={!onNext}>Next</button> +" <span style={{ marginLeft: 8, width: 56, textAlign: "right" }}>{fmt(current)}</span>", + <input + type="range" + min={0} + max={duration || 0} + step={0.01} + value={current} + onChange={seek} + style={{ flex: 1 }} + /> + <span style={{ width: 56 }}>{fmt(duration)}</span> + </div> + {/* Optional thin progress bar */} + <div style={{ height: 4, width: "100%", background: "#eee", borderRadius: 2 }}> + <div style={{ height: "100%", width: `${pct}%`, background: "#999", borderRadius: 2 }} /> + </div> + </> + ); +}; + +const MusicPlayer = () => { + const [musicIndex, setMusicIndex] = useState<number>(0); + + const onNext = useCallback( + () => setMusicIndex((i) => Math.min(i + 1, CHAPTERS.length - 1)), + [] + ); + const onPrev = useCallback(() => setMusicIndex((i) => Math.max(i - 1, 0)), []); + + return ( + <AudioComponent + srcUrl={CHAPTERS[musicIndex]} + onNext={musicIndex === CHAPTERS.length - 1 ? undefined : onNext} + onPrev={musicIndex === 0 ? undefined : onPrev} + /> + ); +}; + +export { + MusicPlayer, +}