diff --git a/i18n/en.json b/i18n/en.json index 7adbba3c88..8d593d9e87 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -993,12 +993,14 @@ "error": "Error", "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Error deleting face from asset", + "error_getting_asset_information": "Error getting asset information: {message}", "error_getting_places": "Error getting places", "error_loading_image": "Error loading image", "error_loading_partners": "Error loading partners: {error}", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates", "error_title": "Error - Something went wrong", + "error_while_navigating": "Error while navigating: {message}", "errors": { "cannot_navigate_next_asset": "Cannot navigate to the next asset", "cannot_navigate_previous_asset": "Cannot navigate to previous asset", diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 296e2af2d0..82760c8cf4 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -226,31 +226,36 @@ return; } - void tracker.invoke(async () => { - let hasNext = false; + void tracker.invoke( + async () => { + let hasNext = false; - if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { - hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); - if (!hasNext) { - const asset = await onRandom?.(); - if (asset) { - slideshowHistory.queue(asset); - hasNext = true; + if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { + hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); + if (!hasNext) { + const asset = await onRandom?.(); + if (asset) { + slideshowHistory.queue(asset); + hasNext = true; + } + } + } else { + hasNext = + order === 'previous' + ? await navigateToAsset(cursor.previousAsset) + : await navigateToAsset(cursor.nextAsset); + } + + if ($slideshowState === SlideshowState.PlaySlideshow) { + if (hasNext) { + $restartSlideshowProgress = true; + } else { + await handleStopSlideshow(); } } - } else { - hasNext = - order === 'previous' ? await navigateToAsset(cursor.previousAsset) : await navigateToAsset(cursor.nextAsset); - } - - if ($slideshowState === SlideshowState.PlaySlideshow) { - if (hasNext) { - $restartSlideshowProgress = true; - } else { - await handleStopSlideshow(); - } - } - }); + }, + (error: Error) => $t('error_while_navigating', { values: { message: error.message } }), + ); }; // const showEditor = () => { diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte index 06ff61d180..95cc55dd9e 100644 --- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte +++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte @@ -11,10 +11,12 @@ import { handlePromiseError } from '$lib/utils'; import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { navigateToAsset } from '$lib/utils/asset-utils'; + import { handleErrorAsync } from '$lib/utils/handle-error'; import { navigate } from '$lib/utils/navigation'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk'; import { onDestroy, onMount, untrack } from 'svelte'; + import { t } from 'svelte-i18n'; let { asset: viewingAsset, gridScrollTarget } = assetViewingStore; @@ -38,28 +40,27 @@ person, }: Props = $props(); - const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { - const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset); - if (earlierTimelineAsset) { - const asset = await assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id }); - if (preload) { - // also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete - void getNextAsset(asset, false); - } - return asset; - } + const getAsset = (id: string) => { + return handleErrorAsync( + () => assetCacheManager.getAsset({ ...authManager.params, id }), + (error: Error) => $t('error_getting_asset_information', { values: { message: error.message } }), + ); }; - const getPreviousAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { - const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset); - if (laterTimelineAsset) { - const asset = await assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id }); - if (preload) { - // also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete - void getPreviousAsset(asset, false); - } - return asset; + const getNextAsset = async (currentAsset: AssetResponseDto) => { + const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset); + if (!earlierTimelineAsset) { + return; } + return getAsset(earlierTimelineAsset.id); + }; + + const getPreviousAsset = async (currentAsset: AssetResponseDto) => { + const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset); + if (!laterTimelineAsset) { + return; + } + return getAsset(laterTimelineAsset.id); }; let assetCursor = $state({ @@ -87,10 +88,12 @@ const handleRandom = async () => { const randomAsset = await timelineManager.getRandomAsset(); - if (randomAsset) { - await navigate({ targetRoute: 'current', assetId: randomAsset.id }); - return { id: randomAsset.id }; + if (!randomAsset) { + return; } + + await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + return { id: randomAsset.id }; }; const handleClose = async (asset: { id: string }) => { @@ -180,12 +183,14 @@ }; const handleUndoDelete = async (assets: TimelineAsset[]) => { timelineManager.upsertAssets(assets); - if (assets.length > 0) { - const restoredAsset = assets[0]; - const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); - assetViewingStore.setAsset(asset); - await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); + if (assets.length === 0) { + return; } + + const restoredAsset = assets[0]; + const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); + assetViewingStore.setAsset(asset); + await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); }; const handleUpdateOrUpload = (asset: AssetResponseDto) => { diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts index 6db28123c2..e9a94730f8 100644 --- a/web/src/lib/utils/handle-error.ts +++ b/web/src/lib/utils/handle-error.ts @@ -19,12 +19,17 @@ export function getServerErrorMessage(error: unknown) { return data?.message || error.message; } -export function handleError(error: unknown, message: string) { - if ((error as Error)?.name === 'AbortError') { +export function standardizeError(error: unknown) { + return error instanceof Error ? error : new Error(String(error)); +} + +export function handleError(error: unknown, localizedMessage: string) { + const standardizedError = standardizeError(error); + if (standardizedError.name === 'AbortError') { return; } - console.error(`[handleError]: ${message}`, error, (error as Error)?.stack); + console.error(`[handleError]: ${standardizedError}`, error, standardizedError.stack); try { let serverMessage = getServerErrorMessage(error); @@ -32,13 +37,25 @@ export function handleError(error: unknown, message: string) { serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`; } - const errorMessage = serverMessage || message; + const errorMessage = serverMessage || localizedMessage; toastManager.danger(errorMessage); return errorMessage; } catch (error) { console.error(error); - return message; + return localizedMessage; + } +} + +export async function handleErrorAsync( + fn: () => Promise, + messageGenerator: (error: Error) => string, +): Promise { + try { + return await fn(); + } catch (error: unknown) { + handleError(error, messageGenerator(standardizeError(error))); + return undefined; } } diff --git a/web/src/lib/utils/invocationTracker.ts b/web/src/lib/utils/invocationTracker.ts index 7d42d8c613..1d0cd13f1a 100644 --- a/web/src/lib/utils/invocationTracker.ts +++ b/web/src/lib/utils/invocationTracker.ts @@ -1,3 +1,5 @@ +import { handleError, standardizeError } from '$lib/utils/handle-error'; + /** * Tracks the state of asynchronous invocations to handle race conditions and stale operations. * This class helps manage concurrent operations by tracking which invocations are active @@ -51,10 +53,12 @@ export class InvocationTracker { return this.invocationsStarted !== this.invocationsEnded; } - async invoke(invocable: () => Promise) { + async invoke(invocable: () => Promise, messageGenerator: (error: Error) => string) { const invocation = this.startInvocation(); try { return await invocable(); + } catch (error: unknown) { + handleError(error, messageGenerator(standardizeError(error))); } finally { invocation.endInvocation(); } diff --git a/web/src/lib/utils/people-utils.ts b/web/src/lib/utils/people-utils.ts index 5fb03842b8..28425b948f 100644 --- a/web/src/lib/utils/people-utils.ts +++ b/web/src/lib/utils/people-utils.ts @@ -24,11 +24,11 @@ export interface boundingBox { export const getBoundingBox = ( faces: Faces[], zoom: ZoomImageWheelState, - photoViewer: HTMLImageElement | null, + photoViewer: HTMLImageElement | undefined, ): boundingBox[] => { const boxes: boundingBox[] = []; - if (photoViewer === null) { + if (!photoViewer) { return boxes; } const clientHeight = photoViewer.clientHeight; @@ -93,7 +93,7 @@ export const zoomImageToBase64 = async ( image = img; } - if (image === null) { + if (!image) { return null; } const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2, imageWidth, imageHeight } = face; @@ -121,11 +121,9 @@ export const zoomImageToBase64 = async ( canvas.height = faceHeight; const context = canvas.getContext('2d'); - if (context) { - context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight); - - return canvas.toDataURL(); - } else { + if (!context) { return null; } + context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight); + return canvas.toDataURL(); };