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 }