import React, { useState, useRef, useEffect, useCallback, memo } from 'react' import { View, Text, StyleSheet, Dimensions, FlatList, Pressable, StatusBar as RNStatusBar, Platform, Animated, } from 'react-native' import { StatusBar } from 'expo-status-bar' import { SafeAreaView } from 'react-native-safe-area-context' import { Image, ImageLoadEventData } from 'expo-image' import { useTranslation } from 'react-i18next' import { Ionicons } from '@expo/vector-icons' import { SameStyleIcon, WhiteStarIcon } from '@/components/icon' import { useRouter } from 'expo-router' import { useTemplates, type TemplateDetail } from '@/hooks' import { VideoSocialButton } from '@/components/blocks/ui/VideoSocialButton' import { useTemplateLike } from '@/hooks/use-template-like' import { useTemplateFavorite } from '@/hooks/use-template-favorite' import { useTemplateSocialStore, useTemplateLiked, useTemplateFavorited, useTemplateLikeCount, useTemplateFavoriteCount, } from '@/stores/templateSocialStore' import LoadingState from '@/components/LoadingState' import ErrorState from '@/components/ErrorState' import PaginationLoader from '@/components/PaginationLoader' const { width: screenWidth, height: screenHeight } = Dimensions.get('window') const TAB_BAR_HEIGHT = 83 // 检查 URL 是否为视频格式 const isVideoUrl = (url: string): boolean => { const videoExtensions = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m3u8'] return videoExtensions.some(ext => url.toLowerCase().endsWith(ext)) } // 统一的布局组件 const Layout = ({ children }: { children: React.ReactNode }) => ( {children} ) const EmptyState = () => ( 暂无视频模板 ) // 计算图片显示尺寸 const calculateImageSize = ( imageSize: { width: number; height: number } | null, videoHeight: number ) => { if (!imageSize) return { width: screenWidth, height: videoHeight } const imageAspectRatio = imageSize.width / imageSize.height const screenAspectRatio = screenWidth / videoHeight return imageAspectRatio > screenAspectRatio ? { width: screenWidth, height: screenWidth / imageAspectRatio } : { width: videoHeight * imageAspectRatio, height: videoHeight } } export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeight: number }) => { const { t } = useTranslation() const router = useRouter() const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null) // 双击点赞状态 const [showHeartAnimation, setShowHeartAnimation] = useState(false) const heartScale = useRef(new Animated.Value(0)).current const heartOpacity = useRef(new Animated.Value(0)).current const lastTap = useRef(0) // 从 store 获取状态 const liked = useTemplateLiked(item.id) ?? false const favorited = useTemplateFavorited(item.id) ?? false const likeCount = useTemplateLikeCount(item.id) ?? item.likeCount const favoriteCount = useTemplateFavoriteCount(item.id) ?? item.favoriteCount // 使用 hooks 获取操作函数 const { like: onLike, unlike: onUnlike, loading: likeLoading } = useTemplateLike(item.id, item.likeCount) const { favorite: onFavorite, unfavorite: onUnfavorite, loading: favoriteLoading } = useTemplateFavorite(item.id, false, item.favoriteCount) const handleImageLoad = useCallback((event: ImageLoadEventData) => { const { source } = event if (source?.width && source?.height) { setImageSize({ width: source.width, height: source.height }) } }, []) // 双击处理 const handleDoubleTap = useCallback(() => { const now = Date.now() if (now - lastTap.current < 300) { // 双击检测成功 lastTap.current = 0 // 显示心形动画 setShowHeartAnimation(true) heartScale.setValue(0) heartOpacity.setValue(1) Animated.parallel([ Animated.timing(heartScale, { toValue: 1, duration: 200, useNativeDriver: true, }), Animated.timing(heartOpacity, { toValue: 0, duration: 800, delay: 200, useNativeDriver: true, }), ]).start(() => { setShowHeartAnimation(false) }) // 触发点赞/取消点赞 if (liked) { onUnlike() } else { onLike() } } else { lastTap.current = now } }, [liked, onLike, onUnlike, heartScale, heartOpacity]) const handlePress = useCallback(() => { router.push({ pathname: '/generateVideo' as any, params: { templateId: item.id }, }) }, [router, item.id]) const imageStyle = calculateImageSize(imageSize, videoHeight) // 优先使用 WebP 格式(支持动画),回退到普通预览图 const displayUrl = item.webpHighPreviewUrl || item.webpPreviewUrl || item.previewUrl return ( {displayUrl && ( )} {/* 双击心形动画 */} {showHeartAnimation && ( )} {item.title} {t('video.makeSame')} {/* 社交按钮 */} ) }) export default function VideoScreen() { const flatListRef = useRef(null) const videoHeight = screenHeight - TAB_BAR_HEIGHT const PAGE_SIZE = 10 // 每页10个数据 const { templates, loading, error, execute, refetch, loadMore, hasMore } = useTemplates({ sortBy: 'likeCount', sortOrder: 'desc', page: 1, limit: PAGE_SIZE, }) useEffect(() => { execute() }, [execute]) const handleRefresh = useCallback(async () => { await refetch() }, [refetch]) const handleLoadMore = useCallback(() => { console.log('onEndReached triggered', { hasMore, loading, templatesLength: templates.length }) if (!hasMore || loading) { console.log('LoadMore blocked', { hasMore, loading }) return } console.log('Loading more...') loadMore() }, [hasMore, loading, loadMore, templates.length]) const renderItem = useCallback(({ item }: { item: TemplateDetail }) => ( ), [videoHeight]) const keyExtractor = useCallback((item: TemplateDetail) => item.id, []) const getItemLayout = useCallback((_: any, index: number) => ({ length: videoHeight, offset: videoHeight * index, index, }), [videoHeight]) // 过滤掉没有可用预览的 item const filteredTemplates = templates.filter((item: TemplateDetail) => { // 优先使用 WebP 格式(支持动画),回退到普通预览图 const displayUrl = item.webpHighPreviewUrl || item.webpPreviewUrl || item.previewUrl // 只检查最终要显示的 URL 是否为视频格式 const isDisplayVideo = displayUrl && isVideoUrl(displayUrl) // 有显示URL且不是视频格式才保留 const shouldKeep = !!displayUrl && !isDisplayVideo console.log(`[VIDEO PAGE] Filtering ${item.title}:`, { webpHighPreviewUrl: item.webpHighPreviewUrl, webpPreviewUrl: item.webpPreviewUrl, previewUrl: item.previewUrl, displayUrl, isDisplayVideo, shouldKeep, }) return shouldKeep }) console.log('Video page state:', { templatesLength: templates.length, filteredTemplatesLength: filteredTemplates.length, loading, error, hasMore, }) if (loading && templates.length === 0) { return ( ) } if (error && templates.length === 0) { return ( ) } if (!loading && filteredTemplates.length === 0) return return ( : null} maxToRenderPerBatch={5} windowSize={7} initialNumToRender={3} /> ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#090A0B', }, centerContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, }, messageText: { color: '#FFFFFF', fontSize: 14, textAlign: 'center', }, videoContainer: { width: screenWidth, position: 'relative', }, videoWrapper: { flex: 1, position: 'relative', alignItems: 'center', justifyContent: 'center', }, videoPressable: { flex: 1, width: '100%', alignItems: 'center', justifyContent: 'center', }, heartAnimation: { position: 'absolute', top: '50%', left: '50%', marginTop: -40, marginLeft: -40, width: 80, height: 80, justifyContent: 'center', alignItems: 'center', }, thumbnailContainer: { position: 'absolute', left: 12, bottom: 80, width: 56, height: 56, borderRadius: 8, overflow: 'hidden', borderWidth: 2, borderColor: '#FFFFFF', }, thumbnail: { width: '100%', height: '100%', }, actionButtonLeft: { position: 'absolute', left: 12, bottom: 16, flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 6, paddingVertical: 8, backgroundColor: '#191B1F', borderRadius: 8, borderWidth: 1, borderColor: '#2F3134', }, actionButtonRight: { position: 'absolute', right: 13, bottom: 13, flexDirection: 'column', alignItems: 'center', }, actionButtonTextTitle: { color: '#F5F5F5', fontSize: 11, }, actionButtonText: { color: '#CCCCCC', fontSize: 10, fontWeight: '500', }, })