@idealyst/files 1.2.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +94 -0
- package/src/components/DropZone.native.tsx +96 -0
- package/src/components/DropZone.styles.tsx +99 -0
- package/src/components/DropZone.web.tsx +178 -0
- package/src/components/FilePickerButton.native.tsx +82 -0
- package/src/components/FilePickerButton.styles.tsx +112 -0
- package/src/components/FilePickerButton.web.tsx +84 -0
- package/src/components/UploadProgress.native.tsx +203 -0
- package/src/components/UploadProgress.styles.tsx +90 -0
- package/src/components/UploadProgress.web.tsx +201 -0
- package/src/components/index.native.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/components/index.web.ts +8 -0
- package/src/constants.ts +336 -0
- package/src/examples/index.ts +181 -0
- package/src/hooks/createUseFilePickerHook.ts +169 -0
- package/src/hooks/createUseFileUploadHook.ts +173 -0
- package/src/hooks/index.native.ts +12 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/index.web.ts +12 -0
- package/src/index.native.ts +142 -0
- package/src/index.ts +139 -0
- package/src/index.web.ts +142 -0
- package/src/permissions/index.native.ts +8 -0
- package/src/permissions/index.ts +8 -0
- package/src/permissions/index.web.ts +8 -0
- package/src/permissions/permissions.native.ts +177 -0
- package/src/permissions/permissions.web.ts +96 -0
- package/src/picker/FilePicker.native.ts +407 -0
- package/src/picker/FilePicker.web.ts +366 -0
- package/src/picker/index.native.ts +2 -0
- package/src/picker/index.ts +2 -0
- package/src/picker/index.web.ts +2 -0
- package/src/types.ts +990 -0
- package/src/uploader/ChunkedUploader.ts +312 -0
- package/src/uploader/FileUploader.native.ts +435 -0
- package/src/uploader/FileUploader.web.ts +350 -0
- package/src/uploader/UploadQueue.ts +519 -0
- package/src/uploader/index.native.ts +4 -0
- package/src/uploader/index.ts +4 -0
- package/src/uploader/index.web.ts +4 -0
- package/src/utils.ts +586 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { PermissionStatus, PermissionResult } from '../types';
|
|
2
|
+
|
|
3
|
+
// Lazy load react-native-image-picker for permission checks
|
|
4
|
+
let ImagePicker: typeof import('react-native-image-picker') | null = null;
|
|
5
|
+
|
|
6
|
+
async function getImagePicker() {
|
|
7
|
+
if (!ImagePicker) {
|
|
8
|
+
ImagePicker = await import('react-native-image-picker');
|
|
9
|
+
}
|
|
10
|
+
return ImagePicker;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Map image picker permission to our PermissionStatus.
|
|
15
|
+
*/
|
|
16
|
+
function mapPermissionStatus(status: string): PermissionStatus {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'granted':
|
|
19
|
+
return 'granted';
|
|
20
|
+
case 'denied':
|
|
21
|
+
return 'denied';
|
|
22
|
+
case 'blocked':
|
|
23
|
+
return 'blocked';
|
|
24
|
+
case 'unavailable':
|
|
25
|
+
return 'unavailable';
|
|
26
|
+
case 'limited':
|
|
27
|
+
return 'granted'; // Limited access is still usable
|
|
28
|
+
default:
|
|
29
|
+
return 'undetermined';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check photo library permission on native.
|
|
35
|
+
*/
|
|
36
|
+
export async function checkPhotoLibraryPermission(): Promise<PermissionStatus> {
|
|
37
|
+
try {
|
|
38
|
+
const imagePicker = await getImagePicker();
|
|
39
|
+
|
|
40
|
+
// Check if the library exists and has the right method
|
|
41
|
+
if (typeof imagePicker.launchImageLibrary !== 'function') {
|
|
42
|
+
return 'unavailable';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// We can't directly check permissions without triggering the picker
|
|
46
|
+
// Return undetermined as we need to request to know
|
|
47
|
+
return 'undetermined';
|
|
48
|
+
} catch {
|
|
49
|
+
return 'unavailable';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Request photo library permission on native.
|
|
55
|
+
*/
|
|
56
|
+
export async function requestPhotoLibraryPermission(): Promise<PermissionStatus> {
|
|
57
|
+
try {
|
|
58
|
+
const imagePicker = await getImagePicker();
|
|
59
|
+
|
|
60
|
+
// Launch the image library to trigger permission request
|
|
61
|
+
// This is a common pattern since react-native-image-picker doesn't expose
|
|
62
|
+
// a direct permission request API
|
|
63
|
+
const result = await imagePicker.launchImageLibrary({
|
|
64
|
+
mediaType: 'mixed',
|
|
65
|
+
selectionLimit: 0,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (result.didCancel) {
|
|
69
|
+
return 'granted'; // User cancelled but permission was granted
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (result.errorCode === 'permission') {
|
|
73
|
+
return 'denied';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return 'granted';
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// If permission was denied, we'll get an error
|
|
79
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
80
|
+
|
|
81
|
+
if (errorMessage.toLowerCase().includes('permission')) {
|
|
82
|
+
return 'denied';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return 'unavailable';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check camera permission on native.
|
|
91
|
+
*/
|
|
92
|
+
export async function checkCameraPermission(): Promise<PermissionStatus> {
|
|
93
|
+
try {
|
|
94
|
+
const imagePicker = await getImagePicker();
|
|
95
|
+
|
|
96
|
+
if (typeof imagePicker.launchCamera !== 'function') {
|
|
97
|
+
return 'unavailable';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 'undetermined';
|
|
101
|
+
} catch {
|
|
102
|
+
return 'unavailable';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Request camera permission on native.
|
|
108
|
+
*/
|
|
109
|
+
export async function requestCameraPermission(): Promise<PermissionStatus> {
|
|
110
|
+
try {
|
|
111
|
+
const imagePicker = await getImagePicker();
|
|
112
|
+
|
|
113
|
+
// Launch camera to trigger permission request
|
|
114
|
+
const result = await imagePicker.launchCamera({
|
|
115
|
+
mediaType: 'photo',
|
|
116
|
+
saveToPhotos: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (result.didCancel) {
|
|
120
|
+
return 'granted'; // User cancelled but permission was granted
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (result.errorCode === 'permission') {
|
|
124
|
+
return 'denied';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (result.errorCode === 'camera_unavailable') {
|
|
128
|
+
return 'unavailable';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return 'granted';
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
134
|
+
|
|
135
|
+
if (errorMessage.toLowerCase().includes('permission')) {
|
|
136
|
+
return 'denied';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return 'unavailable';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check all file-related permissions.
|
|
145
|
+
*/
|
|
146
|
+
export async function checkPermissions(): Promise<PermissionResult> {
|
|
147
|
+
const [photoLibrary, camera] = await Promise.all([
|
|
148
|
+
checkPhotoLibraryPermission(),
|
|
149
|
+
checkCameraPermission(),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
photoLibrary,
|
|
154
|
+
camera,
|
|
155
|
+
canAskAgain: photoLibrary === 'undetermined' || camera === 'undetermined',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Request all file-related permissions.
|
|
161
|
+
*/
|
|
162
|
+
export async function requestPermissions(): Promise<PermissionResult> {
|
|
163
|
+
// Request photo library first (more commonly needed)
|
|
164
|
+
const photoLibrary = await requestPhotoLibraryPermission();
|
|
165
|
+
|
|
166
|
+
// Only request camera if photo library was granted
|
|
167
|
+
let camera: PermissionStatus = 'undetermined';
|
|
168
|
+
if (photoLibrary === 'granted') {
|
|
169
|
+
camera = await requestCameraPermission();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
photoLibrary,
|
|
174
|
+
camera,
|
|
175
|
+
canAskAgain: photoLibrary === 'blocked' || camera === 'blocked' ? false : true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { PermissionStatus, PermissionResult } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check photo library permission (always granted on web).
|
|
5
|
+
*/
|
|
6
|
+
export async function checkPhotoLibraryPermission(): Promise<PermissionStatus> {
|
|
7
|
+
// Web doesn't require permissions for file input
|
|
8
|
+
return 'granted';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Request photo library permission (no-op on web).
|
|
13
|
+
*/
|
|
14
|
+
export async function requestPhotoLibraryPermission(): Promise<PermissionStatus> {
|
|
15
|
+
// Web doesn't require permissions for file input
|
|
16
|
+
return 'granted';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check camera permission for web.
|
|
21
|
+
*/
|
|
22
|
+
export async function checkCameraPermission(): Promise<PermissionStatus> {
|
|
23
|
+
if (!navigator.permissions) {
|
|
24
|
+
// Permissions API not supported, assume granted
|
|
25
|
+
return 'granted';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const result = await navigator.permissions.query({ name: 'camera' as PermissionName });
|
|
30
|
+
|
|
31
|
+
switch (result.state) {
|
|
32
|
+
case 'granted':
|
|
33
|
+
return 'granted';
|
|
34
|
+
case 'denied':
|
|
35
|
+
return 'denied';
|
|
36
|
+
case 'prompt':
|
|
37
|
+
default:
|
|
38
|
+
return 'undetermined';
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Camera permission query not supported in this browser
|
|
42
|
+
return 'undetermined';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Request camera permission for web.
|
|
48
|
+
*/
|
|
49
|
+
export async function requestCameraPermission(): Promise<PermissionStatus> {
|
|
50
|
+
try {
|
|
51
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
52
|
+
// Stop the stream immediately, we just needed to request permission
|
|
53
|
+
stream.getTracks().forEach(track => track.stop());
|
|
54
|
+
return 'granted';
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof DOMException) {
|
|
57
|
+
if (error.name === 'NotAllowedError') {
|
|
58
|
+
return 'denied';
|
|
59
|
+
}
|
|
60
|
+
if (error.name === 'NotFoundError') {
|
|
61
|
+
return 'unavailable';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return 'denied';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check all file-related permissions.
|
|
70
|
+
*/
|
|
71
|
+
export async function checkPermissions(): Promise<PermissionResult> {
|
|
72
|
+
const [photoLibrary, camera] = await Promise.all([
|
|
73
|
+
checkPhotoLibraryPermission(),
|
|
74
|
+
checkCameraPermission(),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
photoLibrary,
|
|
79
|
+
camera,
|
|
80
|
+
canAskAgain: camera === 'undetermined',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Request all file-related permissions.
|
|
86
|
+
*/
|
|
87
|
+
export async function requestPermissions(): Promise<PermissionResult> {
|
|
88
|
+
const photoLibrary = await requestPhotoLibraryPermission();
|
|
89
|
+
const camera = await requestCameraPermission();
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
photoLibrary,
|
|
93
|
+
camera,
|
|
94
|
+
canAskAgain: false, // After requesting, user must change in settings
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IFilePicker,
|
|
3
|
+
FilePickerConfig,
|
|
4
|
+
FilePickerResult,
|
|
5
|
+
FilePickerStatus,
|
|
6
|
+
FilePickerState,
|
|
7
|
+
PickedFile,
|
|
8
|
+
CameraOptions,
|
|
9
|
+
ValidationResult,
|
|
10
|
+
PermissionStatus,
|
|
11
|
+
FileType,
|
|
12
|
+
} from '../types';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_FILE_PICKER_CONFIG,
|
|
15
|
+
INITIAL_FILE_PICKER_STATUS,
|
|
16
|
+
DOCUMENT_PICKER_TYPES,
|
|
17
|
+
} from '../constants';
|
|
18
|
+
import {
|
|
19
|
+
generateId,
|
|
20
|
+
validateFiles as validateFilesUtil,
|
|
21
|
+
createFilePickerError,
|
|
22
|
+
EventEmitter,
|
|
23
|
+
getFileExtension,
|
|
24
|
+
} from '../utils';
|
|
25
|
+
import { checkPermissions, requestPermissions } from '../permissions/permissions.native';
|
|
26
|
+
|
|
27
|
+
// Lazy load native modules
|
|
28
|
+
let DocumentPicker: typeof import('react-native-document-picker') | null = null;
|
|
29
|
+
let ImagePicker: typeof import('react-native-image-picker') | null = null;
|
|
30
|
+
|
|
31
|
+
async function getDocumentPicker() {
|
|
32
|
+
if (!DocumentPicker) {
|
|
33
|
+
DocumentPicker = await import('react-native-document-picker');
|
|
34
|
+
}
|
|
35
|
+
return DocumentPicker;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getImagePicker() {
|
|
39
|
+
if (!ImagePicker) {
|
|
40
|
+
ImagePicker = await import('react-native-image-picker');
|
|
41
|
+
}
|
|
42
|
+
return ImagePicker;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type FilePickerEvents = {
|
|
46
|
+
stateChange: [FilePickerStatus];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Native implementation of IFilePicker using react-native-document-picker and react-native-image-picker.
|
|
51
|
+
*/
|
|
52
|
+
export class NativeFilePicker implements IFilePicker {
|
|
53
|
+
private _status: FilePickerStatus = { ...INITIAL_FILE_PICKER_STATUS };
|
|
54
|
+
private _events = new EventEmitter<FilePickerEvents>();
|
|
55
|
+
private _defaultConfig: FilePickerConfig;
|
|
56
|
+
private _disposed = false;
|
|
57
|
+
|
|
58
|
+
constructor(config?: Partial<FilePickerConfig>) {
|
|
59
|
+
this._defaultConfig = { ...DEFAULT_FILE_PICKER_CONFIG, ...config };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get status(): FilePickerStatus {
|
|
63
|
+
return { ...this._status };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async checkPermission(): Promise<PermissionStatus> {
|
|
67
|
+
const result = await checkPermissions();
|
|
68
|
+
this._status.permission = result.photoLibrary;
|
|
69
|
+
return result.photoLibrary;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async requestPermission(): Promise<PermissionStatus> {
|
|
73
|
+
const result = await requestPermissions();
|
|
74
|
+
this._status.permission = result.photoLibrary;
|
|
75
|
+
return result.photoLibrary;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async pick(config?: Partial<FilePickerConfig>): Promise<FilePickerResult> {
|
|
79
|
+
if (this._disposed) {
|
|
80
|
+
return {
|
|
81
|
+
cancelled: true,
|
|
82
|
+
files: [],
|
|
83
|
+
rejected: [],
|
|
84
|
+
error: createFilePickerError('NOT_SUPPORTED', 'File picker has been disposed'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const finalConfig = { ...this._defaultConfig, ...config };
|
|
89
|
+
this._updateState('picking');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Determine if we should use image picker or document picker
|
|
93
|
+
if (this._isMediaOnly(finalConfig.allowedTypes)) {
|
|
94
|
+
return await this._pickMedia(finalConfig);
|
|
95
|
+
} else {
|
|
96
|
+
return await this._pickDocuments(finalConfig);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this._updateState('error', createFilePickerError('UNKNOWN', 'Failed to pick files', error as Error));
|
|
100
|
+
return {
|
|
101
|
+
cancelled: false,
|
|
102
|
+
files: [],
|
|
103
|
+
rejected: [],
|
|
104
|
+
error: this._status.error,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async captureFromCamera(options?: CameraOptions): Promise<FilePickerResult> {
|
|
110
|
+
if (this._disposed) {
|
|
111
|
+
return {
|
|
112
|
+
cancelled: true,
|
|
113
|
+
files: [],
|
|
114
|
+
rejected: [],
|
|
115
|
+
error: createFilePickerError('NOT_SUPPORTED', 'File picker has been disposed'),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this._updateState('picking');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const imagePicker = await getImagePicker();
|
|
123
|
+
|
|
124
|
+
const result = await imagePicker.launchCamera({
|
|
125
|
+
mediaType: this._mapMediaType(options?.mediaType || 'photo'),
|
|
126
|
+
quality: (options?.quality || 80) / 100,
|
|
127
|
+
durationLimit: options?.maxDuration,
|
|
128
|
+
saveToPhotos: options?.saveToLibrary ?? true,
|
|
129
|
+
cameraType: options?.useFrontCamera ? 'front' : 'back',
|
|
130
|
+
includeBase64: false,
|
|
131
|
+
includeExtra: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (result.didCancel) {
|
|
135
|
+
this._updateState('idle');
|
|
136
|
+
return { cancelled: true, files: [], rejected: [] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result.errorCode) {
|
|
140
|
+
this._updateState('idle');
|
|
141
|
+
return {
|
|
142
|
+
cancelled: false,
|
|
143
|
+
files: [],
|
|
144
|
+
rejected: [],
|
|
145
|
+
error: this._mapImagePickerError(result.errorCode, result.errorMessage),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this._updateState('processing');
|
|
150
|
+
const files = await this._transformImagePickerResponse(result);
|
|
151
|
+
|
|
152
|
+
this._updateState('idle');
|
|
153
|
+
return {
|
|
154
|
+
cancelled: false,
|
|
155
|
+
files,
|
|
156
|
+
rejected: [],
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
this._updateState('error', createFilePickerError('UNKNOWN', 'Failed to capture', error as Error));
|
|
160
|
+
return {
|
|
161
|
+
cancelled: false,
|
|
162
|
+
files: [],
|
|
163
|
+
rejected: [],
|
|
164
|
+
error: this._status.error,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
validateFiles(files: File[] | PickedFile[], config?: Partial<FilePickerConfig>): ValidationResult {
|
|
170
|
+
const finalConfig = { ...this._defaultConfig, ...config };
|
|
171
|
+
return validateFilesUtil(files, finalConfig);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onStateChange(callback: (status: FilePickerStatus) => void): () => void {
|
|
175
|
+
return this._events.on('stateChange', callback);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
dispose(): void {
|
|
179
|
+
this._disposed = true;
|
|
180
|
+
this._events.removeAllListeners();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================
|
|
184
|
+
// PRIVATE METHODS
|
|
185
|
+
// ============================================
|
|
186
|
+
|
|
187
|
+
private _updateState(state: FilePickerState, error?: ReturnType<typeof createFilePickerError>): void {
|
|
188
|
+
this._status = {
|
|
189
|
+
...this._status,
|
|
190
|
+
state,
|
|
191
|
+
error,
|
|
192
|
+
};
|
|
193
|
+
this._events.emit('stateChange', this._status);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private _isMediaOnly(types: FileType[]): boolean {
|
|
197
|
+
const mediaTypes: FileType[] = ['image', 'video'];
|
|
198
|
+
return types.every(t => mediaTypes.includes(t));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async _pickMedia(config: FilePickerConfig): Promise<FilePickerResult> {
|
|
202
|
+
const imagePicker = await getImagePicker();
|
|
203
|
+
|
|
204
|
+
const mediaType = this._getMediaTypeFromConfig(config);
|
|
205
|
+
|
|
206
|
+
const result = await imagePicker.launchImageLibrary({
|
|
207
|
+
mediaType,
|
|
208
|
+
selectionLimit: config.multiple ? (config.maxFiles || 0) : 1,
|
|
209
|
+
quality: (config.imageQuality || 80) / 100,
|
|
210
|
+
includeBase64: false,
|
|
211
|
+
includeExtra: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (result.didCancel) {
|
|
215
|
+
this._updateState('idle');
|
|
216
|
+
return { cancelled: true, files: [], rejected: [] };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (result.errorCode) {
|
|
220
|
+
this._updateState('idle');
|
|
221
|
+
return {
|
|
222
|
+
cancelled: false,
|
|
223
|
+
files: [],
|
|
224
|
+
rejected: [],
|
|
225
|
+
error: this._mapImagePickerError(result.errorCode, result.errorMessage),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this._updateState('processing');
|
|
230
|
+
const files = await this._transformImagePickerResponse(result, config);
|
|
231
|
+
const { accepted, rejected } = validateFilesUtil(files, config);
|
|
232
|
+
|
|
233
|
+
this._updateState('idle');
|
|
234
|
+
return {
|
|
235
|
+
cancelled: false,
|
|
236
|
+
files: accepted,
|
|
237
|
+
rejected,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async _pickDocuments(config: FilePickerConfig): Promise<FilePickerResult> {
|
|
242
|
+
const docPicker = await getDocumentPicker();
|
|
243
|
+
|
|
244
|
+
const types = this._buildDocumentPickerTypes(config);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const results = await docPicker.default.pick({
|
|
248
|
+
type: types,
|
|
249
|
+
allowMultiSelection: config.multiple,
|
|
250
|
+
copyTo: 'cachesDirectory',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
this._updateState('processing');
|
|
254
|
+
const files = await this._transformDocumentPickerResponse(results, config);
|
|
255
|
+
const { accepted, rejected } = validateFilesUtil(files, config);
|
|
256
|
+
|
|
257
|
+
this._updateState('idle');
|
|
258
|
+
return {
|
|
259
|
+
cancelled: false,
|
|
260
|
+
files: accepted,
|
|
261
|
+
rejected,
|
|
262
|
+
};
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const dp = await getDocumentPicker();
|
|
265
|
+
if (dp.default.isCancel(error)) {
|
|
266
|
+
this._updateState('idle');
|
|
267
|
+
return { cancelled: true, files: [], rejected: [] };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private _getMediaTypeFromConfig(config: FilePickerConfig): 'photo' | 'video' | 'mixed' {
|
|
275
|
+
const hasImage = config.allowedTypes.includes('image');
|
|
276
|
+
const hasVideo = config.allowedTypes.includes('video');
|
|
277
|
+
|
|
278
|
+
if (hasImage && hasVideo) return 'mixed';
|
|
279
|
+
if (hasVideo) return 'video';
|
|
280
|
+
return 'photo';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private _mapMediaType(type: string): 'photo' | 'video' | 'mixed' {
|
|
284
|
+
switch (type) {
|
|
285
|
+
case 'video':
|
|
286
|
+
return 'video';
|
|
287
|
+
case 'mixed':
|
|
288
|
+
return 'mixed';
|
|
289
|
+
default:
|
|
290
|
+
return 'photo';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private _buildDocumentPickerTypes(config: FilePickerConfig): string[] {
|
|
295
|
+
const types = new Set<string>();
|
|
296
|
+
|
|
297
|
+
for (const fileType of config.allowedTypes) {
|
|
298
|
+
const docTypes = DOCUMENT_PICKER_TYPES[fileType] || [];
|
|
299
|
+
for (const t of docTypes) {
|
|
300
|
+
types.add(t);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return Array.from(types);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async _transformImagePickerResponse(
|
|
308
|
+
response: import('react-native-image-picker').ImagePickerResponse,
|
|
309
|
+
config?: FilePickerConfig
|
|
310
|
+
): Promise<PickedFile[]> {
|
|
311
|
+
const files: PickedFile[] = [];
|
|
312
|
+
|
|
313
|
+
for (const asset of response.assets || []) {
|
|
314
|
+
if (!asset.uri) continue;
|
|
315
|
+
|
|
316
|
+
const file: PickedFile = {
|
|
317
|
+
id: generateId(),
|
|
318
|
+
name: asset.fileName || `file_${Date.now()}`,
|
|
319
|
+
size: asset.fileSize || 0,
|
|
320
|
+
type: asset.type || 'application/octet-stream',
|
|
321
|
+
uri: asset.uri,
|
|
322
|
+
extension: getFileExtension(asset.fileName || ''),
|
|
323
|
+
lastModified: asset.timestamp ? new Date(asset.timestamp).getTime() : undefined,
|
|
324
|
+
dimensions: asset.width && asset.height ? { width: asset.width, height: asset.height } : undefined,
|
|
325
|
+
duration: asset.duration ? Math.round(asset.duration * 1000) : undefined,
|
|
326
|
+
getArrayBuffer: () => this._readFileAsArrayBuffer(asset.uri!),
|
|
327
|
+
getData: () => this._readFileAsBase64(asset.uri!),
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
files.push(file);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return files;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async _transformDocumentPickerResponse(
|
|
337
|
+
results: import('react-native-document-picker').DocumentPickerResponse[],
|
|
338
|
+
config?: FilePickerConfig
|
|
339
|
+
): Promise<PickedFile[]> {
|
|
340
|
+
const files: PickedFile[] = [];
|
|
341
|
+
|
|
342
|
+
for (const doc of results) {
|
|
343
|
+
const file: PickedFile = {
|
|
344
|
+
id: generateId(),
|
|
345
|
+
name: doc.name || `file_${Date.now()}`,
|
|
346
|
+
size: doc.size || 0,
|
|
347
|
+
type: doc.type || 'application/octet-stream',
|
|
348
|
+
uri: doc.fileCopyUri || doc.uri,
|
|
349
|
+
extension: getFileExtension(doc.name || ''),
|
|
350
|
+
getArrayBuffer: () => this._readFileAsArrayBuffer(doc.fileCopyUri || doc.uri),
|
|
351
|
+
getData: () => this._readFileAsBase64(doc.fileCopyUri || doc.uri),
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
files.push(file);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return files;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private _mapImagePickerError(errorCode: string, errorMessage?: string): ReturnType<typeof createFilePickerError> {
|
|
361
|
+
switch (errorCode) {
|
|
362
|
+
case 'permission':
|
|
363
|
+
return createFilePickerError('PERMISSION_DENIED', errorMessage || 'Permission denied');
|
|
364
|
+
case 'camera_unavailable':
|
|
365
|
+
return createFilePickerError('NOT_SUPPORTED', 'Camera is not available');
|
|
366
|
+
default:
|
|
367
|
+
return createFilePickerError('UNKNOWN', errorMessage || 'Unknown error');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async _readFileAsArrayBuffer(uri: string): Promise<ArrayBuffer> {
|
|
372
|
+
// Use react-native-blob-util for file reading
|
|
373
|
+
try {
|
|
374
|
+
const BlobUtil = await import('react-native-blob-util');
|
|
375
|
+
const base64 = await BlobUtil.default.fs.readFile(uri.replace('file://', ''), 'base64');
|
|
376
|
+
return this._base64ToArrayBuffer(base64);
|
|
377
|
+
} catch {
|
|
378
|
+
// Fallback: return empty buffer if reading fails
|
|
379
|
+
return new ArrayBuffer(0);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private async _readFileAsBase64(uri: string): Promise<string> {
|
|
384
|
+
try {
|
|
385
|
+
const BlobUtil = await import('react-native-blob-util');
|
|
386
|
+
return await BlobUtil.default.fs.readFile(uri.replace('file://', ''), 'base64');
|
|
387
|
+
} catch {
|
|
388
|
+
return '';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private _base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
393
|
+
const binaryString = atob(base64);
|
|
394
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
395
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
396
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
397
|
+
}
|
|
398
|
+
return bytes.buffer;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Create a new NativeFilePicker instance.
|
|
404
|
+
*/
|
|
405
|
+
export function createFilePicker(config?: Partial<FilePickerConfig>): IFilePicker {
|
|
406
|
+
return new NativeFilePicker(config);
|
|
407
|
+
}
|