feat: render video as media item
This commit is contained in:
parent
420284978f
commit
8fb99ba435
|
|
@ -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 |
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue