@uploadista/react-native-core 0.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/.turbo/turbo-check.log +396 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/package.json +42 -0
- package/src/client/create-uploadista-client.ts +65 -0
- package/src/client/index.ts +4 -0
- package/src/components/CameraUploadButton.tsx +130 -0
- package/src/components/FileUploadButton.tsx +130 -0
- package/src/components/GalleryUploadButton.tsx +199 -0
- package/src/components/UploadList.tsx +214 -0
- package/src/components/UploadProgress.tsx +196 -0
- package/src/components/index.ts +19 -0
- package/src/hooks/index.ts +29 -0
- package/src/hooks/uploadista-context.ts +17 -0
- package/src/hooks/use-camera-upload.ts +38 -0
- package/src/hooks/use-file-upload.ts +40 -0
- package/src/hooks/use-flow-upload.ts +242 -0
- package/src/hooks/use-gallery-upload.ts +65 -0
- package/src/hooks/use-multi-upload.ts +363 -0
- package/src/hooks/use-upload-metrics.ts +82 -0
- package/src/hooks/use-upload.ts +378 -0
- package/src/hooks/use-uploadista-client.ts +23 -0
- package/src/hooks/use-uploadista-context.ts +20 -0
- package/src/index.ts +111 -0
- package/src/types/index.ts +2 -0
- package/src/types/types.ts +359 -0
- package/src/types/upload-input.ts +1 -0
- package/src/utils/fileHelpers.ts +201 -0
- package/src/utils/index.ts +36 -0
- package/src/utils/permissions.ts +177 -0
- package/src/utils/uriHelpers.ts +148 -0
- package/test-compile.ts +5 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
import type { FilePickResult, UseMultiUploadOptions } from "../types";
|
|
4
|
+
import { useUploadistaContext } from "./use-uploadista-context";
|
|
5
|
+
|
|
6
|
+
export interface UploadItemState {
|
|
7
|
+
id: string;
|
|
8
|
+
file: FilePickResult;
|
|
9
|
+
status: "idle" | "uploading" | "success" | "error" | "aborted";
|
|
10
|
+
progress: number;
|
|
11
|
+
bytesUploaded: number;
|
|
12
|
+
totalBytes: number;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
result: UploadFile | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MultiUploadState {
|
|
18
|
+
items: UploadItemState[];
|
|
19
|
+
totalProgress: number;
|
|
20
|
+
totalUploaded: number;
|
|
21
|
+
totalBytes: number;
|
|
22
|
+
activeCount: number;
|
|
23
|
+
completedCount: number;
|
|
24
|
+
failedCount: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initialState: MultiUploadState = {
|
|
28
|
+
items: [],
|
|
29
|
+
totalProgress: 0,
|
|
30
|
+
totalUploaded: 0,
|
|
31
|
+
totalBytes: 0,
|
|
32
|
+
activeCount: 0,
|
|
33
|
+
completedCount: 0,
|
|
34
|
+
failedCount: 0,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hook for managing multiple concurrent file uploads with progress tracking.
|
|
39
|
+
* Each file is uploaded independently using the core upload client.
|
|
40
|
+
*
|
|
41
|
+
* Must be used within an UploadistaProvider.
|
|
42
|
+
*
|
|
43
|
+
* @param options - Multi-upload configuration options
|
|
44
|
+
* @returns Multi-upload state and control methods
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* function MyComponent() {
|
|
49
|
+
* const multiUpload = useMultiUpload({
|
|
50
|
+
* maxConcurrent: 3,
|
|
51
|
+
* onSuccess: (result) => console.log('File uploaded:', result),
|
|
52
|
+
* onError: (error) => console.error('Upload failed:', error),
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* const handlePickFiles = async () => {
|
|
56
|
+
* const files = await fileSystemProvider.pickImage({ allowMultiple: true });
|
|
57
|
+
* multiUpload.addFiles(files);
|
|
58
|
+
* await multiUpload.startUploads();
|
|
59
|
+
* };
|
|
60
|
+
*
|
|
61
|
+
* return (
|
|
62
|
+
* <View>
|
|
63
|
+
* <Button title="Pick Files" onPress={handlePickFiles} />
|
|
64
|
+
* <Text>Progress: {multiUpload.state.totalProgress}%</Text>
|
|
65
|
+
* <Text>Active: {multiUpload.state.activeCount}</Text>
|
|
66
|
+
* <Text>Completed: {multiUpload.state.completedCount}/{multiUpload.state.items.length}</Text>
|
|
67
|
+
* </View>
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function useMultiUpload(options: UseMultiUploadOptions = {}) {
|
|
73
|
+
const { client, fileSystemProvider } = useUploadistaContext();
|
|
74
|
+
const [state, setState] = useState<MultiUploadState>(initialState);
|
|
75
|
+
const abortControllersRef = useRef<Map<string, { abort: () => void }>>(
|
|
76
|
+
new Map(),
|
|
77
|
+
);
|
|
78
|
+
const nextIdRef = useRef(0);
|
|
79
|
+
|
|
80
|
+
const generateId = useCallback(() => {
|
|
81
|
+
return `upload-${Date.now()}-${nextIdRef.current++}`;
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const updateAggregateStats = useCallback((items: UploadItemState[]) => {
|
|
85
|
+
const totalBytes = items.reduce((sum, item) => sum + item.totalBytes, 0);
|
|
86
|
+
const totalUploaded = items.reduce(
|
|
87
|
+
(sum, item) => sum + item.bytesUploaded,
|
|
88
|
+
0,
|
|
89
|
+
);
|
|
90
|
+
const totalProgress =
|
|
91
|
+
totalBytes > 0 ? Math.round((totalUploaded / totalBytes) * 100) : 0;
|
|
92
|
+
const activeCount = items.filter(
|
|
93
|
+
(item) => item.status === "uploading",
|
|
94
|
+
).length;
|
|
95
|
+
const completedCount = items.filter(
|
|
96
|
+
(item) => item.status === "success",
|
|
97
|
+
).length;
|
|
98
|
+
const failedCount = items.filter((item) => item.status === "error").length;
|
|
99
|
+
|
|
100
|
+
setState((prev) => ({
|
|
101
|
+
...prev,
|
|
102
|
+
items,
|
|
103
|
+
totalProgress,
|
|
104
|
+
totalUploaded,
|
|
105
|
+
totalBytes,
|
|
106
|
+
activeCount,
|
|
107
|
+
completedCount,
|
|
108
|
+
failedCount,
|
|
109
|
+
}));
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const addFiles = useCallback(
|
|
113
|
+
(files: FilePickResult[]) => {
|
|
114
|
+
const newItems: UploadItemState[] = files.map((file) => ({
|
|
115
|
+
id: generateId(),
|
|
116
|
+
file,
|
|
117
|
+
status: "idle" as const,
|
|
118
|
+
progress: 0,
|
|
119
|
+
bytesUploaded: 0,
|
|
120
|
+
totalBytes: file.size,
|
|
121
|
+
error: null,
|
|
122
|
+
result: null,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
setState((prev) => {
|
|
126
|
+
const updatedItems = [...prev.items, ...newItems];
|
|
127
|
+
const totalBytes = updatedItems.reduce(
|
|
128
|
+
(sum, item) => sum + item.totalBytes,
|
|
129
|
+
0,
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
...prev,
|
|
133
|
+
items: updatedItems,
|
|
134
|
+
totalBytes,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return newItems.map((item) => item.id);
|
|
139
|
+
},
|
|
140
|
+
[generateId],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const uploadSingleItem = useCallback(
|
|
144
|
+
async (item: UploadItemState) => {
|
|
145
|
+
try {
|
|
146
|
+
// Update status to uploading
|
|
147
|
+
setState((prev) => {
|
|
148
|
+
const updatedItems = prev.items.map((i) =>
|
|
149
|
+
i.id === item.id ? { ...i, status: "uploading" as const } : i,
|
|
150
|
+
);
|
|
151
|
+
updateAggregateStats(updatedItems);
|
|
152
|
+
return prev;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Read file content
|
|
156
|
+
const fileContent = await fileSystemProvider.readFile(item.file.uri);
|
|
157
|
+
|
|
158
|
+
// Create a Blob from the file content
|
|
159
|
+
// Convert ArrayBuffer to Uint8Array for better compatibility
|
|
160
|
+
const data =
|
|
161
|
+
fileContent instanceof ArrayBuffer
|
|
162
|
+
? new Uint8Array(fileContent)
|
|
163
|
+
: fileContent;
|
|
164
|
+
// Note: Using any cast here because React Native Blob accepts BufferSource
|
|
165
|
+
// but TypeScript's lib.dom.d.ts Blob type doesn't include it
|
|
166
|
+
// biome-ignore lint/suspicious/noExplicitAny: React Native Blob accepts BufferSource
|
|
167
|
+
const blob = new Blob([data as any], {
|
|
168
|
+
type: item.file.mimeType || "application/octet-stream",
|
|
169
|
+
// biome-ignore lint/suspicious/noExplicitAny: BlobPropertyBag type differs by platform
|
|
170
|
+
} as any);
|
|
171
|
+
|
|
172
|
+
// use the Blob (for React Native)
|
|
173
|
+
const uploadInput = blob;
|
|
174
|
+
|
|
175
|
+
// Start upload using the client
|
|
176
|
+
const uploadPromise = client.upload(uploadInput, {
|
|
177
|
+
metadata: options.metadata,
|
|
178
|
+
|
|
179
|
+
onProgress: (
|
|
180
|
+
_uploadId: string,
|
|
181
|
+
bytesUploaded: number,
|
|
182
|
+
totalBytes: number | null,
|
|
183
|
+
) => {
|
|
184
|
+
const progress = totalBytes
|
|
185
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
186
|
+
: 0;
|
|
187
|
+
|
|
188
|
+
setState((prev) => {
|
|
189
|
+
const updatedItems = prev.items.map((i) =>
|
|
190
|
+
i.id === item.id
|
|
191
|
+
? {
|
|
192
|
+
...i,
|
|
193
|
+
progress,
|
|
194
|
+
bytesUploaded,
|
|
195
|
+
totalBytes: totalBytes || i.totalBytes,
|
|
196
|
+
}
|
|
197
|
+
: i,
|
|
198
|
+
);
|
|
199
|
+
updateAggregateStats(updatedItems);
|
|
200
|
+
return prev;
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
onSuccess: (result: UploadFile) => {
|
|
205
|
+
setState((prev) => {
|
|
206
|
+
const updatedItems = prev.items.map((i) =>
|
|
207
|
+
i.id === item.id
|
|
208
|
+
? {
|
|
209
|
+
...i,
|
|
210
|
+
status: "success" as const,
|
|
211
|
+
progress: 100,
|
|
212
|
+
result,
|
|
213
|
+
bytesUploaded: result.size || i.totalBytes,
|
|
214
|
+
}
|
|
215
|
+
: i,
|
|
216
|
+
);
|
|
217
|
+
updateAggregateStats(updatedItems);
|
|
218
|
+
return prev;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
options.onSuccess?.(result);
|
|
222
|
+
abortControllersRef.current.delete(item.id);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
onError: (error: Error) => {
|
|
226
|
+
setState((prev) => {
|
|
227
|
+
const updatedItems = prev.items.map((i) =>
|
|
228
|
+
i.id === item.id
|
|
229
|
+
? { ...i, status: "error" as const, error }
|
|
230
|
+
: i,
|
|
231
|
+
);
|
|
232
|
+
updateAggregateStats(updatedItems);
|
|
233
|
+
return prev;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
options.onError?.(error);
|
|
237
|
+
abortControllersRef.current.delete(item.id);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Store abort controller
|
|
242
|
+
const controller = await uploadPromise;
|
|
243
|
+
abortControllersRef.current.set(item.id, controller);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
setState((prev) => {
|
|
246
|
+
const updatedItems = prev.items.map((i) =>
|
|
247
|
+
i.id === item.id
|
|
248
|
+
? {
|
|
249
|
+
...i,
|
|
250
|
+
status: "error" as const,
|
|
251
|
+
error: error as Error,
|
|
252
|
+
}
|
|
253
|
+
: i,
|
|
254
|
+
);
|
|
255
|
+
updateAggregateStats(updatedItems);
|
|
256
|
+
return prev;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
options.onError?.(error as Error);
|
|
260
|
+
abortControllersRef.current.delete(item.id);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
[client, fileSystemProvider, options, updateAggregateStats],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const startUploads = useCallback(async () => {
|
|
267
|
+
const maxConcurrent = options.maxConcurrent || 3;
|
|
268
|
+
const itemsToUpload = state.items.filter((item) => item.status === "idle");
|
|
269
|
+
|
|
270
|
+
// Process items in batches
|
|
271
|
+
for (let i = 0; i < itemsToUpload.length; i += maxConcurrent) {
|
|
272
|
+
const batch = itemsToUpload.slice(i, i + maxConcurrent);
|
|
273
|
+
await Promise.all(batch.map((item) => uploadSingleItem(item)));
|
|
274
|
+
}
|
|
275
|
+
}, [state.items, options.maxConcurrent, uploadSingleItem]);
|
|
276
|
+
|
|
277
|
+
const removeItem = useCallback(
|
|
278
|
+
(id: string) => {
|
|
279
|
+
const controller = abortControllersRef.current.get(id);
|
|
280
|
+
if (controller) {
|
|
281
|
+
controller.abort();
|
|
282
|
+
abortControllersRef.current.delete(id);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
setState((prev) => {
|
|
286
|
+
const updatedItems = prev.items.filter((item) => item.id !== id);
|
|
287
|
+
updateAggregateStats(updatedItems);
|
|
288
|
+
return prev;
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
[updateAggregateStats],
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const abortItem = useCallback(
|
|
295
|
+
(id: string) => {
|
|
296
|
+
const controller = abortControllersRef.current.get(id);
|
|
297
|
+
if (controller) {
|
|
298
|
+
controller.abort();
|
|
299
|
+
abortControllersRef.current.delete(id);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setState((prev) => {
|
|
303
|
+
const updatedItems = prev.items.map((item) =>
|
|
304
|
+
item.id === id ? { ...item, status: "aborted" as const } : item,
|
|
305
|
+
);
|
|
306
|
+
updateAggregateStats(updatedItems);
|
|
307
|
+
return prev;
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
[updateAggregateStats],
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const clear = useCallback(() => {
|
|
314
|
+
// Abort all active uploads
|
|
315
|
+
abortControllersRef.current.forEach((controller) => {
|
|
316
|
+
controller.abort();
|
|
317
|
+
});
|
|
318
|
+
abortControllersRef.current.clear();
|
|
319
|
+
|
|
320
|
+
setState(initialState);
|
|
321
|
+
}, []);
|
|
322
|
+
|
|
323
|
+
const retryItem = useCallback(
|
|
324
|
+
async (id: string) => {
|
|
325
|
+
const item = state.items.find((i) => i.id === id);
|
|
326
|
+
if (item && (item.status === "error" || item.status === "aborted")) {
|
|
327
|
+
// Reset item status to idle
|
|
328
|
+
setState((prev) => {
|
|
329
|
+
const updatedItems = prev.items.map((i) =>
|
|
330
|
+
i.id === id
|
|
331
|
+
? {
|
|
332
|
+
...i,
|
|
333
|
+
status: "idle" as const,
|
|
334
|
+
progress: 0,
|
|
335
|
+
bytesUploaded: 0,
|
|
336
|
+
error: null,
|
|
337
|
+
}
|
|
338
|
+
: i,
|
|
339
|
+
);
|
|
340
|
+
updateAggregateStats(updatedItems);
|
|
341
|
+
return prev;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Upload it
|
|
345
|
+
const resetItem = state.items.find((i) => i.id === id);
|
|
346
|
+
if (resetItem) {
|
|
347
|
+
await uploadSingleItem(resetItem);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
[state.items, uploadSingleItem, updateAggregateStats],
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
state,
|
|
356
|
+
addFiles,
|
|
357
|
+
startUploads,
|
|
358
|
+
removeItem,
|
|
359
|
+
abortItem,
|
|
360
|
+
retryItem,
|
|
361
|
+
clear,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import type { UploadMetrics } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook for tracking upload performance metrics
|
|
6
|
+
* @returns Metrics object and methods to track uploads
|
|
7
|
+
*/
|
|
8
|
+
export function useUploadMetrics() {
|
|
9
|
+
const startTimeRef = useRef<number | null>(null);
|
|
10
|
+
const startBytesRef = useRef<number>(0);
|
|
11
|
+
const peakSpeedRef = useRef<number>(0);
|
|
12
|
+
|
|
13
|
+
const [metrics, setMetrics] = useState<UploadMetrics>({
|
|
14
|
+
totalBytes: 0,
|
|
15
|
+
durationMs: 0,
|
|
16
|
+
avgSpeed: 0,
|
|
17
|
+
peakSpeed: 0,
|
|
18
|
+
retries: 0,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Start tracking
|
|
22
|
+
const start = useCallback(() => {
|
|
23
|
+
startTimeRef.current = Date.now();
|
|
24
|
+
startBytesRef.current = 0;
|
|
25
|
+
peakSpeedRef.current = 0;
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
// Update metrics based on current progress
|
|
29
|
+
const update = useCallback(
|
|
30
|
+
(uploadedBytes: number, _totalBytes: number, currentRetries = 0) => {
|
|
31
|
+
if (!startTimeRef.current) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const durationMs = now - startTimeRef.current;
|
|
37
|
+
const speed = durationMs > 0 ? (uploadedBytes / durationMs) * 1000 : 0;
|
|
38
|
+
|
|
39
|
+
if (speed > peakSpeedRef.current) {
|
|
40
|
+
peakSpeedRef.current = speed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setMetrics({
|
|
44
|
+
totalBytes: uploadedBytes,
|
|
45
|
+
durationMs,
|
|
46
|
+
avgSpeed: durationMs > 0 ? (uploadedBytes / durationMs) * 1000 : 0,
|
|
47
|
+
peakSpeed: peakSpeedRef.current,
|
|
48
|
+
retries: currentRetries,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
[],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// End tracking and return final metrics
|
|
55
|
+
const end = useCallback(() => {
|
|
56
|
+
const finalMetrics = metrics;
|
|
57
|
+
startTimeRef.current = null;
|
|
58
|
+
return finalMetrics;
|
|
59
|
+
}, [metrics]);
|
|
60
|
+
|
|
61
|
+
// Reset metrics
|
|
62
|
+
const reset = useCallback(() => {
|
|
63
|
+
startTimeRef.current = null;
|
|
64
|
+
startBytesRef.current = 0;
|
|
65
|
+
peakSpeedRef.current = 0;
|
|
66
|
+
setMetrics({
|
|
67
|
+
totalBytes: 0,
|
|
68
|
+
durationMs: 0,
|
|
69
|
+
avgSpeed: 0,
|
|
70
|
+
peakSpeed: 0,
|
|
71
|
+
retries: 0,
|
|
72
|
+
});
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
metrics,
|
|
77
|
+
start,
|
|
78
|
+
update,
|
|
79
|
+
end,
|
|
80
|
+
reset,
|
|
81
|
+
};
|
|
82
|
+
}
|