import React, { useState, useEffect, useCallback, useRef, forwardRef } from "react"; import { View, Text, Image, StyleSheet, TouchableOpacity, Alert, Animated } from "react-native"; import { useRouter } from "expo-router"; import { Star, Play } from "lucide-react-native"; import { PlayRecordManager } from "@/services/storage"; import { API } from "@/services/api"; import { ThemedText } from "@/components/ThemedText"; import { Colors } from "@/constants/Colors"; import { useResponsiveLayout } from "@/hooks/useResponsiveLayout"; import { DeviceUtils } from "@/utils/DeviceUtils"; import Logger from '@/utils/Logger'; const logger = Logger.withTag('VideoCardTablet'); interface VideoCardTabletProps extends React.ComponentProps { id: string; source: string; title: string; poster: string; year?: string; rate?: string; sourceName?: string; progress?: number; playTime?: number; episodeIndex?: number; totalEpisodes?: number; onFocus?: () => void; onRecordDeleted?: () => void; api: API; } const VideoCardTablet = forwardRef( ( { id, source, title, poster, year, rate, sourceName, progress, episodeIndex, onFocus, onRecordDeleted, api, playTime = 0, }: VideoCardTabletProps, ref ) => { const router = useRouter(); const { cardWidth, cardHeight, spacing } = useResponsiveLayout(); const [fadeAnim] = useState(new Animated.Value(0)); const [isPressed, setIsPressed] = useState(false); const longPressTriggered = useRef(false); const scale = useRef(new Animated.Value(1)).current; const handlePress = () => { if (longPressTriggered.current) { longPressTriggered.current = false; return; } if (progress !== undefined && episodeIndex !== undefined) { router.push({ pathname: "/play", params: { source, id, episodeIndex: episodeIndex - 1, title, position: playTime * 1000 }, }); } else { router.push({ pathname: "/detail", params: { source, q: title }, }); } }; const handlePressIn = useCallback(() => { setIsPressed(true); Animated.spring(scale, { toValue: 0.96, damping: 15, stiffness: 300, useNativeDriver: true, }).start(); }, [scale]); const handlePressOut = useCallback(() => { setIsPressed(false); Animated.spring(scale, { toValue: 1.0, damping: 15, stiffness: 300, useNativeDriver: true, }).start(); }, [scale]); useEffect(() => { Animated.timing(fadeAnim, { toValue: 1, duration: DeviceUtils.getAnimationDuration(400), delay: Math.random() * 150, useNativeDriver: true, }).start(); }, [fadeAnim]); const handleLongPress = () => { if (progress === undefined) return; longPressTriggered.current = true; Alert.alert("删除观看记录", `确定要删除"${title}"的观看记录吗?`, [ { text: "取消", style: "cancel", }, { text: "删除", style: "destructive", onPress: async () => { try { await PlayRecordManager.remove(source, id); onRecordDeleted?.(); } catch (error) { logger.info("Failed to delete play record:", error); Alert.alert("错误", "删除观看记录失败,请重试"); } }, }, ]); }; const isContinueWatching = progress !== undefined && progress > 0 && progress < 1; const animatedStyle = { transform: [{ scale }], }; const styles = createTabletStyles(cardWidth, cardHeight, spacing); return ( {/* 悬停效果遮罩 */} {isPressed && ( {isContinueWatching && ( 继续观看 )} )} {/* 进度条 */} {isContinueWatching && ( )} {/* 评分 */} {rate && ( {rate} )} {/* 年份 */} {year && ( {year} )} {/* 来源 */} {sourceName && ( {sourceName} )} {title} {isContinueWatching && ( 第{episodeIndex! + 1}集 已观看 {Math.round((progress || 0) * 100)}% )} ); } ); VideoCardTablet.displayName = "VideoCardTablet"; const createTabletStyles = (cardWidth: number, cardHeight: number, spacing: number) => { return StyleSheet.create({ wrapper: { width: cardWidth, marginHorizontal: spacing / 2, marginBottom: spacing, }, pressable: { alignItems: 'center', }, card: { width: cardWidth, height: cardHeight, borderRadius: 10, backgroundColor: "#222", overflow: "hidden", }, cardPressed: { borderColor: Colors.dark.primary, borderWidth: 2, }, poster: { width: "100%", height: "100%", resizeMode: 'cover', }, pressOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0,0,0,0.4)", justifyContent: "center", alignItems: "center", borderRadius: 10, }, progressContainer: { position: "absolute", bottom: 0, left: 0, right: 0, height: 4, backgroundColor: "rgba(0, 0, 0, 0.8)", }, progressBar: { height: 4, backgroundColor: Colors.dark.primary, }, continueWatchingBadge: { flexDirection: "row", alignItems: "center", backgroundColor: Colors.dark.primary, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, }, continueWatchingText: { color: "white", marginLeft: 6, fontSize: 14, fontWeight: "bold", }, ratingContainer: { position: "absolute", top: 8, right: 8, flexDirection: "row", alignItems: "center", backgroundColor: "rgba(0, 0, 0, 0.7)", borderRadius: 6, paddingHorizontal: 6, paddingVertical: 3, }, ratingText: { color: "#FFD700", fontSize: 11, fontWeight: "bold", marginLeft: 3, }, yearBadge: { position: "absolute", top: 8, right: 8, backgroundColor: "rgba(0, 0, 0, 0.7)", borderRadius: 6, paddingHorizontal: 6, paddingVertical: 3, }, sourceNameBadge: { position: "absolute", top: 8, left: 8, backgroundColor: "rgba(0, 0, 0, 0.7)", borderRadius: 6, paddingHorizontal: 6, paddingVertical: 3, }, badgeText: { color: "white", fontSize: 11, fontWeight: "bold", }, infoContainer: { width: cardWidth, marginTop: 8, alignItems: "flex-start", paddingHorizontal: 4, }, infoRow: { flexDirection: "row", justifyContent: "space-between", width: "100%", marginTop: 2, }, title: { fontSize: 15, lineHeight: 18, }, continueLabel: { color: Colors.dark.primary, fontSize: 12, }, }); }; export default VideoCardTablet;