rn-file-toolkit 1.0.1 → 1.0.2

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.
@@ -5,16 +5,6 @@ import { NativeEventEmitter, NativeModules } from 'react-native';
5
5
  import FileToolkitSpec from "./NativeFileToolkit.js";
6
6
  const FileToolkitModule = NativeModules.FileToolkit || FileToolkitSpec;
7
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
8
  class DownloadQueue {
19
9
  _maxConcurrent = 3;
20
10
  _active = 0;
@@ -22,7 +12,6 @@ class DownloadQueue {
22
12
  setOptions(opts) {
23
13
  if (opts.maxConcurrent != null && opts.maxConcurrent > 0) {
24
14
  this._maxConcurrent = opts.maxConcurrent;
25
- // Kick off any slots that just opened.
26
15
  this._flush();
27
16
  }
28
17
  }
@@ -52,7 +41,6 @@ class DownloadQueue {
52
41
  while (this._active < this._maxConcurrent && this._queue.length > 0) {
53
42
  const item = this._queue.shift();
54
43
  this._active++;
55
- // Strip queue-specific fields before passing to the native layer.
56
44
  const nativeOptions = {
57
45
  ...item.options
58
46
  };
@@ -70,55 +58,31 @@ class DownloadQueue {
70
58
  }
71
59
  }
72
60
  const _globalQueue = new DownloadQueue();
73
-
74
- /**
75
- * Configure the global managed download queue.
76
- *
77
- * @example
78
- * ```ts
79
- * setQueueOptions({ maxConcurrent: 3 });
80
- * ```
81
- */
61
+ function _generateId() {
62
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
63
+ const r = Math.random() * 16 | 0;
64
+ const v = c === 'x' ? r : r & 0x3 | 0x8;
65
+ return v.toString(16);
66
+ });
67
+ }
82
68
  export function setQueueOptions(options) {
83
69
  _globalQueue.setOptions(options);
84
70
  }
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
71
  export function getQueueStatus() {
95
72
  return _globalQueue.getStatus();
96
73
  }
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
74
  async function _executeDownload(options) {
105
75
  let progressSubscription = null;
106
76
  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)
77
+ const downloadId = options.downloadId || _generateId();
78
+ const knownDownloadId = downloadId;
114
79
  let _lastProgressTs = null;
115
80
  let _lastProgressBytes = 0;
116
- // Smoothed speed using exponential moving average (α = 0.3)
117
81
  let _smoothedSpeedBps = 0;
118
82
  if (options.onProgress) {
119
83
  progressSubscription = eventEmitter.addListener('onDownloadProgress', event => {
120
- const matchesId = knownDownloadId && event.downloadId === knownDownloadId;
121
- const matchesUrl = !knownDownloadId && event.url === options.url;
84
+ const matchesId = event.downloadId === knownDownloadId;
85
+ const matchesUrl = !event.downloadId && event.url === options.url;
122
86
  if ((matchesId || matchesUrl) && options.onProgress) {
123
87
  const now = Date.now();
124
88
  const percent = event.progress ?? 0;
@@ -130,47 +94,30 @@ async function _executeDownload(options) {
130
94
  const dtSec = (now - _lastProgressTs) / 1000;
131
95
  if (dtSec > 0) {
132
96
  const bytesDelta = bytesDownloaded - _lastProgressBytes;
133
- // Only update speed if we have byte-level data from native
134
97
  if (bytesDelta > 0 && totalBytes > 0) {
135
98
  const instantSpeed = bytesDelta / dtSec;
136
- // Exponential moving average for smoother readings
137
99
  _smoothedSpeedBps = _smoothedSpeedBps === 0 ? instantSpeed : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
138
100
  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
- }
101
+ etaSeconds = speedBps > 0 ? (totalBytes - bytesDownloaded) / speedBps : 0;
154
102
  }
155
103
  }
156
104
  }
157
105
  _lastProgressTs = now;
158
106
  _lastProgressBytes = bytesDownloaded;
159
- const info = {
107
+ options.onProgress({
160
108
  percent,
161
109
  bytesDownloaded,
162
110
  totalBytes,
163
111
  speedBps,
164
112
  etaSeconds
165
- };
166
- options.onProgress(info);
113
+ });
167
114
  }
168
115
  });
169
116
  }
