@idealyst/files 1.2.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +94 -0
  2. package/src/components/DropZone.native.tsx +96 -0
  3. package/src/components/DropZone.styles.tsx +99 -0
  4. package/src/components/DropZone.web.tsx +178 -0
  5. package/src/components/FilePickerButton.native.tsx +82 -0
  6. package/src/components/FilePickerButton.styles.tsx +112 -0
  7. package/src/components/FilePickerButton.web.tsx +84 -0
  8. package/src/components/UploadProgress.native.tsx +203 -0
  9. package/src/components/UploadProgress.styles.tsx +90 -0
  10. package/src/components/UploadProgress.web.tsx +201 -0
  11. package/src/components/index.native.ts +8 -0
  12. package/src/components/index.ts +6 -0
  13. package/src/components/index.web.ts +8 -0
  14. package/src/constants.ts +336 -0
  15. package/src/examples/index.ts +181 -0
  16. package/src/hooks/createUseFilePickerHook.ts +169 -0
  17. package/src/hooks/createUseFileUploadHook.ts +173 -0
  18. package/src/hooks/index.native.ts +12 -0
  19. package/src/hooks/index.ts +12 -0
  20. package/src/hooks/index.web.ts +12 -0
  21. package/src/index.native.ts +142 -0
  22. package/src/index.ts +139 -0
  23. package/src/index.web.ts +142 -0
  24. package/src/permissions/index.native.ts +8 -0
  25. package/src/permissions/index.ts +8 -0
  26. package/src/permissions/index.web.ts +8 -0
  27. package/src/permissions/permissions.native.ts +177 -0
  28. package/src/permissions/permissions.web.ts +96 -0
  29. package/src/picker/FilePicker.native.ts +407 -0
  30. package/src/picker/FilePicker.web.ts +366 -0
  31. package/src/picker/index.native.ts +2 -0
  32. package/src/picker/index.ts +2 -0
  33. package/src/picker/index.web.ts +2 -0
  34. package/src/types.ts +990 -0
  35. package/src/uploader/ChunkedUploader.ts +312 -0
  36. package/src/uploader/FileUploader.native.ts +435 -0
  37. package/src/uploader/FileUploader.web.ts +350 -0
  38. package/src/uploader/UploadQueue.ts +519 -0
  39. package/src/uploader/index.native.ts +4 -0
  40. package/src/uploader/index.ts +4 -0
  41. package/src/uploader/index.web.ts +4 -0
  42. package/src/utils.ts +586 -0
