'use client' import { useEffect, useRef, useState } from "react"; import { useTheme } from "next-themes"; import type WaveSurfer from "wavesurfer.js"; import { Download, Loader2, Pause, Play } from "lucide-react"; import { Button } from "~/components/ui/button"; import { toast } from "sonner"; function formatTime(seconds: number) { if (!Number.isFinite(seconds)) return "0:00"; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, "0")}`; } function cssVar(name: string, fallback: string) { if (typeof window === "undefined") return fallback; const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } function waveColors() { return { waveColor: cssVar("--muted-foreground", "#9ca3af"), progressColor: cssVar("--primary", "#e2761b"), cursorColor: cssVar("--foreground", "#111827"), }; } export default function AudioPlayer(props: { /** Streaming-friendly source the player actually plays. */ src: string; /** Original high-quality file offered via the download button. */ downloadUrl: string; downloadName: string; }) { const containerRef = useRef(null); const wsRef = useRef(null); const [playing, setPlaying] = useState(false); const [current, setCurrent] = useState(0); const [duration, setDuration] = useState(0); const [ready, setReady] = useState(false); const [downloading, setDownloading] = useState(false); const { resolvedTheme } = useTheme(); useEffect(() => { let ws: WaveSurfer | null = null; let cancelled = false; setReady(false); setCurrent(0); (async () => { const WaveSurferClass = (await import("wavesurfer.js")).default; if (cancelled || !containerRef.current) return; ws = WaveSurferClass.create({ container: containerRef.current, url: props.src, height: 44, barWidth: 2, barGap: 1, barRadius: 2, normalize: true, cursorWidth: 1, ...waveColors(), }); wsRef.current = ws; ws.on("ready", () => { setReady(true); setDuration(ws?.getDuration() ?? 0); }); ws.on("play", () => setPlaying(true)); ws.on("pause", () => setPlaying(false)); ws.on("finish", () => setPlaying(false)); ws.on("timeupdate", (t) => setCurrent(t)); })(); return () => { cancelled = true; ws?.destroy(); wsRef.current = null; }; }, [props.src]); // Re-tint the waveform when the user toggles light/dark. useEffect(() => { wsRef.current?.setOptions(waveColors()); }, [resolvedTheme]); function togglePlay() { wsRef.current?.playPause(); } async function handleDownload() { setDownloading(true); try { // The download file is cross-origin, so the attribute is // ignored — fetch it as a blob to force a real download. const res = await fetch(props.downloadUrl); if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = props.downloadName; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (e) { toast(`Download failed: ${e instanceof Error ? e.message : "unknown error"}`); } finally { setDownloading(false); } } return (
{formatTime(current)}
{!ready && (
)}
{formatTime(duration)}
); }