From 14cce0cba3987402de06cf70494a3df961aa7edf Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 27 Mar 2026 13:48:51 -0400 Subject: [PATCH] refactor: asset select manager (#27327) --- .../components/album-page/album-viewer.svelte | 7 ++-- .../memory-page/memory-viewer.svelte | 5 +-- .../individual-shared-viewer.svelte | 6 +-- .../gallery-viewer/gallery-viewer.svelte | 36 +++++++---------- .../map/MapTimelinePanel.svelte | 16 ++++---- .../lib/components/timeline/Timeline.svelte | 16 ++++---- .../timeline/actions/SelectAllAction.svelte | 39 +++++++------------ .../actions/TimelineKeyboardActions.svelte | 22 +++++------ .../asset-multi-select-manager.svelte.ts | 32 +++++++-------- web/src/lib/utils/asset-utils.ts | 7 +--- .../[[assetId=id]]/+page.svelte | 27 +++++++------ .../[[assetId=id]]/+page.svelte | 9 ++--- .../[[assetId=id]]/+page.svelte | 9 ++--- .../[[assetId=id]]/+page.svelte | 25 +++++------- .../[[assetId=id]]/+page.svelte | 9 ++--- .../[[assetId=id]]/+page.svelte | 7 +--- .../[[assetId=id]]/+page.svelte | 19 ++++----- .../(user)/photos/[[assetId=id]]/+page.svelte | 16 ++++---- .../[[assetId=id]]/+page.svelte | 11 +++--- .../[[assetId=id]]/+page.svelte | 8 ++-- .../[[assetId=id]]/+page.svelte | 9 ++--- .../(user)/utilities/geolocation/+page.svelte | 25 +++++------- 22 files changed, 148 insertions(+), 212 deletions(-) diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index b66863ad77..448b7f84b5 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -15,7 +15,6 @@ import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { handlePromiseError } from '$lib/utils'; - import { cancelMultiselect } from '$lib/utils/asset-utils'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; import { ActionButton, IconButton, Logo } from '@immich/ui'; @@ -66,7 +65,7 @@ shortcut: { key: 'Escape' }, onShortcut: () => { if (!assetViewerManager.isViewing && assetMultiSelectManager.selectionActive) { - cancelMultiselect(assetMultiSelectManager); + assetMultiSelectManager.clear(); } }, }} @@ -100,8 +99,8 @@ {#if assetMultiSelectManager.selectionActive} assetMultiSelectManager.clearMultiselect()} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > {#if sharedLink.allowDownload} diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index ad0b48b42f..9e24a0a050 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -29,7 +29,6 @@ import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; import { getAssetMediaUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils'; - import { cancelMultiselect } from '$lib/utils/asset-utils'; import { fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetMediaSize, AssetTypeEnum, getAssetInfo } from '@immich/sdk'; import { ActionButton, IconButton, toastManager } from '@immich/ui'; @@ -339,8 +338,8 @@
cancelMultiselect(assetMultiSelectManager)} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 09a57a85b6..388fa15a23 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -12,7 +12,7 @@ import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte'; import { handlePromiseError } from '$lib/utils'; - import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils'; + import { downloadArchive } from '$lib/utils/asset-utils'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; import { toTimelineAsset } from '$lib/utils/timeline-util'; @@ -81,8 +81,8 @@
{#if assetMultiSelectManager.selectionActive} cancelMultiselect(assetMultiSelectManager)} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > toTimelineAsset(a))); }; - const deselectAllAssets = () => { - cancelMultiselect(assetInteraction); - }; - const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Shift') { event.preventDefault(); @@ -153,18 +143,18 @@ // Select/deselect already loaded assets if (deselect) { - for (const candidate of assetInteraction.assetSelectionCandidates) { + for (const candidate of assetInteraction.candidates) { assetInteraction.removeAssetFromMultiselectGroup(candidate.id); } assetInteraction.removeAssetFromMultiselectGroup(asset.id); } else { - for (const candidate of assetInteraction.assetSelectionCandidates) { + for (const candidate of assetInteraction.candidates) { assetInteraction.selectAsset(candidate); } assetInteraction.selectAsset(asset); } - assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.clearCandidates(); assetInteraction.setAssetSelectionStart(deselect ? null : asset); }; @@ -202,13 +192,13 @@ }; const onDelete = () => { - const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed); + const hasTrashedAsset = assetInteraction.assets.some((asset) => asset.isTrashed); handlePromiseError(trashOrDelete(hasTrashedAsset)); }; const trashOrDelete = async (force: boolean = false) => { const forceOrNoTrash = force || !featureFlagsManager.value.trash; - const selectedAssets = assetInteraction.selectedAssets; + const selectedAssets = assetInteraction.assets; if ($showDeleteModal && forceOrNoTrash) { const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: selectedAssets.length }); @@ -224,17 +214,17 @@ onReload, ); - assetInteraction.clearMultiselect(); + assetInteraction.clear(); }; const toggleArchive = async () => { const ids = await archiveAssets( - assetInteraction.selectedAssets, + assetInteraction.assets, assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive, ); if (ids) { assets = assets.filter((asset) => !ids.includes(asset.id)); - deselectAllAssets(); + assetInteraction.clear(); } }; @@ -274,8 +264,8 @@ if (assetInteraction.selectionActive) { shortcuts.push( - { shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets }, - { shortcut: { key: 'D', ctrl: true }, onShortcut: deselectAllAssets }, + { shortcut: { key: 'Escape' }, onShortcut: () => assetInteraction.clear() }, + { shortcut: { key: 'D', ctrl: true }, onShortcut: () => assetInteraction.clear() }, ); if (allowDeletion) { shortcuts.push( @@ -335,13 +325,13 @@ $effect(() => { if (!lastAssetMouseEvent) { - assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.clearCandidates(); } }); $effect(() => { if (!shiftKeyIsDown) { - assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.clearCandidates(); } }); diff --git a/web/src/lib/components/shared-components/map/MapTimelinePanel.svelte b/web/src/lib/components/shared-components/map/MapTimelinePanel.svelte index 094fee241d..552430238d 100644 --- a/web/src/lib/components/shared-components/map/MapTimelinePanel.svelte +++ b/web/src/lib/components/shared-components/map/MapTimelinePanel.svelte @@ -45,7 +45,7 @@ let { bbox, selectedClusterIds, assetCount, onClose }: Props = $props(); let timelineManager = $state() as TimelineManager; - let selectedAssets = $derived(assetMultiSelectManager.selectedAssets); + let selectedAssets = $derived(assetMultiSelectManager.assets); let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack); let isLinkActionAvailable = $derived.by(() => { const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; @@ -69,11 +69,11 @@ const handleSetVisibility = (assetIds: string[]) => { timelineManager.removeAssets(assetIds); - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; const handleEscape = () => { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; const timelineBoundingBox = $derived( @@ -90,7 +90,7 @@ $effect.pre(() => { void timelineOptions; - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }); @@ -124,8 +124,8 @@ assetMultiSelectManager.clearMultiselect()} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > @@ -139,7 +139,7 @@ - {#if assetMultiSelectManager.selectedAssets.length > 1 || isAssetStackSelected} + {#if assetMultiSelectManager.assets.length > 1 || isAssetStackSelected} updateStackedAssetInTimeline(timelineManager, result)} @@ -149,7 +149,7 @@ {#if isLinkActionAvailable} diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index fad77895de..eb237cc7af 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -404,7 +404,7 @@ } } - assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.selectedAssets.length; + assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.assets.length; }; const onSelectAssets = async (asset: TimelineAsset) => { @@ -413,23 +413,23 @@ } onSelect(asset); - const rangeSelection = assetInteraction.assetSelectionCandidates.length > 0; + const rangeSelection = assetInteraction.candidates.length > 0; const deselect = assetInteraction.hasSelectedAsset(asset.id); // Select/deselect already loaded assets if (deselect) { - for (const candidate of assetInteraction.assetSelectionCandidates) { + for (const candidate of assetInteraction.candidates) { assetInteraction.removeAssetFromMultiselectGroup(candidate.id); } assetInteraction.removeAssetFromMultiselectGroup(asset.id); } else { - for (const candidate of assetInteraction.assetSelectionCandidates) { + for (const candidate of assetInteraction.candidates) { handleSelectAsset(candidate); } handleSelectAsset(asset); } - assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.clearCandidates(); if (assetInteraction.startAsset && rangeSelection) { const startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.startAsset.id); @@ -498,13 +498,13 @@ $effect(() => { if (!lastAssetMouseEvent) { - assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.clearCandidates(); } }); $effect(() => { if (!shiftKeyIsDown) { - assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.clearCandidates(); } }); @@ -539,7 +539,7 @@ assetInteraction.removeGroupFromMultiselectGroup(groupTitle); } - assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.selectedAssets.length; + assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.assets.length; }; const _onClick = ( diff --git a/web/src/lib/components/timeline/actions/SelectAllAction.svelte b/web/src/lib/components/timeline/actions/SelectAllAction.svelte index b06382c325..9a6cb05a8f 100644 --- a/web/src/lib/components/timeline/actions/SelectAllAction.svelte +++ b/web/src/lib/components/timeline/actions/SelectAllAction.svelte @@ -1,46 +1,33 @@ {#if withText} - + {:else} - + {/if} diff --git a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte index 3701c4157e..d6cb1c170b 100644 --- a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte +++ b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte @@ -19,7 +19,7 @@ import { searchStore } from '$lib/stores/search.svelte'; import { handlePromiseError } from '$lib/utils'; import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions'; - import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; + import { archiveAssets, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; import { AssetVisibility } from '@immich/sdk'; import { isModalOpen, modalManager } from '@immich/ui'; @@ -34,7 +34,7 @@ const trashOrDelete = async (forceRequested?: boolean) => { const force = forceRequested || !featureFlagsManager.value.trash; - const selectedAssets = assetInteraction.selectedAssets; + const selectedAssets = assetInteraction.assets; if ($showDeleteModal && force) { const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: selectedAssets.length }); @@ -52,16 +52,16 @@ selectedAssets, force ? undefined : (assets) => timelineManager.upsertAssets(assets), ); - assetInteraction.clearMultiselect(); + assetInteraction.clear(); }; const onDelete = () => { - const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed); + const hasTrashedAsset = assetInteraction.assets.some((asset) => asset.isTrashed); handlePromiseError(trashOrDelete(hasTrashedAsset)); }; const onStackAssets = async () => { - const result = await stackAssets(assetInteraction.selectedAssets); + const result = await stackAssets(assetInteraction.assets); updateStackedAssetInTimeline(timelineManager, result); @@ -70,18 +70,14 @@ const toggleArchive = async () => { const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive; - const ids = await archiveAssets(assetInteraction.selectedAssets, visibility); + const ids = await archiveAssets(assetInteraction.assets, visibility); timelineManager.update(ids, (asset) => (asset.visibility = visibility)); eventManager.emit('AssetsArchive', ids); - deselectAllAssets(); + assetInteraction.clear(); }; let shiftKeyIsDown = $state(false); - const deselectAllAssets = () => { - cancelMultiselect(assetInteraction); - }; - const onKeyDown = (event: KeyboardEvent) => { if (searchStore.isSearchEnabled) { return; @@ -125,7 +121,7 @@ $effect(() => { if (isEmpty) { - assetInteraction.clearMultiselect(); + assetInteraction.clear(); } }); @@ -166,7 +162,7 @@ shortcuts.push( { shortcut: { key: 'Delete' }, onShortcut: onDelete }, { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, - { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() }, + { shortcut: { key: 'D', ctrl: true }, onShortcut: () => assetInteraction.clear() }, { shortcut: { key: 's' }, onShortcut: () => onStackAssets() }, { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive }, ); diff --git a/web/src/lib/managers/asset-multi-select-manager.svelte.ts b/web/src/lib/managers/asset-multi-select-manager.svelte.ts index 935bc1adfa..ec282f659a 100644 --- a/web/src/lib/managers/asset-multi-select-manager.svelte.ts +++ b/web/src/lib/managers/asset-multi-select-manager.svelte.ts @@ -19,21 +19,21 @@ export class AssetMultiSelectManager { selectedGroup = new SvelteSet(); - assetSelectionCandidates = $state([]); + candidates = $state([]); selectionActive = $derived(this.#selectedMap.size > 0); - selectedAssets = $derived(Array.from(this.#selectedMap.values())); - isAllTrashed = $derived(this.selectedAssets.every((asset) => asset.isTrashed)); - isAllArchived = $derived(this.selectedAssets.every((asset) => asset.visibility === AssetVisibility.Archive)); - isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite)); - isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.#userId)); + assets = $derived(Array.from(this.#selectedMap.values())); + isAllTrashed = $derived(this.assets.every((asset) => asset.isTrashed)); + isAllArchived = $derived(this.assets.every((asset) => asset.visibility === AssetVisibility.Archive)); + isAllFavorite = $derived(this.assets.every((asset) => asset.isFavorite)); + isAllUserOwned = $derived(this.assets.every((asset) => asset.ownerId === this.#userId)); #unsubscribe?: () => void; constructor(options?: AssetMultiSelectOptions) { const { resetOnNavigate = false } = options ?? {}; if (resetOnNavigate) { - this.#unsubscribe = eventManager.on({ AppNavigate: () => this.clearMultiselect() }); + this.#unsubscribe = eventManager.on({ AppNavigate: () => this.clear() }); } } @@ -43,9 +43,9 @@ export class AssetMultiSelectManager { asControlContext(): AssetControlContext { return { - getOwnedAssets: () => this.selectedAssets.filter((asset) => asset.ownerId === this.#userId), - getAssets: () => this.selectedAssets, - clearSelect: () => this.clearMultiselect(), + getOwnedAssets: () => this.assets.filter((asset) => asset.ownerId === this.#userId), + getAssets: () => this.assets, + clearSelect: () => this.clear(), }; } @@ -54,7 +54,7 @@ export class AssetMultiSelectManager { } hasSelectionCandidate(assetId: string) { - return this.assetSelectionCandidates.some((asset) => asset.id === assetId); + return this.candidates.some((asset) => asset.id === assetId); } selectAsset(asset: TimelineAsset) { @@ -84,14 +84,14 @@ export class AssetMultiSelectManager { } setAssetSelectionCandidates(assets: TimelineAsset[]) { - this.assetSelectionCandidates = assets; + this.candidates = assets; } - clearAssetSelectionCandidates() { - this.assetSelectionCandidates = []; + clearCandidates() { + this.candidates = []; } - clearMultiselect() { + clear() { this.selectAll = false; // Multi-selection @@ -99,7 +99,7 @@ export class AssetMultiSelectManager { this.selectedGroup.clear(); // Range selection - this.assetSelectionCandidates = []; + this.candidates = []; this.startAsset = null; } } diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 37ed78ef6e..9e9cf57726 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -402,7 +402,7 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt } if (!assetInteraction.selectAll) { - assetInteraction.clearMultiselect(); + assetInteraction.clear(); break; // Cancelled } assetInteraction.selectAssets([...monthGroup.assetsIterator()]); @@ -418,11 +418,6 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt } }; -export const cancelMultiselect = (assetInteraction: AssetMultiSelectManager) => { - assetInteraction.selectAll = false; - assetInteraction.clearMultiselect(); -}; - export const toggleArchive = async (asset: AssetResponseDto) => { const $t = get(t); try { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index a2811c399c..60372f5ad4 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -49,7 +49,6 @@ import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { preferences, user } from '$lib/stores/user.store'; import { handlePromiseError } from '$lib/utils'; - import { cancelMultiselect } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { isAlbumsRoute, navigate, type AssetGridRouteSearchParams } from '$lib/utils/navigation'; import { AlbumUserRole, AssetVisibility, getAlbumInfo, updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk'; @@ -127,7 +126,7 @@ return; } if (assetMultiSelectManager.selectionActive) { - cancelMultiselect(assetMultiSelectManager); + assetMultiSelectManager.clear(); return; } await goto(Route.albums()); @@ -148,13 +147,13 @@ }; const handleCloseSelectAssets = async () => { - timelineMultiSelectManager.clearMultiselect(); + timelineMultiSelectManager.clear(); await setModeToView(); }; const handleSetVisibility = (assetIds: string[]) => { timelineManager.removeAssets(assetIds); - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; const handleRemoveAssets = async (assetIds: string[]) => { @@ -175,13 +174,13 @@ await updateThumbnail(assetId); viewMode = AlbumPageViewMode.VIEW; - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; const updateThumbnailUsingCurrentSelection = async () => { - if (assetMultiSelectManager.selectedAssets.length === 1) { - const [firstAsset] = assetMultiSelectManager.selectedAssets; - assetMultiSelectManager.clearMultiselect(); + if (assetMultiSelectManager.assets.length === 1) { + const [firstAsset] = assetMultiSelectManager.assets; + assetMultiSelectManager.clear(); await updateThumbnail(firstAsset.id); } }; @@ -290,7 +289,7 @@ } await refreshAlbum(); - timelineMultiSelectManager.clearMultiselect(); + timelineMultiSelectManager.clear(); await setModeToView(); }; @@ -312,7 +311,7 @@ const { Cast } = $derived(getGlobalActions($t)); const { Share } = $derived(getAlbumActions($t, album)); - const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineMultiSelectManager.selectedAssets)); + const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineMultiSelectManager.assets)); const Close = $derived({ title: $t('go_back'), @@ -453,8 +452,8 @@ {#if assetMultiSelectManager.selectionActive} assetMultiSelectManager.clearMultiselect()} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} @@ -480,7 +479,7 @@ /> {/if} - {#if assetMultiSelectManager.selectedAssets.length === 1} + {#if assetMultiSelectManager.assets.length === 1} {/snippet} diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index d28ec38e0f..0b2df6079c 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,14 +32,14 @@ const handleEscape = () => { if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } }; const handleSetVisibility = (assetIds: string[]) => { timelineManager.removeAssets(assetIds); - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; @@ -59,10 +59,7 @@ {#if assetMultiSelectManager.selectionActive} - assetMultiSelectManager.clearMultiselect()} - > + assetMultiSelectManager.clear()}> {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} { if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } }; const handleSetVisibility = (assetIds: string[]) => { timelineManager.removeAssets(assetIds); - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; @@ -63,10 +63,7 @@ {#if assetMultiSelectManager.selectionActive} - assetMultiSelectManager.clearMultiselect()} - > + assetMultiSelectManager.clear()}> {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} timelineManager.removeAssets(assetIds)} /> diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6fd6e30c8a..264f36fdff 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -25,7 +25,6 @@ import { getAssetBulkActions } from '$lib/services/asset.service'; import { foldersStore } from '$lib/stores/folders.svelte'; import { preferences } from '$lib/stores/user.store'; - import { cancelMultiselect } from '$lib/utils/asset-utils'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { joinPaths } from '$lib/utils/tree-utils'; import { ActionButton, CommandPaletteDefaultProvider, IconButton, Text } from '@immich/ui'; @@ -45,30 +44,29 @@ const getLinkForPath = (path: string) => Route.folders({ path }); - afterNavigate(function clearAssetSelection() { - // Clear the asset selection when we navigate (like going to another folder) - cancelMultiselect(assetMultiSelectManager); + afterNavigate(() => { + assetMultiSelectManager.clear(); }); - function navigateToView(path: string) { + const navigateToView = (path: string) => { return goto(getLinkForPath(path), { keepFocus: true, noScroll: true }); - } + }; - async function triggerAssetUpdate() { - cancelMultiselect(assetMultiSelectManager); + const triggerAssetUpdate = async () => { + assetMultiSelectManager.clear(); if (data.tree.path) { await foldersStore.refreshAssetsByPath(data.tree.path); } await invalidateAll(); - } + }; - function handleSelectAllAssets() { + const handleSelectAllAssets = () => { if (!data.pathAssets) { return; } assetMultiSelectManager.selectAssets(data.pathAssets.map((asset) => toTimelineAsset(asset))); - } + }; @@ -112,10 +110,7 @@ {#if assetMultiSelectManager.selectionActive}
- cancelMultiselect(assetMultiSelectManager)} - > + assetMultiSelectManager.clear()}> {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8c0059bf17..a0d2992409 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -33,13 +33,13 @@ const handleEscape = () => { if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } }; const handleMoveOffLockedFolder = (assetIds: string[]) => { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); timelineManager.removeAssets(assetIds); }; @@ -74,10 +74,7 @@ {#if assetMultiSelectManager.selectionActive} - assetMultiSelectManager.clearMultiselect()} - > + assetMultiSelectManager.clear()}> diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index ef1b1bc8b1..dbe9276c4b 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -28,7 +28,7 @@ const handleEscape = () => { if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } }; @@ -39,10 +39,7 @@ {#if assetMultiSelectManager.selectionActive} - assetMultiSelectManager.clearMultiselect()} - > + assetMultiSelectManager.clear()}> {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 259aaefbe0..611ea2ccd8 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -26,13 +26,13 @@ import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte'; import { PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; + import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte'; import { Route } from '$lib/route'; import { getAssetBulkActions } from '$lib/services/asset.service'; import { getPersonActions } from '$lib/services/person.service'; - import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { locale } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; @@ -106,7 +106,7 @@ const handleEscape = async () => { if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } @@ -126,8 +126,8 @@ }); const handleUnmerge = () => { - timelineManager.removeAssets(assetMultiSelectManager.selectedAssets.map((a) => a.id)); - assetMultiSelectManager.clearMultiselect(); + timelineManager.removeAssets(assetMultiSelectManager.assets.map((a) => a.id)); + assetMultiSelectManager.clear(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -153,7 +153,7 @@ handleError(error, $t('errors.unable_to_set_feature_photo')); } - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -282,7 +282,7 @@ const handleSetVisibility = (assetIds: string[]) => { timelineManager.removeAssets(assetIds); - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; const onPersonUpdate = async (response: PersonResponseDto) => { @@ -458,10 +458,7 @@
{#if assetMultiSelectManager.selectionActive} - assetMultiSelectManager.clearMultiselect()} - > + assetMultiSelectManager.clear()}> {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} @@ -521,7 +518,7 @@ {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} a.id)} + assetIds={assetMultiSelectManager.assets.map((a) => a.id)} personAssets={person} onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} onConfirm={handleUnmerge} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 94ec148e9f..5376804c17 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -19,12 +19,12 @@ import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte'; import { AssetAction } from '$lib/constants'; + import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { memoryManager } from '$lib/managers/memory-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { Route } from '$lib/route'; import { getAssetBulkActions } from '$lib/services/asset.service'; - import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { preferences, user } from '$lib/stores/user.store'; import { getAssetMediaUrl, memoryLaneTitle } from '$lib/utils'; import { @@ -44,7 +44,7 @@ let timelineManager = $state() as TimelineManager; const options = { visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true }; - let selectedAssets = $derived(assetMultiSelectManager.selectedAssets); + let selectedAssets = $derived(assetMultiSelectManager.assets); let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack); let isLinkActionAvailable = $derived.by(() => { const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; @@ -61,7 +61,7 @@ return; } if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } }; @@ -78,7 +78,7 @@ const handleSetVisibility = (assetIds: string[]) => { timelineManager.removeAssets(assetIds); - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; const items = $derived( @@ -114,8 +114,8 @@ {#if assetMultiSelectManager.selectionActive} assetMultiSelectManager.clearMultiselect()} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} @@ -132,7 +132,7 @@ - {#if assetMultiSelectManager.selectedAssets.length > 1 || isAssetStackSelected} + {#if assetMultiSelectManager.assets.length > 1 || isAssetStackSelected} updateStackedAssetInTimeline(timelineManager, result)} @@ -142,7 +142,7 @@ {#if isLinkActionAvailable} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index ed44036e1e..1de18569b0 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -19,15 +19,14 @@ import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { QueryParameter } from '$lib/constants'; + import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import type { Viewport } from '$lib/managers/timeline-manager/types'; import { Route } from '$lib/route'; import { getAssetBulkActions } from '$lib/services/asset.service'; - import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { lang, locale } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; import { handlePromiseError } from '$lib/utils'; - import { cancelMultiselect } from '$lib/utils/asset-utils'; import { parseUtcDate } from '$lib/utils/date-time'; import { handleError } from '$lib/utils/handle-error'; import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; @@ -110,7 +109,7 @@ }; const handleSetVisibility = (assetIds: string[]) => { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); onAssetDelete(assetIds); }; @@ -224,7 +223,7 @@ } const onAlbumAddAssets = ({ assetIds }: { assetIds: string[] }) => { - cancelMultiselect(assetMultiSelectManager); + assetMultiSelectManager.clear(); if (terms.isNotInAlbum) { const assetIdSet = new Set(assetIds); @@ -320,8 +319,8 @@ {#if assetMultiSelectManager.selectionActive}
cancelMultiselect(assetMultiSelectManager)} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index 98f47dd1e0..b79d5d76e1 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -22,11 +22,11 @@ import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; import { AssetAction } from '$lib/constants'; import SkipLink from '$lib/elements/SkipLink.svelte'; + import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { Route } from '$lib/route'; import { getAssetBulkActions } from '$lib/services/asset.service'; import { getTagActions } from '$lib/services/tag.service'; - import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { preferences, user } from '$lib/stores/user.store'; import { joinPaths, TreeNode } from '$lib/utils/tree-utils'; import { getAllTags, type TagResponseDto } from '@immich/sdk'; @@ -56,7 +56,7 @@ const handleSetVisibility = (assetIds: string[]) => { timelineManager.removeAssets(assetIds); - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); }; const onRefresh = async () => { @@ -115,8 +115,8 @@
assetMultiSelectManager.clearMultiselect()} + assets={assetMultiSelectManager.assets} + clearSelect={() => assetMultiSelectManager.clear()} > {@const Actions = getAssetBulkActions($t, assetMultiSelectManager.asControlContext())} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index c4f5f5051e..3c75444f40 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -8,12 +8,12 @@ import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte'; + import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { Route } from '$lib/route'; import { getTrashActions } from '$lib/services/trash.service'; - import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { handlePromiseError } from '$lib/utils'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -33,7 +33,7 @@ const handleEscape = () => { if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } }; @@ -68,10 +68,7 @@ {/if} {#if assetMultiSelectManager.selectionActive} - assetMultiSelectManager.clearMultiselect()} - > + assetMultiSelectManager.clear()}> timelineManager.removeAssets(assetIds)} /> timelineManager.removeAssets(assetIds)} /> diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index 96caa9ba94..5f0bbe8e88 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -4,15 +4,14 @@ import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte'; import { AssetAction } from '$lib/constants'; + import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import GeolocationPointPickerModal from '$lib/modals/GeolocationPointPickerModal.svelte'; import GeolocationUpdateConfirmModal from '$lib/modals/GeolocationUpdateConfirmModal.svelte'; - import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import type { LatLng } from '$lib/types'; - import { cancelMultiselect } from '$lib/utils/asset-utils'; import { setQueryValue } from '$lib/utils/navigation'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetVisibility, getAssetInfo, updateAssets } from '@immich/sdk'; @@ -46,7 +45,7 @@ const confirmed = await modalManager.show(GeolocationUpdateConfirmModal, { point, - assetCount: assetMultiSelectManager.selectedAssets.length, + assetCount: assetMultiSelectManager.assets.length, }); if (!confirmed) { @@ -55,14 +54,14 @@ await updateAssets({ assetBulkUpdateDto: { - ids: assetMultiSelectManager.selectedAssets.map((asset) => asset.id), + ids: assetMultiSelectManager.assets.map((asset) => asset.id), latitude: point.lat, longitude: point.lng, }, }); const updatedAssets = await Promise.all( - assetMultiSelectManager.selectedAssets.map(async (asset) => { + assetMultiSelectManager.assets.map(async (asset) => { const updatedAsset = await getAssetInfo({ ...authManager.params, id: asset.id }); return toTimelineAsset(updatedAsset); }), @@ -70,7 +69,7 @@ timelineManager.upsertAssets(updatedAssets); - handleDeselectAll(); + assetMultiSelectManager.clear(); }; const onKeyDown = (event: KeyboardEvent) => { @@ -78,7 +77,7 @@ event.preventDefault(); } if (event.key === 'Escape' && assetMultiSelectManager.selectionActive) { - cancelMultiselect(assetMultiSelectManager); + assetMultiSelectManager.clear(); } }; const onKeyUp = (event: KeyboardEvent) => { @@ -87,10 +86,6 @@ } }; - const handleDeselectAll = () => { - cancelMultiselect(assetMultiSelectManager); - }; - const handlePickPoint = async () => { const selected = await modalManager.show(GeolocationPointPickerModal, { point }); if (!selected) { @@ -101,7 +96,7 @@ }; const handleEscape = () => { if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clearMultiselect(); + assetMultiSelectManager.clear(); return; } }; @@ -168,7 +163,7 @@ color="secondary" variant="ghost" disabled={!assetMultiSelectManager.selectionActive} - onclick={handleDeselectAll} + onclick={() => assetMultiSelectManager.clear()} > {$t('unselect_all')} @@ -176,11 +171,11 @@ leadingIcon={mdiMapMarkerMultipleOutline} size="small" color="primary" - disabled={assetMultiSelectManager.selectedAssets.length === 0} + disabled={assetMultiSelectManager.assets.length === 0} onclick={() => handleUpdate()} >