mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
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:
parent
07538299cf
commit
837b1e4929
50 changed files with 2947 additions and 843 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,6 @@
|
|||
title={person.name}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
thumbhash={null}
|
||||
hidden={person.isHidden}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue