Commit 124708f1 authored by boojack's avatar boojack

chore: refactor attachment media layout and insert menu organization

parent 7e21b728
...@@ -2,6 +2,7 @@ import { LatLng } from "leaflet"; ...@@ -2,6 +2,7 @@ import { LatLng } from "leaflet";
import { uniqBy } from "lodash-es"; import { uniqBy } from "lodash-es";
import { import {
FileIcon, FileIcon,
ImageIcon,
LinkIcon, LinkIcon,
LoaderIcon, LoaderIcon,
type LucideIcon, type LucideIcon,
...@@ -131,14 +132,22 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -131,14 +132,22 @@ const InsertMenu = (props: InsertMenuProps) => {
setMoreSubmenuOpen(false); setMoreSubmenuOpen(false);
}, [onToggleFocusMode]); }, [onToggleFocusMode]);
const handleMediaUploadClick = useCallback(() => {
handleUploadClick("image/*,video/*");
}, [handleUploadClick]);
const handleFileUploadClick = useCallback(() => {
handleUploadClick();
}, [handleUploadClick]);
const menuItems = useMemo( const menuItems = useMemo(
() => () =>
[ [
{ {
key: "upload", key: "upload-media",
label: t("editor.insert-menu.upload-file"), label: t("attachment-library.tabs.media"),
icon: FileIcon, icon: ImageIcon,
onClick: handleUploadClick, onClick: handleMediaUploadClick,
}, },
{ {
key: "record-audio", key: "record-audio",
...@@ -146,6 +155,12 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -146,6 +155,12 @@ const InsertMenu = (props: InsertMenuProps) => {
icon: MicIcon, icon: MicIcon,
onClick: () => props.onAudioRecorderClick?.(), onClick: () => props.onAudioRecorderClick?.(),
}, },
{
key: "upload-file",
label: t("common.file"),
icon: FileIcon,
onClick: handleFileUploadClick,
},
{ {
key: "link", key: "link",
label: t("editor.insert-menu.link-memo"), label: t("editor.insert-menu.link-memo"),
...@@ -159,7 +174,7 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -159,7 +174,7 @@ const InsertMenu = (props: InsertMenuProps) => {
onClick: handleLocationClick, onClick: handleLocationClick,
}, },
] satisfies Array<{ key: string; label: string; icon: LucideIcon; onClick: () => void }>, ] satisfies Array<{ key: string; label: string; icon: LucideIcon; onClick: () => void }>,
[handleLocationClick, handleOpenLinkDialog, handleUploadClick, props, t], [handleFileUploadClick, handleLocationClick, handleMediaUploadClick, handleOpenLinkDialog, props, t],
); );
return ( return (
...@@ -171,14 +186,14 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -171,14 +186,14 @@ const InsertMenu = (props: InsertMenuProps) => {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start"> <DropdownMenuContent align="start">
{menuItems.slice(0, 2).map((item) => ( {menuItems.slice(0, 3).map((item) => (
<DropdownMenuItem key={item.key} onClick={item.onClick}> <DropdownMenuItem key={item.key} onClick={item.onClick}>
<item.icon className="w-4 h-4" /> <item.icon className="w-4 h-4" />
{item.label} {item.label}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{menuItems.slice(2).map((item) => ( {menuItems.slice(3).map((item) => (
<DropdownMenuItem key={item.key} onClick={item.onClick}> <DropdownMenuItem key={item.key} onClick={item.onClick}>
<item.icon className="w-4 h-4" /> <item.icon className="w-4 h-4" />
{item.label} {item.label}
......
...@@ -26,8 +26,13 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void ...@@ -26,8 +26,13 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void
if (fileInputRef.current) fileInputRef.current.value = ""; if (fileInputRef.current) fileInputRef.current.value = "";
}; };
const handleUploadClick = () => { const handleUploadClick = (accept = "*") => {
fileInputRef.current?.click(); if (!fileInputRef.current) {
return;
}
fileInputRef.current.accept = accept;
fileInputRef.current.click();
}; };
return { return {
......
import { DownloadIcon, FileIcon, PaperclipIcon, PlayIcon } from "lucide-react"; import { DownloadIcon, FileIcon, PaperclipIcon, PlayIcon } from "lucide-react";
import type { PropsWithChildren } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import MetadataSection from "@/components/MemoMetadata/MetadataSection"; import MetadataSection from "@/components/MemoMetadata/MetadataSection";
import MotionPhotoPreview from "@/components/MotionPhotoPreview"; import MotionPhotoPreview from "@/components/MotionPhotoPreview";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentUrl } from "@/utils/attachment"; import { getAttachmentUrl } from "@/utils/attachment";
import type { PreviewMediaItem } from "@/utils/media-item"; import type { AttachmentVisualItem, PreviewMediaItem } from "@/utils/media-item";
import { buildAttachmentVisualItems } from "@/utils/media-item"; import { buildAttachmentVisualItems } from "@/utils/media-item";
import AudioAttachmentItem from "./AudioAttachmentItem"; import AudioAttachmentItem from "./AudioAttachmentItem";
import { getAttachmentMetadata, isAudioAttachment, separateAttachments } from "./attachmentHelpers"; import { getAttachmentMetadata, isAudioAttachment, separateAttachments } from "./attachmentHelpers";
...@@ -15,6 +16,17 @@ interface AttachmentListViewProps { ...@@ -15,6 +16,17 @@ interface AttachmentListViewProps {
onImagePreview?: (items: PreviewMediaItem[], index: number) => void; onImagePreview?: (items: PreviewMediaItem[], index: number) => void;
} }
type VisualItem = AttachmentVisualItem;
const VISUAL_TILE_CLASS =
"group relative overflow-hidden rounded-xl border border-border/70 bg-muted/30 text-left transition-colors hover:border-accent/40";
const COVER_MEDIA_CLASS = "h-full w-full rounded-none object-cover transition-transform duration-300 group-hover:scale-[1.02]";
const NATURAL_MEDIA_CLASS =
"block h-auto max-h-[20rem] w-auto max-w-full rounded-none transition-transform duration-300 group-hover:scale-[1.02]";
const SINGLE_VIDEO_CARD_WIDTH_CLASS = "w-full max-w-[30rem]";
const TWO_ITEM_GRID_HEIGHT_CLASS = "h-[11rem] sm:h-[13rem] md:h-[15rem]";
const MOSAIC_GRID_HEIGHT_CLASS = "h-[13rem] sm:h-[16rem] md:h-[18rem]";
const AttachmentMeta = ({ attachment }: { attachment: Attachment }) => { const AttachmentMeta = ({ attachment }: { attachment: Attachment }) => {
const { fileTypeLabel, fileSizeLabel } = getAttachmentMetadata(attachment); const { fileTypeLabel, fileSizeLabel } = getAttachmentMetadata(attachment);
...@@ -50,89 +62,175 @@ const DocumentItem = ({ attachment }: { attachment: Attachment }) => { ...@@ -50,89 +62,175 @@ const DocumentItem = ({ attachment }: { attachment: Attachment }) => {
); );
}; };
const MotionItem = ({ const getMotionPreviewProps = (item: VisualItem) => ({
motionUrl: item.previewItem.kind === "motion" ? item.previewItem.motionUrl : item.sourceUrl,
presentationTimestampUs: item.previewItem.kind === "motion" ? item.previewItem.presentationTimestampUs : undefined,
});
const VisualTile = ({
className,
onPreview,
overlayLabel,
children,
}: PropsWithChildren<{ className?: string; onPreview?: () => void; overlayLabel?: string }>) => {
return (
<button type="button" className={cn(VISUAL_TILE_CLASS, className)} onClick={onPreview}>
{children}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-foreground/15 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
{overlayLabel && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-black/45 text-2xl font-semibold text-white backdrop-blur-[2px]">
{overlayLabel}
</div>
)}
</button>
);
};
const VideoPlayBadge = ({ className, children }: PropsWithChildren<{ className?: string }>) => (
<span
className={cn(
"pointer-events-none absolute inline-flex items-center justify-center rounded-full bg-background/85 text-foreground shadow-sm backdrop-blur-sm",
className,
)}
>
{children}
</span>
);
const CollageVisualItem = ({
item, item,
featured = false,
onPreview, onPreview,
className,
overlayLabel,
}: { }: {
item: ReturnType<typeof buildAttachmentVisualItems>[number]; item: VisualItem;
featured?: boolean;
onPreview?: () => void; onPreview?: () => void;
className?: string;
overlayLabel?: string;
}) => { }) => {
const motionPreviewProps = item.kind === "motion" ? getMotionPreviewProps(item) : undefined;
return ( return (
<button <VisualTile className={cn("block h-full w-full", className)} onPreview={onPreview} overlayLabel={overlayLabel}>
type="button" {item.kind === "video" ? (
className={cn("group block w-full text-left", featured ? "max-w-[18rem] sm:max-w-[20rem]" : "")} <>
onClick={onPreview} <video src={item.sourceUrl} className={COVER_MEDIA_CLASS} preload="metadata" />
> {!overlayLabel && (
<div <VideoPlayBadge className="bottom-2 right-2 h-7 w-7 bg-background/80 text-foreground/70">
className={cn( <PlayIcon className="h-3.5 w-3.5 fill-current" />
"relative overflow-hidden rounded-xl border border-border/70 bg-muted/30 transition-colors hover:border-accent/40", </VideoPlayBadge>
featured ? "aspect-[4/3]" : "aspect-square", )}
)} </>
> ) : item.kind === "motion" && motionPreviewProps ? (
{item.kind === "video" ? ( <MotionPhotoPreview
<video posterUrl={item.posterUrl}
src={item.sourceUrl} motionUrl={motionPreviewProps.motionUrl}
className="h-full w-full rounded-none object-cover transition-transform duration-300 group-hover:scale-[1.02]" alt={item.filename}
preload="metadata" presentationTimestampUs={motionPreviewProps.presentationTimestampUs}
/> containerClassName="h-full w-full"
) : item.kind === "motion" ? ( badgeClassName="left-2 top-2 px-2 py-0.5 text-[10px]"
<MotionPhotoPreview mediaClassName={COVER_MEDIA_CLASS}
posterUrl={item.posterUrl} />
motionUrl={item.previewItem.kind === "motion" ? item.previewItem.motionUrl : item.sourceUrl} ) : (
alt={item.filename} <img src={item.posterUrl} alt={item.filename} className={COVER_MEDIA_CLASS} loading="lazy" decoding="async" />
presentationTimestampUs={item.previewItem.kind === "motion" ? item.previewItem.presentationTimestampUs : undefined} )}
containerClassName="h-full w-full" </VisualTile>
badgeClassName="left-2 top-2 px-2 py-0.5 text-[10px]" );
mediaClassName="h-full w-full rounded-none object-cover transition-transform duration-300 group-hover:scale-[1.02]" };
/>
) : ( const SingleVisualItem = ({ item, onPreview }: { item: VisualItem; onPreview?: () => void }) => {
<img const motionPreviewProps = item.kind === "motion" ? getMotionPreviewProps(item) : undefined;
src={item.posterUrl}
alt={item.filename} if (item.kind === "image") {
className="h-full w-full rounded-none object-cover transition-transform duration-300 group-hover:scale-[1.02]" return (
loading="lazy" <VisualTile className="inline-block max-w-full" onPreview={onPreview}>
decoding="async" <img src={item.posterUrl} alt={item.filename} className={NATURAL_MEDIA_CLASS} loading="lazy" decoding="async" />
/> </VisualTile>
)} );
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-foreground/15 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100" /> }
{item.kind === "video" && (
<span className="pointer-events-none absolute bottom-2 right-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-background/80 text-foreground/70 backdrop-blur-sm"> if (item.kind === "motion" && motionPreviewProps) {
<PlayIcon className="h-3.5 w-3.5 fill-current" /> return (
</span> <VisualTile className="inline-block max-w-full" onPreview={onPreview}>
)} <MotionPhotoPreview
posterUrl={item.posterUrl}
motionUrl={motionPreviewProps.motionUrl}
alt={item.filename}
presentationTimestampUs={motionPreviewProps.presentationTimestampUs}
containerClassName="max-w-full"
posterClassName={cn(NATURAL_MEDIA_CLASS, "object-contain")}
videoClassName="absolute inset-0 h-full w-full rounded-none object-contain transition-transform duration-300 group-hover:scale-[1.02]"
badgeClassName="left-2 top-2 px-2 py-0.5 text-[10px]"
/>
</VisualTile>
);
}
return (
<VisualTile className={cn("block", SINGLE_VIDEO_CARD_WIDTH_CLASS)} onPreview={onPreview}>
<div className="relative aspect-video bg-black/5">
<video src={item.sourceUrl} poster={item.posterUrl} className={COVER_MEDIA_CLASS} preload="metadata" />
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/35 via-black/5 to-transparent" />
<VideoPlayBadge className="bottom-3 right-3 h-9 w-9">
<PlayIcon className="h-4 w-4 fill-current" />
</VideoPlayBadge>
</div> </div>
</button> </VisualTile>
); );
}; };
const VisualGallery = ({ const VisualGallery = ({ items, onPreview }: { items: VisualItem[]; onPreview?: (itemId: string) => void }) => {
items, if (items.length === 0) {
onPreview, return null;
}: { }
items: ReturnType<typeof buildAttachmentVisualItems>;
onPreview?: (itemId: string) => void;
}) => {
if (items.length === 1) { if (items.length === 1) {
return ( return (
<div className="flex"> <div className="w-full">
<MotionItem item={items[0]} featured onPreview={() => onPreview?.(items[0].id)} /> <SingleVisualItem item={items[0]} onPreview={() => onPreview?.(items[0].id)} />
</div>
);
}
if (items.length === 2) {
return (
<div className={cn("grid grid-cols-2 gap-2", TWO_ITEM_GRID_HEIGHT_CLASS)}>
{items.map((item) => (
<CollageVisualItem key={item.id} item={item} onPreview={() => onPreview?.(item.id)} />
))}
</div> </div>
); );
} }
if (items.length === 3) {
return (
<div className={cn("grid grid-cols-2 grid-rows-2 gap-2", MOSAIC_GRID_HEIGHT_CLASS)}>
<CollageVisualItem item={items[0]} className="row-span-2" onPreview={() => onPreview?.(items[0].id)} />
<CollageVisualItem item={items[1]} onPreview={() => onPreview?.(items[1].id)} />
<CollageVisualItem item={items[2]} onPreview={() => onPreview?.(items[2].id)} />
</div>
);
}
const visibleItems = items.slice(0, 4);
const remainingCount = items.length - visibleItems.length;
return ( return (
<div className="grid max-w-[22rem] grid-cols-2 gap-1.5 sm:max-w-[24rem]"> <div className={cn("grid grid-cols-2 grid-rows-2 gap-2", MOSAIC_GRID_HEIGHT_CLASS)}>
{items.map((item) => ( {visibleItems.map((item, index) => (
<MotionItem key={item.id} item={item} onPreview={() => onPreview?.(item.id)} /> <CollageVisualItem
key={item.id}
item={item}
overlayLabel={index === visibleItems.length - 1 && remainingCount > 0 ? `+${remainingCount}` : undefined}
onPreview={() => onPreview?.(item.id)}
/>
))} ))}
</div> </div>
); );
}; };
const AudioList = ({ attachments }: { attachments: Attachment[] }) => ( const AudioList = ({ attachments, compact = false }: { attachments: Attachment[]; compact?: boolean }) => (
<div className="flex flex-col gap-2"> <div className={cn("gap-2", compact ? "grid grid-cols-1 sm:grid-cols-2" : "flex flex-col")}>
{attachments.map((attachment) => ( {attachments.map((attachment) => (
<AudioAttachmentItem <AudioAttachmentItem
key={attachment.name} key={attachment.name}
...@@ -140,6 +238,7 @@ const AudioList = ({ attachments }: { attachments: Attachment[] }) => ( ...@@ -140,6 +238,7 @@ const AudioList = ({ attachments }: { attachments: Attachment[] }) => (
sourceUrl={getAttachmentUrl(attachment)} sourceUrl={getAttachmentUrl(attachment)}
mimeType={attachment.type} mimeType={attachment.type}
size={Number(attachment.size)} size={Number(attachment.size)}
compact={compact}
/> />
))} ))}
</div> </div>
...@@ -164,7 +263,7 @@ const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewP ...@@ -164,7 +263,7 @@ const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewP
const hasVisual = visualItems.length > 0; const hasVisual = visualItems.length > 0;
const hasAudio = audio.length > 0; const hasAudio = audio.length > 0;
const hasDocs = docs.length > 0; const hasDocs = docs.length > 0;
const sectionCount = [hasVisual, hasAudio, hasDocs].filter(Boolean).length; const hasMedia = hasVisual || hasAudio;
if (attachments.length === 0) { if (attachments.length === 0) {
return null; return null;
...@@ -182,10 +281,13 @@ const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewP ...@@ -182,10 +281,13 @@ const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewP
count={visualItems.length + audio.length + docs.length} count={visualItems.length + audio.length + docs.length}
contentClassName="flex flex-col gap-2 p-2" contentClassName="flex flex-col gap-2 p-2"
> >
{hasVisual && <VisualGallery items={visualItems} onPreview={handlePreview} />} {hasMedia && (
{hasVisual && sectionCount > 1 && <Divider />} <div className="flex flex-col gap-2">
{hasAudio && <AudioList attachments={audio.filter(isAudioAttachment)} />} {hasVisual && <VisualGallery items={visualItems} onPreview={handlePreview} />}
{hasAudio && hasDocs && <Divider />} {hasAudio && <AudioList attachments={audio.filter(isAudioAttachment)} compact />}
</div>
)}
{hasMedia && hasDocs && <Divider />}
{hasDocs && <DocsList attachments={docs} />} {hasDocs && <DocsList attachments={docs} />}
</MetadataSection> </MetadataSection>
); );
......
import { PauseIcon, PlayIcon } from "lucide-react"; import { PauseIcon, PlayIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { formatFileSize, getFileTypeLabel } from "@/utils/format"; import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import { formatAudioTime } from "./attachmentHelpers"; import { formatAudioTime } from "./attachmentHelpers";
...@@ -56,9 +57,11 @@ interface AudioAttachmentItemProps { ...@@ -56,9 +57,11 @@ interface AudioAttachmentItemProps {
mimeType: string; mimeType: string;
size?: number; size?: number;
title?: string; title?: string;
compact?: boolean;
className?: string;
} }
const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: AudioAttachmentItemProps) => { const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title, compact = false, className }: AudioAttachmentItemProps) => {
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
...@@ -119,9 +122,9 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud ...@@ -119,9 +122,9 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
}; };
return ( return (
<div className="rounded-xl border border-border/40 bg-background/75 px-2 py-1.5"> <div className={cn("rounded-xl border border-border/40 bg-background/75", compact ? "px-3 py-2.5" : "px-2 py-1.5", className)}>
<div className="flex items-center justify-between gap-1.5"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex flex-1 items-baseline gap-1"> <div className={cn("min-w-0 flex flex-1", compact ? "flex-col gap-0.5" : "items-baseline gap-1")}>
<div className="truncate text-sm font-medium leading-5 text-foreground" title={filename}> <div className="truncate text-sm font-medium leading-5 text-foreground" title={filename}>
{displayTitle} {displayTitle}
</div> </div>
...@@ -141,7 +144,7 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud ...@@ -141,7 +144,7 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
</button> </button>
</div> </div>
<div className="mt-1 flex items-center gap-1"> <div className={cn("mt-1", compact ? "space-y-1.5" : "flex items-center gap-1")}>
<AudioProgressBar <AudioProgressBar
filename={filename} filename={filename}
currentTime={currentTime} currentTime={currentTime}
...@@ -151,16 +154,18 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud ...@@ -151,16 +154,18 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
className="min-w-0 flex-1" className="min-w-0 flex-1"
/> />
<div className="shrink-0 text-[10px] tabular-nums text-muted-foreground">{timeLabel}</div> <div className={cn("flex items-center", compact ? "justify-between gap-2" : "shrink-0 gap-1")}>
<div className="shrink-0 text-[10px] tabular-nums text-muted-foreground">{timeLabel}</div>
<button
type="button" <button
onClick={handlePlaybackRateChange} type="button"
className="inline-flex h-5 shrink-0 items-center justify-center rounded-md border border-transparent px-1 text-[10px] font-medium text-muted-foreground transition-colors hover:border-border/40 hover:text-foreground" onClick={handlePlaybackRateChange}
aria-label={`Playback speed ${playbackRate}x for ${displayTitle}`} className="inline-flex h-5 shrink-0 items-center justify-center rounded-md border border-transparent px-1 text-[10px] font-medium text-muted-foreground transition-colors hover:border-border/40 hover:text-foreground"
> aria-label={`Playback speed ${playbackRate}x for ${displayTitle}`}
{playbackRate}x >
</button> {playbackRate}x
</button>
</div>
</div> </div>
<audio <audio
......
...@@ -8,6 +8,8 @@ interface MotionPhotoPlayerProps { ...@@ -8,6 +8,8 @@ interface MotionPhotoPlayerProps {
presentationTimestampUs?: bigint; presentationTimestampUs?: bigint;
containerClassName?: string; containerClassName?: string;
mediaClassName?: string; mediaClassName?: string;
posterClassName?: string;
videoClassName?: string;
active?: boolean; active?: boolean;
loop?: boolean; loop?: boolean;
} }
...@@ -19,6 +21,8 @@ const MotionPhotoPlayer = ({ ...@@ -19,6 +21,8 @@ const MotionPhotoPlayer = ({
presentationTimestampUs, presentationTimestampUs,
containerClassName, containerClassName,
mediaClassName, mediaClassName,
posterClassName,
videoClassName,
active, active,
loop = false, loop = false,
}: MotionPhotoPlayerProps) => { }: MotionPhotoPlayerProps) => {
...@@ -87,7 +91,7 @@ const MotionPhotoPlayer = ({ ...@@ -87,7 +91,7 @@ const MotionPhotoPlayer = ({
<img <img
src={posterUrl} src={posterUrl}
alt={alt} alt={alt}
className={cn("block max-h-full max-w-full select-none object-cover", mediaClassName)} className={cn("block max-h-full max-w-full select-none object-cover", mediaClassName, posterClassName)}
draggable={false} draggable={false}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
...@@ -100,6 +104,7 @@ const MotionPhotoPlayer = ({ ...@@ -100,6 +104,7 @@ const MotionPhotoPlayer = ({
"pointer-events-none absolute inset-0 h-full w-full object-cover transition-opacity duration-200", "pointer-events-none absolute inset-0 h-full w-full object-cover transition-opacity duration-200",
isPlaying ? "opacity-100" : "opacity-0", isPlaying ? "opacity-100" : "opacity-0",
mediaClassName, mediaClassName,
videoClassName,
)} )}
muted muted
playsInline playsInline
......
...@@ -9,6 +9,8 @@ interface MotionPhotoPreviewProps { ...@@ -9,6 +9,8 @@ interface MotionPhotoPreviewProps {
presentationTimestampUs?: bigint; presentationTimestampUs?: bigint;
containerClassName?: string; containerClassName?: string;
mediaClassName?: string; mediaClassName?: string;
posterClassName?: string;
videoClassName?: string;
badgeClassName?: string; badgeClassName?: string;
loop?: boolean; loop?: boolean;
} }
...@@ -20,6 +22,8 @@ const MotionPhotoPreview = ({ ...@@ -20,6 +22,8 @@ const MotionPhotoPreview = ({
presentationTimestampUs, presentationTimestampUs,
containerClassName, containerClassName,
mediaClassName, mediaClassName,
posterClassName,
videoClassName,
badgeClassName, badgeClassName,
loop = false, loop = false,
}: MotionPhotoPreviewProps) => { }: MotionPhotoPreviewProps) => {
...@@ -40,6 +44,8 @@ const MotionPhotoPreview = ({ ...@@ -40,6 +44,8 @@ const MotionPhotoPreview = ({
loop={loop} loop={loop}
containerClassName={cn("max-w-full max-h-full", containerClassName)} containerClassName={cn("max-w-full max-h-full", containerClassName)}
mediaClassName={mediaClassName} mediaClassName={mediaClassName}
posterClassName={posterClassName}
videoClassName={videoClassName}
/> />
<div <div
role="button" role="button"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment