1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

feat(web): preload assets in photo-viewer (#7920)

* feat(web): preload assets in photo-viewer

* PR feedback

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Sam Holton 2024-03-14 17:12:32 -04:00 committed by GitHub
parent 582cdcab82
commit ab4b8eca15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 13 deletions

View File

@ -55,6 +55,7 @@
export let assetStore: AssetStore | null = null; export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let preloadAssets: AssetResponseDto[] = [];
export let showNavigation = true; export let showNavigation = true;
export let sharedLink: SharedLinkResponseDto | undefined = undefined; export let sharedLink: SharedLinkResponseDto | undefined = undefined;
$: isTrashEnabled = $featureFlags.trash; $: isTrashEnabled = $featureFlags.trash;
@ -103,6 +104,11 @@
$stackAssetsStore = [...$stackAssetsStore, asset].sort( $stackAssetsStore = [...$stackAssetsStore, asset].sort(
(a, b) => new Date(b.fileCreatedAt).getTime() - new Date(a.fileCreatedAt).getTime(), (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)) { if (!$stackAssetsStore.map((a) => a.id).includes(asset.id)) {
@ -613,7 +619,7 @@
{#if previewStackedAsset} {#if previewStackedAsset}
{#key previewStackedAsset.id} {#key previewStackedAsset.id}
{#if previewStackedAsset.type === AssetTypeEnum.Image} {#if previewStackedAsset.type === AssetTypeEnum.Image}
<PhotoViewer asset={previewStackedAsset} on:close={closeViewer} haveFadeTransition={false} /> <PhotoViewer asset={previewStackedAsset} {preloadAssets} on:close={closeViewer} haveFadeTransition={false} />
{:else} {:else}
<VideoViewer <VideoViewer
assetId={previewStackedAsset.id} assetId={previewStackedAsset.id}
@ -645,7 +651,7 @@
.endsWith('.insp'))} .endsWith('.insp'))}
<PanoramaViewer {asset} /> <PanoramaViewer {asset} />
{:else} {:else}
<PhotoViewer {asset} on:close={closeViewer} /> <PhotoViewer {asset} {preloadAssets} on:close={closeViewer} />
{/if} {/if}
{:else} {:else}
<VideoViewer <VideoViewer
@ -676,7 +682,7 @@
class="z-[1005] flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 mb-1 overflow-x-auto horizontal-scrollbar" class="z-[1005] flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 mb-1 overflow-x-auto horizontal-scrollbar"
> >
<div class="relative w-full whitespace-nowrap transition-all"> <div class="relative w-full whitespace-nowrap transition-all">
{#each $stackAssetsStore as stackedAsset (stackedAsset.id)} {#each $stackAssetsStore as stackedAsset, index (stackedAsset.id)}
<div <div
class="{stackedAsset.id == asset.id class="{stackedAsset.id == asset.id
? '-translate-y-[1px]' ? '-translate-y-[1px]'
@ -687,7 +693,10 @@
? 'bg-transparent border-2 border-white' ? 'bg-transparent border-2 border-white'
: 'bg-gray-700/40'} inline-block hover:bg-transparent" : 'bg-gray-700/40'} inline-block hover:bg-transparent"
asset={stackedAsset} asset={stackedAsset}
on:click={() => (asset = stackedAsset)} on:click={() => {
asset = stackedAsset;
preloadAssets = index + 1 >= $stackAssetsStore.length ? [] : [$stackAssetsStore[index + 1]];
}}
on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)} on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)}
readonly readonly
thumbnailSize={stackedAsset.id == asset.id ? 65 : 60} thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}

View File