170
117
  if (options.retry?.onRetry) {
171
118
  retrySubscription = eventEmitter.addListener('onDownloadRetry', event => {
172
- const matchesId = knownDownloadId && event.downloadId === knownDownloadId;
173
- const matchesUrl = !knownDownloadId && event.url === options.url;
119
+ const matchesId = event.downloadId === knownDownloadId;
120
+ const matchesUrl = !event.downloadId && event.url === options.url;
174
121
  if ((matchesId || matchesUrl) && options.retry?.onRetry) {
175
122
  options.retry.onRetry(event.attempt, event.error ?? '');
176
123
  }
@@ -181,32 +128,13 @@ async function _executeDownload(options) {
181
128
  retrySubscription?.remove();
182
129
  };
183
130
  try {
184
- const resultPromise = FileToolkitSpec.download({
185
- url: options.url,
186
- fileName: options.fileName,
131
+ const result = await FileToolkitModule.download({
132
+ ...options,
133
+ downloadId,
187
134
  background: options.background ?? false,
188
135
  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
136
+ destination: options.destination ?? 'downloads'
197
137
  });
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
138
  cleanup();
211
139
  return result;
212
140
  } catch (error) {
@@ -217,256 +145,149 @@ async function _executeDownload(options) {
217
145
  };
218
146
  }
219
147
  }
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
148
  export function download(options) {
248
- if (options.queue) {
249
- return _globalQueue.enqueue(options);
250
- }
149
+ if (options.queue) return _globalQueue.enqueue(options);
251
150
  return _executeDownload(options);
252
151
  }
253
-
254
- // ─── Core upload ───────────────────────────────────────────────────────────────
255
-
256
- /**
257
- * Upload a file using multipart/form-data.
258
- */
259
152
  export async function upload(options) {
260
- let progressSubscription = null;
153
+ let sub = null;
154
+ const uploadId = options.uploadId || _generateId();
261
155
  if (options.onProgress) {
262
- progressSubscription = eventEmitter.addListener('onUploadProgress', event => {
263
- if (event.url === options.url && options.onProgress) {
264
- options.onProgress(event.progress);
265
- }
156
+ sub = eventEmitter.addListener('onUploadProgress', e => {
157
+ if (e.uploadId === uploadId) options.onProgress(e.progress);
266
158
  });
267
159
  }
268
160
  try {
269
- const result = await FileToolkitSpec.upload({
270
- url: options.url,
271
- filePath: options.filePath,
161
+ const res = await FileToolkitModule.upload({
162
+ ...options,
163
+ uploadId,
272
164
  fieldName: options.fieldName ?? 'file',
273
165
  headers: options.headers ?? {},
274
166
  parameters: options.parameters ?? {}
275
167
  });
276
- if (progressSubscription) {
277
- progressSubscription.remove();
278
- }
279
- return result;
280
- } catch (error) {
281
- if (progressSubscription) {
282
- progressSubscription.remove();
283
- }
168
+ sub?.remove();
169
+ return {
170
+ ...res,
171
+ uploadId
172
+ };
173
+ } catch (err) {
174
+ sub?.remove();
284
175
  return {
285
176
  success: false,
286
- error: error?.message || 'UNKNOWN_ERROR'
177
+ error: err?.message || 'UNKNOWN_ERROR',
178
+ uploadId
287
179
  };
288
180
  }
289
181
  }
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) {
182
+ export async function pauseDownload(id) {
299
183
  try {
300
- return await FileToolkitSpec.pauseDownload(downloadId);
301
- } catch (error) {
184
+ return await FileToolkitModule.pauseDownload(id);
185
+ } catch (err) {
302
186
  return {
303
187
  success: false,
304
- error: error?.message || 'UNKNOWN_ERROR'
188
+ error: err.message || 'UNKNOWN_ERROR'
305
189
  };
306
190
  }
307
191
  }
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) {
192
+ export async function resumeDownload(id) {
314
193
  try {
315
- return await FileToolkitSpec.resumeDownload(downloadId);
316
- } catch (error) {
194
+ return await FileToolkitModule.resumeDownload(id);
195
+ } catch (err) {
317
196
  return {
318
197
  success: false,
319
- error: error?.message || 'UNKNOWN_ERROR'
198
+ error: err.message || 'UNKNOWN_ERROR'
320
199
  };
321
200
  }
322
201
  }
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) {
202
+ export async function cancelDownload(id) {
329
203
  try {
330
- return await FileToolkitSpec.cancelDownload(downloadId);
331
- } catch (error) {
204
+ return await FileToolkitModule.cancelDownload(id);
205
+ } catch (err) {
332
206
  return {
333
207
  success: false,
334
- error: error?.message || 'UNKNOWN_ERROR'
208
+ error: err.message || 'UNKNOWN_ERROR'
335
209
  };
336
210
  }
337
211
  }
338
-
339
- // ─── Cache management ─────────────────────────────────────────────────────────
340
-
341
- /**
342
- * List all files in the app's cache directory.
343
- */
344
212
  export async function getCachedFiles() {
345
213
  try {
346
- return await FileToolkitSpec.getCachedFiles();
347
- } catch (error) {
214
+ return await FileToolkitModule.getCachedFiles();
215
+ } catch (err) {
348
216
  return {
349
217
  success: false,
350
- error: error?.message || 'UNKNOWN_ERROR'
218
+ error: err.message || 'UNKNOWN_ERROR'
351
219
  };
352
220
  }
353
221
  }
354
-
355
- /**
356
- * Delete a single file by its absolute path.
357
- */
358
- export async function deleteFile(filePath) {
222
+ export async function deleteFile(path) {
359
223
  try {
360
- return await FileToolkitSpec.deleteFile(filePath);
361
- } catch (error) {
224
+ return await FileToolkitModule.deleteFile(path);
225
+ } catch (err) {
362
226
  return {
363
227
  success: false,
364
- error: error?.message || 'UNKNOWN_ERROR'
228
+ error: err.message || 'UNKNOWN_ERROR'
365
229
  };
366
230
  }
367
231
  }
368
-
369
- /**
370
- * Delete all files in the app's cache directory.
371
- */
372
232
  export async function clearCache() {
373
233
  try {
374
- return await FileToolkitSpec.clearCache();
375
- } catch (error) {
234
+ return await FileToolkitModule.clearCache();
235
+ } catch (err) {
376
236
  return {
377
237
  success: false,
378
- error: error?.message || 'UNKNOWN_ERROR'
238
+ error: err.message || 'UNKNOWN_ERROR'
379
239
  };
380
240
  }
381
241
  }
382
-
383
- /**
384
- * Get all active background downloads.
385
- * Use this after app restart to "re-attach" to ongoing downloads.
386
- */
387
242
  export async function getBackgroundDownloads() {
388
243
  try {
389
- return await FileToolkitSpec.getBackgroundDownloads();
390
- } catch (error) {
244
+ return await FileToolkitModule.getBackgroundDownloads();
245
+ } catch (err) {
391
246
  return {
392
247
  success: false,
393
- error: error?.message || 'UNKNOWN_ERROR'
248
+ error: err.message || 'UNKNOWN_ERROR'
394
249
  };
395
250
  }
396
251
  }
397
-
398
- // ─── File system helpers ─────────────────────────────────────────────────────
399
-
400
- function _ensureFsSuccess(result, fallback) {
401
- if (!result?.success) {
402
- throw new Error(result?.error || fallback);
403
- }
252
+ function _ensure(res, msg) {
253
+ if (!res?.success) throw new Error(res?.error || msg);
404
254
  }
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;
255
+ export async function exists(path) {
256
+ const res = await FileToolkitModule.exists(path);
257
+ _ensure(res, 'EXISTS_ERROR');
258
+ return !!res.exists;
411
259
  }
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;
260
+ export async function stat(path) {
261
+ const res = await FileToolkitModule.stat(path);
262
+ _ensure(res, 'STAT_ERROR');
263
+ return res.stat;
418
264
  }
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 ?? '';
265
+ export async function readFile(path, enc = 'utf8') {
266
+ const res = await FileToolkitModule.readFile(path, enc);
267
+ _ensure(res, 'READ_ERROR');
268
+ return res.data ?? '';
425
269
  }
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');
270
+ export async function writeFile(path, data, enc = 'utf8') {
271
+ const res = await FileToolkitModule.writeFile(path, data, enc);
272
+ _ensure(res, 'WRITE_ERROR');
431
273
  }
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');
274
+ export async function copyFile(from, to) {
275
+ const res = await FileToolkitModule.copyFile(from, to);
276
+ _ensure(res, 'COPY_ERROR');
437
277
  }
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');
278
+ export async function moveFile(from, to) {
279
+ const res = await FileToolkitModule.moveFile(from, to);
280
+ _ensure(res, 'MOVE_ERROR');
443
281
  }
444
-
445
- /** Create a directory recursively. */
446
- export async function mkdir(dirPath) {
447
- const result = await FileToolkitSpec.mkdir(dirPath);
448
- _ensureFsSuccess(result, 'MKDIR_ERROR');
282
+ export async function mkdir(path) {
283
+ const res = await FileToolkitModule.mkdir(path);
284
+ _ensure(res, 'MKDIR_ERROR');
449
285
  }
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 || [];
286
+ export async function ls(path) {
287
+ const res = await FileToolkitModule.ls(path);
288
+ _ensure(res, 'LS_ERROR');
289
+ return res.entries || [];
456
290
  }
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
291
  export const fs = {
471
292
  exists,
472
293
  stat,
@@ -474,366 +295,130 @@ export const fs = {
474
295
  writeFile,
475
296
  copyFile,
476
297
  moveFile,
477
- deleteFile: async filePath => {
478
- const result = await deleteFile(filePath);
479
- _ensureFsSuccess(result, 'DELETE_FILE_ERROR');
298
+ deleteFile: async p => {
299
+ _ensure(await deleteFile(p), 'DEL_ERROR');
480
300
  },
481
301
  mkdir,
482
302
  ls
483
303
  };
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();
304
+ export function onDownloadComplete(cb) {
305
+ const s = eventEmitter.addListener('onDownloadComplete', cb);
306
+ return () => s.remove();
494
307
  }
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();
308
+ export function onDownloadError(cb) {
309
+ const s = eventEmitter.addListener('onDownloadError', cb);
310
+ return () => s.remove();
503
311
  }
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();
312
+ export function onUploadProgress(cb) {
313
+ const s = eventEmitter.addListener('onUploadProgress', cb);
314
+ return () => s.remove();
512
315
  }
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();
316
+ export function onDownloadRetry(cb) {
317
+ const s = eventEmitter.addListener('onDownloadRetry', cb);
318
+ return () => s.remove();
521
319
  }
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) {
320
+ export async function saveBase64AsFile(opts) {
547
321
  try {
548
322
  let {
549
323
  base64Data,
550
324
  fileName,
551
325
  destination
552
- } = options;
553
-
554
- // Parse data URI if present (e.g., "data:image/png;base64,iVBORw0...")
326
+ } = opts;
555
327
  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
- };
328
+ const m = base64Data.match(/^data:([^;]+);base64,(.+)$/);
329
+ if (m && m[2]) {
330
+ base64Data = m[2];
331
+ if (!fileName && m[1]) fileName = `file_${Date.now()}.${m[1].split('/')[1] || 'bin'}`;
571
332
  }
572
333
  }
573
- const result = await FileToolkitModule.saveBase64AsFile({
334
+ return await FileToolkitModule.saveBase64AsFile({
574
335
  base64Data,
575
336
  fileName,
576
337
  destination
577
338
  });
578
- return result;
579
- } catch (error) {
339
+ } catch (err) {
580
340
  return {
581
341
  success: false,
582
- error: error?.message || 'UNKNOWN_ERROR'
342
+ error: err.message || 'UNKNOWN_ERROR'
583
343
  };
584
344
  }
585
345
  }
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) {
346
+ export async function urlToBase64(opts) {
609
347
  try {
610
- const result = await FileToolkitModule.urlToBase64({
611
- url: options.url,
612
- headers: options.headers
613
- });
614
- return result;
615
- } catch (error) {
348
+ return await FileToolkitModule.urlToBase64(opts);
349
+ } catch (err) {
616
350
  return {
617
351
  success: false,
618
- error: error?.message || 'UNKNOWN_ERROR'
352
+ error: err.message || 'UNKNOWN_ERROR'
619
353
  };
620
354
  }
621
355
  }
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) {
356
+ export async function shareFile(opts) {
643
357
  try {
644
- const result = await FileToolkitModule.shareFile(options.filePath, {
645
- title: options.title,
646
- subject: options.subject
358
+ return await FileToolkitModule.shareFile(opts.filePath, {
359
+ title: opts.title,
360
+ subject: opts.subject
647
361
  });
648
- return result;
649
- } catch (error) {
362
+ } catch (err) {
650
363
  return {
651
364
  success: false,
652
- error: error?.message || 'UNKNOWN_ERROR'
365
+ error: err.message || 'UNKNOWN_ERROR'
653
366
  };
654
367
  }
655
368
  }
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) {
369
+ export async function openFile(opts) {
677
370
  try {
678
- const result = await FileToolkitModule.openFile(options.filePath, options.mimeType || '');
679
- return result;
680
- } catch (error) {
371
+ return await FileToolkitModule.openFile(opts.filePath, opts.mimeType || '');
372
+ } catch (err) {
681
373
  return {
682
374
  success: false,
683
- error: error?.message || 'UNKNOWN_ERROR'
375
+ error: err.message || 'UNKNOWN_ERROR'
684
376
  };
685
377
  }
686
378
  }
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) {
379
+ export async function unzip(s, d) {
713
380
  try {
714
- const result = await FileToolkitSpec.unzip(sourcePath, destDir);
715
- return result;
716
- } catch (error) {
381
+ return await FileToolkitModule.unzip(s, d);
382
+ } catch {
717
383
  return {
718
384
  success: false,
719
- error: error?.message || 'UNZIP_ERROR'
385
+ error: 'UNZIP_ERROR'
720
386
  };
721
387
  }
722
388
  }
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) {
389
+ export async function zip(s, d) {
754
390
  try {
755
- const result = await FileToolkitSpec.zip(sourcePath, destPath);
756
- return result;
757
- } catch (error) {
391
+ return await FileToolkitModule.zip(s, d);
392
+ } catch {
758
393
  return {
759
394
  success: false,
760
- error: error?.message || 'ZIP_ERROR'
395
+ error: 'ZIP_ERROR'
761
396
  };
762
397
  }
763
398
  }
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
399
  export function useDownload() {
812
400
  const [status, setStatus] = useState('idle');
813
401
  const [progress, setProgress] = useState(null);
814
402
  const [result, setResult] = useState(null);
815
403
  const [downloadId, setDownloadId] = useState(null);
816
404
  const downloadIdRef = useRef(null);
817
- const start = useCallback(async options => {
405
+ const start = useCallback(async opts => {
406
+ const id = opts.downloadId || _generateId();
818
407
  setStatus('downloading');
819
408
  setProgress(null);
820
409
  setResult(null);
821
- setDownloadId(null);
822
- downloadIdRef.current = null;
410
+ setDownloadId(id);
411
+ downloadIdRef.current = id;
823
412
  const res = await download({
824
- ...options,
825
- onProgress: info => {
826
- setProgress(info);
827
- options.onProgress?.(info);
413
+ ...opts,
414
+ downloadId: id,
415
+ onProgress: p => {
416
+ setProgress(p);
417
+ opts.onProgress?.(p);
828
418
  }
829
419
  });
830
- if (res.downloadId) {
831
- downloadIdRef.current = res.downloadId;
832
- setDownloadId(res.downloadId);
833
- }
834
420
  setResult(res);
835
- const isBackgroundPending = !!options.background && !!res.success && !!res.downloadId && !res.filePath;
836
- setStatus(isBackgroundPending ? 'downloading' : res.success ? 'done' : 'error');
421
+ setStatus(!!opts.background && !!res.success && !!res.downloadId && !res.filePath ? 'downloading' : res.success ? 'done' : 'error');
837
422
  return res;
838
423
  }, []);
839
424
  const pause = useCallback(async () => {