'use client'; import { useState, useRef, useCallback, useMemo, memo, useEffect } from 'react'; import { usePathname, useSearchParams } from 'next/navigation'; import { VideoCard } from './VideoCard'; import { VideoGroupCard, GroupedVideo } from './VideoGroupCard'; import { settingsStore } from '@/lib/store/settings-store'; import { Video } from '@/lib/types'; import { useResolutionProbe } from '@/lib/hooks/useResolutionProbe'; interface VideoGridProps { videos: Video[]; className?: string; isPremium?: boolean; latencies?: Record; } export const VideoGrid = memo(function VideoGrid({ videos, className = '', isPremium = false, latencies = {} }: VideoGridProps) { const [activeCardId, setActiveCardId] = useState(null); const [visibleCount, setVisibleCount] = useState(24); const [displayMode, setDisplayMode] = useState<'normal' | 'grouped'>('normal'); const gridRef = useRef(null); const observerRef = useRef(null); const pathname = usePathname(); const searchParams = useSearchParams(); // Load display mode from settings useEffect(() => { const settings = settingsStore.getSettings(); setDisplayMode(settings.searchDisplayMode); // Initial load: Check for saved scroll position to ensure we render enough items const params = searchParams.toString(); const scrollKey = `scroll-pos:${pathname}${params ? '?' + params : ''}`; const savedPos = sessionStorage.getItem(scrollKey); if (savedPos && settings.rememberScrollPosition) { const position = parseInt(savedPos, 10); if (!isNaN(position) && position > 500) { // Approximate visible count needed: // 500 is roughly where the second/third row starts. // Each row is ~300-400px high on most screens. // 24 items is 4-6 rows. // If scroll is deep, we force a larger initial visible count. // 24, 48, 72, 96... const estimatedRowsNeeded = Math.ceil(position / 300) + 2; // Match CSS breakpoints: sm: 3, md: 4, lg: 5, xl: 6 const itemsPerRow = window.innerWidth >= 1280 ? 6 : (window.innerWidth >= 1024 ? 5 : (window.innerWidth >= 768 ? 4 : (window.innerWidth >= 640 ? 3 : 2))); const neededCount = Math.min(videos.length, estimatedRowsNeeded * itemsPerRow); if (neededCount > 24) { setVisibleCount(Math.ceil(neededCount / 24) * 24); } } } const unsubscribe = settingsStore.subscribe(() => { const newSettings = settingsStore.getSettings(); setDisplayMode(newSettings.searchDisplayMode); }); return () => unsubscribe(); }, [pathname, searchParams, videos.length]); if (videos.length === 0) { return null; } // Build stable list of videos to probe for resolution const videosToProbe = useMemo(() => { if (displayMode === 'grouped') { // For grouped mode, will probe after grouping return []; } return videos.map(v => ({ id: String(v.vod_id), source: v.source })); }, [videos, displayMode]); // Group videos by name when in grouped mode const groupedVideos = useMemo(() => { if (displayMode !== 'grouped') return []; const groups = new Map(); videos.forEach(video => { const name = video.vod_name.toLowerCase().trim(); if (!groups.has(name)) { groups.set(name, []); } groups.get(name)!.push(video); }); return Array.from(groups.entries()).map(([, groupVideos]) => { // Sort by latency (lowest first) const sorted = [...groupVideos].sort((a, b) => { if (a.latency === undefined) return 1; if (b.latency === undefined) return -1; return a.latency - b.latency; }); return { representative: sorted[0], videos: sorted, name: sorted[0].vod_name, }; }); }, [videos, displayMode]); // Callback ref for the load more trigger const loadMoreRef = useCallback((node: HTMLDivElement | null) => { if (observerRef.current) observerRef.current.disconnect(); if (node) { observerRef.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { setVisibleCount(prev => prev + 24); } }, { rootMargin: '400px' }); observerRef.current.observe(node); } }, []); // Memoize the click handler const handleCardClick = useCallback((e: React.MouseEvent, videoId: string, videoUrl: string) => { const isMobile = window.innerWidth < 1024; if (isMobile) { if (activeCardId === videoId) { window.location.href = videoUrl; } else { e.preventDefault(); setActiveCardId(videoId); } } }, [activeCardId]); // Normal mode items const videoItems = useMemo(() => { if (displayMode === 'grouped') return []; return videos.map((video, index) => { const params: Record = { id: String(video.vod_id), source: video.source, title: video.vod_name, }; if (isPremium) { params.premium = '1'; } const videoUrl = `/player?${new URLSearchParams(params).toString()}`; const cardId = `${video.vod_id}-${index}`; return { video, videoUrl, cardId }; }); }, [videos, displayMode, isPremium]); // Grouped mode items const groupItems = useMemo(() => { if (displayMode !== 'grouped') return []; return groupedVideos.map((group, index) => ({ group, cardId: `group-${group.representative.vod_id}-${index}`, })); }, [groupedVideos, displayMode]); // Build probe list for grouped mode (probe representative of each group) const groupedProbeList = useMemo(() => { if (displayMode !== 'grouped') return []; return groupedVideos.map(g => ({ id: String(g.representative.vod_id), source: g.representative.source })); }, [groupedVideos, displayMode]); // Probe resolutions const probeList = displayMode === 'grouped' ? groupedProbeList : videosToProbe; const { resolutions, isProbing } = useResolutionProbe(probeList); const totalItems = displayMode === 'grouped' ? groupItems.length : videoItems.length; return ( <>
{displayMode === 'grouped' ? ( // Grouped mode groupItems.slice(0, visibleCount).map(({ group, cardId }) => { const isActive = activeCardId === cardId; return ( ); }) ) : ( // Normal mode videoItems.slice(0, visibleCount).map(({ video, videoUrl, cardId }) => { const isActive = activeCardId === cardId; return ( ); }) )}
{/* Load more trigger */} {visibleCount < totalItems && (