Files
Arkie-Library-Frontend/src/components/messageStream/overlays/VideoPlayer.tsx
2026-05-25 05:25:57 +08:00

119 lines
2.9 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
type PropsWithChildren,
} from "react";
import { createPortal } from "react-dom";
import { X } from "lucide-react";
import type { Attachment } from "../../../types/post";
type PlayerState = {
attachment: Attachment;
currentTime: number;
} | null;
type Ctx = {
openVideo: (attachment: Attachment, currentTime?: number) => void;
closeVideo: () => void;
};
const VideoPlayerContext = createContext<Ctx | null>(null);
export function useVideoPlayer(): Ctx {
const ctx = useContext(VideoPlayerContext);
if (!ctx)
throw new Error("useVideoPlayer must be used inside VideoPlayerProvider");
return ctx;
}
export function VideoPlayerProvider({ children }: PropsWithChildren) {
const [state, setState] = useState<PlayerState>(null);
const openVideo = useCallback(
(attachment: Attachment, currentTime = 0) =>
setState({ attachment, currentTime }),
[],
);
const closeVideo = useCallback(() => setState(null), []);
return (
<VideoPlayerContext.Provider value={{ openVideo, closeVideo }}>
{children}
{state ? (
<PlayerView
attachment={state.attachment}
startAt={state.currentTime}
onClose={closeVideo}
/>
) : null}
</VideoPlayerContext.Provider>
);
}
function PlayerView({
attachment,
startAt,
onClose,
}: {
attachment: Attachment;
startAt: number;
onClose: () => void;
}) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
window.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
};
}, [onClose]);
useEffect(() => {
const v = videoRef.current;
if (!v) return;
if (startAt > 0) v.currentTime = startAt;
v.play().catch(() => {});
}, [startAt]);
return createPortal(
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/95"
onClick={onClose}
role="dialog"
aria-modal="true"
>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
<video
ref={videoRef}
src={attachment.url}
poster={attachment.posterUrl}
controls
playsInline
className="max-h-[92vh] max-w-[96vw] outline-none"
onClick={(e) => e.stopPropagation()}
/>
</div>,
document.body,
);
}