expo-popcore-app/app/generateVideo.tsx

333 lines
11 KiB
TypeScript

import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import {
View,
Text,
StyleSheet,
ScrollView,
Dimensions,
Pressable,
StatusBar as RNStatusBar,
Platform,
KeyboardAvoidingView,
} from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Image } from 'expo-image'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { useTranslation } from 'react-i18next'
import { LeftArrowIcon, WhitePointsIcon } from '@/components/icon'
import UploadReferenceImageDrawer from '@/components/drawer/UploadReferenceImageDrawer'
import { StartGeneratingNotification } from '@/components/ui'
import { DynamicForm, type DynamicFormRef, type FormSchema } from '@/components/DynamicForm'
import { useTemplateActions } from '@/hooks/use-template-actions'
import { useTemplateDetail } from '@/hooks/use-template-detail'
import Toast from '@/components/ui/Toast'
const { height: screenHeight } = Dimensions.get('window')
export default function GenerateVideoScreen() {
const { t } = useTranslation()
const router = useRouter()
const params = useLocalSearchParams()
const { runTemplate, loading } = useTemplateActions()
const { data: templateDetail, loading: templateLoading, execute: fetchTemplate } = useTemplateDetail()
const [drawerVisible, setDrawerVisible] = useState(false)
const [showNotification, setShowNotification] = useState(false)
const [currentNodeId, setCurrentNodeId] = useState<string | null>(null)
const dynamicFormRef = useRef<DynamicFormRef>(null)
useEffect(() => {
if (params.templateId && typeof params.templateId === 'string') {
fetchTemplate({ id: params.templateId })
}
}, [params.templateId, fetchTemplate])
const formSchema = useMemo<FormSchema>(() => ({
startNodes: templateDetail?.formSchema?.startNodes || []
}), [templateDetail])
const handleOpenDrawer = useCallback((nodeId: string) => {
setCurrentNodeId(nodeId)
setDrawerVisible(true)
}, [])
const handleSelectImage = useCallback(async (imageUri: string, mimeType?: string, fileName?: string) => {
if (!currentNodeId) return
if (dynamicFormRef.current) {
dynamicFormRef.current.updateFieldValue(currentNodeId, imageUri, imageUri)
}
setDrawerVisible(false)
setCurrentNodeId(null)
}, [currentNodeId])
const handleFormSubmit = useCallback(async (data: Record<string, string>) => {
if (!templateDetail) return { error: { message: 'Template not found' } }
const result = await runTemplate({
templateId: templateDetail.id,
data,
})
if (result.generationId) {
setShowNotification(true)
setTimeout(() => {
setShowNotification(false)
router.back()
}, 3000)
}
return result
}, [templateDetail, runTemplate, router])
return (
<SafeAreaView
style={styles.container}
edges={Platform.OS === 'ios' ? ['top'] : ['top', 'bottom']}
>
<StatusBar style="light" />
{Platform.OS === 'android' && (
<RNStatusBar
barStyle="light-content"
backgroundColor="transparent"
translucent
/>
)}
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* 顶部导航栏 */}
<View style={styles.header}>
<Pressable
style={styles.backButton}
onPress={() => router.back()}
>
<LeftArrowIcon />
</Pressable>
</View>
{/* 主要内容区域 */}
<View style={styles.content}>
{/* 模板预览卡片 */}
<View style={styles.templatePreviewCard}>
<View style={styles.templateThumbnailContainer}>
<Image
source={templateDetail?.coverImageUrl || ''}
style={styles.templateThumbnail}
contentFit="cover"
/>
<View style={styles.templateInfo}>
<Text style={styles.templateTitle}>{templateDetail?.title}</Text>
<Text style={styles.templateDescription}>{templateDetail?.description}</Text>
</View>
</View>
{/* 价格标签 */}
<View style={styles.priceTag}>
<WhitePointsIcon />
<Text style={styles.priceText}>{templateDetail?.price || 10}</Text>
</View>
</View>
{/* 动态表单 */}
<View style={styles.formContainer}>
{templateLoading ? (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>{t('generateVideo.loading') || '加载中...'}</Text>
</View>
) : formSchema.startNodes && formSchema.startNodes.length > 0 ? (
<DynamicForm
ref={dynamicFormRef}
formSchema={formSchema}
onSubmit={handleFormSubmit}
loading={loading}
onOpenDrawer={handleOpenDrawer}
points={templateDetail?.price}
/>
) : (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>
{t('generateVideo.noFormFields') || '暂无可填写的表单项'}
</Text>
</View>
)}
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* 图片上传抽屉 */}
<UploadReferenceImageDrawer
visible={drawerVisible}
onClose={() => {
setDrawerVisible(false)
setCurrentNodeId(null)
}}
onSelectImage={handleSelectImage}
/>
{/* 通知组件 */}
{showNotification && (
<View style={styles.notificationContainer}>
<StartGeneratingNotification
count={1}
visible={showNotification}
title={t('generateVideo.startGenerating')}
message={t('generateVideo.generatingMessage')}
onPress={() => setShowNotification(false)}
style={styles.notification}
/>
</View>
)}
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090A0B',
},
keyboardAvoidingView: {
flex: 1,
},
scrollView: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollContent: {
flexGrow: 1,
backgroundColor: '#090A0B',
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: Platform.select({
ios: 17,
android: 12,
default: 17,
}),
paddingHorizontal: 12,
paddingBottom: 20,
},
backButton: {
width: 22,
height: 22,
alignItems: 'center',
justifyContent: 'center',
},
content: {
paddingHorizontal: 12,
paddingTop: 16,
backgroundColor: '#1C1E20',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
gap: 16,
minHeight: Platform.select({
ios: screenHeight - 100,
android: screenHeight - 100,
default: screenHeight - 60,
}),
},
// 模板预览卡片样式
templatePreviewCard: {
backgroundColor: '#262A31',
borderRadius: 16,
padding: 12,
borderWidth: 1,
borderColor: '#2F3134',
},
templateThumbnailContainer: {
flexDirection: 'row',
gap: 12,
alignItems: 'center',
},
templateThumbnail: {
width: 80,
height: 80,
borderRadius: 12,
backgroundColor: '#1C1E20',
},
templateInfo: {
flex: 1,
gap: 4,
},
templateTitle: {
color: '#F5F5F5',
fontSize: 16,
fontWeight: '600',
lineHeight: 22,
},
templateDescription: {
color: '#ABABAB',
fontSize: 13,
lineHeight: 18,
},
priceTag: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 6,
marginTop: 8,
paddingTop: 8,
borderTopWidth: 1,
borderTopColor: '#2F3134',
},
priceText: {
color: '#FFCF00',
fontSize: 16,
fontWeight: '600',
},
// 表单容器样式
formContainer: {
gap: 12,
},
loadingContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: 40,
},
loadingText: {
color: '#8A8A8A',
fontSize: 14,
},
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: 40,
backgroundColor: '#262A31',
borderRadius: 12,
},
emptyText: {
color: '#8A8A8A',
fontSize: 14,
textAlign: 'center',
},
// 通知容器样式
notificationContainer: {
position: 'absolute',
top: Platform.select({
ios: 60,
android: 50,
default: 60,
}),
left: 0,
right: 0,
paddingHorizontal: 8,
zIndex: 1000,
},
notification: {
width: '100%',
},
})