|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import type { UploadFile } from "#ui-elements" |
3 | 3 | import { |
4 | | - PluginAzureStorage, |
| 4 | + PluginAzureDataLake, |
5 | 5 | ValidatorMaxFiles, |
6 | | - ValidatorMaxfileSize, |
| 6 | + ValidatorMaxFileSize, |
7 | 7 | ValidatorAllowedFileTypes, |
8 | 8 | PluginThumbnailGenerator, |
9 | 9 | PluginImageCompressor, |
|
15 | 15 | // Configuration state |
16 | 16 | const config = ref({ |
17 | 17 | maxFiles: 5, |
18 | | - maxFileSize: 5 * 1024 * 1024, // 5MB |
19 | | - allowedFileTypes: ["image/jpeg", "image/png", "image/webp"], |
| 18 | + maxFileSize: 100 * 1024 * 1024, // 100MB (to support videos) |
| 19 | + allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "video/mp4", "video/webm"], |
20 | 20 | thumbnails: true, |
21 | 21 | imageCompression: false, |
22 | 22 | autoProceed: false, |
| 23 | + thumbnailMaxWidth: 200, |
| 24 | + thumbnailMaxHeight: 200, |
| 25 | + videoCaptureTime: 1, |
23 | 26 | }) |
24 | 27 |
|
25 | | - // Create uploader with all plugins explicitly defined |
| 28 | + // Create uploader with new storage/processing plugin separation |
26 | 29 | const createUploader = () => { |
27 | | - const plugins = [] |
| 30 | + const processingPlugins = [] |
28 | 31 |
|
29 | 32 | // Validators |
30 | 33 | if (config.value.maxFiles) { |
31 | | - plugins.push(ValidatorMaxFiles({ maxFiles: config.value.maxFiles })) |
| 34 | + processingPlugins.push(ValidatorMaxFiles({ maxFiles: config.value.maxFiles })) |
32 | 35 | } |
33 | 36 |
|
34 | 37 | if (config.value.maxFileSize) { |
35 | | - plugins.push(ValidatorMaxfileSize({ maxFileSize: config.value.maxFileSize })) |
| 38 | + processingPlugins.push(ValidatorMaxFileSize({ maxFileSize: config.value.maxFileSize })) |
36 | 39 | } |
37 | 40 |
|
38 | 41 | if (config.value.allowedFileTypes.length > 0) { |
39 | | - plugins.push(ValidatorAllowedFileTypes({ allowedFileTypes: config.value.allowedFileTypes })) |
| 42 | + processingPlugins.push(ValidatorAllowedFileTypes({ allowedFileTypes: config.value.allowedFileTypes })) |
40 | 43 | } |
41 | 44 |
|
42 | | - // Processors |
| 45 | + // Thumbnails (with improved API) |
43 | 46 | if (config.value.thumbnails) { |
44 | | - plugins.push( |
| 47 | + processingPlugins.push( |
45 | 48 | PluginThumbnailGenerator({ |
46 | | - width: 128, |
47 | | - height: 128, |
48 | | - quality: 1, |
| 49 | + maxWidth: config.value.thumbnailMaxWidth, |
| 50 | + maxHeight: config.value.thumbnailMaxHeight, |
| 51 | + quality: 0.7, |
| 52 | + videoCaptureTime: config.value.videoCaptureTime, |
49 | 53 | }), |
50 | 54 | ) |
51 | 55 | } |
52 | 56 |
|
| 57 | + // Image Compression |
53 | 58 | if (config.value.imageCompression) { |
54 | | - plugins.push( |
| 59 | + processingPlugins.push( |
55 | 60 | PluginImageCompressor({ |
56 | 61 | maxWidth: 1920, |
57 | 62 | maxHeight: 1920, |
|
63 | 68 | ) |
64 | 69 | } |
65 | 70 |
|
66 | | - // Storage (optional) |
67 | | - if (useStoragePlugin.value) { |
68 | | - plugins.push( |
69 | | - PluginAzureStorage({ |
| 71 | + // Storage plugin (NEW: separate from processing plugins) |
| 72 | + const storagePlugin = useStoragePlugin.value |
| 73 | + ? PluginAzureDataLake({ |
70 | 74 | sasURL: "mock://azure-storage", |
71 | 75 | path: "uploads/playground", |
72 | 76 | metadata: { source: "playground" }, |
73 | | - }), |
74 | | - ) |
75 | | - } |
| 77 | + retries: 3, |
| 78 | + retryDelay: 1000, |
| 79 | + }) |
| 80 | + : undefined |
76 | 81 |
|
77 | 82 | return useUploadManager({ |
78 | 83 | autoProceed: config.value.autoProceed, |
79 | | - plugins, |
| 84 | + storage: storagePlugin, // NEW: separate storage option |
| 85 | + plugins: processingPlugins, |
80 | 86 | }) |
81 | 87 | } |
82 | 88 |
|
83 | 89 | // Initialize uploader |
84 | 90 | let uploader = createUploader() |
85 | | - let { files, totalProgress, addFiles, removeFile, clearFiles, upload, onUpload, on } = uploader |
| 91 | + let { files, totalProgress, status, addFiles, removeFile, clearFiles, upload, onUpload, on } = uploader |
86 | 92 |
|
87 | | - // Recreate uploader when storage mode changes |
88 | | - watch(useStoragePlugin, () => { |
| 93 | + // Recreate uploader when config changes |
| 94 | + watch([useStoragePlugin, () => config.value.thumbnails, () => config.value.imageCompression], () => { |
89 | 95 | uploader = createUploader() |
90 | 96 | const newUploader = uploader |
91 | 97 | files = newUploader.files |
92 | 98 | totalProgress = newUploader.totalProgress |
| 99 | + status = newUploader.status |
93 | 100 | addFiles = newUploader.addFiles |
94 | 101 | removeFile = newUploader.removeFile |
95 | 102 | clearFiles = newUploader.clearFiles |
|
113 | 120 |
|
114 | 121 | if (progress >= 100) { |
115 | 122 | clearInterval(interval) |
116 | | - resolve(`https://example.com/uploads/${file.id}`) |
| 123 | + resolve({ url: `https://example.com/uploads/${file.id}` }) |
117 | 124 | } |
118 | 125 | }, 200) |
119 | 126 | }) |
|
123 | 130 | setupEventListeners() |
124 | 131 | } |
125 | 132 |
|
| 133 | + // Get toast for notifications |
| 134 | + const toast = useToast() |
| 135 | +
|
126 | 136 | // Setup event listeners |
127 | 137 | const setupEventListeners = () => { |
128 | 138 | on("file:added", (file) => { |
129 | | - console.log(`File added: ${file.name} (${formatFileSize(file.size)})`) |
| 139 | + console.log(`✅ File added: ${file.name} (${formatFileSize(file.size)})`) |
| 140 | + if (file.meta.thumbnail) { |
| 141 | + console.log(`📸 Thumbnail generated for ${file.name}`) |
| 142 | + } |
| 143 | + }) |
| 144 | +
|
| 145 | + on("file:removed", (file) => { |
| 146 | + console.log(`🗑️ File removed: ${file.name}`) |
130 | 147 | }) |
131 | 148 |
|
132 | 149 | on("file:error", ({ file, error }) => { |
133 | | - console.log(`Error uploading ${file.name}: ${error.message}`) |
| 150 | + console.error(`❌ Error with ${file.name}:`, error.message) |
| 151 | +
|
| 152 | + // Show toast notification for validation errors |
| 153 | + toast.add({ |
| 154 | + title: "File Error", |
| 155 | + description: `${file.name}: ${error.message}`, |
| 156 | + color: "error", |
| 157 | + timeout: 5000, |
| 158 | + }) |
| 159 | + }) |
| 160 | +
|
| 161 | + on("upload:start", () => { |
| 162 | + console.log("🚀 Upload started") |
134 | 163 | }) |
135 | 164 |
|
136 | 165 | on("upload:complete", (files) => { |
137 | | - console.log("Upload complete:", files) |
| 166 | + console.log("✨ Upload complete:", files.length, "files") |
138 | 167 | }) |
139 | 168 |
|
140 | 169 | // Image compression events |
| 170 | + on("image-compressor:start", (payload: any) => { |
| 171 | + console.log("[Compression] Started for", payload.file.name) |
| 172 | + }) |
| 173 | +
|
141 | 174 | on("image-compressor:complete", (payload: any) => { |
142 | | - console.log("[Compression] Completed", payload) |
| 175 | + console.log("[Compression] ✅ Completed", payload.file.name, payload.compressionRatio + "% saved") |
143 | 176 | }) |
144 | 177 |
|
145 | 178 | on("image-compressor:skip", (payload: any) => { |
146 | | - console.log("[Compression] Skipped", payload) |
| 179 | + console.log("[Compression] ⏭️ Skipped", payload.file.name, "-", payload.reason) |
147 | 180 | }) |
148 | 181 | } |
149 | 182 |
|
|
155 | 188 | const handleFileSelect = async (event: Event) => { |
156 | 189 | const input = event.target as HTMLInputElement |
157 | 190 | if (input.files) { |
158 | | - await addFiles(Array.from(input.files)) |
| 191 | + const fileCount = input.files.length |
| 192 | + const addedFiles = await addFiles(Array.from(input.files)) |
| 193 | +
|
| 194 | + // Show summary toast |
| 195 | + const failedCount = fileCount - addedFiles.length |
| 196 | + if (failedCount === 0 && addedFiles.length > 0) { |
| 197 | + toast.add({ |
| 198 | + title: "Files Added", |
| 199 | + description: `Successfully added ${addedFiles.length} file${addedFiles.length > 1 ? "s" : ""}`, |
| 200 | + color: "success", |
| 201 | + timeout: 3000, |
| 202 | + }) |
| 203 | + } else if (failedCount > 0 && addedFiles.length > 0) { |
| 204 | + toast.add({ |
| 205 | + title: "Partial Success", |
| 206 | + description: `Added ${addedFiles.length} file${addedFiles.length > 1 ? "s" : ""}, ${failedCount} failed`, |
| 207 | + color: "warning", |
| 208 | + timeout: 3000, |
| 209 | + }) |
| 210 | + } |
159 | 211 | } |
| 212 | + // Reset input to allow selecting the same file again |
| 213 | + if (input) input.value = "" |
160 | 214 | } |
161 | 215 |
|
162 | 216 | const triggerFileSelect = () => { |
|
187 | 241 | } |
188 | 242 | } |
189 | 243 |
|
| 244 | + const getStatusIcon = (status: string) => { |
| 245 | + switch (status) { |
| 246 | + case "waiting": |
| 247 | + return "i-heroicons-clock" |
| 248 | + case "uploading": |
| 249 | + return "i-heroicons-arrow-up-tray" |
| 250 | + case "complete": |
| 251 | + return "i-heroicons-check-circle" |
| 252 | + case "error": |
| 253 | + return "i-heroicons-x-circle" |
| 254 | + default: |
| 255 | + return "i-heroicons-document" |
| 256 | + } |
| 257 | + } |
| 258 | +
|
190 | 259 | const getCompressionInfo = (file: UploadFile) => { |
191 | 260 | if (file.meta.compressed && typeof file.meta.compressionRatio === "number") { |
192 | | - return `Compressed: ${file.meta.compressionRatio}% saved` |
| 261 | + const saved = file.meta.compressionRatio |
| 262 | + return `Compressed: ${saved}% smaller` |
193 | 263 | } |
194 | 264 | return null |
195 | 265 | } |
|
208 | 278 |
|
209 | 279 | // Available file types for testing |
210 | 280 | const fileTypePresets = [ |
211 | | - { label: "Images", value: ["image/jpeg", "image/png", "image/webp"] }, |
| 281 | + { label: "Images Only", value: ["image/jpeg", "image/png", "image/webp"] }, |
212 | 282 | { label: "Images + GIF", value: ["image/jpeg", "image/png", "image/webp", "image/gif"] }, |
213 | | - { label: "Documents", value: ["application/pdf", "application/msword"] }, |
214 | | - { label: "Videos", value: ["video/mp4", "video/webm"] }, |
| 283 | + { label: "Images + Videos", value: ["image/jpeg", "image/png", "image/webp", "video/mp4", "video/webm"] }, |
| 284 | + { label: "Videos Only", value: ["video/mp4", "video/webm", "video/quicktime"] }, |
| 285 | + { label: "Documents", value: ["application/pdf", "application/msword", "text/plain"] }, |
215 | 286 | { label: "All Files", value: [] }, |
216 | 287 | ] |
217 | 288 |
|
218 | | - const selectedPreset = ref(0) |
| 289 | + const selectedPreset = ref(2) // Images + Videos by default |
219 | 290 | const applyPreset = (index: number) => { |
220 | 291 | selectedPreset.value = index |
221 | 292 | const preset = fileTypePresets[index] |
|
268 | 339 | </div> |
269 | 340 |
|
270 | 341 | <!-- Thumbnails --> |
271 | | - <div> |
| 342 | + <div class="space-y-2"> |
272 | 343 | <label class="flex items-center gap-2"> |
273 | 344 | <UCheckbox v-model="config.thumbnails" /> |
274 | 345 | <span class="text-sm font-medium">Generate Thumbnails</span> |
275 | 346 | </label> |
| 347 | + <div v-if="config.thumbnails" class="ml-6 space-y-2"> |
| 348 | + <div> |
| 349 | + <label class="block text-xs text-gray-600 dark:text-gray-400 mb-1">Max Width (px)</label> |
| 350 | + <UInput v-model.number="config.thumbnailMaxWidth" type="number" min="50" max="500" size="sm" /> |
| 351 | + </div> |
| 352 | + <div> |
| 353 | + <label class="block text-xs text-gray-600 dark:text-gray-400 mb-1">Max Height (px)</label> |
| 354 | + <UInput v-model.number="config.thumbnailMaxHeight" type="number" min="50" max="500" size="sm" /> |
| 355 | + </div> |
| 356 | + <div> |
| 357 | + <label class="block text-xs text-gray-600 dark:text-gray-400 mb-1">Video Capture Time (s)</label> |
| 358 | + <UInput v-model.number="config.videoCaptureTime" type="number" min="0" max="10" step="0.5" size="sm" /> |
| 359 | + </div> |
| 360 | + </div> |
276 | 361 | </div> |
277 | 362 |
|
278 | 363 | <!-- Image Compression --> |
|
343 | 428 |
|
344 | 429 | <!-- File List --> |
345 | 430 | <div class="space-y-3"> |
346 | | - <div v-for="file in files" :key="file.id" class="flex items-start gap-4 p-3 border dark:border-gray-700 rounded-lg"> |
| 431 | + <div v-for="file in files" :key="file.id" class="flex items-start gap-4 p-3 border dark:border-gray-700 rounded-lg hover:border-gray-300 dark:hover:border-gray-600 transition-colors"> |
347 | 432 | <!-- Thumbnail --> |
348 | 433 | <div class="shrink-0"> |
349 | | - <img v-if="file.preview" :src="file.preview" class="w-16 h-16 object-cover rounded" :alt="file.name" /> |
350 | | - <div v-else class="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded flex items-center justify-center"> |
351 | | - <UIcon name="i-heroicons-document" class="w-8 h-8 text-gray-400" /> |
| 434 | + <img v-if="file.meta.thumbnail" :src="file.meta.thumbnail" class="w-20 h-20 object-cover rounded border dark:border-gray-700" :alt="file.name" /> |
| 435 | + <div v-else-if="file.mimeType.startsWith('image/') || file.mimeType.startsWith('video/')" class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded flex items-center justify-center border dark:border-gray-700"> |
| 436 | + <UIcon :name="file.mimeType.startsWith('video/') ? 'i-heroicons-film' : 'i-heroicons-photo'" class="w-10 h-10 text-gray-400" /> |
| 437 | + </div> |
| 438 | + <div v-else class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded flex items-center justify-center border dark:border-gray-700"> |
| 439 | + <UIcon name="i-heroicons-document" class="w-10 h-10 text-gray-400" /> |
352 | 440 | </div> |
353 | 441 | </div> |
354 | 442 |
|
|
357 | 445 | <div class="flex items-start justify-between gap-2 mb-1"> |
358 | 446 | <div class="flex-1 min-w-0"> |
359 | 447 | <p class="font-medium truncate">{{ file.name }}</p> |
360 | | - <p class="text-sm text-gray-600 dark:text-gray-400">{{ formatFileSize(file.size) }} • {{ file.mimeType }}</p> |
| 448 | + <p class="text-sm text-gray-600 dark:text-gray-400"> |
| 449 | + {{ formatFileSize(file.size) }} • {{ file.mimeType }} |
| 450 | + </p> |
361 | 451 | </div> |
362 | | - <UBadge :color="getStatusColor(file.status)"> |
| 452 | + <UBadge :color="getStatusColor(file.status)" :icon="getStatusIcon(file.status)"> |
363 | 453 | {{ file.status }} |
364 | 454 | </UBadge> |
365 | 455 | </div> |
366 | 456 |
|
367 | 457 | <!-- Progress Bar --> |
368 | | - <div v-if="file.status === 'uploading' || file.status === 'complete'" class="mt-2"> |
| 458 | + <div v-if="file.status === 'uploading'" class="mt-2"> |
| 459 | + <div class="flex items-center gap-2 mb-1"> |
| 460 | + <span class="text-xs text-gray-600 dark:text-gray-400">{{ file.progress.percentage }}%</span> |
| 461 | + </div> |
369 | 462 | <UProgress :model-value="file.progress.percentage" /> |
370 | 463 | </div> |
371 | 464 |
|
| 465 | + <div v-else-if="file.status === 'complete'" class="mt-2"> |
| 466 | + <div class="flex items-center gap-1 text-xs text-green-600 dark:text-green-400"> |
| 467 | + <UIcon name="i-heroicons-check-circle" class="w-4 h-4" /> |
| 468 | + <span>Upload complete</span> |
| 469 | + </div> |
| 470 | + </div> |
| 471 | + |
372 | 472 | <!-- Error Message --> |
373 | 473 | <div v-if="file.error" class="mt-2"> |
374 | 474 | <UAlert color="error" variant="soft" :title="file.error.message" /> |
375 | 475 | </div> |
376 | 476 |
|
377 | 477 | <!-- Meta Info --> |
378 | | - <div v-if="getCompressionInfo(file)" class="mt-2 text-xs text-gray-500"> |
379 | | - <span>{{ getCompressionInfo(file) }}</span> |
| 478 | + <div v-if="file.meta.thumbnail || getCompressionInfo(file)" class="mt-2 flex gap-3 text-xs text-gray-500"> |
| 479 | + <span v-if="file.meta.thumbnail" class="flex items-center gap-1"> |
| 480 | + <UIcon name="i-heroicons-photo" class="w-3 h-3" /> |
| 481 | + Thumbnail generated |
| 482 | + </span> |
| 483 | + <span v-if="getCompressionInfo(file)" class="flex items-center gap-1"> |
| 484 | + <UIcon name="i-heroicons-archive-box-arrow-down" class="w-3 h-3" /> |
| 485 | + {{ getCompressionInfo(file) }} |
| 486 | + </span> |
380 | 487 | </div> |
381 | 488 | </div> |
382 | 489 |
|
|
0 commit comments