feat: ability to check if asset image is cached

This commit is contained in:
midzelis
2025-12-05 15:19:44 +00:00
parent f86455873d
commit f711b320b1
6 changed files with 97 additions and 10 deletions

View File

@@ -295,13 +295,13 @@
}),
);
let thumbnailPreloaded = $state(false);
// $effect(() => {
// // eslint-disable-next-line @typescript-eslint/no-unused-expressions
// asset;
// untrack(() => {
// void preloadManager.isUrlPreloaded(thumbnailUrl).then((preloaded) => (thumbnailPreloaded = preloaded));
// });
// });
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
asset;
untrack(() => {
void preloadManager.isUrlPreloaded(thumbnailUrl).then((preloaded) => (thumbnailPreloaded = preloaded));
});
});
const exifDimensions = $derived(
asset.exifInfo?.exifImageHeight && asset.exifInfo.exifImageHeight

View File

@@ -3,6 +3,7 @@
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
import { Icon } from '@immich/ui';
import { mdiEyeOffOutline } from '@mdi/js';
import { untrack } from 'svelte';
import type { ActionReturn } from 'svelte/action';
import type { ClassValue } from 'svelte/elements';
@@ -64,6 +65,14 @@
};
}
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
url;
untrack(() => {
preloadManager.loading(url);
});
});
let optionalClasses = $derived(
[
curve && 'rounded-xl',

View File

@@ -1,8 +1,15 @@
import { getAssetUrl } from '$lib/utils';
import { cancelImageUrl, preloadImageUrl } from '$lib/utils/sw-messaging';
import { cancelImageUrl, isImageUrlCached, preloadImageUrl } from '$lib/utils/sw-messaging';
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
class PreloadManager {
#cachedImages = new Set<string>();
loading(url: string) {
if (!globalThis.isSecureContext) {
this.#cachedImages.add(url);
}
}
preload(asset: AssetResponseDto | undefined | null) {
if (globalThis.isSecureContext) {
preloadImageUrl(getAssetUrl({ asset }));
@@ -33,6 +40,24 @@ class PreloadManager {
}
cancelImageUrl(url);
}
isPreloaded(asset: AssetResponseDto | undefined) {
if (!asset) {
return Promise.resolve(false);
}
const url = getAssetUrl({ asset });
return this.isUrlPreloaded(url);
}
isUrlPreloaded(url: string | undefined | null) {
if (!url) {
return Promise.resolve(false);
}
if (globalThis.isSecureContext) {
return isImageUrlCached(url);
}
return Promise.resolve(this.#cachedImages.has(url));
}
}
export const preloadManager = new PreloadManager();

View File

@@ -1,14 +1,48 @@
const broadcast = new BroadcastChannel('immich');
let isLoadedReplyListeners: ((url: string, isUrlCached: boolean) => void)[] = [];
broadcast.addEventListener('message', (event) => {
if (event.data.type == 'isImageUrlCachedReply') {
for (const listener of isLoadedReplyListeners) {
listener(event.data.url, event.data.isImageUrlCached);
}
}
});
export function cancelImageUrl(url: string | undefined | null) {
if (!url) {
return;
}
broadcast.postMessage({ type: 'cancel', url });
}
export function preloadImageUrl(url: string | undefined | null) {
if (!url) {
return;
}
broadcast.postMessage({ type: 'preload', url });
}
export function isImageUrlCached(url: string | undefined | null): Promise<boolean> {
if (!url) {
return Promise.resolve(false);
}
if (!globalThis.isSecureContext) {
return Promise.resolve(false);
}
return new Promise((resolve) => {
const listener = (urlReply: string, isUrlCached: boolean) => {
if (urlReply === url) {
cleanup(isUrlCached);
}
};
const cleanup = (isUrlCached: boolean) => {
isLoadedReplyListeners = isLoadedReplyListeners.filter((element) => element !== listener);
resolve(isUrlCached);
};
isLoadedReplyListeners.push(listener);
broadcast.postMessage({ type: 'isImageUrlCached', url });
setTimeout(() => cleanup(false), 5000);
});
}

View File

@@ -1,7 +1,8 @@
import { handleCancel, handlePreload } from './request';
import { handleCancel, handleIsUrlCached, handlePreload } from './request';
export const broadcast = new BroadcastChannel('immich');
export const installBroadcastChannelListener = () => {
const broadcast = new BroadcastChannel('immich');
// eslint-disable-next-line unicorn/prefer-add-event-listener
broadcast.onmessage = (event) => {
if (!event.data) {
@@ -20,6 +21,15 @@ export const installBroadcastChannelListener = () => {
handleCancel(url);
break;
}
case 'isImageUrlCached': {
void handleIsUrlCached(url);
break;
}
}
};
};
export const replyIsImageUrlCached = (url: string, isImageUrlCached: boolean) => {
broadcast.postMessage({ type: 'isImageUrlCachedReply', url, isImageUrlCached });
};

View File

@@ -1,3 +1,4 @@
import { replyIsImageUrlCached } from './broadcast-channel';
import { get, put } from './cache';
const pendingRequests = new Map<string, { abort: AbortController; callbacks: ((canceled: boolean) => void)[] }>();
@@ -93,3 +94,11 @@ export const handleCancel = (url: URL) => {
requestSignals.abort.abort();
};
export const handleIsUrlCached = async (url: URL) => {
const cacheKey = getCacheKey(url);
const isImageUrlCached = !!(await get(cacheKey));
console.log('cacheKey', cacheKey, isImageUrlCached);
replyIsImageUrlCached(url.pathname + url.search + url.hash, isImageUrlCached);
};