Initial commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { subscribeVideoStatusRefresh } from "./videoStatusEvents";
|
||||
|
||||
interface Status {
|
||||
codes: Set<string>;
|
||||
subtitleCodes: Set<string>;
|
||||
count: number;
|
||||
lastScannedAt: number;
|
||||
rootsScanned: string[];
|
||||
}
|
||||
|
||||
interface Ctx extends Status {
|
||||
hasVideo: (code: string | null | undefined) => boolean;
|
||||
hasSubtitle: (code: string | null | undefined) => boolean;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
const empty: Status = { codes: new Set(), subtitleCodes: new Set(), count: 0, lastScannedAt: 0, rootsScanned: [] };
|
||||
const VideoIdxCtx = createContext<Ctx | null>(null);
|
||||
|
||||
export function VideoIndexProvider({ children }: { children: React.ReactNode }) {
|
||||
const [status, setStatus] = useState<Status>(empty);
|
||||
const inflightRef = useRef<AbortController | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
// Abort any prior fetch so a slow first request can't clobber a
|
||||
// newer second request's result on settle order.
|
||||
inflightRef.current?.abort();
|
||||
const ctrl = new AbortController();
|
||||
inflightRef.current = ctrl;
|
||||
try {
|
||||
const r = await fetch("/api/video-status", { cache: "no-store", signal: ctrl.signal });
|
||||
if (!r.ok) return;
|
||||
const j = await r.json();
|
||||
if (ctrl.signal.aborted) return;
|
||||
setStatus({
|
||||
codes: new Set<string>(Array.isArray(j.codes) ? j.codes : []),
|
||||
subtitleCodes: new Set<string>(Array.isArray(j.subtitleCodes) ? j.subtitleCodes : []),
|
||||
count: j.count ?? 0,
|
||||
lastScannedAt: j.lastScannedAt ?? 0,
|
||||
rootsScanned: Array.isArray(j.rootsScanned) ? j.rootsScanned : [],
|
||||
});
|
||||
} catch {
|
||||
// Silent — if the endpoint fails or aborts, no badges. No user-facing error.
|
||||
} finally {
|
||||
if (inflightRef.current === ctrl) inflightRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { refresh(); return () => { inflightRef.current?.abort(); }; }, [refresh]);
|
||||
useEffect(() => subscribeVideoStatusRefresh(() => { refresh(); }), [refresh]);
|
||||
|
||||
const value = useMemo<Ctx>(() => ({
|
||||
...status,
|
||||
hasVideo: (code) => {
|
||||
if (!code) return false;
|
||||
return status.codes.has(code.toUpperCase());
|
||||
},
|
||||
hasSubtitle: (code) => {
|
||||
if (!code) return false;
|
||||
return status.subtitleCodes.has(code.toUpperCase());
|
||||
},
|
||||
refresh,
|
||||
}), [status, refresh]);
|
||||
|
||||
return <VideoIdxCtx.Provider value={value}>{children}</VideoIdxCtx.Provider>;
|
||||
}
|
||||
|
||||
export function useVideoIndex(): Ctx {
|
||||
const ctx = useContext(VideoIdxCtx);
|
||||
if (!ctx) throw new Error("useVideoIndex must be used within VideoIndexProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user