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