rn-file-toolkit 1.0.1 → 1.0.3

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/src/index.tsx CHANGED
@@ -5,131 +5,60 @@ import FileToolkitSpec from './NativeFileToolkit';
5
5
  const FileToolkitModule = NativeModules.FileToolkit || FileToolkitSpec;
6
6
  const eventEmitter = new NativeEventEmitter(FileToolkitModule);
7
7
 
8
- // ─── Types ────────────────────────────────────────────────────────────────────
9
-
10
- /**
11
- * Rich progress information emitted during a download.
12
- * Replaces the plain `number` percent from earlier versions.
13
- */
14
8
  export interface ProgressInfo {
15
- /** Download progress as a percentage (0–100) */
16
9
  percent: number;
17
- /** Number of bytes downloaded so far */
18
10
  bytesDownloaded: number;
19
- /** Total file size in bytes (0 if unknown) */
20
11
  totalBytes: number;
21
- /** Current download speed in bytes per second */
22
12
  speedBps: number;
23
- /** Estimated seconds remaining (0 if unknown) */
24
13
  etaSeconds: number;
25
14
  }
26
15
 
27
16
  export interface DownloadOptions {
28
- /** Remote URL to download from */
29
17
  url: string;
30
- /** Optional file name. Auto-detected from URL if not provided */
31
18
  fileName?: string;
32
- /**
33
- * Run as a background download.
34
- * - iOS: uses NSURLSession background configuration
35
- * - Android: uses system DownloadManager
36
- * Background downloads survive app suspension. Listen to `onDownloadComplete`
37
- * and `onDownloadError` events instead of awaiting the promise.
38
- */
39
19
  background?: boolean;
40
- /** Custom request headers (e.g. Authorization) */
41
20
  headers?: Record<string, string>;
42
- /**
43
- * Destination directory for the downloaded file.
44
- * - 'downloads': Public Downloads folder (default)
45
- * - 'cache': App-private cache directory (cleared by OS when space is low)
46
- * - 'documents': App-private documents directory (persisted)
47
- */
48
21
  destination?: 'downloads' | 'cache' | 'documents';
49
- /** Android only: custom title for the system download notification */
50
22
  notificationTitle?: string;
51
- /** Android only: custom description for the system download notification */
52
23
  notificationDescription?: string;
53
- /** Optional checksum verification after download completes */
54
24
  checksum?: {
55
25
  hash: string;
56
26
  algorithm: 'md5' | 'sha1' | 'sha256';
57
27
  };
58
- /** Called with rich progress info during foreground downloads */
59
28
  onProgress?: (info: ProgressInfo) => void;
60
- /**
61
- * Join the managed download queue instead of starting immediately.
62
- * Respects the `maxConcurrent` limit set via `setQueueOptions()`.
63
- * Defaults to `false`.
64
- */
65
29
  queue?: boolean;
66
- /**
67
- * Priority inside the queue.
68
- * - `'high'`: inserted at the front of the pending queue.
69
- * - `'normal'` (default): appended to the back.
70
- * Has no effect when `queue` is `false`.
71
- */
30
+ downloadId?: string;
72
31
  priority?: 'high' | 'normal';
73
- /**
74
- * Auto-retry on network failure with exponential backoff.
75
- * Only retries on network errors (timeouts, connection drops).
76
- * Server errors (4xx/5xx) and checksum mismatches are NOT retried.
77
- *
78
- * @example
79
- * ```ts
80
- * download({
81
- * url: '...',
82
- * retry: {
83
- * attempts: 3,
84
- * delay: 1000, // 1s → 2s → 4s (doubles each time, capped at 30s)
85
- * onRetry: (attempt, error) => console.log(`Retry #${attempt}: ${error}`),
86
- * }
87
- * })
88
- * ```
89
- */
90
32
  retry?: {
91
- /** Maximum number of retry attempts (default: 0 = no retry) */
92
33
  attempts: number;
93
- /** Base delay in ms between retries. Doubles each attempt (default: 1000) */
94
34
  delay?: number;
95
- /** Called just before each retry attempt */
96
35
  onRetry?: (attempt: number, error: string) => void;
97
36
  };
98
37
  }
99
38
 
100
39
  export interface UploadOptions {
101
- /** Remote URL to upload to */
102
40
  url: string;
103
- /** Absolute local path of the file to upload */
104
41
  filePath: string;
105
- /** Multi-part field name for the file (default: 'file') */
106
42
  fieldName?: string;
107
- /** Custom request headers */
108
43
  headers?: Record<string, string>;
109
- /** Additional text parameters for the multi-part request */
110
44
  parameters?: Record<string, string>;
111
- /** Called with progress 0–100 during upload */
112
45
  onProgress?: (percent: number) => void;
46
+ uploadId?: string;
113
47
  }
114
48
 
115
49
  export interface DownloadResult {
116
50
  success: boolean;
117
- /** Local path of the saved file */
118
51
  filePath?: string;
119
- /** Unique ID for this download — use with pause/resume/cancel */
120
52
  downloadId?: string;
121
- /** Error message if success is false */
122
53
  error?: string;
123
54
  }
124
55
 
125
56
  export interface UploadResult {
126
57
  success: boolean;
127
- /** HTTP response status code */
128
58
  status?: number;
129
- /** HTTP response body as string (if any) */
130
59
  data?: string;
131
- /** Error message if success is false */
132
60
  error?: string;
61
+ uploadId?: string;
133
62
  }
134
63
 
