1b6261296S猫头猫import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; 2b6261296S猫头猫import {Pressable, StyleSheet, View} from 'react-native'; 3b6261296S猫头猫import rpx from '@/utils/rpx'; 4b6261296S猫头猫import ThemeText from '@/components/base/themeText'; 5b6261296S猫头猫import { 6b6261296S猫头猫 ExternalStorageDirectoryPath, 7b6261296S猫头猫 readDir, 8b6261296S猫头猫 getAllExternalFilesDirs, 9b6261296S猫头猫 exists, 10b6261296S猫头猫} from 'react-native-fs'; 11b6261296S猫头猫import {FlatList} from 'react-native-gesture-handler'; 12b6261296S猫头猫import useColors from '@/hooks/useColors'; 13b6261296S猫头猫import Color from 'color'; 14b6261296S猫头猫import IconButton from '@/components/base/iconButton'; 15b6261296S猫头猫import FileItem from './fileItem'; 16b6261296S猫头猫import Empty from '@/components/base/empty'; 17b6261296S猫头猫import useHardwareBack from '@/hooks/useHardwareBack'; 18b6261296S猫头猫import {useNavigation} from '@react-navigation/native'; 19b6261296S猫头猫import Loading from '@/components/base/loading'; 20b6261296S猫头猫import {useParams} from '@/entry/router'; 21b6261296S猫头猫import StatusBar from '@/components/base/statusBar'; 22*e0caea6eS猫头猫import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 23*e0caea6eS猫头猫import globalStyle from '@/constants/globalStyle'; 24b6261296S猫头猫 25b6261296S猫头猫interface IPathItem { 26b6261296S猫头猫 path: string; 27b6261296S猫头猫 parent: null | IPathItem; 28b6261296S猫头猫} 29b6261296S猫头猫 30b6261296S猫头猫interface IFileItem { 31b6261296S猫头猫 path: string; 32b6261296S猫头猫 type: 'file' | 'folder'; 33b6261296S猫头猫} 34b6261296S猫头猫 35b6261296S猫头猫const ITEM_HEIGHT = rpx(96); 36b6261296S猫头猫 37b6261296S猫头猫export default function FileSelector() { 38b6261296S猫头猫 const { 39b6261296S猫头猫 fileType = 'file-and-folder', 40b6261296S猫头猫 multi = true, 41b6261296S猫头猫 actionText = '确定', 42b6261296S猫头猫 matchExtension, 43b6261296S猫头猫 onAction, 44b6261296S猫头猫 } = useParams<'file-selector'>() ?? {}; 45b6261296S猫头猫 46b6261296S猫头猫 const [currentPath, setCurrentPath] = useState<IPathItem>({ 47b6261296S猫头猫 path: '/', 48b6261296S猫头猫 parent: null, 49b6261296S猫头猫 }); 50b6261296S猫头猫 const currentPathRef = useRef<IPathItem>(currentPath); 51b6261296S猫头猫 const [filesData, setFilesData] = useState<IFileItem[]>([]); 52b6261296S猫头猫 const [checkedItems, setCheckedItems] = useState<IFileItem[]>([]); 5315a52c01S猫头猫 5415a52c01S猫头猫 const checkedPaths = useMemo( 5515a52c01S猫头猫 () => checkedItems.map(_ => _.path), 56b6261296S猫头猫 [checkedItems], 57b6261296S猫头猫 ); 58b6261296S猫头猫 const navigation = useNavigation(); 59b6261296S猫头猫 const colors = useColors(); 60b6261296S猫头猫 const [loading, setLoading] = useState(false); 61b6261296S猫头猫 62b6261296S猫头猫 useEffect(() => { 63b6261296S猫头猫 (async () => { 64b6261296S猫头猫 // 路径变化时,重新读取 65b6261296S猫头猫 setLoading(true); 66b6261296S猫头猫 try { 67b6261296S猫头猫 if (currentPath.path === '/') { 68b6261296S猫头猫 try { 69b6261296S猫头猫 const allExt = await getAllExternalFilesDirs(); 70b6261296S猫头猫 if (allExt.length > 1) { 71b6261296S猫头猫 const sdCardPaths = allExt.map(sdp => 72b6261296S猫头猫 sdp.substring(0, sdp.indexOf('/Android')), 73b6261296S猫头猫 ); 74b6261296S猫头猫 if ( 75b6261296S猫头猫 ( 76b6261296S猫头猫 await Promise.all( 77b6261296S猫头猫 sdCardPaths.map(_ => exists(_)), 78b6261296S猫头猫 ) 79b6261296S猫头猫 ).every(val => val) 80b6261296S猫头猫 ) { 81b6261296S猫头猫 setFilesData( 82b6261296S猫头猫 sdCardPaths.map(_ => ({ 83b6261296S猫头猫 type: 'folder', 84b6261296S猫头猫 path: _, 85b6261296S猫头猫 })), 86b6261296S猫头猫 ); 87b6261296S猫头猫 } 88b6261296S猫头猫 } else { 89b6261296S猫头猫 setCurrentPath({ 90b6261296S猫头猫 path: ExternalStorageDirectoryPath, 91b6261296S猫头猫 parent: null, 92b6261296S猫头猫 }); 93b6261296S猫头猫 return; 94b6261296S猫头猫 } 95b6261296S猫头猫 } catch { 96b6261296S猫头猫 setCurrentPath({ 97b6261296S猫头猫 path: ExternalStorageDirectoryPath, 98b6261296S猫头猫 parent: null, 99b6261296S猫头猫 }); 100b6261296S猫头猫 return; 101b6261296S猫头猫 } 102b6261296S猫头猫 } else { 103b6261296S猫头猫 const res = (await readDir(currentPath.path)) ?? []; 104b6261296S猫头猫 let folders: IFileItem[] = []; 105b6261296S猫头猫 let files: IFileItem[] = []; 106b6261296S猫头猫 if ( 107b6261296S猫头猫 fileType === 'folder' || 108b6261296S猫头猫 fileType === 'file-and-folder' 109b6261296S猫头猫 ) { 110b6261296S猫头猫 folders = res 111b6261296S猫头猫 .filter(_ => _.isDirectory()) 112b6261296S猫头猫 .map(_ => ({ 113b6261296S猫头猫 type: 'folder', 114b6261296S猫头猫 path: _.path, 115b6261296S猫头猫 })); 116b6261296S猫头猫 } 117b6261296S猫头猫 if (fileType === 'file' || fileType === 'file-and-folder') { 118b6261296S猫头猫 files = res 119b6261296S猫头猫 .filter( 120b6261296S猫头猫 _ => 121b6261296S猫头猫 _.isFile() && 122b6261296S猫头猫 (matchExtension 123b6261296S猫头猫 ? matchExtension(_.path) 124b6261296S猫头猫 : true), 125b6261296S猫头猫 ) 126b6261296S猫头猫 .map(_ => ({ 127b6261296S猫头猫 type: 'file', 128b6261296S猫头猫 path: _.path, 129b6261296S猫头猫 })); 130b6261296S猫头猫 } 131b6261296S猫头猫 setFilesData([...folders, ...files]); 132b6261296S猫头猫 } 133b6261296S猫头猫 } catch { 134b6261296S猫头猫 setFilesData([]); 135b6261296S猫头猫 } 136b6261296S猫头猫 setLoading(false); 137b6261296S猫头猫 currentPathRef.current = currentPath; 138b6261296S猫头猫 })(); 139b6261296S猫头猫 }, [currentPath.path]); 140b6261296S猫头猫 141b6261296S猫头猫 useHardwareBack(() => { 142b6261296S猫头猫 // 注意闭包 143b6261296S猫头猫 const _currentPath = currentPathRef.current; 144b6261296S猫头猫 if (_currentPath.parent !== null) { 145b6261296S猫头猫 setCurrentPath(_currentPath.parent); 146b6261296S猫头猫 } else { 147b6261296S猫头猫 navigation.goBack(); 148b6261296S猫头猫 } 149b6261296S猫头猫 return true; 150b6261296S猫头猫 }); 151b6261296S猫头猫 152b6261296S猫头猫 const selectPath = useCallback((item: IFileItem, nextChecked: boolean) => { 153b6261296S猫头猫 if (multi) { 154b6261296S猫头猫 setCheckedItems(prev => { 155b6261296S猫头猫 if (nextChecked) { 156b6261296S猫头猫 return [...prev, item]; 157b6261296S猫头猫 } else { 158b6261296S猫头猫 return prev.filter(_ => _ !== item); 159b6261296S猫头猫 } 160b6261296S猫头猫 }); 161b6261296S猫头猫 } else { 162b6261296S猫头猫 setCheckedItems(nextChecked ? [item] : []); 163b6261296S猫头猫 } 164b6261296S猫头猫 }, []); 165b6261296S猫头猫 166b6261296S猫头猫 const renderItem = ({item}: {item: IFileItem}) => ( 167b6261296S猫头猫 <FileItem 168b6261296S猫头猫 path={item.path} 169b6261296S猫头猫 type={item.type} 170b6261296S猫头猫 parentPath={currentPath.path} 171b6261296S猫头猫 onItemPress={currentChecked => { 172b6261296S猫头猫 if (item.type === 'folder') { 173b6261296S猫头猫 setCurrentPath(prev => ({ 174b6261296S猫头猫 parent: prev, 175b6261296S猫头猫 path: item.path, 176b6261296S猫头猫 })); 177b6261296S猫头猫 } else { 178b6261296S猫头猫 selectPath(item, !currentChecked); 179b6261296S猫头猫 } 180b6261296S猫头猫 }} 18115a52c01S猫头猫 checked={checkedPaths.includes(item.path)} 182b6261296S猫头猫 onCheckedChange={checked => { 183b6261296S猫头猫 selectPath(item, checked); 184b6261296S猫头猫 }} 185b6261296S猫头猫 /> 186b6261296S猫头猫 ); 187b6261296S猫头猫 188b6261296S猫头猫 return ( 189*e0caea6eS猫头猫 <VerticalSafeAreaView style={globalStyle.fwflex1}> 190b6261296S猫头猫 <StatusBar /> 191b6261296S猫头猫 <View style={[style.header, {backgroundColor: colors.primary}]}> 192b6261296S猫头猫 <IconButton 193b6261296S猫头猫 size="small" 194b6261296S猫头猫 name="keyboard-backspace" 195b6261296S猫头猫 onPress={() => { 196b6261296S猫头猫 // 返回上一级 197b6261296S猫头猫 if (currentPath.parent !== null) { 198b6261296S猫头猫 setCurrentPath(currentPath.parent); 199b6261296S猫头猫 } 200b6261296S猫头猫 }} 201b6261296S猫头猫 /> 202b6261296S猫头猫 <ThemeText 203b6261296S猫头猫 numberOfLines={2} 204b6261296S猫头猫 ellipsizeMode="head" 205b6261296S猫头猫 style={style.headerPath}> 206b6261296S猫头猫 {currentPath.path} 207b6261296S猫头猫 </ThemeText> 208b6261296S猫头猫 </View> 209b6261296S猫头猫 {loading ? ( 210b6261296S猫头猫 <Loading /> 211b6261296S猫头猫 ) : ( 212b6261296S猫头猫 <> 213b6261296S猫头猫 <FlatList 214b6261296S猫头猫 ListEmptyComponent={Empty} 215*e0caea6eS猫头猫 style={globalStyle.fwflex1} 216b6261296S猫头猫 data={filesData} 217b6261296S猫头猫 getItemLayout={(_, index) => ({ 218b6261296S猫头猫 length: ITEM_HEIGHT, 219b6261296S猫头猫 offset: ITEM_HEIGHT * index, 220b6261296S猫头猫 index, 221b6261296S猫头猫 })} 222b6261296S猫头猫 renderItem={renderItem} 223b6261296S猫头猫 /> 224b6261296S猫头猫 </> 225b6261296S猫头猫 )} 226b6261296S猫头猫 <Pressable 227b6261296S猫头猫 onPress={async () => { 228b6261296S猫头猫 if (checkedItems.length) { 229b6261296S猫头猫 const shouldBack = await onAction?.(checkedItems); 230b6261296S猫头猫 if (shouldBack) { 231b6261296S猫头猫 navigation.goBack(); 232b6261296S猫头猫 } 233b6261296S猫头猫 } 234b6261296S猫头猫 }}> 235b6261296S猫头猫 <View 236b6261296S猫头猫 style={[ 237b6261296S猫头猫 style.scanBtn, 238b6261296S猫头猫 { 239b6261296S猫头猫 backgroundColor: Color(colors.primary) 240b6261296S猫头猫 .alpha(0.8) 241b6261296S猫头猫 .toString(), 242b6261296S猫头猫 }, 243b6261296S猫头猫 ]}> 244b6261296S猫头猫 <ThemeText 245b6261296S猫头猫 fontColor={ 246b6261296S猫头猫 checkedItems.length > 0 ? 'normal' : 'secondary' 247b6261296S猫头猫 }> 248b6261296S猫头猫 {actionText} 249b6261296S猫头猫 {multi && checkedItems?.length > 0 250b6261296S猫头猫 ? ` (选中${checkedItems.length})` 251b6261296S猫头猫 : ''} 252b6261296S猫头猫 </ThemeText> 253b6261296S猫头猫 </View> 254b6261296S猫头猫 </Pressable> 255*e0caea6eS猫头猫 </VerticalSafeAreaView> 256b6261296S猫头猫 ); 257b6261296S猫头猫} 258b6261296S猫头猫 259b6261296S猫头猫const style = StyleSheet.create({ 260b6261296S猫头猫 header: { 261b6261296S猫头猫 height: rpx(88), 262b6261296S猫头猫 flexDirection: 'row', 263b6261296S猫头猫 alignItems: 'center', 264*e0caea6eS猫头猫 width: '100%', 265b6261296S猫头猫 paddingHorizontal: rpx(24), 266b6261296S猫头猫 }, 267b6261296S猫头猫 headerPath: { 268b6261296S猫头猫 marginLeft: rpx(28), 269b6261296S猫头猫 }, 270b6261296S猫头猫 scanBtn: { 271*e0caea6eS猫头猫 width: '100%', 272b6261296S猫头猫 height: rpx(120), 273b6261296S猫头猫 alignItems: 'center', 274b6261296S猫头猫 justifyContent: 'center', 275b6261296S猫头猫 }, 276b6261296S猫头猫}); 277