From a0d83e1a9e9d0ba50a0103e594adb011728faf8c Mon Sep 17 00:00:00 2001 From: memoclaw <265580040+memoclaw@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:40:36 +0800 Subject: [PATCH] fix(web): refine attachment media layout --- .../Attachment/AttachmentCard.tsx | 11 +- .../Attachment/AttachmentListView.tsx | 149 +++++++++++++----- 2 files changed, 118 insertions(+), 42 deletions(-) diff --git a/web/src/components/MemoMetadata/Attachment/AttachmentCard.tsx b/web/src/components/MemoMetadata/Attachment/AttachmentCard.tsx index c5ce9dc96..df66880a2 100644 --- a/web/src/components/MemoMetadata/Attachment/AttachmentCard.tsx +++ b/web/src/components/MemoMetadata/Attachment/AttachmentCard.tsx @@ -1,6 +1,6 @@ import { cn } from "@/lib/utils"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; -import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; +import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; interface AttachmentCardProps { attachment: Attachment; @@ -15,10 +15,17 @@ const AttachmentCard = ({ attachment, onClick, className }: AttachmentCardProps) if (attachmentType === "image/*") { return ( {attachment.filename} { + const target = e.currentTarget; + if (target.src.includes("?thumbnail=true")) { + target.src = sourceUrl; + } + }} + decoding="async" loading="lazy" /> ); diff --git a/web/src/components/MemoMetadata/Attachment/AttachmentListView.tsx b/web/src/components/MemoMetadata/Attachment/AttachmentListView.tsx index 4ffaa3cef..5cfbe3340 100644 --- a/web/src/components/MemoMetadata/Attachment/AttachmentListView.tsx +++ b/web/src/components/MemoMetadata/Attachment/AttachmentListView.tsx @@ -1,4 +1,4 @@ -import { FileIcon, PaperclipIcon } from "lucide-react"; +import { DownloadIcon, FileIcon, Maximize2Icon, PaperclipIcon, PlayIcon } from "lucide-react"; import { useMemo } from "react"; import { cn } from "@/lib/utils"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; @@ -6,75 +6,144 @@ import { getAttachmentUrl } from "@/utils/attachment"; import SectionHeader from "../SectionHeader"; import AttachmentCard from "./AttachmentCard"; import AudioAttachmentItem from "./AudioAttachmentItem"; -import { getAttachmentMetadata, isImageAttachment, separateAttachments } from "./attachmentViewHelpers"; +import { getAttachmentMetadata, isImageAttachment, isVideoAttachment, separateAttachments } from "./attachmentViewHelpers"; interface AttachmentListViewProps { attachments: Attachment[]; onImagePreview?: (urls: string[], index: number) => void; } -const DocumentItem = ({ attachment }: { attachment: Attachment }) => { +const AttachmentMeta = ({ attachment }: { attachment: Attachment }) => { const { fileTypeLabel, fileSizeLabel } = getAttachmentMetadata(attachment); return ( -
-
- -
-
- - {attachment.filename} - -
- - {fileTypeLabel} - {fileSizeLabel && ( - <> - - {fileSizeLabel} - - )} +
+ {fileTypeLabel} + {fileSizeLabel && ( + <> + + {fileSizeLabel} + + )} +
+ ); +}; + +const DocumentItem = ({ attachment }: { attachment: Attachment }) => { + return ( +
+
+
+ +
+
+
+ {attachment.filename} +
+
+
); }; interface VisualItemProps { attachment: Attachment; - onImageClick?: (url: string) => void; + featured?: boolean; } -const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => { - const isInteractive = isImageAttachment(attachment) && Boolean(onImageClick); - +const ImageItem = ({ attachment, onImageClick, featured = false }: VisualItemProps & { onImageClick?: (url: string) => void }) => { const handleClick = () => { - if (isInteractive) { - onImageClick?.(getAttachmentUrl(attachment)); - } + onImageClick?.(getAttachmentUrl(attachment)); }; return ( -
- +
+ + +
+ + + +
+ + ); +}; + +const ImageGallery = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick?: (url: string) => void }) => { + if (attachments.length === 1) { + return ( +
+ +
+ ); + } + + return ( +
+ {attachments.map((attachment) => ( + + ))}
); }; -const VisualGrid = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick?: (url: string) => void }) => ( -
+const VideoItem = ({ attachment }: VisualItemProps) => ( +
+
+ + + + +
+
+
+ {attachment.filename} +
+ +
+
+); + +const VideoList = ({ attachments }: { attachments: Attachment[] }) => ( +
{attachments.map((attachment) => ( - + ))}
); +const VisualSection = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick?: (url: string) => void }) => { + const images = attachments.filter(isImageAttachment); + const videos = attachments.filter(isVideoAttachment); + + return ( +
+ {images.length > 0 && } + {videos.length > 0 && ( +
+ {images.length > 0 && } + +
+ )} +
+ ); +}; + const AudioList = ({ attachments }: { attachments: Attachment[] }) => (