import { Ionicons } from '@expo/vector-icons' import type { GetUserFavoritesResponse } from '@repo/sdk' import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components' import Img from '@share/components/Img' import { FlashList } from '@shopify/flash-list' import { LinearGradient } from 'expo-linear-gradient' import { router } from 'expo-router' import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ActivityIndicator, RefreshControl, TextInput } from 'react-native' import BannerSection from '@/components/BannerSection' import { useTemplateActions } from '@/hooks/actions/use-template-actions' import { useAuth } from '@/hooks/core/use-auth' import { useUserBalance } from '@/hooks/core/use-user-balance' import { useTemplates } from '@/hooks/data' import { useFavoriteTemplates } from '@/hooks/data/use-favorite-templates' import { usePublicTemplates } from '@/hooks/data/use-public-templates' import { screenWidth } from '@/utils' import { cn } from '@/utils/cn' const BACKGROUND_VIDEOS = [ 'https://cdn.roasmax.cn/material/b46f380532e14cf58dd350dbacc7c34a.mp4', 'https://cdn.roasmax.cn/material/992e6c5d940c42feb71c27e556b754c0.mp4', 'https://cdn.roasmax.cn/material/e4947477843f4067be7c37569a33d17b.mp4', ] const CATEGORY_ID = `cat_iw83x5bg54fmjgvciju` type MediaItem = { id: string type: 'image' | 'video' url: string poster?: string likeCount: number } type ActiveTab = 'gen' | '' | 'new' | 'like' 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)} > {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, 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 { runTemplate } = useTemplateActions() 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]) useEffect(() => { if (isAuthenticated && user?.id) { loadBalance() } }, [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 = 21 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]) 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{' '} 算力。 } onCancel={() => Toast.hideModal()} onConfirm={async () => { Toast.hideModal() // 要传 扣费返回的凭证 const { generationId, error } = await runTemplate({ templateId: selectedItem.id, data: {}, originalUrl: selectedItem.url, }) if (error || !generationId) { Toast.show({ title: error?.message || '生成失败' }) return } Toast.show({ title: '生成成功!' }) if (user?.id) loadBalance() }} />, {}, ) }, [isAuthenticated, balance, selectedItem, runTemplate, user?.id, loadBalance]) const openSearch = useCallback(() => setIsSearchOpen(true), []) const closeSearch = useCallback(() => { setIsSearchOpen(false) setSearchText('') }, []) const itemWidth = Math.floor((screenWidth - 24 - 12 * 2) / 3) const renderGridItem = useCallback( ({ item }: { item: MediaItem }) => { const isSelected = selectedItem?.id === item.id return ( setSelectedItem(item)} /> ) }, [selectedItem?.id], ) const renderListFooter = useCallback(() => { if (!loadingMore) return null return ( ) }, [loadingMore]) const renderListEmpty = useCallback(() => { if (activeTab === 'like' && !isAuthenticated) { return ( 查看您收藏的模板 登录后即可查看和管理收藏 router.push('/auth')} > 立即登录 ) } return ( 暂无内容 ) }, [activeTab, isAuthenticated]) const handleSearch = useCallback((text: string) => { console.log('Search for:', text) // Implement your search logic here }, []) return ( } selectedItem={selectedItem} onQuickGen={handleQuickGen} /> item.id} ListEmptyComponent={renderListEmpty} ListFooterComponent={renderListFooter} numColumns={3} renderItem={renderGridItem} showsVerticalScrollIndicator={false} refreshControl={ } onEndReached={handleLoadMore} onEndReachedThreshold={0.5} /> setIsSearchOpen(false)} onSearch={handleSearch} /> ) }