expo-popcore-app/app/generationRecord.tsx

314 lines
9.7 KiB
TypeScript

import {
View,
Text,
StyleSheet,
FlatList,
Dimensions,
Pressable,
StatusBar as RNStatusBar,
Alert,
} from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Image } from 'expo-image'
import { useRouter } from 'expo-router'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { LeftArrowIcon, DeleteIcon, EditIcon, ChangeIcon, WhiteStarIcon } from '@/components/icon'
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog'
import LoadingState from '@/components/LoadingState'
import ErrorState from '@/components/ErrorState'
import PaginationLoader from '@/components/PaginationLoader'
import {
useTemplateGenerations,
useDeleteGeneration,
type TemplateGeneration,
} from '@/hooks'
const { width: screenWidth } = Dimensions.get('window')
export default function GenerationRecordScreen() {
const { t } = useTranslation()
const router = useRouter()
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const { generations, loading, loadingMore, error, execute, refetch, loadMore, hasMore } = useTemplateGenerations()
const { deleteGeneration, loading: deleting } = useDeleteGeneration()
useEffect(() => {
execute({ page: 1, limit: 20 })
}, [execute])
const handleDelete = async () => {
if (!selectedId) return
const { data, error } = await deleteGeneration(selectedId)
if (error) {
Alert.alert(t('common.error'), error.message || t('generationRecord.deleteError'))
} else {
Alert.alert(t('common.success'), data?.message || t('generationRecord.deleteSuccess'))
// 刷新列表
refetch({ page: 1, limit: 20 })
}
setDeleteDialogOpen(false)
setSelectedId(null)
}
const handleRefresh = () => {
refetch({ page: 1, limit: 20 })
}
const handleLoadMore = () => {
if (hasMore && !loadingMore) {
loadMore({ limit: 20 })
}
}
if (loading && generations.length === 0) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.header}>
<Pressable style={styles.backButton} onPress={() => router.back()}>
<LeftArrowIcon />
</Pressable>
<Text style={styles.headerTitle}>{t('generationRecord.title')}</Text>
<View style={styles.headerSpacer} />
</View>
<LoadingState testID="loading-state" />
</SafeAreaView>
)
}
if (error && generations.length === 0) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.header}>
<Pressable style={styles.backButton} onPress={() => router.back()}>
<LeftArrowIcon />
</Pressable>
<Text style={styles.headerTitle}>{t('generationRecord.title')}</Text>
<View style={styles.headerSpacer} />
</View>
<ErrorState testID="error-state" message={error.message} onRetry={handleRefresh} />
</SafeAreaView>
)
}
const renderItem = ({ item }: { item: TemplateGeneration }) => (
<View style={styles.itemContainer}>
<View style={styles.categorySection}>
<View style={styles.categoryIcon}>
<WhiteStarIcon />
</View>
<Text style={styles.categoryText}>{item.template?.title || t('generationRecord.aiVideo')}</Text>
</View>
<View style={styles.originalImageSection}>
<Text style={styles.originalImageLabel}>{t('generationRecord.originalImage')}</Text>
<View style={styles.originalImageDivider} />
</View>
<View style={styles.imageContainer}>
<Image
source={{ uri: item.resultUrl?.[0] || item.originalUrl }}
style={styles.mainImage}
contentFit="cover"
/>
</View>
<Text style={styles.durationText}>00:03</Text>
<View style={styles.actionButtons}>
<Pressable style={styles.actionButton} onPress={() => {
router.push({
pathname: '/generateVideo' as any,
params: { template: JSON.stringify(item.template) },
})
}}>
<EditIcon />
<Text style={styles.actionButtonText}>{t('generationRecord.reEdit')}</Text>
</Pressable>
<Pressable style={styles.actionButton}>
<ChangeIcon />
<Text style={styles.actionButtonText}>{t('generationRecord.regenerate')}</Text>
</Pressable>
<Pressable
style={[styles.deleteButton, deleting && selectedId === item.id && styles.deleteButtonDisabled]}
onPress={() => {
if (!deleting) {
setSelectedId(item.id)
setDeleteDialogOpen(true)
}
}}
disabled={deleting && selectedId === item.id}
>
<DeleteIcon />
</Pressable>
</View>
</View>
)
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.header}>
<Pressable style={styles.backButton} onPress={() => router.back()}>
<LeftArrowIcon />
</Pressable>
<Text style={styles.headerTitle}>{t('generationRecord.title')}</Text>
<View style={styles.headerSpacer} />
</View>
<FlatList
testID="generation-list"
data={generations}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
ListFooterComponent={loadingMore ? <PaginationLoader testID="pagination-loader" /> : null}
/>
<DeleteConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onConfirm={handleDelete}
/>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollContent: {
paddingBottom: 20,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 16,
paddingBottom: 20,
},
backButton: {
width: 22,
height: 22,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
color: '#F5F5F5',
fontSize: 14,
fontWeight: '600',
flex: 1,
textAlign: 'center',
},
headerSpacer: {
width: 22,
},
itemContainer: {
marginBottom: 24,
},
categorySection: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 8,
gap: 4,
},
categoryIcon: {
width: 16,
height: 16,
alignItems: 'center',
justifyContent: 'center',
},
categoryText: {
color: '#F5F5F5',
fontSize: 14,
fontWeight: '600',
},
originalImageSection: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 8,
},
originalImageLabel: {
color: '#8A8A8A',
fontSize: 13,
marginRight: 6,
},
originalImageDivider: {
width: 1,
height: 12,
backgroundColor: '#FFFFFF33',
},
imageContainer: {
width: screenWidth - 24,
height: (screenWidth - 24) * 1.32,
marginHorizontal: 12,
marginBottom: 14,
borderRadius: 16,
overflow: 'hidden',
backgroundColor: '#1C1E22',
},
mainImage: {
width: '100%',
height: '100%',
},
durationText: {
paddingLeft: 28,
color: '#F5F5F5',
fontSize: 13,
marginBottom: 22,
fontWeight: '500',
},
actionButtons: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
gap: 8,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 8,
paddingVertical: 6,
borderRadius: 8,
backgroundColor: '#1C1E22',
height: 32,
},
actionButtonText: {
color: '#F5F5F5',
fontSize: 11,
fontWeight: '500',
},
deleteButton: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: '#1C1E22',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 'auto',
},
deleteButtonDisabled: {
opacity: 0.5,
},
})