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:
parent
582cdcab82
commit
ab4b8eca15
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user