feat: toasts (#23298)

This commit is contained in:
Jason Rasmussen
2025-10-28 15:09:11 -04:00
committed by GitHub
parent 106effca2e
commit 52596255c8
80 changed files with 341 additions and 1069 deletions

View File

@@ -1,8 +1,9 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import ToastAction from '$lib/components/ToastAction.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { StackResponse } from '$lib/utils/asset-utils';
import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets } from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
@@ -31,17 +32,27 @@ export const deleteAssets = async (
await deleteBulk({ assetBulkDeleteDto: { ids, force } });
onAssetDelete(ids);
notificationController.show({
message: force
? $t('assets_permanently_deleted_count', { values: { count: ids.length } })
: $t('assets_trashed_count', { values: { count: ids.length } }),
type: NotificationType.Info,
...(onUndoDelete &&
!force && {
button: { text: $t('undo'), onClick: () => undoDeleteAssets(onUndoDelete, assets) },
timeout: 5000,
}),
});
toastManager.custom(
{
component: ToastAction,
props: {
title: $t('success'),
description: force
? $t('assets_permanently_deleted_count')
: $t('assets_trashed_count', { values: { count: ids.length } }),
color: 'success',
button:
onUndoDelete && !force
? {
color: 'secondary',
text: $t('undo'),
onClick: () => undoDeleteAssets(onUndoDelete, assets),
}
: undefined,
},
},
{ timeout: 5000 },
);
} catch (error) {
handleError(error, $t('errors.unable_to_delete_assets'));
}

View File

