feat: render video as media item

This commit is contained in:
DIYgod 2024-07-25 00:49:26 +08:00
parent 420284978f
commit 8fb99ba435
No known key found for this signature in database
9 changed files with 112 additions and 38 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path fill="#10161F" fill-rule="evenodd" d="M4.61 6.49c.243-1.11.747-2 1.784-2.6 1.037-.598 2.06-.59 3.143-.244.981.312 2.11.935 3.442 1.67.989.546 1.966 1.111 2.933 1.694 1.3.784 2.4 1.449 3.16 2.142.838.764 1.356 1.646 1.356 2.842 0 1.197-.519 2.079-1.357 2.842-.76.693-1.861 1.356-3.16 2.14a90.42 90.42 0 0 1-2.954 1.704c-1.324.732-2.447 1.352-3.425 1.663-1.08.343-2.1.35-3.136-.248s-1.541-1.485-1.784-2.591c-.22-1.003-.246-2.285-.275-3.797a88.093 88.093 0 0 1 0-3.403c.029-1.52.053-2.809.273-3.815" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 661 B

View File

@ -1,9 +1,8 @@
import type { MediaModel } from "@renderer/hono"
import { tipcClient } from "@renderer/lib/client"
import { getProxyUrl } from "@renderer/lib/img-proxy"
import { showNativeMenu } from "@renderer/lib/native-menu"
import { cn } from "@renderer/lib/utils"
import type { FC, ImgHTMLAttributes } from "react"
import type { FC, ImgHTMLAttributes, VideoHTMLAttributes } from "react"
import { memo, useState } from "react"
import { toast } from "sonner"
import { useEventCallback } from "usehooks-ts"
@ -11,15 +10,25 @@ import { useEventCallback } from "usehooks-ts"
import { usePreviewMedia } from "./media/hooks"
const failedList = new Set<string | undefined>()
export type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
export type ImageProps = (ImgHTMLAttributes<HTMLImageElement> & {
proxy?: {
width: number
height: number
}
disableContextMenu?: boolean
popper?: boolean
type?: MediaModel["type"]
}
type: "photo"
previewImageUrl?: string
}) | (VideoHTMLAttributes<HTMLVideoElement> & {
proxy?: {
width: number
height: number
}
disableContextMenu?: boolean
popper?: boolean
type: "video"
previewImageUrl?: string
})
const MediaImpl: FC<ImageProps> = ({
className,
proxy,
@ -27,7 +36,7 @@ const MediaImpl: FC<ImageProps> = ({
popper = false,
...props
}) => {
const { src, style, type, ...rest } = props
const { src, style, type, previewImageUrl, ...rest } = props
const [hidden, setHidden] = useState(!src)
const [imgSrc, setImgSrc] = useState(
proxy && src && !failedList.has(src) ?
@ -46,28 +55,28 @@ const MediaImpl: FC<ImageProps> = ({
failedList.add(props.src)
} else {
setHidden(true)
props.onError?.(e)
props.onError?.(e as any)
}
})
const previewMedia = usePreviewMedia()
const handleClick = useEventCallback(
(e: React.MouseEvent<HTMLImageElement>) => {
(e: React.MouseEvent) => {
if (popper && src) {
e.stopPropagation()
previewMedia([{
url: src,
type: "photo",
type,
}], 0)
}
props.onClick?.(e)
props.onClick?.(e as any)
},
)
return (
<div className={cn("overflow-hidden rounded", className)} style={style}>
{(!type || type === "photo") && (
{(type === "photo") && (
<img
{...rest}
{...rest as ImgHTMLAttributes<HTMLImageElement>}
onError={errorHandle}
className={cn(
hidden && "hidden",
@ -113,6 +122,32 @@ const MediaImpl: FC<ImageProps> = ({
{})}
/>
)}
{(type === "video") && (
<div
className={cn(
hidden && "hidden",
!(props.width || props.height) && "size-full",
"relative bg-stone-100 object-cover",
)}
onClick={handleClick}
>
{previewImageUrl ? (
<img
src={previewImageUrl}
className="size-full object-cover"
/>
) : (
<video
src={src}
muted
className="relative size-full object-cover"
/>
)}
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 rounded-full bg-black/50 p-2 text-3xl text-white/80">
<i className="i-mgc-play-cute-fi" />
</div>
</div>
)}
</div>
)
}

View File

@ -21,7 +21,7 @@ const Wrapper: Component<{
return (
<div className="center relative size-full p-12" onClick={dismiss}>
<m.div
className="size-full"
className="center size-full"
initial={{ scale: 0.94, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.94, opacity: 0 }}
@ -102,12 +102,23 @@ export const PreviewMediaContent: FC<{
const src = media[0].url
return (
<Wrapper src={src}>
<img
className="size-full object-contain"
alt="cover"
src={src}
onContextMenu={(e) => handleContextMenu(src, e)}
/>
{media[0].type === "video" ? (
<video
src={src}
controls
autoPlay
muted
className="object-contain"
onClick={(e) => e.stopPropagation()}
/>
) : (
<img
className="size-full object-contain"
alt="cover"
src={src}
onContextMenu={(e) => handleContextMenu(src, e)}
/>
)}
</Wrapper>
)
}
@ -134,13 +145,21 @@ export const PreviewMediaContent: FC<{
>
{media.map((med, index) => (
<SwiperSlide key={med.url} virtualIndex={index}>
<img
onContextMenu={(e) => handleContextMenu(med.url, e)}
className="size-full object-contain"
alt="cover"
src={med.url}
loading="lazy"
/>
{med.type === "video" ? (
<video
src={med.url}
controls
className="object-contain"
/>
) : (
<img
onContextMenu={(e) => handleContextMenu(med.url, e)}
className="size-full object-contain"
alt="cover"
src={med.url}
loading="lazy"
/>
)}
</SwiperSlide>
))}
</Swiper>

View File

@ -75,6 +75,7 @@ export function SwipeMedia({
alt="cover"
src={med.url}
type={med.type}
previewImageUrl={med.preview_image_url}
loading="lazy"
proxy={{
width: 600,
@ -115,6 +116,7 @@ export function SwipeMedia({
alt="cover"
src={uniqMedia[0].url}
type={uniqMedia[0].type}
previewImageUrl={uniqMedia[0].preview_image_url}
loading="lazy"
proxy={{
width: 600,

View File

@ -26,12 +26,20 @@ export const parseHtml = async (
.use(rehypeParse, { fragment: true })
.use(rehypeSanitize, {
...defaultSchema,
tagNames: renderInlineStyle ?
defaultSchema.tagNames :
[
...defaultSchema.tagNames!,
"video",
],
attributes: {
...defaultSchema.attributes,
"*": renderInlineStyle ?
[...defaultSchema.attributes!["*"], "style"] :
defaultSchema.attributes!["*"],
"video": ["src", "poster"],
},
})
.use(rehypeInferDescriptionMeta)
@ -58,8 +66,9 @@ export const parseHtml = async (
style: { maxWidth: "100%", display: "inline" },
})
}
return createElement(Media, { ...props, popper: true })
return createElement(Media, { ...props, popper: true, type: "photo" })
},
video: ({ node, ...props }) => createElement(Media, { ...props, popper: true, type: "video" }),
p: ({ node, ...props }) => {
if (node?.children) {
for (const item of node.children) {

View File

@ -180,6 +180,8 @@ export function DiscoverForm({ type }: { type: string }) {
{assertEntry.media?.[0] ? (
<Media
src={assertEntry.media?.[0].url}
type={assertEntry.media?.[0].type}
previewImageUrl={assertEntry.media?.[0].preview_image_url}
className="aspect-square w-full"
/>
) : (

View File

@ -115,6 +115,7 @@ export function ListItem({
<Media
src={entry.entries.media[0].url}
type={entry.entries.media[0].type}
previewImageUrl={entry.entries.media[0].preview_image_url}
className="ml-2 size-20 shrink-0"
loading="lazy"
proxy={{

View File

@ -73,6 +73,7 @@ export const SocialMediaItem: EntryListItemFC = ({
key={media.url}
src={media.url}
type={media.type}
previewImageUrl={media.preview_image_url}
className="size-28 shrink-0"
loading="lazy"
proxy={{

View File

@ -66,17 +66,21 @@ export function VideoItem({ entryId, entryPreview, translation }: UniversalItemP
className={cn("pointer-events-none aspect-video w-full shrink-0 rounded-md bg-black object-cover", isActive && "rounded-b-none")}
/>
) : (
<Media
key={entry.entries.media?.[0].url}
src={entry.entries.media?.[0].url}
className={cn("aspect-video w-full shrink-0 rounded-md object-cover", isActive && "rounded-b-none")}
loading="lazy"
proxy={{
width: 640,
height: 360,
}}
disableContextMenu
/>
entry.entries.media && (
<Media
key={entry.entries.media?.[0].url}
src={entry.entries.media?.[0].url}
type={entry.entries.media?.[0].type}
previewImageUrl={entry.entries.media?.[0].preview_image_url}
className={cn("aspect-video w-full shrink-0 rounded-md object-cover", isActive && "rounded-b-none")}
loading="lazy"
proxy={{
width: 640,
height: 360,
}}
disableContextMenu
/>
)
)}
</div>
</div>