expo-popcore-app/components/drawer/UploadReferenceImageDrawer.tsx

384 lines
12 KiB
TypeScript

import { useState, useRef, useMemo, useCallback, useEffect } from 'react'
import {
View,
Text,
StyleSheet,
Pressable,
FlatList,
useWindowDimensions,
Platform,
} from 'react-native'
import { Image } from 'expo-image'
import { useTranslation } from 'react-i18next'
import BottomSheet, { BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'
import { CloseIcon, DownArrowIcon } from '@/components/icon'
import AIGenerationRecordDrawer from './AIGenerationRecordDrawer'
interface UploadReferenceImageDrawerProps {
visible: boolean
onClose: () => void
onSelectImage?: (imageUri: any) => void
}
type TabType = 'ai-record' | 'recent'
// 模拟图片数据
const mockImages = Array.from({ length: 120 }, (_, i) => ({
id: i + 1,
uri: require('@/assets/images/android-icon-background.png'),
}))
export default function UploadReferenceImageDrawer({
visible,
onClose,
onSelectImage,
}: UploadReferenceImageDrawerProps) {
const { t } = useTranslation()
const { width: screenWidth } = useWindowDimensions()
const bottomSheetRef = useRef<BottomSheet>(null)
const [activeTab, setActiveTab] = useState<TabType>('ai-record')
const [selectedFilter, setSelectedFilter] = useState<'all' | 'face'>('all')
const [aiRecordDrawerVisible, setAiRecordDrawerVisible] = useState(false)
const snapPoints = useMemo(() => ['98%'], [])
useEffect(() => {
if (visible) {
bottomSheetRef.current?.expand()
} else {
bottomSheetRef.current?.close()
}
}, [visible])
const handleSheetChanges = useCallback((index: number) => {
if (index === -1) {
onClose()
}
}, [onClose])
const handleImageSelect = (imageSource: any) => {
onSelectImage?.(imageSource)
onClose()
}
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
opacity={0.5}
/>
),
[]
)
const renderImageItem = ({ item, index }: { item: typeof mockImages[0]; index: number }) => {
const paddingHorizontal = 0
const gap = 2
const itemWidth = (screenWidth - paddingHorizontal * 2 - gap * 2) / 3
const isLastRow = index >= Math.floor(mockImages.length / 3) * 3
return (
<Pressable
style={[
styles.imageItem,
{
width: itemWidth,
marginRight: (index + 1) % 3 !== 0 ? gap : 0,
marginBottom: isLastRow ? 0 : gap,
},
]}
onPress={() => handleImageSelect(item.uri)}
>
<Image
source={item.uri}
style={styles.image}
contentFit="cover"
/>
</Pressable>
)
}
return (
<>
<BottomSheet
ref={bottomSheetRef}
index={visible ? 0 : -1}
snapPoints={snapPoints}
onChange={handleSheetChanges}
enablePanDownToClose
backgroundStyle={styles.bottomSheetBackground}
handleIndicatorStyle={styles.handleIndicator}
backdropComponent={renderBackdrop}
>
<BottomSheetView style={styles.container}>
{/* 顶部标题栏 */}
<View style={styles.header}>
<View >
<Text style={styles.title}>{t('uploadReference.selectImage')}</Text>
<Text style={styles.title}>{t('uploadReference.generateAIVideo')}</Text>
</View>
<Pressable
style={styles.closeButton}
onPress={onClose}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<CloseIcon />
</Pressable>
</View>
{/* 标签切换 */}
<View style={styles.tabContainer}>
<Pressable
style={[styles.tab, activeTab === 'ai-record' && styles.tabActive]}
onPress={() => {
setActiveTab('ai-record')
setAiRecordDrawerVisible(true)
}}
>
<View style={styles.tabIconContainer}>
<View style={styles.tabIcon} />
</View>
<Text style={[styles.tabText, activeTab === 'ai-record' && styles.tabTextActive]}>
{t('uploadReference.aiRecord')}
</Text>
</Pressable>
<Pressable
style={[styles.tab, activeTab === 'recent' && styles.tabActive]}
onPress={() => {
setActiveTab('recent')
setAiRecordDrawerVisible(true)
}}
>
<View style={styles.tabIconContainer}>
<View style={styles.tabIconSmall} />
</View>
<Text style={[styles.tabText, activeTab === 'recent' && styles.tabTextActive]}>
{t('uploadReference.recentUsed')}
</Text>
</Pressable>
</View>
{/* 筛选区域 */}
<View style={styles.filterContainer}>
<Pressable
style={styles.categoryButton}
onPress={() => {
// 可以展开分类选择
}}
>
<Text style={styles.categoryText}>{t('uploadReference.recentProject')}</Text>
<DownArrowIcon />
</Pressable>
<View style={styles.filterButtons}>
<Pressable
style={[
styles.filterButton,
selectedFilter === 'all' && styles.filterButtonActive,
]}
onPress={() => setSelectedFilter('all')}
>
<Text
style={[
styles.filterButtonText,
selectedFilter === 'all' && styles.filterButtonTextActive,
]}
>
{t('uploadReference.all')}
</Text>
</Pressable>
<Pressable
style={[
styles.filterButton,
selectedFilter === 'face' && styles.filterButtonActive,
]}
onPress={() => setSelectedFilter('face')}
>
<Text
style={[
styles.filterButtonText,
selectedFilter === 'face' && styles.filterButtonTextActive,
]}
>
{t('uploadReference.face')}
</Text>
</Pressable>
</View>
</View>
{/* 图片网格 */}
<FlatList
data={mockImages}
renderItem={renderImageItem}
keyExtractor={(item) => item.id.toString()}
numColumns={3}
showsVerticalScrollIndicator={false}
removeClippedSubviews={Platform.OS === 'android'}
maxToRenderPerBatch={Platform.OS === 'ios' ? 10 : 5}
updateCellsBatchingPeriod={Platform.OS === 'ios' ? 50 : 100}
initialNumToRender={Platform.OS === 'ios' ? 15 : 10}
windowSize={Platform.OS === 'ios' ? 10 : 5}
getItemLayout={(data, index) => {
const gap = 2
const itemWidth = (screenWidth - gap * 2) / 3
const rowIndex = Math.floor(index / 3)
return {
length: itemWidth,
offset: rowIndex * (itemWidth + gap),
index,
}
}}
/>
</BottomSheetView>
</BottomSheet>
<AIGenerationRecordDrawer
visible={aiRecordDrawerVisible}
onClose={() => setAiRecordDrawerVisible(false)}
onSelectImage={(imageUri) => {
handleImageSelect(imageUri)
}}
type={activeTab}
/>
</>
)
}
const styles = StyleSheet.create({
bottomSheetBackground: {
backgroundColor: '#16181B',
},
handleIndicator: {
backgroundColor: '#666666',
},
container: {
flex: 1,
backgroundColor: '#16181B',
paddingTop: 24,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingBottom: 20,
},
title: {
color: '#F5F5F5',
fontSize: 20,
fontWeight: '600',
},
closeButton: {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
tabContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
gap: 8,
marginBottom: 24,
},
tab: {
flex: 1,
height: 52,
backgroundColor: '#272A30',
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
},
tabActive: {
backgroundColor: '#262A31',
},
tabIconContainer: {
width: 28,
height: 28,
alignItems: 'center',
justifyContent: 'center',
},
tabIcon: {
width: 27,
height: 27,
borderRadius: 6,
backgroundColor: '#4A4C4F',
},
tabIconSmall: {
width: 26,
height: 26,
borderRadius: 6,
backgroundColor: '#4A4C4F',
},
tabText: {
color: '#F5F5F5',
fontSize: 12,
fontWeight: '600',
},
tabTextActive: {
color: '#F5F5F5',
},
filterContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
marginBottom: 9,
},
categoryButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
categoryText: {
color: '#F5F5F5',
fontSize: 14,
fontWeight: '600',
},
filterButtons: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#1C1E22',
borderRadius: 100,
height: 32,
padding: 3,
},
filterButton: {
paddingHorizontal: 12,
paddingVertical: 4,
minWidth: 48,
alignItems: 'center',
justifyContent: 'center',
},
filterButtonActive: {
backgroundColor: '#F5F5F5',
height: 24,
borderRadius: 100,
},
filterButtonText: {
color: '#CCCCCC',
fontSize: 12,
},
filterButtonTextActive: {
color: '#000000',
},
// imageGrid: {
// // paddingHorizontal: 16,
// // paddingBottom: 20,
// },
imageItem: {
// aspectRatio = width / height
// 1 : 1.3 (width : height) => 1 / 1.3
aspectRatio: 1 / 1.3,
overflow: 'hidden',
backgroundColor: '#262A31',
},
image: {
width: '100%',
height: '100%',
},
})