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猫头猫 exists, 75589cdf3S猫头猫 ExternalStorageDirectoryPath, 85589cdf3S猫头猫 getAllExternalFilesDirs, 95589cdf3S猫头猫 readDir, 10b6261296S猫头猫} from 'react-native-fs'; 11b6261296S猫头猫import {FlatList} from 'react-native-gesture-handler'; 12b6261296S猫头猫import useColors from '@/hooks/useColors'; 13b6261296S猫头猫import IconButton from '@/components/base/iconButton'; 14b6261296S猫头猫import FileItem from './fileItem'; 15b6261296S猫头猫import Empty from '@/components/base/empty'; 16b6261296S猫头猫import useHardwareBack from '@/hooks/useHardwareBack'; 17b6261296S猫头猫import {useNavigation} from '@react-navigation/native'; 18b6261296S猫头猫import Loading from '@/components/base/loading'; 19*b4c389f4Smaotoumaoimport {useParams} from '@/core/router'; 20b6261296S猫头猫import StatusBar from '@/components/base/statusBar'; 21e0caea6eS猫头猫import VerticalSafeAreaView from '@/components/base/verticalSafeAreaView'; 22e0caea6eS猫头猫import globalStyle from '@/constants/globalStyle'; 23756bc302S猫头猫import Button from '@/components/base/textButton.tsx'; 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猫头猫 1525e9f814fS猫头猫 const selectPath = useCallback( 1535e9f814fS猫头猫 (item: IFileItem | IFileItem[], nextChecked: boolean) => { 154b6261296S猫头猫 if (multi) { 1555e9f814fS猫头猫 if (!Array.isArray(item)) { 1565e9f814fS猫头猫 item = [item]; 1575e9f814fS猫头猫 } 158b6261296S猫头猫 setCheckedItems(prev => { 1595e9f814fS猫头猫 const itemPaths = (item as IFileItem[]).map(_ => _.path); 1605e9f814fS猫头猫 const newCheckedItem = prev.filter( 1615e9f814fS猫头猫 _ => !itemPaths.includes(_.path), 1625e9f814fS猫头猫 ); 163b6261296S猫头猫 if (nextChecked) { 1645e9f814fS猫头猫 return [...newCheckedItem, ...(item as IFileItem[])]; 165b6261296S猫头猫 } else { 1665e9f814fS猫头猫 return newCheckedItem; 167b6261296S猫头猫 } 168b6261296S猫头猫 }); 169b6261296S猫头猫 } else { 1705e9f814fS猫头猫 setCheckedItems( 1715e9f814fS猫头猫 nextChecked ? (Array.isArray(item) ? item : [item]) : [], 1725e9f814fS猫头猫 ); 173b6261296S猫头猫 } 1745e9f814fS猫头猫 }, 1755e9f814fS猫头猫 [], 1765e9f814fS猫头猫 ); 177b6261296S猫头猫 178b6261296S猫头猫 const renderItem = ({item}: {item: IFileItem}) => ( 179b6261296S猫头猫 <FileItem 180b6261296S猫头猫 path={item.path} 181b6261296S猫头猫 type={item.type} 182b6261296S猫头猫 parentPath={currentPath.path} 183b6261296S猫头猫 onItemPress={currentChecked => { 184b6261296S猫头猫 if (item.type === 'folder') { 185b6261296S猫头猫 setCurrentPath(prev => ({ 186b6261296S猫头猫 parent: prev, 187b6261296S猫头猫 path: item.path, 188b6261296S猫头猫 })); 189b6261296S猫头猫 } else { 190b6261296S猫头猫 selectPath(item, !currentChecked); 191b6261296S猫头猫 } 192b6261296S猫头猫 }} 19315a52c01S猫头猫 checked={checkedPaths.includes(item.path)} 194b6261296S猫头猫 onCheckedChange={checked => { 195b6261296S猫头猫 selectPath(item, checked); 196b6261296S猫头猫 }} 197b6261296S猫头猫 /> 198b6261296S猫头猫 ); 199b6261296S猫头猫 2005e9f814fS猫头猫 const currentPageAllChecked = useMemo(() => { 2015e9f814fS猫头猫 return ( 2025e9f814fS猫头猫 filesData.length && 2035e9f814fS猫头猫 filesData.every(file => checkedPaths.includes(file.path)) 2045e9f814fS猫头猫 ); 2055e9f814fS猫头猫 }, [filesData, checkedPaths]); 2065e9f814fS猫头猫 2075e9f814fS猫头猫 const renderHeader = () => { 2085e9f814fS猫头猫 return multi ? ( 2095e9f814fS猫头猫 <View style={style.selectAll}> 2105e9f814fS猫头猫 <Button 2115e9f814fS猫头猫 onPress={() => { 2125e9f814fS猫头猫 if (currentPageAllChecked) { 2135e9f814fS猫头猫 selectPath(filesData, false); 2145e9f814fS猫头猫 } else { 2155e9f814fS猫头猫 selectPath(filesData, true); 2165e9f814fS猫头猫 } 2175e9f814fS猫头猫 }}> 2185e9f814fS猫头猫 {currentPageAllChecked ? '全不选' : '全选'} 2195e9f814fS猫头猫 </Button> 2205e9f814fS猫头猫 </View> 2215e9f814fS猫头猫 ) : null; 2225e9f814fS猫头猫 }; 2235e9f814fS猫头猫 224b6261296S猫头猫 return ( 225e0caea6eS猫头猫 <VerticalSafeAreaView style={globalStyle.fwflex1}> 226b6261296S猫头猫 <StatusBar /> 227277c5280S猫头猫 <View style={[style.header, {backgroundColor: colors.appBar}]}> 228b6261296S猫头猫 <IconButton 229e650bfb3S猫头猫 sizeType="small" 2305589cdf3S猫头猫 name="arrow-long-left" 231277c5280S猫头猫 color={colors.appBarText} 232b6261296S猫头猫 onPress={() => { 233b6261296S猫头猫 // 返回上一级 234b6261296S猫头猫 if (currentPath.parent !== null) { 235b6261296S猫头猫 setCurrentPath(currentPath.parent); 236b6261296S猫头猫 } 237b6261296S猫头猫 }} 238b6261296S猫头猫 /> 239b6261296S猫头猫 <ThemeText 240b6261296S猫头猫 numberOfLines={2} 241b6261296S猫头猫 ellipsizeMode="head" 242277c5280S猫头猫 fontColor={'appBarText'} 243b6261296S猫头猫 style={style.headerPath}> 244b6261296S猫头猫 {currentPath.path} 245b6261296S猫头猫 </ThemeText> 246b6261296S猫头猫 </View> 247b6261296S猫头猫 {loading ? ( 248b6261296S猫头猫 <Loading /> 249b6261296S猫头猫 ) : ( 250b6261296S猫头猫 <> 251b6261296S猫头猫 <FlatList 2525e9f814fS猫头猫 ListHeaderComponent={renderHeader} 253b6261296S猫头猫 ListEmptyComponent={Empty} 254e0caea6eS猫头猫 style={globalStyle.fwflex1} 255b6261296S猫头猫 data={filesData} 256b6261296S猫头猫 getItemLayout={(_, index) => ({ 257b6261296S猫头猫 length: ITEM_HEIGHT, 258b6261296S猫头猫 offset: ITEM_HEIGHT * index, 259b6261296S猫头猫 index, 260b6261296S猫头猫 })} 261b6261296S猫头猫 renderItem={renderItem} 262b6261296S猫头猫 /> 263b6261296S猫头猫 </> 264b6261296S猫头猫 )} 265b6261296S猫头猫 <Pressable 266b6261296S猫头猫 onPress={async () => { 267b6261296S猫头猫 if (checkedItems.length) { 268b6261296S猫头猫 const shouldBack = await onAction?.(checkedItems); 269b6261296S猫头猫 if (shouldBack) { 270b6261296S猫头猫 navigation.goBack(); 271b6261296S猫头猫 } 272b6261296S猫头猫 } 273b6261296S猫头猫 }}> 274b6261296S猫头猫 <View 275b6261296S猫头猫 style={[ 276b6261296S猫头猫 style.scanBtn, 277b6261296S猫头猫 { 278277c5280S猫头猫 backgroundColor: colors.appBar, 279b6261296S猫头猫 }, 280b6261296S猫头猫 ]}> 281b6261296S猫头猫 <ThemeText 282277c5280S猫头猫 fontColor={'appBarText'} 2831119c2eaS猫头猫 opacity={checkedItems.length > 0 ? undefined : 0.6}> 284b6261296S猫头猫 {actionText} 285b6261296S猫头猫 {multi && checkedItems?.length > 0 286b6261296S猫头猫 ? ` (选中${checkedItems.length})` 287b6261296S猫头猫 : ''} 288b6261296S猫头猫 </ThemeText> 289b6261296S猫头猫 </View> 290b6261296S猫头猫 </Pressable> 291e0caea6eS猫头猫 </VerticalSafeAreaView> 292b6261296S猫头猫 ); 293b6261296S猫头猫} 294b6261296S猫头猫 295b6261296S猫头猫const style = StyleSheet.create({ 296b6261296S猫头猫 header: { 297b6261296S猫头猫 height: rpx(88), 298b6261296S猫头猫 flexDirection: 'row', 299b6261296S猫头猫 alignItems: 'center', 300e0caea6eS猫头猫 width: '100%', 301b6261296S猫头猫 paddingHorizontal: rpx(24), 302b6261296S猫头猫 }, 303b6261296S猫头猫 headerPath: { 304b6261296S猫头猫 marginLeft: rpx(28), 305b6261296S猫头猫 }, 306b6261296S猫头猫 scanBtn: { 307e0caea6eS猫头猫 width: '100%', 308b6261296S猫头猫 height: rpx(120), 309b6261296S猫头猫 alignItems: 'center', 310b6261296S猫头猫 justifyContent: 'center', 311b6261296S猫头猫 }, 3125e9f814fS猫头猫 selectAll: { 3135e9f814fS猫头猫 width: '100%', 3145e9f814fS猫头猫 height: ITEM_HEIGHT, 3155e9f814fS猫头猫 paddingHorizontal: rpx(24), 3165e9f814fS猫头猫 flexDirection: 'row', 3175e9f814fS猫头猫 alignItems: 'center', 3185e9f814fS猫头猫 }, 319b6261296S猫头猫}); 320