Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 36:84672efec192 | 37:fb9bcd3145cb |
|---|---|
| 1 import { useCallback, useEffect, useRef, useState } from "react"; | |
| 2 | |
| 3 const CHAPTERS: string[] = [ | |
| 4 "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", | |
| 5 "https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3", | |
| 6 "https://download.samplelib.com/mp3/sample-3s.mp3", | |
| 7 ]; | |
| 8 | |
| 9 type AudioComponentProp = { | |
| 10 srcUrl: string; | |
| 11 onNext?: () => void; | |
| 12 onPrev?: () => void; | |
| 13 }; | |
| 14 | |
| 15 function fmt(t: number) { | |
| 16 if (!isFinite(t)) return "0:00"; | |
| 17 const m = Math.floor(t / 60); | |
| 18 const s = Math.floor(t % 60); | |
| 19 return `${m}:${s.toString().padStart(2, "0")}`; | |
| 20 } | |
| 21 | |
| 22 const AudioComponent = ({ srcUrl, onNext, onPrev }: AudioComponentProp) => { | |
| 23 const audioRef = useRef<HTMLAudioElement>(null); | |
| 24 | |
| 25 const [isPlaying, setIsPlaying] = useState(false); | |
| 26 const [current, setCurrent] = useState(0); | |
| 27 const [duration, setDuration] = useState(0); | |
| 28 | |
| 29 // If true, auto-play after src changes and can play. | |
| 30 const autoplayNextRef = useRef(false); | |
| 31 | |
| 32 // Wire up element events once. | |
| 33 useEffect(() => { | |
| 34 const el = audioRef.current; | |
| 35 if (!el) return; | |
| 36 | |
| 37 const onPlay = () => setIsPlaying(true); | |
| 38 const onPause = () => setIsPlaying(false); | |
| 39 const onTime = () => setCurrent(el.currentTime || 0); | |
| 40 const onMeta = () => setDuration(isFinite(el.duration) ? el.duration : 0); | |
| 41 const onEnded = () => { | |
| 42 if (!onNext) return; | |
| 43 autoplayNextRef.current = true; // keep playing the next track | |
| 44 onNext(); | |
| 45 }; | |
| 46 | |
| 47 el.addEventListener("play", onPlay); | |
| 48 el.addEventListener("pause", onPause); | |
| 49 el.addEventListener("timeupdate", onTime); | |
| 50 el.addEventListener("loadedmetadata", onMeta); | |
| 51 el.addEventListener("ended", onEnded); | |
| 52 | |
| 53 // Prime initial state | |
| 54 onMeta(); | |
| 55 onTime(); | |
| 56 setIsPlaying(!el.paused && !el.ended); | |
| 57 | |
| 58 return () => { | |
| 59 el.removeEventListener("play", onPlay); | |
| 60 el.removeEventListener("pause", onPause); | |
| 61 el.removeEventListener("timeupdate", onTime); | |
| 62 el.removeEventListener("loadedmetadata", onMeta); | |
| 63 el.removeEventListener("ended", onEnded); | |
| 64 }; | |
| 65 }, [onNext]); | |
| 66 | |
| 67 // When src changes, load and maybe auto-play when ready. | |
| 68 useEffect(() => { | |
| 69 const el = audioRef.current; | |
| 70 if (!el) return; | |
| 71 | |
| 72 el.load(); // pick up new src | |
| 73 | |
| 74 const tryPlay = () => { | |
| 75 if (autoplayNextRef.current) { | |
| 76 autoplayNextRef.current = false; | |
| 77 el.play().catch(() => { | |
| 78 // Autoplay might be blocked; leave paused state as-is. | |
| 79 }); | |
| 80 } | |
| 81 }; | |
| 82 | |
| 83 // Play once ready (covers both cached and freshly loaded cases) | |
| 84 el.addEventListener("canplay", tryPlay, { once: true }); | |
| 85 // In case it's already buffered: | |
| 86 tryPlay(); | |
| 87 | |
| 88 // Reset progress immediately on new track | |
| 89 setCurrent(0); | |
| 90 }, [srcUrl]); | |
| 91 | |
| 92 const toggle = useCallback(() => { | |
| 93 const el = audioRef.current; | |
| 94 if (!el) return; | |
| 95 if (el.paused) el.play().catch(() => {}); | |
| 96 else el.pause(); | |
| 97 }, []); | |
| 98 | |
| 99 const seek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { | |
| 100 const el = audioRef.current; | |
| 101 if (!el) return; | |
| 102 el.currentTime = Number(e.target.value); | |
| 103 }, []); | |
| 104 | |
| 105 // Button handlers: set the autoplay flag so next track starts immediately. | |
| 106 const handleNext = useCallback(() => { | |
| 107 if (!onNext) return; | |
| 108 autoplayNextRef.current = true; // user gesture → autoplay allowed | |
| 109 onNext(); | |
| 110 }, [onNext]); | |
| 111 | |
| 112 const handlePrev = useCallback(() => { | |
| 113 if (!onPrev) return; | |
| 114 autoplayNextRef.current = true; | |
| 115 onPrev(); | |
| 116 }, [onPrev]); | |
| 117 | |
| 118 const pct = duration > 0 ? (current / duration) * 100 : 0; | |
| 119 | |
| 120 return ( | |
| 121 <> | |
| 122 {/* remove "controls" if you want only custom UI */} | |
| 123 <audio ref={audioRef} src={srcUrl} preload="metadata" /> | |
| 124 <div style={{ display: "flex", gap: 8, alignItems: "center", margin: "8px 0" }}> | |
| 125 <button onClick={toggle}>{isPlaying ? "Pause" : "Play"}</button> | |
| 126 <button onClick={handlePrev} disabled={!onPrev}>Prev</button> | |
| 127 <button onClick={handleNext} disabled={!onNext}>Next</button> | |
| 128 " <span style={{ marginLeft: 8, width: 56, textAlign: "right" }}>{fmt(current)}</span>", | |
| 129 <input | |
| 130 type="range" | |
| 131 min={0} | |
| 132 max={duration || 0} | |
| 133 step={0.01} | |
| 134 value={current} | |
| 135 onChange={seek} | |
| 136 style={{ flex: 1 }} | |
| 137 /> | |
| 138 <span style={{ width: 56 }}>{fmt(duration)}</span> | |
| 139 </div> | |
| 140 {/* Optional thin progress bar */} | |
| 141 <div style={{ height: 4, width: "100%", background: "#eee", borderRadius: 2 }}> | |
| 142 <div style={{ height: "100%", width: `${pct}%`, background: "#999", borderRadius: 2 }} /> | |
| 143 </div> | |
| 144 </> | |
| 145 ); | |
| 146 }; | |
| 147 | |
| 148 const MusicPlayer = () => { | |
| 149 const [musicIndex, setMusicIndex] = useState<number>(0); | |
| 150 | |
| 151 const onNext = useCallback( | |
| 152 () => setMusicIndex((i) => Math.min(i + 1, CHAPTERS.length - 1)), | |
| 153 [] | |
| 154 ); | |
| 155 const onPrev = useCallback(() => setMusicIndex((i) => Math.max(i - 1, 0)), []); | |
| 156 | |
| 157 return ( | |
| 158 <AudioComponent | |
| 159 srcUrl={CHAPTERS[musicIndex]} | |
| 160 onNext={musicIndex === CHAPTERS.length - 1 ? undefined : onNext} | |
| 161 onPrev={musicIndex === 0 ? undefined : onPrev} | |
| 162 /> | |
| 163 ); | |
| 164 }; | |
| 165 | |
| 166 export { | |
| 167 MusicPlayer, | |
| 168 } |