diff --git a/web/src/lib/components/asset-viewer/album-list-item.svelte b/web/src/lib/components/asset-viewer/album-list-item.svelte
index f2cb181e4c..d97a40ae48 100644
--- a/web/src/lib/components/asset-viewer/album-list-item.svelte
+++ b/web/src/lib/components/asset-viewer/album-list-item.svelte
@@ -2,6 +2,7 @@
import { getAssetThumbnailUrl } from '$lib/utils';
import { ThumbnailFormat, type AlbumResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
+ import { normalizeSearchString } from '$lib/utils/string-utils.js';
const dispatch = createEventDispatcher<{
album: void;
@@ -16,7 +17,7 @@
// It is used to highlight the search query in the album name
$: {
let { albumName } = album;
- let findIndex = albumName.toLowerCase().indexOf(searchQuery.toLowerCase());
+ let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery));
let findLength = searchQuery.length;
albumNameArray = [
albumName.slice(0, findIndex),
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 51da304114..be5d13e5f8 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -1,7 +1,6 @@
@@ -55,7 +67,7 @@
transition:fade={{ duration: 250 }}
style:background-color={backgroundColor[notification.type]}
style:border-color={borderColor[notification.type]}
- class="border z-[999999] mb-4 min-h-[80px] w-[300px] rounded-2xl p-4 shadow-md hover:cursor-pointer"
+ class="border z-[999999] mb-4 min-h-[80px] w-[300px] rounded-2xl p-4 shadow-md {hoverStyle}"
on:click={handleClick}
on:keydown={handleClick}
>
@@ -72,6 +84,22 @@
- {notification.message}
+ {#if notification.html}
+
+ {@html notification.message}
+ {:else}
+ {notification.message}
+ {/if}
+
+ {#if notification.button}
+
+
+
+ {/if}
diff --git a/web/src/lib/components/shared-components/notification/notification.ts b/web/src/lib/components/shared-components/notification/notification.ts
index 52e75bf41f..dfe4e1f923 100644
--- a/web/src/lib/components/shared-components/notification/notification.ts
+++ b/web/src/lib/components/shared-components/notification/notification.ts
@@ -6,20 +6,31 @@ export enum NotificationType {
Warning = 'Warning',
}
+export type NotificationButton = {
+ text: string;
+ onClick: () => unknown;
+};
+
export type Notification = {
id: number;
type: NotificationType;
message: string;
+ /**
+ * Allow HTML to be inserted within the message. Make sure to verify/encode
+ * variables that may be interpoalted into 'message'
+ */
+ html?: boolean;
/** The action to take when the notification is clicked */
action: NotificationAction;
+ button?: NotificationButton;
/** Timeout in miliseconds */
timeout: number;
};
type DiscardAction = { type: 'discard' };
type NoopAction = { type: 'noop' };
-type LinkAction = { type: 'link'; target: string };
-export type NotificationAction = DiscardAction | NoopAction | LinkAction;
+
+export type NotificationAction = DiscardAction | NoopAction;
export type NotificationOptions = Partial> & { message: string };
@@ -32,7 +43,9 @@ function createNotificationList() {
currentList.push({
id: count++,
type: NotificationType.Info,
- action: { type: 'discard' },
+ action: {
+ type: options.button ? 'noop' : 'discard',
+ },
timeout: 3000,
...options,
});
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 27c99a4731..532479912a 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -1,15 +1,18 @@
-import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
+import { goto } from '$app/navigation';
+import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
+import { AppRoute } from '$lib/constants';
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
import { downloadManager } from '$lib/stores/download';
import { downloadRequest, getKey } from '$lib/utils';
+import { encodeHTMLSpecialChars } from '$lib/utils/string-utils';
import {
addAssetsToAlbum as addAssets,
+ createAlbum,
defaults,
getDownloadInfo,
type AssetResponseDto,
type AssetTypeEnum,
- type BulkIdResponseDto,
type DownloadInfoDto,
type DownloadResponseDto,
type UserResponseDto,
@@ -18,20 +21,60 @@ import { DateTime } from 'luxon';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
-export const addAssetsToAlbum = async (albumId: string, assetIds: Array): Promise =>
- addAssets({
+export const addAssetsToAlbum = async (albumId: string, assetIds: string[]) => {
+ const result = await addAssets({
id: albumId,
- bulkIdsDto: { ids: assetIds },
+ bulkIdsDto: {
+ ids: assetIds,
+ },
key: getKey(),
- }).then((results) => {
- const count = results.filter(({ success }) => success).length;
+ });
+ const count = result.filter(({ success }) => success).length;
+ notificationController.show({
+ type: NotificationType.Info,
+ timeout: 5000,
+ message:
+ count > 0
+ ? `Added ${count} asset${count === 1 ? '' : 's'} to the album`
+ : `Asset${assetIds.length === 1 ? ' was' : 's were'} already part of the album`,
+ button: {
+ text: 'View Album',
+ onClick() {
+ return goto(`${AppRoute.ALBUMS}/${albumId}`);
+ },
+ },
+ });
+};
+
+export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[]) => {
+ try {
+ const album = await createAlbum({
+ createAlbumDto: {
+ albumName,
+ assetIds,
+ },
+ });
+ const displayName = albumName ? `${encodeHTMLSpecialChars(albumName)}` : 'new album';
notificationController.show({
type: NotificationType.Info,
- message: `Added ${count} asset${count === 1 ? '' : 's'}`,
+ timeout: 5000,
+ message: `Added ${assetIds.length} asset${assetIds.length === 1 ? '' : 's'} to ${displayName}`,
+ html: true,
+ button: {
+ text: 'View Album',
+ onClick() {
+ return goto(`${AppRoute.ALBUMS}/${album.id}`);
+ },
+ },
});
-
- return results;
- });
+ return album;
+ } catch {
+ notificationController.show({
+ type: NotificationType.Error,
+ message: 'Failed to create album',
+ });
+ }
+};
export const downloadBlob = (data: Blob, filename: string) => {
const url = URL.createObjectURL(data);
diff --git a/web/src/lib/utils/string-utils.ts b/web/src/lib/utils/string-utils.ts
new file mode 100644
index 0000000000..b58f859f62
--- /dev/null
+++ b/web/src/lib/utils/string-utils.ts
@@ -0,0 +1,16 @@
+export const removeAccents = (str: string) => {
+ return str.normalize('NFD').replaceAll(/[\u0300-\u036F]/g, '');
+};
+
+export const normalizeSearchString = (str: string) => {
+ return removeAccents(str.toLocaleLowerCase());
+};
+
+export const encodeHTMLSpecialChars = (str: string) => {
+ return str
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+};