import { LoaderCircle, Play, X } from "lucide-react"; import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon"; import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useI18n } from "../../../i18n"; import type { Attachment, Post } from "../../../types/post"; import { AttachmentDownloadPill } from "../AttachmentDownloadPill"; import { useVideoPlayer } from "../overlays/VideoPlayer"; import { autolink } from "../utils/autolink"; import { downloadAttachment } from "../utils/downloadFile"; import { formatBytes } from "../utils/formatBytes"; import { postDisplayText } from "../utils/postText"; import { useToast } from "../../Toast"; const MAX_VISIBLE = 4; function videoGridClass(count: number) { const height = "h-[230px] min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]"; if (count === 2) return `${height} grid grid-cols-1 grid-rows-2`; return `${height} grid grid-cols-2 grid-rows-2`; } function videoItemClass(index: number, count: number) { if (count === 3 && index === 0) return "row-span-2"; return ""; } function formatDuration(sec: number | undefined): string { if (!sec || sec <= 0) return ""; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s.toString().padStart(2, "0")}`; } function isVideoAttachment(att: Attachment): boolean { return att.kind === "video" || att.mime.startsWith("video/"); } function videoRatio(att: Attachment) { return att.width && att.height ? `${att.width} / ${att.height}` : "16 / 9"; } function VideoAttachmentCard({ postId, attachment, compact = false, overlayCount, onMoreClick, }: { postId: string; attachment: Attachment; compact?: boolean; overlayCount?: number; onMoreClick?: () => void; }) { const { openVideo } = useVideoPlayer(); const [playing, setPlaying] = useState(false); const videoRef = useRef(null); const posterUrl = attachment.posterUrl ?? attachment.thumbnailUrl; const duration = formatDuration(attachment.durationSec); const previewVideoUrl = attachment.url.includes("#") ? attachment.url : `${attachment.url}#t=0.1`; return (
{ if (playing) { const v = videoRef.current; openVideo(attachment, v?.currentTime ?? 0); } }} > {playing && !compact ? ( <>
); } function AttachmentListDownloadButton({ postId, attachment, }: { postId: string; attachment: Attachment; }) { const { t } = useI18n(); const { showToast } = useToast(); const [isDownloading, setIsDownloading] = useState(false); const handleDownload = async () => { if (isDownloading) return; setIsDownloading(true); try { await downloadAttachment(postId, attachment.id, attachment.filename); showToast(t("downloadOk")); } catch { showToast(t("downloadFail"), "error"); } finally { setIsDownloading(false); } }; 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); if (!videos.length) return null; if (videos.length >= 2) { const visible = videos.slice(0, MAX_VISIBLE); const extra = videos.length - MAX_VISIBLE; const layoutCount = Math.min(videos.length, MAX_VISIBLE); return (
{visible.map((att, i) => { const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0; return (
setListOpen(true)} />
); })}
{text ? (
{autolink(text)}
) : null} {listOpen ? ( setListOpen(false)} onPick={(att) => { setListOpen(false); openVideo(att, 0); }} /> ) : null}
); } return (
{text ? (
{autolink(text)}
) : null}
); }