From f5e858659f62fcd40a8e5a671d5e8d51e29a34f5 Mon Sep 17 00:00:00 2001 From: TerryM Date: Wed, 27 May 2026 15:28:51 +0800 Subject: [PATCH] feat: add media overflow pickers --- .../messageStream/bubbles/AlbumBubble.tsx | 144 ++++++++++++++- .../messageStream/bubbles/VideoBubble.tsx | 173 +++++++++++++++++- 2 files changed, 311 insertions(+), 6 deletions(-) diff --git a/src/components/messageStream/bubbles/AlbumBubble.tsx b/src/components/messageStream/bubbles/AlbumBubble.tsx index 0040cc5..af4d934 100644 --- a/src/components/messageStream/bubbles/AlbumBubble.tsx +++ b/src/components/messageStream/bubbles/AlbumBubble.tsx @@ -1,7 +1,12 @@ +import { ArrowDownToLine, LoaderCircle, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; import { useI18n } from "../../../i18n"; import type { Attachment, Post } from "../../../types/post"; import { useLightbox } from "../overlays/ImageLightbox"; import { autolink } from "../utils/autolink"; +import { downloadAttachment } from "../utils/downloadFile"; +import { formatBytes } from "../utils/formatBytes"; import { postDisplayText } from "../utils/postText"; const MAX_VISIBLE = 4; @@ -10,9 +15,130 @@ function imageRatio(att: Attachment) { return att.width && att.height ? `${att.width} / ${att.height}` : "4 / 3"; } +function ImageListDownloadButton({ + postId, + attachment, +}: { + postId: string; + attachment: Attachment; +}) { + const { t } = useI18n(); + const [isDownloading, setIsDownloading] = useState(false); + + const handleDownload = () => { + if (isDownloading) return; + setIsDownloading(true); + void downloadAttachment(postId, attachment.id, attachment.filename) + .finally(() => setIsDownloading(false)) + .catch(() => {}); + }; + + return ( + + ); +} + +function ImageListDialog({ + postId, + images, + onClose, + onPick, +}: { + postId: string; + images: Attachment[]; + onClose: () => void; + onPick: (index: number) => void; +}) { + useEffect(() => { + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose]); + + return createPortal( +
+
e.stopPropagation()} + > +
+
选择图片
+ +
+
+ {images.map((image, index) => ( +
+ + +
+ ))} +
+
+
, + document.body, + ); +} + export function AlbumBubble({ post }: { post: Post }) { const { openLightbox } = useLightbox(); const { lang } = useI18n(); + const [listOpen, setListOpen] = useState(false); const images = post.attachments; const text = postDisplayText(post, lang); const shouldMerge = images.length > MAX_VISIBLE; @@ -58,9 +184,12 @@ export function AlbumBubble({ post }: { post: Post }) { @@ -140,8 +145,156 @@ function VideoAttachmentCard({ ); } +function AttachmentListDownloadButton({ + postId, + attachment, +}: { + postId: string; + attachment: Attachment; +}) { + const { t } = useI18n(); + const [isDownloading, setIsDownloading] = useState(false); + + const handleDownload = () => { + if (isDownloading) return; + setIsDownloading(true); + void downloadAttachment(postId, attachment.id, attachment.filename) + .finally(() => setIsDownloading(false)) + .catch(() => {}); + }; + + return ( + + ); +} + +function VideoListDialog({ + postId, + videos, + onClose, + onPick, +}: { + postId: string; + videos: Attachment[]; + onClose: () => void; + onPick: (attachment: Attachment) => void; +}) { + useEffect(() => { + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose]); + + return createPortal( +
+
e.stopPropagation()} + > +
+
选择视频
+ +
+
+ {videos.map((video, index) => { + const thumb = video.posterUrl ?? video.thumbnailUrl; + const previewVideoUrl = video.url.includes("#") + ? video.url + : `${video.url}#t=0.1`; + const duration = formatDuration(video.durationSec); + return ( +
+ + +
+ ); + })} +
+
+
, + document.body, + ); +} + export function VideoBubble({ post }: { post: Post }) { const { lang } = useI18n(); + const { openVideo } = useVideoPlayer(); + const [listOpen, setListOpen] = useState(false); const videos = post.attachments.filter(isVideoAttachment); const text = postDisplayText(post, lang); const shouldMerge = videos.length > MAX_VISIBLE; @@ -163,6 +316,7 @@ export function VideoBubble({ post }: { post: Post }) { attachment={att} compact overlayCount={isLastSlot ? extra : undefined} + onMoreClick={() => setListOpen(true)} /> ); })} @@ -172,6 +326,17 @@ export function VideoBubble({ post }: { post: Post }) { {autolink(text)} ) : null} + {listOpen ? ( + setListOpen(false)} + onPick={(att) => { + setListOpen(false); + openVideo(att, 0); + }} + /> + ) : null} ); }