Initial commit: expo-popcore-app
This commit is contained in:
383
components/drawer/UploadReferenceImageDrawer.tsx
Normal file
383
components/drawer/UploadReferenceImageDrawer.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
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%',
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user