import React, { memo, useMemo, useState, useEffect, useCallback, useRef } from 'react' import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components' import Img from '@share/components/Img' import { Ionicons } from '@expo/vector-icons' import { Dimensions, FlatList, TextInput, RefreshControl, ActivityIndicator } from 'react-native' import { LinearGradient } from 'expo-linear-gradient' import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated' import { router, useRouter } from 'expo-router' import { IOS_UNIVERSAL_LINK } from '@/app.constants' import { FlashList } from '@shopify/flash-list' import { screenHeight, screenWidth } from '@/utils' import { cn } from '@/utils/cn' import { useAuth } from '@/hooks/core/use-auth' import { useUserBalance } from '@/hooks/core/use-user-balance' import { useTemplates } from '@/hooks/data/use-templates' import { usePublicTemplates } from '@/hooks/data/use-public-templates' import { useFavoriteTemplates } from '@/hooks/data/use-favorite-templates' import type { GetUserFavoritesResponse } from '@repo/sdk' import { useAigcTask } from '@/hooks/actions/use-aigc-task' import { useTemplateInteraction } from '@/hooks/actions/use-template-interaction' const BACKGROUND_VIDEOS = [ 'https://cdn.roasmax.cn/material/b46f380532e14cf58dd350dbacc7c34a.mp4', 'https://cdn.roasmax.cn/material/992e6c5d940c42feb71c27e556b754c0.mp4', 'https://cdn.roasmax.cn/material/e4947477843f4067be7c37569a33d17b.mp4', ] type MediaItem = { id: string type: 'image' | 'video' url: string poster?: string likeCount: number } type ActiveTab = 'gen' | '' | 'new' | 'like' /** ========================= * Small memo components * ========================= */ type BannerProps = { bgVideo: string } const Banner = memo(function Banner({ bgVideo }) { return ( {/* */} ) }) type SearchOverlayProps = { isOpen: boolean searchText?: string onChange?: (v: string) => void onClose: () => void onSearch: (v: string) => void } const SearchOverlay = memo(function SearchOverlay({ isOpen, onChange, onClose, onSearch }) { const timerRef = useRef | null>(null) const [searchText, setSearchText] = useState('') const handleTextChange = (text: string) => { setSearchText(text) onChange && onChange(text) if (timerRef.current) { clearTimeout(timerRef.current) } timerRef.current = setTimeout(() => { onSearch(text) }, 2000) } useEffect(() => { // 删除定时器 return () => { if (timerRef.current) { clearTimeout(timerRef.current) } } }, []) if (!isOpen) return null return ( 登出 ) }) type GooActionsProps = { gooPoints: number onAddGoo: () => void onOpenSearch: () => void } const GooActions = memo(function GooActions({ gooPoints, onAddGoo, onOpenSearch }) { return ( {gooPoints} Goo ) }) type HeroCircleProps = { selectedItem: MediaItem | null onQuickGen: () => void rightSlot: React.ReactNode } const HeroCircle = memo(function HeroCircle({ selectedItem, onQuickGen, rightSlot }) { if (!selectedItem) { return ( 加载中... {rightSlot} ) } return ( {selectedItem.type === 'video' ? ( ) : ( )} {selectedItem?.id} GET 同款 {rightSlot} ) }) type FilterSectionProps = { activeTab: ActiveTab onChange: (t: ActiveTab) => void } const FilterSection = memo(function FilterSection({ activeTab, onChange }) { const tabs = useMemo( () => [ { label: '最热', state: '' as const }, { label: '最新', state: 'new' as const }, { label: '喜欢', state: 'like' as const }, ], [], ) return ( {tabs.map(({ label, state }) => { const isActive = activeTab === state return ( onChange(state)} className={`border-[2px] border-black px-[16px] py-[4px] ${isActive ? 'bg-accent' : 'bg-white'}`} style={{ transform: [{ skewX: '-6deg' }], }} > {label} ) })} ) }) type GridItemProps = { item: MediaItem isSelected: boolean itemWidth: number onSelect: () => void } const GridItem = memo(function GridItem({ item, isSelected, itemWidth, onSelect }) { // console.log('item-------------', item); return ( {/* {item.type === 'video' ? ( ) : ( )} */} {/* */} {isSelected && } {item.id} {item.id} {item.likeCount} ) }) /** ========================= * Entry page * ========================= */ export default function Sync() { const { user, isAuthenticated } = useAuth() const { balance, loading: balanceLoading, load: loadBalance } = useUserBalance() const { data: templatesData, loading: templatesLoading, execute: loadTemplates } = useTemplates() const { data: publicTemplatesData, loading: publicTemplatesLoading, execute: loadPublicTemplates } = usePublicTemplates() const { data: favoritesData, loading: favoritesLoading, execute: loadFavorites } = useFavoriteTemplates() const { submitTask, startPolling } = useAigcTask() const [searchText, setSearchText] = useState('') const [isSearchOpen, setIsSearchOpen] = useState(false) const [activeTab, setActiveTab] = useState('') const [page, setPage] = useState(1) const [hasMore, setHasMore] = useState(true) const [refreshing, setRefreshing] = useState(false) const [loadingMore, setLoadingMore] = useState(false) const [allItems, setAllItems] = useState([]) const isLoadingRef = useRef(false) const onAddGoo = useCallback(() => { router.push('/pointList') }, []) const transformTemplateToMediaItem = useCallback((template: any): MediaItem => { const isVideo = template.previewUrl?.includes('.mp4') return { id: template.id, type: (isVideo ? 'video' : 'image') as 'video' | 'image', url: isVideo ? template.previewUrl : template.coverImageUrl || template.previewUrl, poster: isVideo ? template.coverImageUrl : undefined, likeCount: template.likeCount || 0, } }, []) const galleryItems = useMemo(() => allItems, [allItems]) const [selectedItem, setSelectedItem] = useState(null) useEffect(() => { if (galleryItems.length > 0 && !selectedItem) { setSelectedItem(galleryItems[0]) } }, [galleryItems, selectedItem]) const [bgVideo] = useState(() => BACKGROUND_VIDEOS[Math.floor(Math.random() * BACKGROUND_VIDEOS.length)]) const outerRotate = useSharedValue(0) useEffect(() => { if (isAuthenticated && user?.id) { loadBalance(user.id) } }, [isAuthenticated, user?.id]) // Tab 切换时重置并加载数据 useEffect(() => { setPage(1) setHasMore(true) setAllItems([]) const initialLoad = async () => { if (isLoadingRef.current) return isLoadingRef.current = true setRefreshing(true) try { let newItems: MediaItem[] = [] const limit = 20 if (activeTab === 'like') { if (!isAuthenticated) { setHasMore(false) return } const { data, error } = await loadFavorites({ limit, page: 1 }) if (error || !data?.favorites) { setHasMore(false) return } newItems = data.favorites.filter((fav: GetUserFavoritesResponse['favorites'][number]) => fav.template && !fav.template.isDeleted).map((fav: GetUserFavoritesResponse['favorites'][number]) => transformTemplateToMediaItem(fav.template!)) } else if (activeTab === 'new') { if (isAuthenticated) { const { data, error } = await loadTemplates({ limit, page: 1, sortBy: 'createdAt', sortOrder: 'desc' }) if (error || !data?.templates) { setHasMore(false) return } newItems = data.templates.map(transformTemplateToMediaItem) } else { const { data, error } = await loadPublicTemplates({ limit, sortBy: 'createdAt', sortOrder: 'desc' }) if (error || !data?.templates) { setHasMore(false) return } newItems = data.templates.map(transformTemplateToMediaItem) setHasMore(false) } } else { if (isAuthenticated) { const { data, error } = await loadTemplates({ limit, page: 1, sortBy: 'likeCount', sortOrder: 'desc' }) if (error || !data?.templates) { setHasMore(false) return } newItems = data.templates.map(transformTemplateToMediaItem) } else { const { data, error } = await loadPublicTemplates({ limit, sortBy: 'likeCount', sortOrder: 'desc' }) if (error || !data?.templates) { setHasMore(false) return } newItems = data.templates.map(transformTemplateToMediaItem) setHasMore(false) } } if (newItems.length < limit) { setHasMore(false) } setAllItems(newItems) } catch (error) { console.error('Failed to load data:', error) setHasMore(false) } finally { isLoadingRef.current = false setRefreshing(false) } } initialLoad() }, [activeTab, isAuthenticated]) const loadData = useCallback( async (pageNum: number, isRefresh: boolean = false) => { if (!isRefresh && !hasMore) return if (loadingMore || refreshing || isLoadingRef.current) return isLoadingRef.current = true if (isRefresh) { setRefreshing(true) } else { setLoadingMore(true) } try { let newItems: MediaItem[] = [] const limit = 20 if (activeTab === 'like') { if (!isAuthenticated) { setHasMore(false) return } const { data, error } = await loadFavorites({ limit, page: pageNum }) if (error || !data?.favorites) { setHasMore(false) return } newItems = data.favorites.filter((fav: GetUserFavoritesResponse['favorites'][number]) => fav.template && !fav.template.isDeleted).map((fav: GetUserFavoritesResponse['favorites'][number]) => transformTemplateToMediaItem(fav.template!)) } else if (activeTab === 'new') { if (!isAuthenticated) { setHasMore(false) return } const { data, error } = await loadTemplates({ limit, page: pageNum, sortBy: 'createdAt', sortOrder: 'desc' }) if (error || !data?.templates) { setHasMore(false) return } newItems = data.templates.map(transformTemplateToMediaItem) } else { if (!isAuthenticated) { setHasMore(false) return } const { data, error } = await loadTemplates({ limit, page: pageNum, sortBy: 'likeCount', sortOrder: 'desc' }) if (error || !data?.templates) { setHasMore(false) return } newItems = data.templates.map(transformTemplateToMediaItem) } if (newItems.length < limit) { setHasMore(false) } if (isRefresh) { setAllItems(newItems) } else { setAllItems((prev) => [...prev, ...newItems]) } } catch (error) { console.error('Failed to load data:', error) setHasMore(false) } finally { isLoadingRef.current = false setRefreshing(false) setLoadingMore(false) } }, [activeTab, isAuthenticated, hasMore, loadingMore, refreshing, loadTemplates, loadPublicTemplates, loadFavorites, transformTemplateToMediaItem], ) const handleRefresh = useCallback(() => { setPage(1) setHasMore(true) loadData(1, true) }, [loadData]) const handleLoadMore = useCallback(() => { if (!hasMore || loadingMore || refreshing || isLoadingRef.current) return const nextPage = page + 1 setPage(nextPage) loadData(nextPage, false) }, [page, hasMore, loadingMore, refreshing, loadData]) useEffect(() => { outerRotate.value = withRepeat(withTiming(360, { duration: 12000, easing: Easing.linear }), -1, false) }, []) const handleQuickGen = useCallback(async () => { if (!isAuthenticated) { Toast.show({ title: '请先登录' }) return } if (!selectedItem) { Toast.show({ title: '请先选择一个模板' }) return } const cost = 2 if (balance < cost) { Toast.show({ title: '余额不足,请充值' }) return } Toast.showModal( 生成同款风格将消耗 {cost} Goo 算力。 } onConfirm={async () => { Toast.hideModal() const { taskId, error } = await submitTask({ model_name: 'default', prompt: `生成与 ${selectedItem.id} 相似的内容`, img_url: selectedItem.url, }) if (error) { Toast.show({ title: `提交失败: ${error.message}` }) return } if (taskId) { Toast.show({ title: '任务提交成功,正在生成中...' }) startPolling( taskId, (result) => { Toast.show({ title: '生成成功!' }) if (user?.id) loadBalance(user.id) }, (error) => { Toast.show({ title: `生成失败: ${error.message}` }) }, ) } }} onCancel={() => Toast.hideModal()} />, {}, ) }, [isAuthenticated, balance, selectedItem, submitTask, startPolling, user?.id, loadBalance]) const openSearch = useCallback(() => setIsSearchOpen(true), []) const closeSearch = useCallback(() => { setIsSearchOpen(false) setSearchText('') }, []) // keep for parity with your original file (even if not actively used yet) const outerAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${outerRotate.value}deg` }], })) const { width: winW } = Dimensions.get('window') const itemWidth = useMemo(() => Math.floor((winW - 24 - 12 * 2) / 3), [winW]) const renderGridItem = useCallback( ({ item }: { item: MediaItem }) => ( setSelectedItem(item)} /> ), [selectedItem, itemWidth], ) const renderListFooter = useCallback(() => { if (!loadingMore) return null return ( ) }, [loadingMore]) const renderListEmpty = useCallback(() => { if (activeTab === 'like' && !isAuthenticated) { return ( 查看您收藏的模板 登录后即可查看和管理收藏 router.push('/auth')} className="mt-[12px] flex-row items-center gap-[8px] border-[3px] border-white bg-accent px-[32px] py-[12px] shadow-[4px_4px_0px_rgba(255,255,255,0.2)]" style={{ transform: [{ skewX: '-6deg' }] }} > 立即登录 ) } return ( 暂无内容 ) }, [activeTab, isAuthenticated]) const handleSearch = useCallback((text: string) => { console.log('Search for:', text) // Implement your search logic here }, []) return ( } /> item.id} numColumns={3} contentContainerStyle={{ marginTop: 12, paddingBottom: 200 }} showsVerticalScrollIndicator={false} refreshControl={} onEndReached={handleLoadMore} onEndReachedThreshold={0.5} ListFooterComponent={renderListFooter} ListEmptyComponent={renderListEmpty} /> setIsSearchOpen(false)} onSearch={handleSearch} /> ) }