From ab4b8eca1542db3f3ec9028c881a8fb0b352f6d2 Mon Sep 17 00:00:00 2001 From: Sam Holton Date: Thu, 14 Mar 2024 17:12:32 -0400 Subject: [PATCH] feat(web): preload assets in photo-viewer (#7920) * feat(web): preload assets in photo-viewer * PR feedback --------- Co-authored-by: Alex Tran --- .../asset-viewer/asset-viewer.svelte | 17 ++++++++--- .../asset-viewer/photo-viewer.svelte | 29 +++++++++++++++---- .../components/photos-page/asset-grid.svelte | 15 ++++++++-- web/src/lib/stores/asset-viewing.store.ts | 18 +++++++++++- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index b1f70cbeb5..0af40b4d45 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -55,6 +55,7 @@ export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; + export let preloadAssets: AssetResponseDto[] = []; export let showNavigation = true; export let sharedLink: SharedLinkResponseDto | undefined = undefined; $: isTrashEnabled = $featureFlags.trash; @@ -103,6 +104,11 @@ $stackAssetsStore = [...$stackAssetsStore, asset].sort( (a, b) => new Date(b.fileCreatedAt).getTime() - new Date(a.fileCreatedAt).getTime(), ); + + // if its a stack, add the next stack image in addition to the next asset + if (asset.stackCount > 1) { + preloadAssets.push($stackAssetsStore[1]); + } } if (!$stackAssetsStore.map((a) => a.id).includes(asset.id)) { @@ -613,7 +619,7 @@ {#if previewStackedAsset} {#key previewStackedAsset.id} {#if previewStackedAsset.type === AssetTypeEnum.Image} - + {:else} {:else} - + {/if} {:else}
- {#each $stackAssetsStore as stackedAsset (stackedAsset.id)} + {#each $stackAssetsStore as stackedAsset, index (stackedAsset.id)}
(asset = stackedAsset)} + on:click={() => { + asset = stackedAsset; + preloadAssets = index + 1 >= $stackAssetsStore.length ? [] : [$stackAssetsStore[index + 1]]; + }} on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)} readonly thumbnailSize={stackedAsset.id == asset.id ? 65 : 60} diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 00ef670c9c..2093a60b8b 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -7,7 +7,7 @@ import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { getBoundingBox } from '$lib/utils/people-utils'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; - import { type AssetResponseDto } from '@immich/sdk'; + import { type AssetResponseDto, AssetTypeEnum } from '@immich/sdk'; import { useZoomImageWheel } from '@zoom-image/svelte'; import { onDestroy, onMount } from 'svelte'; import { fade } from 'svelte/transition'; @@ -16,6 +16,7 @@ import { getAltText } from '$lib/utils/thumbnail-util'; export let asset: AssetResponseDto; + export let preloadAssets: AssetResponseDto[] | null = null; export let element: HTMLDivElement | undefined = undefined; export let haveFadeTransition = true; @@ -25,6 +26,7 @@ let hasZoomed = false; let copyImageToClipboard: (source: string) => Promise; let canCopyImagesToClipboard: () => boolean; + let imageLoaded: boolean = false; const loadOriginalByDefault = $alwaysLoadOriginalFile && isWebCompatibleImage(asset); @@ -41,6 +43,9 @@ const module = await import('copy-image-clipboard'); copyImageToClipboard = module.copyImageToClipboard; canCopyImagesToClipboard = module.canCopyImagesToClipboard; + + imageLoaded = false; + await loadAssetData({ loadOriginal: loadOriginalByDefault }); }); onDestroy(() => { @@ -60,8 +65,22 @@ }); assetData = URL.createObjectURL(data); + imageLoaded = true; + + if (!preloadAssets) { + return; + } + + for (const preloadAsset of preloadAssets) { + if (preloadAsset.type === AssetTypeEnum.Image) { + await downloadRequest({ + url: getAssetFileUrl(preloadAsset.id, !loadOriginal, false), + signal: abortController.signal, + }); + } + } } catch { - // Do nothing + imageLoaded = false; } }; @@ -128,9 +147,9 @@ transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} class="flex h-full select-none place-content-center place-items-center" > - {#await loadAssetData({ loadOriginal: loadOriginalByDefault })} + {#if !imageLoaded} - {:then} + {:else}
{/each}
- {/await} + {/if}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index b66bfc96fd..772b039f80 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -38,7 +38,7 @@ const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = assetInteractionStore; const viewport: Viewport = { width: 0, height: 0 }; - let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore; + let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets } = assetViewingStore; let element: HTMLElement; let showShortcuts = false; let showSkeleton = true; @@ -141,8 +141,12 @@ const handlePrevious = async () => { const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id); + if (previousAsset) { - await assetViewingStore.setAssetId(previousAsset); + const preloadId = await assetStore.getPreviousAssetId(previousAsset); + preloadId + ? await assetViewingStore.setAssetId(previousAsset, [preloadId]) + : await assetViewingStore.setAssetId(previousAsset); } return !!previousAsset; @@ -150,8 +154,12 @@ const handleNext = async () => { const nextAsset = await assetStore.getNextAssetId($viewingAsset.id); + if (nextAsset) { - await assetViewingStore.setAssetId(nextAsset); + const preloadId = await assetStore.getNextAssetId(nextAsset); + preloadId + ? await assetViewingStore.setAssetId(nextAsset, [preloadId]) + : await assetViewingStore.setAssetId(nextAsset); } return !!nextAsset; @@ -455,6 +463,7 @@ {withStacked} {assetStore} asset={$viewingAsset} + preloadAssets={$preloadAssets} {isShared} {album} on:previous={handlePrevious} diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index f9ddbd76e8..0416d4903c 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -4,10 +4,23 @@ import { writable } from 'svelte/store'; function createAssetViewingStore() { const viewingAssetStoreState = writable(); + const preloadAssets = writable([]); const viewState = writable(false); - const setAssetId = async (id: string) => { + const setAssetId = async (id: string, preloadIds?: string[]) => { const data = await getAssetInfo({ id, key: getKey() }); + + if (preloadIds) { + const preloadList = []; + for (const preloadId of preloadIds) { + if (preloadId) { + const preloadAsset = await getAssetInfo({ id: preloadId, key: getKey() }); + preloadList.push(preloadAsset); + } + } + preloadAssets.set(preloadList); + } + viewingAssetStoreState.set(data); viewState.set(true); }; @@ -20,6 +33,9 @@ function createAssetViewingStore() { asset: { subscribe: viewingAssetStoreState.subscribe, }, + preloadAssets: { + subscribe: preloadAssets.subscribe, + }, isViewing: { subscribe: viewState.subscribe, set: viewState.set,