feat: adaptive progressive image loading for photo viewer (#26636)

* feat(web): adaptive progressive image loading for photo viewer

Replace ImageManager with a new AdaptiveImageLoader that progressively
loads images through quality tiers (thumbnail → preview → original).

New components and utilities:
- AdaptiveImage: layered image renderer with thumbhash, thumbnail,
  preview, and original layers with visibility managed by load state
- AdaptiveImageLoader: state machine driving the quality progression
  with per-quality callbacks and error handling
- ImageLayer/Image: low-level image elements with load/error lifecycle
- PreloadManager: preloads adjacent assets for instant navigation
- AlphaBackground/DelayedLoadingSpinner: loading state UI

Zoom is handled via a derived CSS transform applied to the content
wrapper in AdaptiveImage, with the zoom library (zoomTarget: null)
only tracking state without manipulating the DOM directly.

Also adds scaleToCover to container-utils and getAssetUrls to utils.

* fix: don't partially render images in firefox

* add passive loading indicator to asset-viewer

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Min Idzelis
2026-03-11 10:48:46 -04:00
committed by GitHub
parent 27f69b39b2
commit 8764a1894b
25 changed files with 1340 additions and 416 deletions
@@ -0,0 +1,25 @@
import { cancelImageUrl } from '$lib/utils/sw-messaging';
export function loadImage(src: string, onLoad: () => void, onError: () => void, onStart?: () => void) {
let destroyed = false;
const handleLoad = () => !destroyed && onLoad();
const handleError = () => !destroyed && onError();
const img = document.createElement('img');
img.addEventListener('load', handleLoad);
img.addEventListener('error', handleError);
onStart?.();
img.src = src;
return () => {
destroyed = true;
img.removeEventListener('load', handleLoad);
img.removeEventListener('error', handleError);
cancelImageUrl(src);
img.remove();
};
}
export type LoadImageFunction = typeof loadImage;
+5 -1
View File
@@ -2,7 +2,11 @@ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { createZoomImageWheel } from '@zoom-image/core';
export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => {
const zoomInstance = createZoomImageWheel(node, { maxZoom: 10, initialState: assetViewerManager.zoomState });
const zoomInstance = createZoomImageWheel(node, {
maxZoom: 10,
initialState: assetViewerManager.zoomState,
zoomTarget: null,
});
const unsubscribes = [
assetViewerManager.on({ ZoomChange: (state) => zoomInstance.setState(state) }),