1import type { VideoPreviewInput, VideoPreviewRenderer } from "@clo/markdown-editor";
2import type { EditorView } from "@codemirror/view";
3import { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images";
4import {
5 mediaFileReference,
6 mediaReferenceFileName,
7 mediaUrlReference,
8} from "@t3tools/client-runtime/media-reference";
9import type { ScopedThreadRef } from "@t3tools/contracts";
10import { useCallback, useLayoutEffect, useRef, useState } from "react";
11import { createPortal } from "react-dom";
12
13import { MediaVideoPlayer } from "../media/MediaVideoPlayer";
14import { workspaceAssetResource } from "./filePreviewMode";
15
16export function useMarkdownVideos(context: {
17 readonly cwd: string;
18 readonly mediaBaseDir: string;
19 readonly threadRef: ScopedThreadRef;
20 readonly draft: boolean;
21 readonly revision: string | null;
22}) {
23 const nextKey = useRef(0);
24 const [previews, setPreviews] = useState<
25 readonly {
26 readonly key: number;
27 readonly dom: HTMLElement;
28 readonly input: VideoPreviewInput;
29 readonly view: EditorView;
30 }[]
31 >([]);
32
33 const renderVideo = useCallback<VideoPreviewRenderer>((input, view) => {
34 const dom = document.createElement("span");
35 dom.className = "cm-lp-video";
36 const key = nextKey.current++;
37 setPreviews((current) => [...current, { key, dom, input, view }]);
38 return {
39 dom,
40 update: (input) =>
41 setPreviews((current) =>
42 current.map((preview) => (preview.key === key ? { ...preview, input } : preview)),
43 ),
44 destroy: () => setPreviews((current) => current.filter((preview) => preview.key !== key)),
45 };
46 }, []);
47
48 useLayoutEffect(() => {
49 for (const { view } of previews) view.requestMeasure();
50 }, [previews]);
51
52 const portals = previews.map(({ key, dom, input }) => {
53 const source = classifyMarkdownImageSource(input.source, context.mediaBaseDir);
54 const reference =
55 source._tag === "WorkspaceFile"
56 ? mediaFileReference(source.path, context.cwd)
57 : mediaUrlReference(input.source);
58 const src = input.url === "about:blank" ? null : input.url;
59 const name = (reference && mediaReferenceFileName(reference)) || input.alt || "Video";
60 const asset =
61 source._tag === "WorkspaceFile"
62 ? {
63 environmentId: context.threadRef.environmentId,
64 resource: workspaceAssetResource({
65 kind: "media-file",
66 threadRef: context.threadRef,
67 draft: context.draft,
68 workspaceRoot: context.cwd,
69 absolutePath: source.path,
70 }),
71 }
72 : undefined;
73 return createPortal(
74 <MediaVideoPlayer
75 src={src}
76 label={input.alt || name}
77 originalUrl={source._tag === "Direct" ? source.uri : undefined}
78 sourceFailed={source._tag === "Blocked"}
79 revision={context.revision}
80 preload="metadata"
81 className="w-full max-w-full"
82 videoClassName="max-h-90 max-w-full"
83 actionsSource={{
84 kind: "video",
85 name,
86 src,
87 ...(reference ? { reference } : {}),
88 ...(asset ? { asset } : {}),
89 }}
90 />,
91 dom,
92 String(key),
93 );
94 });
95 return { renderVideo, portals };
96}