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

* 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

View file

@ -15,7 +15,6 @@
import { websocketEvents } from '$lib/stores/websocket';
import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { navigate } from '$lib/utils/navigation';
import { SlideshowHistory } from '$lib/utils/slideshow-history';
import {
AssetJobName,
@ -70,7 +69,8 @@
} = slideshowStore;
const dispatch = createEventDispatcher<{
close: void;
action: { type: AssetAction; asset: AssetResponseDto };
close: { asset: AssetResponseDto };
next: void;
previous: void;
}>();
@ -201,7 +201,6 @@
websocketEvents.on('on_asset_update', onAssetUpdate),
);
await navigate({ targetRoute: 'current', assetId: asset.id });
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
if (value === SlideshowState.PlaySlideshow) {
slideshowHistory.reset();
@ -268,9 +267,8 @@
$isShowDetail = !$isShowDetail;
};
const closeViewer = async () => {
dispatch('close');
await navigate({ targetRoute: 'current', assetId: null });
const closeViewer = () => {
dispatch('close', { asset });
};
const closeEditor = () => {
@ -378,9 +376,7 @@
}
};
const handleStackedAssetMouseEvent = (e: CustomEvent<{ isMouseOver: boolean }>, asset: AssetResponseDto) => {
const { isMouseOver } = e.detail;
const handleStackedAssetMouseEvent = (isMouseOver: boolean, asset: AssetResponseDto) => {
previewStackedAsset = isMouseOver ? asset : undefined;
};
@ -392,8 +388,7 @@
}
case AssetAction.UNSTACK: {
await closeViewer();
break;
closeViewer();
}
}
@ -585,12 +580,11 @@
? 'bg-transparent border-2 border-white'
: 'bg-gray-700/40'} inline-block hover:bg-transparent"
asset={stackedAsset}
onClick={(stackedAsset, event) => {
event.preventDefault();
onClick={(stackedAsset) => {
asset = stackedAsset;
preloadAssets = index + 1 >= stackedAssets.length ? [] : [stackedAssets[index + 1]];
}}
on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)}
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
readonly
thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
showStackedIcon={false}

View file

@ -212,7 +212,6 @@
title={person.name}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
hidden={person.isHidden}
/>
</div>

View file

@ -1,82 +0,0 @@
<script lang="ts">
import { BucketPosition } from '$lib/stores/assets.store';
import { createEventDispatcher, onMount } from 'svelte';
export let once = false;
export let top = 0;
export let bottom = 0;
export let left = 0;
export let right = 0;
export let root: HTMLElement | null = null;
export let intersecting = false;
let container: HTMLDivElement;
const dispatch = createEventDispatcher<{
hidden: HTMLDivElement;
intersected: {
container: HTMLDivElement;
position: BucketPosition;
};
}>();
onMount(() => {
if (typeof IntersectionObserver !== 'undefined') {
const rootMargin = `${top}px ${right}px ${bottom}px ${left}px`;
const observer = new IntersectionObserver(
(entries) => {
intersecting = entries.some((entry) => entry.isIntersecting);
if (!intersecting) {
dispatch('hidden', container);
}
if (intersecting && once) {
observer.unobserve(container);
}
if (intersecting) {
let position: BucketPosition = BucketPosition.Visible;
if (entries[0].boundingClientRect.top + 50 > entries[0].intersectionRect.bottom) {
position = BucketPosition.Below;
} else if (entries[0].boundingClientRect.bottom < 0) {
position = BucketPosition.Above;
}
dispatch('intersected', {
container,
position,
});
}
},
{
rootMargin,
root,
},
);
observer.observe(container);
return () => observer.unobserve(container);
}
// The following is a fallback for older browsers
function handler() {
const bcr = container.getBoundingClientRect();
intersecting =
bcr.bottom + bottom > 0 &&
bcr.right + right > 0 &&
bcr.top - top < window.innerHeight &&
bcr.left - left < window.innerWidth;
if (intersecting && once) {
window.removeEventListener('scroll', handler);
}
}
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
});
</script>
<div bind:this={container}>
<slot {intersecting} />
</div>

View file

@ -12,7 +12,7 @@
import { AssetTypeEnum, type AssetResponseDto, AssetMediaSize, type SharedLinkResponseDto } from '@immich/sdk';
import { zoomImageAction, zoomed } from '$lib/actions/zoom-image';
import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard';
import { onDestroy } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
@ -33,6 +33,7 @@
let imageLoaded: boolean = false;
let imageError: boolean = false;
let forceUseOriginal: boolean = false;
let loader: HTMLImageElement;
$: isWebCompatible = isWebCompatibleImage(asset);
$: useOriginalByDefault = isWebCompatible && $alwaysLoadOriginalFile;
@ -108,6 +109,25 @@
event.preventDefault();
handlePromiseError(copyImage());
};
onMount(() => {
const onload = () => {
imageLoaded = true;
assetFileUrl = imageLoaderUrl;
};
const onerror = () => {
imageError = imageLoaded = true;
};
if (loader.complete) {
onload();
}
loader.addEventListener('load', onload);
loader.addEventListener('error', onerror);
return () => {
loader?.removeEventListener('load', onload);
loader?.removeEventListener('error', onerror);
};
});
</script>
<svelte:window
@ -119,6 +139,8 @@
{#if imageError}
<div class="h-full flex items-center justify-center">{$t('error_loading_image')}</div>
{/if}
<!-- svelte-ignore a11y-missing-attribute -->
<img bind:this={loader} style="display:none" src={imageLoaderUrl} aria-hidden="true" />
<div bind:this={element} class="relative h-full select-none">
<img
style="display:none"
@ -128,7 +150,7 @@
on:error={() => (imageError = imageLoaded = true)}
/>
{#if !imageLoaded}
<div class="flex h-full items-center justify-center">
<div id="spinner" class="flex h-full items-center justify-center">
<LoadingSpinner />
</div>
{:else if !imageError}
@ -159,3 +181,15 @@
</div>
{/if}
</div>
<style>
@keyframes delayedVisibility {
to {
visibility: visible;
}
}
#spinner {
visibility: hidden;
animation: 0s linear 0.4s forwards delayedVisibility;
}
</style>