mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	feat(web): allow uploading more file types (#1570)
* feat(web): allow uploading more file types * fix(web): make filename extension lowercase
This commit is contained in:
		
							parent
							
								
									8c20d8cb3d
								
							
						
					
					
						commit
						adb265794c
					
				| @ -0,0 +1,69 @@ | ||||
| <script lang="ts"> | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import { asByteUnitString } from '$lib/utils/byte-units'; | ||||
| 	import { UploadAsset } from '$lib/models/upload-asset'; | ||||
| 
 | ||||
| 	export let uploadAsset: UploadAsset; | ||||
| 
 | ||||
| 	let showFallbackImage = false; | ||||
| 	const previewURL = URL.createObjectURL(uploadAsset.file); | ||||
| </script> | ||||
| 
 | ||||
| <div | ||||
| 	in:fade={{ duration: 250 }} | ||||
| 	out:fade={{ duration: 100 }} | ||||
| 	class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]" | ||||
| > | ||||
| 	<div class="relative"> | ||||
| 		{#if showFallbackImage} | ||||
| 			<img | ||||
| 				in:fade={{ duration: 250 }} | ||||
| 				src="immich-logo.svg" | ||||
| 				alt="Immich Logo" | ||||
| 				class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg" | ||||
| 				draggable="false" | ||||
| 			/> | ||||
| 		{:else} | ||||
| 			<img | ||||
| 				in:fade={{ duration: 250 }} | ||||
| 				on:load={() => { | ||||
| 					URL.revokeObjectURL(previewURL); | ||||
| 				}} | ||||
| 				on:error={() => { | ||||
| 					URL.revokeObjectURL(previewURL); | ||||
| 					showFallbackImage = true; | ||||
| 				}} | ||||
| 				src={previewURL} | ||||
| 				alt="Preview of asset" | ||||
| 				class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg" | ||||
| 				draggable="false" | ||||
| 			/> | ||||
| 		{/if} | ||||
| 
 | ||||
| 		<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30"> | ||||
| 			<p | ||||
| 				class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase" | ||||
| 			> | ||||
| 				.{uploadAsset.fileExtension} | ||||
| 			</p> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="p-2 pr-4 flex flex-col justify-between"> | ||||
| 		<input | ||||
| 			disabled | ||||
| 			class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2" | ||||
| 			value={`[${asByteUnitString(uploadAsset.file.size)}] ${uploadAsset.file.name}`} | ||||
| 		/> | ||||
| 
 | ||||
| 		<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative"> | ||||
| 			<div | ||||
| 				class="bg-immich-primary h-[15px] rounded-md transition-all" | ||||
| 				style={`width: ${uploadAsset.progress}%`} | ||||
| 			/> | ||||
| 			<p class="absolute h-full w-full text-center top-0 text-[10px] "> | ||||
| 				{uploadAsset.progress}/100 | ||||
| 			</p> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @ -4,55 +4,20 @@ | ||||
| 	import { uploadAssetsStore } from '$lib/stores/upload'; | ||||
| 	import CloudUploadOutline from 'svelte-material-icons/CloudUploadOutline.svelte'; | ||||
| 	import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte'; | ||||
| 	import type { UploadAsset } from '$lib/models/upload-asset'; | ||||
| 	import { notificationController, NotificationType } from './notification/notification'; | ||||
| 	import { asByteUnitString } from '$lib/utils/byte-units'; | ||||
| 	import UploadAssetPreview from './upload-asset-preview.svelte'; | ||||
| 
 | ||||
| 	let showDetail = true; | ||||
| 
 | ||||
| 	let uploadLength = 0; | ||||
| 	let isUploading = false; | ||||
| 
 | ||||
| 	const showUploadImageThumbnail = async (a: UploadAsset) => { | ||||
| 		const extension = a.fileExtension.toLowerCase(); | ||||
| 
 | ||||
| 		if (extension == 'jpeg' || extension == 'jpg' || extension == 'png') { | ||||
| 			try { | ||||
| 				const imgData = await a.file.arrayBuffer(); | ||||
| 				const arrayBufferView = new Uint8Array(imgData); | ||||
| 				const blob = new Blob([arrayBufferView], { type: 'image/jpeg' }); | ||||
| 				const urlCreator = window.URL || window.webkitURL; | ||||
| 				const imageUrl = urlCreator.createObjectURL(blob); | ||||
| 				// TODO: There is probably a cleaner way of doing this | ||||
| 				// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| 				const img: any = document.getElementById(`${a.id}`); | ||||
| 				img.src = imageUrl; | ||||
| 			} catch { | ||||
| 				// Do nothing? | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	// Reactive action to get thumbnail image of upload asset whenever there is a new one added to the list | ||||
| 	// Reactive action to update asset uploadLength whenever there is a new one added to the list | ||||
| 	$: { | ||||
| 		if ($uploadAssetsStore.length != uploadLength) { | ||||
| 			$uploadAssetsStore.map((asset) => { | ||||
| 				showUploadImageThumbnail(asset); | ||||
| 			}); | ||||
| 
 | ||||
| 			uploadLength = $uploadAssetsStore.length; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	$: { | ||||
| 		if (showDetail) { | ||||
| 			$uploadAssetsStore.map((asset) => { | ||||
| 				showUploadImageThumbnail(asset); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	let isUploading = false; | ||||
| 
 | ||||
| 	uploadAssetsStore.isUploading.subscribe((value) => { | ||||
| 		isUploading = value; | ||||
| 	}); | ||||
| @ -88,48 +53,7 @@ | ||||
| 				<div class="max-h-[400px] overflow-y-auto pr-2 rounded-lg immich-scrollbar"> | ||||
| 					{#each $uploadAssetsStore as uploadAsset} | ||||
| 						{#key uploadAsset.id} | ||||
| 							<div | ||||
| 								in:fade={{ duration: 250 }} | ||||
| 								out:fade={{ duration: 100 }} | ||||
| 								class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]" | ||||
| 							> | ||||
| 								<div class="relative"> | ||||
| 									<img | ||||
| 										in:fade={{ duration: 250 }} | ||||
| 										id={`${uploadAsset.id}`} | ||||
| 										src="/immich-logo.svg" | ||||
| 										alt="" | ||||
| 										class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg " | ||||
| 										draggable="false" | ||||
| 									/> | ||||
| 
 | ||||
| 									<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30"> | ||||
| 										<p | ||||
| 											class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase" | ||||
| 										> | ||||
| 											.{uploadAsset.fileExtension} | ||||
| 										</p> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 
 | ||||
| 								<div class="p-2 pr-4 flex flex-col justify-between"> | ||||
| 									<input | ||||
| 										disabled | ||||
| 										class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2" | ||||
| 										value={`[${asByteUnitString(uploadAsset.file.size)}] ${uploadAsset.file.name}`} | ||||
| 									/> | ||||
| 
 | ||||
| 									<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative"> | ||||
| 										<div | ||||
| 											class="bg-immich-primary h-[15px] rounded-md transition-all" | ||||
| 											style={`width: ${uploadAsset.progress}%`} | ||||
| 										/> | ||||
| 										<p class="absolute h-full w-full text-center top-0 text-[10px] "> | ||||
| 											{uploadAsset.progress}/100 | ||||
| 										</p> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							<UploadAssetPreview {uploadAsset} /> | ||||
| 						{/key} | ||||
| 					{/each} | ||||
| 				</div> | ||||
|  | ||||
| @ -111,3 +111,38 @@ export async function bulkDownload( | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns the lowercase filename extension without a dot (.) and | ||||
|  * an empty string when not found. | ||||
|  */ | ||||
| export function getFilenameExtension(filename: string): string { | ||||
| 	const lastIndex = filename.lastIndexOf('.'); | ||||
| 	return filename.slice(lastIndex + 1).toLowerCase(); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns the MIME type of the file and an empty string when not found. | ||||
|  */ | ||||
| export function getFileMimeType(file: File): string { | ||||
| 	if (file.type !== '') { | ||||
| 		// Return the MIME type determined by the browser.
 | ||||
| 		return file.type; | ||||
| 	} | ||||
| 
 | ||||
| 	// Return MIME type based on the file extension.
 | ||||
| 	switch (getFilenameExtension(file.name)) { | ||||
| 		case 'heic': | ||||
| 			return 'image/heic'; | ||||
| 		case 'heif': | ||||
| 			return 'image/heif'; | ||||
| 		case 'dng': | ||||
| 			return 'image/dng'; | ||||
| 		case '3gp': | ||||
| 			return 'video/3gpp'; | ||||
| 		case 'nef': | ||||
| 			return 'image/nef'; | ||||
| 		default: | ||||
| 			return ''; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -7,7 +7,7 @@ import * as exifr from 'exifr'; | ||||
| import { uploadAssetsStore } from '$lib/stores/upload'; | ||||
| import type { UploadAsset } from '../models/upload-asset'; | ||||
| import { api, AssetFileUploadResponseDto } from '@api'; | ||||
| import { addAssetsToAlbum } from '$lib/utils/asset-utils'; | ||||
| import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils'; | ||||
| 
 | ||||
| export const openFileUploadDialog = ( | ||||
| 	albumId: string | undefined = undefined, | ||||
| @ -19,6 +19,9 @@ export const openFileUploadDialog = ( | ||||
| 
 | ||||
| 		fileSelector.type = 'file'; | ||||
| 		fileSelector.multiple = true; | ||||
| 
 | ||||
| 		// When adding a content type that is unsupported by browsers, make sure
 | ||||
| 		// to also add it to getFileMimeType() otherwise the upload will fail.
 | ||||
| 		fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef'; | ||||
| 
 | ||||
| 		fileSelector.onchange = async (e: Event) => { | ||||
| @ -55,9 +58,10 @@ export const fileUploadHandler = async ( | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const acceptedFile = files.filter( | ||||
| 		(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image' | ||||
| 	); | ||||
| 	const acceptedFile = files.filter((file) => { | ||||
| 		const assetType = getFileMimeType(file).split('/')[0]; | ||||
| 		return assetType === 'video' || assetType === 'image'; | ||||
| 	}); | ||||
| 
 | ||||
| 	for (const asset of acceptedFile) { | ||||
| 		await fileUploader(asset, albumId, sharedKey, onDone); | ||||
| @ -71,9 +75,9 @@ async function fileUploader( | ||||
| 	sharedKey: string | undefined = undefined, | ||||
| 	onDone?: (id: string) => void | ||||
| ) { | ||||
| 	const assetType = asset.type.split('/')[0].toUpperCase(); | ||||
| 	const temp = asset.name.split('.'); | ||||
| 	const fileExtension = temp[temp.length - 1]; | ||||
| 	const mimeType = getFileMimeType(asset); | ||||
| 	const assetType = mimeType.split('/')[0].toUpperCase(); | ||||
| 	const fileExtension = getFilenameExtension(asset.name); | ||||
| 	const formData = new FormData(); | ||||
| 
 | ||||
| 	try { | ||||
| @ -114,8 +118,10 @@ async function fileUploader( | ||||
| 		// Get asset file extension
 | ||||
| 		formData.append('fileExtension', '.' + fileExtension); | ||||
| 
 | ||||
| 		// Get asset binary data.
 | ||||
| 		formData.append('assetData', asset); | ||||
| 		// Get asset binary data with a custom MIME type, because browsers will
 | ||||
| 		// use application/octet-stream for unsupported MIME types, leading to
 | ||||
| 		// failed uploads.
 | ||||
| 		formData.append('assetData', new File([asset], asset.name, { type: mimeType })); | ||||
| 
 | ||||
| 		// Check if asset upload on server before performing upload
 | ||||
| 		const { data, status } = await api.assetApi.checkDuplicateAsset( | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user