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',
},
})