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:
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…
Reference in New Issue
Block a user