mirror of https://github.com/usememos/memos.git
feat: youtube embeds
This commit is contained in:
parent
71181b1640
commit
c2194f11aa
|
|
@ -1,6 +1,6 @@
|
|||
import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import { memo, useCallback, useState, useMemo } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||
|
|
@ -20,6 +20,8 @@ import MemoEditor from "./MemoEditor";
|
|||
import MemoLocationView from "./MemoLocationView";
|
||||
import MemoReactionistView from "./MemoReactionListView";
|
||||
import MemoRelationListView from "./MemoRelationListView";
|
||||
import MemoYoutubeEmbedListView from "./MemoYoutubeEmbedListView";
|
||||
import { extractYoutubeVideoIdsFromNodes } from "@/utils/youtube";
|
||||
import PreviewImageDialog from "./PreviewImageDialog";
|
||||
import ReactionSelector from "./ReactionSelector";
|
||||
import UserAvatar from "./UserAvatar";
|
||||
|
|
@ -65,6 +67,8 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
workspaceMemoRelatedSetting.enableBlurNsfwContent &&
|
||||
memo.tags?.some((tag) => workspaceMemoRelatedSetting.nsfwTags.some((nsfwTag) => tag === nsfwTag || tag.startsWith(`${nsfwTag}/`)));
|
||||
|
||||
const youtubeVideoIds = useMemo(() => extractYoutubeVideoIdsFromNodes(memo.nodes), [memo.nodes]);
|
||||
|
||||
// Initial related data: creator.
|
||||
useAsyncEffect(async () => {
|
||||
const user = await userStore.getOrFetchUserByName(memo.creator);
|
||||
|
|
@ -246,6 +250,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
parentPage={parentPage}
|
||||
/>
|
||||
{memo.location && <MemoLocationView location={memo.location} />}
|
||||
<MemoYoutubeEmbedListView videoIds={youtubeVideoIds} />
|
||||
<MemoAttachmentListView attachments={memo.attachments} />
|
||||
<MemoRelationListView memo={memo} relations={referencedMemos} parentPage={parentPage} />
|
||||
<MemoReactionistView memo={memo} reactions={memo.reactions} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { memo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
videoIds: string[];
|
||||
}
|
||||
|
||||
const MemoYoutubeEmbedListView: React.FC<Props> = ({ videoIds }: Props) => {
|
||||
if (!videoIds || videoIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const EmbedCard = ({ videoId, className }: { videoId: string; className?: string }) => {
|
||||
return (
|
||||
<div className={cn("relative w-full", className)}>
|
||||
<div className="relative w-full pt-[56.25%] rounded-lg overflow-hidden border border-border/60 bg-popover">
|
||||
<iframe
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
src={`https://www.youtube.com/embed/${videoId}`}
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
|
||||
{videoIds.map((id) => (
|
||||
<div key={id} className="w-80 flex flex-col justify-start items-start shrink-0">
|
||||
<EmbedCard videoId={id} className="max-h-64 grow" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MemoYoutubeEmbedListView);
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { Node, NodeType } from "@/types/proto/api/v1/markdown_service";
|
||||
|
||||
// Regular expressions to match various YouTube URL formats.
|
||||
const YOUTUBE_REGEXPS: RegExp[] = [
|
||||
/https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([^&\s]+)/i,
|
||||
/https?:\/\/(?:www\.)?youtu\.be\/([^?\s]+)/i,
|
||||
/https?:\/\/(?:www\.)?youtube\.com\/shorts\/([^?\s]+)/i,
|
||||
/https?:\/\/(?:www\.)?youtube\.com\/embed\/([^?\s]+)/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract the YouTube video ID from a given URL, if any.
|
||||
* @param url The URL string to parse.
|
||||
* @returns The video ID, or undefined if the URL is not a YouTube link.
|
||||
*/
|
||||
export const extractYoutubeIdFromUrl = (url: string): string | undefined => {
|
||||
for (const regexp of YOUTUBE_REGEXPS) {
|
||||
const match = url.match(regexp);
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract YouTube video IDs from markdown nodes.
|
||||
* @param nodes The array of markdown nodes to extract YouTube video IDs from .
|
||||
* @returns A deduplicated array of YouTube video IDs.
|
||||
*/
|
||||
export const extractYoutubeVideoIdsFromNodes = (nodes: Node[]): string[] => {
|
||||
const ids = new Set<string>();
|
||||
|
||||
const isNodeArray = (value: unknown): value is Node[] =>
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
typeof value[0] === "object" &&
|
||||
value[0] !== null &&
|
||||
"type" in (value[0] as Record<string, unknown>);
|
||||
|
||||
// Collect all child Node instances nested anywhere inside the given node
|
||||
const collectChildren = (node: Node): Node[] => {
|
||||
const collected: Node[] = [];
|
||||
const queue: unknown[] = Object.values(node);
|
||||
|
||||
while (queue.length) {
|
||||
const item = queue.shift();
|
||||
if (!item) continue;
|
||||
|
||||
if (isNodeArray(item)) {
|
||||
collected.push(...item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
queue.push(...item);
|
||||
} else if (typeof item === "object") {
|
||||
queue.push(...Object.values(item as Record<string, unknown>));
|
||||
}
|
||||
}
|
||||
|
||||
return collected;
|
||||
};
|
||||
|
||||
const stack: Node[] = [...nodes];
|
||||
|
||||
while (stack.length) {
|
||||
const node = stack.pop()!;
|
||||
|
||||
if (node.type === NodeType.LINK && node.linkNode) {
|
||||
const id = extractYoutubeIdFromUrl(node.linkNode.url);
|
||||
if (id) ids.add(id);
|
||||
} else if (node.type === NodeType.AUTO_LINK && node.autoLinkNode) {
|
||||
const id = extractYoutubeIdFromUrl(node.autoLinkNode.url);
|
||||
if (id) ids.add(id);
|
||||
}
|
||||
|
||||
const children = collectChildren(node);
|
||||
if (children.length) {
|
||||
stack.push(...children);
|
||||
}
|
||||
}
|
||||
|
||||
return [...ids];
|
||||
};
|
||||
Loading…
Reference in New Issue