rn-file-toolkit 1.0.1

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 (34) hide show
  1. package/FileToolkit.podspec +22 -0
  2. package/LICENSE +20 -0
  3. package/README.md +522 -0
  4. package/android/build.gradle +61 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/filetoolkit/FileToolkitModule.kt +1204 -0
  7. package/android/src/main/java/com/filetoolkit/FileToolkitPackage.kt +31 -0
  8. package/android/src/main/res/xml/file_provider_paths.xml +14 -0
  9. package/app.plugin.js +49 -0
  10. package/ios/FileToolkit.h +6 -0
  11. package/ios/FileToolkit.mm +1468 -0
  12. package/lib/commonjs/NativeFileToolkit.js +9 -0
  13. package/lib/commonjs/NativeFileToolkit.js.map +1 -0
  14. package/lib/commonjs/index.js +941 -0
  15. package/lib/commonjs/index.js.map +1 -0
  16. package/lib/commonjs/package.json +1 -0
  17. package/lib/module/NativeFileToolkit.js +5 -0
  18. package/lib/module/NativeFileToolkit.js.map +1 -0
  19. package/lib/module/index.js +905 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/typescript/commonjs/package.json +1 -0
  23. package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts +29 -0
  24. package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts.map +1 -0
  25. package/lib/typescript/commonjs/src/index.d.ts +635 -0
  26. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  27. package/lib/typescript/module/package.json +1 -0
  28. package/lib/typescript/module/src/NativeFileToolkit.d.ts +29 -0
  29. package/lib/typescript/module/src/NativeFileToolkit.d.ts.map +1 -0
  30. package/lib/typescript/module/src/index.d.ts +635 -0
  31. package/lib/typescript/module/src/index.d.ts.map +1 -0
  32. package/package.json +232 -0
  33. package/src/NativeFileToolkit.ts +29 -0
  34. package/src/index.tsx +1293 -0
