├── .eslintrc ├── .expo-shared └── assets.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── App.tsx ├── LICENSE ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── components ├── ActionSheet.tsx ├── Browser │ ├── DownloadDialog.tsx │ ├── FileTransferDialog.tsx │ ├── Files │ │ └── FileItem.tsx │ ├── NewFolderDialog.tsx │ └── PickImages │ │ ├── AlbumItem.tsx │ │ ├── AlbumList.tsx │ │ ├── AssetItem.tsx │ │ ├── AssetList.tsx │ │ └── index.tsx └── MiscFileView │ └── PDFViewer.tsx ├── features └── files │ ├── imagesSlice.ts │ ├── snackbarSlice.ts │ ├── tabbarStyleSlice.ts │ └── themeSlice.ts ├── global.d.ts ├── hooks ├── reduxHooks.ts ├── useAppState.ts ├── useBiometrics.ts ├── useColorScheme.ts ├── useLock.ts ├── useMultiImageSelection.ts └── useSelectionChange.ts ├── navigation ├── HomeStackNavigator.tsx ├── MainNavigator.tsx └── SettingsStackNavigator.tsx ├── package.json ├── patches └── react-native-image-viewing+0.2.2.patch ├── screens ├── Browser.tsx ├── FileTransfer.tsx ├── ImageGalleryView.tsx ├── LockScreen.tsx ├── Main.tsx ├── MiscFileView.tsx ├── Settings │ ├── SetPassCodeScreen.tsx │ └── Settings.tsx ├── VideoPlayer.tsx └── Web.tsx ├── screenshots ├── 1.png ├── 10.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── store.ts ├── theme.ts ├── tsconfig.json ├── types.d.ts ├── utils ├── Constants.ts ├── Filesize.ts ├── RemoveFileFolder.ts └── promiseProgress.ts └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier"], 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "es6": true 9 | }, 10 | "rules": { 11 | "prefer-const": "error" 12 | } 13 | } -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | android 3 | .expo/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | *.log 17 | # @generated expo-cli sync-2138f1e3e130677ea10ea873f6d498e3890e677b 18 | # The following patterns were generated by expo-cli 19 | 20 | # OSX 21 | # 22 | .DS_Store 23 | 24 | # Xcode 25 | # 26 | build/ 27 | *.pbxuser 28 | !default.pbxuser 29 | *.mode1v3 30 | !default.mode1v3 31 | *.mode2v3 32 | !default.mode2v3 33 | *.perspectivev3 34 | !default.perspectivev3 35 | xcuserdata 36 | *.xccheckout 37 | *.moved-aside 38 | DerivedData 39 | *.hmap 40 | *.ipa 41 | *.xcuserstate 42 | project.xcworkspace 43 | 44 | # Android/IntelliJ 45 | # 46 | build/ 47 | .idea 48 | .gradle 49 | local.properties 50 | *.iml 51 | *.hprof 52 | 53 | # node.js 54 | # 55 | node_modules/ 56 | npm-debug.log 57 | yarn-error.log 58 | 59 | # BUCK 60 | buck-out/ 61 | \.buckd/ 62 | *.keystore 63 | !debug.keystore 64 | 65 | # Bundle artifacts 66 | *.jsbundle 67 | 68 | # CocoaPods 69 | /ios/Pods/ 70 | 71 | # Expo 72 | .expo/ 73 | web-build/ 74 | 75 | # @end expo-cli -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "jsxBracketSameLine": false, 11 | "proseWrap": "always" 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "scm.autoReveal": false, 4 | "html.format.wrapLineLength": 0 5 | } -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | import React from 'react'; 3 | import { LogBox } from 'react-native'; 4 | import { Provider } from 'react-redux'; 5 | import Main from './screens/Main'; 6 | import { store } from './store'; 7 | 8 | LogBox.ignoreLogs(['componentWillMount', 'componentWillReceiveProps']); 9 | 10 | const App = () => { 11 | return ( 12 | 13 |
14 | 15 | ); 16 | }; 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 martymfly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Expo File Manager

4 | 5 |

6 | A file manager app built with React Native & Expo. 7 |

8 |
9 | 10 | ## About The Project 11 | 12 | With this app you can import/export photos and videos from/to photo gallery and also download files directly from the web. 13 | 14 | Files managed by the app are kept within app's folder therefore they are not exposed to phone's built-in file manager unless the phone is rooted. 15 | 16 | Here are some other features: 17 | 18 | * A simple built-in web browser 19 | * Transfer files from your PC through web sockets. (Socket app repo => https://github.com/martymfly/file-transfer-expo) 20 | * Darkmode - Lightmode 21 | * Passcode protection 22 | * Unlock with biometrics 23 | 24 |
25 |

Screenshots

