|
381 | 381 | // return supportsDocuments(currentModel); |
382 | 382 | }); |
383 | 383 |
|
| 384 | + // Helper to check if file is an image (by MIME type or extension) |
| 385 | + // Chrome Android often returns empty file.type for images from gallery |
| 386 | + const IMAGE_EXTENSIONS = [ |
| 387 | + 'jpg', |
| 388 | + 'jpeg', |
| 389 | + 'png', |
| 390 | + 'gif', |
| 391 | + 'webp', |
| 392 | + 'bmp', |
| 393 | + 'svg', |
| 394 | + 'heic', |
| 395 | + 'heif', |
| 396 | + 'avif', |
| 397 | + ]; |
| 398 | + const EXTENSION_TO_MIME: Record<string, string> = { |
| 399 | + jpg: 'image/jpeg', |
| 400 | + jpeg: 'image/jpeg', |
| 401 | + png: 'image/png', |
| 402 | + gif: 'image/gif', |
| 403 | + webp: 'image/webp', |
| 404 | + bmp: 'image/bmp', |
| 405 | + svg: 'image/svg+xml', |
| 406 | + heic: 'image/heic', |
| 407 | + heif: 'image/heif', |
| 408 | + avif: 'image/avif', |
| 409 | + }; |
| 410 | +
|
| 411 | + function isImageFile(f: File): boolean { |
| 412 | + if (f.type.startsWith('image/')) return true; |
| 413 | + // Fallback: check extension for Chrome Android where MIME type may be empty |
| 414 | + const ext = f.name.split('.').pop()?.toLowerCase(); |
| 415 | + return ext ? IMAGE_EXTENSIONS.includes(ext) : false; |
| 416 | + } |
| 417 | +
|
| 418 | + function getImageMimeType(f: File): string { |
| 419 | + if (f.type) return f.type; |
| 420 | + // Derive MIME type from extension for Chrome Android |
| 421 | + const ext = f.name.split('.').pop()?.toLowerCase(); |
| 422 | + return ext ? (EXTENSION_TO_MIME[ext] ?? 'image/jpeg') : 'image/jpeg'; |
| 423 | + } |
| 424 | +
|
384 | 425 | async function handleFilesSelect(files: File[]) { |
385 | 426 | if (!files.length || !session.current?.session.token) return; |
386 | 427 |
|
387 | | - const imageFiles = files.filter((f) => f.type.startsWith('image/')); |
388 | | - const documentFiles = files.filter((f) => !f.type.startsWith('image/')); |
| 428 | + const imageFiles = files.filter(isImageFile); |
| 429 | + const documentFiles = files.filter((f) => !isImageFile(f)); |
389 | 430 |
|
390 | 431 | if (imageFiles.length > 0) { |
391 | 432 | isUploading = true; |
392 | 433 | const uploadedImages: { url: string; storage_id: string; fileName?: string }[] = []; |
393 | 434 | try { |
394 | 435 | for (const file of imageFiles) { |
| 436 | + const mimeType = getImageMimeType(file); |
395 | 437 | const compressedFile = await compressImage(file, 1024 * 1024); |
396 | 438 | const uploadResult = await fetch('/api/storage', { |
397 | 439 | method: 'POST', |
398 | | - headers: { 'Content-Type': file.type }, |
| 440 | + headers: { 'Content-Type': mimeType }, |
399 | 441 | credentials: 'include', |
400 | 442 | body: compressedFile, |
401 | 443 | }); |
|
965 | 1007 | {/if} |
966 | 1008 | <div class="relative flex flex-grow flex-row items-start"> |
967 | 1009 | <input |
968 | | - {...fileUpload.input} |
969 | | - bind:this={fileInput} |
970 | | - oninput={(e) => { |
971 | | - // Fallback for Chrome Mobile Android where onchange may not fire for images |
972 | | - const input = e.currentTarget as HTMLInputElement; |
973 | | - if (input.files && input.files.length > 0) { |
974 | | - handleFilesSelect(Array.from(input.files)); |
975 | | - input.value = ''; // Clear to allow re-selection of same file |
976 | | - } |
977 | | - }} |
978 | | - /> |
| 1010 | + {...fileUpload.input} |
| 1011 | + bind:this={fileInput} |
| 1012 | + oninput={(e) => { |
| 1013 | + // Fallback for Chrome Mobile Android where onchange may not fire for images |
| 1014 | + const input = e.currentTarget as HTMLInputElement; |
| 1015 | + if (input.files && input.files.length > 0) { |
| 1016 | + handleFilesSelect(Array.from(input.files)); |
| 1017 | + input.value = ''; // Clear to allow re-selection of same file |
| 1018 | + } |
| 1019 | + }} |
| 1020 | + /> |
979 | 1021 | <!-- svelte-ignore a11y_autofocus --> |
980 | 1022 | <textarea |
981 | 1023 | style={popover.trigger.style} |
|
0 commit comments