├── .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 |
33 |
34 |
35 |
40 |
41 |
42 |
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 | Linking.openSettings()} />
179 |
180 | );
181 |
182 | if (!albumsFetched)
183 | return (
184 |
187 |
188 |
189 | );
190 |
191 | if (albumsFetched && albums.length === 0)
192 | return (
193 |
196 |
197 | No Albums Found
198 |
199 |
200 | );
201 |
202 | return (
203 |
204 |
205 |
206 | {isSelecting && (
207 |
211 |
216 |
223 | {selectedAssets.length}
224 |
225 |
226 | )}
227 |
228 |
229 | {selectedAlbum?.title || 'Albums'}
230 |
231 |
232 | {selectedAlbum && (
233 | {
235 | setSelectedAlbum(null);
236 | setAssets([]);
237 | setSelectedAssets([]);
238 | }}
239 | >
240 |
241 |
242 | )}
243 |
244 |
245 |
246 | {albumsFetched && !selectedAlbum && (
247 |
248 | )}
249 | {selectedAlbum && (
250 |
259 | )}
260 |
261 |
262 | );
263 | }
264 |
265 | const styles = StyleSheet.create({
266 | container: {
267 | flex: 1,
268 | alignItems: 'center',
269 | justifyContent: 'center',
270 | },
271 | header: {
272 | display: 'flex',
273 | alignItems: 'center',
274 | justifyContent: 'center',
275 | width: SIZE,
276 | marginBottom: 15,
277 | },
278 | backButtonContainer: {
279 | position: 'absolute',
280 | right: 10,
281 | },
282 | confirmButton: {
283 | position: 'absolute',
284 | left: 10,
285 | },
286 | listContainer: {
287 | width: SIZE,
288 | height: '90%',
289 | alignItems: 'center',
290 | justifyContent: 'center',
291 | },
292 | noAccessContainer: {
293 | flex: 1,
294 | alignItems: 'center',
295 | justifyContent: 'center',
296 | flexDirection: 'column',
297 | },
298 | title: {
299 | fontFamily: 'Poppins_600SemiBold',
300 | fontSize: 20,
301 | },
302 | noAccessText: {
303 | marginBottom: 20,
304 | fontFamily: 'Poppins_500Medium',
305 | },
306 | handleImport: {
307 | display: 'flex',
308 | flexDirection: 'row',
309 | width: 60,
310 | height: 30,
311 | alignItems: 'flex-start',
312 | justifyContent: 'space-between',
313 | },
314 | });
315 |
--------------------------------------------------------------------------------
/components/MiscFileView/PDFViewer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PDFReader from 'rn-pdf-reader-js';
3 |
4 | type IPDFViewerProps = {
5 | fileURI: string;
6 | };
7 |
8 | export const PDFViewer = ({ fileURI }: IPDFViewerProps) => {
9 | return (
10 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/features/files/imagesSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { FileInfo } from 'expo-file-system';
3 | import { RootState } from '../../store';
4 |
5 | interface imagesSliceState {
6 | images: FileInfo[];
7 | }
8 |
9 | const initialState: imagesSliceState = {
10 | images: [],
11 | };
12 |
13 | export const imagesSlice = createSlice({
14 | name: 'images',
15 | initialState,
16 | reducers: {
17 | setImages: (state, action: PayloadAction) => {
18 | state.images = action.payload;
19 | },
20 | },
21 | });
22 |
23 | export const { setImages } = imagesSlice.actions;
24 |
25 | export const selectImages = (state: RootState) => state.images.images;
26 |
27 | export default imagesSlice.reducer;
28 |
--------------------------------------------------------------------------------
/features/files/snackbarSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { RootState } from '../../store';
3 |
4 | export type snackActionPayload = {
5 | message: string;
6 | label?: string | null;
7 | };
8 |
9 | interface snackbarSliceState {
10 | isVisible: boolean;
11 | message: string;
12 | label?: string | null;
13 | }
14 |
15 | const initialState: snackbarSliceState = {
16 | isVisible: false,
17 | message: '',
18 | label: null,
19 | };
20 |
21 | export const snackbarSlice = createSlice({
22 | name: 'snackbar',
23 | initialState,
24 | reducers: {
25 | setSnack: (state, action: PayloadAction) => {
26 | state.isVisible = true;
27 | state.message = action.payload.message;
28 | state.label = action.payload.label;
29 | },
30 | hideSnack: (state) => {
31 | state.isVisible = false;
32 | state.message = '';
33 | state.label = null;
34 | },
35 | },
36 | });
37 |
38 | export const { setSnack, hideSnack } = snackbarSlice.actions;
39 |
40 | export const getSnackbar = (state: RootState) => state.snackbar;
41 |
42 | export default snackbarSlice.reducer;
43 |
--------------------------------------------------------------------------------
/features/files/tabbarStyleSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 |
3 | import { RootState } from '../../store';
4 |
5 | interface initialStateType {
6 | visible: boolean;
7 | }
8 |
9 | const initialState: initialStateType = {
10 | visible: true,
11 | };
12 |
13 | export const tabbarStyleSlice = createSlice({
14 | name: 'tabbarStyle',
15 | initialState,
16 | reducers: {
17 | setTabbarVisible: (state, action: PayloadAction) => {
18 | state.visible = action.payload;
19 | },
20 | },
21 | });
22 |
23 | export const { setTabbarVisible } = tabbarStyleSlice.actions;
24 |
25 | export const tabbarStyle = (state: RootState) => state.tabbarStyle;
26 |
27 | export default tabbarStyleSlice.reducer;
28 |
--------------------------------------------------------------------------------
/features/files/themeSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { RootState } from '../../store';
3 | import { LightTheme, DarkTheme } from '../../theme';
4 |
5 | interface themeSliceState {
6 | theme: typeof LightTheme;
7 | }
8 |
9 | const initialState: themeSliceState = {
10 | theme: LightTheme,
11 | };
12 |
13 | export const themeSlice = createSlice({
14 | name: 'theme',
15 | initialState,
16 | reducers: {
17 | setLightTheme: (state) => {
18 | state.theme = LightTheme;
19 | },
20 | setDarkTheme: (state) => {
21 | state.theme = DarkTheme;
22 | },
23 | },
24 | });
25 |
26 | export const { setLightTheme, setDarkTheme } = themeSlice.actions;
27 |
28 | export const selectTheme = (state: RootState) => state.theme.theme;
29 |
30 | export default themeSlice.reducer;
31 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-native-mime-types';
2 | declare module 'expo-barcode-scanner';
3 |
--------------------------------------------------------------------------------
/hooks/reduxHooks.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
2 | import type { RootState, AppDispatch } from '../store';
3 |
4 | export const useAppDispatch = () => useDispatch();
5 | export const useAppSelector: TypedUseSelectorHook = useSelector;
6 |
--------------------------------------------------------------------------------
/hooks/useAppState.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 | import { AppState } from 'react-native';
3 |
4 | export const useAppState = () => {
5 | const appState = useRef(AppState.currentState);
6 | const [appStateVisible, setAppStateVisible] = useState(true);
7 |
8 | const handleStateChange = (nextAppState) => {
9 | appState.current = nextAppState;
10 | if (nextAppState !== 'active') {
11 | setAppStateVisible(false);
12 | } else {
13 | setAppStateVisible(true);
14 | }
15 | };
16 |
17 | useEffect(() => {
18 | const subscription = AppState.addEventListener('change', handleStateChange);
19 |
20 | return () => {
21 | subscription.remove();
22 | };
23 | }, []);
24 |
25 | return appStateVisible;
26 | };
27 |
--------------------------------------------------------------------------------
/hooks/useBiometrics.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import * as LocalAuthentication from 'expo-local-authentication';
3 | import * as SecureStore from 'expo-secure-store';
4 |
5 | export default function useBiometrics() {
6 | const [biometricsActive, setBiometricsActive] = useState(false);
7 | const [hasHardware, setHasHardware] = useState(false);
8 | const [authTypes, setAuthTypes] = useState<
9 | LocalAuthentication.AuthenticationType[]
10 | >([]);
11 | const [isEnrolled, setIsEnrolled] = useState(false);
12 | const [enrolledTypes, setEnrolledTypes] =
13 | useState();
14 |
15 | const getBiometricsStatus = () => {
16 | SecureStore.getItemAsync('biometricsActive').then((res) => {
17 | if (JSON.parse(res)) setBiometricsActive(true);
18 | else setBiometricsActive(false);
19 | });
20 | };
21 |
22 | const setBiometricsStatus = (status: boolean) => {
23 | SecureStore.setItemAsync('biometricsActive', JSON.stringify(status)).then(
24 | () => {
25 | setBiometricsActive(status);
26 | }
27 | );
28 | };
29 |
30 | const handleBiometricsStatus = () => {
31 | if (!biometricsActive) {
32 | LocalAuthentication.authenticateAsync()
33 | .then((result) => {
34 | if (result.success) {
35 | setBiometricsStatus(true);
36 | }
37 | })
38 | .catch((_) => {
39 | setBiometricsStatus(false);
40 | });
41 | } else {
42 | setBiometricsStatus(false);
43 | }
44 | };
45 |
46 | useEffect(() => {
47 | getBiometricsStatus();
48 | LocalAuthentication.hasHardwareAsync().then((res) => setHasHardware(res));
49 | LocalAuthentication.supportedAuthenticationTypesAsync().then((res) =>
50 | setAuthTypes(res)
51 | );
52 | LocalAuthentication.isEnrolledAsync().then((res) => setIsEnrolled(res));
53 | LocalAuthentication.getEnrolledLevelAsync().then((res) =>
54 | setEnrolledTypes(res)
55 | );
56 | }, []);
57 |
58 | return {
59 | biometricsActive,
60 | hasHardware,
61 | isEnrolled,
62 | handleBiometricsStatus,
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | import { Appearance, ColorSchemeName } from 'react-native';
2 | import { useEffect, useRef, useState } from 'react';
3 |
4 | export default function useColorScheme(
5 | delay = 500
6 | ): NonNullable {
7 | const [colorScheme, setColorScheme] = useState(Appearance.getColorScheme());
8 |
9 | let timeout = useRef(null).current;
10 |
11 | useEffect(() => {
12 | const subscription = Appearance.addChangeListener(onColorSchemeChange);
13 |
14 | return () => {
15 | resetCurrentTimeout();
16 | subscription.remove();
17 | };
18 | }, []);
19 |
20 | function onColorSchemeChange(preferences: Appearance.AppearancePreferences) {
21 | resetCurrentTimeout();
22 |
23 | timeout = setTimeout(() => {
24 | setColorScheme(preferences.colorScheme);
25 | }, delay);
26 | }
27 |
28 | function resetCurrentTimeout() {
29 | if (timeout) {
30 | clearTimeout(timeout);
31 | }
32 | }
33 |
34 | return colorScheme as NonNullable;
35 | }
36 |
--------------------------------------------------------------------------------
/hooks/useLock.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import * as SecureStore from 'expo-secure-store';
3 | import { useAppState } from './useAppState';
4 |
5 | export default function useLock() {
6 | const [locked, setLocked] = useState(false);
7 | const [pinActive, setPinActive] = useState(null);
8 | const appStateVisible = useAppState();
9 |
10 | const getPassCodeStatus = async () => {
11 | const hasPassCode = await SecureStore.getItemAsync('hasPassCode');
12 | if (JSON.parse(hasPassCode)) {
13 | setPinActive(true);
14 | return true;
15 | } else {
16 | setPinActive(false);
17 | return false;
18 | }
19 | };
20 |
21 | useEffect(() => {
22 | getPassCodeStatus();
23 | if (!appStateVisible && pinActive) {
24 | setLocked(true);
25 | } else if (pinActive) {
26 | setLocked(true);
27 | } else if (!pinActive) {
28 | setLocked(false);
29 | }
30 | }, [appStateVisible]);
31 |
32 | return { locked, setLocked, pinActive };
33 | }
34 |
--------------------------------------------------------------------------------
/hooks/useMultiImageSelection.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { ExtendedAsset } from "../types";
3 |
4 | export default function useSelectionChange(items: ExtendedAsset[]) {
5 | const [isSelecting, setIsSelecting] = useState(false);
6 |
7 | useEffect(() => {
8 | if (items.filter((i) => i.selected).length > 0) {
9 | setIsSelecting(true);
10 | } else {
11 | setIsSelecting(false);
12 | }
13 | }, [items]);
14 | return isSelecting;
15 | }
16 |
--------------------------------------------------------------------------------
/hooks/useSelectionChange.ts:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { fileItem } from "../types";
3 |
4 | export default function useSelectionChange(items: fileItem[]) {
5 | const [multiSelect, setMultiSelect] = useState(false);
6 | const [allSelected, setAllSelected] = useState(false);
7 |
8 | useEffect(() => {
9 | if (items.filter((i) => i.selected).length > 0) {
10 | setMultiSelect(true);
11 | } else {
12 | setMultiSelect(false);
13 | }
14 | if (items.filter((i) => i.selected).length === items.length) {
15 | setAllSelected(true);
16 | } else {
17 | setAllSelected(false);
18 | }
19 | }, [items]);
20 | return { multiSelect, allSelected };
21 | }
22 |
--------------------------------------------------------------------------------
/navigation/HomeStackNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { createStackNavigator } from '@react-navigation/stack';
4 |
5 | import ImageGalleryView from '../screens/ImageGalleryView';
6 | import MiscFileView from '../screens/MiscFileView';
7 | import Browser from '../screens/Browser';
8 | import VideoPlayer from '../screens/VideoPlayer';
9 |
10 | type HomeStackParamList = {
11 | Browser: { folderName: string; prevDir: string };
12 | ImageGalleryView: { folderName: string; prevDir: string };
13 | VideoPlayer: { folderName: string; prevDir: string };
14 | MiscFileView: { folderName: string; prevDir: string };
15 | };
16 |
17 | const HomeStack = createStackNavigator();
18 |
19 | const HomeStackNavigator: React.FC = () => {
20 | return (
21 |
27 | ({
30 | title: route?.params?.folderName || 'File Manager',
31 | })}
32 | component={Browser}
33 | />
34 | ({
37 | title: route?.params?.prevDir.split('/').pop() || 'Gallery',
38 | presentation: 'transparentModal',
39 | })}
40 | component={ImageGalleryView}
41 | />
42 | ({
45 | title: route?.params?.prevDir.split('/').pop() || 'Video',
46 | presentation: 'transparentModal',
47 | })}
48 | component={VideoPlayer}
49 | />
50 | ({
53 | title: route?.params?.prevDir.split('/').pop() || 'File View',
54 | presentation: 'transparentModal',
55 | })}
56 | component={MiscFileView}
57 | />
58 |
59 | );
60 | };
61 |
62 | export default HomeStackNavigator;
63 |
--------------------------------------------------------------------------------
/navigation/MainNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Ionicons } from '@expo/vector-icons';
4 |
5 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
6 |
7 | import HomeStackNavigator from './HomeStackNavigator';
8 | import SettingsStackNavigator from './SettingsStackNavigator';
9 |
10 | import Web from '../screens/Web';
11 | import FileTransfer from '../screens/FileTransfer';
12 |
13 | import { useAppSelector } from '../hooks/reduxHooks';
14 |
15 | const Tab = createBottomTabNavigator();
16 |
17 | export const MainNavigator: React.FC = () => {
18 | const { colors } = useAppSelector((state) => state.theme.theme);
19 | const { visible } = useAppSelector((state) => state.tabbarStyle);
20 | return (
21 | ({
24 | headerShown: false,
25 | tabBarActiveTintColor: 'tomato',
26 | tabBarInactiveTintColor: 'gray',
27 | tabBarStyle: {
28 | display: visible ? 'flex' : 'none',
29 | },
30 | tabBarIcon: ({ focused, color, size }) => {
31 | let iconName: any;
32 | if (route.name === 'Home') {
33 | iconName = focused ? 'ios-home' : 'ios-home';
34 | } else if (route.name === 'Settings') {
35 | iconName = focused ? 'ios-list' : 'ios-list';
36 | } else if (route.name === 'Downloads') {
37 | iconName = 'ios-cloud-download';
38 | } else if (route.name === 'Web') {
39 | iconName = 'ios-globe';
40 | } else if (route.name === 'FileTransfer') {
41 | iconName = 'ios-documents-outline';
42 | }
43 | return ;
44 | },
45 | tabBarActiveBackgroundColor: colors.background,
46 | tabBarInactiveBackgroundColor: colors.background,
47 | })}
48 | >
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/navigation/SettingsStackNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { createStackNavigator } from '@react-navigation/stack';
4 |
5 | import Settings from '../screens/Settings/Settings';
6 | import SetPassCodeScreen from '../screens/Settings/SetPassCodeScreen';
7 |
8 | const SettingsStack = createStackNavigator();
9 |
10 | export const SettingsStackNavigator: React.FC = () => {
11 | return (
12 |
18 |
19 |
23 |
24 | );
25 | };
26 |
27 | export default SettingsStackNavigator;
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "expo start",
4 | "android": "expo start --android",
5 | "ios": "expo start --ios",
6 | "web": "expo start --web",
7 | "eject": "expo eject",
8 | "postinstall": "patch-package"
9 | },
10 | "dependencies": {
11 | "@expo-google-fonts/poppins": "^0.2.2",
12 | "@expo/vector-icons": "^13.0.0",
13 | "@react-native-async-storage/async-storage": "~1.17.3",
14 | "@react-native-community/masked-view": "0.1.10",
15 | "@react-navigation/bottom-tabs": "^6.5.0",
16 | "@react-navigation/native": "^6.1.0",
17 | "@react-navigation/stack": "^6.3.8",
18 | "@reduxjs/toolkit": "^1.6.1",
19 | "axios": "^1.2.1",
20 | "expo": "^47.0.0",
21 | "expo-av": "~13.0.2",
22 | "expo-barcode-scanner": "~12.1.0",
23 | "expo-constants": "~14.0.2",
24 | "expo-document-picker": "~11.0.1",
25 | "expo-file-system": "~15.1.1",
26 | "expo-image-picker": "~14.0.2",
27 | "expo-local-authentication": "~13.0.2",
28 | "expo-media-library": "~15.0.0",
29 | "expo-secure-store": "~12.0.0",
30 | "expo-sharing": "~11.0.1",
31 | "expo-splash-screen": "~0.17.5",
32 | "expo-status-bar": "~1.4.2",
33 | "moment": "^2.29.4",
34 | "react": "18.1.0",
35 | "react-dom": "18.1.0",
36 | "react-native": "0.70.5",
37 | "react-native-dialog": "^9.3.0",
38 | "react-native-gesture-handler": "~2.8.0",
39 | "react-native-image-viewing": "^0.2.2",
40 | "react-native-mime-types": "^2.3.0",
41 | "react-native-modal": "^13.0.1",
42 | "react-native-paper": "^4.9.2",
43 | "react-native-safe-area-context": "4.4.1",
44 | "react-native-screens": "~3.18.0",
45 | "react-native-simple-dialogs": "^1.5.0",
46 | "react-native-svg": "13.4.0",
47 | "react-native-webview": "11.23.1",
48 | "react-redux": "^7.2.5",
49 | "rn-pdf-reader-js": "^4.1.1",
50 | "socket.io-client": "^4.5.4"
51 | },
52 | "devDependencies": {
53 | "@babel/core": "^7.19.3",
54 | "@types/react": "17.0.43",
55 | "@types/react-native": "~0.70.6",
56 | "@types/react-redux": "^7.1.18",
57 | "patch-package": "^6.5.0",
58 | "postinstall-postinstall": "^2.1.0",
59 | "typescript": "^4.6.3"
60 | },
61 | "private": false,
62 | "name": "expo-file-manager",
63 | "version": "1.0.0"
64 | }
65 |
--------------------------------------------------------------------------------
/patches/react-native-image-viewing+0.2.2.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/react-native-image-viewing/dist/ImageViewing.js b/node_modules/react-native-image-viewing/dist/ImageViewing.js
2 | index 3d7641a..6ac697c 100644
3 | --- a/node_modules/react-native-image-viewing/dist/ImageViewing.js
4 | +++ b/node_modules/react-native-image-viewing/dist/ImageViewing.js
5 | @@ -6,7 +6,7 @@
6 | *
7 | */
8 | import React, { useCallback, useRef, useEffect } from "react";
9 | -import { Animated, Dimensions, StyleSheet, View, VirtualizedList, Modal, } from "react-native";
10 | +import { Animated, Dimensions, StyleSheet, View, VirtualizedList, } from "react-native";
11 | import ImageItem from "./components/ImageItem/ImageItem";
12 | import ImageDefaultHeader from "./components/ImageDefaultHeader";
13 | import StatusBarManager from "./components/StatusBarManager";
14 | @@ -37,7 +37,7 @@ function ImageViewing({ images, keyExtractor, imageIndex, visible, onRequestClos
15 | if (!visible) {
16 | return null;
17 | }
18 | - return (
19 | + return (<>
20 |
21 |
22 |
23 | @@ -62,7 +62,7 @@ function ImageViewing({ images, keyExtractor, imageIndex, visible, onRequestClos
24 | })}
25 | )}
26 |
27 | - );
28 | + >);
29 | }
30 | const styles = StyleSheet.create({
31 | container: {
32 | @@ -73,7 +73,7 @@ const styles = StyleSheet.create({
33 | position: "absolute",
34 | width: "100%",
35 | zIndex: 1,
36 | - top: 0,
37 | + top: 20,
38 | },
39 | footer: {
40 | position: "absolute",
41 | diff --git a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.android.js b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.android.js
42 | index 5fcaa9f..0035568 100644
43 | --- a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.android.js
44 | +++ b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.android.js
45 | @@ -6,7 +6,7 @@
46 | *
47 | */
48 | import React, { useCallback, useRef, useState } from "react";
49 | -import { Animated, ScrollView, Dimensions, StyleSheet, } from "react-native";
50 | +import { Animated, ScrollView, Dimensions, StyleSheet, View, } from "react-native";
51 | import useImageDimensions from "../../hooks/useImageDimensions";
52 | import usePanResponder from "../../hooks/usePanResponder";
53 | import { getImageStyles, getImageTransform } from "../../utils";
54 | @@ -18,7 +18,8 @@ const SCREEN_WIDTH = SCREEN.width;
55 | const SCREEN_HEIGHT = SCREEN.height;
56 | const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => {
57 | const imageContainer = useRef(null);
58 | - const imageDimensions = useImageDimensions(imageSrc);
59 | + const sourceHasDimensions = imageSrc.width !== undefined && imageSrc.height !== undefined;
60 | + const imageDimensions = sourceHasDimensions ? {width: imageSrc.width, height: imageSrc.height} : useImageDimensions(imageSrc);
61 | const [translate, scale] = getImageTransform(imageDimensions, SCREEN);
62 | const scrollValueY = new Animated.Value(0);
63 | const [isLoaded, setLoadEnd] = useState(false);
64 | @@ -69,7 +70,7 @@ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPre
65 | onScrollEndDrag,
66 | })}>
67 |
68 | - {(!isLoaded || !imageDimensions) && }
69 | + {!isLoaded && }
70 | );
71 | };
72 | const styles = StyleSheet.create({
73 | diff --git a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js
74 | index 0708505..fa0237a 100644
75 | --- a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js
76 | +++ b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js
77 | @@ -20,7 +20,8 @@ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPre
78 | const scrollViewRef = useRef(null);
79 | const [loaded, setLoaded] = useState(false);
80 | const [scaled, setScaled] = useState(false);
81 | - const imageDimensions = useImageDimensions(imageSrc);
82 | + const sourceHasDimensions = imageSrc.width !== undefined && imageSrc.height !== undefined;
83 | + const imageDimensions = sourceHasDimensions ? {width: imageSrc.width, height: imageSrc.height} : useImageDimensions(imageSrc);
84 | const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN);
85 | const [translate, scale] = getImageTransform(imageDimensions, SCREEN);
86 | const scrollValueY = new Animated.Value(0);
87 | @@ -60,10 +61,10 @@ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPre
88 |
91 | - {(!loaded || !imageDimensions) && }
92 |
93 | - setLoaded(true)}/>
94 | + setLoaded(true)}/>
95 |
96 | + {!loaded && }
97 |
98 | );
99 | };
100 | diff --git a/node_modules/react-native-image-viewing/dist/hooks/usePanResponder.js b/node_modules/react-native-image-viewing/dist/hooks/usePanResponder.js
101 | index 99dcbc7..08ad003 100644
102 | --- a/node_modules/react-native-image-viewing/dist/hooks/usePanResponder.js
103 | +++ b/node_modules/react-native-image-viewing/dist/hooks/usePanResponder.js
104 | @@ -12,7 +12,7 @@ const SCREEN = Dimensions.get("window");
105 | const SCREEN_WIDTH = SCREEN.width;
106 | const SCREEN_HEIGHT = SCREEN.height;
107 | const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
108 | -const SCALE_MAX = 2;
109 | +const SCALE_MAX = 5;
110 | const DOUBLE_TAP_DELAY = 300;
111 | const OUT_BOUND_MULTIPLIER = 0.75;
112 | const usePanResponder = ({ initialScale, initialTranslate, onZoom, doubleTapToZoomEnabled, onLongPress, delayLongPress, }) => {
113 | @@ -29,6 +29,19 @@ const usePanResponder = ({ initialScale, initialTranslate, onZoom, doubleTapToZo
114 | const scaleValue = new Animated.Value(initialScale);
115 | const translateValue = new Animated.ValueXY(initialTranslate);
116 | const imageDimensions = getImageDimensionsByTranslate(initialTranslate, SCREEN);
117 | + useEffect(() => {
118 | + onZoom(false);
119 | + Animated.parallel([
120 | + Animated.timing(scaleValue, {
121 | + toValue: initialScale,
122 | + duration: 300,
123 | + useNativeDriver: false,
124 | + }),
125 | + ], { stopTogether: false }).start(() => {
126 | + currentScale = initialScale;
127 | + currentTranslate = initialTranslate;
128 | + });
129 | + });
130 | const getBounds = (scale) => {
131 | const scaledImageDimensions = {
132 | width: imageDimensions.width * scale,
133 | diff --git a/node_modules/react-native-image-viewing/dist/hooks/useZoomPanResponder.js b/node_modules/react-native-image-viewing/dist/hooks/useZoomPanResponder.js
134 | index 37a3376..4c45422 100644
135 | --- a/node_modules/react-native-image-viewing/dist/hooks/useZoomPanResponder.js
136 | +++ b/node_modules/react-native-image-viewing/dist/hooks/useZoomPanResponder.js
137 | @@ -11,7 +11,7 @@ import { createPanResponder, getDistanceBetweenTouches, getImageTranslate, getIm
138 | const SCREEN = Dimensions.get("window");
139 | const SCREEN_WIDTH = SCREEN.width;
140 | const SCREEN_HEIGHT = SCREEN.height;
141 | -const SCALE_MAX = 2;
142 | +const SCALE_MAX = 5;
143 | const DOUBLE_TAP_DELAY = 300;
144 | const OUT_BOUND_MULTIPLIER = 0.75;
145 | const useZoomPanResponder = ({ initialScale, initialTranslate, onZoom, doubleTapToZoomEnabled }) => {
146 |
--------------------------------------------------------------------------------
/screens/Browser.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | FlatList,
6 | TouchableOpacity,
7 | Platform,
8 | Alert,
9 | BackHandler,
10 | TextInput,
11 | } from 'react-native';
12 |
13 | import Dialog from 'react-native-dialog';
14 |
15 | import {
16 | Dialog as GalleryDialog,
17 | ProgressDialog,
18 | } from 'react-native-simple-dialogs';
19 | import { AntDesign, Feather } from '@expo/vector-icons';
20 | import { MaterialCommunityIcons } from '@expo/vector-icons';
21 |
22 | import FileItem from '../components/Browser/Files/FileItem';
23 | import Pickimages from '../components/Browser/PickImages';
24 | import ActionSheet from '../components/ActionSheet';
25 |
26 | import useSelectionChange from '../hooks/useSelectionChange';
27 | import allProgress from '../utils/promiseProgress';
28 |
29 | import { NewFolderDialog } from '../components/Browser/NewFolderDialog';
30 | import { DownloadDialog } from '../components/Browser/DownloadDialog';
31 | import { FileTransferDialog } from '../components/Browser/FileTransferDialog';
32 |
33 | import axios, { AxiosError } from 'axios';
34 | import moment from 'moment';
35 | import Constants from 'expo-constants';
36 | import * as FileSystem from 'expo-file-system';
37 | import * as ImagePicker from 'expo-image-picker';
38 | import * as DocumentPicker from 'expo-document-picker';
39 | import * as MediaLibrary from 'expo-media-library';
40 | import * as mime from 'react-native-mime-types';
41 |
42 | import { StackScreenProps } from '@react-navigation/stack';
43 | import { useNavigation } from '@react-navigation/native';
44 | import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types';
45 | import { ExtendedAsset, fileItem } from '../types';
46 | import { useAppDispatch, useAppSelector } from '../hooks/reduxHooks';
47 | import { setImages } from '../features/files/imagesSlice';
48 | import { setSnack, snackActionPayload } from '../features/files/snackbarSlice';
49 | import { HEIGHT, imageFormats, reExt, SIZE } from '../utils/Constants';
50 |
51 | type BrowserParamList = {
52 | Browser: { prevDir: string; folderName: string };
53 | };
54 |
55 | type IBrowserProps = StackScreenProps;
56 |
57 | const Browser = ({ route }: IBrowserProps) => {
58 | const dispatch = useAppDispatch();
59 | const navigation = useNavigation();
60 | const { colors } = useAppSelector((state) => state.theme.theme);
61 | const docDir: string = FileSystem.documentDirectory || '';
62 | const [currentDir, setCurrentDir] = useState(
63 | route?.params?.prevDir !== undefined ? route?.params?.prevDir : docDir
64 | );
65 | const [moveDir, setMoveDir] = useState('');
66 | const [files, setFiles] = useState([]);
67 | const [selectedFiles, setSelectedFiles] = useState([]);
68 | const [folderDialogVisible, setFolderDialogVisible] = useState(false);
69 | const [downloadDialogVisible, setDownloadDialogVisible] = useState(false);
70 | const [renameDialogVisible, setRenameDialogVisible] = useState(false);
71 | const [newFileName, setNewFileName] = useState('');
72 | const [renamingFile, setRenamingFile] = useState();
73 | const renameInputRef = useRef(null);
74 | const [multiImageVisible, setMultiImageVisible] = useState(false);
75 | const [importProgressVisible, setImportProgressVisible] = useState(false);
76 | const [destinationDialogVisible, setDestinationDialogVisible] =
77 | useState(false);
78 | const [newFileActionSheet, setNewFileActionSheet] = useState(false);
79 | const [moveOrCopy, setMoveOrCopy] = useState('');
80 | const { multiSelect, allSelected } = useSelectionChange(files);
81 |
82 | useEffect(() => {
83 | getFiles();
84 | }, [currentDir]);
85 |
86 | React.useEffect(() => {
87 | const unsubscribe = navigation.addListener('focus', () => {
88 | getFiles();
89 | });
90 |
91 | return unsubscribe;
92 | }, [navigation]);
93 |
94 | useEffect(() => {
95 | if (route?.params?.folderName !== undefined) {
96 | setCurrentDir((prev) =>
97 | prev?.endsWith('/')
98 | ? prev + route.params.folderName
99 | : prev + '/' + route.params.folderName
100 | );
101 | }
102 | }, [route]);
103 |
104 | useEffect(() => {
105 | const backAction = () => {
106 | if (navigation.canGoBack()) navigation.goBack();
107 | return true;
108 | };
109 |
110 | const backHandler = BackHandler.addEventListener(
111 | 'hardwareBackPress',
112 | backAction
113 | );
114 |
115 | return () => backHandler.remove();
116 | }, []);
117 |
118 | const renderItem = ({ item }: { item: fileItem }) => (
119 |
131 | );
132 |
133 | const handleDownload = (downloadUrl: string) => {
134 | axios
135 | .get(downloadUrl)
136 | .then((res) => {
137 | const fileExt = mime.extension(res.headers['content-type']);
138 | FileSystem.downloadAsync(
139 | downloadUrl,
140 | currentDir + '/DL_' + moment().format('DDMMYHmmss') + '.' + fileExt
141 | )
142 | .then(() => {
143 | getFiles();
144 | setDownloadDialogVisible(false);
145 | handleSetSnack({
146 | message: 'Download complete',
147 | });
148 | })
149 | .catch((_) => {
150 | handleSetSnack({
151 | message: 'Please provide a correct url',
152 | });
153 | });
154 | })
155 | .catch((error: AxiosError) =>
156 | handleSetSnack({
157 | message: error.message,
158 | })
159 | );
160 | };
161 |
162 | const toggleSelect = (item: fileItem) => {
163 | if (item.selected && selectedFiles.includes(item)) {
164 | const index = selectedFiles.indexOf(item);
165 | if (index > -1) {
166 | selectedFiles.splice(index, 1);
167 | }
168 | } else if (!item.selected && !selectedFiles.includes(item)) {
169 | setSelectedFiles((prev) => [...prev, item]);
170 | }
171 | setFiles(
172 | files.map((i) => {
173 | if (item === i) {
174 | i.selected = !i.selected;
175 | }
176 | return i;
177 | })
178 | );
179 | };
180 |
181 | const toggleSelectAll = () => {
182 | if (!allSelected) {
183 | setFiles(
184 | files.map((item) => {
185 | item.selected = true;
186 | return item;
187 | })
188 | );
189 | setSelectedFiles(files);
190 | } else {
191 | setFiles(
192 | files.map((item) => {
193 | item.selected = false;
194 | return item;
195 | })
196 | );
197 | setSelectedFiles([]);
198 | }
199 | };
200 |
201 | const getFiles = async () => {
202 | FileSystem.readDirectoryAsync(currentDir)
203 | .then((dirFiles) => {
204 | if (currentDir !== route?.params?.prevDir) {
205 | const filteredFiles = dirFiles.filter(
206 | (file) => file !== 'RCTAsyncLocalStorage'
207 | );
208 | const filesProms = filteredFiles.map((fileName) =>
209 | FileSystem.getInfoAsync(currentDir + '/' + fileName)
210 | );
211 | Promise.all(filesProms).then((results) => {
212 | let tempfiles: fileItem[] = results.map((file) => {
213 | const name = file.uri.endsWith('/')
214 | ? file.uri
215 | .slice(0, file.uri.length - 1)
216 | .split('/')
217 | .pop()
218 | : file.uri.split('/').pop();
219 | return Object({
220 | ...file,
221 | name,
222 | selected: false,
223 | });
224 | });
225 | setFiles(tempfiles);
226 | const tempImageFiles = results.filter((file) => {
227 | let fileExtension = file.uri
228 | .split('/')
229 | .pop()
230 | .split('.')
231 | .pop()
232 | .toLowerCase();
233 | if (imageFormats.includes(fileExtension)) {
234 | return file;
235 | }
236 | });
237 | dispatch(setImages(tempImageFiles));
238 | });
239 | }
240 | })
241 | .catch((_) => {});
242 | };
243 |
244 | async function createDirectory(name: string) {
245 | FileSystem.makeDirectoryAsync(currentDir + '/' + name)
246 | .then(() => {
247 | getFiles();
248 | setFolderDialogVisible(false);
249 | })
250 | .catch(() => {
251 | handleSetSnack({
252 | message: 'Folder could not be created or already exists.',
253 | });
254 | });
255 | }
256 |
257 | const pickImage = async () => {
258 | (async () => {
259 | if (Platform.OS !== 'web') {
260 | const { status } =
261 | await ImagePicker.requestMediaLibraryPermissionsAsync();
262 | if (status !== 'granted') {
263 | handleSetSnack({
264 | message:
265 | 'Sorry, we need camera roll permissions to make this work!',
266 | });
267 | }
268 | MediaLibrary.requestPermissionsAsync();
269 | }
270 | })();
271 |
272 | const result = await ImagePicker.launchImageLibraryAsync({
273 | mediaTypes: ImagePicker.MediaTypeOptions.All,
274 | aspect: [4, 3],
275 | quality: 1,
276 | });
277 |
278 | if (!result.cancelled) {
279 | const { uri, type } = result as ImageInfo;
280 | const filename: string = uri.replace(/^.*[\\\/]/, '');
281 | const ext: string | null = reExt.exec(filename)![1];
282 | const fileNamePrefix = type === 'image' ? 'IMG_' : 'VID_';
283 | FileSystem.moveAsync({
284 | from: uri,
285 | to:
286 | currentDir +
287 | '/' +
288 | fileNamePrefix +
289 | moment().format('DDMMYHmmss') +
290 | '.' +
291 | ext,
292 | })
293 | .then((_) => getFiles())
294 | .catch((err) => console.log(err));
295 | }
296 | };
297 |
298 | async function handleCopy(
299 | from: string,
300 | to: string,
301 | successMessage: string,
302 | errorMessage: string
303 | ): Promise {
304 | FileSystem.copyAsync({ from, to })
305 | .then(() => {
306 | getFiles();
307 | handleSetSnack({
308 | message: successMessage,
309 | });
310 | })
311 | .catch(() =>
312 | handleSetSnack({
313 | message: errorMessage,
314 | })
315 | );
316 | }
317 |
318 | const pickDocument = async () => {
319 | const result = await DocumentPicker.getDocumentAsync({
320 | copyToCacheDirectory: false,
321 | });
322 |
323 | if (result.type === 'success') {
324 | const { exists: fileExists } = await FileSystem.getInfoAsync(
325 | currentDir + '/' + result.name
326 | );
327 | if (fileExists) {
328 | Alert.alert(
329 | 'Conflicting File',
330 | `The destination folder has a file with the same name ${result.name}`,
331 | [
332 | {
333 | text: 'Cancel',
334 | style: 'cancel',
335 | },
336 | {
337 | text: 'Replace the file',
338 | onPress: () => {
339 | handleCopy(
340 | result.uri,
341 | currentDir + '/' + result.name,
342 | `${result.name} successfully copied.`,
343 | 'An unexpected error importing the file.'
344 | );
345 | },
346 | style: 'default',
347 | },
348 | ]
349 | );
350 | } else {
351 | handleCopy(
352 | result.uri,
353 | currentDir + '/' + result.name,
354 | `${result.name} successfully copied.`,
355 | 'An unexpected error importing the file.'
356 | );
357 | }
358 | }
359 | };
360 |
361 | const onMultiSelectSubmit = async (data: ExtendedAsset[]) => {
362 | const transferPromises = data.map((file) =>
363 | FileSystem.copyAsync({
364 | from: file.uri,
365 | to: currentDir + '/' + file.filename,
366 | })
367 | );
368 | Promise.all(transferPromises).then(() => {
369 | setMultiImageVisible(false);
370 | getFiles();
371 | });
372 | };
373 |
374 | const moveSelectedFiles = async (destination: string) => {
375 | const selectedFiles = files.filter((file) => file.selected);
376 | const destinationFolderFiles = await FileSystem.readDirectoryAsync(
377 | destination
378 | );
379 | function executeTransfer() {
380 | const transferPromises = selectedFiles.map((file) => {
381 | if (moveOrCopy === 'Copy')
382 | return FileSystem.copyAsync({
383 | from: currentDir + '/' + file.name,
384 | to: destination + '/' + file.name,
385 | });
386 | else
387 | return FileSystem.moveAsync({
388 | from: currentDir + '/' + file.name,
389 | to: destination + '/' + file.name,
390 | });
391 | });
392 | allProgress(transferPromises, (p) => {}).then((_) => {
393 | setDestinationDialogVisible(false);
394 | setMoveDir('');
395 | setMoveOrCopy('');
396 | getFiles();
397 | });
398 | }
399 | const conflictingFiles = selectedFiles.filter((file) =>
400 | destinationFolderFiles.includes(file.name)
401 | );
402 | const confLen = conflictingFiles.length;
403 | if (confLen > 0) {
404 | Alert.alert(
405 | 'Conflicting Files',
406 | `The destination folder has ${confLen} ${
407 | confLen === 1 ? 'file' : 'files'
408 | } with the same ${confLen === 1 ? 'name' : 'names'}.`,
409 | [
410 | {
411 | text: 'Cancel',
412 | style: 'cancel',
413 | },
414 | {
415 | text: 'Replace the files',
416 | onPress: () => {
417 | executeTransfer();
418 | },
419 | style: 'default',
420 | },
421 | ]
422 | );
423 | } else {
424 | executeTransfer();
425 | }
426 | };
427 |
428 | const deleteSelectedFiles = async (file?: fileItem) => {
429 | const filestoBeDeleted = file ? [file] : selectedFiles;
430 | const deleteProms = filestoBeDeleted.map((file) =>
431 | FileSystem.deleteAsync(file.uri)
432 | );
433 | Promise.all(deleteProms)
434 | .then((_) => {
435 | handleSetSnack({
436 | message: 'Files deleted!',
437 | });
438 | getFiles();
439 | setSelectedFiles([]);
440 | })
441 | .catch((err) => {
442 | console.log(err);
443 | getFiles();
444 | });
445 | };
446 |
447 | const [initialSelectionDone, setInitialSelectionDone] = useState(false);
448 |
449 | useEffect(() => {
450 | if (renameDialogVisible && Platform.OS === 'android') {
451 | setTimeout(() => {
452 | renameInputRef.current?.focus();
453 | }, 100);
454 | }
455 | if (!renameDialogVisible)
456 | setTimeout(() => {
457 | setInitialSelectionDone(false);
458 | }, 500);
459 | }, [renameDialogVisible]);
460 |
461 | const onRename = async () => {
462 | const filePathSplit = renamingFile.uri.split('/');
463 | const fileFolderPath = filePathSplit
464 | .slice(0, filePathSplit.length - 1)
465 | .join('/');
466 | FileSystem.getInfoAsync(fileFolderPath + '/' + newFileName).then((res) => {
467 | if (res.exists)
468 | handleSetSnack({
469 | message: 'A folder or file with the same name already exists.',
470 | });
471 | else
472 | FileSystem.moveAsync({
473 | from: renamingFile.uri,
474 | to: fileFolderPath + '/' + newFileName,
475 | })
476 | .then(() => {
477 | setRenameDialogVisible(false);
478 | getFiles();
479 | })
480 | .catch((_) =>
481 | handleSetSnack({
482 | message: 'Error renaming the file/folder',
483 | })
484 | );
485 | });
486 | };
487 |
488 | const handleSetSnack = (data: snackActionPayload) => {
489 | dispatch(setSnack(data));
490 | };
491 |
492 | return (
493 |
494 | {
514 | if (buttonIndex === 0) {
515 | pickImage();
516 | } else if (buttonIndex === 1) {
517 | setMultiImageVisible(true);
518 | } else if (buttonIndex === 2) {
519 | pickDocument();
520 | } else if (buttonIndex === 3) {
521 | setDownloadDialogVisible(true);
522 | }
523 | }}
524 | cancelButtonIndex={4}
525 | modalStyle={{ backgroundColor: colors.background2 }}
526 | itemTextStyle={{ color: colors.text }}
527 | titleStyle={{ color: colors.secondary }}
528 | />
529 |
539 |
544 |
549 |
550 | Rename file
551 | {
555 | setNewFileName(text);
556 | }}
557 | onKeyPress={() => {
558 | setInitialSelectionDone(true);
559 | }}
560 | selection={
561 | !initialSelectionDone
562 | ? { start: 0, end: decodeURI(newFileName).split('.')[0].length }
563 | : undefined
564 | }
565 | style={{ color: 'black' }}
566 | >
567 | {
570 | setRenameDialogVisible(false);
571 | }}
572 | />
573 | onRename()} />
574 |
575 | setMultiImageVisible(false)}
584 | >
585 | setMultiImageVisible(false)}
588 | />
589 |
590 |
591 |
596 |
597 |
598 |
599 | setNewFileActionSheet(true)}>
600 |
601 |
602 | setFolderDialogVisible(true)}>
603 |
604 |
605 |
606 | {multiSelect && (
607 |
608 | {
610 | setDestinationDialogVisible(true);
611 | setMoveOrCopy('Move');
612 | }}
613 | >
614 |
619 |
620 |
621 |
622 |
628 |
629 |
630 | )}
631 |
632 |
633 |
639 |
640 | {multiSelect && (
641 |
644 |
645 |
650 |
651 |
652 | )}
653 |
654 | );
655 | };
656 |
657 | const _keyExtractor = (item: fileItem) => item.name;
658 |
659 | const styles = StyleSheet.create({
660 | container: {
661 | flex: 1,
662 | width: SIZE,
663 | paddingTop: Constants.statusBarHeight,
664 | },
665 | topButtons: {
666 | display: 'flex',
667 | flexDirection: 'row',
668 | justifyContent: 'space-between',
669 | alignItems: 'center',
670 | marginTop: 15,
671 | marginHorizontal: 10,
672 | },
673 | topLeft: {
674 | display: 'flex',
675 | flexDirection: 'row',
676 | justifyContent: 'space-between',
677 | alignItems: 'center',
678 | width: '25%',
679 | },
680 | topRight: {
681 | width: '75%',
682 | display: 'flex',
683 | flexDirection: 'row',
684 | justifyContent: 'flex-end',
685 | alignItems: 'center',
686 | },
687 | fileList: {
688 | flex: 1,
689 | borderTopWidth: 0.5,
690 | marginTop: 15,
691 | marginHorizontal: 5,
692 | },
693 | bottomMenu: {
694 | height: 45,
695 | display: 'flex',
696 | flexDirection: 'row',
697 | justifyContent: 'space-evenly',
698 | alignItems: 'center',
699 | },
700 | contentStyle: {
701 | width: SIZE,
702 | height: HEIGHT * 0.8,
703 | padding: 0,
704 | margin: 0,
705 | },
706 | overlayStyle: {
707 | width: SIZE,
708 | padding: 0,
709 | margin: 0,
710 | },
711 | });
712 |
713 | export default Browser;
714 |
--------------------------------------------------------------------------------
/screens/FileTransfer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import {
3 | Text,
4 | View,
5 | StyleSheet,
6 | Button,
7 | TextInput,
8 | TouchableOpacity,
9 | } from 'react-native';
10 |
11 | import * as FileSystem from 'expo-file-system';
12 | import Constants from 'expo-constants';
13 |
14 | import { BarCodeScanner } from 'expo-barcode-scanner';
15 | import { io, Socket } from 'socket.io-client';
16 |
17 | import { useAppSelector } from '../hooks/reduxHooks';
18 | import {
19 | fileItem,
20 | fileRequestMessage,
21 | newFileTransfer,
22 | newFolderRequest,
23 | } from '../types';
24 |
25 | import { base64reg, SIZE } from '../utils/Constants';
26 | import { MaterialIcons } from '@expo/vector-icons';
27 |
28 | const FileTransfer: React.FC = () => {
29 | const { colors } = useAppSelector((state) => state.theme.theme);
30 | const [hasPermission, setHasPermission] = useState(null);
31 | const [scanned, setScanned] = useState(true);
32 | const [socket, setSocket] = useState(io());
33 | const [socketURL, setSocketURL] = useState('');
34 | const [roomID, setRoomID] = useState('1234');
35 | const [_, setState] = useState(false);
36 | const fileChunk = React.useRef('');
37 |
38 | const connectServer = () => {
39 | if (!socket.connected) {
40 | const newSocket = io(socketURL);
41 | setSocket(newSocket);
42 | setTimeout(() => {
43 | setState((prev) => !prev);
44 | }, 500);
45 | }
46 | };
47 |
48 | useEffect(() => {
49 | return () => socket.close();
50 | }, []);
51 |
52 | useEffect(() => {
53 | (async () => {
54 | const { status } = await BarCodeScanner.requestPermissionsAsync();
55 | setHasPermission(status === 'granted');
56 | })();
57 | }, []);
58 |
59 | useEffect(() => {
60 | socket.on('connected', (data) => {
61 | console.log(data);
62 | });
63 | }, [socket]);
64 |
65 | const handleScan = ({ _, data }) => {
66 | setScanned(true);
67 | setSocketURL(data);
68 | };
69 |
70 | const joinRoom = () => {
71 | socket.emit('joinRoom', { room: roomID, device: 'phone' });
72 | socket.on('welcome', (msg) => {
73 | setState((prev) => !prev);
74 | });
75 | socket.on('request', (msg: fileRequestMessage) => {
76 | const baseDir =
77 | msg.basedir === 'docdir'
78 | ? FileSystem.documentDirectory
79 | : FileSystem.cacheDirectory;
80 | const path = baseDir + msg.path;
81 | FileSystem.readDirectoryAsync(path)
82 | .then((files) => {
83 | const filesProms = files.map((fileName) =>
84 | FileSystem.getInfoAsync(path + '/' + fileName)
85 | );
86 | Promise.all(filesProms).then((results) => {
87 | let tempfiles: fileItem[] = results.map((file) =>
88 | Object({
89 | ...file,
90 | name: file.uri.split('/').pop(),
91 | selected: false,
92 | })
93 | );
94 | socket.emit('respond', { path, files: tempfiles });
95 | });
96 | })
97 | .catch((err) => console.log(err));
98 | });
99 |
100 | socket.on('readfile', (msg: fileRequestMessage) => {
101 | const baseDir =
102 | msg.basedir === 'docdir'
103 | ? FileSystem.documentDirectory
104 | : FileSystem.cacheDirectory;
105 | const path = baseDir + msg.path;
106 | FileSystem.readAsStringAsync(path, {
107 | encoding: 'base64',
108 | })
109 | .then((file) => {
110 | transferChunks(file, 1024 * 300, file.length, socket);
111 | })
112 | .catch((err) => console.log(err));
113 | });
114 |
115 | socket.on('newfolder', (msg: newFolderRequest) => {
116 | const newFolderURI =
117 | FileSystem.documentDirectory + '/' + msg.path + '/' + msg.name;
118 | FileSystem.getInfoAsync(newFolderURI).then((res) => {
119 | if (!res.exists) {
120 | FileSystem.makeDirectoryAsync(newFolderURI);
121 | }
122 | });
123 | });
124 |
125 | socket.on('sendfile', (msg: newFileTransfer) => {
126 | fileChunk.current += msg.file;
127 | if (msg.file.length === msg.size) {
128 | const base64Data = fileChunk.current.replace(base64reg, '');
129 | const newFilePath =
130 | FileSystem.documentDirectory + '/' + msg.path + '/' + msg.name;
131 | FileSystem.getInfoAsync(newFilePath).then((res) => {
132 | if (!res.exists) {
133 | FileSystem.writeAsStringAsync(newFilePath, base64Data, {
134 | encoding: 'base64',
135 | });
136 | }
137 | });
138 | fileChunk.current = '';
139 | }
140 | });
141 | };
142 |
143 | const transferChunks = (
144 | data: string,
145 | bufferSize: number,
146 | totalSize: number,
147 | socket: Socket
148 | ) => {
149 | let chunk = data.slice(0, bufferSize);
150 | data = data.slice(bufferSize, data.length);
151 | socket.emit('respondfile', { file: chunk, size: totalSize });
152 | if (data.length !== 0) {
153 | transferChunks(data, bufferSize, totalSize, socket);
154 | }
155 | };
156 |
157 | return (
158 |
159 | {!scanned && hasPermission && (
160 |
164 | )}
165 |
166 |
167 |
168 |
174 | Socket Status:
175 |
176 |
177 |
178 |
184 | {socket.connected ? 'Connected' : 'Disconnected'}
185 |
186 |
187 |
188 |
189 |
190 |
196 | Socket URL:
197 |
198 |
199 |
200 |
212 | setScanned((prev) => !prev)}
215 | >
216 |
221 |
222 |
223 |
224 |
225 |
226 |
232 | Room ID:
233 |
234 |
235 |
236 |
248 |
249 |
250 |
251 |
252 |
258 | Socket ID:
259 |
260 |
261 |
262 |
268 | {socket.id}
269 |
270 |
271 |
272 |
273 |
274 |
275 | {
278 | socket.close();
279 | setTimeout(() => {
280 | setState((prev) => !prev);
281 | }, 500);
282 | }}
283 | />
284 |
285 |
286 | );
287 | };
288 |
289 | const styles = StyleSheet.create({
290 | container: {
291 | flex: 1,
292 | paddingTop: Constants.statusBarHeight + 20,
293 | padding: 10,
294 | },
295 | section: {
296 | width: '100%',
297 | display: 'flex',
298 | flexDirection: 'column',
299 | },
300 | row: {
301 | width: SIZE,
302 | display: 'flex',
303 | flexDirection: 'row',
304 | alignItems: 'center',
305 | justifyContent: 'center',
306 | minHeight: 40,
307 | },
308 | rowLeft: {
309 | width: '30%',
310 | height: 40,
311 | padding: 1,
312 | display: 'flex',
313 | alignItems: 'flex-start',
314 | justifyContent: 'flex-end',
315 | },
316 | rowRight: {
317 | width: '70%',
318 | height: 40,
319 | padding: 1,
320 | position: 'relative',
321 | display: 'flex',
322 | alignItems: 'flex-start',
323 | justifyContent: 'flex-end',
324 | },
325 | roomInput: {
326 | height: 40,
327 | width: SIZE * 0.65,
328 | borderBottomWidth: 0.5,
329 | padding: 5,
330 | },
331 | roomIDContainer: {
332 | width: '100%',
333 | display: 'flex',
334 | flexDirection: 'row',
335 | alignItems: 'center',
336 | justifyContent: 'flex-start',
337 | },
338 | scanIcon: {
339 | position: 'absolute',
340 | right: 20,
341 | },
342 | });
343 |
344 | export default FileTransfer;
345 |
--------------------------------------------------------------------------------
/screens/ImageGalleryView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 |
4 | import ImageView from 'react-native-image-viewing';
5 |
6 | import { StackScreenProps } from '@react-navigation/stack';
7 | import { useAppDispatch, useAppSelector } from '../hooks/reduxHooks';
8 | import { useNavigation } from '@react-navigation/native';
9 | import { setTabbarVisible } from '../features/files/tabbarStyleSlice';
10 |
11 | type FileViewParamList = {
12 | ImageGalleryView: { prevDir: string; folderName: string };
13 | };
14 |
15 | type Props = StackScreenProps;
16 |
17 | const ImageGalleryView = ({ route }: Props) => {
18 | const navigation = useNavigation();
19 | const dispatch = useAppDispatch();
20 | const { colors } = useAppSelector((state) => state.theme.theme);
21 | const { prevDir, folderName } = route.params;
22 | const { images } = useAppSelector((state) => state.images);
23 |
24 | const initialImageIndex = useCallback(
25 | () => images.findIndex((item) => item.uri === prevDir + folderName),
26 | []
27 | );
28 |
29 | useEffect(() => {
30 | dispatch(setTabbarVisible(false));
31 |
32 | return () => {
33 | dispatch(setTabbarVisible(true));
34 | };
35 | }, []);
36 |
37 | return (
38 |
39 | navigation.goBack()}
44 | keyExtractor={(_, index) => index.toString()}
45 | doubleTapToZoomEnabled
46 | swipeToCloseEnabled
47 | />
48 |
49 | );
50 | };
51 |
52 | export default ImageGalleryView;
53 |
54 | const styles = StyleSheet.create({
55 | container: {
56 | flex: 1,
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/screens/LockScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
3 |
4 | import { FontAwesome5 } from '@expo/vector-icons';
5 |
6 | import Constants from 'expo-constants';
7 | import * as SecureStore from 'expo-secure-store';
8 | import * as LocalAuthentication from 'expo-local-authentication';
9 |
10 | import useBiometrics from '../hooks/useBiometrics';
11 | import { useAppDispatch, useAppSelector } from '../hooks/reduxHooks';
12 |
13 | import { SIZE } from '../utils/Constants';
14 | import { setSnack } from '../features/files/snackbarSlice';
15 |
16 | const DIGIT_SIZE = SIZE / 6;
17 |
18 | type ILockScreenProps = {
19 | setLocked: (value: boolean) => void;
20 | };
21 |
22 | const LockScreen = ({ setLocked }: ILockScreenProps) => {
23 | const dispatch = useAppDispatch();
24 | const { colors } = useAppSelector((state) => state.theme.theme);
25 | const { biometricsActive } = useBiometrics();
26 | const [secret, setSecret] = useState('');
27 | const [checkPin, setCheckPin] = useState('');
28 | const [dotsArray, setDotsArray] = useState([
29 | false,
30 | false,
31 | false,
32 | false,
33 | ]);
34 |
35 | const getSecret = async () => {
36 | SecureStore.getItemAsync('secret').then((res) => setSecret(res));
37 | };
38 |
39 | const authWithBiometrics = () => {
40 | LocalAuthentication.authenticateAsync().then((result) => {
41 | if (result.success) {
42 | setLocked(false);
43 | }
44 | });
45 | };
46 |
47 | useEffect(() => {
48 | let isMounted = true;
49 | if (isMounted) getSecret();
50 | return () => {
51 | isMounted = false;
52 | };
53 | }, []);
54 |
55 | useEffect(() => {
56 | if (checkPin.length === 4) {
57 | if (secret === checkPin) {
58 | setTimeout(() => {
59 | setLocked(false);
60 | }, 10);
61 | } else {
62 | dispatch(setSnack({ message: 'Wrong PIN!' }));
63 | setCheckPin('');
64 | }
65 | }
66 | }, [checkPin]);
67 |
68 | const DigitItem = ({ digit }: { digit: number }) => {
69 | return (
70 | onDigitPress(digit)}
74 | >
75 |
76 | {digit}
77 |
78 |
79 | );
80 | };
81 |
82 | const DigitsRow = ({ digits }: { digits: number[] }) => {
83 | return (
84 |
85 | {digits.map((digit) => (
86 |
87 | ))}
88 |
89 | );
90 | };
91 |
92 | const handleRemove = () => {
93 | if (checkPin.length > 0) {
94 | setCheckPin((prev) => prev.slice(0, prev.length - 1));
95 | }
96 | };
97 |
98 | const onDigitPress = (digit: number) => {
99 | if (checkPin.length < 4) {
100 | setCheckPin((prev) => prev + digit);
101 | }
102 | };
103 |
104 | const PinDot = ({ filled, index }: { filled: boolean; index: number }) => {
105 | return (
106 |
113 | );
114 | };
115 |
116 | const PinDots = () => {
117 | return (
118 |
119 | {dotsArray.map((dot, index) => (
120 |
121 | ))}
122 |
123 | );
124 | };
125 |
126 | useEffect(() => {
127 | let dotsRef = checkPin;
128 | switch (dotsRef.length) {
129 | case 0:
130 | setDotsArray((_) => [false, false, false, false]);
131 | break;
132 | case 1:
133 | setDotsArray((_) => [true, false, false, false]);
134 | break;
135 | case 2:
136 | setDotsArray((_) => [true, true, false, false]);
137 | break;
138 | case 3:
139 | setDotsArray((_) => [true, true, true, false]);
140 | break;
141 | case 4:
142 | setDotsArray((_) => [true, true, true, true]);
143 | break;
144 | default:
145 | break;
146 | }
147 | }, [checkPin]);
148 |
149 | return (
150 |
151 |
154 |
155 | Enter Your PIN
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | {
168 | if (biometricsActive) {
169 | authWithBiometrics();
170 | }
171 | }}
172 | >
173 |
178 |
179 | onDigitPress(0)}
183 | >
184 |
185 | 0
186 |
187 |
188 |
193 |
194 | {'<'}
195 |
196 |
197 |
198 |
199 |
200 |
201 | );
202 | };
203 |
204 | export default LockScreen;
205 |
206 | const styles = StyleSheet.create({
207 | container: {
208 | flex: 1,
209 | paddingTop: Constants.statusBarHeight + 20,
210 | },
211 | title: {
212 | fontFamily: 'Poppins_600SemiBold',
213 | fontSize: 22,
214 | },
215 | setupContainer: {
216 | width: SIZE,
217 | display: 'flex',
218 | flexDirection: 'column',
219 | alignItems: 'center',
220 | justifyContent: 'center',
221 | },
222 | digitsContainer: {
223 | width: '100%',
224 | height: '70%',
225 | display: 'flex',
226 | flexDirection: 'column',
227 | alignItems: 'center',
228 | justifyContent: 'space-evenly',
229 | marginTop: 20,
230 | },
231 | digitRow: {
232 | width: '100%',
233 | display: 'flex',
234 | flexDirection: 'row',
235 | alignItems: 'center',
236 | justifyContent: 'space-evenly',
237 | },
238 | digitItem: {
239 | display: 'flex',
240 | alignItems: 'center',
241 | justifyContent: 'center',
242 | width: DIGIT_SIZE,
243 | height: DIGIT_SIZE,
244 | },
245 | digitNumber: {
246 | fontFamily: 'Poppins_600SemiBold',
247 | fontSize: DIGIT_SIZE * 0.5,
248 | },
249 | pinDot: {
250 | width: 20,
251 | height: 20,
252 | borderRadius: 10,
253 | marginHorizontal: 15,
254 | },
255 | pinDotsContainer: {
256 | width: SIZE,
257 | height: 25,
258 | display: 'flex',
259 | flexDirection: 'row',
260 | alignItems: 'center',
261 | justifyContent: 'center',
262 | },
263 | });
264 |
--------------------------------------------------------------------------------
/screens/Main.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { LogBox, View } from 'react-native';
3 |
4 | import { StatusBar } from 'expo-status-bar';
5 | import { Snackbar } from 'react-native-paper';
6 | import {
7 | NavigationContainer,
8 | DarkTheme,
9 | DefaultTheme,
10 | } from '@react-navigation/native';
11 | import {
12 | useFonts,
13 | Poppins_400Regular,
14 | Poppins_500Medium,
15 | Poppins_600SemiBold,
16 | Poppins_700Bold,
17 | } from '@expo-google-fonts/poppins';
18 |
19 | import * as SecureStore from 'expo-secure-store';
20 | import AsyncStorage from '@react-native-async-storage/async-storage';
21 | import * as SplashScreen from 'expo-splash-screen';
22 |
23 | import { MainNavigator } from '../navigation/MainNavigator';
24 |
25 | import useColorScheme from '../hooks/useColorScheme';
26 | import useLock from '../hooks/useLock';
27 | import { useAppDispatch, useAppSelector } from '../hooks/reduxHooks';
28 | import { setLightTheme, setDarkTheme } from '../features/files/themeSlice';
29 | import { hideSnack } from '../features/files/snackbarSlice';
30 |
31 | import LockScreen from '../screens/LockScreen';
32 |
33 | SplashScreen.preventAutoHideAsync();
34 |
35 | LogBox.ignoreLogs([
36 | 'VirtualizedLists should never',
37 | 'supplied to `DialogInput`',
38 | ]);
39 |
40 | export default function Main() {
41 | const { locked, setLocked } = useLock();
42 | const { theme } = useAppSelector((state) => state.theme);
43 | const {
44 | isVisible: isSnackVisible,
45 | message: snackMessage,
46 | label: snackLabel,
47 | } = useAppSelector((state) => state.snackbar);
48 | const colorScheme = useColorScheme();
49 | const dispatch = useAppDispatch();
50 |
51 | const getPassCodeStatus = async () => {
52 | const hasPassCode = await SecureStore.getItemAsync('hasPassCode');
53 | if (JSON.parse(hasPassCode)) {
54 | setLocked(true);
55 | return true;
56 | } else {
57 | setLocked(false);
58 | return false;
59 | }
60 | };
61 |
62 | useEffect(() => {
63 | getPassCodeStatus();
64 | }, []);
65 |
66 | useEffect(() => {
67 | const setColorScheme = async () => {
68 | const storedScheme = await AsyncStorage.getItem('colorScheme');
69 | if (!storedScheme) {
70 | await AsyncStorage.setItem('colorScheme', colorScheme);
71 | dispatch(colorScheme === 'dark' ? setDarkTheme() : setLightTheme());
72 | } else {
73 | dispatch(storedScheme === 'dark' ? setDarkTheme() : setLightTheme());
74 | }
75 | };
76 | setColorScheme();
77 | }, []);
78 |
79 | let [fontsLoaded] = useFonts({
80 | Poppins_400Regular,
81 | Poppins_500Medium,
82 | Poppins_600SemiBold,
83 | Poppins_700Bold,
84 | });
85 |
86 | useEffect(() => {
87 | if (fontsLoaded) SplashScreen.hideAsync();
88 | }, [fontsLoaded]);
89 |
90 | if (locked && fontsLoaded) {
91 | return ;
92 | }
93 |
94 | return (
95 |
96 | dispatch(hideSnack())}
103 | duration={2000}
104 | action={
105 | snackLabel
106 | ? {
107 | label: snackLabel,
108 | onPress: () => {},
109 | }
110 | : null
111 | }
112 | >
113 | {snackMessage}
114 |
115 |
116 |
117 |
118 |
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/screens/MiscFileView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text, StyleSheet } from 'react-native';
3 |
4 | import Constants from 'expo-constants';
5 |
6 | import { useAppSelector } from '../hooks/reduxHooks';
7 | import { StackScreenProps } from '@react-navigation/stack';
8 | import { PDFViewer } from '../components/MiscFileView/PDFViewer';
9 |
10 | type MiscFileViewParamList = {
11 | MiscFileView: { prevDir: string; folderName: string };
12 | };
13 |
14 | type Props = StackScreenProps;
15 |
16 | const MiscFileView = ({ route }: Props) => {
17 | const { colors } = useAppSelector((state) => state.theme.theme);
18 | const { prevDir, folderName } = route.params;
19 | const fileExt = folderName.split('/').pop().split('.').pop().toLowerCase();
20 |
21 | if (fileExt === 'pdf')
22 | return ;
23 |
24 | return (
25 |
26 |
27 | This file format is not supported.
28 |
29 |
30 | );
31 | };
32 |
33 | export default MiscFileView;
34 |
35 | const styles = StyleSheet.create({
36 | container: {
37 | flex: 1,
38 | paddingTop: Constants.statusBarHeight,
39 | alignItems: 'center',
40 | justifyContent: 'center',
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/screens/Settings/SetPassCodeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Text, View, StyleSheet, TouchableOpacity } from 'react-native';
3 |
4 | import * as SecureStore from 'expo-secure-store';
5 | import Constants from 'expo-constants';
6 |
7 | import { useNavigation } from '@react-navigation/native';
8 | import { StackNavigationProp } from '@react-navigation/stack';
9 |
10 | import { useAppDispatch, useAppSelector } from '../../hooks/reduxHooks';
11 | import useLock from '../../hooks/useLock';
12 |
13 | import { SIZE } from '../../utils/Constants';
14 | import { setSnack } from '../../features/files/snackbarSlice';
15 |
16 | const DIGIT_SIZE = SIZE / 6;
17 |
18 | const SetPassCodeScreen: React.FC = () => {
19 | const dispatch = useAppDispatch();
20 | const navigation = useNavigation>();
21 | const { colors } = useAppSelector((state) => state.theme.theme);
22 | const [newPinFirst, setNewPinFirst] = useState('');
23 | const [newPinSecond, setNewPinSecond] = useState('');
24 | const [firstPinDone, setFirstPinDone] = useState(false);
25 | const { pinActive } = useLock();
26 | const [secret, setSecret] = useState('');
27 | const [checkPin, setCheckPin] = useState('');
28 | const [dotsArray, setDotsArray] = useState([
29 | false,
30 | false,
31 | false,
32 | false,
33 | ]);
34 |
35 | const getSecret = async () => {
36 | SecureStore.getItemAsync('secret').then((res) => setSecret(res));
37 | };
38 |
39 | useEffect(() => {
40 | getSecret();
41 | }, []);
42 |
43 | useEffect(() => {
44 | if (checkPin.length === 4) {
45 | if (secret === checkPin) {
46 | SecureStore.deleteItemAsync('secret').then(() => {
47 | SecureStore.deleteItemAsync('hasPassCode').then(() => {
48 | dispatch(setSnack({ message: 'PIN Code Removed!' }));
49 | navigation.goBack();
50 | });
51 | });
52 | } else {
53 | dispatch(setSnack({ message: 'Wrong PIN!' }));
54 | setCheckPin('');
55 | }
56 | }
57 | }, [checkPin]);
58 |
59 | useEffect(() => {
60 | if (!pinActive) {
61 | const firstPinLen = newPinFirst.length;
62 | const secondPinLen = newPinSecond.length;
63 | if (firstPinLen === 4) {
64 | setFirstPinDone(true);
65 | }
66 | if (firstPinLen === 4 && secondPinLen === 4) {
67 | if (newPinFirst === newPinSecond) {
68 | savePintoStorage();
69 | } else {
70 | dispatch(setSnack({ message: 'Pins do not match!' }));
71 | setNewPinFirst('');
72 | setNewPinSecond('');
73 | setFirstPinDone(false);
74 | }
75 | }
76 | }
77 | }, [newPinFirst, newPinSecond]);
78 |
79 | const savePintoStorage = async () => {
80 | SecureStore.setItemAsync('hasPassCode', JSON.stringify(true)).then(() => {
81 | SecureStore.setItemAsync('secret', newPinFirst).then(() => {
82 | dispatch(setSnack({ message: 'PIN Code Set!' }));
83 | navigation.goBack();
84 | });
85 | });
86 | };
87 |
88 | const handleRemove = () => {
89 | if (!firstPinDone && newPinFirst.length > 0) {
90 | setNewPinFirst((prev) => prev.slice(0, prev.length - 1));
91 | }
92 | if (firstPinDone && newPinSecond.length > 0) {
93 | setNewPinSecond((prev) => prev.slice(0, prev.length - 1));
94 | }
95 | };
96 |
97 | const onDigitPress = (digit: number) => {
98 | if (!pinActive) {
99 | if (!firstPinDone && newPinFirst.length < 4) {
100 | setNewPinFirst((prev) => prev + digit);
101 | }
102 | if (firstPinDone && newPinSecond.length < 4) {
103 | setNewPinSecond((prev) => prev + digit);
104 | }
105 | } else {
106 | if (checkPin.length < 4) {
107 | setCheckPin((prev) => prev + digit);
108 | }
109 | }
110 | };
111 |
112 | const DigitItem = ({ digit }: { digit: number }) => {
113 | return (
114 | onDigitPress(digit)}
118 | >
119 |
120 | {digit}
121 |
122 |
123 | );
124 | };
125 |
126 | const DigitsRow = ({ digits }: { digits: number[] }) => {
127 | return (
128 |
129 | {digits.map((digit) => (
130 |
131 | ))}
132 |
133 | );
134 | };
135 |
136 | const PinDot = ({ filled, index }: { filled: boolean; index: number }) => {
137 | return (
138 |
145 | );
146 | };
147 |
148 | const PinDots = () => {
149 | return (
150 |
151 | {dotsArray.map((dot, index) => (
152 |
153 | ))}
154 |
155 | );
156 | };
157 |
158 | useEffect(() => {
159 | let dotsRef = !pinActive
160 | ? !firstPinDone
161 | ? newPinFirst
162 | : newPinSecond
163 | : checkPin;
164 | switch (dotsRef.length) {
165 | case 0:
166 | setDotsArray((_) => [false, false, false, false]);
167 | break;
168 | case 1:
169 | setDotsArray((_) => [true, false, false, false]);
170 | break;
171 | case 2:
172 | setDotsArray((_) => [true, true, false, false]);
173 | break;
174 | case 3:
175 | setDotsArray((_) => [true, true, true, false]);
176 | break;
177 | case 4:
178 | setDotsArray((_) => [true, true, true, true]);
179 | setTimeout(() => {
180 | setDotsArray((_) => [false, false, false, false]);
181 | }, 100);
182 | break;
183 | default:
184 | break;
185 | }
186 | }, [newPinFirst, newPinSecond, checkPin]);
187 |
188 | return (
189 |
190 |
193 |
194 | {!pinActive ? 'Setup a PIN Code' : 'Remove PIN Code'}
195 |
196 |
197 | {!pinActive
198 | ? !firstPinDone
199 | ? 'Enter New PIN'
200 | : 'Enter New PIN Again'
201 | : 'Enter Your PIN'}
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 | onDigitPress(0)}
216 | >
217 |
218 | 0
219 |
220 |
221 |
226 |
227 | {'<'}
228 |
229 |
230 |
231 |
232 |
233 |
234 | );
235 | };
236 |
237 | const styles = StyleSheet.create({
238 | container: {
239 | flex: 1,
240 | paddingTop: Constants.statusBarHeight + 20,
241 | },
242 | title: {
243 | fontFamily: 'Poppins_600SemiBold',
244 | fontSize: 22,
245 | },
246 | setupContainer: {
247 | width: SIZE,
248 | display: 'flex',
249 | flexDirection: 'column',
250 | alignItems: 'center',
251 | justifyContent: 'center',
252 | },
253 | digitsContainer: {
254 | width: '100%',
255 | height: '70%',
256 | display: 'flex',
257 | flexDirection: 'column',
258 | alignItems: 'center',
259 | justifyContent: 'space-evenly',
260 | },
261 | digitRow: {
262 | width: '100%',
263 | display: 'flex',
264 | flexDirection: 'row',
265 | alignItems: 'center',
266 | justifyContent: 'space-evenly',
267 | },
268 | digitItem: {
269 | display: 'flex',
270 | alignItems: 'center',
271 | justifyContent: 'center',
272 | width: DIGIT_SIZE,
273 | height: DIGIT_SIZE,
274 | },
275 | digitNumber: {
276 | fontFamily: 'Poppins_600SemiBold',
277 | fontSize: DIGIT_SIZE * 0.5,
278 | },
279 | pinDot: {
280 | width: 20,
281 | height: 20,
282 | borderRadius: 10,
283 | marginHorizontal: 15,
284 | },
285 | pinDotsContainer: {
286 | width: SIZE,
287 | height: 25,
288 | display: 'flex',
289 | flexDirection: 'row',
290 | alignItems: 'center',
291 | justifyContent: 'center',
292 | },
293 | });
294 |
295 | export default SetPassCodeScreen;
296 |
--------------------------------------------------------------------------------
/screens/Settings/Settings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text, Switch, StyleSheet, TouchableOpacity } from 'react-native';
3 | import { Feather, FontAwesome5 } from '@expo/vector-icons';
4 |
5 | import useLock from '../../hooks/useLock';
6 | import { useAppDispatch, useAppSelector } from '../../hooks/reduxHooks';
7 | import { setDarkTheme, setLightTheme } from '../../features/files/themeSlice';
8 |
9 | import Constants from 'expo-constants';
10 | import AsyncStorage from '@react-native-async-storage/async-storage';
11 |
12 | import { useNavigation } from '@react-navigation/native';
13 | import { StackNavigationProp } from '@react-navigation/stack';
14 |
15 | import useBiometrics from '../../hooks/useBiometrics';
16 | import { setSnack } from '../../features/files/snackbarSlice';
17 | import { SIZE } from '../../utils/Constants';
18 |
19 | function Settings() {
20 | const navigation = useNavigation>();
21 | const { theme } = useAppSelector((state) => state.theme);
22 | const { pinActive } = useLock();
23 | const { biometricsActive, hasHardware, isEnrolled, handleBiometricsStatus } =
24 | useBiometrics();
25 | const dispatch = useAppDispatch();
26 |
27 | return (
28 |
31 |
32 |
33 | PREFERENCES
34 |
35 |
41 |
42 |
47 |
48 |
49 |
52 | Dark Mode
53 |
54 |
55 |
56 | {
64 | if (theme.dark) {
65 | dispatch(setLightTheme());
66 | await AsyncStorage.setItem('colorScheme', 'light');
67 | } else {
68 | dispatch(setDarkTheme());
69 | await AsyncStorage.setItem('colorScheme', 'dark');
70 | }
71 | }}
72 | />
73 |
74 |
75 |
76 |
77 |
78 | SECURITY
79 |
80 |
86 |
87 |
92 |
93 |
94 |
97 | PIN Code
98 |
99 |
100 |
101 | {
103 | navigation.navigate('SetPassCodeScreen');
104 | }}
105 | >
106 |
111 |
112 |
113 |
114 |
120 |
121 |
126 |
127 |
128 |
131 | Unlock with Biometrics
132 |
133 |
134 |
135 | {
138 | if (!hasHardware) {
139 | dispatch(
140 | setSnack({ message: 'Device has no biometrics hardware' })
141 | );
142 | }
143 | }}
144 | disabled={!hasHardware}
145 | trackColor={{
146 | false: theme.colors.switchFalse,
147 | true: 'tomato',
148 | }}
149 | thumbColor={theme.colors.switchThumb}
150 | onChange={() => {
151 | if (hasHardware && isEnrolled) {
152 | handleBiometricsStatus();
153 | } else if (hasHardware && !isEnrolled) {
154 | dispatch(setSnack({ message: 'No biometrics enrolled!' }));
155 | }
156 | }}
157 | />
158 |
159 |
160 |
161 |
162 | );
163 | }
164 |
165 | export default Settings;
166 |
167 | const styles = StyleSheet.create({
168 | container: {
169 | flex: 1,
170 | paddingTop: Constants.statusBarHeight + 20,
171 | },
172 | section: {
173 | width: SIZE,
174 | display: 'flex',
175 | flexDirection: 'column',
176 | alignItems: 'center',
177 | justifyContent: 'center',
178 | marginBottom: 20,
179 | },
180 | sectionTitle: {
181 | fontFamily: 'Poppins_600SemiBold',
182 | fontSize: 16,
183 | },
184 | sectionItem: {
185 | display: 'flex',
186 | flexDirection: 'row',
187 | height: 45,
188 | },
189 | sectionItemText: {
190 | fontFamily: 'Poppins_500Medium',
191 | },
192 | sectionItemLeft: {
193 | width: '20%',
194 | display: 'flex',
195 | alignItems: 'center',
196 | justifyContent: 'center',
197 | },
198 | sectionItemCenter: {
199 | width: '60%',
200 | display: 'flex',
201 | alignItems: 'flex-start',
202 | justifyContent: 'center',
203 | },
204 | sectionItemRight: {
205 | width: '20%',
206 | display: 'flex',
207 | alignItems: 'center',
208 | justifyContent: 'center',
209 | },
210 | });
211 |
--------------------------------------------------------------------------------
/screens/VideoPlayer.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import React, { useRef } from 'react';
3 | import { StackScreenProps } from '@react-navigation/stack';
4 | import { useAppSelector } from '../hooks/reduxHooks';
5 | import { ResizeMode, Video } from 'expo-av';
6 | import Constants from 'expo-constants';
7 | import { SIZE } from '../utils/Constants';
8 | import { SafeAreaView } from 'react-native-safe-area-context';
9 |
10 | type VideoViewParamList = {
11 | VideoPlayer: { prevDir: string; folderName: string };
12 | };
13 |
14 | type Props = StackScreenProps;
15 |
16 | export default function VideoPlayer({ route }: Props) {
17 | const { colors } = useAppSelector((state) => state.theme.theme);
18 | const { prevDir, folderName } = route.params;
19 | const videoRef = useRef(null);
20 |
21 | return (
22 |
25 |
39 |
40 | );
41 | }
42 | const styles = StyleSheet.create({
43 | container: {
44 | flex: 1,
45 | paddingTop: Constants.statusBarHeight,
46 | alignItems: 'center',
47 | justifyContent: 'center',
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/screens/Web.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | TextInput,
6 | TouchableOpacity,
7 | Keyboard,
8 | BackHandler,
9 | } from 'react-native';
10 |
11 | import { WebView } from 'react-native-webview';
12 | import { Ionicons } from '@expo/vector-icons';
13 |
14 | import Constants from 'expo-constants';
15 |
16 | import { useAppSelector } from '../hooks/reduxHooks';
17 |
18 | const Web: React.FC = () => {
19 | const { colors } = useAppSelector((state) => state.theme.theme);
20 | const [target, setTarget] = useState('https://google.com/');
21 | const [url, setUrl] = useState(target);
22 | const [loadingProgress, setLoadingProgress] = useState(0);
23 | const [loadingBarVisible, setLoadingBarVisible] = useState(false);
24 | const [canGoBack, setCanGoBack] = useState(false);
25 | const [canGoForward, setCanGoForward] = useState(false);
26 | const [isFocused, setIsFocused] = useState(false);
27 | const browserRef = useRef();
28 |
29 | useEffect(() => {
30 | const backAction = () => {
31 | browserRef.current.goBack();
32 | return true;
33 | };
34 |
35 | const backHandler = BackHandler.addEventListener(
36 | 'hardwareBackPress',
37 | backAction
38 | );
39 |
40 | return () => backHandler.remove();
41 | }, []);
42 |
43 | const searchEngines = {
44 | google: (uri: string) => `https://www.google.com/search?q=${uri}`,
45 | };
46 |
47 | function upgradeURL(uri: string, searchEngine = 'google') {
48 | const isURL = uri.split(' ').length === 1 && uri.includes('.');
49 | if (isURL) {
50 | if (!uri.startsWith('http')) {
51 | return 'https://' + uri;
52 | }
53 | return uri;
54 | }
55 | const encodedURI = encodeURI(uri);
56 | return searchEngines[searchEngine](encodedURI);
57 | }
58 |
59 | const goForward = () => {
60 | if (browserRef && canGoForward) {
61 | browserRef.current.goForward();
62 | }
63 | };
64 |
65 | const goBack = () => {
66 | if (browserRef && canGoBack) {
67 | browserRef.current.goBack();
68 | }
69 | };
70 |
71 | const reloadPage = () => {
72 | if (browserRef) {
73 | browserRef.current.reload();
74 | }
75 | };
76 |
77 | return (
78 |
79 |
80 | setUrl(text)}
89 | onSubmitEditing={() => {
90 | Keyboard.dismiss;
91 | setTarget(upgradeURL(url));
92 | }}
93 | value={url}
94 | onFocus={() => setIsFocused(true)}
95 | onBlur={() => setIsFocused(false)}
96 | />
97 |
107 |
115 | setTarget(url)}>
116 |
121 |
122 |
123 |
124 | {
132 | const { nativeEvent } = syntheticEvent;
133 | setLoadingBarVisible(nativeEvent.loading);
134 | setUrl(nativeEvent.url);
135 | setCanGoBack(nativeEvent.canGoBack);
136 | setCanGoForward(nativeEvent.canGoForward);
137 | }}
138 | onLoadEnd={(syntheticEvent) => {
139 | const { nativeEvent } = syntheticEvent;
140 | setLoadingBarVisible(nativeEvent.loading);
141 | setTarget(nativeEvent.url);
142 | }}
143 | onLoadProgress={({ nativeEvent }) => {
144 | setLoadingProgress(nativeEvent.progress);
145 | }}
146 | />
147 |
148 |
149 |
154 |
155 |
156 |
157 |
158 |
159 |
164 |
165 |
166 |
167 | );
168 | };
169 |
170 | const styles = StyleSheet.create({
171 | container: {
172 | flex: 1,
173 | paddingTop: Constants.statusBarHeight,
174 | },
175 | searchBar: {
176 | flex: 0.1,
177 | flexDirection: 'column',
178 | justifyContent: 'center',
179 | alignItems: 'center',
180 | },
181 | searchBarInput: {
182 | height: 40,
183 | borderWidth: 0.5,
184 | marginTop: 5,
185 | marginHorizontal: 10,
186 | padding: 5,
187 | paddingRight: 45,
188 | borderRadius: 5,
189 | width: '95%',
190 | overflow: 'hidden',
191 | },
192 | bottomBar: {
193 | width: '100%',
194 | height: 50,
195 | display: 'flex',
196 | flexDirection: 'row',
197 | justifyContent: 'space-evenly',
198 | alignItems: 'center',
199 | },
200 | progressBar: {
201 | height: 2,
202 | marginBottom: 2,
203 | },
204 | });
205 |
206 | export default Web;
207 |
--------------------------------------------------------------------------------
/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/1.png
--------------------------------------------------------------------------------
/screenshots/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/10.png
--------------------------------------------------------------------------------
/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/2.png
--------------------------------------------------------------------------------
/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/3.png
--------------------------------------------------------------------------------
/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/4.png
--------------------------------------------------------------------------------
/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/5.png
--------------------------------------------------------------------------------
/screenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/6.png
--------------------------------------------------------------------------------
/screenshots/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/7.png
--------------------------------------------------------------------------------
/screenshots/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/8.png
--------------------------------------------------------------------------------
/screenshots/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martymfly/expo-file-manager/48ffd1138018cecdb9babb0702dc2c078963a049/screenshots/9.png
--------------------------------------------------------------------------------
/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import imagesSlice from './features/files/imagesSlice';
3 | import snackbarSlice from './features/files/snackbarSlice';
4 | import tabbarStyleSlice from './features/files/tabbarStyleSlice';
5 | import themeSlice from './features/files/themeSlice';
6 |
7 | export const store = configureStore({
8 | reducer: {
9 | images: imagesSlice,
10 | theme: themeSlice,
11 | snackbar: snackbarSlice,
12 | tabbarStyle: tabbarStyleSlice,
13 | },
14 | });
15 |
16 | export type RootState = ReturnType;
17 | export type AppDispatch = typeof store.dispatch;
18 |
--------------------------------------------------------------------------------
/theme.ts:
--------------------------------------------------------------------------------
1 | export const LightTheme = {
2 | dark: false,
3 | colors: {
4 | primary: 'gray',
5 | secondary: 'gray',
6 | background: '#FFFFFF',
7 | background2: '#F5F5F5',
8 | background3: '#F5F5F5',
9 | text: 'gray',
10 | switchThumb: '#B3B3B3',
11 | switchFalse: '#D4D4D8',
12 | },
13 | };
14 |
15 | export const DarkTheme = {
16 | dark: true,
17 | colors: {
18 | primary: '#E4E4E7',
19 | secondary: '#B3B3B3',
20 | background: '#121212',
21 | background2: '#212121',
22 | background3: '#424242',
23 | text: '#FFFFFF',
24 | switchThumb: '#B3B3B3',
25 | switchFalse: '#D4D4D8',
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": false,
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/types.d.ts:
--------------------------------------------------------------------------------
1 | import { ParamListBase } from '@react-navigation/routers';
2 | import * as MediaLibrary from 'expo-media-library';
3 |
4 | export type ThemeColors = {
5 | dark: boolean;
6 | colors: {
7 | primary: string;
8 | secondary: string;
9 | background: string;
10 | card: string;
11 | text: string;
12 | border: string;
13 | notification: string;
14 | };
15 | };
16 |
17 | export type fileItem = {
18 | name: string;
19 | selected?: boolean;
20 | exists: true;
21 | uri: string;
22 | size: number;
23 | isDirectory: boolean;
24 | modificationTime: number;
25 | md5?: string;
26 | };
27 |
28 | export type ExtendedAsset = {
29 | id: string;
30 | filename: string;
31 | uri: string;
32 | mediaType: MediaLibrary.MediaTypeValue;
33 | mediaSubtypes?: string[] | undefined;
34 | width: number;
35 | height: number;
36 | creationTime: number;
37 | modificationTime: number;
38 | duration: number;
39 | albumId?: string | undefined;
40 | name?: string;
41 | selected?: boolean;
42 | };
43 |
44 | export type customAlbum = {
45 | id?: string;
46 | title?: string;
47 | assetCount?: number;
48 | type?: string | undefined;
49 | coverImage?: string;
50 | };
51 |
52 | export interface imageFile {
53 | id: string;
54 | albumId: string;
55 | filename: string;
56 | uri: string;
57 | duration: number;
58 | width: number;
59 | height: number;
60 | mediaType: string;
61 | selected: boolean;
62 | creationTime: number;
63 | modificationTime: number;
64 | }
65 |
66 | export type imageDimensions = {
67 | width: number;
68 | height: number;
69 | };
70 |
71 | export type fileRequestMessage = {
72 | device: string;
73 | path: string;
74 | basedir: string;
75 | };
76 |
77 | export type newFolderRequest = {
78 | device: string;
79 | name: string;
80 | path: string;
81 | };
82 |
83 | export type newFileTransfer = {
84 | name: string;
85 | file: string;
86 | size: number;
87 | device: string;
88 | path: string;
89 | };
90 |
91 | export type SubNavigator = {
92 | [K in keyof T]: { screen: K; params?: T[K] };
93 | }[keyof T];
94 |
--------------------------------------------------------------------------------
/utils/Constants.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 |
3 | export const { width: SIZE, height: HEIGHT } = Dimensions.get('window');
4 |
5 | export const reExt = new RegExp(/(?:\.([^.]+))?$/);
6 | export const base64reg = /data:image\/[^;]+;base64,/;
7 |
8 | export const fileIcons = {
9 | json: 'code-json',
10 | pdf: 'file-pdf-box',
11 | msword: 'file-word-outline',
12 | 'vnd.openxmlformats-officedocument.wordprocessingml.document':
13 | 'file-word-outline',
14 | 'vnd.ms-excel': 'file-excel-outline',
15 | 'vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'file-excel-outline',
16 | 'vnd.ms-powerpoint': 'file-powerpoint-outline',
17 | 'vnd.openxmlformats-officedocument.presentationml.presentation':
18 | 'file-powerpoint-outline',
19 | zip: 'folder-zip-outline',
20 | 'vnd.rar': 'folder-zip-outline',
21 | 'x-7z-compressed': 'folder-zip-outline',
22 | xml: 'xml',
23 | css: 'language-css3',
24 | csv: 'file-delimited-outline',
25 | html: 'language-html5',
26 | javascript: 'language-javascript',
27 | plain: 'text-box-outline',
28 | };
29 |
30 | export const videoFormats = ['mp4', 'mov'];
31 | export const imageFormats = [
32 | 'jpg',
33 | 'jpeg',
34 | 'png',
35 | 'gif',
36 | 'tiff',
37 | 'tif',
38 | 'heic',
39 | 'bmp',
40 | ];
41 |
42 | export const Poppins_400Regular = 'Poppins_400Regular';
43 | export const Poppins_500Medium = 'Poppins_500Medium';
44 | export const Poppins_600SemiBold = 'Poppins_600SemiBold';
45 | export const Poppins_700Bold = 'Poppins_700Bold';
46 |
--------------------------------------------------------------------------------
/utils/Filesize.ts:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/a/14919494/13565880
2 | export default function humanFileSize(bytes, si = false, dp = 1) {
3 | const thresh = si ? 1000 : 1024;
4 |
5 | if (Math.abs(bytes) < thresh) {
6 | return bytes + " B";
7 | }
8 |
9 | const units = si
10 | ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
11 | : ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
12 | let u = -1;
13 | const r = 10 ** dp;
14 |
15 | do {
16 | bytes /= thresh;
17 | ++u;
18 | } while (
19 | Math.round(Math.abs(bytes) * r) / r >= thresh &&
20 | u < units.length - 1
21 | );
22 |
23 | return bytes.toFixed(dp) + " " + units[u];
24 | }
25 |
--------------------------------------------------------------------------------
/utils/RemoveFileFolder.ts:
--------------------------------------------------------------------------------
1 | export default function removeFileFolder() {}
2 |
--------------------------------------------------------------------------------
/utils/promiseProgress.ts:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/a/42342373/13565880
2 |
3 | export default function allProgress(
4 | proms: Promise[],
5 | progress_cb: (arg0: number) => void
6 | ) {
7 | let d = 0;
8 | progress_cb(0);
9 | for (const p of proms) {
10 | p.then(() => {
11 | d++;
12 | progress_cb((d * 100) / proms.length);
13 | });
14 | }
15 | return Promise.all(proms);
16 | }
17 |
--------------------------------------------------------------------------------