26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 | 45 | 46 |
47 | 48 | ### Run project in development 49 | 50 | 1. Clone the repo 51 | ```sh 52 | git clone https://github.com/martymfly/expo-file-manager 53 | ``` 54 | 2. Install dependencies 55 | ```sh 56 | npm install 57 | ``` 58 | or 59 | 60 | ```sh 61 | yarn install 62 | ``` 63 | 3. Start the app 64 | - Run on Android: `yarn android` (or `npm run android`). 65 | 66 | - Run on iOS: `yarn ios` (or `npm run ios`). 67 | 68 | - Run on Web: `yarn web` (or `npm run web`). 69 | 70 | 71 | ## Contributing 72 | 73 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 74 | 75 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 76 | Don't forget to give the project a star! Thanks again! 77 | 78 | 1. Fork the Project 79 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 80 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 81 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 82 | 5. Open a Pull Request 83 | 84 | ## License 85 | 86 | Distributed under the MIT License. See `LICENSE` for more information. 87 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-file-manager", 4 | "slug": "expo-file-manager", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "userInterfaceStyle": "automatic", 8 | "icon": "./assets/icon.png", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#FFF" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true, 22 | "usesIcloudStorage": true 23 | }, 24 | "android": { 25 | "adaptiveIcon": { 26 | "foregroundImage": "./assets/adaptive-icon.png", 27 | "backgroundColor": "#FFF" 28 | }, 29 | "package": "com.martymfly.expofilemanager" 30 | }, 31 | "web": { 32 | "favicon": "./assets/favicon.png" 33 | }, 34 | "plugins": [ 35 | "expo-barcode-scanner" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /components/ActionSheet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Modal, 6 | TouchableWithoutFeedback, 7 | StyleSheet, 8 | ViewStyle, 9 | TextStyle, 10 | FlatList, 11 | TouchableOpacity, 12 | Platform, 13 | } from 'react-native'; 14 | 15 | import { MaterialIcons } from '@expo/vector-icons'; 16 | 17 | import { useAppSelector } from '../hooks/reduxHooks'; 18 | 19 | import { SIZE } from '../utils/Constants'; 20 | 21 | type IActionSheetProps = { 22 | visible: boolean; 23 | onClose: (arg0: boolean) => void; 24 | actionItems: string[]; 25 | title?: string; 26 | numberOfLinesTitle: number; 27 | cancelButtonIndex?: number; 28 | modalStyle?: ViewStyle; 29 | itemStyle?: ViewStyle; 30 | itemTextStyle?: TextStyle; 31 | titleStyle?: TextStyle; 32 | itemIcons: React.ComponentProps['name'][]; 33 | onItemPressed: (arg0: number) => void; 34 | }; 35 | 36 | type IActionListItemProps = { 37 | item: string; 38 | index: number; 39 | }; 40 | 41 | const ActionSheet = ({ 42 | visible, 43 | onClose, 44 | actionItems, 45 | title, 46 | numberOfLinesTitle, 47 | cancelButtonIndex, 48 | modalStyle, 49 | itemStyle, 50 | itemTextStyle, 51 | titleStyle, 52 | itemIcons, 53 | onItemPressed, 54 | }: IActionSheetProps) => { 55 | const ActionListItem = ({ item, index }: IActionListItemProps) => { 56 | const { colors } = useAppSelector((state) => state.theme.theme); 57 | return ( 58 | { 61 | onClose(false); 62 | if (Platform.OS === 'ios') { 63 | setTimeout(() => { 64 | onItemPressed(index); 65 | }, 1000); 66 | } else { 67 | onItemPressed(index); 68 | } 69 | }} 70 | > 71 | 72 | 77 | 78 | 86 | {item} 87 | 88 | 89 | ); 90 | }; 91 | 92 | return ( 93 | { 98 | onClose(false); 99 | }} 100 | > 101 | { 103 | onClose(false); 104 | }} 105 | > 106 | 107 | 108 | 109 | {title && ( 110 | 111 | 116 | {title} 117 | 118 | 119 | )} 120 | item} 123 | renderItem={({ item, index }) => ( 124 | 125 | )} 126 | /> 127 | 128 | 129 | ); 130 | }; 131 | 132 | export default ActionSheet; 133 | 134 | const styles = StyleSheet.create({ 135 | modalOverlay: { 136 | position: 'absolute', 137 | top: 0, 138 | bottom: 0, 139 | left: 0, 140 | right: 0, 141 | backgroundColor: 'rgba(0,0,0,0.5)', 142 | }, 143 | modalBody: { 144 | width: SIZE, 145 | position: 'absolute', 146 | bottom: 0, 147 | borderRadius: 10, 148 | }, 149 | titleContainer: { 150 | width: SIZE, 151 | display: 'flex', 152 | alignItems: 'center', 153 | justifyContent: 'center', 154 | padding: 10, 155 | }, 156 | titleText: { 157 | fontFamily: 'Poppins_500Medium', 158 | fontSize: 16, 159 | textAlign: 'center', 160 | }, 161 | itemStyle: { 162 | width: SIZE, 163 | height: 45, 164 | display: 'flex', 165 | flexDirection: 'row', 166 | alignItems: 'center', 167 | justifyContent: 'flex-start', 168 | paddingHorizontal: 10, 169 | }, 170 | itemText: { 171 | fontFamily: 'Poppins_400Regular', 172 | fontSize: 15, 173 | }, 174 | iconContainer: { 175 | marginRight: 10, 176 | }, 177 | }); 178 | -------------------------------------------------------------------------------- /components/Browser/DownloadDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Dialog from 'react-native-dialog'; 3 | import { useAppSelector } from '../../hooks/reduxHooks'; 4 | 5 | type DownloadDialogProps = { 6 | visible: boolean; 7 | handleDownload: (name: string) => void; 8 | setDownloadDialog: (visible: boolean) => void; 9 | }; 10 | export const DownloadDialog = ({ 11 | visible, 12 | handleDownload, 13 | setDownloadDialog, 14 | }: DownloadDialogProps) => { 15 | const [downloadUrl, setDownloadUrl] = useState(''); 16 | const { colors } = useAppSelector((state) => state.theme.theme); 17 | return ( 18 | 22 | 23 | Enter Download URL 24 | 25 | setDownloadUrl(text)} 29 | > 30 | { 33 | setDownloadDialog(false); 34 | setDownloadUrl(''); 35 | }} 36 | /> 37 | { 40 | handleDownload(downloadUrl); 41 | setDownloadDialog(false); 42 | setDownloadUrl(''); 43 | }} 44 | /> 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /components/Browser/FileTransferDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | FlatList, 7 | TouchableOpacity, 8 | } from 'react-native'; 9 | 10 | import Modal from 'react-native-modal'; 11 | import * as FileSystem from 'expo-file-system'; 12 | 13 | import { Feather } from '@expo/vector-icons'; 14 | import { Ionicons } from '@expo/vector-icons'; 15 | 16 | import { useAppSelector } from '../../hooks/reduxHooks'; 17 | 18 | import { SIZE } from '../../utils/Constants'; 19 | 20 | type FileTransferDialogProps = { 21 | isVisible: boolean; 22 | setIsVisible: (value: boolean) => void; 23 | currentDir: string; 24 | moveDir: string; 25 | setMoveDir: any; 26 | moveSelectedFiles: (destination: string) => void; 27 | moveOrCopy: string; 28 | setMoveOrCopy: (value: string) => void; 29 | }; 30 | 31 | export const FileTransferDialog = ({ 32 | isVisible, 33 | setIsVisible, 34 | currentDir, 35 | moveDir, 36 | setMoveDir, 37 | moveSelectedFiles, 38 | moveOrCopy, 39 | setMoveOrCopy, 40 | }: FileTransferDialogProps) => { 41 | const { colors } = useAppSelector((state) => state.theme.theme); 42 | const [currentFolders, setCurrentFolders] = useState([]); 43 | 44 | async function getFolders() { 45 | const folders = await FileSystem.readDirectoryAsync(currentDir + moveDir); 46 | const folderPromises = folders.map((folder) => 47 | FileSystem.getInfoAsync(currentDir + moveDir + `/${folder}`) 48 | ); 49 | Promise.all(folderPromises).then((values) => { 50 | const folderItems = values 51 | .filter((item) => item.isDirectory) 52 | .map((item) => { 53 | const folderName = item.uri.endsWith('/') 54 | ? item.uri 55 | .slice(0, item.uri.length - 1) 56 | .split('/') 57 | .pop() 58 | : item.uri.split('/').pop(); 59 | return folderName; 60 | }); 61 | setCurrentFolders(folderItems); 62 | }); 63 | } 64 | 65 | useEffect(() => { 66 | if (isVisible) getFolders(); 67 | }, [isVisible, moveDir]); 68 | 69 | const handleModalClose = () => { 70 | setIsVisible(false); 71 | setMoveDir(''); 72 | setMoveOrCopy(''); 73 | }; 74 | 75 | const navigateUpFolder = () => { 76 | const path = currentDir + moveDir; 77 | let pathSplit = path.split('/'); 78 | if ( 79 | path.endsWith('expo-file-manager/') || 80 | path.endsWith('expo-file-manager') 81 | ) { 82 | return; 83 | } else { 84 | setMoveDir((prev) => 85 | prev.replace('/' + pathSplit[pathSplit.length - 1], '') 86 | ); 87 | } 88 | }; 89 | 90 | const RenderItem = ({ item, moveDir, setMoveDir }) => ( 91 | { 95 | let pathAppend = moveDir.endsWith('/') 96 | ? moveDir + item 97 | : moveDir + '/' + item; 98 | setMoveDir(pathAppend); 99 | }} 100 | > 101 | 102 | 103 | 104 | 105 | 106 | 107 | {decodeURI(item)} 108 | 109 | 110 | 111 | 112 | ); 113 | 114 | return ( 115 | handleModalClose()} 118 | onBackdropPress={() => handleModalClose()} 119 | > 120 | 123 | {`${moveOrCopy} Files`} 126 | 127 | 131 | 132 | 133 | 138 | {moveDir !== '' ? decodeURI(moveDir) : 'Root'} 139 | 140 | { 143 | moveSelectedFiles(currentDir + moveDir); 144 | }} 145 | > 146 | 151 | 152 | 153 | index.toString()} 156 | renderItem={({ item }) => ( 157 | 158 | )} 159 | /> 160 | 161 | 162 | ); 163 | }; 164 | 165 | const styles = StyleSheet.create({ 166 | modalBody: { 167 | height: SIZE + 50, 168 | display: 'flex', 169 | alignItems: 'center', 170 | justifyContent: 'center', 171 | borderRadius: 10, 172 | }, 173 | header: { 174 | display: 'flex', 175 | alignItems: 'center', 176 | justifyContent: 'space-between', 177 | flexDirection: 'row', 178 | height: 50, 179 | width: '100%', 180 | borderBottomColor: 'gray', 181 | borderBottomWidth: 0.5, 182 | }, 183 | fileRow: { 184 | alignItems: 'flex-start', 185 | display: 'flex', 186 | flexDirection: 'row', 187 | height: 48, 188 | justifyContent: 'center', 189 | width: '100%', 190 | }, 191 | fileRowLeft: { 192 | alignItems: 'center', 193 | display: 'flex', 194 | height: '100%', 195 | justifyContent: 'center', 196 | width: '16.666667%', 197 | }, 198 | fileRowRight: { 199 | alignItems: 'flex-start', 200 | display: 'flex', 201 | height: '100%', 202 | justifyContent: 'center', 203 | width: '83.333333%', 204 | }, 205 | fileTitleText: { 206 | fontSize: 13, 207 | marginBottom: 5, 208 | fontFamily: 'Poppins_500Medium', 209 | }, 210 | folderName: { 211 | width: '60%', 212 | fontFamily: 'Poppins_600SemiBold', 213 | fontSize: 14, 214 | }, 215 | folderUpButton: { 216 | marginLeft: 10, 217 | width: '15%', 218 | }, 219 | confirmButton: { 220 | marginRight: 10, 221 | width: '15%', 222 | }, 223 | actionTitle: { 224 | fontFamily: 'Poppins_700Bold', 225 | fontSize: 18, 226 | }, 227 | }); 228 | -------------------------------------------------------------------------------- /components/Browser/Files/FileItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | Image, 7 | TouchableOpacity, 8 | Alert, 9 | } from 'react-native'; 10 | 11 | import { FontAwesome5 } from '@expo/vector-icons'; 12 | import { Feather } from '@expo/vector-icons'; 13 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 14 | 15 | import { useNavigation } from '@react-navigation/native'; 16 | 17 | import * as Sharing from 'expo-sharing'; 18 | import * as mime from 'react-native-mime-types'; 19 | import moment from 'moment'; 20 | 21 | import humanFileSize from '../../../utils/Filesize'; 22 | import ActionSheet from '../../ActionSheet'; 23 | 24 | import { fileItem } from '../../../types'; 25 | import { StackNavigationProp } from '@react-navigation/stack'; 26 | import { useAppSelector } from '../../../hooks/reduxHooks'; 27 | import { fileIcons } from '../../../utils/Constants'; 28 | 29 | type Props = { 30 | item: fileItem; 31 | currentDir: string; 32 | multiSelect: boolean; 33 | toggleSelect: (arg0: fileItem) => void; 34 | setTransferDialog: (arg0: boolean) => void; 35 | setMoveOrCopy: (arg0: string) => void; 36 | deleteSelectedFiles: (arg0?: fileItem) => void; 37 | setRenamingFile: (arg0: fileItem) => void; 38 | setRenameDialogVisible: (arg0: boolean) => void; 39 | setNewFileName: (arg0: string) => void; 40 | }; 41 | 42 | export default function FileItem({ 43 | item, 44 | currentDir, 45 | multiSelect, 46 | toggleSelect, 47 | setTransferDialog, 48 | setMoveOrCopy, 49 | deleteSelectedFiles, 50 | setRenamingFile, 51 | setRenameDialogVisible, 52 | setNewFileName, 53 | }: Props) { 54 | const { colors } = useAppSelector((state) => state.theme.theme); 55 | const navigation = useNavigation>(); 56 | const [itemActionsOpen, setItemActionsOpen] = useState(false); 57 | const docDir = currentDir; 58 | const itemMime = mime.lookup(item.uri) || ' '; 59 | const itemType: string = item.isDirectory ? 'dir' : itemMime.split('/')[0]; 60 | const itemFormat: string = item.isDirectory ? 'dir' : itemMime.split('/')[1]; 61 | 62 | const ThumbnailImage = ({ uri }) => { 63 | return ( 64 | 70 | ); 71 | }; 72 | 73 | const ItemThumbnail = () => { 74 | switch (itemType) { 75 | case 'dir': 76 | return ; 77 | case 'image': 78 | case 'video': 79 | return ; 80 | case 'audio': 81 | return ( 82 | 83 | ); 84 | case 'font': 85 | return ; 86 | case 'application': 87 | return ( 88 | 93 | ); 94 | case 'text': 95 | return ( 96 | 101 | ); 102 | default: 103 | return ; 104 | } 105 | }; 106 | 107 | const onPressHandler = () => { 108 | if (!multiSelect) { 109 | if (item.isDirectory) { 110 | navigation.push('Browser', { 111 | folderName: item.name, 112 | prevDir: docDir, 113 | }); 114 | } else if (itemType === 'image') { 115 | navigation.push('ImageGalleryView', { 116 | folderName: item.name, 117 | prevDir: docDir, 118 | }); 119 | } else if (itemType === 'video') { 120 | navigation.push('VideoPlayer', { 121 | folderName: item.name, 122 | prevDir: docDir, 123 | }); 124 | } else { 125 | navigation.push('MiscFileView', { 126 | folderName: item.name, 127 | prevDir: docDir, 128 | }); 129 | } 130 | } else { 131 | toggleSelect(item); 132 | } 133 | }; 134 | 135 | return ( 136 | 137 | { 156 | if (buttonIndex === 4) { 157 | setTimeout(() => { 158 | Alert.alert( 159 | 'Confirm Delete', 160 | `Are you sure you want to delete ${ 161 | multiSelect ? 'selected files' : 'this file' 162 | }?`, 163 | [ 164 | { 165 | text: 'Cancel', 166 | onPress: () => {}, 167 | style: 'cancel', 168 | }, 169 | { 170 | text: 'Delete', 171 | onPress: () => { 172 | if (!multiSelect) deleteSelectedFiles(item); 173 | else deleteSelectedFiles(); 174 | }, 175 | }, 176 | ] 177 | ); 178 | }, 300); 179 | } else if (buttonIndex === 3) { 180 | Sharing.isAvailableAsync().then((canShare) => { 181 | if (canShare) { 182 | Sharing.shareAsync(docDir + '/' + item.name); 183 | } 184 | }); 185 | } else if (buttonIndex === 2) { 186 | setMoveOrCopy('Copy'); 187 | if (!multiSelect) toggleSelect(item); 188 | setTransferDialog(true); 189 | } else if (buttonIndex === 1) { 190 | setMoveOrCopy('Move'); 191 | if (!multiSelect) toggleSelect(item); 192 | setTransferDialog(true); 193 | } else if (buttonIndex === 0) { 194 | setRenamingFile(item); 195 | setRenameDialogVisible(true); 196 | setNewFileName(item.name); 197 | } 198 | }} 199 | cancelButtonIndex={5} 200 | modalStyle={{ backgroundColor: colors.background2 }} 201 | itemTextStyle={{ color: colors.text }} 202 | titleStyle={{ color: colors.secondary }} 203 | /> 204 | 205 | { 210 | if (!multiSelect) { 211 | toggleSelect(item); 212 | } 213 | }} 214 | > 215 | 216 | {itemType && } 217 | 218 | 219 | 223 | {decodeURI(item.name)} 224 | 225 | 226 | {humanFileSize(item.size)} 227 | 228 | 229 | {moment(item.modificationTime * 1000).fromNow()} 230 | 231 | 232 | 233 | {/**Item Action Button */} 234 | 240 | setItemActionsOpen(true)}> 241 | 242 | {!item.selected ? ( 243 | 248 | ) : ( 249 | 250 | )} 251 | 252 | 253 | 254 | 255 | 256 | ); 257 | } 258 | 259 | const styles = StyleSheet.create({ 260 | container: { 261 | display: 'flex', 262 | flexDirection: 'row', 263 | justifyContent: 'space-between', 264 | alignItems: 'center', 265 | height: 75, 266 | }, 267 | itemContainer: { 268 | display: 'flex', 269 | flexDirection: 'row', 270 | alignItems: 'center', 271 | justifyContent: 'space-between', 272 | width: '100%', 273 | height: '100%', 274 | }, 275 | itemLeft: { 276 | height: '100%', 277 | width: '83%', 278 | display: 'flex', 279 | flexDirection: 'row', 280 | alignItems: 'center', 281 | justifyContent: 'center', 282 | }, 283 | itemThumbnail: { 284 | height: '100%', 285 | marginLeft: 8, 286 | width: '17%', 287 | display: 'flex', 288 | alignItems: 'center', 289 | justifyContent: 'center', 290 | }, 291 | itemDetails: { 292 | display: 'flex', 293 | flexDirection: 'column', 294 | alignItems: 'flex-start', 295 | justifyContent: 'center', 296 | height: '100%', 297 | width: '83%', 298 | overflow: 'hidden', 299 | }, 300 | itemActionButton: { 301 | width: '8%', 302 | height: '100%', 303 | }, 304 | image: { 305 | margin: 1, 306 | width: 40, 307 | height: 50, 308 | resizeMode: 'cover', 309 | borderRadius: 5, 310 | }, 311 | fileMenu: { 312 | marginRight: 5, 313 | height: 60, 314 | display: 'flex', 315 | justifyContent: 'center', 316 | }, 317 | fileName: { 318 | fontSize: 15, 319 | }, 320 | fileDetailText: { 321 | fontSize: 10, 322 | }, 323 | }); 324 | -------------------------------------------------------------------------------- /components/Browser/NewFolderDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | 3 | import Dialog from 'react-native-dialog'; 4 | import { TextInput } from 'react-native-gesture-handler'; 5 | 6 | import { useAppSelector } from '../../hooks/reduxHooks'; 7 | 8 | type NewFolderDialogProps = { 9 | visible: boolean; 10 | createDirectory: (name: string) => void; 11 | setFolderDialogVisible: (visible: boolean) => void; 12 | }; 13 | 14 | export const NewFolderDialog = ({ 15 | visible, 16 | createDirectory, 17 | setFolderDialogVisible, 18 | }: NewFolderDialogProps) => { 19 | const { colors } = useAppSelector((state) => state.theme.theme); 20 | const [folderName, setFolderName] = useState(''); 21 | const newFolderInputRef = useRef(null); 22 | useEffect(() => { 23 | if (visible) { 24 | setTimeout(() => { 25 | newFolderInputRef.current?.focus(); 26 | }, 100); 27 | } 28 | }, [visible]); 29 | 30 | return ( 31 | 35 | 36 | Add New Folder 37 | 38 | setFolderName(text)} 42 | > 43 | setFolderDialogVisible(false)} 46 | /> 47 | { 50 | createDirectory(folderName); 51 | setFolderName(''); 52 | }} 53 | /> 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /components/Browser/PickImages/AlbumItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StyleSheet, 4 | Text, 5 | View, 6 | Dimensions, 7 | Image, 8 | TouchableOpacity, 9 | } from 'react-native'; 10 | import { selectedAlbumType } from '.'; 11 | import { customAlbum } from '../../../types'; 12 | import { useAppSelector } from '../../../hooks/reduxHooks'; 13 | 14 | const { width: SIZE } = Dimensions.get('window'); 15 | const ITEM_SIZE = SIZE / 2; 16 | 17 | type AlbumProps = { 18 | item: customAlbum; 19 | setSelectedAlbum: (album: selectedAlbumType) => void; 20 | }; 21 | 22 | export const AlbumItem = ({ item: album, setSelectedAlbum }: AlbumProps) => { 23 | const { colors } = useAppSelector((state) => state.theme.theme); 24 | return ( 25 | setSelectedAlbum({ id: album.id, title: album.title })} 29 | > 30 | 31 | 32 | 36 | {album.title} 37 | 38 | 39 | {album.assetCount} 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | const styles = StyleSheet.create({ 47 | albumContainer: { 48 | width: ITEM_SIZE, 49 | height: ITEM_SIZE * 1.2, 50 | display: 'flex', 51 | alignItems: 'center', 52 | justifyContent: 'space-between', 53 | }, 54 | albumCover: { 55 | width: ITEM_SIZE * 0.9, 56 | height: ITEM_SIZE * 0.9, 57 | resizeMode: 'cover', 58 | borderRadius: 10, 59 | }, 60 | albumTitle: { 61 | fontFamily: 'Poppins_400Regular', 62 | fontSize: 12, 63 | }, 64 | albumDetailsContainer: { 65 | position: 'absolute', 66 | display: 'flex', 67 | flexDirection: 'column', 68 | alignItems: 'center', 69 | justifyContent: 'center', 70 | bottom: 0, 71 | width: ITEM_SIZE, 72 | height: ITEM_SIZE * 0.3, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /components/Browser/PickImages/AlbumList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, StyleSheet } from 'react-native'; 3 | import { AlbumItem } from './AlbumItem'; 4 | import { SIZE } from '../../../utils/Constants'; 5 | import { customAlbum } from '../../../types'; 6 | import { selectedAlbumType } from '.'; 7 | import { useAppSelector } from '../../../hooks/reduxHooks'; 8 | 9 | type AlbumListProps = { 10 | albums: customAlbum[]; 11 | setSelectedAlbum: (album: selectedAlbumType) => void; 12 | }; 13 | 14 | export const AlbumList = ({ albums, setSelectedAlbum }: AlbumListProps) => { 15 | const { colors } = useAppSelector((state) => state.theme.theme); 16 | return ( 17 | ( 23 | 24 | )} 25 | keyExtractor={(item) => item.id} 26 | /> 27 | ); 28 | }; 29 | 30 | const styles = StyleSheet.create({ 31 | albumList: { 32 | width: SIZE, 33 | }, 34 | contentContainer: { 35 | display: 'flex', 36 | alignItems: 'center', 37 | justifyContent: 'space-evenly', 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /components/Browser/PickImages/AssetItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Image, TouchableOpacity, View } from 'react-native'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { SIZE } from '../../../utils/Constants'; 5 | import { ExtendedAsset } from '../../../types'; 6 | 7 | const ITEM_SIZE = SIZE / 3; 8 | 9 | type AssetProps = { 10 | item: ExtendedAsset; 11 | isSelecting: boolean; 12 | toggleSelect: (asset: ExtendedAsset) => void; 13 | }; 14 | 15 | export const AssetItem = ({ 16 | item: asset, 17 | isSelecting, 18 | toggleSelect, 19 | }: AssetProps) => { 20 | return ( 21 | toggleSelect(asset)} 26 | onPress={() => { 27 | if (isSelecting) toggleSelect(asset); 28 | }} 29 | > 30 | 31 | {isSelecting && ( 32 | 33 | 34 | {asset.selected && ( 35 | 36 | )} 37 | 38 | )} 39 | 40 | ); 41 | }; 42 | 43 | const styles = StyleSheet.create({ 44 | assetContainer: { 45 | width: ITEM_SIZE, 46 | height: ITEM_SIZE, 47 | display: 'flex', 48 | alignItems: 'center', 49 | justifyContent: 'space-between', 50 | }, 51 | assetImage: { 52 | width: ITEM_SIZE * 0.95, 53 | height: ITEM_SIZE * 0.95, 54 | resizeMode: 'cover', 55 | borderRadius: 10, 56 | }, 57 | checkCircleContainer: { 58 | display: 'flex', 59 | alignItems: 'center', 60 | justifyContent: 'center', 61 | position: 'absolute', 62 | bottom: 5, 63 | right: 0, 64 | width: 24, 65 | height: 24, 66 | }, 67 | checkCircleBG: { 68 | position: 'absolute', 69 | bottom: 0, 70 | right: 0, 71 | width: 24, 72 | height: 24, 73 | borderRadius: 12, 74 | backgroundColor: 'gray', 75 | borderWidth: 1.5, 76 | borderColor: 'white', 77 | opacity: 0.9, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /components/Browser/PickImages/AssetList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, StyleSheet } from 'react-native'; 3 | import { SIZE } from '../../../utils/Constants'; 4 | import { AssetItem } from './AssetItem'; 5 | import { ExtendedAsset } from '../../../types'; 6 | 7 | type AssetListProps = { 8 | assets: ExtendedAsset[]; 9 | albumId: string; 10 | hasNextPage: boolean; 11 | endCursor: string; 12 | isSelecting: boolean; 13 | getAlbumAssets: (albumId: string, after?: string | undefined) => void; 14 | toggleSelect: (asset: ExtendedAsset) => void; 15 | }; 16 | 17 | export const AssetList = ({ 18 | assets, 19 | albumId, 20 | hasNextPage, 21 | endCursor, 22 | isSelecting, 23 | getAlbumAssets, 24 | toggleSelect, 25 | }: AssetListProps) => { 26 | return ( 27 | ( 33 | 38 | )} 39 | keyExtractor={(item) => item.albumId} 40 | onEndReached={() => { 41 | if (hasNextPage) getAlbumAssets(albumId, endCursor); 42 | }} 43 | onEndReachedThreshold={0.9} 44 | /> 45 | ); 46 | }; 47 | 48 | const styles = StyleSheet.create({ 49 | albumList: { 50 | width: SIZE, 51 | }, 52 | contentContainer: { 53 | display: 'flex', 54 | alignItems: 'center', 55 | justifyContent: 'space-evenly', 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /components/Browser/PickImages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | Alert, 7 | Linking, 8 | AlertButton, 9 | ActivityIndicator, 10 | TouchableOpacity, 11 | Button, 12 | } from 'react-native'; 13 | import { Feather, MaterialCommunityIcons } from '@expo/vector-icons'; 14 | import * as MediaLibrary from 'expo-media-library'; 15 | import useMultiImageSelection from '../../../hooks/useMultiImageSelection'; 16 | import { customAlbum, ExtendedAsset } from '../../../types'; 17 | import { AlbumList } from './AlbumList'; 18 | import { AssetList } from './AssetList'; 19 | import { SIZE } from '../../../utils/Constants'; 20 | import { useAppSelector } from '../../../hooks/reduxHooks'; 21 | 22 | type PickImagesProps = { 23 | onMultiSelectSubmit: (data: ExtendedAsset[]) => void; 24 | onClose: () => void; 25 | }; 26 | 27 | export type selectedAlbumType = { 28 | id: string; 29 | title: string; 30 | }; 31 | 32 | export default function PickImages({ 33 | onMultiSelectSubmit, 34 | onClose, 35 | }: PickImagesProps) { 36 | const { colors } = useAppSelector((state) => state.theme.theme); 37 | const [isMediaGranted, setIsMediaGranted] = useState(null); 38 | const [albums, setAlbums] = useState([]); 39 | const [albumsFetched, setAlbumsFetched] = useState(false); 40 | const [selectedAlbum, setSelectedAlbum] = useState( 41 | null 42 | ); 43 | const [assets, setAssets] = useState([]); 44 | const [hasNextPage, setHasNextPage] = useState(null); 45 | const [endCursor, setEndCursor] = useState(null); 46 | const [selectedAssets, setSelectedAssets] = useState([]); 47 | const isSelecting = useMultiImageSelection(assets); 48 | 49 | async function getAlbums() { 50 | const albums = await MediaLibrary.getAlbumsAsync(); 51 | const albumsPromiseArray = albums.map( 52 | async (album) => 53 | await MediaLibrary.getAssetsAsync({ 54 | album: album, 55 | first: 10, 56 | sortBy: MediaLibrary.SortBy.default, 57 | }) 58 | ); 59 | Promise.all(albumsPromiseArray).then((values) => { 60 | const nonEmptyAlbums = values 61 | .filter((item) => item.totalCount > 0) 62 | .map((item) => { 63 | const album = albums.find((a) => a.id === item.assets[0].albumId); 64 | const albumObject: customAlbum = { 65 | id: album?.id, 66 | title: album?.title, 67 | assetCount: album?.assetCount, 68 | type: album?.type, 69 | coverImage: item.assets[0].uri, 70 | }; 71 | return albumObject; 72 | }); 73 | setAlbums(nonEmptyAlbums); 74 | setAlbumsFetched(true); 75 | }); 76 | } 77 | 78 | async function getAlbumAssets(albumId: string, after?: string | undefined) { 79 | const options = { 80 | album: albumId, 81 | first: 25, 82 | sortBy: MediaLibrary.SortBy.creationTime, 83 | }; 84 | if (after) options['after'] = after; 85 | const albumAssets = await MediaLibrary.getAssetsAsync(options); 86 | setAssets((prev) => [...prev, ...albumAssets.assets]); 87 | setHasNextPage(albumAssets.hasNextPage); 88 | setEndCursor(albumAssets.endCursor); 89 | } 90 | 91 | const toggleSelect = (item: ExtendedAsset) => { 92 | const isSelected = 93 | selectedAssets.findIndex((asset) => asset.id === item.id) !== -1; 94 | if (!isSelected) setSelectedAssets((prev) => [...prev, item]); 95 | else 96 | setSelectedAssets((prev) => [ 97 | ...prev.filter((asset) => asset.id != item.id), 98 | ]); 99 | setAssets( 100 | assets.map((i) => { 101 | if (item.id === i.id) { 102 | i.selected = !i.selected; 103 | } 104 | return i; 105 | }) 106 | ); 107 | }; 108 | 109 | const handleImport = () => { 110 | onClose(); 111 | onMultiSelectSubmit(selectedAssets); 112 | return selectedAssets; 113 | }; 114 | 115 | // const unSelectAll = () => { 116 | // setAssets( 117 | // assets.map((i) => { 118 | // i.selected = false; 119 | // return i; 120 | // }) 121 | // ); 122 | // }; 123 | 124 | useEffect(() => { 125 | const requestMediaPermission = async () => { 126 | MediaLibrary.requestPermissionsAsync() 127 | .then((result) => { 128 | setIsMediaGranted(result.granted); 129 | if (!result.granted || result.accessPrivileges === 'limited') { 130 | const alertOptions: AlertButton[] = [ 131 | { 132 | text: 'Go to App Settings', 133 | onPress: () => { 134 | Linking.openSettings(); 135 | }, 136 | }, 137 | { 138 | text: 'Nevermind', 139 | onPress: () => {}, 140 | style: 'cancel', 141 | }, 142 | ]; 143 | if (result.canAskAgain && result.accessPrivileges !== 'limited') 144 | alertOptions.push({ 145 | text: 'Request again', 146 | onPress: () => requestMediaPermission(), 147 | }); 148 | Alert.alert( 149 | 'Denied Media Access', 150 | 'App needs access to all media library', 151 | [...alertOptions] 152 | ); 153 | } 154 | if (result.granted) getAlbums(); 155 | }) 156 | .catch((err) => { 157 | console.log(err); 158 | }); 159 | }; 160 | requestMediaPermission(); 161 | }, []); 162 | 163 | useEffect(() => { 164 | if (selectedAlbum) getAlbumAssets(selectedAlbum.id); 165 | }, [selectedAlbum]); 166 | 167 | if (!isMediaGranted && isMediaGranted !== null) 168 | return ( 169 | 175 | 176 | {'Media Access Denied'} 177 | 178 |