feat(web): Scroll to asset in gridview; increase gridview perf; reduce memory; scrollbar ticks in fixed position ()

* Squashed

* Change strategy - now pre-measure buckets offscreen, so don't need to worry about sub-bucket scroll preservation

* Reduce jank on scroll, delay DOM updates until after scroll

* css opt, log measure time

* Trickle out queue while scrolling, flush when stopped

* yay

* Cleanup cleanup...

* everybody...

* everywhere...

* Clean up cleanup!

* Everybody do their share

* CLEANUP!

* package-lock ?

* dynamic measure, todo

* Fix web test

* type lint

* fix e2e

* e2e test

* Better scrollbar

* Tuning, and more tunables

* Tunable tweaks, more tunables

* Scrollbar dots and viewport events

* lint

* Tweaked tunnables, use requestIdleCallback for garbage tasks, bug fixes

* New tunables, and don't update url by default

* Bug fixes

* Bug fix, with debug

* Fix flickr, fix graybox bug, reduced debug

* Refactor/cleanup

* Fix

* naming

* Final cleanup

* review comment

* Forgot to update this after naming change

* scrubber works, with debug

* cleanup

* Rename scrollbar to scrubber

* rename  to

* left over rename and change to previous album bar

* bugfix addassets, comments

* missing destroy(), cleanup

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Min Idzelis 2024-08-21 22:15:21 -04:00 committed by GitHub
parent 07538299cf
commit 837b1e4929
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 2947 additions and 843 deletions
web/src/lib/components/assets/thumbnail

View file

@ -1,17 +1,19 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import { decodeBase64 } from '$lib/utils';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { thumbHashToDataURL } from 'thumbhash';
import { mdiEyeOffOutline } from '@mdi/js';
import { thumbhash } from '$lib/actions/thumbhash';
import Icon from '$lib/components/elements/icon.svelte';
import { TUNABLES } from '$lib/utils/tunables';
import { mdiEyeOffOutline, mdiImageBrokenVariant } from '@mdi/js';
export let url: string;
export let altText: string | undefined;
export let title: string | null = null;
export let heightStyle: string | undefined = undefined;
export let widthStyle: string;
export let thumbhash: string | null = null;
export let base64ThumbHash: string | null = null;
export let curve = false;
export let shadow = false;
export let circle = false;
@ -19,37 +21,58 @@
export let border = false;
export let preload = true;
export let hiddenIconClass = 'text-white';
export let onComplete: (() => void) | undefined = undefined;
let {
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
let loaded = false;
let errored = false;
let complete = false;
let img: HTMLImageElement;
onMount(async () => {
await img.decode();
await tick();
complete = true;
const setLoaded = () => {
loaded = true;
onComplete?.();
};
const setErrored = () => {
errored = true;
onComplete?.();
};
onMount(() => {
if (img.complete) {
setLoaded();
}
});
</script>
<img
bind:this={img}
loading={preload ? 'eager' : 'lazy'}
style:width={widthStyle}
style:height={heightStyle}
style:filter={hidden ? 'grayscale(50%)' : 'none'}
style:opacity={hidden ? '0.5' : '1'}
src={url}
alt={altText}
{title}
class="object-cover transition duration-300 {border
? 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary'
: ''}"
class:rounded-xl={curve}
class:shadow-lg={shadow}
class:rounded-full={circle}
class:aspect-square={circle || !heightStyle}
class:opacity-0={!thumbhash && !complete}
draggable="false"
/>
{#if errored}
<div class="absolute flex h-full w-full items-center justify-center p-4 z-10">
<Icon path={mdiImageBrokenVariant} size="48" />
</div>
{:else}
<img
bind:this={img}
on:load={setLoaded}
on:error={setErrored}
loading={preload ? 'eager' : 'lazy'}
style:width={widthStyle}
style:height={heightStyle}
style:filter={hidden ? 'grayscale(50%)' : 'none'}
style:opacity={hidden ? '0.5' : '1'}
src={url}
alt={loaded || errored ? altText : ''}
{title}
class="object-cover {border ? 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary' : ''}"
class:rounded-xl={curve}
class:shadow-lg={shadow}
class:rounded-full={circle}
class:aspect-square={circle || !heightStyle}
class:opacity-0={!thumbhash && !loaded}
draggable="false"
/>
{/if}
{#if hidden}
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
@ -57,18 +80,18 @@
</div>
{/if}
{#if thumbhash && !complete}
<img
{#if base64ThumbHash && (!loaded || errored)}
<canvas
use:thumbhash={{ base64ThumbHash }}
data-testid="thumbhash"
style:width={widthStyle}
style:height={heightStyle}
src={thumbHashToDataURL(decodeBase64(thumbhash))}
alt={altText}
{title}
class="absolute top-0 object-cover"
class:rounded-xl={curve}
class:shadow-lg={shadow}
class:rounded-full={circle}
draggable="false"
out:fade={{ duration: 300 }}
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
/>
{/if}