@@ -0,0 +1,905 @@
1
+ "use strict";
2
+
3
+ import { useState, useCallback, useRef } from 'react';
4
+ import { NativeEventEmitter, NativeModules } from 'react-native';
5
+ import FileToolkitSpec from "./NativeFileToolkit.js";
6
+ const FileToolkitModule = NativeModules.FileToolkit || FileToolkitSpec;
7
+ const eventEmitter = new NativeEventEmitter(FileToolkitModule);
8
+
9
+ // ─── Types ────────────────────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Rich progress information emitted during a download.
13
+ * Replaces the plain `number` percent from earlier versions.
14
+ */
15
+
16
+ // ─── Queue ───────────────────────────────────────────────────────────────────
17
+
18
+ class DownloadQueue {
19
+ _maxConcurrent = 3;
20
+ _active = 0;
21
+ _queue = [];
22
+ setOptions(opts) {
23
+ if (opts.maxConcurrent != null && opts.maxConcurrent > 0) {
24
+ this._maxConcurrent = opts.maxConcurrent;
25
+ // Kick off any slots that just opened.
26
+ this._flush();
27
+ }
28
+ }
29
+ getStatus() {
30
+ return {
31
+ active: this._active,
32
+ pending: this._queue.length,
33
+ maxConcurrent: this._maxConcurrent
34
+ };
35
+ }
36
+ enqueue(options) {
37
+ return new Promise((resolve, reject) => {
38
+ const item = {
39
+ options,
40
+ resolve,
41
+ reject
42
+ };
43
+ if (options.priority === 'high') {
44
+ this._queue.unshift(item);
45
+ } else {
46
+ this._queue.push(item);
47
+ }
48
+ this._flush();
49
+ });
50
+ }
51
+ _flush() {
52
+ while (this._active < this._maxConcurrent && this._queue.length > 0) {
53
+ const item = this._queue.shift();
54
+ this._active++;
55
+ // Strip queue-specific fields before passing to the native layer.
56
+ const nativeOptions = {
57
+ ...item.options
58
+ };
59
+ delete nativeOptions.queue;
60
+ delete nativeOptions.priority;
61
+ _executeDownload(nativeOptions).then(result => {
62
+ item.resolve(result);
63
+ }).catch(err => {
64
+ item.reject(err);
65
+ }).finally(() => {
66
+ this._active--;
67
+ this._flush();
68
+ });
69
+ }
70
+ }
71
+ }
72
+ const _globalQueue = new DownloadQueue();
73
+
74
+ /**
75
+ * Configure the global managed download queue.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * setQueueOptions({ maxConcurrent: 3 });
80
+ * ```
81
+ */
82
+ export function setQueueOptions(options) {
83
+ _globalQueue.setOptions(options);
84
+ }
85
+
86
+ /**
87
+ * Get the current state of the managed download queue.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const { active, pending, maxConcurrent } = getQueueStatus();
92
+ * ```
93
+ */
94
+ export function getQueueStatus() {
95
+ return _globalQueue.getStatus();
96
+ }
97
+
98
+ // ─── Core download ────────────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Core native download executor — used internally by `download()` and the queue.
102
+ * Never routes through the queue.
103
+ */
104
+ async function _executeDownload(options) {
105
+ let progressSubscription = null;
106
+ let retrySubscription = null;
107
+
108
+ // downloadId is resolved immediately on Android foreground, allowing
109
+ // ID-based event filtering. On iOS it only arrives on completion, so we
110
+ // fall back to URL-based filtering in that case.
111
+ let knownDownloadId = null;
112
+
113
+ // Speed / ETA tracking (JS-side, works regardless of native platform)
114
+ let _lastProgressTs = null;
115
+ let _lastProgressBytes = 0;
116
+ // Smoothed speed using exponential moving average (α = 0.3)
117
+ let _smoothedSpeedBps = 0;
118
+ if (options.onProgress) {
119
+ progressSubscription = eventEmitter.addListener('onDownloadProgress', event => {
120
+ const matchesId = knownDownloadId && event.downloadId === knownDownloadId;
121
+ const matchesUrl = !knownDownloadId && event.url === options.url;
122
+ if ((matchesId || matchesUrl) && options.onProgress) {
123
+ const now = Date.now();
124
+ const percent = event.progress ?? 0;
125
+ const bytesDownloaded = event.bytesDownloaded ?? 0;
126
+ const totalBytes = event.totalBytes ?? 0;
127
+ let speedBps = 0;
128
+ let etaSeconds = 0;
129
+ if (_lastProgressTs !== null) {
130
+ const dtSec = (now - _lastProgressTs) / 1000;
131
+ if (dtSec > 0) {
132
+ const bytesDelta = bytesDownloaded - _lastProgressBytes;
133
+ // Only update speed if we have byte-level data from native
134
+ if (bytesDelta > 0 && totalBytes > 0) {
135
+ const instantSpeed = bytesDelta / dtSec;
136
+ // Exponential moving average for smoother readings
137
+ _smoothedSpeedBps = _smoothedSpeedBps === 0 ? instantSpeed : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
138
+ speedBps = _smoothedSpeedBps;
139
+ const remaining = totalBytes - bytesDownloaded;
140
+ etaSeconds = speedBps > 0 ? remaining / speedBps : 0;
141
+ } else if (percent > 0 && percent < 100) {
142
+ // Fallback: estimate from percent when bytes aren't available
143
+ const estimatedTotalBytes = totalBytes > 0 ? totalBytes : _lastProgressBytes / (percent / 100);
144
+ const percentDelta = percent - _lastProgressBytes / estimatedTotalBytes * 100;
145
+ if (percentDelta > 0) {
146
+ const estimatedByteDelta = percentDelta / 100 * estimatedTotalBytes;
147
+ const instantSpeed = estimatedByteDelta / dtSec;
148
+ _smoothedSpeedBps = _smoothedSpeedBps === 0 ? instantSpeed : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
149
+ speedBps = _smoothedSpeedBps;
150
+ const remainingPct = 100 - percent;
151
+ const estimatedRemaining = remainingPct / 100 * estimatedTotalBytes;
152
+ etaSeconds = speedBps > 0 ? estimatedRemaining / speedBps : 0;
153
+ }
154
+ }
155
+ }
156
+ }
157
+ _lastProgressTs = now;
158
+ _lastProgressBytes = bytesDownloaded;
159
+ const info = {
160
+ percent,
161
+ bytesDownloaded,
162
+ totalBytes,
163
+ speedBps,
164
+ etaSeconds
165
+ };
166
+ options.onProgress(info);
167
+ }
168
+ });
169
+ }
170
+ if (options.retry?.onRetry) {
171
+ retrySubscription = eventEmitter.addListener('onDownloadRetry', event => {
172
+ const matchesId = knownDownloadId && event.downloadId === knownDownloadId;
173
+ const matchesUrl = !knownDownloadId && event.url === options.url;
174
+ if ((matchesId || matchesUrl) && options.retry?.onRetry) {
175
+ options.retry.onRetry(event.attempt, event.error ?? '');
176
+ }
177
+ });
178
+ }
179
+ const cleanup = () => {
180
+ progressSubscription?.remove();
181
+ retrySubscription?.remove();
182
+ };
183
+ try {
184
+ const resultPromise = FileToolkitSpec.download({
185
+ url: options.url,
186
+ fileName: options.fileName,
187
+ background: options.background ?? false,
188
+ headers: options.headers ?? {},
189
+ destination: options.destination ?? 'downloads',
190
+ notificationTitle: options.notificationTitle,
191
+ notificationDescription: options.notificationDescription,
192
+ checksum: options.checksum,
193
+ retry: options.retry ? {
194
+ attempts: options.retry.attempts,
195
+ delay: options.retry.delay ?? 1000
196
+ } : undefined
197
+ });
198
+
199
+ // On Android foreground the promise resolves immediately with downloadId,
200
+ // giving us a precise ID to filter events by before download completes.
201
+ // On iOS it only resolves on completion, so knownDownloadId stays null
202
+ // and URL-based filtering is used as the fallback.
203
+ resultPromise.then?.(partial => {
204
+ if (partial?.downloadId && !partial?.filePath) {
205
+ // Android early-resolve: has downloadId but no filePath yet
206
+ knownDownloadId = partial.downloadId;
207
+ }
208
+ });
209
+ const result = await resultPromise;
210
+ cleanup();
211
+ return result;
212
+ } catch (error) {
213
+ cleanup();
214
+ return {
215
+ success: false,
216
+ error: error?.message || 'UNKNOWN_ERROR'
217
+ };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Download a file.
223
+ *
224
+ * For **foreground** downloads the promise resolves when the file is saved.
225
+ * For **background** downloads the promise resolves immediately with a
226
+ * `downloadId`; listen to the `onDownloadComplete` / `onDownloadError`
227
+ * events for the final result.
228
+ *
229
+ * Pass `queue: true` to join the managed queue and respect the `maxConcurrent`
230
+ * limit configured via `setQueueOptions()`. Use `priority: 'high'` to jump
231
+ * ahead of other pending items in the queue.
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * // Direct (unqueued) download
236
+ * const result = await download({ url: 'https://...' });
237
+ *
238
+ * // Queued download with high priority
239
+ * setQueueOptions({ maxConcurrent: 3 });
240
+ * const result = await download({
241
+ * url: 'https://...',
242
+ * queue: true,
243
+ * priority: 'high',
244
+ * });
245
+ * ```
246
+ */
247
+ export function download(options) {
248
+ if (options.queue) {
249
+ return _globalQueue.enqueue(options);
250
+ }
251
+ return _executeDownload(options);
252
+ }
253
+
254
+ // ─── Core upload ───────────────────────────────────────────────────────────────
255
+
256
+ /**
257
+ * Upload a file using multipart/form-data.
258
+ */
259
+ export async function upload(options) {
260
+ let progressSubscription = null;
261
+ if (options.onProgress) {
262
+ progressSubscription = eventEmitter.addListener('onUploadProgress', event => {
263
+ if (event.url === options.url && options.onProgress) {
264
+ options.onProgress(event.progress);
265
+ }
266
+ });
267
+ }
268
+ try {
269
+ const result = await FileToolkitSpec.upload({
270
+ url: options.url,
271
+ filePath: options.filePath,
272
+ fieldName: options.fieldName ?? 'file',
273
+ headers: options.headers ?? {},
274
+ parameters: options.parameters ?? {}
275
+ });
276
+ if (progressSubscription) {
277
+ progressSubscription.remove();
278
+ }
279
+ return result;
280
+ } catch (error) {
281
+ if (progressSubscription) {
282
+ progressSubscription.remove();
283
+ }
284
+ return {
285
+ success: false,
286
+ error: error?.message || 'UNKNOWN_ERROR'
287
+ };
288
+ }
289
+ }
290
+
291
+ // ─── Pause / Resume / Cancel ──────────────────────────────────────────────────
292
+
293
+ /**
294
+ * Pause an active foreground download.
295
+ * On iOS the partial data is stored so it can be resumed.
296
+ * On Android the download thread is suspended.
297
+ */
298
+ export async function pauseDownload(downloadId) {
299
+ try {
300
+ return await FileToolkitSpec.pauseDownload(downloadId);
301
+ } catch (error) {
302
+ return {
303
+ success: false,
304
+ error: error?.message || 'UNKNOWN_ERROR'
305
+ };
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Resume a previously paused download.
311
+ * On iOS resumes from partial data (HTTP Range). On Android unblocks the thread.
312
+ */
313
+ export async function resumeDownload(downloadId) {
314
+ try {
315
+ return await FileToolkitSpec.resumeDownload(downloadId);
316
+ } catch (error) {
317
+ return {
318
+ success: false,
319
+ error: error?.message || 'UNKNOWN_ERROR'
320
+ };
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Cancel and discard a download (foreground or background).
326
+ * Any partially-downloaded file is deleted.
327
+ */
328
+ export async function cancelDownload(downloadId) {
329
+ try {
330
+ return await FileToolkitSpec.cancelDownload(downloadId);
331
+ } catch (error) {
332
+ return {
333
+ success: false,
334
+ error: error?.message || 'UNKNOWN_ERROR'
335
+ };
336
+ }
337
+ }
338
+
339
+ // ─── Cache management ─────────────────────────────────────────────────────────
340
+
341
+ /**
342
+ * List all files in the app's cache directory.
343
+ */
344
+ export async function getCachedFiles() {
345
+ try {
346
+ return await FileToolkitSpec.getCachedFiles();
347
+ } catch (error) {
348
+ return {
349
+ success: false,
350
+ error: error?.message || 'UNKNOWN_ERROR'
351
+ };
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Delete a single file by its absolute path.
357
+ */
358
+ export async function deleteFile(filePath) {
359
+ try {
360
+ return await FileToolkitSpec.deleteFile(filePath);
361
+ } catch (error) {
362
+ return {
363
+ success: false,
364
+ error: error?.message || 'UNKNOWN_ERROR'
365
+ };
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Delete all files in the app's cache directory.
371
+ */
372
+ export async function clearCache() {
373
+ try {
374
+ return await FileToolkitSpec.clearCache();
375
+ } catch (error) {
376
+ return {
377
+ success: false,
378
+ error: error?.message || 'UNKNOWN_ERROR'
379
+ };
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Get all active background downloads.
385
+ * Use this after app restart to "re-attach" to ongoing downloads.
386
+ */
387
+ export async function getBackgroundDownloads() {
388
+ try {
389
+ return await FileToolkitSpec.getBackgroundDownloads();
390
+ } catch (error) {
391
+ return {
392
+ success: false,
393
+ error: error?.message || 'UNKNOWN_ERROR'
394
+ };
395
+ }
396
+ }
397
+
398
+ // ─── File system helpers ─────────────────────────────────────────────────────
399
+
400
+ function _ensureFsSuccess(result, fallback) {
401
+ if (!result?.success) {
402
+ throw new Error(result?.error || fallback);
403
+ }
404
+ }
405
+
406
+ /** Check whether a file or directory exists. */
407
+ export async function exists(filePath) {
408
+ const result = await FileToolkitSpec.exists(filePath);
409
+ _ensureFsSuccess(result, 'EXISTS_ERROR');
410
+ return !!result.exists;
411
+ }
412
+
413
+ /** Get file or directory metadata. */
414
+ export async function stat(filePath) {
415
+ const result = await FileToolkitSpec.stat(filePath);
416
+ _ensureFsSuccess(result, 'STAT_ERROR');
417
+ return result.stat;
418
+ }
419
+
420
+ /** Read a file as utf8 (default) or base64 string. */
421
+ export async function readFile(filePath, encoding = 'utf8') {
422
+ const result = await FileToolkitSpec.readFile(filePath, encoding);
423
+ _ensureFsSuccess(result, 'READ_FILE_ERROR');
424
+ return result.data ?? '';
425
+ }
426
+
427
+ /** Write utf8 (default) or base64 data to a file. */
428
+ export async function writeFile(filePath, data, encoding = 'utf8') {
429
+ const result = await FileToolkitSpec.writeFile(filePath, data, encoding);
430
+ _ensureFsSuccess(result, 'WRITE_FILE_ERROR');
431
+ }
432
+
433
+ /** Copy a file from source to destination. */
434
+ export async function copyFile(fromPath, toPath) {
435
+ const result = await FileToolkitSpec.copyFile(fromPath, toPath);
436
+ _ensureFsSuccess(result, 'COPY_FILE_ERROR');
437
+ }
438
+
439
+ /** Move a file from source to destination. */
440
+ export async function moveFile(fromPath, toPath) {
441
+ const result = await FileToolkitSpec.moveFile(fromPath, toPath);
442
+ _ensureFsSuccess(result, 'MOVE_FILE_ERROR');
443
+ }
444
+
445
+ /** Create a directory recursively. */
446
+ export async function mkdir(dirPath) {
447
+ const result = await FileToolkitSpec.mkdir(dirPath);
448
+ _ensureFsSuccess(result, 'MKDIR_ERROR');
449
+ }
450
+
451
+ /** List direct entries (names) in a directory. */
452
+ export async function ls(dirPath) {
453
+ const result = await FileToolkitSpec.ls(dirPath);
454
+ _ensureFsSuccess(result, 'LS_ERROR');
455
+ return result.entries || [];
456
+ }
457
+
458
+ /**
459
+ * File system API namespace.
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * import { fs } from 'rn-file-toolkit';
464
+ *
465
+ * const ok = await fs.exists('/path/to/file.pdf');
466
+ * const meta = await fs.stat('/path/to/file.pdf');
467
+ * const text = await fs.readFile('/path/to/file.txt');
468
+ * ```
469
+ */
470
+ export const fs = {
471
+ exists,
472
+ stat,
473
+ readFile,
474
+ writeFile,
475
+ copyFile,
476
+ moveFile,
477
+ deleteFile: async filePath => {
478
+ const result = await deleteFile(filePath);
479
+ _ensureFsSuccess(result, 'DELETE_FILE_ERROR');
480
+ },
481
+ mkdir,
482
+ ls
483
+ };
484
+
485
+ // ─── Event helpers ────────────────────────────────────────────────────────────
486
+
487
+ /**
488
+ * Subscribe to background download completion events.
489
+ * Returns an unsubscribe function.
490
+ */
491
+ export function onDownloadComplete(callback) {
492
+ const sub = eventEmitter.addListener('onDownloadComplete', callback);
493
+ return () => sub.remove();
494
+ }
495
+
496
+ /**
497
+ * Subscribe to background download error events.
498
+ * Returns an unsubscribe function.
499
+ */
500
+ export function onDownloadError(callback) {
501
+ const sub = eventEmitter.addListener('onDownloadError', callback);
502
+ return () => sub.remove();
503
+ }
504
+
505
+ /**
506
+ * Subscribe to upload progress events.
507
+ * Returns an unsubscribe function.
508
+ */
509
+ export function onUploadProgress(callback) {
510
+ const sub = eventEmitter.addListener('onUploadProgress', callback);
511
+ return () => sub.remove();
512
+ }
513
+
514
+ /**
515
+ * Subscribe to download retry events (fired before each retry attempt).
516
+ * Returns an unsubscribe function.
517
+ */
518
+ export function onDownloadRetry(callback) {
519
+ const sub = eventEmitter.addListener('onDownloadRetry', callback);
520
+ return () => sub.remove();
521
+ }
522
+
523
+ // ─── saveBase64AsFile ─────────────────────────────────────────────────────────
524
+
525
+ /**
526
+ * Save a base64 string or data URI as a file.
527
+ * Supports data URIs (e.g., "data:image/png;base64,iVBORw0...") and plain base64 strings.
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * // From data URI
532
+ * const result = await saveBase64AsFile({
533
+ * base64Data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...',
534
+ * fileName: 'photo.png',
535
+ * destination: 'documents'
536
+ * });
537
+ *
538
+ * // From plain base64
539
+ * const result = await saveBase64AsFile({
540
+ * base64Data: 'SGVsbG8gV29ybGQ=',
541
+ * fileName: 'hello.txt',
542
+ * destination: 'cache'
543
+ * });
544
+ * ```
545
+ */
546
+ export async function saveBase64AsFile(options) {
547
+ try {
548
+ let {
549
+ base64Data,
550
+ fileName,
551
+ destination
552
+ } = options;
553
+
554
+ // Parse data URI if present (e.g., "data:image/png;base64,iVBORw0...")
555
+ if (base64Data.startsWith('data:')) {
556
+ const matches = base64Data.match(/^data:([^;]+);base64,(.+)$/);
557
+ if (matches && matches[2]) {
558
+ base64Data = matches[2];
559
+
560
+ // Auto-detect file extension from MIME type if fileName not provided
561
+ if (!fileName && matches[1]) {
562
+ const mimeType = matches[1];
563
+ const ext = mimeType.split('/')[1] || 'bin';
564
+ fileName = `file_${Date.now()}.${ext}`;
565
+ }
566
+ } else {
567
+ return {
568
+ success: false,
569
+ error: 'Invalid data URI format. Expected: data:<mimetype>;base64,<data>'
570
+ };
571
+ }
572
+ }
573
+ const result = await FileToolkitModule.saveBase64AsFile({
574
+ base64Data,
575
+ fileName,
576
+ destination
577
+ });
578
+ return result;
579
+ } catch (error) {
580
+ return {
581
+ success: false,
582
+ error: error?.message || 'UNKNOWN_ERROR'
583
+ };
584
+ }
585
+ }
586
+
587
+ // ─── urlToBase64 ──────────────────────────────────────────────────────────────
588
+
589
+ /**
590
+ * Convert a web URL (image, video, gif, etc.) to base64 string.
591
+ * Downloads the file and returns both the base64 string and data URI format.
592
+ *
593
+ * @example
594
+ * ```typescript
595
+ * // Convert image to base64
596
+ * const result = await urlToBase64({
597
+ * url: 'https://example.com/photo.jpg',
598
+ * headers: { Authorization: 'Bearer token' }
599
+ * });
600
+ *
601
+ * if (result.success) {
602
+ * console.log('Base64:', result.base64);
603
+ * console.log('Data URI:', result.dataUri);
604
+ * console.log('MIME Type:', result.mimeType);
605
+ * }
606
+ * ```
607
+ */
608
+ export async function urlToBase64(options) {
609
+ try {
610
+ const result = await FileToolkitModule.urlToBase64({
611
+ url: options.url,
612
+ headers: options.headers
613
+ });
614
+ return result;
615
+ } catch (error) {
616
+ return {
617
+ success: false,
618
+ error: error?.message || 'UNKNOWN_ERROR'
619
+ };
620
+ }
621
+ }
622
+
623
+ // ─── shareFile ────────────────────────────────────────────────────────────────
624
+
625
+ /**
626
+ * Share a file with other apps using the native share dialog.
627
+ * Opens the system share sheet on iOS and share chooser on Android.
628
+ *
629
+ * @example
630
+ * ```typescript
631
+ * const result = await shareFile({
632
+ * filePath: '/path/to/document.pdf',
633
+ * title: 'Share Document',
634
+ * subject: 'Check out this file'
635
+ * });
636
+ *
637
+ * if (result.success) {
638
+ * console.log('File shared successfully');
639
+ * }
640
+ * ```
641
+ */
642
+ export async function shareFile(options) {
643
+ try {
644
+ const result = await FileToolkitModule.shareFile(options.filePath, {
645
+ title: options.title,
646
+ subject: options.subject
647
+ });
648
+ return result;
649
+ } catch (error) {
650
+ return {
651
+ success: false,
652
+ error: error?.message || 'UNKNOWN_ERROR'
653
+ };
654
+ }
655
+ }
656
+
657
+ // ─── openFile ─────────────────────────────────────────────────────────────────
658
+
659
+ /**
660
+ * Open a file with the default app or app chooser.
661
+ * On iOS, displays a preview or shows apps that can handle the file.
662
+ * On Android, opens with the default app or shows app chooser.
663
+ *
664
+ * @example
665
+ * ```typescript
666
+ * const result = await openFile({
667
+ * filePath: '/path/to/document.pdf',
668
+ * mimeType: 'application/pdf' // optional, auto-detected if not provided
669
+ * });
670
+ *
671
+ * if (result.success) {
672
+ * console.log('File opened successfully');
673
+ * }
674
+ * ```
675
+ */
676
+ export async function openFile(options) {
677
+ try {
678
+ const result = await FileToolkitModule.openFile(options.filePath, options.mimeType || '');
679
+ return result;
680
+ } catch (error) {
681
+ return {
682
+ success: false,
683
+ error: error?.message || 'UNKNOWN_ERROR'
684
+ };
685
+ }
686
+ }
687
+
688
+ // ─── Zip / Unzip ─────────────────────────────────────────────────────────────
689
+
690
+ /**
691
+ * Extract a ZIP archive to a destination directory.
692
+ *
693
+ * Uses `java.util.zip` on Android and zlib (system framework) on iOS.
694
+ * No third-party dependency required.
695
+ *
696
+ * @param sourcePath Absolute path to the `.zip` file
697
+ * @param destDir Absolute path to the directory where files will be extracted.
698
+ * The directory is created automatically if it does not exist.
699
+ *
700
+ * @example
701
+ * ```ts
702
+ * const result = await unzip(
703
+ * '/path/to/archive.zip',
704
+ * '/path/to/output-folder'
705
+ * );
706
+ *
707
+ * if (result.success) {
708
+ * console.log('Extracted files:', result.files);
709
+ * }
710
+ * ```
711
+ */
712
+ export async function unzip(sourcePath, destDir) {
713
+ try {
714
+ const result = await FileToolkitSpec.unzip(sourcePath, destDir);
715
+ return result;
716
+ } catch (error) {
717
+ return {
718
+ success: false,
719
+ error: error?.message || 'UNZIP_ERROR'
720
+ };
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Create a ZIP archive from a file or directory.
726
+ *
727
+ * Uses `java.util.zip` on Android and zlib (system framework) on iOS.
728
+ * No third-party dependency required.
729
+ *
730
+ * @param sourcePath Absolute path to the file or directory to compress
731
+ * @param destPath Absolute path for the output `.zip` file.
732
+ * Parent directory is created automatically if needed.
733
+ *
734
+ * @example
735
+ * ```ts
736
+ * // Zip a single file
737
+ * const result = await zip(
738
+ * '/path/to/document.pdf',
739
+ * '/path/to/document.zip'
740
+ * );
741
+ *
742
+ * // Zip an entire directory
743
+ * const result = await zip(
744
+ * '/path/to/my-folder',
745
+ * '/path/to/my-folder.zip'
746
+ * );
747
+ *
748
+ * if (result.success) {
749
+ * console.log('Archive created at:', result.zipPath);
750
+ * }
751
+ * ```
752
+ */
753
+ export async function zip(sourcePath, destPath) {
754
+ try {
755
+ const result = await FileToolkitSpec.zip(sourcePath, destPath);
756
+ return result;
757
+ } catch (error) {
758
+ return {
759
+ success: false,
760
+ error: error?.message || 'ZIP_ERROR'
761
+ };
762
+ }
763
+ }
764
+
765
+ // ─── useDownload hook ─────────────────────────────────────────────────────────
766
+
767
+ /**
768
+ * React hook for managing a single download with built-in state.
769
+ *
770
+ * Tracks status, rich progress (percent, speed, ETA), and the final result.
771
+ * Exposes `pause`, `resume`, and `cancel` controls tied to the active download.
772
+ *
773
+ * @example
774
+ * ```tsx
775
+ * function DownloadButton() {
776
+ * const { start, pause, resume, cancel, status, progress, result } = useDownload();
777
+ *
778
+ * return (
779
+ * <View>
780
+ * <Button
781
+ * title="Download"
782
+ * onPress={() => start({ url: 'https://example.com/file.zip' })}
783
+ * />
784
+ *
785
+ * {status === 'downloading' && progress && (
786
+ * <View>
787
+ * <Text>{progress.percent.toFixed(1)}%</Text>
788
+ * <Text>Speed: {(progress.speedBps / 1024).toFixed(1)} KB/s</Text>
789
+ * <Text>ETA: {progress.etaSeconds.toFixed(0)}s</Text>
790
+ * <ProgressBar value={progress.percent / 100} />
791
+ * <Button title="Pause" onPress={pause} />
792
+ * </View>
793
+ * )}
794
+ *
795
+ * {status === 'paused' && (
796
+ * <Button title="Resume" onPress={resume} />
797
+ * )}
798
+ *
799
+ * {status === 'done' && result?.success && (
800
+ * <Text>Saved to: {result.filePath}</Text>
801
+ * )}
802
+ *
803
+ * {status === 'error' && (
804
+ * <Text>Error: {result?.error}</Text>
805
+ * )}
806
+ * </View>
807
+ * );
808
+ * }
809
+ * ```
810
+ */
811
+ export function useDownload() {
812
+ const [status, setStatus] = useState('idle');
813
+ const [progress, setProgress] = useState(null);
814
+ const [result, setResult] = useState(null);
815
+ const [downloadId, setDownloadId] = useState(null);
816
+ const downloadIdRef = useRef(null);
817
+ const start = useCallback(async options => {
818
+ setStatus('downloading');
819
+ setProgress(null);
820
+ setResult(null);
821
+ setDownloadId(null);
822
+ downloadIdRef.current = null;
823
+ const res = await download({
824
+ ...options,
825
+ onProgress: info => {
826
+ setProgress(info);
827
+ options.onProgress?.(info);
828
+ }
829
+ });
830
+ if (res.downloadId) {
831
+ downloadIdRef.current = res.downloadId;
832
+ setDownloadId(res.downloadId);
833
+ }
834
+ setResult(res);
835
+ const isBackgroundPending = !!options.background && !!res.success && !!res.downloadId && !res.filePath;
836
+ setStatus(isBackgroundPending ? 'downloading' : res.success ? 'done' : 'error');
837
+ return res;
838
+ }, []);
839
+ const pause = useCallback(async () => {
840
+ if (downloadIdRef.current) {
841
+ await pauseDownload(downloadIdRef.current);
842
+ setStatus('paused');
843
+ }
844
+ }, []);
845
+ const resume = useCallback(async () => {
846
+ if (downloadIdRef.current) {
847
+ await resumeDownload(downloadIdRef.current);
848
+ setStatus('downloading');
849
+ }
850
+ }, []);
851
+ const cancel = useCallback(async () => {
852
+ if (downloadIdRef.current) {
853
+ await cancelDownload(downloadIdRef.current);
854
+ setStatus('idle');
855
+ setProgress(null);
856
+ setResult(null);
857
+ downloadIdRef.current = null;
858
+ setDownloadId(null);
859
+ }
860
+ }, []);
861
+ return {
862
+ start,
863
+ pause,
864
+ resume,
865
+ cancel,
866
+ status,
867
+ progress,
868
+ result,
869
+ downloadId
870
+ };
871
+ }
872
+ export default {
873
+ download,
874
+ upload,
875
+ pauseDownload,
876
+ resumeDownload,
877
+ cancelDownload,
878
+ getCachedFiles,
879
+ deleteFile,
880
+ clearCache,
881
+ getBackgroundDownloads,
882
+ saveBase64AsFile,
883
+ urlToBase64,
884
+ shareFile,
885
+ openFile,
886
+ onDownloadComplete,
887
+ onDownloadError,
888
+ onUploadProgress,
889
+ onDownloadRetry,
890
+ setQueueOptions,
891
+ getQueueStatus,
892
+ exists,
893
+ stat,
894
+ readFile,
895
+ writeFile,
896
+ copyFile,
897
+ moveFile,
898
+ mkdir,
899
+ ls,
900
+ fs,
901
+ useDownload,
902
+ unzip,
903
+ zip
904
+ };
905
+ //# sourceMappingURL=index.js.map