1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02: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:
Michel Heusschen 2023-02-09 17:08:19 +01:00 committed by GitHub
parent 8c20d8cb3d
commit adb265794c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 123 additions and 89 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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 '';
}
}

View File

@ -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(