xref: /MusicFree/src/pages/fileSelector/index.tsx (revision b4c389f44ac4dad056e7314478fadd2eca82a4b1)
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