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,
+}