'use client'; /** * VideoGroupCard - Displays grouped videos with same name as single card * Following Liquid Glass design system */ import { memo, useMemo } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Icons } from '@/components/ui/Icon'; import { LatencyBadge } from '@/components/ui/LatencyBadge'; import { FavoriteButton } from '@/components/favorites/FavoriteButton'; import { Video } from '@/lib/types'; import { parseVideoTitle } from '@/lib/utils/video'; import { storeGroupedSources } from '@/lib/utils/grouped-sources-cache'; import type { ResolutionInfo } from '@/lib/hooks/useResolutionProbe'; export interface GroupedVideo { /** Representative video (lowest latency) */ representative: Video; /** All videos in this group */ videos: Video[]; /** Group name (vod_name) */ name: string; } interface VideoGroupCardProps { group: GroupedVideo; cardId: string; isActive: boolean; onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void; isPremium?: boolean; latencies?: Record; resolution?: ResolutionInfo | null; isProbing?: boolean; } export const VideoGroupCard = memo(({ group, cardId, isActive, onCardClick, isPremium = false, latencies = {}, resolution, isProbing = false, }) => { const { representative, videos, name } = group; // Best latency from the group, preferring real-time updates const bestLatency = useMemo(() => { const currentLatencies = videos.map(v => latencies[v.source] ?? v.latency).filter(l => l !== undefined) as number[]; return currentLatencies.length > 0 ? Math.min(...currentLatencies) : undefined; }, [videos, latencies]); // Generate URL with grouped sources stored in sessionStorage (avoids long URLs / 414 errors) const videoUrl = useMemo(() => { const params = new URLSearchParams({ id: String(representative.vod_id), source: representative.source, title: representative.vod_name, }); // Store group data in sessionStorage and pass short key in URL if (videos.length > 1) { const groupData = videos.map(v => ({ id: v.vod_id, source: v.source, sourceName: v.sourceName, latency: v.latency, pic: v.vod_pic, typeName: v.type_name, remarks: v.vod_remarks, })); const cacheKey = storeGroupedSources(groupData); if (cacheKey) { params.set('gs', cacheKey); } } if (isPremium) { params.set('premium', '1'); } return `/player?${params.toString()}`; }, [representative, videos, isPremium]); return (
(e.currentTarget.style.zIndex = '100')} onMouseLeave={(e) => (e.currentTarget.style.zIndex = '1')} > onCardClick(e, cardId, videoUrl)} role="listitem" aria-label={`${name} - ${videos.length} 个源${representative.vod_remarks ? ` - ${representative.vod_remarks}` : ''}`} prefetch={false} data-focusable className="group cursor-pointer hover:translate-y-[-2px] transition-transform duration-200 ease-out block h-full" > {/* Poster */}
{representative.vod_pic ? ( {name} { const target = e.currentTarget as HTMLImageElement; target.style.opacity = '0'; }} /> ) : (
)} {/* Fallback Icon - visible when image fails */}
{name}
{/* Badge Container */}
{/* Source count badge */} {videos.length} 源 {bestLatency !== undefined && ( )}
{/* Favorite Button - Top Right */}
[video.source, video.vod_id]))} size={16} className="shadow-md" isPremium={isPremium} />
{/* Overlay */}
{isActive && (
再次点击播放 →
)} {representative.type_name && ( {representative.type_name} )} {representative.vod_year && (
{representative.vod_year}
)}
{/* Info */}
{(() => { const { cleanTitle } = parseVideoTitle(name); return ( <>

{cleanTitle}

{resolution ? ( {resolution.label} ) : isProbing ? ( ... ) : null}
{representative.vod_lang && (

{representative.vod_lang}

)} ); })()}
); }); VideoGroupCard.displayName = 'VideoGroupCard';