"use client"; import { createContext, useContext, useRef, useState, useCallback, useEffect, type ReactNode, } from "react"; import { useAuth } from "@/components/auth/auth-provider"; import { trackKey, loadAllProgress, saveProgress, removeProgress, syncLocalToRemote, type ProgressEntry, } from "@/lib/audio-progress"; import { primeOfflineAudio, offlineAudioURL, releaseOfflineAudio, } from "@/lib/offline"; /** * Resolve the media source for a track: prefer a saved offline object URL * (works with no network + full seeking) and fall back to the remote URL. */ function resolveSrc(url: string): string { return offlineAudioURL(trackKey(url)) ?? url; } // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type AudioTrack = { url: string; title: string; subtitle?: string; coverImage?: string; sourceUrl?: string; type?: "lecture" | "conversation" | "kirtan" | "music" | string; }; type AudioPlayerState = { currentTrack: AudioTrack | null; isPlaying: boolean; currentTime: number; duration: number; volume: number; isMuted: boolean; isLoading: boolean; playlist: AudioTrack[]; currentIndex: number; }; type AudioPlayerActions = { play: (track: AudioTrack) => void; playPlaylist: (tracks: AudioTrack[], startIndex?: number) => void; /** * Gives the track that's already playing a queue to sit in, without * restarting it — for a queue that arrives after playback began. Ignored * if the current track isn't in it (the user moved on meanwhile). */ attachQueue: (tracks: AudioTrack[]) => void; pause: () => void; resume: () => void; togglePlayPause: () => void; stop: () => void; seekTo: (time: number) => void; setVolume: (vol: number) => void; toggleMute: () => void; next: () => void; previous: () => void; }; type AudioPlayerContextValue = AudioPlayerState & AudioPlayerActions; // --------------------------------------------------------------------------- // Context // --------------------------------------------------------------------------- const AudioPlayerContext = createContext(null); // --------------------------------------------------------------------------- // localStorage helpers // --------------------------------------------------------------------------- const VOLUME_KEY = "audio-player-volume"; const MUTED_KEY = "audio-player-muted"; // Playback-resume tuning. Persist at most once every SAVE_INTERVAL_MS while // playing (discrete events — pause/close/tab-hide — always flush immediately). // Positions within RESUME_END_GUARD_S of the end are treated as "finished": // never saved and never resumed, so a completed track restarts from 0. const SAVE_INTERVAL_MS = 5000; const RESUME_END_GUARD_S = 5; function readStoredVolume(): number { if (typeof window === "undefined") return 0.75; try { const raw = localStorage.getItem(VOLUME_KEY); if (raw !== null) { const v = parseFloat(raw); if (isFinite(v) && v >= 0 && v <= 1) return v; } } catch { // localStorage unavailable } return 0.75; } function readStoredMuted(): boolean { if (typeof window === "undefined") return false; try { return localStorage.getItem(MUTED_KEY) === "true"; } catch { return false; } } function persistVolume(vol: number) { try { localStorage.setItem(VOLUME_KEY, String(vol)); } catch { // ignore } } function persistMuted(muted: boolean) { try { localStorage.setItem(MUTED_KEY, String(muted)); } catch { // ignore } } // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- export function AudioPlayerProvider({ children }: { children: ReactNode }) { const audioRef = useRef(null); // --- State --- const [currentTrack, setCurrentTrack] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolumeState] = useState(readStoredVolume); const [isMuted, setIsMuted] = useState(readStoredMuted); const [isLoading, setIsLoading] = useState(false); const [playlist, setPlaylist] = useState([]); const [currentIndex, setCurrentIndex] = useState(-1); // Keep refs for values accessed in callbacks to avoid stale closures const playlistRef = useRef(playlist); const currentIndexRef = useRef(currentIndex); // --- Playback-resume memory --- const { user, loading: authLoading } = useAuth(); const authedRef = useRef(false); useEffect(() => { authedRef.current = !!user; }, [user]); // The track currently loaded into the