| 1 | import type { VideoPreviewInput, VideoPreviewRenderer } from "@clo/markdown-editor"; |
| 2 | import type { EditorView } from "@codemirror/view"; |
| 3 | import { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images"; |
| 4 | import { |
| 5 | mediaFileReference, |
| 6 | mediaReferenceFileName, |
| 7 | mediaUrlReference, |
| 8 | } from "@t3tools/client-runtime/media-reference"; |
| 9 | import type { ScopedThreadRef } from "@t3tools/contracts"; |
| 10 | import { useCallback, useLayoutEffect, useRef, useState } from "react"; |
| 11 | import { createPortal } from "react-dom"; |
| 12 | |
| 13 | import { MediaVideoPlayer } from "../media/MediaVideoPlayer"; |
| 14 | import { workspaceAssetResource } from "./filePreviewMode"; |
| 15 | |
| 16 | export 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 | } |