mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	feat(web): better UX when creating a new album (#8270)
* feat(web): ask user before going to newly created album * feat(web): add button option to notification cards * feat(web): allow html messages in notification cards * show album -> view album * remove 'link' action from notifications * remove unused type
This commit is contained in:
		
							parent
							
								
									613b544bf0
								
							
						
					
					
						commit
						8bf571bf48
					
				@ -2,6 +2,7 @@
 | 
				
			|||||||
  import { getAssetThumbnailUrl } from '$lib/utils';
 | 
					  import { getAssetThumbnailUrl } from '$lib/utils';
 | 
				
			||||||
  import { ThumbnailFormat, type AlbumResponseDto } from '@immich/sdk';
 | 
					  import { ThumbnailFormat, type AlbumResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { createEventDispatcher } from 'svelte';
 | 
					  import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					  import { normalizeSearchString } from '$lib/utils/string-utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const dispatch = createEventDispatcher<{
 | 
					  const dispatch = createEventDispatcher<{
 | 
				
			||||||
    album: void;
 | 
					    album: void;
 | 
				
			||||||
@ -16,7 +17,7 @@
 | 
				
			|||||||
  // It is used to highlight the search query in the album name
 | 
					  // It is used to highlight the search query in the album name
 | 
				
			||||||
  $: {
 | 
					  $: {
 | 
				
			||||||
    let { albumName } = album;
 | 
					    let { albumName } = album;
 | 
				
			||||||
    let findIndex = albumName.toLowerCase().indexOf(searchQuery.toLowerCase());
 | 
					    let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery));
 | 
				
			||||||
    let findLength = searchQuery.length;
 | 
					    let findLength = searchQuery.length;
 | 
				
			||||||
    albumNameArray = [
 | 
					    albumNameArray = [
 | 
				
			||||||
      albumName.slice(0, findIndex),
 | 
					      albumName.slice(0, findIndex),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
  import { AppRoute, AssetAction, ProjectionType } from '$lib/constants';
 | 
					  import { AssetAction, ProjectionType } from '$lib/constants';
 | 
				
			||||||
  import { updateNumberOfComments } from '$lib/stores/activity.store';
 | 
					  import { updateNumberOfComments } from '$lib/stores/activity.store';
 | 
				
			||||||
  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
					  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
				
			||||||
  import type { AssetStore } from '$lib/stores/assets.store';
 | 
					  import type { AssetStore } from '$lib/stores/assets.store';
 | 
				
			||||||
@ -11,7 +10,7 @@
 | 
				
			|||||||
  import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
 | 
					  import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
 | 
				
			||||||
  import { user } from '$lib/stores/user.store';
 | 
					  import { user } from '$lib/stores/user.store';
 | 
				
			||||||
  import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
 | 
					  import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
 | 
				
			||||||
  import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
 | 
					  import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { shortcuts } from '$lib/utils/shortcut';
 | 
					  import { shortcuts } from '$lib/utils/shortcut';
 | 
				
			||||||
  import { SlideshowHistory } from '$lib/utils/slideshow-history';
 | 
					  import { SlideshowHistory } from '$lib/utils/slideshow-history';
 | 
				
			||||||
@ -20,7 +19,6 @@
 | 
				
			|||||||
    AssetTypeEnum,
 | 
					    AssetTypeEnum,
 | 
				
			||||||
    ReactionType,
 | 
					    ReactionType,
 | 
				
			||||||
    createActivity,
 | 
					    createActivity,
 | 
				
			||||||
    createAlbum,
 | 
					 | 
				
			||||||
    deleteActivity,
 | 
					    deleteActivity,
 | 
				
			||||||
    deleteAssets,
 | 
					    deleteAssets,
 | 
				
			||||||
    getActivities,
 | 
					    getActivities,
 | 
				
			||||||
@ -390,8 +388,7 @@
 | 
				
			|||||||
  const handleAddToNewAlbum = async (albumName: string) => {
 | 
					  const handleAddToNewAlbum = async (albumName: string) => {
 | 
				
			||||||
    isShowAlbumPicker = false;
 | 
					    isShowAlbumPicker = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const album = await createAlbum({ createAlbumDto: { albumName, assetIds: [asset.id] } });
 | 
					    await addAssetsToNewAlbum(albumName, [asset.id]);
 | 
				
			||||||
    await goto(`${AppRoute.ALBUMS}/${album.id}`);
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddToAlbum = async (album: AlbumResponseDto) => {
 | 
					  const handleAddToAlbum = async (album: AlbumResponseDto) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,14 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					 | 
				
			||||||
  import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
 | 
					  import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
 | 
				
			||||||
  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 | 
					  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
  import {
 | 
					  import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils';
 | 
				
			||||||
    NotificationType,
 | 
					  import type { AlbumResponseDto } from '@immich/sdk';
 | 
				
			||||||
    notificationController,
 | 
					 | 
				
			||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					 | 
				
			||||||
  import { AppRoute } from '$lib/constants';
 | 
					 | 
				
			||||||
  import { addAssetsToAlbum } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
  import { createAlbum, type AlbumResponseDto } from '@immich/sdk';
 | 
					 | 
				
			||||||
  import { getMenuContext } from '../asset-select-context-menu.svelte';
 | 
					  import { getMenuContext } from '../asset-select-context-menu.svelte';
 | 
				
			||||||
  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
					  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
				
			||||||
  import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
 | 
					  import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let shared = false;
 | 
					  export let shared = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let showAlbumPicker = false;
 | 
					  let showAlbumPicker = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { getAssets, clearSelect } = getAssetControlContext();
 | 
					  const { getAssets, clearSelect } = getAssetControlContext();
 | 
				
			||||||
@ -24,26 +19,12 @@
 | 
				
			|||||||
    closeMenu();
 | 
					    closeMenu();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddToNewAlbum = (albumName: string) => {
 | 
					  const handleAddToNewAlbum = async (albumName: string) => {
 | 
				
			||||||
    showAlbumPicker = false;
 | 
					    showAlbumPicker = false;
 | 
				
			||||||
 | 
					    closeMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const assetIds = [...getAssets()].map((asset) => asset.id);
 | 
					    const assetIds = [...getAssets()].map((asset) => asset.id);
 | 
				
			||||||
    createAlbum({ createAlbumDto: { albumName, assetIds } })
 | 
					    await addAssetsToNewAlbum(albumName, assetIds);
 | 
				
			||||||
      .then(async (response) => {
 | 
					 | 
				
			||||||
        const { id, albumName } = response;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        notificationController.show({
 | 
					 | 
				
			||||||
          message: `Added ${assetIds.length} to ${albumName}`,
 | 
					 | 
				
			||||||
          type: NotificationType.Info,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        clearSelect();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await goto(`${AppRoute.ALBUMS}/${id}`);
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch((error) => {
 | 
					 | 
				
			||||||
        console.error(`[add-to-album.svelte]:handleAddToNewAlbum ${error}`, error);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddToAlbum = async (album: AlbumResponseDto) => {
 | 
					  const handleAddToAlbum = async (album: AlbumResponseDto) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@
 | 
				
			|||||||
  import { createEventDispatcher, onMount } from 'svelte';
 | 
					  import { createEventDispatcher, onMount } from 'svelte';
 | 
				
			||||||
  import AlbumListItem from '../asset-viewer/album-list-item.svelte';
 | 
					  import AlbumListItem from '../asset-viewer/album-list-item.svelte';
 | 
				
			||||||
  import BaseModal from './base-modal.svelte';
 | 
					  import BaseModal from './base-modal.svelte';
 | 
				
			||||||
 | 
					  import { normalizeSearchString } from '$lib/utils/string-utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let albums: AlbumResponseDto[] = [];
 | 
					  let albums: AlbumResponseDto[] = [];
 | 
				
			||||||
  let recentAlbums: AlbumResponseDto[] = [];
 | 
					  let recentAlbums: AlbumResponseDto[] = [];
 | 
				
			||||||
@ -30,7 +31,7 @@
 | 
				
			|||||||
    filteredAlbums =
 | 
					    filteredAlbums =
 | 
				
			||||||
      search.length > 0 && albums.length > 0
 | 
					      search.length > 0 && albums.length > 0
 | 
				
			||||||
        ? albums.filter((album) => {
 | 
					        ? albums.filter((album) => {
 | 
				
			||||||
            return album.albumName.toLowerCase().includes(search.toLowerCase());
 | 
					            return normalizeSearchString(album.albumName).includes(normalizeSearchString(search));
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        : albums;
 | 
					        : albums;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -84,7 +85,7 @@
 | 
				
			|||||||
            <Icon path={mdiPlus} size="30" />
 | 
					            <Icon path={mdiPlus} size="30" />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <p class="">
 | 
					          <p class="">
 | 
				
			||||||
            New {shared ? 'Shared ' : ''}Album {#if search.length > 0}<b>{search}</b>{/if}
 | 
					            New Album {#if search.length > 0}<b>{search}</b>{/if}
 | 
				
			||||||
          </p>
 | 
					          </p>
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
        {#if filteredAlbums.length > 0}
 | 
					        {#if filteredAlbums.length > 0}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@
 | 
				
			|||||||
  export let notification: Notification;
 | 
					  export let notification: Notification;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: icon = notification.type === NotificationType.Error ? mdiCloseCircleOutline : mdiInformationOutline;
 | 
					  $: icon = notification.type === NotificationType.Error ? mdiCloseCircleOutline : mdiInformationOutline;
 | 
				
			||||||
 | 
					  $: hoverStyle = notification.action.type === 'discard' ? 'hover:cursor-pointer' : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const backgroundColor: Record<NotificationType, string> = {
 | 
					  const backgroundColor: Record<NotificationType, string> = {
 | 
				
			||||||
    [NotificationType.Info]: '#E0E2F0',
 | 
					    [NotificationType.Info]: '#E0E2F0',
 | 
				
			||||||
@ -31,6 +32,12 @@
 | 
				
			|||||||
    [NotificationType.Warning]: '#D08613',
 | 
					    [NotificationType.Warning]: '#D08613',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const buttonStyle: Record<NotificationType, string> = {
 | 
				
			||||||
 | 
					    [NotificationType.Info]: 'text-white bg-immich-primary hover:bg-immich-primary/75',
 | 
				
			||||||
 | 
					    [NotificationType.Error]: 'text-white bg-immich-error hover:bg-immich-error/75',
 | 
				
			||||||
 | 
					    [NotificationType.Warning]: 'text-white bg-immich-warning hover:bg-immich-warning/75',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onMount(() => {
 | 
					  onMount(() => {
 | 
				
			||||||
    const timeoutId = setTimeout(discard, notification.timeout);
 | 
					    const timeoutId = setTimeout(discard, notification.timeout);
 | 
				
			||||||
    return () => clearTimeout(timeoutId);
 | 
					    return () => clearTimeout(timeoutId);
 | 
				
			||||||
@ -41,11 +48,16 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClick = () => {
 | 
					  const handleClick = () => {
 | 
				
			||||||
    const action = notification.action;
 | 
					    if (notification.action.type === 'discard') {
 | 
				
			||||||
    if (action.type === 'discard') {
 | 
					 | 
				
			||||||
      discard();
 | 
					      discard();
 | 
				
			||||||
    } else if (action.type == 'link') {
 | 
					    }
 | 
				
			||||||
      window.open(action.target);
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleButtonClick = () => {
 | 
				
			||||||
 | 
					    const button = notification.button;
 | 
				
			||||||
 | 
					    if (button) {
 | 
				
			||||||
 | 
					      discard();
 | 
				
			||||||
 | 
					      return notification.button?.onClick();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@ -55,7 +67,7 @@
 | 
				
			|||||||
  transition:fade={{ duration: 250 }}
 | 
					  transition:fade={{ duration: 250 }}
 | 
				
			||||||
  style:background-color={backgroundColor[notification.type]}
 | 
					  style:background-color={backgroundColor[notification.type]}
 | 
				
			||||||
  style:border-color={borderColor[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:click={handleClick}
 | 
				
			||||||
  on:keydown={handleClick}
 | 
					  on:keydown={handleClick}
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
@ -72,6 +84,22 @@
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <p class="whitespace-pre-wrap pl-[28px] pr-[16px] text-sm" data-testid="message">
 | 
					  <p class="whitespace-pre-wrap pl-[28px] pr-[16px] text-sm" data-testid="message">
 | 
				
			||||||
    {notification.message}
 | 
					    {#if notification.html}
 | 
				
			||||||
 | 
					      <!-- eslint-disable-next-line svelte/no-at-html-tags -->
 | 
				
			||||||
 | 
					      {@html notification.message}
 | 
				
			||||||
 | 
					    {:else}
 | 
				
			||||||
 | 
					      {notification.message}
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
  </p>
 | 
					  </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  {#if notification.button}
 | 
				
			||||||
 | 
					    <p class="pl-[28px] mt-2.5 text-sm">
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        class="{buttonStyle[notification.type]} rounded px-3 pt-1.5 pb-1 transition-all duration-200"
 | 
				
			||||||
 | 
					        on:click={handleButtonClick}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {notification.button.text}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  {/if}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -6,20 +6,31 @@ export enum NotificationType {
 | 
				
			|||||||
  Warning = 'Warning',
 | 
					  Warning = 'Warning',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type NotificationButton = {
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					  onClick: () => unknown;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Notification = {
 | 
					export type Notification = {
 | 
				
			||||||
  id: number;
 | 
					  id: number;
 | 
				
			||||||
  type: NotificationType;
 | 
					  type: NotificationType;
 | 
				
			||||||
  message: string;
 | 
					  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 */
 | 
					  /** The action to take when the notification is clicked */
 | 
				
			||||||
  action: NotificationAction;
 | 
					  action: NotificationAction;
 | 
				
			||||||
 | 
					  button?: NotificationButton;
 | 
				
			||||||
  /** Timeout in miliseconds */
 | 
					  /** Timeout in miliseconds */
 | 
				
			||||||
  timeout: number;
 | 
					  timeout: number;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DiscardAction = { type: 'discard' };
 | 
					type DiscardAction = { type: 'discard' };
 | 
				
			||||||
type NoopAction = { type: 'noop' };
 | 
					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<Exclude<Notification, 'id'>> & { message: string };
 | 
					export type NotificationOptions = Partial<Exclude<Notification, 'id'>> & { message: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -32,7 +43,9 @@ function createNotificationList() {
 | 
				
			|||||||
      currentList.push({
 | 
					      currentList.push({
 | 
				
			||||||
        id: count++,
 | 
					        id: count++,
 | 
				
			||||||
        type: NotificationType.Info,
 | 
					        type: NotificationType.Info,
 | 
				
			||||||
        action: { type: 'discard' },
 | 
					        action: {
 | 
				
			||||||
 | 
					          type: options.button ? 'noop' : 'discard',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        timeout: 3000,
 | 
					        timeout: 3000,
 | 
				
			||||||
        ...options,
 | 
					        ...options,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
				
			|||||||
@ -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 type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
				
			||||||
import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
 | 
					import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
 | 
				
			||||||
import { downloadManager } from '$lib/stores/download';
 | 
					import { downloadManager } from '$lib/stores/download';
 | 
				
			||||||
import { downloadRequest, getKey } from '$lib/utils';
 | 
					import { downloadRequest, getKey } from '$lib/utils';
 | 
				
			||||||
 | 
					import { encodeHTMLSpecialChars } from '$lib/utils/string-utils';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  addAssetsToAlbum as addAssets,
 | 
					  addAssetsToAlbum as addAssets,
 | 
				
			||||||
 | 
					  createAlbum,
 | 
				
			||||||
  defaults,
 | 
					  defaults,
 | 
				
			||||||
  getDownloadInfo,
 | 
					  getDownloadInfo,
 | 
				
			||||||
  type AssetResponseDto,
 | 
					  type AssetResponseDto,
 | 
				
			||||||
  type AssetTypeEnum,
 | 
					  type AssetTypeEnum,
 | 
				
			||||||
  type BulkIdResponseDto,
 | 
					 | 
				
			||||||
  type DownloadInfoDto,
 | 
					  type DownloadInfoDto,
 | 
				
			||||||
  type DownloadResponseDto,
 | 
					  type DownloadResponseDto,
 | 
				
			||||||
  type UserResponseDto,
 | 
					  type UserResponseDto,
 | 
				
			||||||
@ -18,20 +21,60 @@ import { DateTime } from 'luxon';
 | 
				
			|||||||
import { get } from 'svelte/store';
 | 
					import { get } from 'svelte/store';
 | 
				
			||||||
import { handleError } from './handle-error';
 | 
					import { handleError } from './handle-error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> =>
 | 
					export const addAssetsToAlbum = async (albumId: string, assetIds: string[]) => {
 | 
				
			||||||
  addAssets({
 | 
					  const result = await addAssets({
 | 
				
			||||||
    id: albumId,
 | 
					    id: albumId,
 | 
				
			||||||
    bulkIdsDto: { ids: assetIds },
 | 
					    bulkIdsDto: {
 | 
				
			||||||
 | 
					      ids: assetIds,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    key: getKey(),
 | 
					    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 ? `<b>${encodeHTMLSpecialChars(albumName)}</b>` : 'new album';
 | 
				
			||||||
    notificationController.show({
 | 
					    notificationController.show({
 | 
				
			||||||
      type: NotificationType.Info,
 | 
					      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 album;
 | 
				
			||||||
    return results;
 | 
					  } catch {
 | 
				
			||||||
  });
 | 
					    notificationController.show({
 | 
				
			||||||
 | 
					      type: NotificationType.Error,
 | 
				
			||||||
 | 
					      message: 'Failed to create album',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const downloadBlob = (data: Blob, filename: string) => {
 | 
					export const downloadBlob = (data: Blob, filename: string) => {
 | 
				
			||||||
  const url = URL.createObjectURL(data);
 | 
					  const url = URL.createObjectURL(data);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										16
									
								
								web/src/lib/utils/string-utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								web/src/lib/utils/string-utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -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("'", ''');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user