Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com
131 lines
3.1 KiB
TypeScript
131 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
type ReactNode,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { useWebSocket } from "@/lib/ws/context";
|
|
import { mediaApi } from "@/lib/api";
|
|
import type { MediaState, MediaItem } from "@/lib/types";
|
|
|
|
interface MediaPlayerContextValue {
|
|
/** Current play state */
|
|
playing: boolean;
|
|
/** Current track, or null */
|
|
current: MediaItem | null;
|
|
/** Upcoming queue */
|
|
queue: MediaItem[];
|
|
/** Current volume [0-1] */
|
|
volume: number;
|
|
/** True while a mutation is in flight */
|
|
pending: boolean;
|
|
|
|
/** Skip to next track */
|
|
skip: () => void;
|
|
/** Stop playback */
|
|
stop: () => void;
|
|
/** Set volume [0-1] */
|
|
setVolume: (vol: number) => void;
|
|
/** Queue a URL for playback */
|
|
queueUrl: (url: string) => void;
|
|
}
|
|
|
|
const MediaPlayerContext = createContext<MediaPlayerContextValue | null>(null);
|
|
|
|
export function MediaPlayerProvider({ children }: { children: ReactNode }) {
|
|
const ws = useWebSocket();
|
|
const [state, setState] = useState<MediaState>({
|
|
playing: false,
|
|
musicVolume: 0.5,
|
|
current: null,
|
|
queue: [],
|
|
});
|
|
const [pending, setPending] = useState(false);
|
|
const fetched = useRef(false);
|
|
|
|
// Fetch initial state
|
|
useEffect(() => {
|
|
if (fetched.current) return;
|
|
fetched.current = true;
|
|
mediaApi.getStatus().then((data) => {
|
|
if (data) setState(data as MediaState);
|
|
}).catch(() => {
|
|
// API not yet available
|
|
});
|
|
}, []);
|
|
|
|
// Subscribe to live media_state events via WS
|
|
useEffect(() => {
|
|
const unsub = ws.on("media_state", (data) => {
|
|
setState(data as unknown as MediaState);
|
|
});
|
|
return unsub;
|
|
}, [ws]);
|
|
|
|
const skip = useCallback(() => {
|
|
setPending(true);
|
|
mediaApi.skip().then((data) => {
|
|
if (data) setState(data as MediaState);
|
|
}).catch(() => {
|
|
// ignore
|
|
}).finally(() => setPending(false));
|
|
}, []);
|
|
|
|
const stop = useCallback(() => {
|
|
setPending(true);
|
|
mediaApi.stop().then((data) => {
|
|
if (data) setState(data as MediaState);
|
|
}).catch(() => {
|
|
// ignore
|
|
}).finally(() => setPending(false));
|
|
}, []);
|
|
|
|
const setVolume = useCallback((vol: number) => {
|
|
mediaApi.volume(vol).then((data) => {
|
|
if (data) setState(data as MediaState);
|
|
}).catch(() => {
|
|
// ignore
|
|
});
|
|
}, []);
|
|
|
|
const queueUrl = useCallback((url: string) => {
|
|
setPending(true);
|
|
mediaApi.queue(url, "music").then((data) => {
|
|
if (data) setState(data as MediaState);
|
|
}).catch(() => {
|
|
// ignore
|
|
}).finally(() => setPending(false));
|
|
}, []);
|
|
|
|
return (
|
|
<MediaPlayerContext.Provider
|
|
value={{
|
|
playing: state.playing,
|
|
current: state.current,
|
|
queue: state.queue,
|
|
volume: state.musicVolume,
|
|
pending,
|
|
skip,
|
|
stop,
|
|
setVolume,
|
|
queueUrl,
|
|
}}
|
|
>
|
|
{children}
|
|
</MediaPlayerContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useMediaPlayer(): MediaPlayerContextValue {
|
|
const ctx = useContext(MediaPlayerContext);
|
|
if (!ctx) {
|
|
throw new Error("useMediaPlayer must be used within a MediaPlayerProvider");
|
|
}
|
|
return ctx;
|
|
}
|