mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	fix(web): use native image decoder (#3074)
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									e5d083fe79
								
							
						
					
					
						commit
						696900228b
					
				@ -45,7 +45,7 @@
 | 
				
			|||||||
  <section class="flex flex-wrap gap-14 overflow-y-auto px-20">
 | 
					  <section class="flex flex-wrap gap-14 overflow-y-auto px-20">
 | 
				
			||||||
    <!-- Image grid -->
 | 
					    <!-- Image grid -->
 | 
				
			||||||
    <div class="flex flex-wrap gap-[2px]">
 | 
					    <div class="flex flex-wrap gap-[2px]">
 | 
				
			||||||
      {#each album.assets as asset}
 | 
					      {#each album.assets as asset (asset.id)}
 | 
				
			||||||
        <Thumbnail {asset} on:click={() => (selectedThumbnail = asset)} selected={isSelected(asset.id)} />
 | 
					        <Thumbnail {asset} on:click={() => (selectedThumbnail = asset)} selected={isSelected(asset.id)} />
 | 
				
			||||||
      {/each}
 | 
					      {/each}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { imageLoad } from '$lib/utils/image-load';
 | 
					  import { onMount, tick } from 'svelte';
 | 
				
			||||||
  import { fade } from 'svelte/transition';
 | 
					  import { fade } from 'svelte/transition';
 | 
				
			||||||
  import { thumbHashToDataURL } from 'thumbhash';
 | 
					  import { thumbHashToDataURL } from 'thumbhash';
 | 
				
			||||||
  import { Buffer } from 'buffer';
 | 
					  import { Buffer } from 'buffer';
 | 
				
			||||||
@ -18,12 +18,20 @@
 | 
				
			|||||||
  export let hidden = false;
 | 
					  export let hidden = false;
 | 
				
			||||||
  export let border = false;
 | 
					  export let border = false;
 | 
				
			||||||
  export let preload = true;
 | 
					  export let preload = true;
 | 
				
			||||||
  let complete = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  export let eyeColor: 'black' | 'white' = 'white';
 | 
					  export let eyeColor: 'black' | 'white' = 'white';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let complete = false;
 | 
				
			||||||
 | 
					  let img: HTMLImageElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMount(async () => {
 | 
				
			||||||
 | 
					    await img.decode();
 | 
				
			||||||
 | 
					    await tick();
 | 
				
			||||||
 | 
					    complete = true;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<img
 | 
					<img
 | 
				
			||||||
 | 
					  bind:this={img}
 | 
				
			||||||
  loading={preload ? 'eager' : 'lazy'}
 | 
					  loading={preload ? 'eager' : 'lazy'}
 | 
				
			||||||
  style:width={widthStyle}
 | 
					  style:width={widthStyle}
 | 
				
			||||||
  style:height={heightStyle}
 | 
					  style:height={heightStyle}
 | 
				
			||||||
@ -40,8 +48,6 @@
 | 
				
			|||||||
  class:rounded-full={circle}
 | 
					  class:rounded-full={circle}
 | 
				
			||||||
  class:opacity-0={!thumbhash && !complete}
 | 
					  class:opacity-0={!thumbhash && !complete}
 | 
				
			||||||
  draggable="false"
 | 
					  draggable="false"
 | 
				
			||||||
  use:imageLoad
 | 
					 | 
				
			||||||
  on:image-load|once={() => (complete = true)}
 | 
					 | 
				
			||||||
/>
 | 
					/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if hidden}
 | 
					{#if hidden}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { imageLoad } from '$lib/utils/image-load';
 | 
					  import { onMount, tick } from 'svelte';
 | 
				
			||||||
  import { UserAvatarColor, api } from '@api';
 | 
					  import { UserAvatarColor, api } from '@api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface User {
 | 
					  interface User {
 | 
				
			||||||
@ -22,8 +22,19 @@
 | 
				
			|||||||
  export let showTitle = true;
 | 
					  export let showTitle = true;
 | 
				
			||||||
  export let showProfileImage = true;
 | 
					  export let showProfileImage = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let img: HTMLImageElement;
 | 
				
			||||||
  let showFallback = true;
 | 
					  let showFallback = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMount(async () => {
 | 
				
			||||||
 | 
					    if (!user.profileImagePath) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await img.decode();
 | 
				
			||||||
 | 
					    await tick();
 | 
				
			||||||
 | 
					    showFallback = false;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const colorClasses: Record<UserAvatarColor, string> = {
 | 
					  const colorClasses: Record<UserAvatarColor, string> = {
 | 
				
			||||||
    primary: 'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
 | 
					    primary: 'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
 | 
				
			||||||
    pink: 'bg-pink-400 text-immich-bg',
 | 
					    pink: 'bg-pink-400 text-immich-bg',
 | 
				
			||||||
@ -62,13 +73,12 @@
 | 
				
			|||||||
>
 | 
					>
 | 
				
			||||||
  {#if showProfileImage && user.profileImagePath}
 | 
					  {#if showProfileImage && user.profileImagePath}
 | 
				
			||||||
    <img
 | 
					    <img
 | 
				
			||||||
 | 
					      bind:this={img}
 | 
				
			||||||
      src={api.getProfileImageUrl(user.id)}
 | 
					      src={api.getProfileImageUrl(user.id)}
 | 
				
			||||||
      alt="Profile image of {title}"
 | 
					      alt="Profile image of {title}"
 | 
				
			||||||
      class="h-full w-full object-cover"
 | 
					      class="h-full w-full object-cover"
 | 
				
			||||||
      class:hidden={showFallback}
 | 
					      class:hidden={showFallback}
 | 
				
			||||||
      draggable="false"
 | 
					      draggable="false"
 | 
				
			||||||
      use:imageLoad
 | 
					 | 
				
			||||||
      on:image-load={() => (showFallback = false)}
 | 
					 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
  {#if showFallback}
 | 
					  {#if showFallback}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,38 +0,0 @@
 | 
				
			|||||||
import { tick } from 'svelte';
 | 
					 | 
				
			||||||
import type { ActionReturn } from 'svelte/action';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Attributes {
 | 
					 | 
				
			||||||
  'on:image-error'?: (e: CustomEvent) => void;
 | 
					 | 
				
			||||||
  'on:image-load'?: (e: CustomEvent) => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function imageLoad(img: HTMLImageElement): ActionReturn<void, Attributes> {
 | 
					 | 
				
			||||||
  const onImageError = () => img.dispatchEvent(new CustomEvent('image-error'));
 | 
					 | 
				
			||||||
  const onImageLoaded = () => img.dispatchEvent(new CustomEvent('image-load'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (img.complete) {
 | 
					 | 
				
			||||||
    // Browser has fetched the image, naturalHeight is used to check
 | 
					 | 
				
			||||||
    // if any loading errors have occurred.
 | 
					 | 
				
			||||||
    const loadingError = img.naturalHeight === 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Report status after a tick, to make sure event listeners are registered.
 | 
					 | 
				
			||||||
    if (loadingError) {
 | 
					 | 
				
			||||||
      tick().then(onImageError);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      tick().then(onImageLoaded);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {};
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Image has not been loaded yet, report status with event listeners.
 | 
					 | 
				
			||||||
  img.addEventListener('load', onImageLoaded, { once: true });
 | 
					 | 
				
			||||||
  img.addEventListener('error', onImageError, { once: true });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    destroy() {
 | 
					 | 
				
			||||||
      img.removeEventListener('load', onImageLoaded);
 | 
					 | 
				
			||||||
      img.removeEventListener('error', onImageError);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -80,7 +80,7 @@
 | 
				
			|||||||
        <p class="mb-4 font-medium dark:text-immich-dark-fg">Places</p>
 | 
					        <p class="mb-4 font-medium dark:text-immich-dark-fg">Places</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="flex flex-row flex-wrap gap-4">
 | 
					      <div class="flex flex-row flex-wrap gap-4">
 | 
				
			||||||
        {#each places as item}
 | 
					        {#each places as item (item.data.id)}
 | 
				
			||||||
          <a class="relative" href="/search?{Field.CITY}={item.value}" draggable="false">
 | 
					          <a class="relative" href="/search?{Field.CITY}={item.value}" draggable="false">
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center overflow-hidden rounded-xl brightness-75 filter"
 | 
					              class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center overflow-hidden rounded-xl brightness-75 filter"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user