- {#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,