import React, { useState, useRef, useEffect, useCallback, memo } from 'react' import { View, Text, StyleSheet, Dimensions, FlatList, Pressable, StatusBar as RNStatusBar, ActivityIndicator, RefreshControl, Platform, } 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 { SameStyleIcon, WhiteStarIcon } from '@/components/icon' import { useRouter } from 'expo-router' import { useTemplates, type TemplateDetail } from '@/hooks' 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 LoadingState = () => ( 加载中... ) const ErrorState = () => ( 加载失败,请下拉刷新重试 ) const EmptyState = () => ( 暂无视频模板 ) const FooterLoading = () => ( ) // 计算图片显示尺寸 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 handleImageLoad = useCallback((event: ImageLoadEventData) => { const { source } = event if (source?.width && source?.height) { setImageSize({ width: source.width, height: source.height }) } }, []) 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 && ( )} {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, }) const [refreshing, setRefreshing] = useState(false) useEffect(() => { execute() }, [execute]) const handleRefresh = useCallback(async () => { setRefreshing(true) await refetch() setRefreshing(false) }, [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(`Filtering ${item.title}:`, { 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 ( } onEndReached={handleLoadMore} onEndReachedThreshold={0.1} ListFooterComponent={loading ? : 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', }, footerLoading: { paddingVertical: 20, alignItems: 'center', }, videoContainer: { width: screenWidth, position: 'relative', }, videoWrapper: { flex: 1, position: 'relative', alignItems: 'center', justifyContent: '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', }, })