import { useRef, useEffect, useMemo, useState } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' import { usePhotoStore } from '../../store/photoStore' import { PhotoThumbnail } from './PhotoThumbnail' import { useQuery } from '@tanstack/react-query' import axios from 'axios' import type { Photo } from '../../types/photo' export function Timeline() { const parentRef = useRef(null) const [containerWidth, setContainerWidth] = useState(0) const { selectedPhotos, lastSelectedIndex, rangeStartIndex, selectPhoto, togglePhotoSelection, clearSelection, openLoupe, } = usePhotoStore() // Helper function for range selection const selectRange = (endIndex: number) => { const startIndex = rangeStartIndex ?? lastSelectedIndex ?? 0 const minIndex = Math.min(startIndex, endIndex) const maxIndex = Math.max(startIndex, endIndex) // Select all photos in the range for (let i = minIndex; i <= maxIndex; i++) { if (i < photos.length && !selectedPhotos.includes(photos[i].id)) { togglePhotoSelection(photos[i].id, i) } } } // Thumbnail size configuration const thumbnailSize = 200 // Base size for thumbnails const gap = 4 const padding = 16 // Calculate number of columns based on container width const columns = useMemo(() => { if (containerWidth === 0) return 4 return Math.floor((containerWidth - padding * 2) / (thumbnailSize + gap)) }, [containerWidth, thumbnailSize, gap, padding]) // Fetch photos from backend const { data: photos = [], isLoading } = useQuery({ queryKey: ['photos'], queryFn: async () => { const response = await axios.get<{photos: Photo[], total: number}>('http://localhost:8001/api/v1/photos', { params: { limit: 1000, offset: 0, }, }) return response.data.photos || [] }, staleTime: 30000, }) // Group photos into rows for grid layout const rows = useMemo(() => { const result: Photo[][] = [] for (let i = 0; i < photos.length; i += columns) { result.push(photos.slice(i, i + columns)) } return result }, [photos, columns]) // Virtual scrolling setup const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => thumbnailSize + gap, overscan: 5, }) // Measure container width on mount and resize useEffect(() => { const measureWidth = () => { if (parentRef.current) { setContainerWidth(parentRef.current.clientWidth) } } measureWidth() window.addEventListener('resize', measureWidth) return () => window.removeEventListener('resize', measureWidth) }, []) // Handle keyboard shortcuts for photo navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (photos.length === 0) return const currentIndex = lastSelectedIndex ?? -1 switch (e.key) { case 'ArrowUp': e.preventDefault() if (currentIndex > columns - 1) { const newIndex = currentIndex - columns if (e.shiftKey) { selectRange(newIndex) } else { selectPhoto(photos[newIndex].id, newIndex) } } break case 'ArrowDown': e.preventDefault() if (currentIndex < photos.length - columns) { const newIndex = Math.min(currentIndex + columns, photos.length - 1) if (e.shiftKey) { selectRange(newIndex) } else { selectPhoto(photos[newIndex].id, newIndex) } } break case 'ArrowLeft': e.preventDefault() if (currentIndex > 0) { const newIndex = currentIndex - 1 if (e.shiftKey) { selectRange(newIndex) } else { selectPhoto(photos[newIndex].id, newIndex) } } break case 'ArrowRight': e.preventDefault() if (currentIndex < photos.length - 1) { const newIndex = currentIndex + 1 if (e.shiftKey) { selectRange(newIndex) } else { selectPhoto(photos[newIndex].id, newIndex) } } break case 'a': if (e.ctrlKey || e.metaKey) { e.preventDefault() // Select all photos.forEach((photo, index) => { if (!selectedPhotos.includes(photo.id)) { togglePhotoSelection(photo.id, index) } }) } break case 'Escape': e.preventDefault() clearSelection() break } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [photos, selectedPhotos, lastSelectedIndex, columns, selectPhoto, togglePhotoSelection, selectRange, clearSelection]) if (isLoading) { return (
Loading photos...
) } if (photos.length === 0) { return (
No photos found. Add a source folder to get started.
) } return (
{virtualizer.getVirtualItems().map((virtualRow) => { const row = rows[virtualRow.index] return (
{row.map((photo, colIndex) => { const globalIndex = virtualRow.index * columns + colIndex return ( { if (e.shiftKey && lastSelectedIndex !== null) { selectRange(globalIndex) } else if (e.ctrlKey || e.metaKey) { togglePhotoSelection(photo.id, globalIndex) } else { selectPhoto(photo.id, globalIndex) } }} onDoubleClick={() => openLoupe(photo.id)} /> ) })}
) })}
) }