mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(web): lighter timeline buckets
This commit is contained in:
		
							parent
							
								
									242a559e0f
								
							
						
					
					
						commit
						5a8f9f3b5c
					
				
							
								
								
									
										21
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@ -8,7 +8,11 @@
 | 
				
			|||||||
  "[typescript]": {
 | 
					  "[typescript]": {
 | 
				
			||||||
    "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
					    "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
				
			||||||
    "editor.tabSize": 2,
 | 
					    "editor.tabSize": 2,
 | 
				
			||||||
    "editor.formatOnSave": true
 | 
					    "editor.formatOnSave": true,
 | 
				
			||||||
 | 
					    "editor.codeActionsOnSave": {
 | 
				
			||||||
 | 
					      "source.removeUnusedImports": "explicit",
 | 
				
			||||||
 | 
					      "source.organizeImports": "explicit"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "[css]": {
 | 
					  "[css]": {
 | 
				
			||||||
    "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
					    "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
				
			||||||
@ -17,13 +21,14 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "[svelte]": {
 | 
					  "[svelte]": {
 | 
				
			||||||
    "editor.defaultFormatter": "svelte.svelte-vscode",
 | 
					    "editor.defaultFormatter": "svelte.svelte-vscode",
 | 
				
			||||||
    "editor.tabSize": 2
 | 
					    "editor.tabSize": 2,
 | 
				
			||||||
 | 
					    "editor.codeActionsOnSave": {
 | 
				
			||||||
 | 
					      "source.removeUnusedImports": "explicit",
 | 
				
			||||||
 | 
					      "source.organizeImports": "explicit"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "svelte.enable-ts-plugin": true,
 | 
					  "svelte.enable-ts-plugin": true,
 | 
				
			||||||
  "eslint.validate": [
 | 
					  "eslint.validate": ["javascript", "svelte"],
 | 
				
			||||||
    "javascript",
 | 
					 | 
				
			||||||
    "svelte"
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  "typescript.preferences.importModuleSpecifier": "non-relative",
 | 
					  "typescript.preferences.importModuleSpecifier": "non-relative",
 | 
				
			||||||
  "[dart]": {
 | 
					  "[dart]": {
 | 
				
			||||||
    "editor.formatOnSave": true,
 | 
					    "editor.formatOnSave": true,
 | 
				
			||||||
@ -34,9 +39,7 @@
 | 
				
			|||||||
    "editor.wordBasedSuggestions": "off",
 | 
					    "editor.wordBasedSuggestions": "off",
 | 
				
			||||||
    "editor.defaultFormatter": "Dart-Code.dart-code"
 | 
					    "editor.defaultFormatter": "Dart-Code.dart-code"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "cSpell.words": [
 | 
					  "cSpell.words": ["immich"],
 | 
				
			||||||
    "immich"
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  "explorer.fileNesting.enabled": true,
 | 
					  "explorer.fileNesting.enabled": true,
 | 
				
			||||||
  "explorer.fileNesting.patterns": {
 | 
					  "explorer.fileNesting.patterns": {
 | 
				
			||||||
    "*.ts": "${capture}.spec.ts,${capture}.mock.ts",
 | 
					    "*.ts": "${capture}.spec.ts,${capture}.mock.ts",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,25 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import { shortcut } from '$lib/actions/shortcut';
 | 
				
			||||||
  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
					  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
				
			||||||
 | 
					  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
					  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
				
			||||||
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
 | 
					  import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
 | 
				
			||||||
 | 
					  import { handlePromiseError } from '$lib/utils';
 | 
				
			||||||
 | 
					  import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
					  import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
  import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
 | 
					  import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					  import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
 | 
				
			||||||
  import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
 | 
					  import { onDestroy } from 'svelte';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
					  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
  import DownloadAction from '../photos-page/actions/download-action.svelte';
 | 
					  import DownloadAction from '../photos-page/actions/download-action.svelte';
 | 
				
			||||||
  import AssetGrid from '../photos-page/asset-grid.svelte';
 | 
					  import AssetGrid from '../photos-page/asset-grid.svelte';
 | 
				
			||||||
  import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte';
 | 
					 | 
				
			||||||
  import ControlAppBar from '../shared-components/control-app-bar.svelte';
 | 
					  import ControlAppBar from '../shared-components/control-app-bar.svelte';
 | 
				
			||||||
  import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
 | 
					  import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
 | 
				
			||||||
  import ThemeButton from '../shared-components/theme-button.svelte';
 | 
					  import ThemeButton from '../shared-components/theme-button.svelte';
 | 
				
			||||||
  import { shortcut } from '$lib/actions/shortcut';
 | 
					 | 
				
			||||||
  import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { handlePromiseError } from '$lib/utils';
 | 
					 | 
				
			||||||
  import AlbumSummary from './album-summary.svelte';
 | 
					  import AlbumSummary from './album-summary.svelte';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
  import { onDestroy } from 'svelte';
 | 
					 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    sharedLink: SharedLinkResponseDto;
 | 
					    sharedLink: SharedLinkResponseDto;
 | 
				
			||||||
@ -36,7 +36,7 @@
 | 
				
			|||||||
  $effect(() => void assetStore.updateOptions({ albumId: album.id, order: album.order }));
 | 
					  $effect(() => void assetStore.updateOptions({ albumId: album.id, order: album.order }));
 | 
				
			||||||
  onDestroy(() => assetStore.destroy());
 | 
					  onDestroy(() => assetStore.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dragAndDropFilesStore.subscribe((value) => {
 | 
					  dragAndDropFilesStore.subscribe((value) => {
 | 
				
			||||||
    if (value.isDragging && value.files.length > 0) {
 | 
					    if (value.isDragging && value.files.length > 0) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,19 @@
 | 
				
			|||||||
import type { AssetAction } from '$lib/constants';
 | 
					import type { AssetAction } from '$lib/constants';
 | 
				
			||||||
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
 | 
					import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					import type { AlbumResponseDto } from '@immich/sdk';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ActionMap = {
 | 
					type ActionMap = {
 | 
				
			||||||
  [AssetAction.ARCHIVE]: { asset: AssetResponseDto };
 | 
					  [AssetAction.ARCHIVE]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.UNARCHIVE]: { asset: AssetResponseDto };
 | 
					  [AssetAction.UNARCHIVE]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.FAVORITE]: { asset: AssetResponseDto };
 | 
					  [AssetAction.FAVORITE]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.UNFAVORITE]: { asset: AssetResponseDto };
 | 
					  [AssetAction.UNFAVORITE]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.TRASH]: { asset: AssetResponseDto };
 | 
					  [AssetAction.TRASH]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.DELETE]: { asset: AssetResponseDto };
 | 
					  [AssetAction.DELETE]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.RESTORE]: { asset: AssetResponseDto };
 | 
					  [AssetAction.RESTORE]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.ADD]: { asset: AssetResponseDto };
 | 
					  [AssetAction.ADD]: { asset: TimelineAsset };
 | 
				
			||||||
  [AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
 | 
					  [AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto };
 | 
				
			||||||
  [AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
 | 
					  [AssetAction.UNSTACK]: { assets: TimelineAsset[] };
 | 
				
			||||||
  [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
 | 
					  [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Action = {
 | 
					export type Action = {
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@
 | 
				
			|||||||
  import Portal from '$lib/components/shared-components/portal/portal.svelte';
 | 
					  import Portal from '$lib/components/shared-components/portal/portal.svelte';
 | 
				
			||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils';
 | 
					  import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
 | 
					  import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
 | 
					  import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
@ -24,14 +25,14 @@
 | 
				
			|||||||
    showSelectionModal = false;
 | 
					    showSelectionModal = false;
 | 
				
			||||||
    const album = await addAssetsToNewAlbum(albumName, [asset.id]);
 | 
					    const album = await addAssetsToNewAlbum(albumName, [asset.id]);
 | 
				
			||||||
    if (album) {
 | 
					    if (album) {
 | 
				
			||||||
      onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album });
 | 
					      onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddToAlbum = async (album: AlbumResponseDto) => {
 | 
					  const handleAddToAlbum = async (album: AlbumResponseDto) => {
 | 
				
			||||||
    showSelectionModal = false;
 | 
					    showSelectionModal = false;
 | 
				
			||||||
    await addAssetsToAlbum(album.id, [asset.id]);
 | 
					    await addAssetsToAlbum(album.id, [asset.id]);
 | 
				
			||||||
    onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album });
 | 
					    onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@
 | 
				
			|||||||
  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 { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { toggleArchive } from '$lib/utils/asset-utils';
 | 
					  import { toggleArchive } from '$lib/utils/asset-utils';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import type { AssetResponseDto } from '@immich/sdk';
 | 
					  import type { AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline } from '@mdi/js';
 | 
					  import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
@ -18,11 +19,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const onArchive = async () => {
 | 
					  const onArchive = async () => {
 | 
				
			||||||
    if (!asset.isArchived) {
 | 
					    if (!asset.isArchived) {
 | 
				
			||||||
      preAction({ type: AssetAction.ARCHIVE, asset });
 | 
					      preAction({ type: AssetAction.ARCHIVE, asset: toTimelineAsset(asset) });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const updatedAsset = await toggleArchive(asset);
 | 
					    const updatedAsset = await toggleArchive(asset);
 | 
				
			||||||
    if (updatedAsset) {
 | 
					    if (updatedAsset) {
 | 
				
			||||||
      onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset });
 | 
					      onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: toTimelineAsset(asset) });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@
 | 
				
			|||||||
  import { showDeleteModal } from '$lib/stores/preferences.store';
 | 
					  import { showDeleteModal } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { featureFlags } from '$lib/stores/server-config.store';
 | 
					  import { featureFlags } from '$lib/stores/server-config.store';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
 | 
					  import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
 | 
					  import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
@ -42,9 +43,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const trashAsset = async () => {
 | 
					  const trashAsset = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      preAction({ type: AssetAction.TRASH, asset });
 | 
					      preAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) });
 | 
				
			||||||
      await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } });
 | 
					      await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } });
 | 
				
			||||||
      onAction({ type: AssetAction.TRASH, asset });
 | 
					      onAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      notificationController.show({
 | 
					      notificationController.show({
 | 
				
			||||||
        message: $t('moved_to_trash'),
 | 
					        message: $t('moved_to_trash'),
 | 
				
			||||||
@ -58,7 +59,7 @@
 | 
				
			|||||||
  const deleteAsset = async () => {
 | 
					  const deleteAsset = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } });
 | 
					      await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } });
 | 
				
			||||||
      onAction({ type: AssetAction.DELETE, asset });
 | 
					      onAction({ type: AssetAction.DELETE, asset: toTimelineAsset(asset) });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      notificationController.show({
 | 
					      notificationController.show({
 | 
				
			||||||
        message: $t('permanently_deleted_asset'),
 | 
					        message: $t('permanently_deleted_asset'),
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@
 | 
				
			|||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import { updateAsset, type AssetResponseDto } from '@immich/sdk';
 | 
					  import { updateAsset, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiHeart, mdiHeartOutline } from '@mdi/js';
 | 
					  import { mdiHeart, mdiHeartOutline } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
@ -30,7 +31,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      asset = { ...asset, isFavorite: data.isFavorite };
 | 
					      asset = { ...asset, isFavorite: data.isFavorite };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      onAction({ type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE, asset });
 | 
					      onAction({
 | 
				
			||||||
 | 
					        type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE,
 | 
				
			||||||
 | 
					        asset: toTimelineAsset(asset),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      notificationController.show({
 | 
					      notificationController.show({
 | 
				
			||||||
        type: NotificationType.Info,
 | 
					        type: NotificationType.Info,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,13 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  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 { dialogController } from '$lib/components/shared-components/dialog/dialog';
 | 
				
			||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { keepThisDeleteOthers } from '$lib/utils/asset-utils';
 | 
					  import { keepThisDeleteOthers } from '$lib/utils/asset-utils';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import type { AssetResponseDto, StackResponseDto } from '@immich/sdk';
 | 
					  import type { AssetResponseDto, StackResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiPinOutline } from '@mdi/js';
 | 
					  import { mdiPinOutline } from '@mdi/js';
 | 
				
			||||||
  import type { OnAction } from './action';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import { dialogController } from '$lib/components/shared-components/dialog/dialog';
 | 
					  import type { OnAction } from './action';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    stack: StackResponseDto;
 | 
					    stack: StackResponseDto;
 | 
				
			||||||
@ -29,7 +30,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const keptAsset = await keepThisDeleteOthers(asset, stack);
 | 
					    const keptAsset = await keepThisDeleteOthers(asset, stack);
 | 
				
			||||||
    if (keptAsset) {
 | 
					    if (keptAsset) {
 | 
				
			||||||
      onAction({ type: AssetAction.UNSTACK, assets: [keptAsset] });
 | 
					      onAction({ type: AssetAction.UNSTACK, assets: [toTimelineAsset(keptAsset)] });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@
 | 
				
			|||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import { restoreAssets, type AssetResponseDto } from '@immich/sdk';
 | 
					  import { restoreAssets, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiHistory } from '@mdi/js';
 | 
					  import { mdiHistory } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
@ -23,7 +24,7 @@
 | 
				
			|||||||
      await restoreAssets({ bulkIdsDto: { ids: [asset.id] } });
 | 
					      await restoreAssets({ bulkIdsDto: { ids: [asset.id] } });
 | 
				
			||||||
      asset.isTrashed = false;
 | 
					      asset.isTrashed = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      onAction({ type: AssetAction.RESTORE, asset });
 | 
					      onAction({ type: AssetAction.RESTORE, asset: toTimelineAsset(asset) });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      notificationController.show({
 | 
					      notificationController.show({
 | 
				
			||||||
        type: NotificationType.Info,
 | 
					        type: NotificationType.Info,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
  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 { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { deleteStack } from '$lib/utils/asset-utils';
 | 
					  import { deleteStack } from '$lib/utils/asset-utils';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import type { StackResponseDto } from '@immich/sdk';
 | 
					  import type { StackResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiImageMinusOutline } from '@mdi/js';
 | 
					  import { mdiImageMinusOutline } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
@ -17,7 +18,7 @@
 | 
				
			|||||||
  const handleUnstack = async () => {
 | 
					  const handleUnstack = async () => {
 | 
				
			||||||
    const unstackedAssets = await deleteStack([stack.id]);
 | 
					    const unstackedAssets = await deleteStack([stack.id]);
 | 
				
			||||||
    if (unstackedAssets) {
 | 
					    if (unstackedAssets) {
 | 
				
			||||||
      onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets });
 | 
					      onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets.map((a) => toTimelineAsset(a)) });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
				
			|||||||
@ -13,8 +13,9 @@ describe('AssetViewerNavBar component', () => {
 | 
				
			|||||||
    showDownloadButton: false,
 | 
					    showDownloadButton: false,
 | 
				
			||||||
    showMotionPlayButton: false,
 | 
					    showMotionPlayButton: false,
 | 
				
			||||||
    showShareButton: false,
 | 
					    showShareButton: false,
 | 
				
			||||||
 | 
					    preAction: () => {},
 | 
				
			||||||
    onZoomImage: () => {},
 | 
					    onZoomImage: () => {},
 | 
				
			||||||
    onCopyImage: () => {},
 | 
					    onCopyImage: async () => {},
 | 
				
			||||||
    onAction: () => {},
 | 
					    onAction: () => {},
 | 
				
			||||||
    onRunJob: () => {},
 | 
					    onRunJob: () => {},
 | 
				
			||||||
    onPlaySlideshow: () => {},
 | 
					    onPlaySlideshow: () => {},
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@
 | 
				
			|||||||
  import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
 | 
					  import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { SlideshowHistory } from '$lib/utils/slideshow-history';
 | 
					  import { SlideshowHistory } from '$lib/utils/slideshow-history';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
    AssetJobName,
 | 
					    AssetJobName,
 | 
				
			||||||
    AssetTypeEnum,
 | 
					    AssetTypeEnum,
 | 
				
			||||||
@ -52,7 +53,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    asset: AssetResponseDto;
 | 
					    asset: AssetResponseDto;
 | 
				
			||||||
    preloadAssets?: AssetResponseDto[];
 | 
					    preloadAssets?: { id: string }[];
 | 
				
			||||||
    showNavigation?: boolean;
 | 
					    showNavigation?: boolean;
 | 
				
			||||||
    withStacked?: boolean;
 | 
					    withStacked?: boolean;
 | 
				
			||||||
    isShared?: boolean;
 | 
					    isShared?: boolean;
 | 
				
			||||||
@ -62,7 +63,7 @@
 | 
				
			|||||||
    onAction?: OnAction | undefined;
 | 
					    onAction?: OnAction | undefined;
 | 
				
			||||||
    reactions?: ActivityResponseDto[];
 | 
					    reactions?: ActivityResponseDto[];
 | 
				
			||||||
    showCloseButton?: boolean;
 | 
					    showCloseButton?: boolean;
 | 
				
			||||||
    onClose: (dto: { asset: AssetResponseDto }) => void;
 | 
					    onClose: (asset: AssetResponseDto) => void;
 | 
				
			||||||
    onNext: () => Promise<HasAsset>;
 | 
					    onNext: () => Promise<HasAsset>;
 | 
				
			||||||
    onPrevious: () => Promise<HasAsset>;
 | 
					    onPrevious: () => Promise<HasAsset>;
 | 
				
			||||||
    onRandom: () => Promise<AssetResponseDto | undefined>;
 | 
					    onRandom: () => Promise<AssetResponseDto | undefined>;
 | 
				
			||||||
@ -267,7 +268,7 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const closeViewer = () => {
 | 
					  const closeViewer = () => {
 | 
				
			||||||
    onClose({ asset });
 | 
					    onClose(asset);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const closeEditor = () => {
 | 
					  const closeEditor = () => {
 | 
				
			||||||
@ -605,8 +606,8 @@
 | 
				
			|||||||
              imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
 | 
					              imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
 | 
				
			||||||
              brokenAssetClass="text-xs"
 | 
					              brokenAssetClass="text-xs"
 | 
				
			||||||
              dimmed={stackedAsset.id !== asset.id}
 | 
					              dimmed={stackedAsset.id !== asset.id}
 | 
				
			||||||
              asset={stackedAsset}
 | 
					              asset={toTimelineAsset(stackedAsset)}
 | 
				
			||||||
              onClick={(stackedAsset) => {
 | 
					              onClick={() => {
 | 
				
			||||||
                asset = stackedAsset;
 | 
					                asset = stackedAsset;
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
              onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
 | 
					              onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@
 | 
				
			|||||||
  import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
 | 
					  import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { getBoundingBox } from '$lib/utils/people-utils';
 | 
					  import { getBoundingBox } from '$lib/utils/people-utils';
 | 
				
			||||||
  import { getAltText } from '$lib/utils/thumbnail-util';
 | 
					  import { getAltText } from '$lib/utils/thumbnail-util';
 | 
				
			||||||
  import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
 | 
					  import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { onDestroy, onMount } from 'svelte';
 | 
					  import { onDestroy, onMount } from 'svelte';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
 | 
					  import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
 | 
				
			||||||
@ -24,7 +24,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    asset: AssetResponseDto;
 | 
					    asset: AssetResponseDto;
 | 
				
			||||||
    preloadAssets?: AssetResponseDto[] | undefined;
 | 
					    preloadAssets?: { id: string }[] | undefined;
 | 
				
			||||||
    element?: HTMLDivElement | undefined;
 | 
					    element?: HTMLDivElement | undefined;
 | 
				
			||||||
    haveFadeTransition?: boolean;
 | 
					    haveFadeTransition?: boolean;
 | 
				
			||||||
    sharedLink?: SharedLinkResponseDto | undefined;
 | 
					    sharedLink?: SharedLinkResponseDto | undefined;
 | 
				
			||||||
@ -68,12 +68,10 @@
 | 
				
			|||||||
    $boundingBoxesArray = [];
 | 
					    $boundingBoxesArray = [];
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: AssetResponseDto[]) => {
 | 
					  const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: { id: string }[]) => {
 | 
				
			||||||
    for (const preloadAsset of preloadAssets || []) {
 | 
					    for (const preloadAsset of preloadAssets || []) {
 | 
				
			||||||
      if (preloadAsset.type === AssetTypeEnum.Image) {
 | 
					 | 
				
			||||||
      let img = new Image();
 | 
					      let img = new Image();
 | 
				
			||||||
        img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash);
 | 
					      img.src = getAssetUrl(preloadAsset.id, targetSize, null);
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,8 +4,8 @@
 | 
				
			|||||||
  import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
 | 
					  import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { getAssetPlaybackUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
 | 
					  import { getAssetPlaybackUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
 | 
				
			||||||
  import { timeToSeconds } from '$lib/utils/date-time';
 | 
					  import { timeToSeconds } from '$lib/utils/date-time';
 | 
				
			||||||
  import { getAltText } from '$lib/utils/thumbnail-util';
 | 
					  // import { getAltText } from '$lib/utils/thumbnail-util';
 | 
				
			||||||
  import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
 | 
					  import { AssetMediaSize } from '@immich/sdk';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
    mdiArchiveArrowDownOutline,
 | 
					    mdiArchiveArrowDownOutline,
 | 
				
			||||||
    mdiCameraBurst,
 | 
					    mdiCameraBurst,
 | 
				
			||||||
@ -17,22 +17,23 @@
 | 
				
			|||||||
  } from '@mdi/js';
 | 
					  } from '@mdi/js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  import { thumbhash } from '$lib/actions/thumbhash';
 | 
					  import { thumbhash } from '$lib/actions/thumbhash';
 | 
				
			||||||
 | 
					  import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { mobileDevice } from '$lib/stores/mobile-device.svelte';
 | 
					  import { mobileDevice } from '$lib/stores/mobile-device.svelte';
 | 
				
			||||||
 | 
					  import { getFocusable } from '$lib/utils/focus-util';
 | 
				
			||||||
  import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
 | 
					  import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
 | 
				
			||||||
  import { TUNABLES } from '$lib/utils/tunables';
 | 
					  import { TUNABLES } from '$lib/utils/tunables';
 | 
				
			||||||
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
  import type { ClassValue } from 'svelte/elements';
 | 
					  import type { ClassValue } from 'svelte/elements';
 | 
				
			||||||
  import { fade } from 'svelte/transition';
 | 
					  import { fade } from 'svelte/transition';
 | 
				
			||||||
  import ImageThumbnail from './image-thumbnail.svelte';
 | 
					  import ImageThumbnail from './image-thumbnail.svelte';
 | 
				
			||||||
  import VideoThumbnail from './video-thumbnail.svelte';
 | 
					  import VideoThumbnail from './video-thumbnail.svelte';
 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
  import { getFocusable } from '$lib/utils/focus-util';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    asset: AssetResponseDto;
 | 
					    asset: TimelineAsset;
 | 
				
			||||||
    groupIndex?: number;
 | 
					    groupIndex?: number;
 | 
				
			||||||
    thumbnailSize?: number | undefined;
 | 
					    thumbnailSize?: number;
 | 
				
			||||||
    thumbnailWidth?: number | undefined;
 | 
					    thumbnailWidth?: number;
 | 
				
			||||||
    thumbnailHeight?: number | undefined;
 | 
					    thumbnailHeight?: number;
 | 
				
			||||||
    selected?: boolean;
 | 
					    selected?: boolean;
 | 
				
			||||||
    focussed?: boolean;
 | 
					    focussed?: boolean;
 | 
				
			||||||
    selectionCandidate?: boolean;
 | 
					    selectionCandidate?: boolean;
 | 
				
			||||||
@ -44,10 +45,10 @@
 | 
				
			|||||||
    imageClass?: ClassValue;
 | 
					    imageClass?: ClassValue;
 | 
				
			||||||
    brokenAssetClass?: ClassValue;
 | 
					    brokenAssetClass?: ClassValue;
 | 
				
			||||||
    dimmed?: boolean;
 | 
					    dimmed?: boolean;
 | 
				
			||||||
    onClick?: ((asset: AssetResponseDto) => void) | undefined;
 | 
					    onClick?: (asset: TimelineAsset) => void;
 | 
				
			||||||
    onSelect?: ((asset: AssetResponseDto) => void) | undefined;
 | 
					    onSelect?: (asset: TimelineAsset) => void;
 | 
				
			||||||
    onMouseEvent?: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined;
 | 
					    onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
 | 
				
			||||||
    handleFocus?: (() => void) | undefined;
 | 
					    handleFocus?: () => void;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let {
 | 
					  let {
 | 
				
			||||||
@ -331,7 +332,7 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        {/if}
 | 
					        {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
 | 
					        {#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR}
 | 
				
			||||||
          <div class="absolute right-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
 | 
					          <div class="absolute right-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
 | 
				
			||||||
            <span class="pr-2 pt-2">
 | 
					            <span class="pr-2 pt-2">
 | 
				
			||||||
              <Icon path={mdiRotate360} size="24" />
 | 
					              <Icon path={mdiRotate360} size="24" />
 | 
				
			||||||
@ -344,7 +345,7 @@
 | 
				
			|||||||
          <div
 | 
					          <div
 | 
				
			||||||
            class={[
 | 
					            class={[
 | 
				
			||||||
              'absolute z-10 flex place-items-center gap-1 text-xs font-medium text-white',
 | 
					              'absolute z-10 flex place-items-center gap-1 text-xs font-medium text-white',
 | 
				
			||||||
              asset.type == AssetTypeEnum.Image && !asset.livePhotoVideoId ? 'top-0 right-0' : 'top-7 right-1',
 | 
					              asset.isImage && !asset.livePhotoVideoId ? 'top-0 right-0' : 'top-7 right-1',
 | 
				
			||||||
            ]}
 | 
					            ]}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <span class="pr-2 pt-2 flex place-items-center gap-1">
 | 
					            <span class="pr-2 pt-2 flex place-items-center gap-1">
 | 
				
			||||||
@ -354,27 +355,28 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        {/if}
 | 
					        {/if}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <!-- altText={$getAltText(asset)} -->
 | 
				
			||||||
      <ImageThumbnail
 | 
					      <ImageThumbnail
 | 
				
			||||||
        class={imageClass}
 | 
					        class={imageClass}
 | 
				
			||||||
        {brokenAssetClass}
 | 
					        {brokenAssetClass}
 | 
				
			||||||
        url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
 | 
					        url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
 | 
				
			||||||
        altText={$getAltText(asset)}
 | 
					        altText="todo"
 | 
				
			||||||
        widthStyle="{width}px"
 | 
					        widthStyle="{width}px"
 | 
				
			||||||
        heightStyle="{height}px"
 | 
					        heightStyle="{height}px"
 | 
				
			||||||
        curve={selected}
 | 
					        curve={selected}
 | 
				
			||||||
        onComplete={(errored) => ((loaded = true), (thumbError = errored))}
 | 
					        onComplete={(errored) => ((loaded = true), (thumbError = errored))}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      {#if asset.type === AssetTypeEnum.Video}
 | 
					      {#if asset.isVideo}
 | 
				
			||||||
        <div class="absolute top-0 h-full w-full">
 | 
					        <div class="absolute top-0 h-full w-full">
 | 
				
			||||||
          <VideoThumbnail
 | 
					          <VideoThumbnail
 | 
				
			||||||
            url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
 | 
					            url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
 | 
				
			||||||
            enablePlayback={mouseOver && $playVideoThumbnailOnHover}
 | 
					            enablePlayback={mouseOver && $playVideoThumbnailOnHover}
 | 
				
			||||||
            curve={selected}
 | 
					            curve={selected}
 | 
				
			||||||
            durationInSeconds={timeToSeconds(asset.duration)}
 | 
					            durationInSeconds={timeToSeconds(asset.duration!)}
 | 
				
			||||||
            playbackOnIconHover={!$playVideoThumbnailOnHover}
 | 
					            playbackOnIconHover={!$playVideoThumbnailOnHover}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      {:else if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId}
 | 
					      {:else if asset.isImage && asset.livePhotoVideoId}
 | 
				
			||||||
        <div class="absolute top-0 h-full w-full">
 | 
					        <div class="absolute top-0 h-full w-full">
 | 
				
			||||||
          <VideoThumbnail
 | 
					          <VideoThumbnail
 | 
				
			||||||
            url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}
 | 
					            url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}
 | 
				
			||||||
 | 
				
			|||||||
@ -73,7 +73,7 @@
 | 
				
			|||||||
  const viewport: Viewport = $state({ width: 0, height: 0 });
 | 
					  const viewport: Viewport = $state({ width: 0, height: 0 });
 | 
				
			||||||
  // need to include padding in the viewport for gallery
 | 
					  // need to include padding in the viewport for gallery
 | 
				
			||||||
  const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
 | 
					  const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<AssetResponseDto>();
 | 
				
			||||||
  let progressBarController: Tween<number> | undefined = $state(undefined);
 | 
					  let progressBarController: Tween<number> | undefined = $state(undefined);
 | 
				
			||||||
  let videoPlayer: HTMLVideoElement | undefined = $state();
 | 
					  let videoPlayer: HTMLVideoElement | undefined = $state();
 | 
				
			||||||
  const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`;
 | 
					  const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,11 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
  import type { OnArchive } from '$lib/utils/actions';
 | 
					  import type { OnArchive } from '$lib/utils/actions';
 | 
				
			||||||
 | 
					  import { archiveAssets } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
 | 
					  import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
					  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
					  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
				
			||||||
  import { archiveAssets } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    onArchive?: OnArchive;
 | 
					    onArchive?: OnArchive;
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,10 @@
 | 
				
			|||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
  import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils';
 | 
					  import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { AssetJobName, AssetTypeEnum, runAssetJobs } from '@immich/sdk';
 | 
					  import { isTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
					  import { AssetJobName, AssetTypeEnum, runAssetJobs, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    jobs?: AssetJobName[];
 | 
					    jobs?: AssetJobName[];
 | 
				
			||||||
@ -19,7 +20,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const { clearSelect, getOwnedAssets } = getAssetControlContext();
 | 
					  const { clearSelect, getOwnedAssets } = getAssetControlContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video));
 | 
					  let isAllVideos = $derived(
 | 
				
			||||||
 | 
					    [...getOwnedAssets()].every((asset) =>
 | 
				
			||||||
 | 
					      isTimelineAsset(asset) ? asset.isVideo : (asset as AssetResponseDto).type === AssetTypeEnum.Video,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleRunJob = async (name: AssetJobName) => {
 | 
					  const handleRunJob = async (name: AssetJobName) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,11 +4,11 @@
 | 
				
			|||||||
  import { getSelectedAssets } from '$lib/utils/asset-utils';
 | 
					  import { getSelectedAssets } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { updateAssets } from '@immich/sdk';
 | 
					  import { updateAssets } from '@immich/sdk';
 | 
				
			||||||
 | 
					  import { mdiCalendarEditOutline } from '@mdi/js';
 | 
				
			||||||
  import { DateTime } from 'luxon';
 | 
					  import { DateTime } from 'luxon';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
					  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
					  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
				
			||||||
  import { mdiCalendarEditOutline } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    menuItem?: boolean;
 | 
					    menuItem?: boolean;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,14 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
					 | 
				
			||||||
  import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
					 | 
				
			||||||
  import { shortcut } from '$lib/actions/shortcut';
 | 
					  import { shortcut } from '$lib/actions/shortcut';
 | 
				
			||||||
  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import { getKey } from '$lib/utils';
 | 
				
			||||||
 | 
					  import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
 | 
				
			||||||
 | 
					  import { isTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
 | 
					  import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
 | 
					  import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
 | 
					  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    filename?: string;
 | 
					    filename?: string;
 | 
				
			||||||
@ -20,7 +23,11 @@
 | 
				
			|||||||
    const assets = [...getAssets()];
 | 
					    const assets = [...getAssets()];
 | 
				
			||||||
    if (assets.length === 1) {
 | 
					    if (assets.length === 1) {
 | 
				
			||||||
      clearSelect();
 | 
					      clearSelect();
 | 
				
			||||||
      await downloadFile(assets[0]);
 | 
					      let asset: AssetResponseDto = assets[0] as AssetResponseDto;
 | 
				
			||||||
 | 
					      if (isTimelineAsset(assets[0])) {
 | 
				
			||||||
 | 
					        asset = await getAssetInfo({ id: assets[0].id, key: getKey() });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await downloadFile(asset);
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,14 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					  import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import type { OnLink, OnUnlink } from '$lib/utils/actions';
 | 
					  import type { OnLink, OnUnlink } from '$lib/utils/actions';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { AssetTypeEnum, getAssetInfo, updateAsset } from '@immich/sdk';
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
 | 
					  import { getAssetInfo, updateAsset } from '@immich/sdk';
 | 
				
			||||||
  import { mdiLinkOff, mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
 | 
					  import { mdiLinkOff, mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
					  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    onLink: OnLink;
 | 
					    onLink: OnLink;
 | 
				
			||||||
@ -28,14 +30,14 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const handleLink = async () => {
 | 
					  const handleLink = async () => {
 | 
				
			||||||
    let [still, motion] = [...getOwnedAssets()];
 | 
					    let [still, motion] = [...getOwnedAssets()];
 | 
				
			||||||
    if (still.type === AssetTypeEnum.Video) {
 | 
					    if ((still as TimelineAsset).isVideo) {
 | 
				
			||||||
      [still, motion] = [motion, still];
 | 
					      [still, motion] = [motion, still];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      loading = true;
 | 
					      loading = true;
 | 
				
			||||||
      const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: motion.id } });
 | 
					      const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: motion.id } });
 | 
				
			||||||
      onLink({ still: stillResponse, motion });
 | 
					      onLink({ still: toTimelineAsset(stillResponse), motion: motion as TimelineAsset });
 | 
				
			||||||
      clearSelect();
 | 
					      clearSelect();
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      handleError(error, $t('errors.unable_to_link_motion_video'));
 | 
					      handleError(error, $t('errors.unable_to_link_motion_video'));
 | 
				
			||||||
@ -46,23 +48,23 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const handleUnlink = async () => {
 | 
					  const handleUnlink = async () => {
 | 
				
			||||||
    const [still] = [...getOwnedAssets()];
 | 
					    const [still] = [...getOwnedAssets()];
 | 
				
			||||||
 | 
					    if (still) {
 | 
				
			||||||
    const motionId = still?.livePhotoVideoId;
 | 
					      const motionId = (still as TimelineAsset).livePhotoVideoId;
 | 
				
			||||||
      if (!motionId) {
 | 
					      if (!motionId) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        loading = true;
 | 
					        loading = true;
 | 
				
			||||||
        const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: null } });
 | 
					        const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: null } });
 | 
				
			||||||
        const motionResponse = await getAssetInfo({ id: motionId });
 | 
					        const motionResponse = await getAssetInfo({ id: motionId });
 | 
				
			||||||
      onUnlink({ still: stillResponse, motion: motionResponse });
 | 
					        onUnlink({ still: toTimelineAsset(stillResponse), motion: toTimelineAsset(motionResponse) });
 | 
				
			||||||
        clearSelect();
 | 
					        clearSelect();
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        handleError(error, $t('errors.unable_to_unlink_motion_video'));
 | 
					        handleError(error, $t('errors.unable_to_unlink_motion_video'));
 | 
				
			||||||
      } finally {
 | 
					      } finally {
 | 
				
			||||||
        loading = false;
 | 
					        loading = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,14 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import type { AssetInteraction, BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
 | 
					  import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					  import { cancelMultiselect, selectAllAssets } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
 | 
					  import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
 | 
				
			||||||
  import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    assetStore: AssetStore;
 | 
					    assetStore: AssetStore;
 | 
				
			||||||
    assetInteraction: AssetInteraction;
 | 
					    assetInteraction: AssetInteraction<BaseInteractionAsset>;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let { assetStore, assetInteraction }: Props = $props();
 | 
					  let { assetStore, assetInteraction }: Props = $props();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 | 
					 | 
				
			||||||
  import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
					  import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
  import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
 | 
					  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
  import { stackAssets, deleteStack } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
  import type { OnStack, OnUnstack } from '$lib/utils/actions';
 | 
					  import type { OnStack, OnUnstack } from '$lib/utils/actions';
 | 
				
			||||||
 | 
					  import { deleteStack, stackAssets } from '$lib/utils/asset-utils';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
 | 
					  import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
@ -34,7 +35,7 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    const unstackedAssets = await deleteStack([stack.id]);
 | 
					    const unstackedAssets = await deleteStack([stack.id]);
 | 
				
			||||||
    if (unstackedAssets) {
 | 
					    if (unstackedAssets) {
 | 
				
			||||||
      onUnstack?.(unstackedAssets);
 | 
					      onUnstack?.(unstackedAssets.map((a) => toTimelineAsset(a)));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    clearSelect();
 | 
					    clearSelect();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,20 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
    type AssetStore,
 | 
					 | 
				
			||||||
    type AssetBucket,
 | 
					    type AssetBucket,
 | 
				
			||||||
    assetSnapshot,
 | 
					    assetSnapshot,
 | 
				
			||||||
    assetsSnapshot,
 | 
					    assetsSnapshot,
 | 
				
			||||||
 | 
					    type AssetStore,
 | 
				
			||||||
    isSelectingAllAssets,
 | 
					    isSelectingAllAssets,
 | 
				
			||||||
 | 
					    type TimelineAsset,
 | 
				
			||||||
  } from '$lib/stores/assets-store.svelte';
 | 
					  } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { navigate } from '$lib/utils/navigation';
 | 
					  import { navigate } from '$lib/utils/navigation';
 | 
				
			||||||
  import { getDateLocaleString } from '$lib/utils/timeline-util';
 | 
					  import { getDateLocaleString } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import type { AssetResponseDto } from '@immich/sdk';
 | 
					
 | 
				
			||||||
 | 
					  import type { AssetInteraction, BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
 | 
					  import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
 | 
				
			||||||
  import { fly } from 'svelte/transition';
 | 
					  import { fly, scale } from 'svelte/transition';
 | 
				
			||||||
  import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
 | 
					  import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
 | 
				
			||||||
  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					 | 
				
			||||||
  import { scale } from 'svelte/transition';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  import { flip } from 'svelte/animate';
 | 
					  import { flip } from 'svelte/animate';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,11 +29,11 @@
 | 
				
			|||||||
    showArchiveIcon: boolean;
 | 
					    showArchiveIcon: boolean;
 | 
				
			||||||
    bucket: AssetBucket;
 | 
					    bucket: AssetBucket;
 | 
				
			||||||
    assetStore: AssetStore;
 | 
					    assetStore: AssetStore;
 | 
				
			||||||
    assetInteraction: AssetInteraction;
 | 
					    assetInteraction: AssetInteraction<BaseInteractionAsset>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onSelect: ({ title, assets }: { title: string; assets: AssetResponseDto[] }) => void;
 | 
					    onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
 | 
				
			||||||
    onSelectAssets: (asset: AssetResponseDto) => void;
 | 
					    onSelectAssets: (asset: TimelineAsset) => void;
 | 
				
			||||||
    onSelectAssetCandidates: (asset: AssetResponseDto | null) => void;
 | 
					    onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let {
 | 
					  let {
 | 
				
			||||||
@ -54,7 +54,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const transitionDuration = $derived.by(() => (bucket.store.suspendTransitions && !$isUploading ? 0 : 150));
 | 
					  const transitionDuration = $derived.by(() => (bucket.store.suspendTransitions && !$isUploading ? 0 : 150));
 | 
				
			||||||
  const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
 | 
					  const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
 | 
				
			||||||
  const onClick = (assetStore: AssetStore, assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => {
 | 
					  const onClick = (assetStore: AssetStore, assets: TimelineAsset[], groupTitle: string, asset: TimelineAsset) => {
 | 
				
			||||||
    if (isSelectionMode || assetInteraction.selectionActive) {
 | 
					    if (isSelectionMode || assetInteraction.selectionActive) {
 | 
				
			||||||
      assetSelectHandler(assetStore, asset, assets, groupTitle);
 | 
					      assetSelectHandler(assetStore, asset, assets, groupTitle);
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
@ -62,12 +62,12 @@
 | 
				
			|||||||
    void navigate({ targetRoute: 'current', assetId: asset.id });
 | 
					    void navigate({ targetRoute: 'current', assetId: asset.id });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => onSelect({ title, assets });
 | 
					  const handleSelectGroup = (title: string, assets: TimelineAsset[]) => onSelect({ title, assets });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetSelectHandler = (
 | 
					  const assetSelectHandler = (
 | 
				
			||||||
    assetStore: AssetStore,
 | 
					    assetStore: AssetStore,
 | 
				
			||||||
    asset: AssetResponseDto,
 | 
					    asset: TimelineAsset,
 | 
				
			||||||
    assetsInDateGroup: AssetResponseDto[],
 | 
					    assetsInDateGroup: TimelineAsset[],
 | 
				
			||||||
    groupTitle: string,
 | 
					    groupTitle: string,
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
    onSelectAssets(asset);
 | 
					    onSelectAssets(asset);
 | 
				
			||||||
@ -91,7 +91,7 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetMouseEventHandler = (groupTitle: string, asset: AssetResponseDto | null) => {
 | 
					  const assetMouseEventHandler = (groupTitle: string, asset: TimelineAsset | null) => {
 | 
				
			||||||
    // Show multi select icon on hover on date group
 | 
					    // Show multi select icon on hover on date group
 | 
				
			||||||
    hoveredDateGroup = groupTitle;
 | 
					    hoveredDateGroup = groupTitle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -100,7 +100,7 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetOnFocusHandler = (asset: AssetResponseDto) => {
 | 
					  const assetOnFocusHandler = (asset: TimelineAsset) => {
 | 
				
			||||||
    assetInteraction.focussedAssetId = asset.id;
 | 
					    assetInteraction.focussedAssetId = asset.id;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  function filterIntersecting<R extends { intersecting: boolean }>(intersectable: R[]) {
 | 
					  function filterIntersecting<R extends { intersecting: boolean }>(intersectable: R[]) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,21 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
 | 
					  import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
 | 
				
			||||||
 | 
					  import { page } from '$app/stores';
 | 
				
			||||||
 | 
					  import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
 | 
				
			||||||
  import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
 | 
					  import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
 | 
				
			||||||
  import type { Action } from '$lib/components/asset-viewer/actions/action';
 | 
					  import type { Action } from '$lib/components/asset-viewer/actions/action';
 | 
				
			||||||
 | 
					  import Skeleton from '$lib/components/photos-page/skeleton.svelte';
 | 
				
			||||||
  import { AppRoute, AssetAction } from '$lib/constants';
 | 
					  import { AppRoute, AssetAction } from '$lib/constants';
 | 
				
			||||||
 | 
					  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
					  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
				
			||||||
  import { AssetBucket, assetsSnapshot, AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
 | 
					  import {
 | 
				
			||||||
 | 
					    AssetBucket,
 | 
				
			||||||
 | 
					    assetsSnapshot,
 | 
				
			||||||
 | 
					    AssetStore,
 | 
				
			||||||
 | 
					    isSelectingAllAssets,
 | 
				
			||||||
 | 
					    type TimelineAsset,
 | 
				
			||||||
 | 
					  } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					  import { mobileDevice } from '$lib/stores/mobile-device.svelte';
 | 
				
			||||||
  import { showDeleteModal } from '$lib/stores/preferences.store';
 | 
					  import { showDeleteModal } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { searchStore } from '$lib/stores/search.svelte';
 | 
					  import { searchStore } from '$lib/stores/search.svelte';
 | 
				
			||||||
  import { featureFlags } from '$lib/stores/server-config.store';
 | 
					  import { featureFlags } from '$lib/stores/server-config.store';
 | 
				
			||||||
@ -13,19 +24,14 @@
 | 
				
			|||||||
  import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
 | 
					  import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { navigate } from '$lib/utils/navigation';
 | 
					  import { navigate } from '$lib/utils/navigation';
 | 
				
			||||||
  import { type ScrubberListener } from '$lib/utils/timeline-util';
 | 
					  import { type ScrubberListener } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto } from '@immich/sdk';
 | 
					  import { getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { onMount, type Snippet } from 'svelte';
 | 
					  import { onMount, type Snippet } from 'svelte';
 | 
				
			||||||
 | 
					  import type { UpdatePayload } from 'vite';
 | 
				
			||||||
  import Portal from '../shared-components/portal/portal.svelte';
 | 
					  import Portal from '../shared-components/portal/portal.svelte';
 | 
				
			||||||
  import Scrubber from '../shared-components/scrubber/scrubber.svelte';
 | 
					  import Scrubber from '../shared-components/scrubber/scrubber.svelte';
 | 
				
			||||||
  import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
 | 
					  import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
 | 
				
			||||||
  import AssetDateGroup from './asset-date-group.svelte';
 | 
					  import AssetDateGroup from './asset-date-group.svelte';
 | 
				
			||||||
  import DeleteAssetDialog from './delete-asset-dialog.svelte';
 | 
					  import DeleteAssetDialog from './delete-asset-dialog.svelte';
 | 
				
			||||||
  import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
 | 
					 | 
				
			||||||
  import Skeleton from '$lib/components/photos-page/skeleton.svelte';
 | 
					 | 
				
			||||||
  import { page } from '$app/stores';
 | 
					 | 
				
			||||||
  import type { UpdatePayload } from 'vite';
 | 
					 | 
				
			||||||
  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					 | 
				
			||||||
  import { mobileDevice } from '$lib/stores/mobile-device.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    isSelectionMode?: boolean;
 | 
					    isSelectionMode?: boolean;
 | 
				
			||||||
@ -35,7 +41,7 @@
 | 
				
			|||||||
     additionally, update the page location/url with the asset as the asset-grid is scrolled */
 | 
					     additionally, update the page location/url with the asset as the asset-grid is scrolled */
 | 
				
			||||||
    enableRouting: boolean;
 | 
					    enableRouting: boolean;
 | 
				
			||||||
    assetStore: AssetStore;
 | 
					    assetStore: AssetStore;
 | 
				
			||||||
    assetInteraction: AssetInteraction;
 | 
					    assetInteraction: AssetInteraction<TimelineAsset>;
 | 
				
			||||||
    removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null;
 | 
					    removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null;
 | 
				
			||||||
    withStacked?: boolean;
 | 
					    withStacked?: boolean;
 | 
				
			||||||
    showArchiveIcon?: boolean;
 | 
					    showArchiveIcon?: boolean;
 | 
				
			||||||
@ -43,7 +49,7 @@
 | 
				
			|||||||
    album?: AlbumResponseDto | null;
 | 
					    album?: AlbumResponseDto | null;
 | 
				
			||||||
    person?: PersonResponseDto | null;
 | 
					    person?: PersonResponseDto | null;
 | 
				
			||||||
    isShowDeleteConfirmation?: boolean;
 | 
					    isShowDeleteConfirmation?: boolean;
 | 
				
			||||||
    onSelect?: (asset: AssetResponseDto) => void;
 | 
					    onSelect?: (asset: TimelineAsset) => void;
 | 
				
			||||||
    onEscape?: () => void;
 | 
					    onEscape?: () => void;
 | 
				
			||||||
    children?: Snippet;
 | 
					    children?: Snippet;
 | 
				
			||||||
    empty?: Snippet;
 | 
					    empty?: Snippet;
 | 
				
			||||||
@ -352,7 +358,7 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSelectAsset = (asset: AssetResponseDto) => {
 | 
					  const handleSelectAsset = (asset: TimelineAsset) => {
 | 
				
			||||||
    if (!assetStore.albumAssets.has(asset.id)) {
 | 
					    if (!assetStore.albumAssets.has(asset.id)) {
 | 
				
			||||||
      assetInteraction.selectAsset(asset);
 | 
					      assetInteraction.selectAsset(asset);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -363,7 +369,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (previousAsset) {
 | 
					    if (previousAsset) {
 | 
				
			||||||
      const preloadAsset = await assetStore.getPreviousAsset(previousAsset);
 | 
					      const preloadAsset = await assetStore.getPreviousAsset(previousAsset);
 | 
				
			||||||
      assetViewingStore.setAsset(previousAsset, preloadAsset ? [preloadAsset] : []);
 | 
					      const asset = await getAssetInfo({ id: previousAsset.id });
 | 
				
			||||||
 | 
					      assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
 | 
				
			||||||
      await navigate({ targetRoute: 'current', assetId: previousAsset.id });
 | 
					      await navigate({ targetRoute: 'current', assetId: previousAsset.id });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -375,7 +382,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (nextAsset) {
 | 
					    if (nextAsset) {
 | 
				
			||||||
      const preloadAsset = await assetStore.getNextAsset(nextAsset);
 | 
					      const preloadAsset = await assetStore.getNextAsset(nextAsset);
 | 
				
			||||||
      assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []);
 | 
					      const asset = await getAssetInfo({ id: nextAsset.id });
 | 
				
			||||||
 | 
					      assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
 | 
				
			||||||
      await navigate({ targetRoute: 'current', assetId: nextAsset.id });
 | 
					      await navigate({ targetRoute: 'current', assetId: nextAsset.id });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -387,14 +395,14 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (randomAsset) {
 | 
					    if (randomAsset) {
 | 
				
			||||||
      const preloadAsset = await assetStore.getNextAsset(randomAsset);
 | 
					      const preloadAsset = await assetStore.getNextAsset(randomAsset);
 | 
				
			||||||
      assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []);
 | 
					      const asset = await getAssetInfo({ id: randomAsset.id });
 | 
				
			||||||
 | 
					      assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
 | 
				
			||||||
      await navigate({ targetRoute: 'current', assetId: randomAsset.id });
 | 
					      await navigate({ targetRoute: 'current', assetId: randomAsset.id });
 | 
				
			||||||
 | 
					      return asset;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return randomAsset;
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClose = async ({ asset }: { asset: AssetResponseDto }) => {
 | 
					  const handleClose = async (asset: { id: string }) => {
 | 
				
			||||||
    assetViewingStore.showAssetViewer(false);
 | 
					    assetViewingStore.showAssetViewer(false);
 | 
				
			||||||
    showSkeleton = true;
 | 
					    showSkeleton = true;
 | 
				
			||||||
    $gridScrollTarget = { at: asset.id };
 | 
					    $gridScrollTarget = { at: asset.id };
 | 
				
			||||||
@ -410,7 +418,7 @@
 | 
				
			|||||||
      case AssetAction.ARCHIVE: {
 | 
					      case AssetAction.ARCHIVE: {
 | 
				
			||||||
        // find the next asset to show or close the viewer
 | 
					        // find the next asset to show or close the viewer
 | 
				
			||||||
        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
 | 
				
			||||||
        (await handleNext()) || (await handlePrevious()) || (await handleClose({ asset: action.asset }));
 | 
					        (await handleNext()) || (await handlePrevious()) || (await handleClose(action.asset));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // delete after find the next one
 | 
					        // delete after find the next one
 | 
				
			||||||
        assetStore.removeAssets([action.asset.id]);
 | 
					        assetStore.removeAssets([action.asset.id]);
 | 
				
			||||||
@ -439,7 +447,7 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
 | 
					  let lastAssetMouseEvent: TimelineAsset | null = $state(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let shiftKeyIsDown = $state(false);
 | 
					  let shiftKeyIsDown = $state(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -469,14 +477,14 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
 | 
					  const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
 | 
				
			||||||
    if (asset) {
 | 
					    if (asset) {
 | 
				
			||||||
      selectAssetCandidates(asset);
 | 
					      selectAssetCandidates(asset);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    lastAssetMouseEvent = asset;
 | 
					    lastAssetMouseEvent = asset;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleGroupSelect = (assetStore: AssetStore, group: string, assets: AssetResponseDto[]) => {
 | 
					  const handleGroupSelect = (assetStore: AssetStore, group: string, assets: TimelineAsset[]) => {
 | 
				
			||||||
    if (assetInteraction.selectedGroup.has(group)) {
 | 
					    if (assetInteraction.selectedGroup.has(group)) {
 | 
				
			||||||
      assetInteraction.removeGroupFromMultiselectGroup(group);
 | 
					      assetInteraction.removeGroupFromMultiselectGroup(group);
 | 
				
			||||||
      for (const asset of assets) {
 | 
					      for (const asset of assets) {
 | 
				
			||||||
@ -496,7 +504,7 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSelectAssets = async (asset: AssetResponseDto) => {
 | 
					  const handleSelectAssets = async (asset: TimelineAsset) => {
 | 
				
			||||||
    if (!asset) {
 | 
					    if (!asset) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -579,7 +587,7 @@
 | 
				
			|||||||
    assetInteraction.setAssetSelectionStart(deselect ? null : asset);
 | 
					    assetInteraction.setAssetSelectionStart(deselect ? null : asset);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const selectAssetCandidates = (endAsset: AssetResponseDto) => {
 | 
					  const selectAssetCandidates = (endAsset: TimelineAsset) => {
 | 
				
			||||||
    if (!shiftKeyIsDown) {
 | 
					    if (!shiftKeyIsDown) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -4,8 +4,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  export interface AssetControlContext {
 | 
					  export interface AssetControlContext {
 | 
				
			||||||
    // Wrap assets in a function, because context isn't reactive.
 | 
					    // Wrap assets in a function, because context isn't reactive.
 | 
				
			||||||
    getAssets: () => AssetResponseDto[]; // All assets includes partners' assets
 | 
					    getAssets: () => BaseInteractionAsset[]; // All assets includes partners' assets
 | 
				
			||||||
    getOwnedAssets: () => AssetResponseDto[]; // Only assets owned by the user
 | 
					    getOwnedAssets: () => BaseInteractionAsset[]; // Only assets owned by the user
 | 
				
			||||||
    clearSelect: () => void;
 | 
					    clearSelect: () => void;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,13 +14,13 @@
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import type { AssetResponseDto } from '@immich/sdk';
 | 
					  import type { BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { mdiClose } from '@mdi/js';
 | 
					  import { mdiClose } from '@mdi/js';
 | 
				
			||||||
  import ControlAppBar from '../shared-components/control-app-bar.svelte';
 | 
					 | 
				
			||||||
  import type { Snippet } from 'svelte';
 | 
					  import type { Snippet } from 'svelte';
 | 
				
			||||||
 | 
					  import ControlAppBar from '../shared-components/control-app-bar.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    assets: AssetResponseDto[];
 | 
					    assets: BaseInteractionAsset[];
 | 
				
			||||||
    clearSelect: () => void;
 | 
					    clearSelect: () => void;
 | 
				
			||||||
    ownerId?: string | undefined;
 | 
					    ownerId?: string | undefined;
 | 
				
			||||||
    children?: Snippet;
 | 
					    children?: Snippet;
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@
 | 
				
			|||||||
  import { downloadArchive } from '$lib/utils/asset-utils';
 | 
					  import { downloadArchive } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
					  import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { addSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk';
 | 
					  import { addSharedLinkAssets, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
 | 
					  import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
 | 
				
			||||||
  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
					  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
  import DownloadAction from '../photos-page/actions/download-action.svelte';
 | 
					  import DownloadAction from '../photos-page/actions/download-action.svelte';
 | 
				
			||||||
@ -31,7 +31,7 @@
 | 
				
			|||||||
  let { sharedLink = $bindable(), isOwned }: Props = $props();
 | 
					  let { sharedLink = $bindable(), isOwned }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const viewport: Viewport = $state({ width: 0, height: 0 });
 | 
					  const viewport: Viewport = $state({ width: 0, height: 0 });
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<AssetResponseDto>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let assets = $derived(sharedLink.assets);
 | 
					  let assets = $derived(sharedLink.assets);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,31 +1,32 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { type ShortcutOptions, shortcuts } from '$lib/actions/shortcut';
 | 
					 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					  import { goto } from '$app/navigation';
 | 
				
			||||||
 | 
					  import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
 | 
				
			||||||
  import type { Action } from '$lib/components/asset-viewer/actions/action';
 | 
					  import type { Action } from '$lib/components/asset-viewer/actions/action';
 | 
				
			||||||
  import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
 | 
					  import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
 | 
				
			||||||
  import { AppRoute, AssetAction } from '$lib/constants';
 | 
					  import { AppRoute, AssetAction } from '$lib/constants';
 | 
				
			||||||
 | 
					  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
					  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
				
			||||||
  import type { Viewport } from '$lib/stores/assets-store.svelte';
 | 
					  import type { Viewport } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { showDeleteModal } from '$lib/stores/preferences.store';
 | 
					  import { showDeleteModal } from '$lib/stores/preferences.store';
 | 
				
			||||||
 | 
					  import { featureFlags } from '$lib/stores/server-config.store';
 | 
				
			||||||
 | 
					  import { handlePromiseError } from '$lib/utils';
 | 
				
			||||||
  import { deleteAssets } from '$lib/utils/actions';
 | 
					  import { deleteAssets } from '$lib/utils/actions';
 | 
				
			||||||
  import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
 | 
					  import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { featureFlags } from '$lib/stores/server-config.store';
 | 
					 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
 | 
				
			||||||
  import { navigate } from '$lib/utils/navigation';
 | 
					  import { navigate } from '$lib/utils/navigation';
 | 
				
			||||||
 | 
					  import { toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
  import { type AssetResponseDto } from '@immich/sdk';
 | 
					  import { type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
 | 
					  import { debounce } from 'lodash-es';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
 | 
					  import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
 | 
				
			||||||
  import ShowShortcuts from '../show-shortcuts.svelte';
 | 
					 | 
				
			||||||
  import Portal from '../portal/portal.svelte';
 | 
					 | 
				
			||||||
  import { handlePromiseError } from '$lib/utils';
 | 
					 | 
				
			||||||
  import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte';
 | 
					  import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte';
 | 
				
			||||||
  import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import Portal from '../portal/portal.svelte';
 | 
				
			||||||
  import { debounce } from 'lodash-es';
 | 
					  import ShowShortcuts from '../show-shortcuts.svelte';
 | 
				
			||||||
  import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    assets: AssetResponseDto[];
 | 
					    assets: AssetResponseDto[];
 | 
				
			||||||
    assetInteraction: AssetInteraction;
 | 
					    assetInteraction: AssetInteraction<AssetResponseDto>;
 | 
				
			||||||
    disableAssetSelect?: boolean;
 | 
					    disableAssetSelect?: boolean;
 | 
				
			||||||
    showArchiveIcon?: boolean;
 | 
					    showArchiveIcon?: boolean;
 | 
				
			||||||
    viewport: Viewport;
 | 
					    viewport: Viewport;
 | 
				
			||||||
@ -481,18 +482,18 @@
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          <Thumbnail
 | 
					          <Thumbnail
 | 
				
			||||||
            readonly={disableAssetSelect}
 | 
					            readonly={disableAssetSelect}
 | 
				
			||||||
            onClick={(asset) => {
 | 
					            onClick={() => {
 | 
				
			||||||
              if (assetInteraction.selectionActive) {
 | 
					              if (assetInteraction.selectionActive) {
 | 
				
			||||||
                handleSelectAssets(asset);
 | 
					                handleSelectAssets(asset);
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              void viewAssetHandler(asset);
 | 
					              void viewAssetHandler(asset);
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
            onSelect={(asset) => handleSelectAssets(asset)}
 | 
					            onSelect={() => handleSelectAssets(asset)}
 | 
				
			||||||
            onMouseEvent={() => assetMouseEventHandler(asset)}
 | 
					            onMouseEvent={() => assetMouseEventHandler(asset)}
 | 
				
			||||||
            handleFocus={() => assetOnFocusHandler(asset)}
 | 
					            handleFocus={() => assetOnFocusHandler(asset)}
 | 
				
			||||||
            {showArchiveIcon}
 | 
					            {showArchiveIcon}
 | 
				
			||||||
            {asset}
 | 
					            asset={toTimelineAsset(asset)}
 | 
				
			||||||
            selected={assetInteraction.hasSelectedAsset(asset.id)}
 | 
					            selected={assetInteraction.hasSelectedAsset(asset.id)}
 | 
				
			||||||
            selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
 | 
					            selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
 | 
				
			||||||
            focussed={assetInteraction.isFocussedAsset(asset.id)}
 | 
					            focussed={assetInteraction.isFocussedAsset(asset.id)}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					import { AssetInteraction, type BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
import { resetSavedUser, user } from '$lib/stores/user.store';
 | 
					import { resetSavedUser, user } from '$lib/stores/user.store';
 | 
				
			||||||
import { assetFactory } from '@test-data/factories/asset-factory';
 | 
					import { assetFactory } from '@test-data/factories/asset-factory';
 | 
				
			||||||
import { userAdminFactory } from '@test-data/factories/user-factory';
 | 
					import { userAdminFactory } from '@test-data/factories/user-factory';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('AssetInteraction', () => {
 | 
					describe('AssetInteraction', () => {
 | 
				
			||||||
  let assetInteraction: AssetInteraction;
 | 
					  let assetInteraction: AssetInteraction<BaseInteractionAsset>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    assetInteraction = new AssetInteraction();
 | 
					    assetInteraction = new AssetInteraction();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,27 @@
 | 
				
			|||||||
import { user } from '$lib/stores/user.store';
 | 
					import { user } from '$lib/stores/user.store';
 | 
				
			||||||
import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk';
 | 
					import type { AssetStackResponseDto, UserAdminResponseDto } from '@immich/sdk';
 | 
				
			||||||
import { SvelteSet } from 'svelte/reactivity';
 | 
					import { SvelteSet } from 'svelte/reactivity';
 | 
				
			||||||
import { fromStore } from 'svelte/store';
 | 
					import { fromStore } from 'svelte/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AssetInteraction {
 | 
					export type BaseInteractionAsset = {
 | 
				
			||||||
  selectedAssets = $state<AssetResponseDto[]>([]);
 | 
					  id: string;
 | 
				
			||||||
 | 
					  isTrashed: boolean;
 | 
				
			||||||
 | 
					  isArchived: boolean;
 | 
				
			||||||
 | 
					  isFavorite: boolean;
 | 
				
			||||||
 | 
					  ownerId: string;
 | 
				
			||||||
 | 
					  stack?: AssetStackResponseDto | null | undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export class AssetInteraction<T extends BaseInteractionAsset> {
 | 
				
			||||||
 | 
					  selectedAssets = $state<T[]>([]);
 | 
				
			||||||
  hasSelectedAsset(assetId: string) {
 | 
					  hasSelectedAsset(assetId: string) {
 | 
				
			||||||
    return this.selectedAssets.some((asset) => asset.id === assetId);
 | 
					    return this.selectedAssets.some((asset) => asset.id === assetId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  selectedGroup = new SvelteSet<string>();
 | 
					  selectedGroup = new SvelteSet<string>();
 | 
				
			||||||
  assetSelectionCandidates = $state<AssetResponseDto[]>([]);
 | 
					  assetSelectionCandidates = $state<T[]>([]);
 | 
				
			||||||
  hasSelectionCandidate(assetId: string) {
 | 
					  hasSelectionCandidate(assetId: string) {
 | 
				
			||||||
    return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
 | 
					    return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  assetSelectionStart = $state<AssetResponseDto | null>(null);
 | 
					  assetSelectionStart = $state<T | null>(null);
 | 
				
			||||||
  focussedAssetId = $state<string | null>(null);
 | 
					  focussedAssetId = $state<string | null>(null);
 | 
				
			||||||
  selectionActive = $derived(this.selectedAssets.length > 0);
 | 
					  selectionActive = $derived(this.selectedAssets.length > 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -25,13 +33,13 @@ export class AssetInteraction {
 | 
				
			|||||||
  isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
 | 
					  isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
 | 
				
			||||||
  isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
 | 
					  isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  selectAsset(asset: AssetResponseDto) {
 | 
					  selectAsset(asset: T) {
 | 
				
			||||||
    if (!this.hasSelectedAsset(asset.id)) {
 | 
					    if (!this.hasSelectedAsset(asset.id)) {
 | 
				
			||||||
      this.selectedAssets.push(asset);
 | 
					      this.selectedAssets.push(asset);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  selectAssets(assets: AssetResponseDto[]) {
 | 
					  selectAssets(assets: T[]) {
 | 
				
			||||||
    for (const asset of assets) {
 | 
					    for (const asset of assets) {
 | 
				
			||||||
      this.selectAsset(asset);
 | 
					      this.selectAsset(asset);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -52,11 +60,11 @@ export class AssetInteraction {
 | 
				
			|||||||
    this.selectedGroup.delete(group);
 | 
					    this.selectedGroup.delete(group);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setAssetSelectionStart(asset: AssetResponseDto | null) {
 | 
					  setAssetSelectionStart(asset: T | null) {
 | 
				
			||||||
    this.assetSelectionStart = asset;
 | 
					    this.assetSelectionStart = asset;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setAssetSelectionCandidates(assets: AssetResponseDto[]) {
 | 
					  setAssetSelectionCandidates(assets: T[]) {
 | 
				
			||||||
    this.assetSelectionCandidates = assets;
 | 
					    this.assetSelectionCandidates = assets;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,11 +5,11 @@ import { readonly, writable } from 'svelte/store';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function createAssetViewingStore() {
 | 
					function createAssetViewingStore() {
 | 
				
			||||||
  const viewingAssetStoreState = writable<AssetResponseDto>();
 | 
					  const viewingAssetStoreState = writable<AssetResponseDto>();
 | 
				
			||||||
  const preloadAssets = writable<AssetResponseDto[]>([]);
 | 
					  const preloadAssets = writable<{ id: string }[]>([]);
 | 
				
			||||||
  const viewState = writable<boolean>(false);
 | 
					  const viewState = writable<boolean>(false);
 | 
				
			||||||
  const gridScrollTarget = writable<AssetGridRouteSearchParams | null | undefined>();
 | 
					  const gridScrollTarget = writable<AssetGridRouteSearchParams | null | undefined>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setAsset = (asset: AssetResponseDto, assetsToPreload: AssetResponseDto[] = []) => {
 | 
					  const setAsset = (asset: AssetResponseDto, assetsToPreload: { id: string }[] = []) => {
 | 
				
			||||||
    preloadAssets.set(assetsToPreload);
 | 
					    preloadAssets.set(assetsToPreload);
 | 
				
			||||||
    viewingAssetStoreState.set(asset);
 | 
					    viewingAssetStoreState.set(asset);
 | 
				
			||||||
    viewState.set(true);
 | 
					    viewState.set(true);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
 | 
					import { sdkMock } from '$lib/__mocks__/sdk.mock';
 | 
				
			||||||
import { AbortError } from '$lib/utils';
 | 
					import { AbortError } from '$lib/utils';
 | 
				
			||||||
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
 | 
					import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
import { assetFactory } from '@test-data/factories/asset-factory';
 | 
					import { assetFactory, timelineAssetFactory } from '@test-data/factories/asset-factory';
 | 
				
			||||||
import { AssetStore } from './assets-store.svelte';
 | 
					import { AssetStore } from './assets-store.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('AssetStore', () => {
 | 
					describe('AssetStore', () => {
 | 
				
			||||||
@ -149,9 +149,8 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('adds assets to new bucket', () => {
 | 
					    it('adds assets to new bucket', () => {
 | 
				
			||||||
      const asset = assetFactory.build({
 | 
					      const asset = timelineAssetFactory.build({
 | 
				
			||||||
        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
					        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
				
			||||||
        fileCreatedAt: '2024-01-20T12:00:00.000Z',
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      assetStore.addAssets([asset]);
 | 
					      assetStore.addAssets([asset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -163,9 +162,8 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('adds assets to existing bucket', () => {
 | 
					    it('adds assets to existing bucket', () => {
 | 
				
			||||||
      const [assetOne, assetTwo] = assetFactory.buildList(2, {
 | 
					      const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, {
 | 
				
			||||||
        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
					        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
				
			||||||
        fileCreatedAt: '2024-01-20T12:00:00.000Z',
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      assetStore.addAssets([assetOne]);
 | 
					      assetStore.addAssets([assetOne]);
 | 
				
			||||||
      assetStore.addAssets([assetTwo]);
 | 
					      assetStore.addAssets([assetTwo]);
 | 
				
			||||||
@ -177,16 +175,13 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('orders assets in buckets by descending date', () => {
 | 
					    it('orders assets in buckets by descending date', () => {
 | 
				
			||||||
      const assetOne = assetFactory.build({
 | 
					      const assetOne = timelineAssetFactory.build({
 | 
				
			||||||
        fileCreatedAt: '2024-01-20T12:00:00.000Z',
 | 
					 | 
				
			||||||
        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
					        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const assetTwo = assetFactory.build({
 | 
					      const assetTwo = timelineAssetFactory.build({
 | 
				
			||||||
        fileCreatedAt: '2024-01-15T12:00:00.000Z',
 | 
					 | 
				
			||||||
        localDateTime: '2024-01-15T12:00:00.000Z',
 | 
					        localDateTime: '2024-01-15T12:00:00.000Z',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const assetThree = assetFactory.build({
 | 
					      const assetThree = timelineAssetFactory.build({
 | 
				
			||||||
        fileCreatedAt: '2024-01-16T12:00:00.000Z',
 | 
					 | 
				
			||||||
        localDateTime: '2024-01-16T12:00:00.000Z',
 | 
					        localDateTime: '2024-01-16T12:00:00.000Z',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      assetStore.addAssets([assetOne, assetTwo, assetThree]);
 | 
					      assetStore.addAssets([assetOne, assetTwo, assetThree]);
 | 
				
			||||||
@ -200,9 +195,9 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('orders buckets by descending date', () => {
 | 
					    it('orders buckets by descending date', () => {
 | 
				
			||||||
      const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
					      const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
				
			||||||
      const assetTwo = assetFactory.build({ localDateTime: '2024-04-20T12:00:00.000Z' });
 | 
					      const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-04-20T12:00:00.000Z' });
 | 
				
			||||||
      const assetThree = assetFactory.build({ localDateTime: '2023-01-20T12:00:00.000Z' });
 | 
					      const assetThree = timelineAssetFactory.build({ localDateTime: '2023-01-20T12:00:00.000Z' });
 | 
				
			||||||
      assetStore.addAssets([assetOne, assetTwo, assetThree]);
 | 
					      assetStore.addAssets([assetOne, assetTwo, assetThree]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetStore.buckets.length).toEqual(3);
 | 
					      expect(assetStore.buckets.length).toEqual(3);
 | 
				
			||||||
@ -213,7 +208,7 @@ describe('AssetStore', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('updates existing asset', () => {
 | 
					    it('updates existing asset', () => {
 | 
				
			||||||
      const updateAssetsSpy = vi.spyOn(assetStore, 'updateAssets');
 | 
					      const updateAssetsSpy = vi.spyOn(assetStore, 'updateAssets');
 | 
				
			||||||
      const asset = assetFactory.build();
 | 
					      const asset = timelineAssetFactory.build();
 | 
				
			||||||
      assetStore.addAssets([asset]);
 | 
					      assetStore.addAssets([asset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      assetStore.addAssets([asset]);
 | 
					      assetStore.addAssets([asset]);
 | 
				
			||||||
@ -223,8 +218,8 @@ describe('AssetStore', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // disabled due to the wasm Justified Layout import
 | 
					    // disabled due to the wasm Justified Layout import
 | 
				
			||||||
    it('ignores trashed assets when isTrashed is true', async () => {
 | 
					    it('ignores trashed assets when isTrashed is true', async () => {
 | 
				
			||||||
      const asset = assetFactory.build({ isTrashed: false });
 | 
					      const asset = timelineAssetFactory.build({ isTrashed: false });
 | 
				
			||||||
      const trashedAsset = assetFactory.build({ isTrashed: true });
 | 
					      const trashedAsset = timelineAssetFactory.build({ isTrashed: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const assetStore = new AssetStore();
 | 
					      const assetStore = new AssetStore();
 | 
				
			||||||
      await assetStore.updateOptions({ isTrashed: true });
 | 
					      await assetStore.updateOptions({ isTrashed: true });
 | 
				
			||||||
@ -244,14 +239,14 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('ignores non-existing assets', () => {
 | 
					    it('ignores non-existing assets', () => {
 | 
				
			||||||
      assetStore.updateAssets([assetFactory.build()]);
 | 
					      assetStore.updateAssets([timelineAssetFactory.build()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetStore.buckets.length).toEqual(0);
 | 
					      expect(assetStore.buckets.length).toEqual(0);
 | 
				
			||||||
      expect(assetStore.getAssets().length).toEqual(0);
 | 
					      expect(assetStore.getAssets().length).toEqual(0);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('updates an asset', () => {
 | 
					    it('updates an asset', () => {
 | 
				
			||||||
      const asset = assetFactory.build({ isFavorite: false });
 | 
					      const asset = timelineAssetFactory.build({ isFavorite: false });
 | 
				
			||||||
      const updatedAsset = { ...asset, isFavorite: true };
 | 
					      const updatedAsset = { ...asset, isFavorite: true };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      assetStore.addAssets([asset]);
 | 
					      assetStore.addAssets([asset]);
 | 
				
			||||||
@ -264,7 +259,7 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('asset moves buckets when asset date changes', () => {
 | 
					    it('asset moves buckets when asset date changes', () => {
 | 
				
			||||||
      const asset = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
					      const asset = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
				
			||||||
      const updatedAsset = { ...asset, localDateTime: '2024-03-20T12:00:00.000Z' };
 | 
					      const updatedAsset = { ...asset, localDateTime: '2024-03-20T12:00:00.000Z' };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      assetStore.addAssets([asset]);
 | 
					      assetStore.addAssets([asset]);
 | 
				
			||||||
@ -292,7 +287,7 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('ignores invalid IDs', () => {
 | 
					    it('ignores invalid IDs', () => {
 | 
				
			||||||
      assetStore.addAssets(assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }));
 | 
					      assetStore.addAssets(timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }));
 | 
				
			||||||
      assetStore.removeAssets(['', 'invalid', '4c7d9acc']);
 | 
					      assetStore.removeAssets(['', 'invalid', '4c7d9acc']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetStore.getAssets().length).toEqual(2);
 | 
					      expect(assetStore.getAssets().length).toEqual(2);
 | 
				
			||||||
@ -301,7 +296,7 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('removes asset from bucket', () => {
 | 
					    it('removes asset from bucket', () => {
 | 
				
			||||||
      const [assetOne, assetTwo] = assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
					      const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
				
			||||||
      assetStore.addAssets([assetOne, assetTwo]);
 | 
					      assetStore.addAssets([assetOne, assetTwo]);
 | 
				
			||||||
      assetStore.removeAssets([assetOne.id]);
 | 
					      assetStore.removeAssets([assetOne.id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -311,7 +306,7 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('does not remove bucket when empty', () => {
 | 
					    it('does not remove bucket when empty', () => {
 | 
				
			||||||
      const assets = assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
					      const assets = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
				
			||||||
      assetStore.addAssets(assets);
 | 
					      assetStore.addAssets(assets);
 | 
				
			||||||
      assetStore.removeAssets(assets.map((asset) => asset.id));
 | 
					      assetStore.removeAssets(assets.map((asset) => asset.id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -334,12 +329,10 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('populated store returns first asset', () => {
 | 
					    it('populated store returns first asset', () => {
 | 
				
			||||||
      const assetOne = assetFactory.build({
 | 
					      const assetOne = timelineAssetFactory.build({
 | 
				
			||||||
        fileCreatedAt: '2024-01-20T12:00:00.000Z',
 | 
					 | 
				
			||||||
        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
					        localDateTime: '2024-01-20T12:00:00.000Z',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const assetTwo = assetFactory.build({
 | 
					      const assetTwo = timelineAssetFactory.build({
 | 
				
			||||||
        fileCreatedAt: '2024-01-15T12:00:00.000Z',
 | 
					 | 
				
			||||||
        localDateTime: '2024-01-15T12:00:00.000Z',
 | 
					        localDateTime: '2024-01-15T12:00:00.000Z',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      assetStore.addAssets([assetOne, assetTwo]);
 | 
					      assetStore.addAssets([assetOne, assetTwo]);
 | 
				
			||||||
@ -445,8 +438,8 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('returns the bucket index', () => {
 | 
					    it('returns the bucket index', () => {
 | 
				
			||||||
      const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
					      const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
				
			||||||
      const assetTwo = assetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
 | 
					      const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
 | 
				
			||||||
      assetStore.addAssets([assetOne, assetTwo]);
 | 
					      assetStore.addAssets([assetOne, assetTwo]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.bucketDate).toEqual('2024-02-01T00:00:00.000Z');
 | 
					      expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.bucketDate).toEqual('2024-02-01T00:00:00.000Z');
 | 
				
			||||||
@ -454,8 +447,8 @@ describe('AssetStore', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('ignores removed buckets', () => {
 | 
					    it('ignores removed buckets', () => {
 | 
				
			||||||
      const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
					      const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
 | 
				
			||||||
      const assetTwo = assetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
 | 
					      const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
 | 
				
			||||||
      assetStore.addAssets([assetOne, assetTwo]);
 | 
					      assetStore.addAssets([assetOne, assetTwo]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      assetStore.removeAssets([assetTwo.id]);
 | 
					      assetStore.removeAssets([assetTwo.id]);
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import {
 | 
				
			|||||||
  type CommonLayoutOptions,
 | 
					  type CommonLayoutOptions,
 | 
				
			||||||
  type CommonPosition,
 | 
					  type CommonPosition,
 | 
				
			||||||
} from '$lib/utils/layout-utils';
 | 
					} from '$lib/utils/layout-utils';
 | 
				
			||||||
import { formatDateGroupTitle, fromLocalDateTime } from '$lib/utils/timeline-util';
 | 
					import { formatDateGroupTitle, toTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
import { TUNABLES } from '$lib/utils/tunables';
 | 
					import { TUNABLES } from '$lib/utils/tunables';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AssetOrder,
 | 
					  AssetOrder,
 | 
				
			||||||
@ -16,11 +16,11 @@ import {
 | 
				
			|||||||
  getTimeBuckets,
 | 
					  getTimeBuckets,
 | 
				
			||||||
  TimeBucketSize,
 | 
					  TimeBucketSize,
 | 
				
			||||||
  type AssetResponseDto,
 | 
					  type AssetResponseDto,
 | 
				
			||||||
 | 
					  type AssetStackResponseDto,
 | 
				
			||||||
} from '@immich/sdk';
 | 
					} from '@immich/sdk';
 | 
				
			||||||
import { clamp, debounce, isEqual, throttle } from 'lodash-es';
 | 
					import { clamp, debounce, isEqual, throttle } from 'lodash-es';
 | 
				
			||||||
import { DateTime } from 'luxon';
 | 
					import { DateTime } from 'luxon';
 | 
				
			||||||
import { t } from 'svelte-i18n';
 | 
					import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import { SvelteSet } from 'svelte/reactivity';
 | 
					import { SvelteSet } from 'svelte/reactivity';
 | 
				
			||||||
import { get, writable, type Unsubscriber } from 'svelte/store';
 | 
					import { get, writable, type Unsubscriber } from 'svelte/store';
 | 
				
			||||||
import { handleError } from '../utils/handle-error';
 | 
					import { handleError } from '../utils/handle-error';
 | 
				
			||||||
@ -62,13 +62,30 @@ function updateObject(target: any, source: any): boolean {
 | 
				
			|||||||
  return updated;
 | 
					  return updated;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function assetSnapshot(asset: AssetResponseDto) {
 | 
					export function assetSnapshot(asset: TimelineAsset): TimelineAsset {
 | 
				
			||||||
  return $state.snapshot(asset);
 | 
					  return $state.snapshot(asset) as TimelineAsset;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function assetsSnapshot(assets: AssetResponseDto[]) {
 | 
					export function assetsSnapshot(assets: TimelineAsset[]): TimelineAsset[] {
 | 
				
			||||||
  return assets.map((a) => $state.snapshot(a));
 | 
					  return assets.map((a) => $state.snapshot(a)) as TimelineAsset[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type TimelineAsset = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  ownerId: string;
 | 
				
			||||||
 | 
					  ratio: number;
 | 
				
			||||||
 | 
					  thumbhash: string | null;
 | 
				
			||||||
 | 
					  localDateTime: string;
 | 
				
			||||||
 | 
					  isArchived: boolean;
 | 
				
			||||||
 | 
					  isFavorite: boolean;
 | 
				
			||||||
 | 
					  isTrashed: boolean;
 | 
				
			||||||
 | 
					  isVideo: boolean;
 | 
				
			||||||
 | 
					  isImage: boolean;
 | 
				
			||||||
 | 
					  stack: AssetStackResponseDto | null;
 | 
				
			||||||
 | 
					  duration: string | null;
 | 
				
			||||||
 | 
					  projectionType: string | null;
 | 
				
			||||||
 | 
					  livePhotoVideoId: string | null;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
class IntersectingAsset {
 | 
					class IntersectingAsset {
 | 
				
			||||||
  // --- public ---
 | 
					  // --- public ---
 | 
				
			||||||
  readonly #group: AssetDateGroup;
 | 
					  readonly #group: AssetDateGroup;
 | 
				
			||||||
@ -92,17 +109,17 @@ class IntersectingAsset {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  position: CommonPosition | undefined = $state();
 | 
					  position: CommonPosition | undefined = $state();
 | 
				
			||||||
  asset: AssetResponseDto | undefined = $state();
 | 
					  asset: TimelineAsset | undefined = $state();
 | 
				
			||||||
  id: string | undefined = $derived(this.asset?.id);
 | 
					  id: string | undefined = $derived(this.asset?.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(group: AssetDateGroup, asset: AssetResponseDto) {
 | 
					  constructor(group: AssetDateGroup, asset: TimelineAsset) {
 | 
				
			||||||
    this.#group = group;
 | 
					    this.#group = group;
 | 
				
			||||||
    this.asset = asset;
 | 
					    this.asset = asset;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
type AssetOperation = (asset: AssetResponseDto) => { remove: boolean };
 | 
					type AssetOperation = (asset: TimelineAsset) => { remove: boolean };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type MoveAsset = { asset: AssetResponseDto; year: number; month: number };
 | 
					type MoveAsset = { asset: TimelineAsset; year: number; month: number };
 | 
				
			||||||
export class AssetDateGroup {
 | 
					export class AssetDateGroup {
 | 
				
			||||||
  // --- public
 | 
					  // --- public
 | 
				
			||||||
  readonly bucket: AssetBucket;
 | 
					  readonly bucket: AssetBucket;
 | 
				
			||||||
@ -131,8 +148,8 @@ export class AssetDateGroup {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
 | 
					  sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
 | 
				
			||||||
    this.intersetingAssets.sort((a, b) => {
 | 
					    this.intersetingAssets.sort((a, b) => {
 | 
				
			||||||
      const aDate = DateTime.fromISO(a.asset!.fileCreatedAt).toUTC();
 | 
					      const aDate = DateTime.fromISO(a.asset!.localDateTime).toUTC();
 | 
				
			||||||
      const bDate = DateTime.fromISO(b.asset!.fileCreatedAt).toUTC();
 | 
					      const bDate = DateTime.fromISO(b.asset!.localDateTime).toUTC();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (sortOrder === AssetOrder.Asc) {
 | 
					      if (sortOrder === AssetOrder.Asc) {
 | 
				
			||||||
        return aDate.diff(bDate).milliseconds;
 | 
					        return aDate.diff(bDate).milliseconds;
 | 
				
			||||||
@ -223,6 +240,25 @@ export type ViewportXY = Viewport & {
 | 
				
			|||||||
  y: number;
 | 
					  y: number;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddContext {
 | 
				
			||||||
 | 
					  lookupCache: {
 | 
				
			||||||
 | 
					    [dayOfMonth: number]: AssetDateGroup;
 | 
				
			||||||
 | 
					  } = {};
 | 
				
			||||||
 | 
					  unprocessedAssets: TimelineAsset[] = [];
 | 
				
			||||||
 | 
					  changedDateGroups = new Set<AssetDateGroup>();
 | 
				
			||||||
 | 
					  newDateGroups = new Set<AssetDateGroup>();
 | 
				
			||||||
 | 
					  sort(bucket: AssetBucket, sortOrder: AssetOrder = AssetOrder.Desc) {
 | 
				
			||||||
 | 
					    for (const group of this.changedDateGroups) {
 | 
				
			||||||
 | 
					      group.sortAssets(sortOrder);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const group of this.newDateGroups) {
 | 
				
			||||||
 | 
					      group.sortAssets(sortOrder);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.newDateGroups.size > 0) {
 | 
				
			||||||
 | 
					      bucket.sortDateGroups();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
export class AssetBucket {
 | 
					export class AssetBucket {
 | 
				
			||||||
  // --- public ---
 | 
					  // --- public ---
 | 
				
			||||||
  #intersecting: boolean = $state(false);
 | 
					  #intersecting: boolean = $state(false);
 | 
				
			||||||
@ -314,7 +350,7 @@ export class AssetBucket {
 | 
				
			|||||||
  getAssets() {
 | 
					  getAssets() {
 | 
				
			||||||
    // eslint-disable-next-line unicorn/no-array-reduce
 | 
					    // eslint-disable-next-line unicorn/no-array-reduce
 | 
				
			||||||
    return this.dateGroups.reduce(
 | 
					    return this.dateGroups.reduce(
 | 
				
			||||||
      (accumulator: AssetResponseDto[], g: AssetDateGroup) => accumulator.concat(g.getAssets()),
 | 
					      (accumulator: TimelineAsset[], g: AssetDateGroup) => accumulator.concat(g.getAssets()),
 | 
				
			||||||
      [],
 | 
					      [],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -379,56 +415,52 @@ export class AssetBucket {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // note - if the assets are not part of this bucket, they will not be added
 | 
					  // note - if the assets are not part of this bucket, they will not be added
 | 
				
			||||||
  addAssets(assets: AssetResponseDto[]) {
 | 
					  addAssets(bucketResponse: AssetResponseDto[]) {
 | 
				
			||||||
    const lookupCache: {
 | 
					    const addContext = new AddContext();
 | 
				
			||||||
      [dayOfMonth: number]: AssetDateGroup;
 | 
					    for (const asset of bucketResponse) {
 | 
				
			||||||
    } = {};
 | 
					      const timelineAsset = toTimelineAsset(asset);
 | 
				
			||||||
    const unprocessedAssets: AssetResponseDto[] = [];
 | 
					      this.addTimelineAsset(timelineAsset, addContext);
 | 
				
			||||||
    const changedDateGroups = new Set<AssetDateGroup>();
 | 
					    }
 | 
				
			||||||
    const newDateGroups = new Set<AssetDateGroup>();
 | 
					
 | 
				
			||||||
    for (const asset of assets) {
 | 
					    addContext.sort(this, this.#sortOrder);
 | 
				
			||||||
      const date = DateTime.fromISO(asset.localDateTime).toUTC();
 | 
					    return addContext.unprocessedAssets;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) {
 | 
				
			||||||
 | 
					    const { id, localDateTime } = timelineAsset;
 | 
				
			||||||
 | 
					    const date = DateTime.fromISO(localDateTime).toUTC();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const month = date.get('month');
 | 
					    const month = date.get('month');
 | 
				
			||||||
    const year = date.get('year');
 | 
					    const year = date.get('year');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.month === month && this.year === year) {
 | 
					    if (this.month === month && this.year === year) {
 | 
				
			||||||
      const day = date.get('day');
 | 
					      const day = date.get('day');
 | 
				
			||||||
        let dateGroup: AssetDateGroup | undefined = lookupCache[day];
 | 
					      let dateGroup: AssetDateGroup | undefined = addContext.lookupCache[day];
 | 
				
			||||||
      if (!dateGroup) {
 | 
					      if (!dateGroup) {
 | 
				
			||||||
        dateGroup = this.findDateGroupByDay(day);
 | 
					        dateGroup = this.findDateGroupByDay(day);
 | 
				
			||||||
        if (dateGroup) {
 | 
					        if (dateGroup) {
 | 
				
			||||||
            lookupCache[day] = dateGroup;
 | 
					          addContext.lookupCache[day] = dateGroup;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (dateGroup) {
 | 
					      if (dateGroup) {
 | 
				
			||||||
          const intersectingAsset = new IntersectingAsset(dateGroup, asset);
 | 
					        const intersectingAsset = new IntersectingAsset(dateGroup, timelineAsset);
 | 
				
			||||||
          if (dateGroup.intersetingAssets.some((a) => a.id === asset.id)) {
 | 
					        if (dateGroup.intersetingAssets.some((a) => a.id === id)) {
 | 
				
			||||||
            console.error(`Ignoring attempt to add duplicate asset ${asset.id} to ${dateGroup.groupTitle}`);
 | 
					          console.error(`Ignoring attempt to add duplicate asset ${id} to ${dateGroup.groupTitle}`);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          dateGroup.intersetingAssets.push(intersectingAsset);
 | 
					          dateGroup.intersetingAssets.push(intersectingAsset);
 | 
				
			||||||
            changedDateGroups.add(dateGroup);
 | 
					          addContext.changedDateGroups.add(dateGroup);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day);
 | 
					        dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day);
 | 
				
			||||||
          dateGroup.intersetingAssets.push(new IntersectingAsset(dateGroup, asset));
 | 
					        dateGroup.intersetingAssets.push(new IntersectingAsset(dateGroup, timelineAsset));
 | 
				
			||||||
        this.dateGroups.push(dateGroup);
 | 
					        this.dateGroups.push(dateGroup);
 | 
				
			||||||
          lookupCache[day] = dateGroup;
 | 
					        addContext.lookupCache[day] = dateGroup;
 | 
				
			||||||
          newDateGroups.add(dateGroup);
 | 
					        addContext.newDateGroups.add(dateGroup);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        unprocessedAssets.push(asset);
 | 
					      addContext.unprocessedAssets.push(timelineAsset);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    for (const group of changedDateGroups) {
 | 
					 | 
				
			||||||
      group.sortAssets(this.#sortOrder);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    for (const group of newDateGroups) {
 | 
					 | 
				
			||||||
      group.sortAssets(this.#sortOrder);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (newDateGroups.size > 0) {
 | 
					 | 
				
			||||||
      this.sortDateGroups();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return unprocessedAssets;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  getRandomDateGroup() {
 | 
					  getRandomDateGroup() {
 | 
				
			||||||
    const random = Math.floor(Math.random() * this.dateGroups.length);
 | 
					    const random = Math.floor(Math.random() * this.dateGroups.length);
 | 
				
			||||||
    return this.dateGroups[random];
 | 
					    return this.dateGroups[random];
 | 
				
			||||||
@ -514,12 +546,12 @@ const isMismatched = (option: boolean | undefined, value: boolean): boolean =>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface AddAsset {
 | 
					interface AddAsset {
 | 
				
			||||||
  type: 'add';
 | 
					  type: 'add';
 | 
				
			||||||
  values: AssetResponseDto[];
 | 
					  values: TimelineAsset[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface UpdateAsset {
 | 
					interface UpdateAsset {
 | 
				
			||||||
  type: 'update';
 | 
					  type: 'update';
 | 
				
			||||||
  values: AssetResponseDto[];
 | 
					  values: TimelineAsset[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface DeleteAsset {
 | 
					interface DeleteAsset {
 | 
				
			||||||
@ -701,9 +733,13 @@ export class AssetStore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  connect() {
 | 
					  connect() {
 | 
				
			||||||
    this.#unsubscribers.push(
 | 
					    this.#unsubscribers.push(
 | 
				
			||||||
      websocketEvents.on('on_upload_success', (asset) => this.#addPendingChanges({ type: 'add', values: [asset] })),
 | 
					      websocketEvents.on('on_upload_success', (asset) =>
 | 
				
			||||||
 | 
					        this.#addPendingChanges({ type: 'add', values: [toTimelineAsset(asset)] }),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      websocketEvents.on('on_asset_trash', (ids) => this.#addPendingChanges({ type: 'trash', values: ids })),
 | 
					      websocketEvents.on('on_asset_trash', (ids) => this.#addPendingChanges({ type: 'trash', values: ids })),
 | 
				
			||||||
      websocketEvents.on('on_asset_update', (asset) => this.#addPendingChanges({ type: 'update', values: [asset] })),
 | 
					      websocketEvents.on('on_asset_update', (asset) =>
 | 
				
			||||||
 | 
					        this.#addPendingChanges({ type: 'update', values: [toTimelineAsset(asset)] }),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      websocketEvents.on('on_asset_delete', (id: string) => this.#addPendingChanges({ type: 'delete', values: [id] })),
 | 
					      websocketEvents.on('on_asset_delete', (id: string) => this.#addPendingChanges({ type: 'delete', values: [id] })),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -717,8 +753,8 @@ export class AssetStore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  #getPendingChangeBatches() {
 | 
					  #getPendingChangeBatches() {
 | 
				
			||||||
    const batch: {
 | 
					    const batch: {
 | 
				
			||||||
      add: AssetResponseDto[];
 | 
					      add: TimelineAsset[];
 | 
				
			||||||
      update: AssetResponseDto[];
 | 
					      update: TimelineAsset[];
 | 
				
			||||||
      remove: string[];
 | 
					      remove: string[];
 | 
				
			||||||
    } = {
 | 
					    } = {
 | 
				
			||||||
      add: [],
 | 
					      add: [],
 | 
				
			||||||
@ -1042,7 +1078,7 @@ export class AssetStore {
 | 
				
			|||||||
        // so no need to load the bucket, it already has assets
 | 
					        // so no need to load the bucket, it already has assets
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const assets = await getTimeBucket(
 | 
					      const bucketResponse = await getTimeBucket(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          ...this.#options,
 | 
					          ...this.#options,
 | 
				
			||||||
          timeBucket: bucketDate,
 | 
					          timeBucket: bucketDate,
 | 
				
			||||||
@ -1051,9 +1087,9 @@ export class AssetStore {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        { signal },
 | 
					        { signal },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      if (assets) {
 | 
					      if (bucketResponse) {
 | 
				
			||||||
        if (this.#options.timelineAlbumId) {
 | 
					        if (this.#options.timelineAlbumId) {
 | 
				
			||||||
          const albumAssets = await getTimeBucket(
 | 
					          const bucketAssets = await getTimeBucket(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              albumId: this.#options.timelineAlbumId,
 | 
					              albumId: this.#options.timelineAlbumId,
 | 
				
			||||||
              timeBucket: bucketDate,
 | 
					              timeBucket: bucketDate,
 | 
				
			||||||
@ -1062,12 +1098,11 @@ export class AssetStore {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            { signal },
 | 
					            { signal },
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          for (const asset of albumAssets) {
 | 
					          for (const { id } of bucketAssets) {
 | 
				
			||||||
            this.albumAssets.add(asset.id);
 | 
					            this.albumAssets.add(id);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        const unprocessed = bucket.addAssets(bucketResponse);
 | 
				
			||||||
        const unprocessed = bucket.addAssets(assets);
 | 
					 | 
				
			||||||
        if (unprocessed.length > 0) {
 | 
					        if (unprocessed.length > 0) {
 | 
				
			||||||
          console.error(
 | 
					          console.error(
 | 
				
			||||||
            `Warning: getTimeBucket API returning assets not in requested month: ${bucket.bucketDate}, ${JSON.stringify(unprocessed.map((a) => ({ id: a.id, localDateTime: a.localDateTime })))}`,
 | 
					            `Warning: getTimeBucket API returning assets not in requested month: ${bucket.bucketDate}, ${JSON.stringify(unprocessed.map((a) => ({ id: a.id, localDateTime: a.localDateTime })))}`,
 | 
				
			||||||
@ -1081,8 +1116,8 @@ export class AssetStore {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addAssets(assets: AssetResponseDto[]) {
 | 
					  addAssets(assets: TimelineAsset[]) {
 | 
				
			||||||
    const assetsToUpdate: AssetResponseDto[] = [];
 | 
					    const assetsToUpdate: TimelineAsset[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const asset of assets) {
 | 
					    for (const asset of assets) {
 | 
				
			||||||
      if (this.isExcluded(asset)) {
 | 
					      if (this.isExcluded(asset)) {
 | 
				
			||||||
@ -1095,7 +1130,7 @@ export class AssetStore {
 | 
				
			|||||||
    this.#addAssetsToBuckets([...notUpdated]);
 | 
					    this.#addAssetsToBuckets([...notUpdated]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #addAssetsToBuckets(assets: AssetResponseDto[]) {
 | 
					  #addAssetsToBuckets(assets: TimelineAsset[]) {
 | 
				
			||||||
    if (assets.length === 0) {
 | 
					    if (assets.length === 0) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -1112,7 +1147,9 @@ export class AssetStore {
 | 
				
			|||||||
        bucket = new AssetBucket(this, utc, 1, this.#options.order);
 | 
					        bucket = new AssetBucket(this, utc, 1, this.#options.order);
 | 
				
			||||||
        this.buckets.push(bucket);
 | 
					        this.buckets.push(bucket);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      bucket.addAssets([asset]);
 | 
					      const addContext = new AddContext();
 | 
				
			||||||
 | 
					      bucket.addTimelineAsset(asset, addContext);
 | 
				
			||||||
 | 
					      addContext.sort(bucket, this.#options.order);
 | 
				
			||||||
      updatedBuckets.add(bucket);
 | 
					      updatedBuckets.add(bucket);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1138,7 +1175,7 @@ export class AssetStore {
 | 
				
			|||||||
    await this.initTask.waitUntilCompletion();
 | 
					    await this.initTask.waitUntilCompletion();
 | 
				
			||||||
    let bucket = this.#findBucketForAsset(id);
 | 
					    let bucket = this.#findBucketForAsset(id);
 | 
				
			||||||
    if (!bucket) {
 | 
					    if (!bucket) {
 | 
				
			||||||
      const asset = await getAssetInfo({ id });
 | 
					      const asset = toTimelineAsset(await getAssetInfo({ id }));
 | 
				
			||||||
      if (!asset || this.isExcluded(asset)) {
 | 
					      if (!asset || this.isExcluded(asset)) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -1151,7 +1188,7 @@ export class AssetStore {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async #loadBucketAtTime(localDateTime: string, options?: { cancelable: boolean }) {
 | 
					  async #loadBucketAtTime(localDateTime: string, options?: { cancelable: boolean }) {
 | 
				
			||||||
    let date = fromLocalDateTime(localDateTime);
 | 
					    let date = DateTime.fromISO(localDateTime).toUTC();
 | 
				
			||||||
    // Only support TimeBucketSize.Month
 | 
					    // Only support TimeBucketSize.Month
 | 
				
			||||||
    date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
 | 
					    date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
 | 
				
			||||||
    const iso = date.toISO()!;
 | 
					    const iso = date.toISO()!;
 | 
				
			||||||
@ -1161,7 +1198,7 @@ export class AssetStore {
 | 
				
			|||||||
    return this.getBucketByDate(year, month);
 | 
					    return this.getBucketByDate(year, month);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async #getBucketInfoForAsset(asset: AssetResponseDto, options?: { cancelable: boolean }) {
 | 
					  async #getBucketInfoForAsset(asset: { id: string; localDateTime: string }, options?: { cancelable: boolean }) {
 | 
				
			||||||
    const bucketInfo = this.#findBucketForAsset(asset.id);
 | 
					    const bucketInfo = this.#findBucketForAsset(asset.id);
 | 
				
			||||||
    if (bucketInfo) {
 | 
					    if (bucketInfo) {
 | 
				
			||||||
      return bucketInfo;
 | 
					      return bucketInfo;
 | 
				
			||||||
@ -1195,7 +1232,7 @@ export class AssetStore {
 | 
				
			|||||||
    const changedBuckets = new Set<AssetBucket>();
 | 
					    const changedBuckets = new Set<AssetBucket>();
 | 
				
			||||||
    let idsToProcess = new Set(ids);
 | 
					    let idsToProcess = new Set(ids);
 | 
				
			||||||
    const idsProcessed = new Set<string>();
 | 
					    const idsProcessed = new Set<string>();
 | 
				
			||||||
    const combinedMoveAssets: { asset: AssetResponseDto; year: number; month: number }[][] = [];
 | 
					    const combinedMoveAssets: { asset: TimelineAsset; year: number; month: number }[][] = [];
 | 
				
			||||||
    for (const bucket of this.buckets) {
 | 
					    for (const bucket of this.buckets) {
 | 
				
			||||||
      if (idsToProcess.size > 0) {
 | 
					      if (idsToProcess.size > 0) {
 | 
				
			||||||
        const { moveAssets, processedIds, changedGeometry } = bucket.runAssetOperation(idsToProcess, operation);
 | 
					        const { moveAssets, processedIds, changedGeometry } = bucket.runAssetOperation(idsToProcess, operation);
 | 
				
			||||||
@ -1238,8 +1275,8 @@ export class AssetStore {
 | 
				
			|||||||
    this.#runAssetOperation(new Set(ids), operation);
 | 
					    this.#runAssetOperation(new Set(ids), operation);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateAssets(assets: AssetResponseDto[]) {
 | 
					  updateAssets(assets: TimelineAsset[]) {
 | 
				
			||||||
    const lookup = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, asset]));
 | 
					    const lookup = new Map<string, TimelineAsset>(assets.map((asset) => [asset.id, asset]));
 | 
				
			||||||
    const { unprocessedIds } = this.#runAssetOperation(new Set(lookup.keys()), (asset) => {
 | 
					    const { unprocessedIds } = this.#runAssetOperation(new Set(lookup.keys()), (asset) => {
 | 
				
			||||||
      updateObject(asset, lookup.get(asset.id));
 | 
					      updateObject(asset, lookup.get(asset.id));
 | 
				
			||||||
      return { remove: false };
 | 
					      return { remove: false };
 | 
				
			||||||
@ -1261,11 +1298,11 @@ export class AssetStore {
 | 
				
			|||||||
    this.updateIntersections();
 | 
					    this.updateIntersections();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getFirstAsset(): AssetResponseDto | undefined {
 | 
					  getFirstAsset(): TimelineAsset | undefined {
 | 
				
			||||||
    return this.buckets[0]?.getFirstAsset();
 | 
					    return this.buckets[0]?.getFirstAsset();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getPreviousAsset(asset: AssetResponseDto): Promise<AssetResponseDto | undefined> {
 | 
					  async getPreviousAsset(asset: { id: string; localDateTime: string }): Promise<TimelineAsset | undefined> {
 | 
				
			||||||
    let bucket = await this.#getBucketInfoForAsset(asset);
 | 
					    let bucket = await this.#getBucketInfoForAsset(asset);
 | 
				
			||||||
    if (!bucket) {
 | 
					    if (!bucket) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
@ -1308,7 +1345,7 @@ export class AssetStore {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getNextAsset(asset: AssetResponseDto): Promise<AssetResponseDto | undefined> {
 | 
					  async getNextAsset(asset: { id: string; localDateTime: string }): Promise<TimelineAsset | undefined> {
 | 
				
			||||||
    let bucket = await this.#getBucketInfoForAsset(asset);
 | 
					    let bucket = await this.#getBucketInfoForAsset(asset);
 | 
				
			||||||
    if (!bucket) {
 | 
					    if (!bucket) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
@ -1347,7 +1384,7 @@ export class AssetStore {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isExcluded(asset: AssetResponseDto) {
 | 
					  isExcluded(asset: TimelineAsset) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      isMismatched(this.#options.isArchived, asset.isArchived) ||
 | 
					      isMismatched(this.#options.isArchived, asset.isArchived) ||
 | 
				
			||||||
      isMismatched(this.#options.isFavorite, asset.isFavorite) ||
 | 
					      isMismatched(this.#options.isFavorite, asset.isFavorite) ||
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,20 @@
 | 
				
			|||||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
 | 
					import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
import type { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
import type { StackResponse } from '$lib/utils/asset-utils';
 | 
					import type { StackResponse } from '$lib/utils/asset-utils';
 | 
				
			||||||
import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk';
 | 
					import { deleteAssets as deleteBulk } from '@immich/sdk';
 | 
				
			||||||
import { t } from 'svelte-i18n';
 | 
					import { t } from 'svelte-i18n';
 | 
				
			||||||
import { get } from 'svelte/store';
 | 
					import { get } from 'svelte/store';
 | 
				
			||||||
import { handleError } from './handle-error';
 | 
					import { handleError } from './handle-error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type OnDelete = (assetIds: string[]) => void;
 | 
					export type OnDelete = (assetIds: string[]) => void;
 | 
				
			||||||
export type OnRestore = (ids: string[]) => void;
 | 
					export type OnRestore = (ids: string[]) => void;
 | 
				
			||||||
export type OnLink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
 | 
					export type OnLink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
 | 
				
			||||||
export type OnUnlink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
 | 
					export type OnUnlink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
 | 
				
			||||||
export type OnAddToAlbum = (ids: string[], albumId: string) => void;
 | 
					export type OnAddToAlbum = (ids: string[], albumId: string) => void;
 | 
				
			||||||
export type OnArchive = (ids: string[], isArchived: boolean) => void;
 | 
					export type OnArchive = (ids: string[], isArchived: boolean) => void;
 | 
				
			||||||
export type OnFavorite = (ids: string[], favorite: boolean) => void;
 | 
					export type OnFavorite = (ids: string[], favorite: boolean) => void;
 | 
				
			||||||
export type OnStack = (result: StackResponse) => void;
 | 
					export type OnStack = (result: StackResponse) => void;
 | 
				
			||||||
export type OnUnstack = (assets: AssetResponseDto[]) => void;
 | 
					export type OnUnstack = (assets: TimelineAsset[]) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
 | 
					export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
 | 
				
			||||||
  const $t = get(t);
 | 
					  const $t = get(t);
 | 
				
			||||||
@ -64,11 +64,11 @@ export function updateStackedAssetInTimeline(assetStore: AssetStore, { stack, to
 | 
				
			|||||||
 * @param assetStore - The asset store to update.
 | 
					 * @param assetStore - The asset store to update.
 | 
				
			||||||
 * @param assets - The array of asset response DTOs to update in the asset store.
 | 
					 * @param assets - The array of asset response DTOs to update in the asset store.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function updateUnstackedAssetInTimeline(assetStore: AssetStore, assets: AssetResponseDto[]) {
 | 
					export function updateUnstackedAssetInTimeline(assetStore: AssetStore, assets: TimelineAsset[]) {
 | 
				
			||||||
  assetStore.updateAssetOperation(
 | 
					  assetStore.updateAssetOperation(
 | 
				
			||||||
    assets.map((asset) => asset.id),
 | 
					    assets.map((asset) => asset.id),
 | 
				
			||||||
    (asset) => {
 | 
					    (asset) => {
 | 
				
			||||||
      asset.stack = undefined;
 | 
					      asset.stack = null;
 | 
				
			||||||
      return { remove: false };
 | 
					      return { remove: false };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
 | 
				
			|||||||
import type { InterpolationValues } from '$lib/components/i18n/format-message';
 | 
					import type { InterpolationValues } from '$lib/components/i18n/format-message';
 | 
				
			||||||
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
 | 
					import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
import { AppRoute } from '$lib/constants';
 | 
					import { AppRoute } from '$lib/constants';
 | 
				
			||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					import type { AssetInteraction, BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
import { downloadManager } from '$lib/stores/download-store.svelte';
 | 
					import { downloadManager } from '$lib/stores/download-store.svelte';
 | 
				
			||||||
import { preferences } from '$lib/stores/user.store';
 | 
					import { preferences } from '$lib/stores/user.store';
 | 
				
			||||||
@ -364,7 +364,7 @@ export const getAssetType = (type: AssetTypeEnum) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getSelectedAssets = (assets: AssetResponseDto[], user: UserResponseDto | null): string[] => {
 | 
					export const getSelectedAssets = (assets: BaseInteractionAsset[], user: UserResponseDto | null): string[] => {
 | 
				
			||||||
  const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
 | 
					  const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
 | 
					  const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
 | 
				
			||||||
@ -383,7 +383,7 @@ export type StackResponse = {
 | 
				
			|||||||
  toDeleteIds: string[];
 | 
					  toDeleteIds: string[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const stackAssets = async (assets: AssetResponseDto[], showNotification = true): Promise<StackResponse> => {
 | 
					export const stackAssets = async (assets: { id: string }[], showNotification = true): Promise<StackResponse> => {
 | 
				
			||||||
  if (assets.length < 2) {
 | 
					  if (assets.length < 2) {
 | 
				
			||||||
    return { stack: undefined, toDeleteIds: [] };
 | 
					    return { stack: undefined, toDeleteIds: [] };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -403,9 +403,9 @@ export const stackAssets = async (assets: AssetResponseDto[], showNotification =
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const [index, asset] of assets.entries()) {
 | 
					    // for (const [index, asset] of assets.entries()) {
 | 
				
			||||||
      asset.stack = index === 0 ? { id: stack.id, assetCount: stack.assets.length, primaryAssetId: asset.id } : null;
 | 
					    //   asset.stack = index === 0 ? { id: stack.id, assetCount: stack.assets.length, primaryAssetId: asset.id } : null;
 | 
				
			||||||
    }
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      stack,
 | 
					      stack,
 | 
				
			||||||
@ -467,7 +467,10 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: AssetInteraction) => {
 | 
					export const selectAllAssets = async (
 | 
				
			||||||
 | 
					  assetStore: AssetStore,
 | 
				
			||||||
 | 
					  assetInteraction: AssetInteraction<BaseInteractionAsset>,
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
  if (get(isSelectingAllAssets)) {
 | 
					  if (get(isSelectingAllAssets)) {
 | 
				
			||||||
    // Selection is already ongoing
 | 
					    // Selection is already ongoing
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
@ -495,7 +498,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteraction:
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const cancelMultiselect = (assetInteraction: AssetInteraction) => {
 | 
					export const cancelMultiselect = (assetInteraction: AssetInteraction<BaseInteractionAsset>) => {
 | 
				
			||||||
  isSelectingAllAssets.set(false);
 | 
					  isSelectingAllAssets.set(false);
 | 
				
			||||||
  assetInteraction.clearMultiselect();
 | 
					  assetInteraction.clearMultiselect();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -523,7 +526,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
 | 
				
			|||||||
  return asset;
 | 
					  return asset;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean) => {
 | 
					export const archiveAssets = async (assets: { id: string }[], archive: boolean) => {
 | 
				
			||||||
  const isArchived = archive;
 | 
					  const isArchived = archive;
 | 
				
			||||||
  const ids = assets.map(({ id }) => id);
 | 
					  const ids = assets.map(({ id }) => id);
 | 
				
			||||||
  const $t = get(t);
 | 
					  const $t = get(t);
 | 
				
			||||||
@ -533,9 +536,9 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
 | 
				
			|||||||
      await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
 | 
					      await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const asset of assets) {
 | 
					    // for (const asset of assets) {
 | 
				
			||||||
      asset.isArchived = isArchived;
 | 
					    //   asset.isArchived = isArchived;
 | 
				
			||||||
    }
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    notificationController.show({
 | 
					    notificationController.show({
 | 
				
			||||||
      message: isArchived
 | 
					      message: isArchived
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,10 @@
 | 
				
			|||||||
import { getAssetRatio } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
// import { TUNABLES } from '$lib/utils/tunables';
 | 
					// import { TUNABLES } from '$lib/utils/tunables';
 | 
				
			||||||
// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
 | 
					// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
 | 
				
			||||||
// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
 | 
					// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					import { getAssetRatio } from '$lib/utils/asset-utils';
 | 
				
			||||||
 | 
					import { isTimelineAsset } from '$lib/utils/timeline-util';
 | 
				
			||||||
import type { AssetResponseDto } from '@immich/sdk';
 | 
					import type { AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
import createJustifiedLayout from 'justified-layout';
 | 
					import createJustifiedLayout from 'justified-layout';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,7 +29,7 @@ export type CommonLayoutOptions = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getJustifiedLayoutFromAssets(
 | 
					export function getJustifiedLayoutFromAssets(
 | 
				
			||||||
  assets: AssetResponseDto[],
 | 
					  assets: (TimelineAsset | AssetResponseDto)[],
 | 
				
			||||||
  options: CommonLayoutOptions,
 | 
					  options: CommonLayoutOptions,
 | 
				
			||||||
): CommonJustifiedLayout {
 | 
					): CommonJustifiedLayout {
 | 
				
			||||||
  // if (useWasm) {
 | 
					  // if (useWasm) {
 | 
				
			||||||
@ -87,7 +90,7 @@ class Adapter {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function justifiedLayout(assets: AssetResponseDto[], options: CommonLayoutOptions) {
 | 
					export function justifiedLayout(assets: (TimelineAsset | AssetResponseDto)[], options: CommonLayoutOptions) {
 | 
				
			||||||
  const adapter = {
 | 
					  const adapter = {
 | 
				
			||||||
    targetRowHeight: options.rowHeight,
 | 
					    targetRowHeight: options.rowHeight,
 | 
				
			||||||
    containerWidth: options.rowWidth,
 | 
					    containerWidth: options.rowWidth,
 | 
				
			||||||
@ -96,7 +99,7 @@ export function justifiedLayout(assets: AssetResponseDto[], options: CommonLayou
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const result = createJustifiedLayout(
 | 
					  const result = createJustifiedLayout(
 | 
				
			||||||
    assets.map((g) => getAssetRatio(g)),
 | 
					    assets.map((a) => (isTimelineAsset(a) ? a.ratio : getAssetRatio(a))),
 | 
				
			||||||
    adapter,
 | 
					    adapter,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return new Adapter(result);
 | 
					  return new Adapter(result);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,10 @@
 | 
				
			|||||||
import type { AssetBucket } from '$lib/stores/assets-store.svelte';
 | 
					import type { BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
 | 
					import type { AssetBucket, TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
import { locale } from '$lib/stores/preferences.store';
 | 
					import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
 | 
					import { getAssetRatio } from '$lib/utils/asset-utils';
 | 
				
			||||||
import { type CommonJustifiedLayout } from '$lib/utils/layout-utils';
 | 
					import { type CommonJustifiedLayout } from '$lib/utils/layout-utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { AssetResponseDto } from '@immich/sdk';
 | 
					import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
import { memoize } from 'lodash-es';
 | 
					import { memoize } from 'lodash-es';
 | 
				
			||||||
import { DateTime, type LocaleOptions } from 'luxon';
 | 
					import { DateTime, type LocaleOptions } from 'luxon';
 | 
				
			||||||
import { get } from 'svelte/store';
 | 
					import { get } from 'svelte/store';
 | 
				
			||||||
@ -105,3 +107,30 @@ export const getDateLocaleString = (date: DateTime, opts?: LocaleOptions): strin
 | 
				
			|||||||
  date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts);
 | 
					  date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const formatDateGroupTitle = memoize(formatGroupTitle);
 | 
					export const formatDateGroupTitle = memoize(formatGroupTitle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const toTimelineAsset = (unknownAsset: BaseInteractionAsset): TimelineAsset => {
 | 
				
			||||||
 | 
					  if (isTimelineAsset(unknownAsset)) {
 | 
				
			||||||
 | 
					    return unknownAsset;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const assetResponse = unknownAsset as AssetResponseDto;
 | 
				
			||||||
 | 
					  const { width, height } = getAssetRatio(assetResponse);
 | 
				
			||||||
 | 
					  const ratio = width / height;
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    id: assetResponse.id,
 | 
				
			||||||
 | 
					    ownerId: assetResponse.ownerId,
 | 
				
			||||||
 | 
					    ratio,
 | 
				
			||||||
 | 
					    thumbhash: assetResponse.thumbhash,
 | 
				
			||||||
 | 
					    localDateTime: assetResponse.localDateTime,
 | 
				
			||||||
 | 
					    isFavorite: assetResponse.isFavorite,
 | 
				
			||||||
 | 
					    isArchived: assetResponse.isArchived,
 | 
				
			||||||
 | 
					    isTrashed: assetResponse.isTrashed,
 | 
				
			||||||
 | 
					    isVideo: assetResponse.type == AssetTypeEnum.Video,
 | 
				
			||||||
 | 
					    isImage: assetResponse.type == AssetTypeEnum.Image,
 | 
				
			||||||
 | 
					    stack: assetResponse.stack || null,
 | 
				
			||||||
 | 
					    duration: assetResponse.duration || null,
 | 
				
			||||||
 | 
					    projectionType: assetResponse.exifInfo?.projectionType || null,
 | 
				
			||||||
 | 
					    livePhotoVideoId: assetResponse.livePhotoVideoId || null,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const isTimelineAsset = (arg: BaseInteractionAsset): arg is TimelineAsset =>
 | 
				
			||||||
 | 
					  (arg as TimelineAsset).ratio !== undefined;
 | 
				
			||||||
 | 
				
			|||||||
@ -22,9 +22,10 @@
 | 
				
			|||||||
  import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
 | 
					  import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
 | 
				
			||||||
  import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
 | 
					  import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
 | 
				
			||||||
  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
					  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
				
			||||||
 | 
					  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
 | 
				
			||||||
  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
					  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
				
			||||||
  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
					 | 
				
			||||||
  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
					  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 | 
					  import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 | 
				
			||||||
  import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
 | 
					  import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
 | 
				
			||||||
@ -33,14 +34,16 @@
 | 
				
			|||||||
    notificationController,
 | 
					    notificationController,
 | 
				
			||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
  import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
 | 
					  import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
 | 
				
			||||||
  import { AppRoute, AlbumPageViewMode } from '$lib/constants';
 | 
					  import { AlbumPageViewMode, AppRoute } from '$lib/constants';
 | 
				
			||||||
  import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
 | 
					  import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
 | 
				
			||||||
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
					  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
 | 
					  import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
 | 
				
			||||||
  import { preferences, user } from '$lib/stores/user.store';
 | 
					  import { preferences, user } from '$lib/stores/user.store';
 | 
				
			||||||
  import { handlePromiseError } from '$lib/utils';
 | 
					  import { handlePromiseError } from '$lib/utils';
 | 
				
			||||||
  import { downloadAlbum, cancelMultiselect } from '$lib/utils/asset-utils';
 | 
					  import { confirmAlbumDelete } from '$lib/utils/album-utils';
 | 
				
			||||||
 | 
					  import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
					  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
@ -80,13 +83,10 @@
 | 
				
			|||||||
    mdiPresentationPlay,
 | 
					    mdiPresentationPlay,
 | 
				
			||||||
    mdiShareVariantOutline,
 | 
					    mdiShareVariantOutline,
 | 
				
			||||||
  } from '@mdi/js';
 | 
					  } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { onDestroy } from 'svelte';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import { fly } from 'svelte/transition';
 | 
					  import { fly } from 'svelte/transition';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
  import { onDestroy } from 'svelte';
 | 
					 | 
				
			||||||
  import { confirmAlbumDelete } from '$lib/utils/album-utils';
 | 
					 | 
				
			||||||
  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
 | 
					 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    data: PageData;
 | 
					    data: PageData;
 | 
				
			||||||
@ -94,7 +94,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  let { data = $bindable() }: Props = $props();
 | 
					  let { data = $bindable() }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore;
 | 
					  let { isViewing: showAssetViewer, setAssetId, gridScrollTarget } = assetViewingStore;
 | 
				
			||||||
  let { slideshowState, slideshowNavigation } = slideshowStore;
 | 
					  let { slideshowState, slideshowNavigation } = slideshowStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
 | 
					  let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
 | 
				
			||||||
@ -107,8 +107,8 @@
 | 
				
			|||||||
  let reactions: ActivityResponseDto[] = $state([]);
 | 
					  let reactions: ActivityResponseDto[] = $state([]);
 | 
				
			||||||
  let albumOrder: AssetOrder | undefined = $state(data.album.order);
 | 
					  let albumOrder: AssetOrder | undefined = $state(data.album.order);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
  const timelineInteraction = new AssetInteraction();
 | 
					  const timelineInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  afterNavigate(({ from }) => {
 | 
					  afterNavigate(({ from }) => {
 | 
				
			||||||
    let url: string | undefined = from?.url?.pathname;
 | 
					    let url: string | undefined = from?.url?.pathname;
 | 
				
			||||||
@ -207,8 +207,7 @@
 | 
				
			|||||||
        ? await assetStore.getRandomAsset()
 | 
					        ? await assetStore.getRandomAsset()
 | 
				
			||||||
        : assetStore.buckets[0]?.dateGroups[0]?.intersetingAssets[0]?.asset;
 | 
					        : assetStore.buckets[0]?.dateGroups[0]?.intersetingAssets[0]?.asset;
 | 
				
			||||||
    if (asset) {
 | 
					    if (asset) {
 | 
				
			||||||
      setAsset(asset);
 | 
					      handlePromiseError(setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow)));
 | 
				
			||||||
      $slideshowState = SlideshowState.PlaySlideshow;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -9,16 +9,16 @@
 | 
				
			|||||||
  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
					  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
				
			||||||
  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
					  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
				
			||||||
  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
					  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
				
			||||||
  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
					 | 
				
			||||||
  import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
 | 
					  import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
 | 
				
			||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
  import { mdiPlus, mdiDotsVertical } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
  import { onDestroy } from 'svelte';
 | 
					 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { onDestroy } from 'svelte';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    data: PageData;
 | 
					    data: PageData;
 | 
				
			||||||
@ -29,7 +29,7 @@
 | 
				
			|||||||
  void assetStore.updateOptions({ isArchived: true });
 | 
					  void assetStore.updateOptions({ isArchived: true });
 | 
				
			||||||
  onDestroy(() => assetStore.destroy());
 | 
					  onDestroy(() => assetStore.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleEscape = () => {
 | 
					  const handleEscape = () => {
 | 
				
			||||||
    if (assetInteraction.selectionActive) {
 | 
					    if (assetInteraction.selectionActive) {
 | 
				
			||||||
 | 
				
			|||||||
@ -9,19 +9,19 @@
 | 
				
			|||||||
  import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
 | 
					  import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
 | 
				
			||||||
  import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
 | 
					  import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
 | 
				
			||||||
  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
					  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
 | 
				
			||||||
 | 
					  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
 | 
				
			||||||
  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
					  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
				
			||||||
  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
					 | 
				
			||||||
  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
					  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
				
			||||||
  import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
 | 
					  import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
 | 
				
			||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					 | 
				
			||||||
  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
  import { onDestroy } from 'svelte';
 | 
					 | 
				
			||||||
  import { preferences } from '$lib/stores/user.store';
 | 
					 | 
				
			||||||
  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
 | 
					 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					  import { preferences } from '$lib/stores/user.store';
 | 
				
			||||||
 | 
					  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { onDestroy } from 'svelte';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    data: PageData;
 | 
					    data: PageData;
 | 
				
			||||||
@ -30,10 +30,10 @@
 | 
				
			|||||||
  let { data }: Props = $props();
 | 
					  let { data }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetStore = new AssetStore();
 | 
					  const assetStore = new AssetStore();
 | 
				
			||||||
  void assetStore.updateOptions({ isFavorite: true });
 | 
					  void assetStore.updateOptions({ isFavorite: true, withStacked: true });
 | 
				
			||||||
  onDestroy(() => assetStore.destroy());
 | 
					  onDestroy(() => assetStore.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleEscape = () => {
 | 
					  const handleEscape = () => {
 | 
				
			||||||
    if (assetInteraction.selectionActive) {
 | 
					    if (assetInteraction.selectionActive) {
 | 
				
			||||||
@ -76,6 +76,7 @@
 | 
				
			|||||||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
 | 
					<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
 | 
				
			||||||
  <AssetGrid
 | 
					  <AssetGrid
 | 
				
			||||||
    enableRouting={true}
 | 
					    enableRouting={true}
 | 
				
			||||||
 | 
					    withStacked={true}
 | 
				
			||||||
    {assetStore}
 | 
					    {assetStore}
 | 
				
			||||||
    {assetInteraction}
 | 
					    {assetInteraction}
 | 
				
			||||||
    removeAction={AssetAction.UNFAVORITE}
 | 
					    removeAction={AssetAction.UNFAVORITE}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,37 +1,38 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { afterNavigate, goto, invalidateAll } from '$app/navigation';
 | 
					  import { afterNavigate, goto, invalidateAll } from '$app/navigation';
 | 
				
			||||||
  import { page } from '$app/stores';
 | 
					  import { page } from '$app/stores';
 | 
				
			||||||
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
 | 
				
			||||||
  import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
 | 
					  import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
 | 
				
			||||||
 | 
					  import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
 | 
				
			||||||
 | 
					  import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
 | 
				
			||||||
 | 
					  import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
 | 
				
			||||||
 | 
					  import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
 | 
				
			||||||
 | 
					  import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
 | 
				
			||||||
 | 
					  import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
 | 
				
			||||||
 | 
					  import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
 | 
				
			||||||
 | 
					  import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
 | 
				
			||||||
 | 
					  import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
 | 
				
			||||||
 | 
					  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
 | 
				
			||||||
 | 
					  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
				
			||||||
  import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
 | 
					  import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
 | 
				
			||||||
  import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
 | 
					  import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
 | 
				
			||||||
 | 
					  import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
 | 
				
			||||||
  import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
 | 
					  import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
 | 
				
			||||||
  import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
 | 
					  import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
 | 
				
			||||||
  import { AppRoute, QueryParameter } from '$lib/constants';
 | 
					  import { AppRoute, QueryParameter } from '$lib/constants';
 | 
				
			||||||
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import type { Viewport } from '$lib/stores/assets-store.svelte';
 | 
					  import type { Viewport } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { foldersStore } from '$lib/stores/folders.svelte';
 | 
					  import { foldersStore } from '$lib/stores/folders.svelte';
 | 
				
			||||||
 | 
					  import { preferences } from '$lib/stores/user.store';
 | 
				
			||||||
 | 
					  import { cancelMultiselect } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
 | 
					  import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
 | 
				
			||||||
 | 
					  import type { AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
 | 
					  import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
  import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
 | 
					 | 
				
			||||||
  import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
 | 
					 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					 | 
				
			||||||
  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
					 | 
				
			||||||
  import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
 | 
					 | 
				
			||||||
  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
					 | 
				
			||||||
  import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
 | 
					 | 
				
			||||||
  import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
 | 
					 | 
				
			||||||
  import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
 | 
					 | 
				
			||||||
  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
 | 
					 | 
				
			||||||
  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
					 | 
				
			||||||
  import { preferences } from '$lib/stores/user.store';
 | 
					 | 
				
			||||||
  import { cancelMultiselect } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
  import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
 | 
					 | 
				
			||||||
  import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
 | 
					 | 
				
			||||||
  import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
 | 
					 | 
				
			||||||
  import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
 | 
					 | 
				
			||||||
  import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    data: PageData;
 | 
					    data: PageData;
 | 
				
			||||||
@ -46,7 +47,7 @@
 | 
				
			|||||||
  let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
 | 
					  let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
 | 
				
			||||||
  let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
 | 
					  let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<AssetResponseDto>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onMount(async () => {
 | 
					  onMount(async () => {
 | 
				
			||||||
    await foldersStore.fetchUniquePaths();
 | 
					    await foldersStore.fetchUniquePaths();
 | 
				
			||||||
 | 
				
			|||||||
@ -4,16 +4,16 @@
 | 
				
			|||||||
  import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
 | 
					  import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
 | 
				
			||||||
  import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
 | 
					  import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
 | 
				
			||||||
  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
					  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 | 
				
			||||||
  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
					 | 
				
			||||||
  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
					  import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
				
			||||||
  import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 | 
					  import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 | 
				
			||||||
  import { AppRoute } from '$lib/constants';
 | 
					  import { AppRoute } from '$lib/constants';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					 | 
				
			||||||
  import { onDestroy } from 'svelte';
 | 
					 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					 | 
				
			||||||
  import { mdiPlus, mdiArrowLeft } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					  import { mdiArrowLeft, mdiPlus } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { onDestroy } from 'svelte';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    data: PageData;
 | 
					    data: PageData;
 | 
				
			||||||
@ -24,7 +24,7 @@
 | 
				
			|||||||
  const assetStore = new AssetStore();
 | 
					  const assetStore = new AssetStore();
 | 
				
			||||||
  $effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true }));
 | 
					  $effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true }));
 | 
				
			||||||
  onDestroy(() => assetStore.destroy());
 | 
					  onDestroy(() => assetStore.destroy());
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleEscape = () => {
 | 
					  const handleEscape = () => {
 | 
				
			||||||
    if (assetInteraction.selectionActive) {
 | 
					    if (assetInteraction.selectionActive) {
 | 
				
			||||||
 | 
				
			|||||||
@ -33,20 +33,14 @@
 | 
				
			|||||||
  import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
 | 
					  import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
					  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
 | 
					  import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { preferences } from '$lib/stores/user.store';
 | 
					  import { preferences } from '$lib/stores/user.store';
 | 
				
			||||||
  import { websocketEvents } from '$lib/stores/websocket';
 | 
					  import { websocketEvents } from '$lib/stores/websocket';
 | 
				
			||||||
  import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
 | 
					  import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { isExternalUrl } from '$lib/utils/navigation';
 | 
					  import { isExternalUrl } from '$lib/utils/navigation';
 | 
				
			||||||
  import {
 | 
					  import { getPersonStatistics, mergePerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk';
 | 
				
			||||||
    getPersonStatistics,
 | 
					 | 
				
			||||||
    mergePerson,
 | 
					 | 
				
			||||||
    searchPerson,
 | 
					 | 
				
			||||||
    updatePerson,
 | 
					 | 
				
			||||||
    type AssetResponseDto,
 | 
					 | 
				
			||||||
    type PersonResponseDto,
 | 
					 | 
				
			||||||
  } from '@immich/sdk';
 | 
					 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
    mdiAccountBoxOutline,
 | 
					    mdiAccountBoxOutline,
 | 
				
			||||||
    mdiAccountMultipleCheckOutline,
 | 
					    mdiAccountMultipleCheckOutline,
 | 
				
			||||||
@ -59,11 +53,10 @@
 | 
				
			|||||||
    mdiHeartOutline,
 | 
					    mdiHeartOutline,
 | 
				
			||||||
    mdiPlus,
 | 
					    mdiPlus,
 | 
				
			||||||
  } from '@mdi/js';
 | 
					  } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { DateTime } from 'luxon';
 | 
				
			||||||
  import { onDestroy, onMount } from 'svelte';
 | 
					  import { onDestroy, onMount } from 'svelte';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
  import { locale } from '$lib/stores/preferences.store';
 | 
					 | 
				
			||||||
  import { DateTime } from 'luxon';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    data: PageData;
 | 
					    data: PageData;
 | 
				
			||||||
@ -78,7 +71,7 @@
 | 
				
			|||||||
  $effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id }));
 | 
					  $effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id }));
 | 
				
			||||||
  onDestroy(() => assetStore.destroy());
 | 
					  onDestroy(() => assetStore.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
 | 
					  let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
 | 
				
			||||||
  let isEditingName = $state(false);
 | 
					  let isEditingName = $state(false);
 | 
				
			||||||
@ -202,7 +195,7 @@
 | 
				
			|||||||
    data = { ...data, person };
 | 
					    data = { ...data, person };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => {
 | 
					  const handleSelectFeaturePhoto = async (asset: TimelineAsset) => {
 | 
				
			||||||
    if (viewMode !== PersonPageViewMode.SELECT_PERSON) {
 | 
					    if (viewMode !== PersonPageViewMode.SELECT_PERSON) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,7 @@
 | 
				
			|||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
					  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
 | 
					  import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
 | 
				
			||||||
  import { preferences, user } from '$lib/stores/user.store';
 | 
					  import { preferences, user } from '$lib/stores/user.store';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
@ -32,7 +32,7 @@
 | 
				
			|||||||
    type OnUnlink,
 | 
					    type OnUnlink,
 | 
				
			||||||
  } from '$lib/utils/actions';
 | 
					  } from '$lib/utils/actions';
 | 
				
			||||||
  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
					  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
  import { AssetTypeEnum } from '@immich/sdk';
 | 
					
 | 
				
			||||||
  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
 | 
					  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
 | 
				
			||||||
  import { onDestroy } from 'svelte';
 | 
					  import { onDestroy } from 'svelte';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
@ -42,7 +42,7 @@
 | 
				
			|||||||
  void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
 | 
					  void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
 | 
				
			||||||
  onDestroy(() => assetStore.destroy());
 | 
					  onDestroy(() => assetStore.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let selectedAssets = $derived(assetInteraction.selectedAssets);
 | 
					  let selectedAssets = $derived(assetInteraction.selectedAssets);
 | 
				
			||||||
  let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
 | 
					  let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
 | 
				
			||||||
@ -50,8 +50,8 @@
 | 
				
			|||||||
    const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
 | 
					    const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
 | 
				
			||||||
    const isLivePhotoCandidate =
 | 
					    const isLivePhotoCandidate =
 | 
				
			||||||
      selectedAssets.length === 2 &&
 | 
					      selectedAssets.length === 2 &&
 | 
				
			||||||
      selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) &&
 | 
					      selectedAssets.some((asset) => asset.isImage) &&
 | 
				
			||||||
      selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video);
 | 
					      selectedAssets.some((asset) => asset.isVideo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
 | 
					    return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,7 @@
 | 
				
			|||||||
  let scrollY = $state(0);
 | 
					  let scrollY = $state(0);
 | 
				
			||||||
  let scrollYHistory = 0;
 | 
					  let scrollYHistory = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<AssetResponseDto>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
 | 
					  type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
 | 
				
			||||||
  let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
 | 
					  let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
 | 
				
			||||||
 | 
				
			|||||||
@ -17,14 +17,14 @@
 | 
				
			|||||||
  import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
 | 
					  import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
 | 
				
			||||||
  import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
 | 
					  import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
 | 
					  import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
 | 
				
			||||||
  import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
 | 
					  import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { Button, HStack, Text } from '@immich/ui';
 | 
					  import { Button, HStack, Text } from '@immich/ui';
 | 
				
			||||||
  import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
 | 
					  import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { onDestroy } from 'svelte';
 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
  import { onDestroy } from 'svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    data: PageData;
 | 
					    data: PageData;
 | 
				
			||||||
@ -35,7 +35,7 @@
 | 
				
			|||||||
  let pathSegments = $derived(data.path ? data.path.split('/') : []);
 | 
					  let pathSegments = $derived(data.path ? data.path.split('/') : []);
 | 
				
			||||||
  let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
 | 
					  let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const buildMap = (tags: TagResponseDto[]) => {
 | 
					  const buildMap = (tags: TagResponseDto[]) => {
 | 
				
			||||||
    return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
 | 
					    return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@
 | 
				
			|||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
  import { AppRoute } from '$lib/constants';
 | 
					  import { AppRoute } from '$lib/constants';
 | 
				
			||||||
  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
					  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets-store.svelte';
 | 
					  import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
 | 
					  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
 | 
				
			||||||
  import { handlePromiseError } from '$lib/utils';
 | 
					  import { handlePromiseError } from '$lib/utils';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
@ -40,7 +40,7 @@
 | 
				
			|||||||
  void assetStore.updateOptions({ isTrashed: true });
 | 
					  void assetStore.updateOptions({ isTrashed: true });
 | 
				
			||||||
  onDestroy(() => assetStore.destroy());
 | 
					  onDestroy(() => assetStore.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetInteraction = new AssetInteraction();
 | 
					  const assetInteraction = new AssetInteraction<TimelineAsset>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleEmptyTrash = async () => {
 | 
					  const handleEmptyTrash = async () => {
 | 
				
			||||||
    const isConfirmed = await dialogController.show({
 | 
					    const isConfirmed = await dialogController.show({
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
 | 
				
			||||||
import { faker } from '@faker-js/faker';
 | 
					import { faker } from '@faker-js/faker';
 | 
				
			||||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
 | 
					import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
import { Sync } from 'factory.ts';
 | 
					import { Sync } from 'factory.ts';
 | 
				
			||||||
@ -25,3 +26,20 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
 | 
				
			|||||||
  isOffline: Sync.each(() => faker.datatype.boolean()),
 | 
					  isOffline: Sync.each(() => faker.datatype.boolean()),
 | 
				
			||||||
  hasMetadata: Sync.each(() => faker.datatype.boolean()),
 | 
					  hasMetadata: Sync.each(() => faker.datatype.boolean()),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
 | 
				
			||||||
 | 
					  id: Sync.each(() => faker.string.uuid()),
 | 
				
			||||||
 | 
					  ratio: Sync.each(() => faker.number.int()),
 | 
				
			||||||
 | 
					  ownerId: Sync.each(() => faker.string.uuid()),
 | 
				
			||||||
 | 
					  thumbhash: Sync.each(() => faker.string.alphanumeric(28)),
 | 
				
			||||||
 | 
					  localDateTime: Sync.each(() => faker.date.past().toISOString()),
 | 
				
			||||||
 | 
					  isFavorite: Sync.each(() => faker.datatype.boolean()),
 | 
				
			||||||
 | 
					  isArchived: false,
 | 
				
			||||||
 | 
					  isTrashed: false,
 | 
				
			||||||
 | 
					  isImage: true,
 | 
				
			||||||
 | 
					  isVideo: false,
 | 
				
			||||||
 | 
					  duration: '0:00:00.00000',
 | 
				
			||||||
 | 
					  stack: null,
 | 
				
			||||||
 | 
					  projectionType: null,
 | 
				
			||||||
 | 
					  livePhotoVideoId: Sync.each(() => faker.string.uuid()),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user