@@ -0,0 +1,366 @@
1
+ import type {
2
+ IFilePicker,
3
+ FilePickerConfig,
4
+ FilePickerResult,
5
+ FilePickerStatus,
6
+ FilePickerState,
7
+ PickedFile,
8
+ CameraOptions,
9
+ ValidationResult,
10
+ PermissionStatus,
11
+ } from '../types';
12
+ import {
13
+ DEFAULT_FILE_PICKER_CONFIG,
14
+ INITIAL_FILE_PICKER_STATUS,
15
+ } from '../constants';
16
+ import {
17
+ generateId,
18
+ buildAcceptString,
19
+ validateFiles as validateFilesUtil,
20
+ createPickedFileFromFile,
21
+ createFilePickerError,
22
+ EventEmitter,
23
+ getFileExtension,
24
+ } from '../utils';
25
+ import { checkPermissions, requestPermissions } from '../permissions/permissions.web';
26
+
27
+ type FilePickerEvents = {
28
+ stateChange: [FilePickerStatus];
29
+ };
30
+
31
+ /**
32
+ * Web implementation of IFilePicker using HTML5 File API.
33
+ */
34
+ export class WebFilePicker implements IFilePicker {
35
+ private _status: FilePickerStatus = { ...INITIAL_FILE_PICKER_STATUS };
36
+ private _events = new EventEmitter<FilePickerEvents>();
37
+ private _defaultConfig: FilePickerConfig;
38
+ private _disposed = false;
39
+
40
+ constructor(config?: Partial<FilePickerConfig>) {
41
+ this._defaultConfig = { ...DEFAULT_FILE_PICKER_CONFIG, ...config };
42
+ // Web always has permission for file input
43
+ this._status.permission = 'granted';
44
+ }
45
+
46
+ get status(): FilePickerStatus {
47
+ return { ...this._status };
48
+ }
49
+
50
+ async checkPermission(): Promise<PermissionStatus> {
51
+ // File input doesn't require permission on web
52
+ return 'granted';
53
+ }
54
+
55
+ async requestPermission(): Promise<PermissionStatus> {
56
+ // File input doesn't require permission on web
57
+ return 'granted';
58
+ }
59
+
60
+ async pick(config?: Partial<FilePickerConfig>): Promise<FilePickerResult> {
61
+ if (this._disposed) {
62
+ return {
63
+ cancelled: true,
64
+ files: [],
65
+ rejected: [],
66
+ error: createFilePickerError('NOT_SUPPORTED', 'File picker has been disposed'),
67
+ };
68
+ }
69
+
70
+ const finalConfig = { ...this._defaultConfig, ...config };
71
+ this._updateState('picking');
72
+
73
+ return new Promise((resolve) => {
74
+ const input = document.createElement('input');
75
+ input.type = 'file';
76
+ input.multiple = finalConfig.multiple;
77
+ input.accept = buildAcceptString(finalConfig);
78
+
79
+ // Handle capture attribute for mobile
80
+ if (finalConfig.allowCamera && finalConfig.allowedTypes.some(t => t === 'image' || t === 'video')) {
81
+ // Don't set capture, let user choose between camera and library
82
+ }
83
+
84
+ const handleChange = async () => {
85
+ cleanup();
86
+ this._updateState('processing');
87
+
88
+ try {
89
+ const files = Array.from(input.files || []);
90
+
91
+ if (files.length === 0) {
92
+ this._updateState('idle');
93
+ resolve({ cancelled: true, files: [], rejected: [] });
94
+ return;
95
+ }
96
+
97
+ // Convert to PickedFile and validate
98
+ const pickedFiles = await this._processFiles(files, finalConfig);
99
+ const { accepted, rejected } = validateFilesUtil(pickedFiles, finalConfig);
100
+
101
+ // Get image dimensions for accepted files
102
+ const filesWithDimensions = await this._addImageDimensions(accepted);
103
+
104
+ this._updateState('idle');
105
+ resolve({
106
+ cancelled: false,
107
+ files: filesWithDimensions,
108
+ rejected,
109
+ });
110
+ } catch (error) {
111
+ this._updateState('error', createFilePickerError('UNKNOWN', 'Failed to process files', error as Error));
112
+ resolve({
113
+ cancelled: false,
114
+ files: [],
115
+ rejected: [],
116
+ error: this._status.error,
117
+ });
118
+ }
119
+ };
120
+
121
+ const handleCancel = () => {
122
+ cleanup();
123
+ this._updateState('idle');
124
+ resolve({ cancelled: true, files: [], rejected: [] });
125
+ };
126
+
127
+ const cleanup = () => {
128
+ input.removeEventListener('change', handleChange);
129
+ input.removeEventListener('cancel', handleCancel);
130
+ window.removeEventListener('focus', handleFocusAfterPicker);
131
+ };
132
+
133
+ // Fallback for browsers that don't fire 'cancel' event
134
+ let focusTimeout: ReturnType<typeof setTimeout>;
135
+ const handleFocusAfterPicker = () => {
136
+ // Give time for the change event to fire
137
+ focusTimeout = setTimeout(() => {
138
+ if (!input.files?.length) {
139
+ handleCancel();
140
+ }
141
+ }, 300);
142
+ };
143
+
144
+ input.addEventListener('change', handleChange);
145
+ input.addEventListener('cancel', handleCancel);
146
+
147
+ // Some browsers fire 'focus' when picker is closed without selection
148
+ window.addEventListener('focus', handleFocusAfterPicker, { once: true });
149
+
150
+ // Trigger the file picker
151
+ input.click();
152
+ });
153
+ }
154
+
155
+ async captureFromCamera(options?: CameraOptions): Promise<FilePickerResult> {
156
+ if (this._disposed) {
157
+ return {
158
+ cancelled: true,
159
+ files: [],
160
+ rejected: [],
161
+ error: createFilePickerError('NOT_SUPPORTED', 'File picker has been disposed'),
162
+ };
163
+ }
164
+
165
+ // Check camera permission
166
+ const permissions = await checkPermissions();
167
+ if (permissions.camera !== 'granted') {
168
+ const requested = await requestPermissions();
169
+ if (requested.camera !== 'granted') {
170
+ return {
171
+ cancelled: false,
172
+ files: [],
173
+ rejected: [],
174
+ error: createFilePickerError('PERMISSION_DENIED', 'Camera permission denied'),
175
+ };
176
+ }
177
+ }
178
+
179
+ this._updateState('picking');
180
+
181
+ return new Promise((resolve) => {
182
+ const input = document.createElement('input');
183
+ input.type = 'file';
184
+ input.capture = options?.useFrontCamera ? 'user' : 'environment';
185
+ input.accept = options?.mediaType === 'video' ? 'video/*' : 'image/*';
186
+
187
+ const handleChange = async () => {
188
+ cleanup();
189
+ this._updateState('processing');
190
+
191
+ try {
192
+ const files = Array.from(input.files || []);
193
+
194
+ if (files.length === 0) {
195
+ this._updateState('idle');
196
+ resolve({ cancelled: true, files: [], rejected: [] });
197
+ return;
198
+ }
199
+
200
+ const pickedFiles = await this._processFiles(files);
201
+ const filesWithDimensions = await this._addImageDimensions(pickedFiles);
202
+
203
+ this._updateState('idle');
204
+ resolve({
205
+ cancelled: false,
206
+ files: filesWithDimensions,
207
+ rejected: [],
208
+ });
209
+ } catch (error) {
210
+ this._updateState('error', createFilePickerError('UNKNOWN', 'Failed to capture', error as Error));
211
+ resolve({
212
+ cancelled: false,
213
+ files: [],
214
+ rejected: [],
215
+ error: this._status.error,
216
+ });
217
+ }
218
+ };
219
+
220
+ const handleCancel = () => {
221
+ cleanup();
222
+ this._updateState('idle');
223
+ resolve({ cancelled: true, files: [], rejected: [] });
224
+ };
225
+
226
+ const cleanup = () => {
227
+ input.removeEventListener('change', handleChange);
228
+ input.removeEventListener('cancel', handleCancel);
229
+ };
230
+
231
+ input.addEventListener('change', handleChange);
232
+ input.addEventListener('cancel', handleCancel);
233
+
234
+ input.click();
235
+ });
236
+ }
237
+
238
+ validateFiles(files: File[] | PickedFile[], config?: Partial<FilePickerConfig>): ValidationResult {
239
+ const finalConfig = { ...this._defaultConfig, ...config };
240
+ return validateFilesUtil(files, finalConfig);
241
+ }
242
+
243
+ onStateChange(callback: (status: FilePickerStatus) => void): () => void {
244
+ return this._events.on('stateChange', callback);
245
+ }
246
+
247
+ dispose(): void {
248
+ this._disposed = true;
249
+ this._events.removeAllListeners();
250
+ }
251
+
252
+ // ============================================
253
+ // PRIVATE METHODS
254
+ // ============================================
255
+
256
+ private _updateState(state: FilePickerState, error?: ReturnType<typeof createFilePickerError>): void {
257
+ this._status = {
258
+ ...this._status,
259
+ state,
260
+ error,
261
+ };
262
+ this._events.emit('stateChange', this._status);
263
+ }
264
+
265
+ private async _processFiles(files: File[], config?: Partial<FilePickerConfig>): Promise<PickedFile[]> {
266
+ const pickedFiles: PickedFile[] = [];
267
+
268
+ for (const file of files) {
269
+ const uri = URL.createObjectURL(file);
270
+ const extension = getFileExtension(file.name);
271
+
272
+ const pickedFile: PickedFile = {
273
+ id: generateId(),
274
+ name: file.name,
275
+ size: file.size,
276
+ type: file.type,
277
+ uri,
278
+ extension,
279
+ lastModified: file.lastModified,
280
+ getArrayBuffer: () => file.arrayBuffer(),
281
+ getData: () => Promise.resolve(file),
282
+ };
283
+
284
+ pickedFiles.push(pickedFile);
285
+ }
286
+
287
+ return pickedFiles;
288
+ }
289
+
290
+ private async _addImageDimensions(files: PickedFile[]): Promise<PickedFile[]> {
291
+ const filesWithDimensions: PickedFile[] = [];
292
+
293
+ for (const file of files) {
294
+ if (file.type.startsWith('image/')) {
295
+ try {
296
+ const dimensions = await this._getImageDimensions(file.uri);
297
+ filesWithDimensions.push({
298
+ ...file,
299
+ dimensions,
300
+ });
301
+ } catch {
302
+ // If we can't get dimensions, just use the file as-is
303
+ filesWithDimensions.push(file);
304
+ }
305
+ } else if (file.type.startsWith('video/')) {
306
+ try {
307
+ const { dimensions, duration } = await this._getVideoDimensions(file.uri);
308
+ filesWithDimensions.push({
309
+ ...file,
310
+ dimensions,
311
+ duration,
312
+ });
313
+ } catch {
314
+ filesWithDimensions.push(file);
315
+ }
316
+ } else {
317
+ filesWithDimensions.push(file);
318
+ }
319
+ }
320
+
321
+ return filesWithDimensions;
322
+ }
323
+
324
+ private _getImageDimensions(uri: string): Promise<{ width: number; height: number }> {
325
+ return new Promise((resolve, reject) => {
326
+ const img = new Image();
327
+
328
+ img.onload = () => {
329
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
330
+ };
331
+
332
+ img.onerror = () => {
333
+ reject(new Error('Failed to load image'));
334
+ };
335
+
336
+ img.src = uri;
337
+ });
338
+ }
339
+
340
+ private _getVideoDimensions(uri: string): Promise<{ dimensions: { width: number; height: number }; duration: number }> {
341
+ return new Promise((resolve, reject) => {
342
+ const video = document.createElement('video');
343
+
344
+ video.onloadedmetadata = () => {
345
+ resolve({
346
+ dimensions: { width: video.videoWidth, height: video.videoHeight },
347
+ duration: Math.round(video.duration * 1000), // Convert to milliseconds
348
+ });
349
+ URL.revokeObjectURL(uri); // Clean up
350
+ };
351
+
352
+ video.onerror = () => {
353
+ reject(new Error('Failed to load video'));
354
+ };
355
+
356
+ video.src = uri;
357
+ });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Create a new WebFilePicker instance.
363
+ */
364
+ export function createFilePicker(config?: Partial<FilePickerConfig>): IFilePicker {
365
+ return new WebFilePicker(config);
366
+ }
@@ -0,0 +1,2 @@
1
+ export { NativeFilePicker, createFilePicker } from './FilePicker.native';
2
+ export type { IFilePicker } from '../types';
@@ -0,0 +1,2 @@
1
+ export { WebFilePicker, createFilePicker } from './FilePicker.web';
2
+ export type { IFilePicker } from '../types';
@@ -0,0 +1,2 @@
1
+ export { WebFilePicker, createFilePicker } from './FilePicker.web';
2
+ export type { IFilePicker } from '../types';