nuxt-ui-elements 0.1.32 → 0.1.33
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/dist/module.json +1 -1
- package/dist/runtime/composables/useUploadManager/index.d.ts +4 -4
- package/dist/runtime/composables/useUploadManager/index.js +112 -87
- package/dist/runtime/composables/useUploadManager/plugins/image-compressor.d.ts +1 -1
- package/dist/runtime/composables/useUploadManager/plugins/image-compressor.js +2 -2
- package/dist/runtime/composables/useUploadManager/plugins/storage/azure-datalake.d.ts +12 -1
- package/dist/runtime/composables/useUploadManager/plugins/storage/azure-datalake.js +62 -32
- package/dist/runtime/composables/useUploadManager/plugins/thumbnail-generator.d.ts +4 -3
- package/dist/runtime/composables/useUploadManager/plugins/thumbnail-generator.js +87 -56
- package/dist/runtime/composables/useUploadManager/plugins/video-compressor.d.ts +1 -1
- package/dist/runtime/composables/useUploadManager/plugins/video-compressor.js +17 -7
- package/dist/runtime/composables/useUploadManager/types.d.ts +128 -5
- package/dist/runtime/composables/useUploadManager/types.js +6 -0
- package/dist/runtime/composables/useUploadManager/utils.d.ts +23 -0
- package/dist/runtime/composables/useUploadManager/utils.js +45 -0
- package/dist/runtime/composables/useUploadManager/validators/allowed-file-types.d.ts +1 -1
- package/dist/runtime/composables/useUploadManager/validators/allowed-file-types.js +7 -3
- package/dist/runtime/composables/useUploadManager/validators/max-file-size.d.ts +2 -2
- package/dist/runtime/composables/useUploadManager/validators/max-file-size.js +7 -3
- package/dist/runtime/composables/useUploadManager/validators/max-files.d.ts +1 -1
- package/dist/runtime/composables/useUploadManager/validators/max-files.js +7 -3
- package/package.json +6 -1
package/dist/module.json
CHANGED
|
@@ -184,8 +184,8 @@ export declare const useUploadManager: <TUploadResult = any>(_options?: UploadOp
|
|
|
184
184
|
};
|
|
185
185
|
})[]>>;
|
|
186
186
|
totalProgress: import("vue").ComputedRef<number>;
|
|
187
|
-
addFiles: (newFiles: File[]) => Promise<
|
|
188
|
-
addFile: (file: File) => Promise<UploadFile
|
|
187
|
+
addFiles: (newFiles: File[]) => Promise<UploadFile[]>;
|
|
188
|
+
addFile: (file: File) => Promise<UploadFile>;
|
|
189
189
|
onGetRemoteFile: (fn: GetRemoteFileFn) => void;
|
|
190
190
|
onUpload: (fn: UploadFn<TUploadResult>) => void;
|
|
191
191
|
removeFile: (fileId: string) => Promise<void>;
|
|
@@ -453,14 +453,14 @@ export declare const useUploadManager: <TUploadResult = any>(_options?: UploadOp
|
|
|
453
453
|
} | undefined;
|
|
454
454
|
uploadResult?: import("vue").UnwrapRef<TUploadResult> | undefined;
|
|
455
455
|
meta: Record<string, unknown>;
|
|
456
|
-
}
|
|
456
|
+
};
|
|
457
457
|
upload: () => Promise<void>;
|
|
458
458
|
reset: () => void;
|
|
459
459
|
status: import("vue").Ref<UploadStatus, UploadStatus>;
|
|
460
460
|
getFileData: (fileId: string) => Promise<Blob>;
|
|
461
461
|
getFileURL: (fileId: string) => Promise<string>;
|
|
462
462
|
getFileStream: (fileId: string) => Promise<ReadableStream<Uint8Array>>;
|
|
463
|
-
replaceFileData: (fileId: string, newData: Blob, newName?: string) => Promise<
|
|
463
|
+
replaceFileData: (fileId: string, newData: Blob, newName?: string, shouldAutoUpload?: boolean) => Promise<UploadFile>;
|
|
464
464
|
updateFile: (fileId: string, updatedFile: Partial<UploadFile<TUploadResult>>) => void;
|
|
465
465
|
initializeExistingFiles: (initialFiles: Array<Partial<UploadFile>>) => Promise<void>;
|
|
466
466
|
addPlugin: (plugin: UploaderPlugin<any, any>) => void;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import mitt from "mitt";
|
|
2
|
-
import { computed, readonly, ref } from "vue";
|
|
3
|
-
import { ValidatorAllowedFileTypes,
|
|
2
|
+
import { computed, onBeforeUnmount, readonly, ref } from "vue";
|
|
3
|
+
import { ValidatorAllowedFileTypes, ValidatorMaxFileSize, ValidatorMaxFiles } from "./validators/index.js";
|
|
4
4
|
import { PluginThumbnailGenerator, PluginImageCompressor } from "./plugins/index.js";
|
|
5
|
+
import { createPluginContext, createFileError, cleanupObjectURLs } from "./utils.js";
|
|
6
|
+
function isStoragePlugin(plugin) {
|
|
7
|
+
return plugin !== null && "upload" in plugin.hooks;
|
|
8
|
+
}
|
|
5
9
|
function getExtension(fullFileName) {
|
|
6
10
|
const lastDot = fullFileName.lastIndexOf(".");
|
|
7
11
|
if (lastDot === -1 || lastDot === fullFileName.length - 1) {
|
|
@@ -10,6 +14,7 @@ function getExtension(fullFileName) {
|
|
|
10
14
|
return fullFileName.slice(lastDot + 1).toLocaleLowerCase();
|
|
11
15
|
}
|
|
12
16
|
const defaultOptions = {
|
|
17
|
+
storage: void 0,
|
|
13
18
|
plugins: [],
|
|
14
19
|
maxFileSize: false,
|
|
15
20
|
allowedFileTypes: false,
|
|
@@ -23,6 +28,7 @@ export const useUploadManager = (_options = {}) => {
|
|
|
23
28
|
const files = ref([]);
|
|
24
29
|
const emitter = mitt();
|
|
25
30
|
const status = ref("waiting");
|
|
31
|
+
const createdObjectURLs = /* @__PURE__ */ new Map();
|
|
26
32
|
const pluginEmitFunctions = /* @__PURE__ */ new Map();
|
|
27
33
|
const getPluginEmitFn = (pluginId) => {
|
|
28
34
|
let emitFn = pluginEmitFunctions.get(pluginId);
|
|
@@ -46,45 +52,53 @@ export const useUploadManager = (_options = {}) => {
|
|
|
46
52
|
const sum = files.value.reduce((acc, file) => acc + file.progress.percentage, 0);
|
|
47
53
|
return Math.round(sum / files.value.length);
|
|
48
54
|
});
|
|
49
|
-
const isStoragePlugin = (plugin) => {
|
|
50
|
-
return !!(plugin.hooks.upload || plugin.hooks.getRemoteFile || plugin.hooks.remove);
|
|
51
|
-
};
|
|
52
55
|
const getStoragePlugin = () => {
|
|
56
|
+
if (options.storage) {
|
|
57
|
+
return options.storage;
|
|
58
|
+
}
|
|
53
59
|
if (!options.plugins) return null;
|
|
54
60
|
for (let i = options.plugins.length - 1; i >= 0; i--) {
|
|
55
|
-
const plugin = options.plugins[i];
|
|
56
|
-
if (
|
|
61
|
+
const plugin = options.plugins[i] || null;
|
|
62
|
+
if (isStoragePlugin(plugin)) {
|
|
63
|
+
if (import.meta.dev) {
|
|
64
|
+
console.warn(
|
|
65
|
+
`[useUploadManager] Storage plugin "${plugin.id}" found in plugins array.
|
|
66
|
+
This is deprecated. Use the 'storage' option instead:
|
|
67
|
+
|
|
68
|
+
useUploadManager({ storage: ${plugin.id}(...) })`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
57
71
|
return plugin;
|
|
58
72
|
}
|
|
59
73
|
}
|
|
60
74
|
return null;
|
|
61
75
|
};
|
|
62
76
|
const addPlugin = (plugin) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
77
|
+
const hasUploadHook = "upload" in plugin.hooks;
|
|
78
|
+
if (hasUploadHook) {
|
|
79
|
+
if (import.meta.dev) {
|
|
66
80
|
console.warn(
|
|
67
|
-
`[useUploadManager]
|
|
81
|
+
`[useUploadManager] Storage plugin "${plugin.id}" should use the 'storage' option instead of 'plugins':
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const s3Uploader = useUploadManager({ plugins: [PluginS3Storage({ ... })] })
|
|
75
|
-
const azureUploader = useUploadManager({ plugins: [PluginAzureStorage({ ... })] })
|
|
83
|
+
useUploadManager({
|
|
84
|
+
storage: ${plugin.id}({ ... }), // \u2713 Correct
|
|
85
|
+
plugins: [...] // Only for validators/processors
|
|
86
|
+
})
|
|
76
87
|
`
|
|
77
88
|
);
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
if (options.plugins) {
|
|
92
|
+
options.plugins.push(plugin);
|
|
93
|
+
} else {
|
|
94
|
+
options.plugins = [plugin];
|
|
95
|
+
}
|
|
82
96
|
};
|
|
83
97
|
if (options.maxFiles !== false && options.maxFiles !== void 0) {
|
|
84
98
|
addPlugin(ValidatorMaxFiles({ maxFiles: options.maxFiles }));
|
|
85
99
|
}
|
|
86
100
|
if (options.maxFileSize !== false && options.maxFileSize !== void 0) {
|
|
87
|
-
addPlugin(
|
|
101
|
+
addPlugin(ValidatorMaxFileSize({ maxFileSize: options.maxFileSize }));
|
|
88
102
|
}
|
|
89
103
|
if (options.allowedFileTypes !== false && options.allowedFileTypes !== void 0 && options.allowedFileTypes.length > 0) {
|
|
90
104
|
addPlugin(ValidatorAllowedFileTypes({ allowedFileTypes: options.allowedFileTypes }));
|
|
@@ -93,8 +107,8 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
93
107
|
const thumbOpts = options.thumbnails === true ? {} : options.thumbnails || {};
|
|
94
108
|
addPlugin(
|
|
95
109
|
PluginThumbnailGenerator({
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
maxWidth: thumbOpts.width ?? 128,
|
|
111
|
+
maxHeight: thumbOpts.height ?? 128,
|
|
98
112
|
quality: thumbOpts.quality ?? 1
|
|
99
113
|
})
|
|
100
114
|
);
|
|
@@ -131,14 +145,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
131
145
|
const storagePlugin = getStoragePlugin();
|
|
132
146
|
let remoteFileData;
|
|
133
147
|
if (storagePlugin?.hooks.getRemoteFile) {
|
|
134
|
-
const context =
|
|
135
|
-
files: files.value,
|
|
136
|
-
options,
|
|
137
|
-
emit: (event, payload) => {
|
|
138
|
-
const prefixedEvent = `${storagePlugin.id}:${String(event)}`;
|
|
139
|
-
emitter.emit(prefixedEvent, payload);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
148
|
+
const context = createPluginContext(storagePlugin.id, files.value, options, emitter);
|
|
142
149
|
remoteFileData = await storagePlugin.hooks.getRemoteFile(file.id, context);
|
|
143
150
|
} else {
|
|
144
151
|
remoteFileData = await getRemoteFileFn(file.id);
|
|
@@ -183,46 +190,55 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
183
190
|
extension
|
|
184
191
|
}
|
|
185
192
|
};
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
try {
|
|
194
|
+
const validatedFile = await runPluginStage("validate", uploadFile);
|
|
195
|
+
if (!validatedFile) {
|
|
196
|
+
throw new Error(`File validation failed for ${file.name}`);
|
|
197
|
+
}
|
|
198
|
+
const preprocessedFile = await runPluginStage("preprocess", validatedFile);
|
|
199
|
+
const fileToAdd = preprocessedFile || validatedFile;
|
|
200
|
+
files.value.push(fileToAdd);
|
|
201
|
+
emitter.emit("file:added", fileToAdd);
|
|
202
|
+
if (options.autoProceed) {
|
|
203
|
+
upload();
|
|
204
|
+
}
|
|
205
|
+
return validatedFile;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const error = createFileError(uploadFile, err);
|
|
208
|
+
const fileWithError = { ...uploadFile, status: "error", error };
|
|
209
|
+
files.value.push(fileWithError);
|
|
210
|
+
emitter.emit("file:error", { file: fileWithError, error });
|
|
211
|
+
throw err;
|
|
194
212
|
}
|
|
195
|
-
return validatedFile;
|
|
196
213
|
};
|
|
197
|
-
const addFiles = (newFiles) => {
|
|
198
|
-
|
|
214
|
+
const addFiles = async (newFiles) => {
|
|
215
|
+
const results = await Promise.allSettled(newFiles.map((file) => addFile(file)));
|
|
216
|
+
const addedFiles = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
217
|
+
return addedFiles;
|
|
199
218
|
};
|
|
200
219
|
const removeFile = async (fileId) => {
|
|
201
|
-
const file =
|
|
220
|
+
const file = files.value.find((f) => f.id === fileId);
|
|
202
221
|
if (!file) return;
|
|
203
222
|
if (file.remoteUrl) {
|
|
204
223
|
const storagePlugin = getStoragePlugin();
|
|
205
224
|
if (storagePlugin?.hooks.remove) {
|
|
206
225
|
try {
|
|
207
|
-
const context =
|
|
208
|
-
files: files.value,
|
|
209
|
-
options,
|
|
210
|
-
emit: (event, payload) => {
|
|
211
|
-
const prefixedEvent = `${storagePlugin.id}:${String(event)}`;
|
|
212
|
-
emitter.emit(prefixedEvent, payload);
|
|
213
|
-
}
|
|
214
|
-
};
|
|
226
|
+
const context = createPluginContext(storagePlugin.id, files.value, options, emitter);
|
|
215
227
|
await storagePlugin.hooks.remove(file, context);
|
|
216
228
|
} catch (error) {
|
|
217
229
|
console.error(`Storage plugin remove error:`, error);
|
|
218
230
|
}
|
|
219
231
|
}
|
|
220
232
|
}
|
|
233
|
+
cleanupObjectURLs(createdObjectURLs, file.id);
|
|
221
234
|
files.value = files.value.filter((f) => f.id !== fileId);
|
|
222
235
|
emitter.emit("file:removed", file);
|
|
223
236
|
};
|
|
224
237
|
const removeFiles = (fileIds) => {
|
|
225
238
|
const removedFiles = files.value.filter((f) => fileIds.includes(f.id));
|
|
239
|
+
removedFiles.forEach((file) => {
|
|
240
|
+
cleanupObjectURLs(createdObjectURLs, file.id);
|
|
241
|
+
});
|
|
226
242
|
files.value = files.value.filter((f) => !fileIds.includes(f.id));
|
|
227
243
|
removedFiles.forEach((file) => {
|
|
228
244
|
emitter.emit("file:removed", file);
|
|
@@ -231,6 +247,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
231
247
|
};
|
|
232
248
|
const clearFiles = () => {
|
|
233
249
|
const allFiles = [...files.value];
|
|
250
|
+
cleanupObjectURLs(createdObjectURLs);
|
|
234
251
|
files.value = [];
|
|
235
252
|
allFiles.forEach((file) => {
|
|
236
253
|
emitter.emit("file:removed", file);
|
|
@@ -239,9 +256,6 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
239
256
|
};
|
|
240
257
|
const getFileData = async (fileId) => {
|
|
241
258
|
const file = getFile(fileId);
|
|
242
|
-
if (!file) {
|
|
243
|
-
throw new Error(`File not found: ${fileId}`);
|
|
244
|
-
}
|
|
245
259
|
if (file.size > 100 * 1024 * 1024) {
|
|
246
260
|
console.warn(
|
|
247
261
|
`getFileData: Loading large file (${(file.size / 1024 / 1024).toFixed(2)}MB) into memory. Consider using getFileURL() or getFileStream() for better performance.`
|
|
@@ -258,19 +272,19 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
258
272
|
};
|
|
259
273
|
const getFileURL = async (fileId) => {
|
|
260
274
|
const file = getFile(fileId);
|
|
261
|
-
if (!file) {
|
|
262
|
-
throw new Error(`File not found: ${fileId}`);
|
|
263
|
-
}
|
|
264
275
|
if (file.source === "local") {
|
|
265
|
-
|
|
276
|
+
const existingURL = createdObjectURLs.get(file.id);
|
|
277
|
+
if (existingURL) {
|
|
278
|
+
return existingURL;
|
|
279
|
+
}
|
|
280
|
+
const objectURL = URL.createObjectURL(file.data);
|
|
281
|
+
createdObjectURLs.set(file.id, objectURL);
|
|
282
|
+
return objectURL;
|
|
266
283
|
}
|
|
267
284
|
return file.remoteUrl;
|
|
268
285
|
};
|
|
269
286
|
const getFileStream = async (fileId) => {
|
|
270
287
|
const file = getFile(fileId);
|
|
271
|
-
if (!file) {
|
|
272
|
-
throw new Error(`File not found: ${fileId}`);
|
|
273
|
-
}
|
|
274
288
|
if (file.source === "local") {
|
|
275
289
|
return file.data.stream();
|
|
276
290
|
}
|
|
@@ -280,11 +294,9 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
280
294
|
}
|
|
281
295
|
return response.body;
|
|
282
296
|
};
|
|
283
|
-
const replaceFileData = async (fileId, newData, newName) => {
|
|
297
|
+
const replaceFileData = async (fileId, newData, newName, shouldAutoUpload) => {
|
|
284
298
|
const file = getFile(fileId);
|
|
285
|
-
|
|
286
|
-
throw new Error(`File not found: ${fileId}`);
|
|
287
|
-
}
|
|
299
|
+
cleanupObjectURLs(createdObjectURLs, fileId);
|
|
288
300
|
const updatedFile = {
|
|
289
301
|
...file,
|
|
290
302
|
source: "local",
|
|
@@ -294,12 +306,25 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
294
306
|
status: "waiting",
|
|
295
307
|
// Mark as needing upload
|
|
296
308
|
progress: { percentage: 0 },
|
|
297
|
-
remoteUrl: void 0
|
|
309
|
+
remoteUrl: void 0,
|
|
298
310
|
// Clear old remote URL
|
|
311
|
+
meta: {}
|
|
312
|
+
// Clear old metadata (thumbnails, dimensions, etc.)
|
|
299
313
|
};
|
|
314
|
+
const preprocessedFile = await runPluginStage("preprocess", updatedFile);
|
|
315
|
+
const finalFile = preprocessedFile || updatedFile;
|
|
300
316
|
const index = files.value.findIndex((f) => f.id === fileId);
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
if (index === -1) {
|
|
318
|
+
throw new Error(`File not found: ${fileId}`);
|
|
319
|
+
}
|
|
320
|
+
files.value[index] = finalFile;
|
|
321
|
+
emitter.emit("file:replaced", finalFile);
|
|
322
|
+
emitter.emit("file:added", finalFile);
|
|
323
|
+
const shouldUpload = shouldAutoUpload ?? options.autoProceed;
|
|
324
|
+
if (shouldUpload) {
|
|
325
|
+
upload();
|
|
326
|
+
}
|
|
327
|
+
return finalFile;
|
|
303
328
|
};
|
|
304
329
|
const reorderFile = (oldIndex, newIndex) => {
|
|
305
330
|
if (oldIndex === newIndex) {
|
|
@@ -321,7 +346,11 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
321
346
|
emitter.emit("files:reorder", { oldIndex, newIndex });
|
|
322
347
|
};
|
|
323
348
|
const getFile = (fileId) => {
|
|
324
|
-
|
|
349
|
+
const file = files.value.find((f) => f.id === fileId);
|
|
350
|
+
if (!file) {
|
|
351
|
+
throw new Error(`File not found: ${fileId}`);
|
|
352
|
+
}
|
|
353
|
+
return file;
|
|
325
354
|
};
|
|
326
355
|
const upload = async () => {
|
|
327
356
|
const filesToUpload = files.value.filter((f) => f.status === "waiting");
|
|
@@ -330,14 +359,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
330
359
|
try {
|
|
331
360
|
const processedFile = await runPluginStage("process", file);
|
|
332
361
|
if (!processedFile) {
|
|
333
|
-
const error =
|
|
334
|
-
message: "File processing failed",
|
|
335
|
-
details: {
|
|
336
|
-
fileName: file.name,
|
|
337
|
-
fileSize: file.size,
|
|
338
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
339
|
-
}
|
|
340
|
-
};
|
|
362
|
+
const error = createFileError(file, new Error("File processing failed"));
|
|
341
363
|
updateFile(file.id, { status: "error", error });
|
|
342
364
|
emitter.emit("file:error", { file, error });
|
|
343
365
|
continue;
|
|
@@ -352,6 +374,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
352
374
|
};
|
|
353
375
|
const storagePlugin = getStoragePlugin();
|
|
354
376
|
let uploadResult;
|
|
377
|
+
let remoteUrl;
|
|
355
378
|
if (storagePlugin?.hooks.upload) {
|
|
356
379
|
const context = {
|
|
357
380
|
files: files.value,
|
|
@@ -359,23 +382,18 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
359
382
|
onProgress,
|
|
360
383
|
emit: getPluginEmitFn(storagePlugin.id)
|
|
361
384
|
};
|
|
362
|
-
|
|
385
|
+
const result = await storagePlugin.hooks.upload(processedFile, context);
|
|
386
|
+
uploadResult = result;
|
|
387
|
+
remoteUrl = result.url;
|
|
363
388
|
} else {
|
|
364
389
|
uploadResult = await uploadFn(processedFile, onProgress);
|
|
390
|
+
remoteUrl = typeof uploadResult === "string" ? uploadResult : void 0;
|
|
365
391
|
}
|
|
366
|
-
const remoteUrl = uploadResult?.url;
|
|
367
392
|
const currentFile = files.value.find((f) => f.id === processedFile.id);
|
|
368
393
|
const preview = currentFile?.preview || remoteUrl;
|
|
369
394
|
updateFile(processedFile.id, { status: "complete", uploadResult, remoteUrl, preview });
|
|
370
395
|
} catch (err) {
|
|
371
|
-
const error =
|
|
372
|
-
message: err instanceof Error ? err.message : "Unknown upload error",
|
|
373
|
-
details: {
|
|
374
|
-
fileName: file.name,
|
|
375
|
-
fileSize: file.size,
|
|
376
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
377
|
-
}
|
|
378
|
-
};
|
|
396
|
+
const error = createFileError(file, err);
|
|
379
397
|
updateFile(file.id, { status: "error", error });
|
|
380
398
|
emitter.emit("file:error", { file, error });
|
|
381
399
|
}
|
|
@@ -384,8 +402,15 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
|
|
|
384
402
|
emitter.emit("upload:complete", completed);
|
|
385
403
|
};
|
|
386
404
|
const reset = () => {
|
|
405
|
+
cleanupObjectURLs(createdObjectURLs);
|
|
387
406
|
files.value = [];
|
|
388
407
|
};
|
|
408
|
+
onBeforeUnmount(() => {
|
|
409
|
+
createdObjectURLs.forEach((url) => {
|
|
410
|
+
URL.revokeObjectURL(url);
|
|
411
|
+
});
|
|
412
|
+
createdObjectURLs.clear();
|
|
413
|
+
});
|
|
389
414
|
const callPluginHook = async (hook, stage, file, context) => {
|
|
390
415
|
switch (stage) {
|
|
391
416
|
case "validate":
|
|
@@ -52,5 +52,5 @@ interface ImageCompressorOptions {
|
|
|
52
52
|
*/
|
|
53
53
|
preserveMetadata?: boolean;
|
|
54
54
|
}
|
|
55
|
-
export declare const PluginImageCompressor: (options: ImageCompressorOptions) => import("../types.js").
|
|
55
|
+
export declare const PluginImageCompressor: (options: ImageCompressorOptions) => import("../types.js").ProcessingPlugin<any, ImageCompressorEvents>;
|
|
56
56
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const PluginImageCompressor =
|
|
1
|
+
import { defineProcessingPlugin } from "../types.js";
|
|
2
|
+
export const PluginImageCompressor = defineProcessingPlugin((pluginOptions) => {
|
|
3
3
|
const {
|
|
4
4
|
maxWidth = 1920,
|
|
5
5
|
maxHeight = 1920,
|
|
@@ -30,6 +30,17 @@ export interface AzureDataLakeOptions {
|
|
|
30
30
|
* @default true
|
|
31
31
|
*/
|
|
32
32
|
autoCreateDirectory?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Number of retry attempts for failed operations
|
|
35
|
+
* @default 3
|
|
36
|
+
*/
|
|
37
|
+
retries?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Initial delay between retries in milliseconds
|
|
40
|
+
* Uses exponential backoff: delay * (2 ^ attempt)
|
|
41
|
+
* @default 1000 (1 second)
|
|
42
|
+
*/
|
|
43
|
+
retryDelay?: number;
|
|
33
44
|
}
|
|
34
45
|
export interface AzureUploadResult {
|
|
35
46
|
/**
|
|
@@ -41,4 +52,4 @@ export interface AzureUploadResult {
|
|
|
41
52
|
*/
|
|
42
53
|
blobPath: string;
|
|
43
54
|
}
|
|
44
|
-
export declare const PluginAzureDataLake: (options: AzureDataLakeOptions) => import("../../types.js").
|
|
55
|
+
export declare const PluginAzureDataLake: (options: AzureDataLakeOptions) => import("../../types.js").StoragePlugin<AzureUploadResult, Record<string, never>>;
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import { ref } from "vue";
|
|
2
2
|
import { DataLakeDirectoryClient } from "@azure/storage-file-datalake";
|
|
3
|
-
import {
|
|
4
|
-
export const PluginAzureDataLake =
|
|
3
|
+
import { defineStoragePlugin } from "../../types.js";
|
|
4
|
+
export const PluginAzureDataLake = defineStoragePlugin((options) => {
|
|
5
5
|
const sasURL = ref(options.sasURL || "");
|
|
6
6
|
let refreshPromise = null;
|
|
7
7
|
const directoryCheckedCache = /* @__PURE__ */ new Set();
|
|
8
|
+
const maxRetries = options.retries ?? 3;
|
|
9
|
+
const initialRetryDelay = options.retryDelay ?? 1e3;
|
|
10
|
+
async function withRetry(operation, operationName) {
|
|
11
|
+
let lastError;
|
|
12
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
13
|
+
try {
|
|
14
|
+
return await operation();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
lastError = error;
|
|
17
|
+
if (attempt === maxRetries) {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
const delay = initialRetryDelay * Math.pow(2, attempt);
|
|
21
|
+
if (import.meta.dev) {
|
|
22
|
+
console.warn(
|
|
23
|
+
`[Azure Storage] ${operationName} failed (attempt ${attempt + 1}/${maxRetries + 1}). Retrying in ${delay}ms...`,
|
|
24
|
+
error
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`[Azure Storage] ${operationName} failed after ${maxRetries + 1} attempts: ${lastError?.message}`);
|
|
31
|
+
}
|
|
8
32
|
if (options.getSASUrl && !options.sasURL) {
|
|
9
33
|
options.getSASUrl().then((url) => {
|
|
10
34
|
sasURL.value = url;
|
|
@@ -61,46 +85,52 @@ export const PluginAzureDataLake = defineUploaderPlugin((options) => {
|
|
|
61
85
|
if (file.source !== "local" || file.data === null) {
|
|
62
86
|
throw new Error("Cannot upload remote file - no local data available");
|
|
63
87
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
return withRetry(async () => {
|
|
89
|
+
const fileClient = await getFileClient(file.id);
|
|
90
|
+
await fileClient.upload(file.data, {
|
|
91
|
+
metadata: {
|
|
92
|
+
...options.metadata,
|
|
93
|
+
mimeType: file.mimeType,
|
|
94
|
+
size: String(file.size),
|
|
95
|
+
originalName: file.name
|
|
96
|
+
},
|
|
97
|
+
pathHttpHeaders: {
|
|
98
|
+
...options.pathHttpHeaders,
|
|
99
|
+
contentType: file.mimeType
|
|
100
|
+
},
|
|
101
|
+
onProgress: ({ loadedBytes }) => {
|
|
102
|
+
const uploadedPercentage = Math.round(loadedBytes / file.size * 100);
|
|
103
|
+
context.onProgress(uploadedPercentage);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
url: fileClient.url,
|
|
108
|
+
blobPath: fileClient.name
|
|
109
|
+
};
|
|
110
|
+
}, `Upload file "${file.name}"`);
|
|
85
111
|
},
|
|
86
112
|
/**
|
|
87
113
|
* Get remote file metadata from Azure
|
|
88
114
|
*/
|
|
89
115
|
async getRemoteFile(fileId, _context) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
return withRetry(async () => {
|
|
117
|
+
const fileClient = await getFileClient(fileId);
|
|
118
|
+
const properties = await fileClient.getProperties();
|
|
119
|
+
return {
|
|
120
|
+
size: properties.contentLength || 0,
|
|
121
|
+
mimeType: properties.contentType || "application/octet-stream",
|
|
122
|
+
remoteUrl: fileClient.url
|
|
123
|
+
};
|
|
124
|
+
}, `Get remote file "${fileId}"`);
|
|
97
125
|
},
|
|
98
126
|
/**
|
|
99
127
|
* Delete file from Azure Blob Storage
|
|
100
128
|
*/
|
|
101
129
|
async remove(file, _context) {
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
return withRetry(async () => {
|
|
131
|
+
const fileClient = await getFileClient(file.id);
|
|
132
|
+
await fileClient.deleteIfExists();
|
|
133
|
+
}, `Delete file "${file.name}"`);
|
|
104
134
|
}
|
|
105
135
|
}
|
|
106
136
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
interface ThumbnailGeneratorOptions {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
maxWidth?: number;
|
|
3
|
+
maxHeight?: number;
|
|
4
4
|
quality?: number;
|
|
5
|
+
videoCaptureTime?: number;
|
|
5
6
|
}
|
|
6
|
-
export declare const PluginThumbnailGenerator: (options: ThumbnailGeneratorOptions) => import("../types.js").
|
|
7
|
+
export declare const PluginThumbnailGenerator: (options: ThumbnailGeneratorOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
|
|
7
8
|
export {};
|
|
@@ -1,66 +1,97 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { defineProcessingPlugin } from "../types.js";
|
|
2
|
+
import { calculateThumbnailDimensions } from "../utils.js";
|
|
3
|
+
export const PluginThumbnailGenerator = defineProcessingPlugin((pluginOptions) => {
|
|
3
4
|
return {
|
|
4
5
|
id: "thumbnail-generator",
|
|
5
6
|
hooks: {
|
|
6
|
-
preprocess: async (file,
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (ctx) {
|
|
29
|
-
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
30
|
-
const thumbnailPreviewUrl = canvas.toDataURL("image/jpeg", quality);
|
|
31
|
-
file.preview = thumbnailPreviewUrl;
|
|
32
|
-
}
|
|
33
|
-
} else if (file.mimeType.startsWith("video/")) {
|
|
34
|
-
const video = document.createElement("video");
|
|
35
|
-
video.src = sourceUrl;
|
|
36
|
-
video.crossOrigin = "anonymous";
|
|
37
|
-
video.currentTime = 1;
|
|
38
|
-
await new Promise((resolve) => {
|
|
39
|
-
video.onloadeddata = () => {
|
|
40
|
-
video.onseeked = resolve;
|
|
41
|
-
video.currentTime = 1;
|
|
42
|
-
};
|
|
43
|
-
});
|
|
44
|
-
const aspectRatio = video.videoWidth / video.videoHeight;
|
|
45
|
-
let targetWidth = width;
|
|
46
|
-
let targetHeight = height;
|
|
47
|
-
if (aspectRatio > 1) {
|
|
48
|
-
targetHeight = width / aspectRatio;
|
|
49
|
-
} else {
|
|
50
|
-
targetWidth = height * aspectRatio;
|
|
51
|
-
}
|
|
52
|
-
const canvas = document.createElement("canvas");
|
|
53
|
-
canvas.width = targetWidth;
|
|
54
|
-
canvas.height = targetHeight;
|
|
55
|
-
const ctx = canvas.getContext("2d");
|
|
56
|
-
if (ctx) {
|
|
57
|
-
ctx.drawImage(video, 0, 0, targetWidth, targetHeight);
|
|
58
|
-
const thumbnailPreviewUrl = canvas.toDataURL("image/jpeg", quality);
|
|
59
|
-
file.preview = thumbnailPreviewUrl;
|
|
7
|
+
preprocess: async (file, context) => {
|
|
8
|
+
const { maxWidth = 200, maxHeight = 200, quality = 0.7, videoCaptureTime = 1 } = pluginOptions;
|
|
9
|
+
if (!file.mimeType.startsWith("image/") && !file.mimeType.startsWith("video/")) {
|
|
10
|
+
return file;
|
|
11
|
+
}
|
|
12
|
+
if (file.mimeType === "image/gif") {
|
|
13
|
+
return file;
|
|
14
|
+
}
|
|
15
|
+
if (file.mimeType === "image/svg+xml") {
|
|
16
|
+
return file;
|
|
17
|
+
}
|
|
18
|
+
if (file.source !== "local" || !file.data) {
|
|
19
|
+
return file;
|
|
20
|
+
}
|
|
21
|
+
const sourceUrl = URL.createObjectURL(file.data);
|
|
22
|
+
try {
|
|
23
|
+
if (file.mimeType.startsWith("image/")) {
|
|
24
|
+
const thumbnailUrl = await generateImageThumbnail(sourceUrl, maxWidth, maxHeight, quality);
|
|
25
|
+
file.meta.thumbnail = thumbnailUrl;
|
|
26
|
+
} else if (file.mimeType.startsWith("video/")) {
|
|
27
|
+
const thumbnailUrl = await generateVideoThumbnail(sourceUrl, maxWidth, maxHeight, quality, videoCaptureTime);
|
|
28
|
+
file.meta.thumbnail = thumbnailUrl;
|
|
60
29
|
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn(`[ThumbnailGenerator] Failed for ${file.name}:`, error);
|
|
32
|
+
} finally {
|
|
33
|
+
URL.revokeObjectURL(sourceUrl);
|
|
61
34
|
}
|
|
62
35
|
return file;
|
|
63
36
|
}
|
|
64
37
|
}
|
|
65
38
|
};
|
|
66
39
|
});
|
|
40
|
+
async function generateImageThumbnail(sourceUrl, maxWidth, maxHeight, quality) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const image = new Image();
|
|
43
|
+
image.onload = () => {
|
|
44
|
+
try {
|
|
45
|
+
const { width, height } = calculateThumbnailDimensions(image.width, image.height, maxWidth, maxHeight);
|
|
46
|
+
const canvas = document.createElement("canvas");
|
|
47
|
+
canvas.width = width;
|
|
48
|
+
canvas.height = height;
|
|
49
|
+
const ctx = canvas.getContext("2d");
|
|
50
|
+
if (!ctx) {
|
|
51
|
+
throw new Error("Failed to get canvas context");
|
|
52
|
+
}
|
|
53
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
54
|
+
const thumbnailUrl = canvas.toDataURL("image/jpeg", quality);
|
|
55
|
+
resolve(thumbnailUrl);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
reject(error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
image.onerror = () => {
|
|
61
|
+
reject(new Error("Failed to load image"));
|
|
62
|
+
};
|
|
63
|
+
image.src = sourceUrl;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function generateVideoThumbnail(sourceUrl, maxWidth, maxHeight, quality, captureTime) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const video = document.createElement("video");
|
|
69
|
+
video.preload = "metadata";
|
|
70
|
+
video.muted = true;
|
|
71
|
+
video.onloadedmetadata = () => {
|
|
72
|
+
const seekTime = Math.min(captureTime, video.duration * 0.1);
|
|
73
|
+
video.currentTime = seekTime;
|
|
74
|
+
};
|
|
75
|
+
video.onseeked = () => {
|
|
76
|
+
try {
|
|
77
|
+
const { width, height } = calculateThumbnailDimensions(video.videoWidth, video.videoHeight, maxWidth, maxHeight);
|
|
78
|
+
const canvas = document.createElement("canvas");
|
|
79
|
+
canvas.width = width;
|
|
80
|
+
canvas.height = height;
|
|
81
|
+
const ctx = canvas.getContext("2d");
|
|
82
|
+
if (!ctx) {
|
|
83
|
+
throw new Error("Failed to get canvas context");
|
|
84
|
+
}
|
|
85
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
86
|
+
const thumbnailUrl = canvas.toDataURL("image/jpeg", quality);
|
|
87
|
+
resolve(thumbnailUrl);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
reject(error);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
video.onerror = () => {
|
|
93
|
+
reject(new Error("Failed to load video"));
|
|
94
|
+
};
|
|
95
|
+
video.src = sourceUrl;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -68,5 +68,5 @@ type VideoCompressorEvents = {
|
|
|
68
68
|
error: Error;
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
|
-
export declare const PluginVideoCompressor: (options: VideoCompressorOptions) => import("../types.js").
|
|
71
|
+
export declare const PluginVideoCompressor: (options: VideoCompressorOptions) => import("../types.js").ProcessingPlugin<any, VideoCompressorEvents>;
|
|
72
72
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineProcessingPlugin } from "../types.js";
|
|
2
2
|
import { useFFMpeg } from "../../useFFMpeg.js";
|
|
3
|
-
import {
|
|
4
|
-
export const PluginVideoCompressor =
|
|
3
|
+
import { watch } from "vue";
|
|
4
|
+
export const PluginVideoCompressor = defineProcessingPlugin((pluginOptions = {}) => {
|
|
5
5
|
return {
|
|
6
6
|
id: "video-compressor",
|
|
7
7
|
hooks: {
|
|
@@ -32,14 +32,19 @@ export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions = {}) =
|
|
|
32
32
|
});
|
|
33
33
|
return file;
|
|
34
34
|
}
|
|
35
|
+
let inputUrl;
|
|
36
|
+
let stopProgressWatch;
|
|
35
37
|
try {
|
|
36
38
|
context.emit("start", { file, originalSize: file.size });
|
|
37
|
-
|
|
39
|
+
inputUrl = URL.createObjectURL(file.data);
|
|
38
40
|
const ffmpeg = useFFMpeg({ inputUrl });
|
|
39
41
|
await ffmpeg.load();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
stopProgressWatch = watch(
|
|
43
|
+
() => ffmpeg.progress.value,
|
|
44
|
+
(progress) => {
|
|
45
|
+
context.emit("progress", { file, percentage: Math.round(progress * 100) });
|
|
46
|
+
}
|
|
47
|
+
);
|
|
43
48
|
const convertOptions = ["-c:v", codec];
|
|
44
49
|
if (bitrate) {
|
|
45
50
|
convertOptions.push("-b:v", bitrate);
|
|
@@ -92,6 +97,11 @@ export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions = {}) =
|
|
|
92
97
|
context.emit("error", { file, error });
|
|
93
98
|
console.error(`Video compression error for ${file.name}:`, error);
|
|
94
99
|
return file;
|
|
100
|
+
} finally {
|
|
101
|
+
stopProgressWatch?.();
|
|
102
|
+
if (inputUrl) {
|
|
103
|
+
URL.revokeObjectURL(inputUrl);
|
|
104
|
+
}
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
}
|
|
@@ -24,7 +24,7 @@ export interface FileError {
|
|
|
24
24
|
* - 'onedrive': OneDrive picker
|
|
25
25
|
* - ... add more as needed
|
|
26
26
|
*/
|
|
27
|
-
export type FileSource =
|
|
27
|
+
export type FileSource = "local" | "storage" | "instagram" | "dropbox" | "google-drive" | "onedrive";
|
|
28
28
|
/**
|
|
29
29
|
* Base properties shared by both local and remote upload files
|
|
30
30
|
*/
|
|
@@ -86,7 +86,7 @@ export interface BaseUploadFile<TUploadResult = any> {
|
|
|
86
86
|
*/
|
|
87
87
|
export interface LocalUploadFile<TUploadResult = any> extends BaseUploadFile<TUploadResult> {
|
|
88
88
|
/** Always 'local' for files selected from user's device */
|
|
89
|
-
source:
|
|
89
|
+
source: "local";
|
|
90
90
|
/**
|
|
91
91
|
* The actual file data (File from input or Blob)
|
|
92
92
|
* Available for processing, compression, thumbnail generation, etc.
|
|
@@ -125,7 +125,7 @@ export interface RemoteUploadFile<TUploadResult = any> extends BaseUploadFile<TU
|
|
|
125
125
|
* - 'storage': File from your storage (previously uploaded)
|
|
126
126
|
* - 'instagram', 'dropbox', etc.: File from cloud picker
|
|
127
127
|
*/
|
|
128
|
-
source: Exclude<FileSource,
|
|
128
|
+
source: Exclude<FileSource, "local">;
|
|
129
129
|
/**
|
|
130
130
|
* Always null for remote files (no local data available)
|
|
131
131
|
* File exists remotely and is accessed via remoteUrl
|
|
@@ -157,9 +157,38 @@ export type UploadFn<TUploadResult = any> = (file: UploadFile<TUploadResult>, on
|
|
|
157
157
|
export type GetRemoteFileFn = (fileId: string) => Promise<MinimumRemoteFileAttributes>;
|
|
158
158
|
export interface UploadOptions {
|
|
159
159
|
/**
|
|
160
|
-
*
|
|
160
|
+
* Storage plugin for uploading files (only one storage plugin can be active)
|
|
161
|
+
*
|
|
162
|
+
* Storage plugins handle the actual upload, download, and deletion of files
|
|
163
|
+
* from remote storage (Azure, S3, GCS, etc.)
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* storage: PluginAzureDataLake({
|
|
168
|
+
* sasURL: 'https://...',
|
|
169
|
+
* path: 'uploads'
|
|
170
|
+
* })
|
|
171
|
+
* ```
|
|
161
172
|
*/
|
|
162
|
-
|
|
173
|
+
storage?: StoragePlugin<any, any>;
|
|
174
|
+
/**
|
|
175
|
+
* Processing and validation plugins (validators, compressors, etc.)
|
|
176
|
+
*
|
|
177
|
+
* These plugins run during the file lifecycle:
|
|
178
|
+
* - validate: Check file before adding
|
|
179
|
+
* - preprocess: Generate thumbnails/previews immediately
|
|
180
|
+
* - process: Compress/transform before upload
|
|
181
|
+
* - complete: Post-upload processing
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* plugins: [
|
|
186
|
+
* ValidatorMaxFiles({ maxFiles: 10 }),
|
|
187
|
+
* PluginImageCompressor({ quality: 0.8 })
|
|
188
|
+
* ]
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
plugins?: ProcessingPlugin<any, any>[];
|
|
163
192
|
/**
|
|
164
193
|
* Validate maximum number of files
|
|
165
194
|
* - false: disabled
|
|
@@ -219,6 +248,7 @@ export interface ImageCompressionOptions {
|
|
|
219
248
|
type CoreUploaderEvents<TUploadResult = any> = {
|
|
220
249
|
"file:added": Readonly<UploadFile<TUploadResult>>;
|
|
221
250
|
"file:removed": Readonly<UploadFile<TUploadResult>>;
|
|
251
|
+
"file:replaced": Readonly<UploadFile<TUploadResult>>;
|
|
222
252
|
"file:processing": Readonly<UploadFile<TUploadResult>>;
|
|
223
253
|
"file:error": {
|
|
224
254
|
file: Readonly<UploadFile<TUploadResult>>;
|
|
@@ -279,6 +309,26 @@ export type UploadHook<TUploadResult = any, TPluginEvents extends Record<string,
|
|
|
279
309
|
export type GetRemoteFileHook<TPluginEvents extends Record<string, any> = Record<string, never>> = (fileId: string, context: PluginContext<TPluginEvents>) => Promise<MinimumRemoteFileAttributes>;
|
|
280
310
|
export type RemoveHook<TPluginEvents extends Record<string, any> = Record<string, never>> = (file: UploadFile, context: PluginContext<TPluginEvents>) => Promise<void>;
|
|
281
311
|
export type PluginLifecycleStage = "validate" | "preprocess" | "process" | "upload" | "complete";
|
|
312
|
+
/**
|
|
313
|
+
* Processing plugin hooks (validators, compressors, thumbnail generators)
|
|
314
|
+
*/
|
|
315
|
+
export type ProcessingPluginHooks<TPluginEvents extends Record<string, any> = Record<string, never>> = {
|
|
316
|
+
validate?: ValidationHook<TPluginEvents>;
|
|
317
|
+
preprocess?: ProcessingHook<TPluginEvents>;
|
|
318
|
+
process?: ProcessingHook<TPluginEvents>;
|
|
319
|
+
complete?: ProcessingHook<TPluginEvents>;
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Storage plugin hooks (upload, download, delete from remote storage)
|
|
323
|
+
*/
|
|
324
|
+
export type StoragePluginHooks<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> = {
|
|
325
|
+
upload: UploadHook<TUploadResult, TPluginEvents>;
|
|
326
|
+
getRemoteFile?: GetRemoteFileHook<TPluginEvents>;
|
|
327
|
+
remove?: RemoveHook<TPluginEvents>;
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* All possible plugin hooks (for internal use)
|
|
331
|
+
*/
|
|
282
332
|
export type PluginHooks<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> = {
|
|
283
333
|
validate?: ValidationHook<TPluginEvents>;
|
|
284
334
|
preprocess?: ProcessingHook<TPluginEvents>;
|
|
@@ -288,16 +338,89 @@ export type PluginHooks<TUploadResult = any, TPluginEvents extends Record<string
|
|
|
288
338
|
remove?: RemoveHook<TPluginEvents>;
|
|
289
339
|
complete?: ProcessingHook<TPluginEvents>;
|
|
290
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* Processing plugin (validators, compressors, thumbnail generators)
|
|
343
|
+
*
|
|
344
|
+
* These plugins transform or validate files without handling storage.
|
|
345
|
+
*/
|
|
346
|
+
export interface ProcessingPlugin<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> {
|
|
347
|
+
id: string;
|
|
348
|
+
hooks: ProcessingPluginHooks<TPluginEvents>;
|
|
349
|
+
options?: UploadOptions;
|
|
350
|
+
events?: TPluginEvents;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Storage plugin (Azure, S3, GCS, etc.)
|
|
354
|
+
*
|
|
355
|
+
* Storage plugins handle uploading, downloading, and deleting files from remote storage.
|
|
356
|
+
* Only one storage plugin can be active at a time.
|
|
357
|
+
*/
|
|
358
|
+
export interface StoragePlugin<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> {
|
|
359
|
+
id: string;
|
|
360
|
+
hooks: StoragePluginHooks<TUploadResult, TPluginEvents>;
|
|
361
|
+
options?: UploadOptions;
|
|
362
|
+
events?: TPluginEvents;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Base plugin interface (for internal use - supports both types)
|
|
366
|
+
*/
|
|
291
367
|
export interface Plugin<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> {
|
|
292
368
|
id: string;
|
|
293
369
|
hooks: PluginHooks<TUploadResult, TPluginEvents>;
|
|
294
370
|
options?: UploadOptions;
|
|
295
371
|
events?: TPluginEvents;
|
|
296
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Define a processing plugin (validators, compressors, thumbnail generators)
|
|
375
|
+
*
|
|
376
|
+
* @example Validator
|
|
377
|
+
* ```typescript
|
|
378
|
+
* export const ValidatorMaxFiles = defineProcessingPlugin<ValidatorOptions>((options) => ({
|
|
379
|
+
* id: 'validator-max-files',
|
|
380
|
+
* hooks: {
|
|
381
|
+
* validate: async (file, context) => {
|
|
382
|
+
* if (context.files.length >= options.maxFiles) {
|
|
383
|
+
* throw { message: 'Too many files' }
|
|
384
|
+
* }
|
|
385
|
+
* return file
|
|
386
|
+
* }
|
|
387
|
+
* }
|
|
388
|
+
* }))
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
export declare function defineProcessingPlugin<TPluginOptions = unknown, TPluginEvents extends Record<string, any> = Record<string, never>>(factory: (options: TPluginOptions) => ProcessingPlugin<any, TPluginEvents>): (options: TPluginOptions) => ProcessingPlugin<any, TPluginEvents>;
|
|
392
|
+
/**
|
|
393
|
+
* Define a storage plugin (Azure, S3, GCS, etc.)
|
|
394
|
+
*
|
|
395
|
+
* Storage plugins MUST implement the `upload` hook and should return an object with a `url` property.
|
|
396
|
+
*
|
|
397
|
+
* @example Azure Storage
|
|
398
|
+
* ```typescript
|
|
399
|
+
* export const PluginAzureDataLake = defineStoragePlugin<AzureOptions, AzureEvents>((options) => ({
|
|
400
|
+
* id: 'azure-datalake-storage',
|
|
401
|
+
* hooks: {
|
|
402
|
+
* upload: async (file, context) => {
|
|
403
|
+
* const fileClient = await getFileClient(file.id)
|
|
404
|
+
* await fileClient.upload(file.data, { onProgress: context.onProgress })
|
|
405
|
+
* return { url: fileClient.url, blobPath: fileClient.name }
|
|
406
|
+
* },
|
|
407
|
+
* getRemoteFile: async (fileId, context) => {
|
|
408
|
+
* // ... fetch file metadata ...
|
|
409
|
+
* },
|
|
410
|
+
* remove: async (file, context) => {
|
|
411
|
+
* // ... delete file ...
|
|
412
|
+
* }
|
|
413
|
+
* }
|
|
414
|
+
* }))
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
export declare function defineStoragePlugin<TPluginOptions = unknown, TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>>(factory: (options: TPluginOptions) => StoragePlugin<TUploadResult, TPluginEvents>): (options: TPluginOptions) => StoragePlugin<TUploadResult, TPluginEvents>;
|
|
297
418
|
/**
|
|
298
419
|
* Define an uploader plugin with type safety, context access, and custom events.
|
|
299
420
|
* This is the universal plugin factory for all plugin types (storage, validators, processors).
|
|
300
421
|
*
|
|
422
|
+
* @deprecated Use defineProcessingPlugin or defineStoragePlugin instead for better type safety
|
|
423
|
+
*
|
|
301
424
|
* Hooks receive context as a parameter, making it clear when context is available.
|
|
302
425
|
* Context includes current files, options, and an emit function for custom events.
|
|
303
426
|
*
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PluginContext, UploadFile, FileError, UploadOptions } from "./types.js";
|
|
2
|
+
import type { Emitter } from "mitt";
|
|
3
|
+
/**
|
|
4
|
+
* Create a plugin context object with consistent structure
|
|
5
|
+
*/
|
|
6
|
+
export declare function createPluginContext<TPluginEvents extends Record<string, any> = Record<string, never>>(pluginId: string, files: UploadFile[], options: UploadOptions, emitter: Emitter<any>): PluginContext<TPluginEvents>;
|
|
7
|
+
/**
|
|
8
|
+
* Create a consistent file error object
|
|
9
|
+
*/
|
|
10
|
+
export declare function createFileError(file: UploadFile, error: unknown): FileError;
|
|
11
|
+
/**
|
|
12
|
+
* Calculate thumbnail dimensions while maintaining aspect ratio
|
|
13
|
+
*/
|
|
14
|
+
export declare function calculateThumbnailDimensions(originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number): {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Cleanup object URLs to prevent memory leaks
|
|
20
|
+
* @param urlMap Map of file IDs to object URLs
|
|
21
|
+
* @param fileId Optional file ID to cleanup specific URL, or cleanup all if not provided
|
|
22
|
+
*/
|
|
23
|
+
export declare function cleanupObjectURLs(urlMap: Map<string, string>, fileId?: string): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function createPluginContext(pluginId, files, options, emitter) {
|
|
2
|
+
return {
|
|
3
|
+
files,
|
|
4
|
+
options,
|
|
5
|
+
emit: (event, payload) => {
|
|
6
|
+
const prefixedEvent = `${pluginId}:${String(event)}`;
|
|
7
|
+
emitter.emit(prefixedEvent, payload);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function createFileError(file, error) {
|
|
12
|
+
return {
|
|
13
|
+
message: error instanceof Error ? error.message : String(error),
|
|
14
|
+
details: {
|
|
15
|
+
fileName: file.name,
|
|
16
|
+
fileSize: file.size,
|
|
17
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function calculateThumbnailDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
|
|
22
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
23
|
+
let width = maxWidth;
|
|
24
|
+
let height = maxHeight;
|
|
25
|
+
if (aspectRatio > 1) {
|
|
26
|
+
height = maxWidth / aspectRatio;
|
|
27
|
+
} else {
|
|
28
|
+
width = maxHeight * aspectRatio;
|
|
29
|
+
}
|
|
30
|
+
return { width, height };
|
|
31
|
+
}
|
|
32
|
+
export function cleanupObjectURLs(urlMap, fileId) {
|
|
33
|
+
if (fileId) {
|
|
34
|
+
const url = urlMap.get(fileId);
|
|
35
|
+
if (url) {
|
|
36
|
+
URL.revokeObjectURL(url);
|
|
37
|
+
urlMap.delete(fileId);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
for (const url of urlMap.values()) {
|
|
41
|
+
URL.revokeObjectURL(url);
|
|
42
|
+
}
|
|
43
|
+
urlMap.clear();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
interface ValidatorAllowedFileTypesOptions {
|
|
2
2
|
allowedFileTypes?: string[];
|
|
3
3
|
}
|
|
4
|
-
export declare const ValidatorAllowedFileTypes: (options: ValidatorAllowedFileTypesOptions) => import("../types.js").
|
|
4
|
+
export declare const ValidatorAllowedFileTypes: (options: ValidatorAllowedFileTypesOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
|
|
5
5
|
export {};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const ValidatorAllowedFileTypes =
|
|
1
|
+
import { defineProcessingPlugin } from "../types.js";
|
|
2
|
+
export const ValidatorAllowedFileTypes = defineProcessingPlugin((options) => {
|
|
3
3
|
return {
|
|
4
4
|
id: "validator-allowed-file-types",
|
|
5
5
|
hooks: {
|
|
6
6
|
validate: async (file, _context) => {
|
|
7
|
-
if (options.allowedFileTypes
|
|
7
|
+
if (!options.allowedFileTypes || options.allowedFileTypes.length === 0) {
|
|
8
8
|
return file;
|
|
9
|
+
}
|
|
10
|
+
if (options.allowedFileTypes.includes(file.mimeType)) {
|
|
11
|
+
return file;
|
|
12
|
+
}
|
|
9
13
|
throw { message: `File type ${file.mimeType} is not allowed` };
|
|
10
14
|
}
|
|
11
15
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
interface
|
|
1
|
+
interface ValidatorMaxFileSizeOptions {
|
|
2
2
|
maxFileSize?: number;
|
|
3
3
|
}
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const ValidatorMaxFileSize: (options: ValidatorMaxFileSizeOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
|
|
5
5
|
export {};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const
|
|
1
|
+
import { defineProcessingPlugin } from "../types.js";
|
|
2
|
+
export const ValidatorMaxFileSize = defineProcessingPlugin((options) => {
|
|
3
3
|
return {
|
|
4
4
|
id: "validator-max-file-size",
|
|
5
5
|
hooks: {
|
|
6
6
|
validate: async (file, _context) => {
|
|
7
|
-
if (options.maxFileSize
|
|
7
|
+
if (!options.maxFileSize || options.maxFileSize === Infinity) {
|
|
8
8
|
return file;
|
|
9
|
+
}
|
|
10
|
+
if (file.size <= options.maxFileSize) {
|
|
11
|
+
return file;
|
|
12
|
+
}
|
|
9
13
|
throw { message: `File size exceeds the maximum limit of ${options.maxFileSize} bytes` };
|
|
10
14
|
}
|
|
11
15
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
interface ValidatorMaxFilesOptions {
|
|
2
2
|
maxFiles?: number;
|
|
3
3
|
}
|
|
4
|
-
export declare const ValidatorMaxFiles: (options: ValidatorMaxFilesOptions) => import("../types.js").
|
|
4
|
+
export declare const ValidatorMaxFiles: (options: ValidatorMaxFilesOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
|
|
5
5
|
export {};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const ValidatorMaxFiles =
|
|
1
|
+
import { defineProcessingPlugin } from "../types.js";
|
|
2
|
+
export const ValidatorMaxFiles = defineProcessingPlugin((options) => {
|
|
3
3
|
return {
|
|
4
4
|
id: "validator-max-files",
|
|
5
5
|
hooks: {
|
|
6
6
|
validate: async (file, context) => {
|
|
7
|
-
if (options.maxFiles
|
|
7
|
+
if (options.maxFiles === void 0 || options.maxFiles === Infinity) {
|
|
8
8
|
return file;
|
|
9
|
+
}
|
|
10
|
+
if (context.files.length < options.maxFiles) {
|
|
11
|
+
return file;
|
|
12
|
+
}
|
|
9
13
|
throw { message: `Maximum number of files (${options.maxFiles}) exceeded` };
|
|
10
14
|
}
|
|
11
15
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ui-elements",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "A collection of beautiful, animated UI components for Nuxt applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/genu/nuxt-ui-elements.git",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"@types/culori": "^4.0.1",
|
|
46
46
|
"@types/node": "latest",
|
|
47
47
|
"@vitest/coverage-v8": "^4.0.17",
|
|
48
|
+
"better-sqlite3": "^12.6.0",
|
|
48
49
|
"changelogen": "^0.6.2",
|
|
49
50
|
"eslint": "^9.39.2",
|
|
50
51
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -73,6 +74,10 @@
|
|
|
73
74
|
"dev": "pnpm dev:prepare && nuxi dev playground",
|
|
74
75
|
"dev:build": "nuxi build playground",
|
|
75
76
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
77
|
+
"docs:dev": "nuxi dev docs",
|
|
78
|
+
"docs:build": "nuxi build docs",
|
|
79
|
+
"docs:generate": "nuxi generate docs",
|
|
80
|
+
"docs:preview": "nuxi preview docs",
|
|
76
81
|
"lint": "eslint .",
|
|
77
82
|
"lint:fix": "eslint . --fix",
|
|
78
83
|
"format": "prettier --write .",
|