@ -7,7 +7,7 @@
import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { isWebCompatibleImage } from '$lib/utils/asset-utils';
import { getBoundingBox } from '$lib/utils/people-utils'; import { getBoundingBox } from '$lib/utils/people-utils';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; 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 { useZoomImageWheel } from '@zoom-image/svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@ -16,6 +16,7 @@
import { getAltText } from '$lib/utils/thumbnail-util'; import { getAltText } from '$lib/utils/thumbnail-util';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let preloadAssets: AssetResponseDto[] | null = null;
export let element: HTMLDivElement | undefined = undefined; export let element: HTMLDivElement | undefined = undefined;
export let haveFadeTransition = true; export let haveFadeTransition = true;
@ -25,6 +26,7 @@
let hasZoomed = false; let hasZoomed = false;
let copyImageToClipboard: (source: string) => Promise<Blob>; let copyImageToClipboard: (source: string) => Promise<Blob>;
let canCopyImagesToClipboard: () => boolean; let canCopyImagesToClipboard: () => boolean;
let imageLoaded: boolean = false;
const loadOriginalByDefault = $alwaysLoadOriginalFile && isWebCompatibleImage(asset); const loadOriginalByDefault = $alwaysLoadOriginalFile && isWebCompatibleImage(asset);
@ -41,6 +43,9 @@
const module = await import('copy-image-clipboard'); const module = await import('copy-image-clipboard');
copyImageToClipboard = module.copyImageToClipboard; copyImageToClipboard = module.copyImageToClipboard;
canCopyImagesToClipboard = module.canCopyImagesToClipboard; canCopyImagesToClipboard = module.canCopyImagesToClipboard;
imageLoaded = false;
await loadAssetData({ loadOriginal: loadOriginalByDefault });
}); });
onDestroy(() => { onDestroy(() => {
@ -60,8 +65,22 @@
}); });
assetData = URL.createObjectURL(data); 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 { } catch {
// Do nothing imageLoaded = false;
} }
}; };
@ -128,9 +147,9 @@
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
class="flex h-full select-none place-content-center place-items-center" class="flex h-full select-none place-content-center place-items-center"
> >
{#await loadAssetData({ loadOriginal: loadOriginalByDefault })} {#if !imageLoaded}
<LoadingSpinner /> <LoadingSpinner />
{:then} {:else}
<div bind:this={imgElement} class="h-full w-full"> <div bind:this={imgElement} class="h-full w-full">
<img <img
bind:this={$photoViewer} bind:this={$photoViewer}
@ -147,5 +166,5 @@
/> />
{/each} {/each}
</div> </div>
{/await} {/if}
</div> </div>

View File

@ -38,7 +38,7 @@
const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } =
assetInteractionStore; assetInteractionStore;
const viewport: Viewport = { width: 0, height: 0 }; const viewport: Viewport = { width: 0, height: 0 };
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore; let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets } = assetViewingStore;
let element: HTMLElement; let element: HTMLElement;
let showShortcuts = false; let showShortcuts = false;
let showSkeleton = true; let showSkeleton = true;
@ -141,8 +141,12 @@
const handlePrevious = async () => { const handlePrevious = async () => {
const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id); const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id);
if (previousAsset) { if (previousAsset) {
await assetViewingStore.setAssetId(previousAsset); const preloadId = await assetStore.getPreviousAssetId(previousAsset);
preloadId
? await assetViewingStore.setAssetId(previousAsset, [preloadId])
: await assetViewingStore.setAssetId(previousAsset);
} }
return !!previousAsset; return !!previousAsset;
@ -150,8 +154,12 @@
const handleNext = async () => { const handleNext = async () => {
const nextAsset = await assetStore.getNextAssetId($viewingAsset.id); const nextAsset = await assetStore.getNextAssetId($viewingAsset.id);
if (nextAsset) { if (nextAsset) {
await assetViewingStore.setAssetId(nextAsset); const preloadId = await assetStore.getNextAssetId(nextAsset);
preloadId
? await assetViewingStore.setAssetId(nextAsset, [preloadId])
: await assetViewingStore.setAssetId(nextAsset);
} }
return !!nextAsset; return !!nextAsset;
@ -455,6 +463,7 @@
{withStacked} {withStacked}
{assetStore} {assetStore}
asset={$viewingAsset} asset={$viewingAsset}
preloadAssets={$preloadAssets}
{isShared} {isShared}
{album} {album}
on:previous={handlePrevious} on:previous={handlePrevious}

View File

@ -4,10 +4,23 @@ import { writable } from 'svelte/store';
function createAssetViewingStore() { function createAssetViewingStore() {
const viewingAssetStoreState = writable<AssetResponseDto>(); const viewingAssetStoreState = writable<AssetResponseDto>();
const preloadAssets = writable<AssetResponseDto[]>([]);
const viewState = writable<boolean>(false); const viewState = writable<boolean>(false);
const setAssetId = async (id: string) => { const setAssetId = async (id: string, preloadIds?: string[]) => {
const data = await getAssetInfo({ id, key: getKey() }); 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); viewingAssetStoreState.set(data);
viewState.set(true); viewState.set(true);
}; };
@ -20,6 +33,9 @@ function createAssetViewingStore() {
asset: { asset: {
subscribe: viewingAssetStoreState.subscribe, subscribe: viewingAssetStoreState.subscribe,
}, },
preloadAssets: {
subscribe: preloadAssets.subscribe,
},
isViewing: { isViewing: {
subscribe: viewState.subscribe, subscribe: viewState.subscribe,
set: viewState.set, set: viewState.set,