mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	chore(web): cleanup promise handling (#7382)
* no-misused-promises * no-floating-promises * format * revert for now * remove load function * require-await * revert a few no-floating-promises changes that would cause no-misused-promises failures * format * fix a few more * fix most remaining errors * executor-queue * executor-queue.spec * remove duplicate comments by grouping rules * upgrade sveltekit and enforce rules * oops. move await * try this * just ignore for now since it's only a test * run in parallel * Update web/src/routes/admin/jobs-status/+page.svelte Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * remove Promise.resolve call * rename function * remove unnecessary warning silencing * make handleError sync * fix new errors from recently merged PR to main * extract method * use handlePromiseError --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									57f25855d3
								
							
						
					
					
						commit
						907a95a746
					
				@ -13,6 +13,7 @@ module.exports = {
 | 
			
		||||
    sourceType: 'module',
 | 
			
		||||
    ecmaVersion: 2022,
 | 
			
		||||
    extraFileExtensions: ['.svelte'],
 | 
			
		||||
    project: ['./tsconfig.json'],
 | 
			
		||||
  },
 | 
			
		||||
  env: {
 | 
			
		||||
    browser: true,
 | 
			
		||||
@ -32,13 +33,6 @@ module.exports = {
 | 
			
		||||
    NodeJS: true,
 | 
			
		||||
  },
 | 
			
		||||
  rules: {
 | 
			
		||||
    'unicorn/no-useless-undefined': 'off',
 | 
			
		||||
    'unicorn/prefer-spread': 'off',
 | 
			
		||||
    'unicorn/no-null': 'off',
 | 
			
		||||
    'unicorn/prevent-abbreviations': 'off',
 | 
			
		||||
    'unicorn/no-nested-ternary': 'off',
 | 
			
		||||
    'unicorn/consistent-function-scoping': 'off',
 | 
			
		||||
    'unicorn/prefer-top-level-await': 'off',
 | 
			
		||||
    '@typescript-eslint/no-unused-vars': [
 | 
			
		||||
      'warn',
 | 
			
		||||
      {
 | 
			
		||||
@ -48,5 +42,17 @@ module.exports = {
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    curly: 2,
 | 
			
		||||
    'unicorn/no-useless-undefined': 'off',
 | 
			
		||||
    'unicorn/prefer-spread': 'off',
 | 
			
		||||
    'unicorn/no-null': 'off',
 | 
			
		||||
    'unicorn/prevent-abbreviations': 'off',
 | 
			
		||||
    'unicorn/no-nested-ternary': 'off',
 | 
			
		||||
    'unicorn/consistent-function-scoping': 'off',
 | 
			
		||||
    'unicorn/prefer-top-level-await': 'off',
 | 
			
		||||
    // TODO: set recommended-type-checked and remove these rules
 | 
			
		||||
    '@typescript-eslint/await-thenable': 'error',
 | 
			
		||||
    '@typescript-eslint/no-floating-promises': 'error',
 | 
			
		||||
    '@typescript-eslint/no-misused-promises': 'error',
 | 
			
		||||
    '@typescript-eslint/require-await': 'error',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -32,7 +32,7 @@
 | 
			
		||||
        "@socket.io/component-emitter": "^3.1.0",
 | 
			
		||||
        "@sveltejs/adapter-static": "^3.0.1",
 | 
			
		||||
        "@sveltejs/enhanced-img": "^0.1.8",
 | 
			
		||||
        "@sveltejs/kit": "^2.5.1",
 | 
			
		||||
        "@sveltejs/kit": "^2.5.2",
 | 
			
		||||
        "@sveltejs/vite-plugin-svelte": "^3.0.2",
 | 
			
		||||
        "@testing-library/jest-dom": "^6.1.5",
 | 
			
		||||
        "@testing-library/svelte": "^4.0.3",
 | 
			
		||||
@ -1859,9 +1859,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@sveltejs/kit": {
 | 
			
		||||
      "version": "2.5.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz",
 | 
			
		||||
      "integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==",
 | 
			
		||||
      "version": "2.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@
 | 
			
		||||
    "@socket.io/component-emitter": "^3.1.0",
 | 
			
		||||
    "@sveltejs/adapter-static": "^3.0.1",
 | 
			
		||||
    "@sveltejs/enhanced-img": "^0.1.8",
 | 
			
		||||
    "@sveltejs/kit": "^2.5.1",
 | 
			
		||||
    "@sveltejs/kit": "^2.5.2",
 | 
			
		||||
    "@sveltejs/vite-plugin-svelte": "^3.0.2",
 | 
			
		||||
    "@testing-library/jest-dom": "^6.1.5",
 | 
			
		||||
    "@testing-library/svelte": "^4.0.3",
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { isHttpError } from '@immich/sdk';
 | 
			
		||||
import type { HandleClientError } from '@sveltejs/kit';
 | 
			
		||||
 | 
			
		||||
const LOG_PREFIX = '[hooks.client.ts]';
 | 
			
		||||
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
 | 
			
		||||
 | 
			
		||||
const parseError = (error: unknown) => {
 | 
			
		||||
@ -23,6 +22,6 @@ const parseError = (error: unknown) => {
 | 
			
		||||
 | 
			
		||||
export const handleError: HandleClientError = ({ error }) => {
 | 
			
		||||
  const result = parseError(error);
 | 
			
		||||
  console.error(`${LOG_PREFIX}:handleError ${result.message}`);
 | 
			
		||||
  console.error(`[hooks.client.ts]:handleError ${result.message}`);
 | 
			
		||||
  return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -48,11 +48,11 @@
 | 
			
		||||
    await handleCommand(jobId, dto);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onConfirm = () => {
 | 
			
		||||
  const onConfirm = async () => {
 | 
			
		||||
    if (!confirmJob) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    handleCommand(confirmJob, { command: JobCommand.Start, force: true });
 | 
			
		||||
    await handleCommand(confirmJob, { command: JobCommand.Start, force: true });
 | 
			
		||||
    confirmJob = null;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const resetToDefault = async (configKeys: Array<keyof SystemConfigDto>) => {
 | 
			
		||||
  const resetToDefault = (configKeys: Array<keyof SystemConfigDto>) => {
 | 
			
		||||
    for (const key of configKeys) {
 | 
			
		||||
      config = { ...config, [key]: defaultConfig[key] };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@
 | 
			
		||||
  import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
 | 
			
		||||
  import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
 | 
			
		||||
  import UpdatePanel from '../shared-components/update-panel.svelte';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let sharedLink: SharedLinkResponseDto;
 | 
			
		||||
  export let user: UserResponseDto | undefined = undefined;
 | 
			
		||||
@ -35,7 +36,7 @@
 | 
			
		||||
 | 
			
		||||
  dragAndDropFilesStore.subscribe((value) => {
 | 
			
		||||
    if (value.isDragging && value.files.length > 0) {
 | 
			
		||||
      fileUploadHandler(value.files, album.id);
 | 
			
		||||
      handlePromiseError(fileUploadHandler(value.files, album.id));
 | 
			
		||||
      dragAndDropFilesStore.set({ isDragging: false, files: [] });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
@ -67,7 +68,7 @@
 | 
			
		||||
 | 
			
		||||
  const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
 | 
			
		||||
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    document.addEventListener('keydown', onKeyboardPress);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
			
		||||
  import { timeBeforeShowLoadingSpinner } from '$lib/constants';
 | 
			
		||||
  import { getAssetThumbnailUrl } from '$lib/utils';
 | 
			
		||||
  import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { getAssetType } from '$lib/utils/asset-utils';
 | 
			
		||||
  import { autoGrowHeight } from '$lib/utils/autogrow';
 | 
			
		||||
  import { clickOutside } from '$lib/utils/click-outside';
 | 
			
		||||
@ -79,7 +79,7 @@
 | 
			
		||||
 | 
			
		||||
  $: {
 | 
			
		||||
    if (assetId && previousAssetId != assetId) {
 | 
			
		||||
      getReactions();
 | 
			
		||||
      handlePromiseError(getReactions());
 | 
			
		||||
      previousAssetId = assetId;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -95,10 +95,10 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleEnter = (event: KeyboardEvent) => {
 | 
			
		||||
  const handleEnter = async (event: KeyboardEvent) => {
 | 
			
		||||
    if (event.key === 'Enter') {
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      handleSendComment();
 | 
			
		||||
      await handleSendComment();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
  import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
 | 
			
		||||
  import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
 | 
			
		||||
  import { user } from '$lib/stores/user.store';
 | 
			
		||||
  import { getAssetJobMessage, isSharedLink } from '$lib/utils';
 | 
			
		||||
  import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
 | 
			
		||||
  import { handleError } from '$lib/utils/handle-error';
 | 
			
		||||
  import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
 | 
			
		||||
@ -174,8 +174,8 @@
 | 
			
		||||
 | 
			
		||||
  $: {
 | 
			
		||||
    if (isShared && asset.id) {
 | 
			
		||||
      getFavorite();
 | 
			
		||||
      getNumberOfComments();
 | 
			
		||||
      handlePromiseError(getFavorite());
 | 
			
		||||
      handlePromiseError(getNumberOfComments());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -184,9 +184,9 @@
 | 
			
		||||
      if (value === SlideshowState.PlaySlideshow) {
 | 
			
		||||
        slideshowHistory.reset();
 | 
			
		||||
        slideshowHistory.queue(asset.id);
 | 
			
		||||
        handlePlaySlideshow();
 | 
			
		||||
        handlePromiseError(handlePlaySlideshow());
 | 
			
		||||
      } else if (value === SlideshowState.StopSlideshow) {
 | 
			
		||||
        handleStopSlideshow();
 | 
			
		||||
        handlePromiseError(handleStopSlideshow());
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -226,7 +226,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  $: asset.id && !sharedLink && handleGetAllAlbums(); // Update the album information when the asset ID changes
 | 
			
		||||
  $: asset.id && !sharedLink && handlePromiseError(handleGetAllAlbums()); // Update the album information when the asset ID changes
 | 
			
		||||
 | 
			
		||||
  const handleGetAllAlbums = async () => {
 | 
			
		||||
    if (isSharedLink()) {
 | 
			
		||||
@ -247,7 +247,7 @@
 | 
			
		||||
    isShowActivity = !isShowActivity;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleKeypress = (event: KeyboardEvent) => {
 | 
			
		||||
  const handleKeypress = async (event: KeyboardEvent) => {
 | 
			
		||||
    if (shouldIgnoreShortcut(event)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -264,7 +264,7 @@
 | 
			
		||||
      case 'a':
 | 
			
		||||
      case 'A': {
 | 
			
		||||
        if (shiftKey) {
 | 
			
		||||
          toggleArchive();
 | 
			
		||||
          await toggleArchive();
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
@ -273,18 +273,18 @@
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      case 'ArrowRight': {
 | 
			
		||||
        navigateAssetForward();
 | 
			
		||||
        await navigateAssetForward();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      case 'd':
 | 
			
		||||
      case 'D': {
 | 
			
		||||
        if (shiftKey) {
 | 
			
		||||
          downloadFile(asset);
 | 
			
		||||
          await downloadFile(asset);
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      case 'Delete': {
 | 
			
		||||
        trashOrDelete(shiftKey);
 | 
			
		||||
        await trashOrDelete(shiftKey);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      case 'Escape': {
 | 
			
		||||
@ -296,7 +296,7 @@
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      case 'f': {
 | 
			
		||||
        toggleFavorite();
 | 
			
		||||
        await toggleFavorite();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      case 'i': {
 | 
			
		||||
@ -326,7 +326,7 @@
 | 
			
		||||
 | 
			
		||||
    slideshowHistory.queue(asset.id);
 | 
			
		||||
 | 
			
		||||
    setAssetId(asset.id);
 | 
			
		||||
    await setAssetId(asset.id);
 | 
			
		||||
    $restartSlideshowProgress = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -369,17 +369,17 @@
 | 
			
		||||
    $isShowDetail = !$isShowDetail;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const trashOrDelete = (force: boolean = false) => {
 | 
			
		||||
  const trashOrDelete = async (force: boolean = false) => {
 | 
			
		||||
    if (force || !isTrashEnabled) {
 | 
			
		||||
      if ($showDeleteModal) {
 | 
			
		||||
        isShowDeleteConfirmation = true;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      deleteAsset();
 | 
			
		||||
      await deleteAsset();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    trashAsset();
 | 
			
		||||
    await trashAsset();
 | 
			
		||||
    return;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -432,7 +432,7 @@
 | 
			
		||||
        message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`);
 | 
			
		||||
      handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -472,7 +472,7 @@
 | 
			
		||||
        message: asset.isArchived ? `Added to archive` : `Removed from archive`,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
 | 
			
		||||
      handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -481,7 +481,7 @@
 | 
			
		||||
      await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
 | 
			
		||||
      notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, `Unable to submit job`);
 | 
			
		||||
      handleError(error, `Unable to submit job`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -492,7 +492,7 @@
 | 
			
		||||
  let assetViewerHtmlElement: HTMLElement;
 | 
			
		||||
 | 
			
		||||
  const slideshowHistory = new SlideshowHistory((assetId: string) => {
 | 
			
		||||
    setAssetId(assetId);
 | 
			
		||||
    handlePromiseError(setAssetId(assetId));
 | 
			
		||||
    $restartSlideshowProgress = true;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -550,7 +550,7 @@
 | 
			
		||||
      dispatch('close');
 | 
			
		||||
      notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, `Unable to unstack`);
 | 
			
		||||
      handleError(error, `Unable to unstack`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
  import { featureFlags } from '$lib/stores/server-config.store';
 | 
			
		||||
  import { user } from '$lib/stores/user.store';
 | 
			
		||||
  import { websocketEvents } from '$lib/stores/websocket';
 | 
			
		||||
  import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils';
 | 
			
		||||
  import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { delay, getAssetFilename } from '$lib/utils/asset-utils';
 | 
			
		||||
  import { autoGrowHeight } from '$lib/utils/autogrow';
 | 
			
		||||
  import { clickOutside } from '$lib/utils/click-outside';
 | 
			
		||||
@ -78,7 +78,7 @@
 | 
			
		||||
    originalDescription = description;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $: handleNewAsset(asset);
 | 
			
		||||
  $: handlePromiseError(handleNewAsset(asset));
 | 
			
		||||
 | 
			
		||||
  $: latlng = (() => {
 | 
			
		||||
    const lat = asset.exifInfo?.latitude;
 | 
			
		||||
@ -113,7 +113,7 @@
 | 
			
		||||
    switch (event.key) {
 | 
			
		||||
      case 'Enter': {
 | 
			
		||||
        if (ctrl && event.target === textArea) {
 | 
			
		||||
          handleFocusOut();
 | 
			
		||||
          await handleFocusOut();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
  import { boundingBoxesArray } from '$lib/stores/people.store';
 | 
			
		||||
  import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
 | 
			
		||||
  import { photoZoomState } from '$lib/stores/zoom-image.store';
 | 
			
		||||
  import { getKey } from '$lib/utils';
 | 
			
		||||
  import { getKey, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { isWebCompatibleImage } from '$lib/utils/asset-utils';
 | 
			
		||||
  import { getBoundingBox } from '$lib/utils/people-utils';
 | 
			
		||||
  import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
 | 
			
		||||
@ -102,7 +102,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const doZoomImage = async () => {
 | 
			
		||||
  const doZoomImage = () => {
 | 
			
		||||
    setZoomImageWheelState({
 | 
			
		||||
      currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
 | 
			
		||||
    });
 | 
			
		||||
@ -120,7 +120,7 @@
 | 
			
		||||
    if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) {
 | 
			
		||||
      hasZoomed = true;
 | 
			
		||||
 | 
			
		||||
      loadAssetData({ loadOriginal: true });
 | 
			
		||||
      handlePromiseError(loadAssetData({ loadOriginal: true }));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
      video.muted = false;
 | 
			
		||||
      dispatch('onVideoStarted');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, 'Unable to play video');
 | 
			
		||||
      handleError(error, 'Unable to play video');
 | 
			
		||||
    } finally {
 | 
			
		||||
      isVideoLoading = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,7 @@
 | 
			
		||||
    if (assetType === AssetTypeEnum.Image) {
 | 
			
		||||
      image = $photoViewer;
 | 
			
		||||
    } else if (assetType === AssetTypeEnum.Video) {
 | 
			
		||||
      const data = await getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
 | 
			
		||||
      const data = getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
 | 
			
		||||
      const img: HTMLImageElement = new Image();
 | 
			
		||||
      img.src = data;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -43,10 +43,10 @@
 | 
			
		||||
    dispatch('back');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSwapPeople = () => {
 | 
			
		||||
  const handleSwapPeople = async () => {
 | 
			
		||||
    [person, selectedPeople[0]] = [selectedPeople[0], person];
 | 
			
		||||
    $page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
 | 
			
		||||
    goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
 | 
			
		||||
    await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onSelect = (selected: PersonResponseDto) => {
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
  import { timeBeforeShowLoadingSpinner } from '$lib/constants';
 | 
			
		||||
  import { boundingBoxesArray } from '$lib/stores/people.store';
 | 
			
		||||
  import { websocketEvents } from '$lib/stores/websocket';
 | 
			
		||||
  import { getPeopleThumbnailUrl } from '$lib/utils';
 | 
			
		||||
  import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { handleError } from '$lib/utils/handle-error';
 | 
			
		||||
  import { getPersonNameWithHiddenValue } from '$lib/utils/person';
 | 
			
		||||
  import {
 | 
			
		||||
@ -85,7 +85,7 @@
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    loadPeople();
 | 
			
		||||
    handlePromiseError(loadPeople());
 | 
			
		||||
    return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -164,7 +164,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePersonPicker = async (index: number) => {
 | 
			
		||||
  const handlePersonPicker = (index: number) => {
 | 
			
		||||
    editedPersonIndex = index;
 | 
			
		||||
    showSeletecFaces = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -132,9 +132,7 @@
 | 
			
		||||
          title={'Assign selected assets to a new person'}
 | 
			
		||||
          size={'sm'}
 | 
			
		||||
          disabled={disableButtons || hasSelection}
 | 
			
		||||
          on:click={() => {
 | 
			
		||||
            handleCreate();
 | 
			
		||||
          }}
 | 
			
		||||
          on:click={handleCreate}
 | 
			
		||||
        >
 | 
			
		||||
          {#if !showLoadingSpinnerCreate}
 | 
			
		||||
            <Icon path={mdiPlus} size={18} />
 | 
			
		||||
@ -147,9 +145,7 @@
 | 
			
		||||
          size={'sm'}
 | 
			
		||||
          title={'Assign selected assets to an existing person'}
 | 
			
		||||
          disabled={disableButtons || !hasSelection}
 | 
			
		||||
          on:click={() => {
 | 
			
		||||
            handleReassign();
 | 
			
		||||
          }}
 | 
			
		||||
          on:click={handleReassign}
 | 
			
		||||
        >
 | 
			
		||||
          {#if !showLoadingSpinnerReassign}
 | 
			
		||||
            <div>
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@
 | 
			
		||||
    dispatch('submit', { library, type: LibraryType.External });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleAddExclusionPattern = async () => {
 | 
			
		||||
  const handleAddExclusionPattern = () => {
 | 
			
		||||
    if (!addExclusionPattern) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -60,7 +60,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleEditExclusionPattern = async () => {
 | 
			
		||||
  const handleEditExclusionPattern = () => {
 | 
			
		||||
    if (editExclusionPattern === null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -79,7 +79,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleDeleteExclusionPattern = async () => {
 | 
			
		||||
  const handleDeleteExclusionPattern = () => {
 | 
			
		||||
    if (editExclusionPattern === null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, 'Unable to connect!');
 | 
			
		||||
      handleError(error, 'Unable to connect!');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    oauthLoading = false;
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
  import { AppRoute, QueryParameter } from '$lib/constants';
 | 
			
		||||
  import type { Viewport } from '$lib/stores/assets.store';
 | 
			
		||||
  import { memoryStore } from '$lib/stores/memory.store';
 | 
			
		||||
  import { getAssetThumbnailUrl } from '$lib/utils';
 | 
			
		||||
  import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { fromLocalDateTime } from '$lib/utils/timeline-util';
 | 
			
		||||
  import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
 | 
			
		||||
  import { mdiChevronDown, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiPause, mdiPlay } from '@mdi/js';
 | 
			
		||||
@ -59,30 +59,30 @@
 | 
			
		||||
  let paused = false;
 | 
			
		||||
 | 
			
		||||
  // Play or pause progress when the paused state changes.
 | 
			
		||||
  $: paused ? pause() : play();
 | 
			
		||||
  $: paused ? handlePromiseError(pause()) : handlePromiseError(play());
 | 
			
		||||
 | 
			
		||||
  // Progress should be paused when it's no longer possible to advance.
 | 
			
		||||
  $: paused ||= !canGoForward || galleryInView;
 | 
			
		||||
 | 
			
		||||
  // Advance to the next asset or memory when progress is complete.
 | 
			
		||||
  $: $progress === 1 && toNext();
 | 
			
		||||
  $: $progress === 1 && handlePromiseError(toNext());
 | 
			
		||||
 | 
			
		||||
  // Progress should be resumed when reset and not paused.
 | 
			
		||||
  $: !$progress && !paused && play();
 | 
			
		||||
  $: !$progress && !paused && handlePromiseError(play());
 | 
			
		||||
 | 
			
		||||
  // Progress should be reset when the current memory or asset changes.
 | 
			
		||||
  $: memoryIndex, assetIndex, reset();
 | 
			
		||||
  $: memoryIndex, assetIndex, handlePromiseError(reset());
 | 
			
		||||
 | 
			
		||||
  const handleKeyDown = (e: KeyboardEvent) => {
 | 
			
		||||
  const handleKeyDown = async (e: KeyboardEvent) => {
 | 
			
		||||
    if (e.key === 'ArrowRight' && canGoForward) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      toNext();
 | 
			
		||||
      await toNext();
 | 
			
		||||
    } else if (e.key === 'ArrowLeft' && canGoBack) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      toPrevious();
 | 
			
		||||
      await toPrevious();
 | 
			
		||||
    } else if (e.key === 'Escape') {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      goto(AppRoute.PHOTOS);
 | 
			
		||||
      await goto(AppRoute.PHOTOS);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,8 @@
 | 
			
		||||
    showAlbumPicker = false;
 | 
			
		||||
 | 
			
		||||
    const assetIds = [...getAssets()].map((asset) => asset.id);
 | 
			
		||||
    createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => {
 | 
			
		||||
    createAlbum({ createAlbumDto: { albumName, assetIds } })
 | 
			
		||||
      .then(async (response) => {
 | 
			
		||||
        const { id, albumName } = response;
 | 
			
		||||
 | 
			
		||||
        notificationController.show({
 | 
			
		||||
@ -37,7 +38,10 @@
 | 
			
		||||
 | 
			
		||||
        clearSelect();
 | 
			
		||||
 | 
			
		||||
      goto(`${AppRoute.ALBUMS}/${id}`);
 | 
			
		||||
        await goto(`${AppRoute.ALBUMS}/${id}`);
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        console.error(`[add-to-album.svelte]:handleAddToNewAlbum ${error}`, error);
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -80,13 +80,17 @@
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const assetClickHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => {
 | 
			
		||||
  const assetClickHandler = async (
 | 
			
		||||
    asset: AssetResponseDto,
 | 
			
		||||
    assetsInDateGroup: AssetResponseDto[],
 | 
			
		||||
    groupTitle: string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    if (isSelectionMode || $isMultiSelectState) {
 | 
			
		||||
      assetSelectHandler(asset, assetsInDateGroup, groupTitle);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    assetViewingStore.setAssetId(asset.id);
 | 
			
		||||
    await assetViewingStore.setAssetId(asset.id);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets });
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@
 | 
			
		||||
  import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
 | 
			
		||||
  import AssetDateGroup from './asset-date-group.svelte';
 | 
			
		||||
  import DeleteAssetDialog from './delete-asset-dialog.svelte';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let isSelectionMode = false;
 | 
			
		||||
  export let singleSelect = false;
 | 
			
		||||
@ -47,19 +48,19 @@
 | 
			
		||||
  $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0;
 | 
			
		||||
  $: idsSelectedAssets = [...$selectedAssets].filter((a) => !a.isExternal).map((a) => a.id);
 | 
			
		||||
 | 
			
		||||
  const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
 | 
			
		||||
  const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
 | 
			
		||||
 | 
			
		||||
  const onKeydown = (event: KeyboardEvent) => handlePromiseError(handleKeyboardPress(event));
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    showSkeleton = false;
 | 
			
		||||
    document.addEventListener('keydown', onKeyboardPress);
 | 
			
		||||
    document.addEventListener('keydown', onKeydown);
 | 
			
		||||
    assetStore.connect();
 | 
			
		||||
    await assetStore.init(viewport);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  onDestroy(() => {
 | 
			
		||||
    if (browser) {
 | 
			
		||||
      document.removeEventListener('keydown', onKeyboardPress);
 | 
			
		||||
      document.removeEventListener('keydown', onKeydown);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($showAssetViewer) {
 | 
			
		||||
@ -69,13 +70,13 @@
 | 
			
		||||
    assetStore.disconnect();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const trashOrDelete = (force: boolean = false) => {
 | 
			
		||||
  const trashOrDelete = async (force: boolean = false) => {
 | 
			
		||||
    isShowDeleteConfirmation = false;
 | 
			
		||||
    deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets);
 | 
			
		||||
    await deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets);
 | 
			
		||||
    assetInteractionStore.clearMultiselect();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleKeyboardPress = (event: KeyboardEvent) => {
 | 
			
		||||
  const handleKeyboardPress = async (event: KeyboardEvent) => {
 | 
			
		||||
    if ($isSearchEnabled || shouldIgnoreShortcut(event)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -98,7 +99,7 @@
 | 
			
		||||
        }
 | 
			
		||||
        case '/': {
 | 
			
		||||
          event.preventDefault();
 | 
			
		||||
          goto(AppRoute.EXPLORE);
 | 
			
		||||
          await goto(AppRoute.EXPLORE);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'Delete': {
 | 
			
		||||
@ -112,7 +113,7 @@
 | 
			
		||||
              force = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            trashOrDelete(force);
 | 
			
		||||
            await trashOrDelete(force);
 | 
			
		||||
          }
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
@ -126,12 +127,12 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function intersectedHandler(event: CustomEvent) {
 | 
			
		||||
  async function intersectedHandler(event: CustomEvent) {
 | 
			
		||||
    const element_ = event.detail.container as HTMLElement;
 | 
			
		||||
    const target = element_.firstChild as HTMLElement;
 | 
			
		||||
    if (target) {
 | 
			
		||||
      const bucketDate = target.id.split('_')[1];
 | 
			
		||||
      assetStore.loadBucket(bucketDate, event.detail.position);
 | 
			
		||||
      await assetStore.loadBucket(bucketDate, event.detail.position);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -142,7 +143,7 @@
 | 
			
		||||
  const handlePrevious = async () => {
 | 
			
		||||
    const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id);
 | 
			
		||||
    if (previousAsset) {
 | 
			
		||||
      assetViewingStore.setAssetId(previousAsset);
 | 
			
		||||
      await assetViewingStore.setAssetId(previousAsset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return !!previousAsset;
 | 
			
		||||
@ -151,7 +152,7 @@
 | 
			
		||||
  const handleNext = async () => {
 | 
			
		||||
    const nextAsset = await assetStore.getNextAssetId($viewingAsset.id);
 | 
			
		||||
    if (nextAsset) {
 | 
			
		||||
      assetViewingStore.setAssetId(nextAsset);
 | 
			
		||||
      await assetViewingStore.setAssetId(nextAsset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return !!nextAsset;
 | 
			
		||||
@ -369,7 +370,7 @@
 | 
			
		||||
  <DeleteAssetDialog
 | 
			
		||||
    size={idsSelectedAssets.length}
 | 
			
		||||
    on:cancel={() => (isShowDeleteConfirmation = false)}
 | 
			
		||||
    on:confirm={() => trashOrDelete(true)}
 | 
			
		||||
    on:confirm={() => handlePromiseError(trashOrDelete(true))}
 | 
			
		||||
  />
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
  import { goto } from '$app/navigation';
 | 
			
		||||
  import { AppRoute } from '$lib/constants';
 | 
			
		||||
  import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
 | 
			
		||||
  import { getKey } from '$lib/utils';
 | 
			
		||||
  import { getKey, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { downloadArchive } from '$lib/utils/asset-utils';
 | 
			
		||||
  import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
			
		||||
  import { handleError } from '$lib/utils/handle-error';
 | 
			
		||||
@ -29,7 +29,7 @@
 | 
			
		||||
 | 
			
		||||
  dragAndDropFilesStore.subscribe((value) => {
 | 
			
		||||
    if (value.isDragging && value.files.length > 0) {
 | 
			
		||||
      handleUploadAssets(value.files);
 | 
			
		||||
      handlePromiseError(handleUploadAssets(value.files));
 | 
			
		||||
      dragAndDropFilesStore.set({ isDragging: false, files: [] });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
@ -59,7 +59,7 @@
 | 
			
		||||
        type: NotificationType.Info,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, 'Unable to add assets to shared link');
 | 
			
		||||
      handleError(error, 'Unable to add assets to shared link');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
			
		||||
  import { Theme } from '$lib/constants';
 | 
			
		||||
  import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
 | 
			
		||||
  import { getAssetThumbnailUrl } from '$lib/utils';
 | 
			
		||||
  import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { getMapStyle, MapTheme, type MapMarkerResponseDto } from '@immich/sdk';
 | 
			
		||||
  import { mdiCog, mdiMapMarker } from '@mdi/js';
 | 
			
		||||
  import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
 | 
			
		||||
@ -152,9 +152,7 @@
 | 
			
		||||
        applyToClusters
 | 
			
		||||
        asButton
 | 
			
		||||
        let:feature
 | 
			
		||||
        on:click={(event) => {
 | 
			
		||||
          handleClusterClick(event.detail.feature.properties.cluster_id, map);
 | 
			
		||||
        }}
 | 
			
		||||
        on:click={(event) => handlePromiseError(handleClusterClick(event.detail.feature.properties.cluster_id, map))}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          class="rounded-full w-[40px] h-[40px] bg-immich-primary text-immich-gray flex justify-center items-center font-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90"
 | 
			
		||||
 | 
			
		||||
@ -8,8 +8,8 @@
 | 
			
		||||
    easing: cubicOut,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    progress.set(90);
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    await progress.set(90);
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
<script context="module" lang="ts">
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { tick } from 'svelte';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -36,7 +37,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update(target);
 | 
			
		||||
    handlePromiseError(update(target));
 | 
			
		||||
    return {
 | 
			
		||||
      update,
 | 
			
		||||
      destroy,
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,8 @@
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  import { createEventDispatcher, onMount } from 'svelte';
 | 
			
		||||
  import { tweened } from 'svelte/motion';
 | 
			
		||||
 | 
			
		||||
@ -24,14 +26,14 @@
 | 
			
		||||
 | 
			
		||||
  export let duration = 5;
 | 
			
		||||
 | 
			
		||||
  const onChange = () => {
 | 
			
		||||
  const onChange = async () => {
 | 
			
		||||
    progress = setDuration(duration);
 | 
			
		||||
    play();
 | 
			
		||||
    await play();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let progress = setDuration(duration);
 | 
			
		||||
 | 
			
		||||
  $: duration, onChange();
 | 
			
		||||
  $: duration, handlePromiseError(onChange());
 | 
			
		||||
 | 
			
		||||
  $: {
 | 
			
		||||
    if ($progress === 1) {
 | 
			
		||||
@ -45,35 +47,35 @@
 | 
			
		||||
    paused: void;
 | 
			
		||||
  }>();
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    if (autoplay) {
 | 
			
		||||
      play();
 | 
			
		||||
      await play();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  export const play = () => {
 | 
			
		||||
  export const play = async () => {
 | 
			
		||||
    status = ProgressBarStatus.Playing;
 | 
			
		||||
    dispatch('playing');
 | 
			
		||||
    progress.set(1);
 | 
			
		||||
    await progress.set(1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  export const pause = () => {
 | 
			
		||||
  export const pause = async () => {
 | 
			
		||||
    status = ProgressBarStatus.Paused;
 | 
			
		||||
    dispatch('paused');
 | 
			
		||||
    progress.set($progress);
 | 
			
		||||
    await progress.set($progress);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  export const restart = (autoplay: boolean) => {
 | 
			
		||||
    progress.set(0);
 | 
			
		||||
  export const restart = async (autoplay: boolean) => {
 | 
			
		||||
    await progress.set(0);
 | 
			
		||||
 | 
			
		||||
    if (autoplay) {
 | 
			
		||||
      play();
 | 
			
		||||
      await play();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  export const reset = () => {
 | 
			
		||||
  export const reset = async () => {
 | 
			
		||||
    status = ProgressBarStatus.Paused;
 | 
			
		||||
    progress.set(0);
 | 
			
		||||
    await progress.set(0);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function setDuration(newDuration: number) {
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@
 | 
			
		||||
  import SearchFilterBox from './search-filter-box.svelte';
 | 
			
		||||
  import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
 | 
			
		||||
  import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let value = '';
 | 
			
		||||
  export let grayTheme: boolean;
 | 
			
		||||
@ -21,13 +22,13 @@
 | 
			
		||||
  let showFilter = false;
 | 
			
		||||
  $: showClearIcon = value.length > 0;
 | 
			
		||||
 | 
			
		||||
  const onSearch = (payload: SmartSearchDto | MetadataSearchDto) => {
 | 
			
		||||
  const onSearch = async (payload: SmartSearchDto | MetadataSearchDto) => {
 | 
			
		||||
    const params = getMetadataSearchQuery(payload);
 | 
			
		||||
 | 
			
		||||
    showHistory = false;
 | 
			
		||||
    showFilter = false;
 | 
			
		||||
    $isSearchEnabled = false;
 | 
			
		||||
    goto(`${AppRoute.SEARCH}?${params}`);
 | 
			
		||||
    await goto(`${AppRoute.SEARCH}?${params}`);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const clearSearchTerm = (searchTerm: string) => {
 | 
			
		||||
@ -63,9 +64,9 @@
 | 
			
		||||
    showFilter = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onHistoryTermClick = (searchTerm: string) => {
 | 
			
		||||
  const onHistoryTermClick = async (searchTerm: string) => {
 | 
			
		||||
    const searchPayload = { query: searchTerm };
 | 
			
		||||
    onSearch(searchPayload);
 | 
			
		||||
    await onSearch(searchPayload);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onFilterClick = () => {
 | 
			
		||||
@ -78,7 +79,7 @@
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onSubmit = () => {
 | 
			
		||||
    onSearch({ query: value });
 | 
			
		||||
    handlePromiseError(onSearch({ query: value }));
 | 
			
		||||
    saveSearchTerm(value);
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
@ -141,7 +142,7 @@
 | 
			
		||||
      <SearchHistoryBox
 | 
			
		||||
        on:clearAllSearchTerms={clearAllSearchTerms}
 | 
			
		||||
        on:clearSearchTerm={({ detail: searchTerm }) => clearSearchTerm(searchTerm)}
 | 
			
		||||
        on:selectSearchTerm={({ detail: searchTerm }) => onHistoryTermClick(searchTerm)}
 | 
			
		||||
        on:selectSearchTerm={({ detail: searchTerm }) => handlePromiseError(onHistoryTermClick(searchTerm))}
 | 
			
		||||
      />
 | 
			
		||||
    {/if}
 | 
			
		||||
  </form>
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { SearchSuggestionType, getSearchSuggestions } from '@immich/sdk';
 | 
			
		||||
  import Combobox, { toComboBoxOptions } from '../combobox.svelte';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let filters: SearchCameraFilter;
 | 
			
		||||
 | 
			
		||||
@ -16,8 +17,8 @@
 | 
			
		||||
 | 
			
		||||
  $: makeFilter = filters.make;
 | 
			
		||||
  $: modelFilter = filters.model;
 | 
			
		||||
  $: updateMakes(modelFilter);
 | 
			
		||||
  $: updateModels(makeFilter);
 | 
			
		||||
  $: handlePromiseError(updateMakes(modelFilter));
 | 
			
		||||
  $: handlePromiseError(updateModels(makeFilter));
 | 
			
		||||
 | 
			
		||||
  async function updateMakes(model?: string) {
 | 
			
		||||
    makes = await getSearchSuggestions({
 | 
			
		||||
 | 
			
		||||
@ -82,7 +82,7 @@
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const search = async () => {
 | 
			
		||||
  const search = () => {
 | 
			
		||||
    if (filter.context && filter.personIds.size > 0) {
 | 
			
		||||
      handleError(
 | 
			
		||||
        new Error('Context search does not support people filter'),
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk';
 | 
			
		||||
  import Combobox, { toComboBoxOptions } from '../combobox.svelte';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let filters: SearchLocationFilter;
 | 
			
		||||
 | 
			
		||||
@ -18,9 +19,9 @@
 | 
			
		||||
 | 
			
		||||
  $: countryFilter = filters.country;
 | 
			
		||||
  $: stateFilter = filters.state;
 | 
			
		||||
  $: updateCountries();
 | 
			
		||||
  $: updateStates(countryFilter);
 | 
			
		||||
  $: updateCities(countryFilter, stateFilter);
 | 
			
		||||
  $: handlePromiseError(updateCountries());
 | 
			
		||||
  $: handlePromiseError(updateStates(countryFilter));
 | 
			
		||||
  $: handlePromiseError(updateCities(countryFilter, stateFilter));
 | 
			
		||||
 | 
			
		||||
  async function updateCountries() {
 | 
			
		||||
    countries = await getSearchSuggestions({
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { page } from '$app/stores';
 | 
			
		||||
  import { QueryParameter } from '$lib/constants';
 | 
			
		||||
  import { hasParamValue, updateParamList } from '$lib/utils';
 | 
			
		||||
  import { hasParamValue, handlePromiseError, updateParamList } from '$lib/utils';
 | 
			
		||||
  import { slide } from 'svelte/transition';
 | 
			
		||||
 | 
			
		||||
  export let title: string;
 | 
			
		||||
@ -12,12 +12,12 @@
 | 
			
		||||
  const syncFromUrl = () => (isOpen = hasParamValue(QueryParameter.IS_OPEN, key));
 | 
			
		||||
  const syncToUrl = (isOpen: boolean) => updateParamList({ param: QueryParameter.IS_OPEN, value: key, add: isOpen });
 | 
			
		||||
 | 
			
		||||
  isOpen ? syncToUrl(true) : syncFromUrl();
 | 
			
		||||
  isOpen ? handlePromiseError(syncToUrl(true)) : syncFromUrl();
 | 
			
		||||
  $: $page.url && syncFromUrl();
 | 
			
		||||
 | 
			
		||||
  const toggle = () => {
 | 
			
		||||
  const toggle = async () => {
 | 
			
		||||
    isOpen = !isOpen;
 | 
			
		||||
    syncToUrl(isOpen);
 | 
			
		||||
    await syncToUrl(isOpen);
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,9 +13,9 @@
 | 
			
		||||
 | 
			
		||||
  export let uploadAsset: UploadAsset;
 | 
			
		||||
 | 
			
		||||
  const handleRetry = (uploadAsset: UploadAsset) => {
 | 
			
		||||
  const handleRetry = async (uploadAsset: UploadAsset) => {
 | 
			
		||||
    uploadAssetsStore.removeUploadAsset(uploadAsset.id);
 | 
			
		||||
    fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
 | 
			
		||||
    await fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -56,8 +56,8 @@
 | 
			
		||||
  let selectedLibraryIndex = 0;
 | 
			
		||||
  let selectedLibrary: LibraryResponseDto | null = null;
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    readLibraryList();
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    await readLibraryList();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const closeAll = () => {
 | 
			
		||||
@ -234,11 +234,11 @@
 | 
			
		||||
    updateLibraryIndex = selectedLibraryIndex;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onScanNewLibraryClicked = () => {
 | 
			
		||||
  const onScanNewLibraryClicked = async () => {
 | 
			
		||||
    closeAll();
 | 
			
		||||
 | 
			
		||||
    if (selectedLibrary) {
 | 
			
		||||
      handleScan(selectedLibrary.id);
 | 
			
		||||
      await handleScan(selectedLibrary.id);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -248,38 +248,38 @@
 | 
			
		||||
    updateLibraryIndex = selectedLibraryIndex;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onScanAllLibraryFilesClicked = () => {
 | 
			
		||||
  const onScanAllLibraryFilesClicked = async () => {
 | 
			
		||||
    closeAll();
 | 
			
		||||
    if (selectedLibrary) {
 | 
			
		||||
      handleScanChanges(selectedLibrary.id);
 | 
			
		||||
      await handleScanChanges(selectedLibrary.id);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onForceScanAllLibraryFilesClicked = () => {
 | 
			
		||||
  const onForceScanAllLibraryFilesClicked = async () => {
 | 
			
		||||
    closeAll();
 | 
			
		||||
    if (selectedLibrary) {
 | 
			
		||||
      handleForceScan(selectedLibrary.id);
 | 
			
		||||
      await handleForceScan(selectedLibrary.id);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onRemoveOfflineFilesClicked = () => {
 | 
			
		||||
  const onRemoveOfflineFilesClicked = async () => {
 | 
			
		||||
    closeAll();
 | 
			
		||||
    if (selectedLibrary) {
 | 
			
		||||
      handleRemoveOffline(selectedLibrary.id);
 | 
			
		||||
      await handleRemoveOffline(selectedLibrary.id);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onDeleteLibraryClicked = () => {
 | 
			
		||||
  const onDeleteLibraryClicked = async () => {
 | 
			
		||||
    closeAll();
 | 
			
		||||
 | 
			
		||||
    if (selectedLibrary && confirm(`Are you sure you want to delete ${selectedLibrary.name} library?`) == true) {
 | 
			
		||||
      refreshStats(selectedLibraryIndex);
 | 
			
		||||
      await refreshStats(selectedLibraryIndex);
 | 
			
		||||
      if (totalCount[selectedLibraryIndex] > 0) {
 | 
			
		||||
        deleteAssetCount = totalCount[selectedLibraryIndex];
 | 
			
		||||
        confirmDeleteLibrary = selectedLibrary;
 | 
			
		||||
      } else {
 | 
			
		||||
        deletedLibrary = selectedLibrary;
 | 
			
		||||
        handleDelete();
 | 
			
		||||
        await handleDelete();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
@ -348,27 +348,27 @@
 | 
			
		||||
 | 
			
		||||
                {#if showContextMenu}
 | 
			
		||||
                  <Portal target="body">
 | 
			
		||||
                    <ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
 | 
			
		||||
                      <MenuOption on:click={() => onRenameClicked()} text={`Rename`} />
 | 
			
		||||
                    <ContextMenu {...contextMenuPosition} on:outclick={onMenuExit}>
 | 
			
		||||
                      <MenuOption on:click={onRenameClicked} text={`Rename`} />
 | 
			
		||||
 | 
			
		||||
                      {#if selectedLibrary && selectedLibrary.type === LibraryType.External}
 | 
			
		||||
                        <MenuOption on:click={() => onEditImportPathClicked()} text="Edit Import Paths" />
 | 
			
		||||
                        <MenuOption on:click={() => onScanSettingClicked()} text="Scan Settings" />
 | 
			
		||||
                        <MenuOption on:click={onEditImportPathClicked} text="Edit Import Paths" />
 | 
			
		||||
                        <MenuOption on:click={onScanSettingClicked} text="Scan Settings" />
 | 
			
		||||
                        <hr />
 | 
			
		||||
                        <MenuOption on:click={() => onScanNewLibraryClicked()} text="Scan New Library Files" />
 | 
			
		||||
                        <MenuOption on:click={onScanNewLibraryClicked} text="Scan New Library Files" />
 | 
			
		||||
                        <MenuOption
 | 
			
		||||
                          on:click={() => onScanAllLibraryFilesClicked()}
 | 
			
		||||
                          on:click={onScanAllLibraryFilesClicked}
 | 
			
		||||
                          text="Re-scan All Library Files"
 | 
			
		||||
                          subtitle={'Only refreshes modified files'}
 | 
			
		||||
                        />
 | 
			
		||||
                        <MenuOption
 | 
			
		||||
                          on:click={() => onForceScanAllLibraryFilesClicked()}
 | 
			
		||||
                          on:click={onForceScanAllLibraryFilesClicked}
 | 
			
		||||
                          text="Force Re-scan All Library Files"
 | 
			
		||||
                          subtitle={'Refreshes every file'}
 | 
			
		||||
                        />
 | 
			
		||||
                        <hr />
 | 
			
		||||
                        <MenuOption on:click={() => onRemoveOfflineFilesClicked()} text="Remove Offline Files" />
 | 
			
		||||
                        <MenuOption on:click={() => onDeleteLibraryClicked()}>
 | 
			
		||||
                        <MenuOption on:click={onRemoveOfflineFilesClicked} text="Remove Offline Files" />
 | 
			
		||||
                        <MenuOption on:click={onDeleteLibraryClicked}>
 | 
			
		||||
                          <p class="text-red-600">Delete library</p>
 | 
			
		||||
                        </MenuOption>
 | 
			
		||||
                      {/if}
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        handleError(error, 'Unable to link OAuth account');
 | 
			
		||||
      } finally {
 | 
			
		||||
        goto('?open=oauth');
 | 
			
		||||
        await goto('?open=oauth');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -31,8 +31,8 @@
 | 
			
		||||
  let removePartnerDto: PartnerResponseDto | null = null;
 | 
			
		||||
  let partners: Array<PartnerSharing> = [];
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    refreshPartners();
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    await refreshPartners();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const refreshPartners = async () => {
 | 
			
		||||
 | 
			
		||||
@ -172,15 +172,17 @@ export class AssetStore {
 | 
			
		||||
    this.emit(false);
 | 
			
		||||
 | 
			
		||||
    let height = 0;
 | 
			
		||||
    const loaders = [];
 | 
			
		||||
    for (const bucket of this.buckets) {
 | 
			
		||||
      if (height < viewport.height) {
 | 
			
		||||
        height += bucket.bucketHeight;
 | 
			
		||||
        this.loadBucket(bucket.bucketDate, BucketPosition.Visible);
 | 
			
		||||
        loaders.push(this.loadBucket(bucket.bucketDate, BucketPosition.Visible));
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(loaders);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async loadBucket(bucketDate: string, position: BucketPosition): Promise<void> {
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ websocket
 | 
			
		||||
  .on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion))
 | 
			
		||||
  .on('connect_error', (e) => console.log('Websocket Connect Error', e));
 | 
			
		||||
 | 
			
		||||
export const openWebsocketConnection = async () => {
 | 
			
		||||
export const openWebsocketConnection = () => {
 | 
			
		||||
  try {
 | 
			
		||||
    if (!get(user)) {
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
@ -194,3 +194,13 @@ export const findLocale = (code: string | undefined) => {
 | 
			
		||||
    name: language?.name,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const asyncTimeout = (ms: number) => {
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    setTimeout(resolve, ms);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const handlePromiseError = <T>(promise: Promise<T>): void => {
 | 
			
		||||
  promise.catch((error) => console.error(`[utils.ts]:handlePromiseError ${error}`, error));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -28,10 +28,14 @@ describe('Executor Queue test', function () {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    // The first 3 should be finished within 200ms (concurrency 3)
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    eq.addTask(() => timeoutPromiseBuilder(100, 'T1'));
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    eq.addTask(() => timeoutPromiseBuilder(200, 'T2'));
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    eq.addTask(() => timeoutPromiseBuilder(150, 'T3'));
 | 
			
		||||
    // The last task will be executed after 200ms and will finish at 400ms
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    eq.addTask(() => timeoutPromiseBuilder(200, 'T4'));
 | 
			
		||||
 | 
			
		||||
    expect(finished).not.toBeCalled();
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
interface Options {
 | 
			
		||||
  concurrency: number;
 | 
			
		||||
}
 | 
			
		||||
@ -66,6 +68,6 @@ export class ExecutorQueue {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    runnable();
 | 
			
		||||
    handlePromiseError(runnable());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ export const openFileUploadDialog = async (albumId?: string | undefined) => {
 | 
			
		||||
      fileSelector.type = 'file';
 | 
			
		||||
      fileSelector.multiple = true;
 | 
			
		||||
      fileSelector.accept = extensions.join(',');
 | 
			
		||||
      fileSelector.addEventListener('change', async (e: Event) => {
 | 
			
		||||
      fileSelector.addEventListener('change', (e: Event) => {
 | 
			
		||||
        const target = e.target as HTMLInputElement;
 | 
			
		||||
        if (!target.files) {
 | 
			
		||||
          return;
 | 
			
		||||
@ -119,7 +119,7 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(async (error) => {
 | 
			
		||||
      await handleError(error, 'Unable to upload file');
 | 
			
		||||
      handleError(error, 'Unable to upload file');
 | 
			
		||||
      const reason = (await getServerErrorMessage(error)) || error;
 | 
			
		||||
      uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason });
 | 
			
		||||
      return undefined;
 | 
			
		||||
 | 
			
		||||
@ -22,14 +22,15 @@ export async function getServerErrorMessage(error: unknown) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function handleError(error: unknown, message: string) {
 | 
			
		||||
export function handleError(error: unknown, message: string) {
 | 
			
		||||
  if ((error as Error)?.name === 'AbortError') {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.error(`[handleError]: ${message}`, error, (error as Error)?.stack);
 | 
			
		||||
 | 
			
		||||
  let serverMessage = await getServerErrorMessage(error);
 | 
			
		||||
  getServerErrorMessage(error)
 | 
			
		||||
    .then((serverMessage) => {
 | 
			
		||||
      if (serverMessage) {
 | 
			
		||||
        serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`;
 | 
			
		||||
      }
 | 
			
		||||
@ -38,4 +39,8 @@ export async function handleError(error: unknown, message: string) {
 | 
			
		||||
        message: serverMessage || message,
 | 
			
		||||
        type: NotificationType.Error,
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .catch((error) => {
 | 
			
		||||
      console.error(error);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -196,7 +196,7 @@
 | 
			
		||||
  const handleCreateAlbum = async () => {
 | 
			
		||||
    const newAlbum = await createAlbum();
 | 
			
		||||
    if (newAlbum) {
 | 
			
		||||
      goto(`${AppRoute.ALBUMS}/${newAlbum.id}`);
 | 
			
		||||
      await goto(`${AppRoute.ALBUMS}/${newAlbum.id}`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -204,8 +204,8 @@
 | 
			
		||||
    return new Date(dateString).toLocaleDateString($locale, dateFormats.album);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    removeAlbumsIfEmpty();
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    await removeAlbumsIfEmpty();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const removeAlbumsIfEmpty = async () => {
 | 
			
		||||
 | 
			
		||||
@ -220,12 +220,11 @@
 | 
			
		||||
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    if (album.sharedUsers.length > 0) {
 | 
			
		||||
      getFavorite();
 | 
			
		||||
      getNumberOfComments();
 | 
			
		||||
      await Promise.all([getFavorite(), getNumberOfComments()]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const handleKeypress = async (event: KeyboardEvent) => {
 | 
			
		||||
  const handleKeypress = (event: KeyboardEvent) => {
 | 
			
		||||
    if (event.target !== textArea) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -242,12 +241,12 @@
 | 
			
		||||
  const handleStartSlideshow = async () => {
 | 
			
		||||
    const asset = $slideshowShuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
 | 
			
		||||
    if (asset) {
 | 
			
		||||
      setAssetId(asset.id);
 | 
			
		||||
      await setAssetId(asset.id);
 | 
			
		||||
      $slideshowState = SlideshowState.PlaySlideshow;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleEscape = () => {
 | 
			
		||||
  const handleEscape = async () => {
 | 
			
		||||
    if (viewMode === ViewMode.SELECT_USERS) {
 | 
			
		||||
      viewMode = ViewMode.VIEW;
 | 
			
		||||
      return;
 | 
			
		||||
@ -275,7 +274,7 @@
 | 
			
		||||
      assetInteractionStore.clearMultiselect();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    goto(backUrl);
 | 
			
		||||
    await goto(backUrl);
 | 
			
		||||
    return;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -371,7 +370,7 @@
 | 
			
		||||
 | 
			
		||||
  const handleRemoveUser = async (userId: string) => {
 | 
			
		||||
    if (userId == 'me' || userId === $user.id) {
 | 
			
		||||
      goto(backUrl);
 | 
			
		||||
      await goto(backUrl);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -390,7 +389,7 @@
 | 
			
		||||
  const handleRemoveAlbum = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await deleteAlbum({ id: album.id });
 | 
			
		||||
      goto(backUrl);
 | 
			
		||||
      await goto(backUrl);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      handleError(error, 'Unable to delete album');
 | 
			
		||||
    } finally {
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load: PageLoad = async ({ params }) => {
 | 
			
		||||
export const load: PageLoad = ({ params }) => {
 | 
			
		||||
  const albumId = params.albumId;
 | 
			
		||||
 | 
			
		||||
  if (albumId) {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card';
 | 
			
		||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
 | 
			
		||||
import { asyncTimeout } from '$lib/utils';
 | 
			
		||||
import { handleError } from '$lib/utils/handle-error';
 | 
			
		||||
import { createAlbum, deleteAlbum, getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
 | 
			
		||||
import { derived, get, writable } from 'svelte/store';
 | 
			
		||||
@ -20,9 +21,8 @@ export const useAlbums = (properties: AlbumsProperties) => {
 | 
			
		||||
      // Delete album that has no photos and is named ''
 | 
			
		||||
      for (const album of data) {
 | 
			
		||||
        if (album.albumName === '' && album.assetCount === 0) {
 | 
			
		||||
          setTimeout(async () => {
 | 
			
		||||
          await asyncTimeout(500);
 | 
			
		||||
          await handleDeleteAlbum(album);
 | 
			
		||||
          }, 500);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch {
 | 
			
		||||
@ -46,10 +46,7 @@ export const useAlbums = (properties: AlbumsProperties) => {
 | 
			
		||||
    albums.set(get(albums).filter(({ id }) => id !== albumToDelete.id));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function showAlbumContextMenu(
 | 
			
		||||
    contextMenuDetail: OnShowContextMenuDetail,
 | 
			
		||||
    album: AlbumResponseDto,
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
  function showAlbumContextMenu(contextMenuDetail: OnShowContextMenuDetail, album: AlbumResponseDto): void {
 | 
			
		||||
    contextMenuTargetAlbum.set(album);
 | 
			
		||||
 | 
			
		||||
    contextMenuPosition.set({
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load: PageLoad = async () => {
 | 
			
		||||
export const load: PageLoad = () => {
 | 
			
		||||
  redirect(302, AppRoute.ARCHIVE);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load: PageLoad = async () => {
 | 
			
		||||
export const load: PageLoad = () => {
 | 
			
		||||
  redirect(302, AppRoute.FAVORITES);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
  import { DateTime, Duration } from 'luxon';
 | 
			
		||||
  import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
@ -26,8 +27,8 @@
 | 
			
		||||
  let viewingAssetCursor = 0;
 | 
			
		||||
  let showSettingsModal = false;
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    loadMapMarkers().then((data) => (mapMarkers = data));
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    mapMarkers = await loadMapMarkers();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  onDestroy(() => {
 | 
			
		||||
@ -35,7 +36,7 @@
 | 
			
		||||
    assetViewingStore.showAssetViewer(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  $: $featureFlags.map || goto(AppRoute.PHOTOS);
 | 
			
		||||
  $: $featureFlags.map || handlePromiseError(goto(AppRoute.PHOTOS));
 | 
			
		||||
  const omit = (obj: MapSettings, key: string) => {
 | 
			
		||||
    return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key));
 | 
			
		||||
  };
 | 
			
		||||
@ -85,21 +86,21 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onViewAssets(assetIds: string[]) {
 | 
			
		||||
    assetViewingStore.setAssetId(assetIds[0]);
 | 
			
		||||
  async function onViewAssets(assetIds: string[]) {
 | 
			
		||||
    await assetViewingStore.setAssetId(assetIds[0]);
 | 
			
		||||
    viewingAssets = assetIds;
 | 
			
		||||
    viewingAssetCursor = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function navigateNext() {
 | 
			
		||||
  async function navigateNext() {
 | 
			
		||||
    if (viewingAssetCursor < viewingAssets.length - 1) {
 | 
			
		||||
      assetViewingStore.setAssetId(viewingAssets[++viewingAssetCursor]);
 | 
			
		||||
      await assetViewingStore.setAssetId(viewingAssets[++viewingAssetCursor]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function navigatePrevious() {
 | 
			
		||||
  async function navigatePrevious() {
 | 
			
		||||
    if (viewingAssetCursor > 0) {
 | 
			
		||||
      assetViewingStore.setAssetId(viewingAssets[--viewingAssetCursor]);
 | 
			
		||||
      await assetViewingStore.setAssetId(viewingAssets[--viewingAssetCursor]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async () => {
 | 
			
		||||
export const load = (() => {
 | 
			
		||||
  redirect(302, AppRoute.PHOTOS);
 | 
			
		||||
}) satisfies PageLoad;
 | 
			
		||||
 | 
			
		||||
@ -81,12 +81,12 @@
 | 
			
		||||
 | 
			
		||||
  const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    document.addEventListener('keydown', onKeyboardPress);
 | 
			
		||||
    const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
 | 
			
		||||
    if (getSearchedPeople) {
 | 
			
		||||
      searchName = getSearchedPeople;
 | 
			
		||||
      handleSearchPeople(true);
 | 
			
		||||
      await handleSearchPeople(true);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -108,10 +108,10 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSearch = (force: boolean) => {
 | 
			
		||||
  const handleSearch = async (force: boolean) => {
 | 
			
		||||
    $page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName);
 | 
			
		||||
    goto($page.url);
 | 
			
		||||
    handleSearchPeople(force);
 | 
			
		||||
    await goto($page.url);
 | 
			
		||||
    await handleSearchPeople(force);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleCloseClick = () => {
 | 
			
		||||
@ -293,8 +293,8 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleMergePeople = (detail: PersonResponseDto) => {
 | 
			
		||||
    goto(
 | 
			
		||||
  const handleMergePeople = async (detail: PersonResponseDto) => {
 | 
			
		||||
    await goto(
 | 
			
		||||
      `${AppRoute.PEOPLE}/${detail.id}?${QueryParameter.ACTION}=${ActionQueryParameterValue.MERGE}&${QueryParameter.PREVIOUS_ROUTE}=${AppRoute.PEOPLE}`,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
@ -303,7 +303,7 @@
 | 
			
		||||
    if (searchName === '') {
 | 
			
		||||
      if ($page.url.searchParams.has(QueryParameter.SEARCHED_PEOPLE)) {
 | 
			
		||||
        $page.url.searchParams.delete(QueryParameter.SEARCHED_PEOPLE);
 | 
			
		||||
        goto($page.url);
 | 
			
		||||
        await goto($page.url);
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -331,7 +331,7 @@
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (personName === '') {
 | 
			
		||||
      changeName();
 | 
			
		||||
      await changeName();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const data = await searchPerson({ name: personName, withHidden: true });
 | 
			
		||||
@ -359,7 +359,7 @@
 | 
			
		||||
        .slice(0, 3);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    changeName();
 | 
			
		||||
    await changeName();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const submitBirthDateChange = async (value: string) => {
 | 
			
		||||
 | 
			
		||||
@ -185,7 +185,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleEscape = () => {
 | 
			
		||||
  const handleEscape = async () => {
 | 
			
		||||
    if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -193,7 +193,7 @@
 | 
			
		||||
      assetInteractionStore.clearMultiselect();
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      goto(previousRoute);
 | 
			
		||||
      await goto(previousRoute);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
@ -235,7 +235,7 @@
 | 
			
		||||
        type: NotificationType.Info,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      goto(previousRoute, { replaceState: true });
 | 
			
		||||
      await goto(previousRoute, { replaceState: true });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      handleError(error, 'Unable to hide person');
 | 
			
		||||
    }
 | 
			
		||||
@ -244,7 +244,7 @@
 | 
			
		||||
  const handleMerge = async (person: PersonResponseDto) => {
 | 
			
		||||
    const { assets } = await getPersonStatistics({ id: person.id });
 | 
			
		||||
    numberOfAssets = assets;
 | 
			
		||||
    handleGoBack();
 | 
			
		||||
    await handleGoBack();
 | 
			
		||||
 | 
			
		||||
    data.person = person;
 | 
			
		||||
 | 
			
		||||
@ -292,7 +292,7 @@
 | 
			
		||||
        refreshAssetGrid = !refreshAssetGrid;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true });
 | 
			
		||||
      await goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      handleError(error, 'Unable to save name');
 | 
			
		||||
    }
 | 
			
		||||
@ -341,7 +341,7 @@
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (name === '') {
 | 
			
		||||
      changeName();
 | 
			
		||||
      await changeName();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -366,7 +366,7 @@
 | 
			
		||||
      viewMode = ViewMode.SUGGEST_MERGE;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    changeName();
 | 
			
		||||
    await changeName();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSetBirthDate = async (birthDate: string) => {
 | 
			
		||||
@ -392,11 +392,11 @@
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleGoBack = () => {
 | 
			
		||||
  const handleGoBack = async () => {
 | 
			
		||||
    viewMode = ViewMode.VIEW_ASSETS;
 | 
			
		||||
    if ($page.url.searchParams.has(QueryParameter.ACTION)) {
 | 
			
		||||
      $page.url.searchParams.delete(QueryParameter.ACTION);
 | 
			
		||||
      goto($page.url);
 | 
			
		||||
      await goto($page.url);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async ({ params }) => {
 | 
			
		||||
export const load = (({ params }) => {
 | 
			
		||||
  redirect(302, `${AppRoute.PEOPLE}/${params.personId}`);
 | 
			
		||||
}) satisfies PageLoad;
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async () => {
 | 
			
		||||
export const load = (() => {
 | 
			
		||||
  redirect(302, AppRoute.PHOTOS);
 | 
			
		||||
}) satisfies PageLoad;
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@
 | 
			
		||||
  import type { Viewport } from '$lib/stores/assets.store';
 | 
			
		||||
  import { locale } from '$lib/stores/preferences.store';
 | 
			
		||||
  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
  import { parseUtcDate } from '$lib/utils/date-time';
 | 
			
		||||
 | 
			
		||||
  const MAX_ASSET_COUNT = 5000;
 | 
			
		||||
@ -53,7 +54,7 @@
 | 
			
		||||
 | 
			
		||||
  const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
 | 
			
		||||
 | 
			
		||||
  const handleKeyboardPress = (event: KeyboardEvent) => {
 | 
			
		||||
  const handleKeyboardPress = async (event: KeyboardEvent) => {
 | 
			
		||||
    if (shouldIgnoreShortcut(event)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -65,7 +66,7 @@
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          if (!$preventRaceConditionSearchBar) {
 | 
			
		||||
            goto(previousRoute);
 | 
			
		||||
            await goto(previousRoute);
 | 
			
		||||
          }
 | 
			
		||||
          $preventRaceConditionSearchBar = false;
 | 
			
		||||
          return;
 | 
			
		||||
@ -108,13 +109,13 @@
 | 
			
		||||
    return searchQuery ? JSON.parse(searchQuery) : {};
 | 
			
		||||
  })();
 | 
			
		||||
 | 
			
		||||
  $: terms, onSearchQueryUpdate();
 | 
			
		||||
  $: terms, handlePromiseError(onSearchQueryUpdate());
 | 
			
		||||
 | 
			
		||||
  async function onSearchQueryUpdate() {
 | 
			
		||||
    nextPage = 1;
 | 
			
		||||
    searchResultAssets = [];
 | 
			
		||||
    searchResultAlbums = [];
 | 
			
		||||
    loadNextPage();
 | 
			
		||||
    await loadNextPage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export const loadNextPage = async () => {
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async () => {
 | 
			
		||||
export const load = (() => {
 | 
			
		||||
  redirect(302, AppRoute.SEARCH);
 | 
			
		||||
}) satisfies PageLoad;
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
  const createSharedAlbum = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const newAlbum = await createAlbum({ createAlbumDto: { albumName: '' } });
 | 
			
		||||
      goto(`${AppRoute.ALBUMS}/${newAlbum.id}`);
 | 
			
		||||
      await goto(`${AppRoute.ALBUMS}/${newAlbum.id}`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      handleError(error, 'Unable to create album');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@
 | 
			
		||||
      deleteLinkId = null;
 | 
			
		||||
      await refresh();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      await handleError(error, 'Unable to delete shared link');
 | 
			
		||||
      handleError(error, 'Unable to delete shared link');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,10 +24,11 @@
 | 
			
		||||
  import { emptyTrash, restoreTrash } from '@immich/sdk';
 | 
			
		||||
  import { mdiDeleteOutline, mdiHistory } from '@mdi/js';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
  import { handlePromiseError } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
  $: $featureFlags.trash || goto(AppRoute.PHOTOS);
 | 
			
		||||
  $featureFlags.trash || handlePromiseError(goto(AppRoute.PHOTOS));
 | 
			
		||||
 | 
			
		||||
  const assetStore = new AssetStore({ isTrashed: true });
 | 
			
		||||
  const assetInteractionStore = createAssetInteractionStore();
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async () => {
 | 
			
		||||
export const load = (() => {
 | 
			
		||||
  redirect(302, AppRoute.TRASH);
 | 
			
		||||
}) satisfies PageLoad;
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import type { LayoutLoad } from './$types';
 | 
			
		||||
export const ssr = false;
 | 
			
		||||
export const csr = true;
 | 
			
		||||
 | 
			
		||||
export const load = (async () => {
 | 
			
		||||
export const load = (() => {
 | 
			
		||||
  return {
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Immich',
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants';
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async () => {
 | 
			
		||||
export const load = (() => {
 | 
			
		||||
  redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
 | 
			
		||||
}) satisfies PageLoad;
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@
 | 
			
		||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
			
		||||
  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
 | 
			
		||||
  import { AppRoute } from '$lib/constants';
 | 
			
		||||
  import { asyncTimeout } from '$lib/utils';
 | 
			
		||||
  import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk';
 | 
			
		||||
  import { mdiCog } from '@mdi/js';
 | 
			
		||||
  import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
@ -11,21 +12,19 @@
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
  let timer: ReturnType<typeof setInterval>;
 | 
			
		||||
 | 
			
		||||
  let jobs: AllJobStatusResponseDto;
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    jobs = await getAllJobsStatus();
 | 
			
		||||
  };
 | 
			
		||||
  let running = true;
 | 
			
		||||
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    await load();
 | 
			
		||||
    timer = setInterval(load, 5000);
 | 
			
		||||
    while (running) {
 | 
			
		||||
      jobs = await getAllJobsStatus();
 | 
			
		||||
      await asyncTimeout(5000);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  onDestroy(() => {
 | 
			
		||||
    clearInterval(timer);
 | 
			
		||||
    running = false;
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,19 +4,21 @@
 | 
			
		||||
  import { getServerStatistics } from '@immich/sdk';
 | 
			
		||||
  import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
  import { asyncTimeout } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
  let setIntervalHandler: ReturnType<typeof setInterval>;
 | 
			
		||||
  let running = true;
 | 
			
		||||
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    setIntervalHandler = setInterval(async () => {
 | 
			
		||||
    while (running) {
 | 
			
		||||
      data.stats = await getServerStatistics();
 | 
			
		||||
    }, 5000);
 | 
			
		||||
      await asyncTimeout(5000);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  onDestroy(() => {
 | 
			
		||||
    clearInterval(setIntervalHandler);
 | 
			
		||||
    running = false;
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@
 | 
			
		||||
    shouldShowCreateUserForm = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const editUserHandler = async (user: UserResponseDto) => {
 | 
			
		||||
  const editUserHandler = (user: UserResponseDto) => {
 | 
			
		||||
    selectedUser = user;
 | 
			
		||||
    shouldShowEditUserForm = true;
 | 
			
		||||
  };
 | 
			
		||||
@ -67,7 +67,7 @@
 | 
			
		||||
    shouldShowInfoPanel = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const deleteUserHandler = async (user: UserResponseDto) => {
 | 
			
		||||
  const deleteUserHandler = (user: UserResponseDto) => {
 | 
			
		||||
    selectedUser = user;
 | 
			
		||||
    shouldShowDeleteConfirmDialog = true;
 | 
			
		||||
  };
 | 
			
		||||
@ -82,7 +82,7 @@
 | 
			
		||||
    shouldShowDeleteConfirmDialog = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const restoreUserHandler = async (user: UserResponseDto) => {
 | 
			
		||||
  const restoreUserHandler = (user: UserResponseDto) => {
 | 
			
		||||
    selectedUser = user;
 | 
			
		||||
    shouldShowRestoreDialog = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,17 @@
 | 
			
		||||
  import ChangePasswordForm from '$lib/components/forms/change-password-form.svelte';
 | 
			
		||||
  import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
 | 
			
		||||
  import { AppRoute } from '$lib/constants';
 | 
			
		||||
  import { user } from '$lib/stores/user.store';
 | 
			
		||||
  import { resetSavedUser, user } from '$lib/stores/user.store';
 | 
			
		||||
  import { logout } from '@immich/sdk';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
  const onSuccess = async () => {
 | 
			
		||||
    await goto(AppRoute.AUTH_LOGIN);
 | 
			
		||||
    resetSavedUser();
 | 
			
		||||
    await logout();
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<FullscreenContainer title={data.meta.title}>
 | 
			
		||||
@ -18,5 +25,5 @@
 | 
			
		||||
    enter the new password below.
 | 
			
		||||
  </p>
 | 
			
		||||
 | 
			
		||||
  <ChangePasswordForm user={$user} on:success={() => goto(AppRoute.AUTH_LOGIN)} />
 | 
			
		||||
  <ChangePasswordForm user={$user} on:success={onSuccess} />
 | 
			
		||||
</FullscreenContainer>
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,12 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { afterNavigate, goto } from '$app/navigation';
 | 
			
		||||
  import { goto } from '$app/navigation';
 | 
			
		||||
  import LoginForm from '$lib/components/forms/login-form.svelte';
 | 
			
		||||
  import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
 | 
			
		||||
  import { AppRoute } from '$lib/constants';
 | 
			
		||||
  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
 | 
			
		||||
  import { resetSavedUser } from '$lib/stores/user.store';
 | 
			
		||||
  import { logout } from '@immich/sdk';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
  afterNavigate(async ({ from }) => {
 | 
			
		||||
    if (from?.url?.pathname === AppRoute.AUTH_CHANGE_PASSWORD) {
 | 
			
		||||
      resetSavedUser();
 | 
			
		||||
      await logout();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $featureFlags.loaded}
 | 
			
		||||
 | 
			
		||||
@ -29,17 +29,17 @@
 | 
			
		||||
  const handleDoneClicked = async () => {
 | 
			
		||||
    if (index >= onboardingSteps.length - 1) {
 | 
			
		||||
      await setAdminOnboarding();
 | 
			
		||||
      goto(AppRoute.PHOTOS);
 | 
			
		||||
      await goto(AppRoute.PHOTOS);
 | 
			
		||||
    } else {
 | 
			
		||||
      index++;
 | 
			
		||||
      goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`);
 | 
			
		||||
      await goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePrevious = () => {
 | 
			
		||||
  const handlePrevious = async () => {
 | 
			
		||||
    if (index >= 1) {
 | 
			
		||||
      index--;
 | 
			
		||||
      goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`);
 | 
			
		||||
      await goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -4,12 +4,6 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
 | 
			
		||||
/** @type {import('@sveltejs/kit').Config} */
 | 
			
		||||
const config = {
 | 
			
		||||
  preprocess: vitePreprocess(),
 | 
			
		||||
  onwarn: (warning, handler) => {
 | 
			
		||||
    if (warning.code.includes('a11y')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    handler(warning);
 | 
			
		||||
  },
 | 
			
		||||
  kit: {
 | 
			
		||||
    adapter: adapter({
 | 
			
		||||
      // default options are shown. On some platforms
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user