'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { FixedSizeGrid } from 'react-window'; import { Card, CardContent } from '@/components/ui/card'; import { StarRating } from '@/components/star-rating'; import { Film, Image as ImageIcon, HardDrive, Search, Bookmark } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; interface MediaItem { id: number; title: string; path: string; size: number; thumbnail: string; type: string; bookmark_count: number; avg_rating: number; star_count: number; } interface PaginationInfo { total: number; limit: number; offset: number; hasMore: boolean; } interface VirtualizedMediaGridProps { type: 'video' | 'photo' | 'bookmark'; onItemClick: (item: MediaItem) => void; onBookmark: (id: number) => Promise; onUnbookmark: (id: number) => Promise; onRate: (id: number, rating: number) => Promise; } export default function VirtualizedMediaGrid({ type, onItemClick, onBookmark, onUnbookmark, onRate }: VirtualizedMediaGridProps) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [pagination, setPagination] = useState({ total: 0, limit: 50, offset: 0, hasMore: true }); const [isLoadingMore, setIsLoadingMore] = useState(false); const observerTarget = useRef(null); const loadingRef = useRef(false); const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const formatFilePath = (path: string) => { if (!path) return ''; const lastSlashIndex = path.lastIndexOf('/'); const lastBackslashIndex = path.lastIndexOf('\\'); const lastSeparatorIndex = Math.max(lastSlashIndex, lastBackslashIndex); if (lastSeparatorIndex === -1) { return path; } const directory = path.substring(0, lastSeparatorIndex); const filename = path.substring(lastSeparatorIndex + 1); if (directory.length <= 30) { return `${directory}/${filename}`; } const maxDirLength = 25; const startLength = Math.floor(maxDirLength / 2); const endLength = maxDirLength - startLength - 3; const truncatedDir = directory.length > maxDirLength ? `${directory.substring(0, startLength)}...${directory.substring(directory.length - endLength)}` : directory; return `${truncatedDir}/${filename}`; }; const fetchItems = useCallback(async (offset: number, search?: string) => { if (loadingRef.current) return; loadingRef.current = true; setIsLoadingMore(offset > 0); try { const params = new URLSearchParams({ limit: '50', offset: offset.toString(), ...(search && { search }) }); const endpoint = type === 'bookmark' ? 'bookmarks' : `${type}s`; const response = await fetch(`/api/${endpoint}?${params}`); const data = await response.json(); const itemsKey = type === 'bookmark' ? 'bookmarks' : `${type}s`; if (offset === 0) { setItems(data[itemsKey] || []); } else { setItems(prev => [...prev, ...(data[itemsKey] || [])]); } setPagination(data.pagination); } catch (error) { console.error(`Error fetching ${type}s:`, error); } finally { setLoading(false); setIsLoadingMore(false); loadingRef.current = false; } }, [type]); const handleSearch = useCallback((term: string) => { setSearchTerm(term); setItems([]); setPagination({ total: 0, limit: 50, offset: 0, hasMore: true }); fetchItems(0, term); }, [fetchItems]); const loadMoreItems = useCallback(() => { if (pagination.hasMore && !loadingRef.current) { fetchItems(pagination.offset + pagination.limit, searchTerm); } }, [pagination, searchTerm, fetchItems]); useEffect(() => { setLoading(true); fetchItems(0, searchTerm); }, [type]); useEffect(() => { const observer = new IntersectionObserver( entries => { if (entries[0].isIntersecting && pagination.hasMore && !loadingRef.current) { loadMoreItems(); } }, { threshold: 0.1 } ); if (observerTarget.current) { observer.observe(observerTarget.current); } return () => observer.disconnect(); }, [loadMoreItems, pagination.hasMore]); const Cell = ({ columnIndex, rowIndex, style }: any) => { const columnCount = 6; const index = rowIndex * columnCount + columnIndex; const item = items[index]; if (!item) return null; return (
onItemClick(item)} >
{item.title} { (e.target as HTMLImageElement).src = type === 'video' ? "/placeholder-video.svg" : "/placeholder-photo.svg"; }} />
{type === 'video' ? : }
{type === 'video' ? : }

{item.title || item.path.split('/').pop()}

{(item.avg_rating > 0 || item.star_count > 0) && (
)}
{formatFileSize(item.size)}

{formatFilePath(item.path)}

); }; const getColumnCount = () => { if (typeof window === 'undefined') return 6; const width = window.innerWidth; if (width < 640) return 2; if (width < 768) return 3; if (width < 1024) return 4; if (width < 1280) return 5; if (width < 1536) return 6; return 7; }; const columnCount = getColumnCount(); const rowCount = Math.ceil(items.length / columnCount); if (loading && items.length === 0) { return (
{type === 'video' ? : }

Loading {type}s...

); } return (
{/* Header */}
{type === 'video' ? : type === 'photo' ? : }

{type === 'bookmark' ? 'Bookmarked Items' : `${type}s`}

{pagination.total.toLocaleString()} {type === 'bookmark' ? 'item' : type}{pagination.total === 1 ? '' : 's'} in your library

{/* Search */}
handleSearch(e.target.value)} className="pl-10 bg-background border-border" />
{/* Media Grid */} {items.length > 0 ? (
{Cell} {pagination.hasMore && (
)}
) : (

No {type === 'bookmark' ? 'bookmarked items' : `${type}s`} found

{searchTerm ? 'Try adjusting your search terms' : type === 'bookmark' ? 'Start bookmarking videos and photos to see them here' : 'Add media libraries and scan for content to get started'}

)}
); }