mirror of
https://github.com/immich-app/immich.git
synced 2026-04-17 16:11:55 -04:00
refactor(web): turn thumbhash action into Thumbhash component (#27741)
refactor(web): extract thumbhash canvas into Thumbhash component Change-Id: If78955bed48b6e690df398e5e2ae61fb6a6a6964
This commit is contained in:
parent
2ff9f95527
commit
3d8df74b43
@ -1,29 +0,0 @@
|
||||
import { decodeBase64 } from '$lib/utils';
|
||||
import { thumbHashToRGBA } from 'thumbhash';
|
||||
|
||||
/**
|
||||
* Renders a thumbnail onto a canvas from a base64 encoded hash.
|
||||
*/
|
||||
export function thumbhash(canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) {
|
||||
render(canvas, options);
|
||||
|
||||
return {
|
||||
update(newOptions: { base64ThumbHash: string }) {
|
||||
render(canvas, newOptions);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const render = (canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(options.base64ThumbHash));
|
||||
const pixels = ctx.createImageData(w, h);
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
pixels.data.set(rgba);
|
||||
ctx.putImageData(pixels, 0, 0);
|
||||
};
|
||||
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { thumbhash } from '$lib/actions/thumbhash';
|
||||
import AlphaBackground from '$lib/components/AlphaBackground.svelte';
|
||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||
import DelayedLoadingSpinner from '$lib/components/DelayedLoadingSpinner.svelte';
|
||||
import ImageLayer from '$lib/components/ImageLayer.svelte';
|
||||
import Thumbhash from '$lib/components/Thumbhash.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { getAssetUrls } from '$lib/utils';
|
||||
import { AdaptiveImageLoader, type QualityList } from '$lib/utils/adaptive-image-loader.svelte';
|
||||
@ -165,7 +165,7 @@
|
||||
{#if show.thumbhash}
|
||||
{#if asset.thumbhash}
|
||||
<!-- Thumbhash / spinner layer -->
|
||||
<canvas use:thumbhash={{ base64ThumbHash: asset.thumbhash }} class="h-full w-full absolute"></canvas>
|
||||
<Thumbhash base64ThumbHash={asset.thumbhash} class="h-full w-full absolute" />
|
||||
{:else if show.spinner}
|
||||
<DelayedLoadingSpinner />
|
||||
{/if}
|
||||
|
||||
40
web/src/lib/components/Thumbhash.svelte
Normal file
40
web/src/lib/components/Thumbhash.svelte
Normal file
@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { decodeBase64 } from '$lib/utils';
|
||||
import { TUNABLES } from '$lib/utils/tunables';
|
||||
import type { HTMLCanvasAttributes } from 'svelte/elements';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { thumbHashToRGBA } from 'thumbhash';
|
||||
|
||||
type Props = HTMLCanvasAttributes & {
|
||||
base64ThumbHash: string;
|
||||
fadeOut?: boolean;
|
||||
};
|
||||
|
||||
const { base64ThumbHash, fadeOut = false, class: className, ...restProps }: Props = $props();
|
||||
|
||||
const {
|
||||
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
|
||||
} = TUNABLES;
|
||||
|
||||
let canvas = $state<HTMLCanvasElement>();
|
||||
|
||||
$effect(() => {
|
||||
const ctx = canvas?.getContext('2d');
|
||||
if (!canvas || !ctx) {
|
||||
return;
|
||||
}
|
||||
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash));
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const pixels = ctx.createImageData(w, h);
|
||||
pixels.data.set(rgba);
|
||||
ctx.putImageData(pixels, 0, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
class={className}
|
||||
out:fade={{ duration: fadeOut ? THUMBHASH_FADE_DURATION : 0 }}
|
||||
{...restProps}
|
||||
></canvas>
|
||||
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import { thumbhash } from '$lib/actions/thumbhash';
|
||||
import { zoomImageAction } from '$lib/actions/zoom-image';
|
||||
import AdaptiveImage from '$lib/components/AdaptiveImage.svelte';
|
||||
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
|
||||
import Thumbhash from '$lib/components/Thumbhash.svelte';
|
||||
import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte';
|
||||
import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
@ -242,10 +242,7 @@
|
||||
>
|
||||
{#snippet backdrop()}
|
||||
{#if blurredSlideshow}
|
||||
<canvas
|
||||
use:thumbhash={{ base64ThumbHash: asset.thumbhash! }}
|
||||
class="absolute top-0 left-0 inset-s-0 h-dvh w-dvw"
|
||||
></canvas>
|
||||
<Thumbhash base64ThumbHash={asset.thumbhash!} class="absolute top-0 left-0 inset-s-0 h-dvh w-dvw" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet overlays()}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { thumbhash } from '$lib/actions/thumbhash';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
@ -10,7 +9,6 @@
|
||||
import { moveFocus } from '$lib/utils/focus-util';
|
||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { TUNABLES } from '$lib/utils/tunables';
|
||||
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import {
|
||||
@ -27,6 +25,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Thumbhash from '$lib/components/Thumbhash.svelte';
|
||||
import ImageThumbnail from './image-thumbnail.svelte';
|
||||
import VideoThumbnail from './video-thumbnail.svelte';
|
||||
interface Props {
|
||||
@ -75,10 +74,6 @@
|
||||
dimmed = false,
|
||||
}: Props = $props();
|
||||
|
||||
let {
|
||||
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
|
||||
} = TUNABLES;
|
||||
|
||||
let usingMobileDevice = $derived(mediaQueryManager.pointerCoarse);
|
||||
let element: HTMLElement | undefined = $state();
|
||||
let mouseOver = $state(false);
|
||||
@ -312,16 +307,14 @@
|
||||
{/if}
|
||||
|
||||
{#if (!loaded || thumbError) && asset.thumbhash}
|
||||
<canvas
|
||||
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
|
||||
<Thumbhash
|
||||
base64ThumbHash={asset.thumbhash}
|
||||
data-testid="thumbhash"
|
||||
class="absolute top-0 object-cover group-focus-visible:rounded-lg"
|
||||
style:width="{width}px"
|
||||
style:height="{height}px"
|
||||
class:rounded-xl={selected}
|
||||
class={['absolute top-0 object-cover group-focus-visible:rounded-lg', { 'rounded-xl': selected }]}
|
||||
style="width: {width}px; height: {height}px"
|
||||
draggable="false"
|
||||
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
|
||||
></canvas>
|
||||
fadeOut
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- icon overlay -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user