@@ -1,5 +1,5 @@
import { goto } from '$app/navigation';
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import ToastAction from '$lib/components/ToastAction.svelte';
import { AppRoute } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { downloadManager } from '$lib/managers/download-manager.svelte';
@@ -39,6 +39,7 @@ import {
type UserPreferencesResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
@@ -57,23 +58,29 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show
const $t = get(t);
if (showNotification) {
let message = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
if (count > 0) {
message = $t('assets_added_to_album_count', { values: { count } });
description = $t('assets_added_to_album_count', { values: { count } });
} else if (duplicateErrorCount > 0) {
message = $t('assets_were_part_of_album_count', { values: { count: duplicateErrorCount } });
description = $t('assets_were_part_of_album_count', { values: { count: duplicateErrorCount } });
}
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
message,
button: {
text: $t('view_album'),
onClick() {
return goto(`${AppRoute.ALBUMS}/${albumId}`);
toastManager.custom(
{
component: ToastAction,
props: {
title: $t('info'),
color: 'info',
description,
button: {
text: $t('view_album'),
onClick() {
return goto(`${AppRoute.ALBUMS}/${albumId}`);
},
},
},
},
});
{ timeout: 5000 },
);
}
};
@@ -94,31 +101,16 @@ export const addAssetsToAlbums = async (albumIds: string[], assetIds: string[],
const $t = get(t);
if (result.error === BulkIdErrorReason.Duplicate) {
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
message: $t('assets_were_part_of_albums_count', { values: { count: assetIds.length } }),
});
toastManager.info($t('assets_were_part_of_albums_count', { values: { count: assetIds.length } }));
return result;
}
if (result.error) {
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
message: $t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } }),
});
toastManager.warning($t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } }));
return result;
}
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
message: $t('assets_added_to_albums_count', {
values: {
albumTotal: albumIds.length,
assetTotal: assetIds.length,
},
}),
});
toastManager.success(
$t('assets_added_to_albums_count', { values: { albumTotal: albumIds.length, assetTotal: assetIds.length } }),
);
return result;
}
};
@@ -136,10 +128,7 @@ export const tagAssets = async ({
if (showNotification) {
const $t = await getFormatter();
notificationController.show({
message: $t('tagged_assets', { values: { count: assetIds.length } }),
type: NotificationType.Info,
});
toastManager.success($t('tagged_assets', { values: { count: assetIds.length } }));
}
return assetIds;
@@ -160,10 +149,7 @@ export const removeTag = async ({
if (showNotification) {
const $t = await getFormatter();
notificationController.show({
message: $t('removed_tagged_assets', { values: { count: assetIds.length } }),
type: NotificationType.Info,
});
toastManager.success($t('removed_tagged_assets', { values: { count: assetIds.length } }));
}
return assetIds;
@@ -286,11 +272,7 @@ export const downloadFile = async (asset: AssetResponseDto) => {
}
try {
notificationController.show({
type: NotificationType.Info,
message: $t('downloading_asset_filename', { values: { filename: asset.originalFileName } }),
});
toastManager.success($t('downloading_asset_filename', { values: { filename: asset.originalFileName } }));
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}` : ''), filename);
} catch (error) {
handleError(error, $t('errors.error_downloading', { values: { filename } }));
@@ -411,10 +393,7 @@ export const getOwnedAssetsWithWarning = (assets: TimelineAsset[], user: UserRes
const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
if (numberOfIssues > 0) {
const $t = get(t);
notificationController.show({
message: $t('errors.cant_change_metadata_assets_count', { values: { count: numberOfIssues } }),
type: NotificationType.Warning,
});
toastManager.warning($t('errors.cant_change_metadata_assets_count', { values: { count: numberOfIssues } }));
}
return ids;
};
@@ -434,12 +413,16 @@ export const stackAssets = async (assets: { id: string }[], showNotification = t
try {
const stack = await createStack({ stackCreateDto: { assetIds: assets.map(({ id }) => id) } });
if (showNotification) {
notificationController.show({
message: $t('stacked_assets_count', { values: { count: stack.assets.length } }),
type: NotificationType.Info,
button: {
text: $t('view_stack'),
onClick: () => navigate({ targetRoute: 'current', assetId: stack.primaryAssetId }),
toastManager.custom({
component: ToastAction,
props: {
title: $t('success'),
description: $t('stacked_assets_count', { values: { count: stack.assets.length } }),
color: 'success',
button: {
text: $t('view_stack'),
onClick: () => navigate({ targetRoute: 'current', assetId: stack.primaryAssetId }),
},
},
});
}
@@ -468,10 +451,7 @@ export const deleteStack = async (stackIds: string[]) => {
await deleteStacks({ bulkIdsDto: { ids: [...ids] } });
notificationController.show({
type: NotificationType.Info,
message: $t('unstacked_assets_count', { values: { count } }),
});
toastManager.success($t('unstacked_assets_count', { values: { count } }));
const assets = stacks.flatMap((stack) => stack.assets);
for (const asset of assets) {
@@ -492,10 +472,7 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S
await deleteAssets({ assetBulkDeleteDto: { ids: assetsToDeleteIds } });
await deleteStacks({ bulkIdsDto: { ids: [stack.id] } });
notificationController.show({
type: NotificationType.Info,
message: $t('kept_this_deleted_others', { values: { count: assetsToDeleteIds.length } }),
});
toastManager.success($t('kept_this_deleted_others', { values: { count: assetsToDeleteIds.length } }));
keepAsset.stack = null;
return keepAsset;
@@ -548,11 +525,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
});
asset.isArchived = data.isArchived;
notificationController.show({
type: NotificationType.Info,
message: asset.isArchived ? $t(`added_to_archive`) : $t(`removed_from_archive`),
});
toastManager.success(asset.isArchived ? $t(`added_to_archive`) : $t(`removed_from_archive`));
} catch (error) {
handleError(error, $t('errors.unable_to_add_remove_archive', { values: { archived: asset.isArchived } }));
}
@@ -571,13 +544,11 @@ export const archiveAssets = async (assets: { id: string }[], visibility: AssetV
});
}
notificationController.show({
message:
visibility === AssetVisibility.Archive
? $t('archived_count', { values: { count: ids.length } })
: $t('unarchived_count', { values: { count: ids.length } }),
type: NotificationType.Info,
});
toastManager.success(
visibility === AssetVisibility.Archive
? $t('archived_count', { values: { count: ids.length } })
: $t('unarchived_count', { values: { count: ids.length } }),
);
} catch (error) {
handleError(
error,

View File

@@ -1,5 +1,5 @@
import { isHttpError } from '@immich/sdk';
import { notificationController, NotificationType } from '../components/shared-components/notification/notification';
import { toastManager } from '@immich/ui';
export function getServerErrorMessage(error: unknown) {
if (!isHttpError(error)) {
@@ -34,7 +34,7 @@ export function handleError(error: unknown, message: string) {
const errorMessage = serverMessage || message;
notificationController.show({ message: errorMessage, type: NotificationType.Error });
toastManager.danger(errorMessage);
return errorMessage;
} catch (error) {