mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03: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:
@@ -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">
|
<script lang="ts">
|
||||||
import { thumbhash } from '$lib/actions/thumbhash';
|
|
||||||
import AlphaBackground from '$lib/components/AlphaBackground.svelte';
|
import AlphaBackground from '$lib/components/AlphaBackground.svelte';
|
||||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||||
import DelayedLoadingSpinner from '$lib/components/DelayedLoadingSpinner.svelte';
|
import DelayedLoadingSpinner from '$lib/components/DelayedLoadingSpinner.svelte';
|
||||||
import ImageLayer from '$lib/components/ImageLayer.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 { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import { getAssetUrls } from '$lib/utils';
|
import { getAssetUrls } from '$lib/utils';
|
||||||
import { AdaptiveImageLoader, type QualityList } from '$lib/utils/adaptive-image-loader.svelte';
|
import { AdaptiveImageLoader, type QualityList } from '$lib/utils/adaptive-image-loader.svelte';
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
{#if show.thumbhash}
|
{#if show.thumbhash}
|
||||||
{#if asset.thumbhash}
|
{#if asset.thumbhash}
|
||||||
<!-- Thumbhash / spinner layer -->
|
<!-- 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}
|
{:else if show.spinner}
|
||||||
<DelayedLoadingSpinner />
|
<DelayedLoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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">
|
<script lang="ts">
|
||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
import { thumbhash } from '$lib/actions/thumbhash';
|
|
||||||
import { zoomImageAction } from '$lib/actions/zoom-image';
|
import { zoomImageAction } from '$lib/actions/zoom-image';
|
||||||
import AdaptiveImage from '$lib/components/AdaptiveImage.svelte';
|
import AdaptiveImage from '$lib/components/AdaptiveImage.svelte';
|
||||||
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.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 OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte';
|
||||||
import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte';
|
import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte';
|
||||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
@@ -242,10 +242,7 @@
|
|||||||
>
|
>
|
||||||
{#snippet backdrop()}
|
{#snippet backdrop()}
|
||||||
{#if blurredSlideshow}
|
{#if blurredSlideshow}
|
||||||
<canvas
|
<Thumbhash base64ThumbHash={asset.thumbhash!} class="absolute top-0 left-0 inset-s-0 h-dvh w-dvw" />
|
||||||
use:thumbhash={{ base64ThumbHash: asset.thumbhash! }}
|
|
||||||
class="absolute top-0 left-0 inset-s-0 h-dvh w-dvw"
|
|
||||||
></canvas>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet overlays()}
|
{#snippet overlays()}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { thumbhash } from '$lib/actions/thumbhash';
|
|
||||||
import { ProjectionType } from '$lib/constants';
|
import { ProjectionType } from '$lib/constants';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
@@ -10,7 +9,6 @@
|
|||||||
import { moveFocus } from '$lib/utils/focus-util';
|
import { moveFocus } from '$lib/utils/focus-util';
|
||||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
|
||||||
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
||||||
import { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +25,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { ClassValue } from 'svelte/elements';
|
import type { ClassValue } from 'svelte/elements';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import Thumbhash from '$lib/components/Thumbhash.svelte';
|
||||||
import ImageThumbnail from './image-thumbnail.svelte';
|
import ImageThumbnail from './image-thumbnail.svelte';
|
||||||
import VideoThumbnail from './video-thumbnail.svelte';
|
import VideoThumbnail from './video-thumbnail.svelte';
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -75,10 +74,6 @@
|
|||||||
dimmed = false,
|
dimmed = false,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let {
|
|
||||||
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
|
|
||||||
} = TUNABLES;
|
|
||||||
|
|
||||||
let usingMobileDevice = $derived(mediaQueryManager.pointerCoarse);
|
let usingMobileDevice = $derived(mediaQueryManager.pointerCoarse);
|
||||||
let element: HTMLElement | undefined = $state();
|
let element: HTMLElement | undefined = $state();
|
||||||
let mouseOver = $state(false);
|
let mouseOver = $state(false);
|
||||||
@@ -312,16 +307,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (!loaded || thumbError) && asset.thumbhash}
|
{#if (!loaded || thumbError) && asset.thumbhash}
|
||||||
<canvas
|
<Thumbhash
|
||||||
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
|
base64ThumbHash={asset.thumbhash}
|
||||||
data-testid="thumbhash"
|
data-testid="thumbhash"
|
||||||
class="absolute top-0 object-cover group-focus-visible:rounded-lg"
|
class={['absolute top-0 object-cover group-focus-visible:rounded-lg', { 'rounded-xl': selected }]}
|
||||||
style:width="{width}px"
|
style="width: {width}px; height: {height}px"
|
||||||
style:height="{height}px"
|
|
||||||
class:rounded-xl={selected}
|
|
||||||
draggable="false"
|
draggable="false"
|
||||||
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
|
fadeOut
|
||||||
></canvas>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- icon overlay -->
|
<!-- icon overlay -->
|
||||||
|
|||||||
Reference in New Issue
Block a user