@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.
Files changed (42) hide show
  1. package/package.json +94 -0
  2. package/src/components/DropZone.native.tsx +96 -0
  3. package/src/components/DropZone.styles.tsx +99 -0
  4. package/src/components/DropZone.web.tsx +178 -0
  5. package/src/components/FilePickerButton.native.tsx +82 -0
  6. package/src/components/FilePickerButton.styles.tsx +112 -0
  7. package/src/components/FilePickerButton.web.tsx +84 -0
  8. package/src/components/UploadProgress.native.tsx +203 -0
  9. package/src/components/UploadProgress.styles.tsx +90 -0
  10. package/src/components/UploadProgress.web.tsx +201 -0
  11. package/src/components/index.native.ts +8 -0
  12. package/src/components/index.ts +6 -0
  13. package/src/components/index.web.ts +8 -0
  14. package/src/constants.ts +336 -0
  15. package/src/examples/index.ts +181 -0
  16. package/src/hooks/createUseFilePickerHook.ts +169 -0
  17. package/src/hooks/createUseFileUploadHook.ts +173 -0
  18. package/src/hooks/index.native.ts +12 -0
  19. package/src/hooks/index.ts +12 -0
  20. package/src/hooks/index.web.ts +12 -0
  21. package/src/index.native.ts +142 -0
  22. package/src/index.ts +139 -0
  23. package/src/index.web.ts +142 -0
  24. package/src/permissions/index.native.ts +8 -0
  25. package/src/permissions/index.ts +8 -0
  26. package/src/permissions/index.web.ts +8 -0
  27. package/src/permissions/permissions.native.ts +177 -0
  28. package/src/permissions/permissions.web.ts +96 -0
  29. package/src/picker/FilePicker.native.ts +407 -0
  30. package/src/picker/FilePicker.web.ts +366 -0
  31. package/src/picker/index.native.ts +2 -0
  32. package/src/picker/index.ts +2 -0
  33. package/src/picker/index.web.ts +2 -0
  34. package/src/types.ts +990 -0
  35. package/src/uploader/ChunkedUploader.ts +312 -0
  36. package/src/uploader/FileUploader.native.ts +435 -0
  37. package/src/uploader/FileUploader.web.ts +350 -0
  38. package/src/uploader/UploadQueue.ts +519 -0
  39. package/src/uploader/index.native.ts +4 -0
  40. package/src/uploader/index.ts +4 -0
  41. package/src/uploader/index.web.ts +4 -0
  42. 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
+ }