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/utils

View file

@ -5,6 +5,9 @@ import { getAssetInfo } from '@immich/sdk';
import type { NavigationTarget } from '@sveltejs/kit';
import { get } from 'svelte/store';
export type AssetGridRouteSearchParams = {
at: string | null | undefined;
};
export const isExternalUrl = (url: string): boolean => {
return new URL(url, window.location.href).origin !== window.location.origin;
};
@ -33,17 +36,38 @@ function currentUrlWithoutAsset() {
export function currentUrlReplaceAssetId(assetId: string) {
const $page = get(page);
const params = new URLSearchParams($page.url.search);
// always remove the assetGridScrollTargetParams
params.delete('at');
const searchparams = params.size > 0 ? '?' + params.toString() : '';
// this contains special casing for the /photos/:assetId photos route, which hangs directly
// off / instead of a subpath, unlike every other asset-containing route.
return isPhotosRoute($page.route.id)
? `${AppRoute.PHOTOS}/${assetId}${$page.url.search}`
: `${$page.url.pathname.replace(/(\/photos.*)$/, '')}/photos/${assetId}${$page.url.search}`;
? `${AppRoute.PHOTOS}/${assetId}${searchparams}`
: `${$page.url.pathname.replace(/(\/photos.*)$/, '')}/photos/${assetId}${searchparams}`;
}
function replaceScrollTarget(url: string, searchParams?: AssetGridRouteSearchParams | null) {
const $page = get(page);
const parsed = new URL(url, $page.url);
const { at: assetId } = searchParams || { at: null };
if (!assetId) {
return parsed.pathname;
}
const params = new URLSearchParams($page.url.search);
if (assetId) {
params.set('at', assetId);
}
return parsed.pathname + '?' + params.toString();
}
function currentUrl() {
const $page = get(page);
const current = $page.url;
return current.pathname + current.search;
return current.pathname + current.search + current.hash;
}
interface Route {
@ -55,24 +79,58 @@ interface Route {
interface AssetRoute extends Route {
targetRoute: 'current';
assetId: string | null;
assetId: string | null | undefined;
}
interface AssetGridRoute extends Route {
targetRoute: 'current';
assetId: string | null | undefined;
assetGridRouteSearchParams: AssetGridRouteSearchParams | null | undefined;
}
type ImmichRoute = AssetRoute | AssetGridRoute;
type NavOptions = {
/* navigate even if url is the same */
forceNavigate?: boolean | undefined;
replaceState?: boolean | undefined;
noScroll?: boolean | undefined;
keepFocus?: boolean | undefined;
invalidateAll?: boolean | undefined;
state?: App.PageState | undefined;
};
function isAssetRoute(route: Route): route is AssetRoute {
return route.targetRoute === 'current' && 'assetId' in route;
}
async function navigateAssetRoute(route: AssetRoute) {
function isAssetGridRoute(route: Route): route is AssetGridRoute {
return route.targetRoute === 'current' && 'assetId' in route && 'assetGridRouteSearchParams' in route;
}
async function navigateAssetRoute(route: AssetRoute, options?: NavOptions) {
const { assetId } = route;
const next = assetId ? currentUrlReplaceAssetId(assetId) : currentUrlWithoutAsset();
if (next !== currentUrl()) {
await goto(next, { replaceState: false });
const current = currentUrl();
if (next !== current || options?.forceNavigate) {
await goto(next, options);
}
}
export function navigate<T extends Route>(change: T): Promise<void> {
if (isAssetRoute(change)) {
return navigateAssetRoute(change);
async function navigateAssetGridRoute(route: AssetGridRoute, options?: NavOptions) {
const { assetId, assetGridRouteSearchParams: assetGridScrollTarget } = route;
const assetUrl = assetId ? currentUrlReplaceAssetId(assetId) : currentUrlWithoutAsset();
const next = replaceScrollTarget(assetUrl, assetGridScrollTarget);
const current = currentUrl();
if (next !== current || options?.forceNavigate) {
await goto(next, options);
}
}
export function navigate(change: ImmichRoute, options?: NavOptions): Promise<void> {
if (isAssetGridRoute(change)) {
return navigateAssetGridRoute(change, options);
} else if (isAssetRoute(change)) {
return navigateAssetRoute(change, options);
}
// future navigation requests here
throw `Invalid navigation: ${JSON.stringify(change)}`;