'use client'; import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; interface AudioPlayerProps { src: string; unlockedSeconds: number; // 2, 4, 7, 11, 16, 30 (or full length) startTime?: number; // Start offset in seconds (for curated specials) onPlay?: () => void; onReplay?: () => void; autoPlay?: boolean; onHasPlayedChange?: (hasPlayed: boolean) => void; } export interface AudioPlayerRef { play: () => void; } const AudioPlayer = forwardRef(({ src, unlockedSeconds, startTime = 0, onPlay, onReplay, autoPlay = false, onHasPlayedChange }, ref) => { const audioRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const [hasPlayedOnce, setHasPlayedOnce] = useState(false); const [processedSrc, setProcessedSrc] = useState(null); const [processedUnlockedSeconds, setProcessedUnlockedSeconds] = useState(null); useEffect(() => { console.log('[AudioPlayer] MOUNTED'); return () => console.log('[AudioPlayer] UNMOUNTED'); }, []); useEffect(() => { if (audioRef.current) { // Check if props changed compared to what we last processed const hasChanged = src !== processedSrc || unlockedSeconds !== processedUnlockedSeconds; if (hasChanged) { audioRef.current.pause(); let startPos = startTime; // If same song but more time unlocked, start from where previous segment ended if (processedSrc !== null && src === processedSrc && processedUnlockedSeconds !== null && unlockedSeconds > processedUnlockedSeconds) { startPos = startTime + processedUnlockedSeconds; } const targetPos = startPos; audioRef.current.currentTime = targetPos; // Ensure position is set correctly even if browser resets it setTimeout(() => { if (audioRef.current && Math.abs(audioRef.current.currentTime - targetPos) > 0.5) { audioRef.current.currentTime = targetPos; } }, 50); setIsPlaying(false); // Calculate initial progress const initialElapsed = startPos - startTime; const initialPercent = unlockedSeconds > 0 ? (initialElapsed / unlockedSeconds) * 100 : 0; setProgress(Math.min(initialPercent, 100)); // Only reset hasPlayedOnce if the song changed, not if just more time was unlocked if (processedSrc !== null && src !== processedSrc) { setHasPlayedOnce(false); // Reset for new song onHasPlayedChange?.(false); // Notify parent } // Update processed state setProcessedSrc(src); setProcessedUnlockedSeconds(unlockedSeconds); if (autoPlay) { // Delay play slightly to ensure currentTime sticks setTimeout(() => { if (audioRef.current) { // Use startPos (which may be startTime + processedUnlockedSeconds if more time was unlocked) // instead of always using startTime audioRef.current.currentTime = startPos; const playPromise = audioRef.current.play(); if (playPromise !== undefined) { playPromise .then(() => { setIsPlaying(true); onPlay?.(); setHasPlayedOnce(true); onHasPlayedChange?.(true); // Notify parent }) .catch(error => { console.log("Autoplay prevented:", error); setIsPlaying(false); }); } } }, 150); } } else if (startTime !== undefined && startTime > 0) { // If startTime is set and we haven't processed changes, ensure currentTime is at least at startTime // This handles the case where the audio element was reset or reloaded, or when manually playing for the first time const current = audioRef.current.currentTime; if (current < startTime) { audioRef.current.currentTime = startTime; } } } }, [src, unlockedSeconds, startTime, autoPlay, processedSrc, processedUnlockedSeconds]); // Expose play method to parent component useImperativeHandle(ref, () => ({ play: () => { if (!audioRef.current) return; // Check if we need to reset to startTime const current = audioRef.current.currentTime; const elapsed = current - startTime; // Reset if: never played before, current position is before startTime, or we've exceeded the unlocked segment if (!hasPlayedOnce || current < startTime || elapsed >= unlockedSeconds) { // Reset to start of segment audioRef.current.currentTime = startTime; } const playPromise = audioRef.current.play(); if (playPromise !== undefined) { playPromise .then(() => { setIsPlaying(true); onPlay?.(); if (!hasPlayedOnce) { setHasPlayedOnce(true); onHasPlayedChange?.(true); } }) .catch(error => { console.error("Play failed:", error); setIsPlaying(false); }); } } })); const togglePlay = () => { if (!audioRef.current) return; if (isPlaying) { audioRef.current.pause(); setIsPlaying(false); } else { // Ensure we're at the correct position before playing const current = audioRef.current.currentTime; const elapsed = current - startTime; // Determine target position let targetPos = startTime; // If we've played before and we're within the unlocked segment, continue from current position if (hasPlayedOnce && current >= startTime && elapsed < unlockedSeconds) { targetPos = current; // Continue from current position } else { // Reset to start of segment if: never played, before startTime, or exceeded unlocked segment targetPos = startTime; } // Set position before playing audioRef.current.currentTime = targetPos; // Ensure position sticks (browser might reset it) setTimeout(() => { if (audioRef.current && Math.abs(audioRef.current.currentTime - targetPos) > 0.5) { audioRef.current.currentTime = targetPos; } }, 50); audioRef.current.play(); setIsPlaying(true); onPlay?.(); if (hasPlayedOnce) { onReplay?.(); } else { setHasPlayedOnce(true); onHasPlayedChange?.(true); // Notify parent } } }; const handleTimeUpdate = () => { if (!audioRef.current) return; const current = audioRef.current.currentTime; const elapsed = current - startTime; const percent = (elapsed / unlockedSeconds) * 100; setProgress(Math.min(percent, 100)); if (elapsed >= unlockedSeconds) { audioRef.current.pause(); audioRef.current.currentTime = startTime; setIsPlaying(false); setProgress(0); } }; return (