import { useEffect, useLayoutEffect, useRef, useCallback, useMemo, useState } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' import { ImageRecord, parseSearchValue, tileSizeForZoom, useGalleryStore } from '../store' import { BulkActionBar } from './BulkActionBar' import { ImageContextMenu } from './ImageContextMenu' import { GalleryEmptyState, GalleryLoadingState } from './gallery/GalleryEmptyState' import { ImageTile } from './gallery/ImageTile' const GAP = 6 export function Gallery() { const images = useGalleryStore((state) => state.images) const loadMoreImages = useGalleryStore((state) => state.loadMoreImages) const openImage = useGalleryStore((state) => state.openImage) const totalImages = useGalleryStore((state) => state.totalImages) const loadingImages = useGalleryStore((state) => state.loadingImages) const zoomPreset = useGalleryStore((state) => state.zoomPreset) const search = useGalleryStore((state) => state.search) const collectionTitle = useGalleryStore((state) => state.collectionTitle) const imageLoadError = useGalleryStore((state) => state.imageLoadError) const galleryScrollResetKey = useGalleryStore((state) => state.galleryScrollResetKey) const isSimilarResults = collectionTitle === 'Similar Images' const parsedSearch = parseSearchValue(search) const parentRef = useRef(null) const [containerWidth, setContainerWidth] = useState(0) const [contextMenu, setContextMenu] = useState<{ x: number y: number image: ImageRecord } | null>(null) useLayoutEffect(() => { const el = parentRef.current if (!el) return setContainerWidth(el.clientWidth) const ro = new ResizeObserver((entries) => { setContainerWidth(entries[0].contentRect.width) }) ro.observe(el) return () => ro.disconnect() }, []) const tileSize = tileSizeForZoom(zoomPreset) const cols = useMemo( () => Math.max(1, Math.floor((containerWidth - GAP) / (tileSize + GAP))), [containerWidth, tileSize] ) const rowCount = Math.ceil(images.length / cols) const estimateSize = useCallback(() => tileSize + GAP, [tileSize]) const virtualizer = useVirtualizer({ count: rowCount, getScrollElement: () => parentRef.current, estimateSize, overscan: 3, paddingStart: GAP, }) useEffect(() => { virtualizer.measure() }, [cols, virtualizer]) useEffect(() => { parentRef.current?.scrollTo({ top: 0, left: 0 }) }, [galleryScrollResetKey]) const handleScroll = useCallback(() => { const el = parentRef.current if (!el) return if (el.scrollTop < 24) return const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 600 if (nearBottom && !loadingImages && images.length < totalImages) { void loadMoreImages() } }, [images.length, loadMoreImages, loadingImages, totalImages]) useEffect(() => { const el = parentRef.current if (!el) return el.addEventListener('scroll', handleScroll, { passive: true }) return () => el.removeEventListener('scroll', handleScroll) }, [handleScroll]) return (
{images.length === 0 && loadingImages ? ( ) : images.length === 0 && !loadingImages ? ( ) : (
{virtualizer.getVirtualItems().map((virtualRow) => { const startIndex = virtualRow.index * cols const rowImages = images.slice(startIndex, startIndex + cols) return (
{rowImages.map((image) => ( openImage(image)} onContextMenu={(event) => { event.preventDefault() setContextMenu({ x: event.clientX, y: event.clientY, image }) }} /> ))}
) })}
)} {images.length > 0 && loadingImages ? (
) : null} {contextMenu ? ( setContextMenu(null)} /> ) : null}
{/* Pinned to the bottom of the gallery viewport — outside the scroll container so it stays put while the grid scrolls. */}
) }