import { FontAwesome, Fontisto, Ionicons } from '@expo/vector-icons' import { FlashList } from '@shopify/flash-list' import { useRouter } from 'expo-router' import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' import { ActivityIndicator, Platform, RefreshControl } from 'react-native' import { useAnimatedStyle } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { imgPicker } from '@/@share/apis/imgPicker' import { Block, Img, Input, Text } from '@/@share/components' import BannerSection from '@/components/BannerSection' import { useFileUpload } from '@/hooks/actions' import { useTemplates } from '@/hooks/data/use-templates' import { screenHeight, screenWidth } from '@/utils' const CATEGORY_ID = process.env.EXPO_PUBLIC_GENERATE_GROUP_ID /** ========================= * Entry page * ========================= */ export default function Generate() { const router = useRouter() const env = process.env.EXPO_PUBLIC_ENV const [prompt, setPrompt] = useState(`${env} update`) const [selectedTemplateId, setSelectedTemplateId] = useState('') const [meImg, setMeImg] = useState('') const [friendImg, setFriendImg] = useState('') const templates = useTemplates() const { uploadFile, loading: uploadLoading } = useFileUpload() useEffect(() => { templates.execute({ categoryId: CATEGORY_ID, page: 1, limit: 12, sortBy: 'createdAt', sortOrder: 'desc' }) }, []) const displayTemplates = useMemo(() => { const regular = templates.data?.templates || [] const all = regular return all.map( (t: any): Template => ({ id: t.id, name: t.title, image: t.coverImageUrl, type: 'video' as const, price: t.price, data: t, }), ) }, [templates.data]) const selectedTemplate = useMemo(() => { return displayTemplates.find((t) => t.id === selectedTemplateId) }, [displayTemplates, selectedTemplateId]) useEffect(() => { if (displayTemplates.length > 0 && !selectedTemplateId) { setSelectedTemplateId(displayTemplates[0].id) } }, [displayTemplates, selectedTemplateId]) const handleSearch = useCallback(() => { router.push('/searchTemplate') }, [router]) const handleGenerate = useCallback(() => { if (!selectedTemplate) return setTimeout(() => {}, 2000) }, [selectedTemplate]) const pickImage = useCallback(async (target: 'me' | 'friend') => { const assetList = (await imgPicker({ maxImages: 1, resultType: 'asset' })) as string[] const result = assetList[0] as any if (!result) return const uri = result?.uri if (target === 'me') setMeImg(uri) else setFriendImg(uri) const file = { name: result.fileName || `image_${Date.now()}.jpg`, type: result.mimeType || 'image/jpeg', uri: Platform.OS === 'android' ? result.uri : result.uri.replace('file://', ''), } const formData = new FormData() formData.append('file', file as any) const { url, error } = await uploadFile(file as any) // console.log('pickImage---------url:', url, 'error:', error) if (error || !url) { return } if (target === 'me') setMeImg(url) else setFriendImg(url) }, []) const handleRandom = useCallback(() => { if (displayTemplates.length === 0) return const random = displayTemplates[Math.floor(Math.random() * displayTemplates.length)] setSelectedTemplateId(random.id) }, [displayTemplates]) const handleSelectTemplate = useCallback((t: Template) => { setSelectedTemplateId(t.id) }, []) const onPickMe = useCallback(() => pickImage('me'), [pickImage]) const onPickFriend = useCallback(() => pickImage('friend'), [pickImage]) const isLoading = templates.loading const isLoadingMore = templates.loadingMore const hasError = templates.error const handleRetry = useCallback(() => { templates.refetch({ categoryId: CATEGORY_ID, page: 1, limit: 20, sortBy: 'createdAt', sortOrder: 'desc' }) }, [templates]) const [refreshing, setRefreshing] = useState(false) const onRefresh = useCallback(async () => { setRefreshing(true) try { await templates.refetch({ categoryId: CATEGORY_ID, page: 1, limit: 20, sortBy: 'createdAt', sortOrder: 'desc' }) } finally { setRefreshing(false) } }, [templates]) const onLoadMore = useCallback(() => { templates.loadMore({ categoryId: CATEGORY_ID, limit: 20, sortBy: 'createdAt', sortOrder: 'desc' }) }, [templates]) const itemWidth = useMemo(() => { return Math.floor((screenWidth - 24 - 12 * 2) / 3) }, []) const renderItem = useCallback( ({ item }: { item: Template }) => { const isSelected = selectedTemplateId === item.id return ( handleSelectTemplate(item)} /> ) }, [selectedTemplateId, itemWidth, handleSelectTemplate], ) const ListHeader = useMemo( () => (
), [handleSearch, friendImg, meImg, onPickFriend, onPickMe, prompt, handleRandom], ) const ListFooter = useMemo(() => { if (isLoadingMore) { return ( ) } return }, [isLoadingMore]) const ListEmpty = useMemo(() => { if (hasError) { return ( 加载失败 重试 ) } return ( 暂无模板 ) }, [isLoading, hasError, handleRetry]) return ( item.id} ListEmptyComponent={ListEmpty} ListFooterComponent={ListFooter} ListHeaderComponent={ListHeader} numColumns={3} renderItem={renderItem} showsVerticalScrollIndicator={false} refreshControl={ } onEndReached={onLoadMore} onEndReachedThreshold={0.3} /> ) } /** ========================= * Small memo components * ========================= */ type HeaderProps = { onSearch: () => void } const Header = memo(function Header({ onSearch }) { return ( 创造终端 GEN_STUDIO 创造终端 ) }) type UploadCardProps = { variant: 'me' | 'friend' img: string onPick: () => void } const UploadCard = memo(function UploadCard({ variant, img, onPick }) { const isMe = variant === 'me' return ( {isMe ? ( ) : ( 朋友 )} {img ? ( ) : ( {isMe ? ( + ) : ( + )} 点击上传 )} ) }) type UploadSectionProps = { meImg: string friendImg: string onPickMe: () => void onPickFriend: () => void } const UploadSection = memo(function UploadSection({ meImg, friendImg, onPickMe, onPickFriend }) { return ( ) }) type PromptSectionProps = { prompt: string onChangePrompt: (v: string) => void } const PromptSection = memo(function PromptSection({ prompt, onChangePrompt }) { return ( 提示词 ) }) type Template = { id: string name: string image: string type: 'video' price?: number data?: any } type TemplateItemProps = { item: Template itemWidth: number isSelected: boolean onSelect: () => void } const TemplateItem = memo(function TemplateItem({ item, itemWidth, isSelected, onSelect }) { return ( {isSelected && } {isSelected && } {item.name} {item.type === 'video' && ( )} ) }) type TemplateSectionHeaderProps = { onRandom: () => void } const TemplateSectionHeader = memo(function TemplateSectionHeader({ onRandom }) { const style = useAnimatedStyle(() => ({ transform: [{ skewX: '-6deg' }], backgroundColor: '#FFE500', })) return ( 视频模版 换一波 ) }) type GenerateSectionProps = { selectedTemplate: Template | undefined onGenerate: () => void } const GenerateSection = memo(function GenerateSection({ selectedTemplate, onGenerate }) { const insets = useSafeAreaInsets() if (!selectedTemplate) return null const price = selectedTemplate.price || selectedTemplate.data?.price || 0 return ( {/* 左侧文字 */} 立即生成 开始创作 {/* 右侧 Goo 标签 */} {price} Goo ) })