import { useEffect, useRef, useState } from "react" import type { CSSProperties } from "react" import { createPortal } from "react-dom" import { AnimatePresence, motion } from "motion/react" import { ChevronLeft, ChevronRight, X } from "lucide-react" import { Button } from "@/components/ui/button" import { useCloudImageSrc } from "@/hooks/use-cloud-image-src" import { cn } from "@/lib/utils" import { useTranslation } from "react-i18next" import type { AlbumPhoto } from "./utils" type AlbumPreviewProps = { isOpen: boolean images: AlbumPhoto[] activeId: number | null onClose: () => void onActiveIdChange: (id: number) => void } export function AlbumPreview({ isOpen, images, activeId, onClose, onActiveIdChange, }: AlbumPreviewProps) { const { t } = useTranslation() const [direction, setDirection] = useState(0) const [isSlideMode, setIsSlideMode] = useState(false) const pendingCloseRef = useRef(false) const activeIndex = images.findIndex((image) => image.id === activeId) const resolvedIndex = activeIndex >= 0 ? activeIndex : 0 const activeImage = images[resolvedIndex] const canNavigate = images.length > 1 const isSingle = images.length === 1 const isActive = isOpen && Boolean(activeImage) const portalRoot = typeof document === "undefined" ? null : document.body const activeImageName = activeImage?.name ?? "" const countLabel = t("ALBUM_PREVIEW_COUNT", { current: resolvedIndex + 1, total: images.length, }) const activeImageUrl = activeImage ? activeImage.imgUrl ?? activeImage.originImgUrl ?? "" : "" const { src: resolvedImageSrc, onError: handleCloudImageError } = useCloudImageSrc( activeImage?.type, activeImageUrl ) const handleClose = () => { setDirection(0) if (isSingle || !isSlideMode) { onClose() return } pendingCloseRef.current = true setIsSlideMode(false) } const handleSlideExitComplete = () => { if (!pendingCloseRef.current) return pendingCloseRef.current = false onClose() } const goToIndex = (index: number) => { const nextImage = images[index] if (!nextImage) return onActiveIdChange(nextImage.id) } const navigateToIndex = (nextIndex: number, nextDirection: number) => { setDirection(nextDirection) setIsSlideMode(true) goToIndex(nextIndex) } const handlePrev = () => { if (!canNavigate) return const nextIndex = (resolvedIndex - 1 + images.length) % images.length navigateToIndex(nextIndex, -1) } const handleNext = () => { if (!canNavigate) return const nextIndex = (resolvedIndex + 1) % images.length navigateToIndex(nextIndex, 1) } useEffect(() => { if (!isActive) return const originalOverflow = document.body.style.overflow const originalPadding = document.body.style.paddingRight const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth document.body.style.overflow = "hidden" if (scrollbarWidth > 0) { document.body.style.paddingRight = `${scrollbarWidth}px` } return () => { document.body.style.overflow = originalOverflow document.body.style.paddingRight = originalPadding } }, [isActive]) useEffect(() => { if (!isOpen) { pendingCloseRef.current = false } }, [isOpen]) useEffect(() => { if (!isActive) return const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault() setDirection(0) if (isSingle || !isSlideMode) { onClose() return } pendingCloseRef.current = true setIsSlideMode(false) return } if (event.key === "ArrowLeft") { event.preventDefault() if (!canNavigate) return const nextIndex = (resolvedIndex - 1 + images.length) % images.length const nextImage = images[nextIndex] if (nextImage) { setDirection(-1) setIsSlideMode(true) onActiveIdChange(nextImage.id) } return } if (event.key === "ArrowRight") { event.preventDefault() if (!canNavigate) return const nextIndex = (resolvedIndex + 1) % images.length const nextImage = images[nextIndex] if (nextImage) { setDirection(1) setIsSlideMode(true) onActiveIdChange(nextImage.id) } } } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) }, [ canNavigate, images, isActive, isSingle, isSlideMode, onActiveIdChange, onClose, resolvedIndex, ]) if (!portalRoot) return null return createPortal( {isActive ? (
{activeImage.isVideo ? ( event.stopPropagation()} /> ) : ( { event.stopPropagation() handleClose() }} /> )} {isSlideMode ? ( activeImage.isVideo ? ( ({ x: value > 0 ? 60 : value < 0 ? -60 : 0, opacity: value === 0 ? 1 : 0, }), center: { x: 0, opacity: 1 }, exit: (value: number) => ({ x: value > 0 ? -60 : value < 0 ? 60 : 0, opacity: value === 0 ? 1 : 0, }), }} initial="enter" animate="center" exit="exit" transition={{ duration: 0.26, ease: "easeOut" }} src={activeImageUrl} controls className="absolute inset-0 m-auto max-h-full max-w-full rounded-(--radius-gallery-preview) object-contain shadow-(--app-gallery-preview-shadow)" draggable={false} onClick={(event) => event.stopPropagation()} /> ) : ( ({ x: value > 0 ? 60 : value < 0 ? -60 : 0, opacity: value === 0 ? 1 : 0, }), center: { x: 0, opacity: 1 }, exit: (value: number) => ({ x: value > 0 ? -60 : value < 0 ? 60 : 0, opacity: value === 0 ? 1 : 0, }), }} initial="enter" animate="center" exit="exit" transition={{ duration: 0.26, ease: "easeOut" }} src={resolvedImageSrc} alt={activeImage.alt ?? ""} className="absolute inset-0 m-auto max-h-full max-w-full rounded-(--radius-gallery-preview) object-contain shadow-(--app-gallery-preview-shadow)" loading="lazy" decoding="async" draggable={false} onError={handleCloudImageError} onClick={(event) => { event.stopPropagation() handleClose() }} /> ) ) : null}
{canNavigate ? (
) : null}
{activeImageName}
{countLabel}
) : null}
, portalRoot ) }