316 lines
9.7 KiB
TypeScript
316 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 })
|
|
}
|
|
}
|
|
|
|
|
|
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>
|
|
)
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
|
|
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,
|
|
},
|
|
})
|
|
|