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