/* eslint-disable @typescript-eslint/no-explicit-any */ import { CheckCircle, Heart, Link, PlayCircleIcon } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { deleteFavorite, deletePlayRecord, generateStorageKey, isFavorited, saveFavorite, subscribeToDataUpdates, } from '@/lib/db.client'; import { SearchResult } from '@/lib/types'; import { processImageUrl } from '@/lib/utils'; import { ImagePlaceholder } from '@/components/ImagePlaceholder'; interface VideoCardProps { id?: string; source?: string; title?: string; query?: string; poster?: string; episodes?: number; source_name?: string; progress?: number; year?: string; from: 'playrecord' | 'favorite' | 'search' | 'douban'; currentEpisode?: number; douban_id?: string; onDelete?: () => void; rate?: string; items?: SearchResult[]; type?: string; } export default function VideoCard({ id, title = '', query = '', poster = '', episodes, source, source_name, progress = 0, year, from, currentEpisode, douban_id, onDelete, rate, items, type = '', }: VideoCardProps) { const router = useRouter(); const [favorited, setFavorited] = useState(false); const [isLoading, setIsLoading] = useState(false); const isAggregate = from === 'search' && !!items?.length; const aggregateData = useMemo(() => { if (!isAggregate || !items) return null; const countMap = new Map(); const episodeCountMap = new Map(); items.forEach((item) => { if (item.douban_id && item.douban_id !== 0) { countMap.set(item.douban_id, (countMap.get(item.douban_id) || 0) + 1); } const len = item.episodes?.length || 0; if (len > 0) { episodeCountMap.set(len, (episodeCountMap.get(len) || 0) + 1); } }); const getMostFrequent = ( map: Map ) => { let maxCount = 0; let result: T | undefined; map.forEach((cnt, key) => { if (cnt > maxCount) { maxCount = cnt; result = key; } }); return result; }; return { first: items[0], mostFrequentDoubanId: getMostFrequent(countMap), mostFrequentEpisodes: getMostFrequent(episodeCountMap) || 0, }; }, [isAggregate, items]); const actualTitle = aggregateData?.first.title ?? title; const actualPoster = aggregateData?.first.poster ?? poster; const actualSource = aggregateData?.first.source ?? source; const actualId = aggregateData?.first.id ?? id; const actualDoubanId = String( aggregateData?.mostFrequentDoubanId ?? douban_id ); const actualEpisodes = aggregateData?.mostFrequentEpisodes ?? episodes; const actualYear = aggregateData?.first.year ?? year; const actualQuery = query || ''; const actualSearchType = isAggregate ? aggregateData?.first.episodes?.length === 1 ? 'movie' : 'tv' : type; // 获取收藏状态 useEffect(() => { if (from === 'douban' || !actualSource || !actualId) return; const fetchFavoriteStatus = async () => { try { const fav = await isFavorited(actualSource, actualId); setFavorited(fav); } catch (err) { throw new Error('检查收藏状态失败'); } }; fetchFavoriteStatus(); // 监听收藏状态更新事件 const storageKey = generateStorageKey(actualSource, actualId); const unsubscribe = subscribeToDataUpdates( 'favoritesUpdated', (newFavorites: Record) => { // 检查当前项目是否在新的收藏列表中 const isNowFavorited = !!newFavorites[storageKey]; setFavorited(isNowFavorited); } ); return unsubscribe; }, [from, actualSource, actualId]); const handleToggleFavorite = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (from === 'douban' || !actualSource || !actualId) return; try { if (favorited) { // 如果已收藏,删除收藏 await deleteFavorite(actualSource, actualId); setFavorited(false); } else { // 如果未收藏,添加收藏 await saveFavorite(actualSource, actualId, { title: actualTitle, source_name: source_name || '', year: actualYear || '', cover: actualPoster, total_episodes: actualEpisodes ?? 1, save_time: Date.now(), }); setFavorited(true); } } catch (err) { throw new Error('切换收藏状态失败'); } }, [ from, actualSource, actualId, actualTitle, source_name, actualYear, actualPoster, actualEpisodes, favorited, ] ); const handleDeleteRecord = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (from !== 'playrecord' || !actualSource || !actualId) return; try { await deletePlayRecord(actualSource, actualId); onDelete?.(); } catch (err) { throw new Error('删除播放记录失败'); } }, [from, actualSource, actualId, onDelete] ); const handleClick = useCallback(() => { if (from === 'douban') { router.push( `/play?title=${encodeURIComponent(actualTitle.trim())}${ actualYear ? `&year=${actualYear}` : '' }${actualSearchType ? `&stype=${actualSearchType}` : ''}` ); } else if (actualSource && actualId) { router.push( `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent( actualTitle )}${actualYear ? `&year=${actualYear}` : ''}${ isAggregate ? '&prefer=true' : '' }${ actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : '' }${actualSearchType ? `&stype=${actualSearchType}` : ''}` ); } }, [ from, actualSource, actualId, router, actualTitle, actualYear, isAggregate, actualQuery, actualSearchType, ]); const config = useMemo(() => { const configs = { playrecord: { showSourceName: true, showProgress: true, showPlayButton: true, showHeart: true, showCheckCircle: true, showDoubanLink: false, showRating: false, }, favorite: { showSourceName: true, showProgress: false, showPlayButton: true, showHeart: true, showCheckCircle: false, showDoubanLink: false, showRating: false, }, search: { showSourceName: true, showProgress: false, showPlayButton: true, showHeart: !isAggregate, showCheckCircle: false, showDoubanLink: !!actualDoubanId, showRating: false, }, douban: { showSourceName: false, showProgress: false, showPlayButton: true, showHeart: false, showCheckCircle: false, showDoubanLink: true, showRating: !!rate, }, }; return configs[from] || configs.search; }, [from, isAggregate, actualDoubanId, rate]); return (
{/* 海报容器 */}
{/* 骨架屏 */} {!isLoading && } {/* 图片 */} {actualTitle} setIsLoading(true)} /> {/* 悬浮遮罩 */}
{/* 播放按钮 */} {config.showPlayButton && (
)} {/* 操作按钮 */} {(config.showHeart || config.showCheckCircle) && (
{config.showCheckCircle && ( )} {config.showHeart && ( )}
)} {/* 徽章 */} {config.showRating && rate && (
{rate}
)} {actualEpisodes && actualEpisodes > 1 && (
{currentEpisode ? `${currentEpisode}/${actualEpisodes}` : actualEpisodes}
)} {/* 豆瓣链接 */} {config.showDoubanLink && actualDoubanId && ( e.stopPropagation()} className='absolute top-2 left-2 opacity-0 -translate-x-2 transition-all duration-300 ease-in-out delay-100 group-hover:opacity-100 group-hover:translate-x-0' >
)}
{/* 进度条 */} {config.showProgress && progress !== undefined && (
)} {/* 标题与来源 */}
{actualTitle} {/* 自定义 tooltip */}
{actualTitle}
{config.showSourceName && source_name && ( {source_name} )}
); }