@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.
- package/package.json +94 -0
- package/src/components/DropZone.native.tsx +96 -0
- package/src/components/DropZone.styles.tsx +99 -0
- package/src/components/DropZone.web.tsx +178 -0
- package/src/components/FilePickerButton.native.tsx +82 -0
- package/src/components/FilePickerButton.styles.tsx +112 -0
- package/src/components/FilePickerButton.web.tsx +84 -0
- package/src/components/UploadProgress.native.tsx +203 -0
- package/src/components/UploadProgress.styles.tsx +90 -0
- package/src/components/UploadProgress.web.tsx +201 -0
- package/src/components/index.native.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/components/index.web.ts +8 -0
- package/src/constants.ts +336 -0
- package/src/examples/index.ts +181 -0
- package/src/hooks/createUseFilePickerHook.ts +169 -0
- package/src/hooks/createUseFileUploadHook.ts +173 -0
- package/src/hooks/index.native.ts +12 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/index.web.ts +12 -0
- package/src/index.native.ts +142 -0
- package/src/index.ts +139 -0
- package/src/index.web.ts +142 -0
- package/src/permissions/index.native.ts +8 -0
- package/src/permissions/index.ts +8 -0
- package/src/permissions/index.web.ts +8 -0
- package/src/permissions/permissions.native.ts +177 -0
- package/src/permissions/permissions.web.ts +96 -0
- package/src/picker/FilePicker.native.ts +407 -0
- package/src/picker/FilePicker.web.ts +366 -0
- package/src/picker/index.native.ts +2 -0
- package/src/picker/index.ts +2 -0
- package/src/picker/index.web.ts +2 -0
- package/src/types.ts +990 -0
- package/src/uploader/ChunkedUploader.ts +312 -0
- package/src/uploader/FileUploader.native.ts +435 -0
- package/src/uploader/FileUploader.web.ts +350 -0
- package/src/uploader/UploadQueue.ts +519 -0
- package/src/uploader/index.native.ts +4 -0
- package/src/uploader/index.ts +4 -0
- package/src/uploader/index.web.ts +4 -0
- 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
|
+
}
|