135
64
  export interface ActionResult {
@@ -140,9 +69,7 @@ export interface ActionResult {
140
69
  export interface CachedFile {
141
70
  fileName: string;
142
71
  filePath: string;
143
- /** File size in bytes */
144
72
  size: number;
145
- /** Last-modified timestamp in milliseconds */
146
73
  modifiedAt: number;
147
74
  }
148
75
 
@@ -153,91 +80,59 @@ export interface CacheResult {
153
80
  }
154
81
 
155
82
  export interface SaveBase64Options {
156
- /**
157
- * Base64 string or data URI (e.g., "data:image/png;base64,iVBORw0...")
158
- * If a data URI is provided, the base64 portion will be extracted automatically.
159
- */
160
83
  base64Data: string;
161
- /** Optional file name. Auto-generated if not provided */
162
84
  fileName?: string;
163
- /**
164
- * Destination directory for the saved file.
165
- * - 'downloads': Public Downloads folder (default)
166
- * - 'cache': App-private cache directory
167
- * - 'documents': App-private documents directory
168
- */
169
85
  destination?: 'downloads' | 'cache' | 'documents';
170
86
  }
171
87
 
172
88
  export interface SaveBase64Result {
173
89
  success: boolean;
174
- /** Local path of the saved file */
175
90
  filePath?: string;
176
- /** Error message if success is false */
177
91
  error?: string;
178
92
  }
179
93
 
180
94
  export interface UrlToBase64Options {
181
- /** URL of the file to convert to base64 (image, video, gif, etc.) */
182
95
  url: string;
183
- /** Optional custom headers (e.g., Authorization) */
184
96
  headers?: Record<string, string>;
185
97
  }
186
98
 
187
99
  export interface UrlToBase64Result {
188
100
  success: boolean;
189
- /** Base64-encoded string */
190
101
  base64?: string;
191
- /** MIME type detected from response (e.g., 'image/png') */
192
102
  mimeType?: string;
193
- /** Complete data URI (e.g., 'data:image/png;base64,...') */
194
103
  dataUri?: string;
195
- /** Error message if success is false */
196
104
  error?: string;
197
105
  }
198
106
 
199
107
  export interface ShareFileOptions {
200
- /** Absolute path to the file to share */
201
108
  filePath: string;
202
- /** Optional title for the share dialog (Android only) */
203
109
  title?: string;
204
- /** Optional subject for email sharing (Android only) */
205
110
  subject?: string;
206
111
  }
207
112
 
208
113
  export interface OpenFileOptions {
209
- /** Absolute path to the file to open */
210
114
  filePath: string;
211
- /** MIME type of the file (e.g., 'application/pdf', 'image/jpeg'). Auto-detected if not provided. */
212
115
  mimeType?: string;
213
116
  }
214
117
 
215
118
  export interface ShareFileResult {
216
119
  success: boolean;
217
- /** Whether user completed the share action (iOS only) */
218
120
  completed?: boolean;
219
- /** Error message if success is false */
220
121
  error?: string;
221
122
  }
222
123
 
223
124
  export interface OpenFileResult {
224
125
  success: boolean;
225
- /** Error message if success is false */
226
126
  error?: string;
227
127
  }
228
128
 
229
129
  export type FsEncoding = 'utf8' | 'base64';
230
130
 
231
131
  export interface FsStat {
232
- /** Absolute path */
233
132
  path: string;
234
- /** Basename */
235
133
  name: string;
236
- /** Size in bytes (0 for directories) */
237
134
  size: number;
238
- /** Last modified timestamp in milliseconds */
239
135
  modified: number;
240
- /** Whether path is a directory */
241
136
  isDir: boolean;
242
137
  }
243
138
 
@@ -257,22 +152,13 @@ export interface FsApi {
257
152
  ls: (dirPath: string) => Promise<string[]>;
258
153
  }
259
154
 
260
- // ─── Queue ───────────────────────────────────────────────────────────────────
261
-
262
155
  export interface QueueOptions {
263
- /**
264
- * Maximum number of simultaneous downloads when using the managed queue.
265
- * Defaults to `3`.
266
- */
267
156
  maxConcurrent?: number;
268
157
  }
269
158
 
270
159
  export interface QueueStatus {
271
- /** Number of downloads actively running right now */
272
160
  active: number;
273
- /** Number of downloads waiting in the queue */
274
161
  pending: number;
275
- /** Current `maxConcurrent` setting */
276
162
  maxConcurrent: number;
277
163
  }
278
164
 
@@ -290,7 +176,6 @@ class DownloadQueue {
290
176
  setOptions(opts: QueueOptions): void {
291
177
  if (opts.maxConcurrent != null && opts.maxConcurrent > 0) {
292
178
  this._maxConcurrent = opts.maxConcurrent;
293
- // Kick off any slots that just opened.
294
179
  this._flush();
295
180
  }
296
181
  }
@@ -319,7 +204,6 @@ class DownloadQueue {
319
204
  while (this._active < this._maxConcurrent && this._queue.length > 0) {
320
205
  const item = this._queue.shift()!;
321
206
  this._active++;
322
- // Strip queue-specific fields before passing to the native layer.
323
207
  const nativeOptions = { ...item.options };
324
208
  delete nativeOptions.queue;
325
209
  delete nativeOptions.priority;
@@ -340,123 +224,71 @@ class DownloadQueue {
340
224
 
341
225
  const _globalQueue = new DownloadQueue();
342
226
 
343
- /**
344
- * Configure the global managed download queue.
345
- *
346
- * @example
347
- * ```ts
348
- * setQueueOptions({ maxConcurrent: 3 });
349
- * ```
350
- */
227
+ function _generateId(): string {
228
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
229
+ const r = (Math.random() * 16) | 0;
230
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
231
+ return v.toString(16);
232
+ });
233
+ }
234
+
351
235
  export function setQueueOptions(options: QueueOptions): void {
352
236
  _globalQueue.setOptions(options);
353
237
  }
354
238
 
355
- /**
356
- * Get the current state of the managed download queue.
357
- *
358
- * @example
359
- * ```ts
360
- * const { active, pending, maxConcurrent } = getQueueStatus();
361
- * ```
362
- */
363
239
  export function getQueueStatus(): QueueStatus {
364
240
  return _globalQueue.getStatus();
365
241
  }
366
242
 
367
- // ─── Core download ────────────────────────────────────────────────────────────
368
-
369
- /**
370
- * Core native download executor — used internally by `download()` and the queue.
371
- * Never routes through the queue.
372
- */
373
243
  async function _executeDownload(
374
244
  options: DownloadOptions
375
245
  ): Promise<DownloadResult> {
376
- let progressSubscription: ReturnType<typeof eventEmitter.addListener> | null =
377
- null;
378
- let retrySubscription: ReturnType<typeof eventEmitter.addListener> | null =
379
- null;
380
-
381
- // downloadId is resolved immediately on Android foreground, allowing
382
- // ID-based event filtering. On iOS it only arrives on completion, so we
383
- // fall back to URL-based filtering in that case.
384
- let knownDownloadId: string | null = null;
385
-
386
- // Speed / ETA tracking (JS-side, works regardless of native platform)
246
+ let progressSubscription: any = null;
247
+ let retrySubscription: any = null;
248
+ const downloadId = options.downloadId || _generateId();
249
+ const knownDownloadId: string = downloadId;
387
250
  let _lastProgressTs: number | null = null;
388
251
  let _lastProgressBytes: number = 0;
389
- // Smoothed speed using exponential moving average (α = 0.3)
390
252
  let _smoothedSpeedBps: number = 0;
391
253
 
392
254
  if (options.onProgress) {
393
255
  progressSubscription = eventEmitter.addListener(
394
256
  'onDownloadProgress',
395
257
  (event: any) => {
396
- const matchesId =
397
- knownDownloadId && event.downloadId === knownDownloadId;
398
- const matchesUrl = !knownDownloadId && event.url === options.url;
258
+ const matchesId = event.downloadId === knownDownloadId;
259
+ const matchesUrl = !event.downloadId && event.url === options.url;
399
260
  if ((matchesId || matchesUrl) && options.onProgress) {
400
261
  const now = Date.now();
401
- const percent: number = event.progress ?? 0;
402
- const bytesDownloaded: number = event.bytesDownloaded ?? 0;
403
- const totalBytes: number = event.totalBytes ?? 0;
404
-
262
+ const percent = event.progress ?? 0;
263
+ const bytesDownloaded = event.bytesDownloaded ?? 0;
264
+ const totalBytes = event.totalBytes ?? 0;
405
265
  let speedBps = 0;
406
266
  let etaSeconds = 0;
407
-
408
267
  if (_lastProgressTs !== null) {
409
268
  const dtSec = (now - _lastProgressTs) / 1000;
410
269
  if (dtSec > 0) {
411
270
  const bytesDelta = bytesDownloaded - _lastProgressBytes;
412
- // Only update speed if we have byte-level data from native
413
271
  if (bytesDelta > 0 && totalBytes > 0) {
414
272
  const instantSpeed = bytesDelta / dtSec;
415
- // Exponential moving average for smoother readings
416
273
  _smoothedSpeedBps =
417
274
  _smoothedSpeedBps === 0
418
275
  ? instantSpeed
419
276
  : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
420
277
  speedBps = _smoothedSpeedBps;
421
- const remaining = totalBytes - bytesDownloaded;
422
- etaSeconds = speedBps > 0 ? remaining / speedBps : 0;
423
- } else if (percent > 0 && percent < 100) {
424
- // Fallback: estimate from percent when bytes aren't available
425
- const estimatedTotalBytes =
426
- totalBytes > 0
427
- ? totalBytes
428
- : _lastProgressBytes / (percent / 100);
429
- const percentDelta =
430
- percent - (_lastProgressBytes / estimatedTotalBytes) * 100;
431
- if (percentDelta > 0) {
432
- const estimatedByteDelta =
433
- (percentDelta / 100) * estimatedTotalBytes;
434
- const instantSpeed = estimatedByteDelta / dtSec;
435
- _smoothedSpeedBps =
436
- _smoothedSpeedBps === 0
437
- ? instantSpeed
438
- : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
439
- speedBps = _smoothedSpeedBps;
440
- const remainingPct = 100 - percent;
441
- const estimatedRemaining =
442
- (remainingPct / 100) * estimatedTotalBytes;
443
- etaSeconds = speedBps > 0 ? estimatedRemaining / speedBps : 0;
444
- }
278
+ etaSeconds =
279
+ speedBps > 0 ? (totalBytes - bytesDownloaded) / speedBps : 0;
445
280
  }
446
281
  }
447
282
  }
448
-
449
283
  _lastProgressTs = now;
450
284
  _lastProgressBytes = bytesDownloaded;
451
-
452
- const info: ProgressInfo = {
285
+ options.onProgress!({
453
286
  percent,
454
287
  bytesDownloaded,
455
288
  totalBytes,
456
289
  speedBps,
457
290
  etaSeconds,
458
- };
459
- options.onProgress(info);
291
+ });
460
292
  }
461
293
  }
462
294
  );
@@ -466,9 +298,8 @@ async function _executeDownload(
466
298
  retrySubscription = eventEmitter.addListener(
467
299
  'onDownloadRetry',
468
300
  (event: any) => {
469
- const matchesId =
470
- knownDownloadId && event.downloadId === knownDownloadId;
471
- const matchesUrl = !knownDownloadId && event.url === options.url;
301
+ const matchesId = event.downloadId === knownDownloadId;
302
+ const matchesUrl = !event.downloadId && event.url === options.url;
472
303
  if ((matchesId || matchesUrl) && options.retry?.onRetry) {
473
304
  options.retry.onRetry(event.attempt, event.error ?? '');
474
305
  }
@@ -482,35 +313,13 @@ async function _executeDownload(
482
313
  };
483
314
 
484
315
  try {
485
- const resultPromise = (FileToolkitSpec as any).download({
486
- url: options.url,
487
- fileName: options.fileName,
316
+ const result = await (FileToolkitModule as any).download({
317
+ ...options,
318
+ downloadId,
488
319
  background: options.background ?? false,
489
320
  headers: options.headers ?? {},
490
321
  destination: options.destination ?? 'downloads',
491
- notificationTitle: options.notificationTitle,
492
- notificationDescription: options.notificationDescription,
493
- checksum: options.checksum,
494
- retry: options.retry
495
- ? {
496
- attempts: options.retry.attempts,
497
- delay: options.retry.delay ?? 1000,
498
- }
499
- : undefined,
500
- });
501
-
502
- // On Android foreground the promise resolves immediately with downloadId,
503
- // giving us a precise ID to filter events by before download completes.
504
- // On iOS it only resolves on completion, so knownDownloadId stays null
505
- // and URL-based filtering is used as the fallback.
506
- resultPromise.then?.((partial: any) => {
507
- if (partial?.downloadId && !partial?.filePath) {
508
- // Android early-resolve: has downloadId but no filePath yet
509
- knownDownloadId = partial.downloadId;
510
- }
511
322
  });
512
-
513
- const result = await resultPromise;
514
323
  cleanup();
515
324
  return result as DownloadResult;
516
325
  } catch (error: any) {
@@ -519,277 +328,146 @@ async function _executeDownload(
519
328
  }
520
329
  }
521
330
 
522
- /**
523
- * Download a file.
524
- *
525
- * For **foreground** downloads the promise resolves when the file is saved.
526
- * For **background** downloads the promise resolves immediately with a
527
- * `downloadId`; listen to the `onDownloadComplete` / `onDownloadError`
528
- * events for the final result.
529
- *
530
- * Pass `queue: true` to join the managed queue and respect the `maxConcurrent`
531
- * limit configured via `setQueueOptions()`. Use `priority: 'high'` to jump
532
- * ahead of other pending items in the queue.
533
- *
534
- * @example
535
- * ```ts
536
- * // Direct (unqueued) download
537
- * const result = await download({ url: 'https://...' });
538
- *
539
- * // Queued download with high priority
540
- * setQueueOptions({ maxConcurrent: 3 });
541
- * const result = await download({
542
- * url: 'https://...',
543
- * queue: true,
544
- * priority: 'high',
545
- * });
546
- * ```
547
- */
548
331
  export function download(options: DownloadOptions): Promise<DownloadResult> {
549
- if (options.queue) {
550
- return _globalQueue.enqueue(options);
551
- }
332
+ if (options.queue) return _globalQueue.enqueue(options);
552
333
  return _executeDownload(options);
553
334
  }
554
335
 
555
- // ─── Core upload ───────────────────────────────────────────────────────────────
556
-
557
- /**
558
- * Upload a file using multipart/form-data.
559
- */
560
336
  export async function upload(options: UploadOptions): Promise<UploadResult> {
561
- let progressSubscription: ReturnType<typeof eventEmitter.addListener> | null =
562
- null;
563
-
337
+ let sub: any = null;
338
+ const uploadId = options.uploadId || _generateId();
564
339
  if (options.onProgress) {
565
- progressSubscription = eventEmitter.addListener(
566
- 'onUploadProgress',
567
- (event: any) => {
568
- if (event.url === options.url && options.onProgress) {
569
- options.onProgress(event.progress);
570
- }
571
- }
572
- );
340
+ sub = eventEmitter.addListener('onUploadProgress', (e: any) => {
341
+ if (e.uploadId === uploadId) options.onProgress!(e.progress);
342
+ });
573
343
  }
574
-
575
344
  try {
576
- const result = await (FileToolkitSpec as any).upload({
577
- url: options.url,
578
- filePath: options.filePath,
345
+ const res = await (FileToolkitModule as any).upload({
346
+ ...options,
347
+ uploadId,
579
348
  fieldName: options.fieldName ?? 'file',
580
349
  headers: options.headers ?? {},
581
350
  parameters: options.parameters ?? {},
582
351
  });
583
-
584
- if (progressSubscription) {
585
- progressSubscription.remove();
586
- }
587
-
588
- return result as UploadResult;
589
- } catch (error: any) {
590
- if (progressSubscription) {
591
- progressSubscription.remove();
592
- }
593
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
352
+ sub?.remove();
353
+ return { ...res, uploadId } as UploadResult;
354
+ } catch (err: any) {
355
+ sub?.remove();
356
+ return { success: false, error: err?.message || 'UNKNOWN_ERROR', uploadId };
594
357
  }
595
358
  }
596
359
 
597
- // ─── Pause / Resume / Cancel ──────────────────────────────────────────────────
598
-
599
- /**
600
- * Pause an active foreground download.
601
- * On iOS the partial data is stored so it can be resumed.
602
- * On Android the download thread is suspended.
603
- */
604
- export async function pauseDownload(downloadId: string): Promise<ActionResult> {
360
+ export async function pauseDownload(id: string): Promise<ActionResult> {
605
361
  try {
606
- return (await (FileToolkitSpec as any).pauseDownload(
607
- downloadId
608
- )) as ActionResult;
609
- } catch (error: any) {
610
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
362
+ return await (FileToolkitModule as any).pauseDownload(id);
363
+ } catch (err: any) {
364
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
611
365
  }
612
366
  }
613
367
 
614
- /**
615
- * Resume a previously paused download.
616
- * On iOS resumes from partial data (HTTP Range). On Android unblocks the thread.
617
- */
618
- export async function resumeDownload(
619
- downloadId: string
620
- ): Promise<ActionResult> {
368
+ export async function resumeDownload(id: string): Promise<ActionResult> {
621
369
  try {
622
- return (await (FileToolkitSpec as any).resumeDownload(
623
- downloadId
624
- )) as ActionResult;
625
- } catch (error: any) {
626
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
370
+ return await (FileToolkitModule as any).resumeDownload(id);
371
+ } catch (err: any) {
372
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
627
373
  }
628
374
  }
629
375
 
630
- /**
631
- * Cancel and discard a download (foreground or background).
632
- * Any partially-downloaded file is deleted.
633
- */
634
- export async function cancelDownload(
635
- downloadId: string
636
- ): Promise<ActionResult> {
376
+ export async function cancelDownload(id: string): Promise<ActionResult> {
637
377
  try {
638
- return (await (FileToolkitSpec as any).cancelDownload(
639
- downloadId
640
- )) as ActionResult;
641
- } catch (error: any) {
642
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
378
+ return await (FileToolkitModule as any).cancelDownload(id);
379
+ } catch (err: any) {
380
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
643
381
  }
644
382
  }
645
383
 
646
- // ─── Cache management ─────────────────────────────────────────────────────────
647
-
648
- /**
649
- * List all files in the app's cache directory.
650
- */
651
384
  export async function getCachedFiles(): Promise<CacheResult> {
652
385
  try {
653
- return (await (FileToolkitSpec as any).getCachedFiles()) as CacheResult;
654
- } catch (error: any) {
655
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
386
+ return await (FileToolkitModule as any).getCachedFiles();
387
+ } catch (err: any) {
388
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
656
389
  }
657
390
  }
658
391
 
659
- /**
660
- * Delete a single file by its absolute path.
661
- */
662
- export async function deleteFile(filePath: string): Promise<ActionResult> {
392
+ export async function deleteFile(path: string): Promise<ActionResult> {
663
393
  try {
664
- return (await (FileToolkitSpec as any).deleteFile(
665
- filePath
666
- )) as ActionResult;
667
- } catch (error: any) {
668
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
394
+ return await (FileToolkitModule as any).deleteFile(path);
395
+ } catch (err: any) {
396
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
669
397
  }
670
398
  }
671
399
 
672
- /**
673
- * Delete all files in the app's cache directory.
674
- */
675
400
  export async function clearCache(): Promise<ActionResult> {
676
401
  try {
677
- return (await (FileToolkitSpec as any).clearCache()) as ActionResult;
678
- } catch (error: any) {
679
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
402
+ return await (FileToolkitModule as any).clearCache();
403
+ } catch (err: any) {
404
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
680
405
  }
681
406
  }
682
407
 
683
- /**
684
- * Get all active background downloads.
685
- * Use this after app restart to "re-attach" to ongoing downloads.
686
- */
687
- export async function getBackgroundDownloads(): Promise<{
688
- success: boolean;
689
- downloads?: Array<{
690
- downloadId: string;
691
- url: string;
692
- status: number;
693
- progress: number;
694
- }>;
695
- error?: string;
696
- }> {
408
+ export async function getBackgroundDownloads(): Promise<any> {
697
409
  try {
698
- return (await (FileToolkitSpec as any).getBackgroundDownloads()) as any;
699
- } catch (error: any) {
700
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
410
+ return await (FileToolkitModule as any).getBackgroundDownloads();
411
+ } catch (err: any) {
412
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
701
413
  }
702
414
  }
703
415
 
704
- // ─── File system helpers ─────────────────────────────────────────────────────
705
-
706
- function _ensureFsSuccess(result: any, fallback: string): void {
707
- if (!result?.success) {
708
- throw new Error(result?.error || fallback);
709
- }
416
+ function _ensure(res: any, msg: string) {
417
+ if (!res?.success) throw new Error(res?.error || msg);
710
418
  }
711
419
 
712
- /** Check whether a file or directory exists. */
713
- export async function exists(filePath: string): Promise<boolean> {
714
- const result = await (FileToolkitSpec as any).exists(filePath);
715
- _ensureFsSuccess(result, 'EXISTS_ERROR');
716
- return !!result.exists;
420
+ export async function exists(path: string): Promise<boolean> {
421
+ const res = await (FileToolkitModule as any).exists(path);
422
+ _ensure(res, 'EXISTS_ERROR');
423
+ return !!res.exists;
717
424
  }
718
425
 
719
- /** Get file or directory metadata. */
720
- export async function stat(filePath: string): Promise<FsStat> {
721
- const result = await (FileToolkitSpec as any).stat(filePath);
722
- _ensureFsSuccess(result, 'STAT_ERROR');
723
- return result.stat as FsStat;
426
+ export async function stat(path: string): Promise<FsStat> {
427
+ const res = await (FileToolkitModule as any).stat(path);
428
+ _ensure(res, 'STAT_ERROR');
429
+ return res.stat;
724
430
  }
725
431
 
726
- /** Read a file as utf8 (default) or base64 string. */
727
432
  export async function readFile(
728
- filePath: string,
729
- encoding: FsEncoding = 'utf8'
433
+ path: string,
434
+ enc: FsEncoding = 'utf8'
730
435
  ): Promise<string> {
731
- const result = await (FileToolkitSpec as any).readFile(filePath, encoding);
732
- _ensureFsSuccess(result, 'READ_FILE_ERROR');
733
- return result.data ?? '';
436
+ const res = await (FileToolkitModule as any).readFile(path, enc);
437
+ _ensure(res, 'READ_ERROR');
438
+ return res.data ?? '';
734
439
  }
735
440
 
736
- /** Write utf8 (default) or base64 data to a file. */
737
441
  export async function writeFile(
738
- filePath: string,
442
+ path: string,
739
443
  data: string,
740
- encoding: FsEncoding = 'utf8'
444
+ enc: FsEncoding = 'utf8'
741
445
  ): Promise<void> {
742
- const result = await (FileToolkitSpec as any).writeFile(
743
- filePath,
744
- data,
745
- encoding
746
- );
747
- _ensureFsSuccess(result, 'WRITE_FILE_ERROR');
446
+ const res = await (FileToolkitModule as any).writeFile(path, data, enc);
447
+ _ensure(res, 'WRITE_ERROR');
748
448
  }
749
449
 
750
- /** Copy a file from source to destination. */
751
- export async function copyFile(
752
- fromPath: string,
753
- toPath: string
754
- ): Promise<void> {
755
- const result = await (FileToolkitSpec as any).copyFile(fromPath, toPath);
756
- _ensureFsSuccess(result, 'COPY_FILE_ERROR');
450
+ export async function copyFile(from: string, to: string): Promise<void> {
451
+ const res = await (FileToolkitModule as any).copyFile(from, to);
452
+ _ensure(res, 'COPY_ERROR');
757
453
  }
758
454
 
759
- /** Move a file from source to destination. */
760
- export async function moveFile(
761
- fromPath: string,
762
- toPath: string
763
- ): Promise<void> {
764
- const result = await (FileToolkitSpec as any).moveFile(fromPath, toPath);
765
- _ensureFsSuccess(result, 'MOVE_FILE_ERROR');
455
+ export async function moveFile(from: string, to: string): Promise<void> {
456
+ const res = await (FileToolkitModule as any).moveFile(from, to);
457
+ _ensure(res, 'MOVE_ERROR');
766
458
  }
767
459
 
768
- /** Create a directory recursively. */
769
- export async function mkdir(dirPath: string): Promise<void> {
770
- const result = await (FileToolkitSpec as any).mkdir(dirPath);
771
- _ensureFsSuccess(result, 'MKDIR_ERROR');
460
+ export async function mkdir(path: string): Promise<void> {
461
+ const res = await (FileToolkitModule as any).mkdir(path);
462
+ _ensure(res, 'MKDIR_ERROR');
772
463
  }
773
464
 
774
- /** List direct entries (names) in a directory. */
775
- export async function ls(dirPath: string): Promise<string[]> {
776
- const result = await (FileToolkitSpec as any).ls(dirPath);
777
- _ensureFsSuccess(result, 'LS_ERROR');
778
- return (result.entries || []) as string[];
465
+ export async function ls(path: string): Promise<string[]> {
466
+ const res = await (FileToolkitModule as any).ls(path);
467
+ _ensure(res, 'LS_ERROR');
468
+ return res.entries || [];
779
469
  }
780
470
 
781
- /**
782
- * File system API namespace.
783
- *
784
- * @example
785
- * ```ts
786
- * import { fs } from 'rn-file-toolkit';
787
- *
788
- * const ok = await fs.exists('/path/to/file.pdf');
789
- * const meta = await fs.stat('/path/to/file.pdf');
790
- * const text = await fs.readFile('/path/to/file.txt');
791
- * ```
792
- */
793
471
  export const fs: FsApi = {
794
472
  exists,
795
473
  stat,
@@ -797,454 +475,170 @@ export const fs: FsApi = {
797
475
  writeFile,
798
476
  copyFile,
799
477
  moveFile,
800
- deleteFile: async (filePath: string) => {
801
- const result = await deleteFile(filePath);
802
- _ensureFsSuccess(result, 'DELETE_FILE_ERROR');
478
+ deleteFile: async (p) => {
479
+ _ensure(await deleteFile(p), 'DEL_ERROR');
803
480
  },
804
481
  mkdir,
805
482
  ls,
806
483
  };
807
484
 
808
- // ─── Event helpers ────────────────────────────────────────────────────────────
809
-
810
- /**
811
- * Subscribe to background download completion events.
812
- * Returns an unsubscribe function.
813
- */
814
- export function onDownloadComplete(
815
- callback: (result: DownloadResult) => void
816
- ): () => void {
817
- const sub = eventEmitter.addListener('onDownloadComplete', callback as any);
818
- return () => sub.remove();
485
+ export function onDownloadComplete(cb: any) {
486
+ const s = eventEmitter.addListener('onDownloadComplete', cb);
487
+ return () => s.remove();
819
488
  }
820
-
821
- /**
822
- * Subscribe to background download error events.
823
- * Returns an unsubscribe function.
824
- */
825
- export function onDownloadError(
826
- callback: (result: DownloadResult) => void
827
- ): () => void {
828
- const sub = eventEmitter.addListener('onDownloadError', callback as any);
829
- return () => sub.remove();
489
+ export function onDownloadError(cb: any) {
490
+ const s = eventEmitter.addListener('onDownloadError', cb);
491
+ return () => s.remove();
830
492
  }
831
-
832
- /**
833
- * Subscribe to upload progress events.
834
- * Returns an unsubscribe function.
835
- */
836
- export function onUploadProgress(
837
- callback: (result: { url: string; progress: number }) => void
838
- ): () => void {
839
- const sub = eventEmitter.addListener('onUploadProgress', callback as any);
840
- return () => sub.remove();
493
+ export function onUploadProgress(cb: any) {
494
+ const s = eventEmitter.addListener('onUploadProgress', cb);
495
+ return () => s.remove();
841
496
  }
842
-
843
- /**
844
- * Subscribe to download retry events (fired before each retry attempt).
845
- * Returns an unsubscribe function.
846
- */
847
- export function onDownloadRetry(
848
- callback: (result: {
849
- downloadId: string;
850
- url: string;
851
- attempt: number;
852
- }) => void
853
- ): () => void {
854
- const sub = eventEmitter.addListener('onDownloadRetry', callback as any);
855
- return () => sub.remove();
497
+ export function onDownloadRetry(cb: any) {
498
+ const s = eventEmitter.addListener('onDownloadRetry', cb);
499
+ return () => s.remove();
856
500
  }
857
501
 
858
- // ─── saveBase64AsFile ─────────────────────────────────────────────────────────
859
-
860
- /**
861
- * Save a base64 string or data URI as a file.
862
- * Supports data URIs (e.g., "data:image/png;base64,iVBORw0...") and plain base64 strings.
863
- *
864
- * @example
865
- * ```typescript
866
- * // From data URI
867
- * const result = await saveBase64AsFile({
868
- * base64Data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...',
869
- * fileName: 'photo.png',
870
- * destination: 'documents'
871
- * });
872
- *
873
- * // From plain base64
874
- * const result = await saveBase64AsFile({
875
- * base64Data: 'SGVsbG8gV29ybGQ=',
876
- * fileName: 'hello.txt',
877
- * destination: 'cache'
878
- * });
879
- * ```
880
- */
881
502
  export async function saveBase64AsFile(
882
- options: SaveBase64Options
503
+ opts: SaveBase64Options
883
504
  ): Promise<SaveBase64Result> {
884
505
  try {
885
- let { base64Data, fileName, destination } = options;
886
-
887
- // Parse data URI if present (e.g., "data:image/png;base64,iVBORw0...")
506
+ let { base64Data, fileName, destination } = opts;
888
507
  if (base64Data.startsWith('data:')) {
889
- const matches = base64Data.match(/^data:([^;]+);base64,(.+)$/);
890
- if (matches && matches[2]) {
891
- base64Data = matches[2];
892
-
893
- // Auto-detect file extension from MIME type if fileName not provided
894
- if (!fileName && matches[1]) {
895
- const mimeType = matches[1];
896
- const ext = mimeType.split('/')[1] || 'bin';
897
- fileName = `file_${Date.now()}.${ext}`;
898
- }
899
- } else {
900
- return {
901
- success: false,
902
- error:
903
- 'Invalid data URI format. Expected: data:<mimetype>;base64,<data>',
904
- };
508
+ const m = base64Data.match(/^data:([^;]+);base64,(.+)$/);
509
+ if (m && m[2]) {
510
+ base64Data = m[2];
511
+ if (!fileName && m[1])
512
+ fileName = `file_${Date.now()}.${m[1].split('/')[1] || 'bin'}`;
905
513
  }
906
514
  }
907
-
908
- const result = await FileToolkitModule.saveBase64AsFile({
515
+ return await FileToolkitModule.saveBase64AsFile({
909
516
  base64Data,
910
517
  fileName,
911
518
  destination,
912
519
  });
913
-
914
- return result as SaveBase64Result;
915
- } catch (error: any) {
916
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
520
+ } catch (err: any) {
521
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
917
522
  }
918
523
  }
919
524
 
920
- // ─── urlToBase64 ──────────────────────────────────────────────────────────────
921
-
922
- /**
923
- * Convert a web URL (image, video, gif, etc.) to base64 string.
924
- * Downloads the file and returns both the base64 string and data URI format.
925
- *
926
- * @example
927
- * ```typescript
928
- * // Convert image to base64
929
- * const result = await urlToBase64({
930
- * url: 'https://example.com/photo.jpg',
931
- * headers: { Authorization: 'Bearer token' }
932
- * });
933
- *
934
- * if (result.success) {
935
- * console.log('Base64:', result.base64);
936
- * console.log('Data URI:', result.dataUri);
937
- * console.log('MIME Type:', result.mimeType);
938
- * }
939
- * ```
940
- */
941
525
  export async function urlToBase64(
942
- options: UrlToBase64Options
526
+ opts: UrlToBase64Options
943
527
  ): Promise<UrlToBase64Result> {
944
528
  try {
945
- const result = await FileToolkitModule.urlToBase64({
946
- url: options.url,
947
- headers: options.headers,
948
- });
949
-
950
- return result as UrlToBase64Result;
951
- } catch (error: any) {
952
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
529
+ return await FileToolkitModule.urlToBase64(opts);
530
+ } catch (err: any) {
531
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
953
532
  }
954
533
  }
955
534
 
956
- // ─── shareFile ────────────────────────────────────────────────────────────────
957
-
958
- /**
959
- * Share a file with other apps using the native share dialog.
960
- * Opens the system share sheet on iOS and share chooser on Android.
961
- *
962
- * @example
963
- * ```typescript
964
- * const result = await shareFile({
965
- * filePath: '/path/to/document.pdf',
966
- * title: 'Share Document',
967
- * subject: 'Check out this file'
968
- * });
969
- *
970
- * if (result.success) {
971
- * console.log('File shared successfully');
972
- * }
973
- * ```
974
- */
975
535
  export async function shareFile(
976
- options: ShareFileOptions
536
+ opts: ShareFileOptions
977
537
  ): Promise<ShareFileResult> {
978
538
  try {
979
- const result = await FileToolkitModule.shareFile(options.filePath, {
980
- title: options.title,
981
- subject: options.subject,
539
+ return await FileToolkitModule.shareFile(opts.filePath, {
540
+ title: opts.title,
541
+ subject: opts.subject,
982
542
  });
983
-
984
- return result as ShareFileResult;
985
- } catch (error: any) {
986
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
543
+ } catch (err: any) {
544
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
987
545
  }
988
546
  }
989
547
 
990
- // ─── openFile ─────────────────────────────────────────────────────────────────
991
-
992
- /**
993
- * Open a file with the default app or app chooser.
994
- * On iOS, displays a preview or shows apps that can handle the file.
995
- * On Android, opens with the default app or shows app chooser.
996
- *
997
- * @example
998
- * ```typescript
999
- * const result = await openFile({
1000
- * filePath: '/path/to/document.pdf',
1001
- * mimeType: 'application/pdf' // optional, auto-detected if not provided
1002
- * });
1003
- *
1004
- * if (result.success) {
1005
- * console.log('File opened successfully');
1006
- * }
1007
- * ```
1008
- */
1009
- export async function openFile(
1010
- options: OpenFileOptions
1011
- ): Promise<OpenFileResult> {
548
+ export async function openFile(opts: OpenFileOptions): Promise<OpenFileResult> {
1012
549
  try {
1013
- const result = await FileToolkitModule.openFile(
1014
- options.filePath,
1015
- options.mimeType || ''
1016
- );
1017
-
1018
- return result as OpenFileResult;
1019
- } catch (error: any) {
1020
- return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
550
+ return await FileToolkitModule.openFile(opts.filePath, opts.mimeType || '');
551
+ } catch (err: any) {
552
+ return { success: false, error: err.message || 'UNKNOWN_ERROR' };
1021
553
  }
1022
554
  }
1023
555
 
1024
- // ─── Zip / Unzip ─────────────────────────────────────────────────────────────
1025
-
1026
556
  export interface UnzipResult {
1027
557
  success: boolean;
1028
- /** Absolute path of the destination directory */
1029
558
  destDir?: string;
1030
- /** List of absolute paths of all extracted files */
1031
559
  files?: string[];
1032
- /** Error message if success is false */
1033
560
  error?: string;
1034
561
  }
1035
562
 
1036
563
  export interface ZipResult {
1037
564
  success: boolean;
1038
- /** Absolute path of the created zip archive */
1039
565
  zipPath?: string;
1040
- /** Error message if success is false */
1041
566
  error?: string;
1042
567
  }
1043
568
 
1044
- /**
1045
- * Extract a ZIP archive to a destination directory.
1046
- *
1047
- * Uses `java.util.zip` on Android and zlib (system framework) on iOS.
1048
- * No third-party dependency required.
1049
- *
1050
- * @param sourcePath Absolute path to the `.zip` file
1051
- * @param destDir Absolute path to the directory where files will be extracted.
1052
- * The directory is created automatically if it does not exist.
1053
- *
1054
- * @example
1055
- * ```ts
1056
- * const result = await unzip(
1057
- * '/path/to/archive.zip',
1058
- * '/path/to/output-folder'
1059
- * );
1060
- *
1061
- * if (result.success) {
1062
- * console.log('Extracted files:', result.files);
1063
- * }
1064
- * ```
1065
- */
1066
- export async function unzip(
1067
- sourcePath: string,
1068
- destDir: string
1069
- ): Promise<UnzipResult> {
569
+ export async function unzip(s: string, d: string): Promise<UnzipResult> {
1070
570
  try {
1071
- const result = await (FileToolkitSpec as any).unzip(sourcePath, destDir);
1072
- return result as UnzipResult;
1073
- } catch (error: any) {
1074
- return { success: false, error: error?.message || 'UNZIP_ERROR' };
571
+ return await (FileToolkitModule as any).unzip(s, d);
572
+ } catch {
573
+ return { success: false, error: 'UNZIP_ERROR' };
1075
574
  }
1076
575
  }
1077
576
 
1078
- /**
1079
- * Create a ZIP archive from a file or directory.
1080
- *
1081
- * Uses `java.util.zip` on Android and zlib (system framework) on iOS.
1082
- * No third-party dependency required.
1083
- *
1084
- * @param sourcePath Absolute path to the file or directory to compress
1085
- * @param destPath Absolute path for the output `.zip` file.
1086
- * Parent directory is created automatically if needed.
1087
- *
1088
- * @example
1089
- * ```ts
1090
- * // Zip a single file
1091
- * const result = await zip(
1092
- * '/path/to/document.pdf',
1093
- * '/path/to/document.zip'
1094
- * );
1095
- *
1096
- * // Zip an entire directory
1097
- * const result = await zip(
1098
- * '/path/to/my-folder',
1099
- * '/path/to/my-folder.zip'
1100
- * );
1101
- *
1102
- * if (result.success) {
1103
- * console.log('Archive created at:', result.zipPath);
1104
- * }
1105
- * ```
1106
- */
1107
- export async function zip(
1108
- sourcePath: string,
1109
- destPath: string
1110
- ): Promise<ZipResult> {
577
+ export async function zip(s: string, d: string): Promise<ZipResult> {
1111
578
  try {
1112
- const result = await (FileToolkitSpec as any).zip(sourcePath, destPath);
1113
- return result as ZipResult;
1114
- } catch (error: any) {
1115
- return { success: false, error: error?.message || 'ZIP_ERROR' };
579
+ return await (FileToolkitModule as any).zip(s, d);
580
+ } catch {
581
+ return { success: false, error: 'ZIP_ERROR' };
1116
582
  }
1117
583
  }
1118
584
 
1119
- // ─── useDownload hook ─────────────────────────────────────────────────────────
1120
-
1121
- export type DownloadStatus =
1122
- | 'idle'
1123
- | 'downloading'
1124
- | 'paused'
1125
- | 'done'
1126
- | 'error';
1127
-
1128
585
  export interface UseDownloadReturn {
1129
- /** Start a download. Resolves with the final result. */
1130
586
  start: (options: DownloadOptions) => Promise<DownloadResult>;
1131
- /** Pause the current download */
1132
587
  pause: () => Promise<void>;
1133
- /** Resume the current download */
1134
588
  resume: () => Promise<void>;
1135
- /** Cancel the current download */
1136
589
  cancel: () => Promise<void>;
1137
- /** Current status of the download */
1138
- status: DownloadStatus;
1139
- /** Rich progress information (null until first progress event) */
590
+ status: 'idle' | 'downloading' | 'paused' | 'done' | 'error';
1140
591
  progress: ProgressInfo | null;
1141
- /** Final result once download completes or fails */
1142
592
  result: DownloadResult | null;
1143
- /** The active download ID (available after download starts) */
1144
593
  downloadId: string | null;
1145
594
  }
1146
595
 
1147
- /**
1148
- * React hook for managing a single download with built-in state.
1149
- *
1150
- * Tracks status, rich progress (percent, speed, ETA), and the final result.
1151
- * Exposes `pause`, `resume`, and `cancel` controls tied to the active download.
1152
- *
1153
- * @example
1154
- * ```tsx
1155
- * function DownloadButton() {
1156
- * const { start, pause, resume, cancel, status, progress, result } = useDownload();
1157
- *
1158
- * return (
1159
- * <View>
1160
- * <Button
1161
- * title="Download"
1162
- * onPress={() => start({ url: 'https://example.com/file.zip' })}
1163
- * />
1164
- *
1165
- * {status === 'downloading' && progress && (
1166
- * <View>
1167
- * <Text>{progress.percent.toFixed(1)}%</Text>
1168
- * <Text>Speed: {(progress.speedBps / 1024).toFixed(1)} KB/s</Text>
1169
- * <Text>ETA: {progress.etaSeconds.toFixed(0)}s</Text>
1170
- * <ProgressBar value={progress.percent / 100} />
1171
- * <Button title="Pause" onPress={pause} />
1172
- * </View>
1173
- * )}
1174
- *
1175
- * {status === 'paused' && (
1176
- * <Button title="Resume" onPress={resume} />
1177
- * )}
1178
- *
1179
- * {status === 'done' && result?.success && (
1180
- * <Text>Saved to: {result.filePath}</Text>
1181
- * )}
1182
- *
1183
- * {status === 'error' && (
1184
- * <Text>Error: {result?.error}</Text>
1185
- * )}
1186
- * </View>
1187
- * );
1188
- * }
1189
- * ```
1190
- */
1191
596
  export function useDownload(): UseDownloadReturn {
1192
- const [status, setStatus] = useState<DownloadStatus>('idle');
597
+ const [status, setStatus] = useState<any>('idle');
1193
598
  const [progress, setProgress] = useState<ProgressInfo | null>(null);
1194
599
  const [result, setResult] = useState<DownloadResult | null>(null);
1195
600
  const [downloadId, setDownloadId] = useState<string | null>(null);
1196
601
  const downloadIdRef = useRef<string | null>(null);
1197
602
 
1198
- const start = useCallback(
1199
- async (options: DownloadOptions): Promise<DownloadResult> => {
1200
- setStatus('downloading');
1201
- setProgress(null);
1202
- setResult(null);
1203
- setDownloadId(null);
1204
- downloadIdRef.current = null;
1205
-
1206
- const res = await download({
1207
- ...options,
1208
- onProgress: (info: ProgressInfo) => {
1209
- setProgress(info);
1210
- options.onProgress?.(info);
1211
- },
1212
- });
1213
-
1214
- if (res.downloadId) {
1215
- downloadIdRef.current = res.downloadId;
1216
- setDownloadId(res.downloadId);
1217
- }
603
+ const start = useCallback(async (opts: DownloadOptions) => {
604
+ const id = opts.downloadId || _generateId();
605
+ setStatus('downloading');
606
+ setProgress(null);
607
+ setResult(null);
608
+ setDownloadId(id);
609
+ downloadIdRef.current = id;
610
+ const res = await download({
611
+ ...opts,
612
+ downloadId: id,
613
+ onProgress: (p) => {
614
+ setProgress(p);
615
+ opts.onProgress?.(p);
616
+ },
617
+ });
618
+ setResult(res);
619
+ setStatus(
620
+ !!opts.background && !!res.success && !!res.downloadId && !res.filePath
621
+ ? 'downloading'
622
+ : res.success
623
+ ? 'done'
624
+ : 'error'
625
+ );
626
+ return res;
627
+ }, []);
1218
628
 
1219
- setResult(res);
1220
- const isBackgroundPending =
1221
- !!options.background &&
1222
- !!res.success &&
1223
- !!res.downloadId &&
1224
- !res.filePath;
1225
- setStatus(
1226
- isBackgroundPending ? 'downloading' : res.success ? 'done' : 'error'
1227
- );
1228
- return res;
1229
- },
1230
- []
1231
- );
1232
-
1233
- const pause = useCallback(async (): Promise<void> => {
629
+ const pause = useCallback(async () => {
1234
630
  if (downloadIdRef.current) {
1235
631
  await pauseDownload(downloadIdRef.current);
1236
632
  setStatus('paused');
1237
633
  }
1238
634
  }, []);
1239
-
1240
- const resume = useCallback(async (): Promise<void> => {
635
+ const resume = useCallback(async () => {
1241
636
  if (downloadIdRef.current) {
1242
637
  await resumeDownload(downloadIdRef.current);
1243
638
  setStatus('downloading');
1244
639
  }
1245
640
  }, []);
1246
-
1247
- const cancel = useCallback(async (): Promise<void> => {
641
+ const cancel = useCallback(async () => {
1248
642
  if (downloadIdRef.current) {
1249
643
  await cancelDownload(downloadIdRef.current);
1250
644
  setStatus('idle');