diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index bdd442e50c..75bdc0f8a6 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -52,7 +52,7 @@ {#if isShowConfirmation} (isShowConfirmation = false)} /> diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte index 89eca9c6a8..1651936c08 100644 --- a/web/src/lib/components/photos-page/actions/download-action.svelte +++ b/web/src/lib/components/photos-page/actions/download-action.svelte @@ -28,7 +28,7 @@ await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }); }; - let menuItemIcon = $derived(getAssets().size === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline); + let menuItemIcon = $derived(getAssets().length === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline); diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte index 19c1e54cfa..e1f7e33baa 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte @@ -23,7 +23,7 @@ const removeFromAlbum = async () => { const isConfirmed = await dialogController.show({ - prompt: $t('remove_assets_album_confirmation', { values: { count: getAssets().size } }), + prompt: $t('remove_assets_album_confirmation', { values: { count: getAssets().length } }), }); if (!isConfirmed) { diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte index e884a929a3..1639a642b5 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte @@ -20,7 +20,7 @@ const handleRemove = async () => { const isConfirmed = await dialogController.show({ title: $t('remove_assets_title'), - prompt: $t('remove_assets_shared_link_confirmation', { values: { count: getAssets().size } }), + prompt: $t('remove_assets_shared_link_confirmation', { values: { count: getAssets().length } }), confirmText: $t('remove'), }); diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 4208fdeb7c..3a9f6356b8 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -1,6 +1,6 @@ @@ -39,8 +38,8 @@ {#snippet leading()}
-

{assets.size}

- +

{assets.length}

+
{/snippet} {#snippet trailing()} diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index 4d20d9df91..de10e62bc5 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -15,7 +15,6 @@ import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte'; import Map from '$lib/components/shared-components/map/map.svelte'; import { get } from 'svelte/store'; - interface Point { lng: number; lat: number; diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 1625c92d3c..a94d6b7c93 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -169,9 +169,9 @@ // Select/deselect already loaded assets if (deselect) { for (const candidate of assetInteraction.assetSelectionCandidates) { - assetInteraction.removeAssetFromMultiselectGroup(candidate); + assetInteraction.removeAssetFromMultiselectGroup(candidate.id); } - assetInteraction.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset.id); } else { for (const candidate of assetInteraction.assetSelectionCandidates) { assetInteraction.selectAsset(candidate); @@ -217,7 +217,7 @@ }; const onDelete = () => { - const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); + const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed); if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { isShowDeleteConfirmation = true; @@ -245,7 +245,7 @@ }; const toggleArchive = async () => { - const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); + const ids = await archiveAssets(assetInteraction.selectedAssets, !assetInteraction.isAllArchived); if (ids) { assets = assets.filter((asset) => !ids.includes(asset.id)); deselectAllAssets(); @@ -407,7 +407,7 @@ }; let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); - let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); + let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id)); $effect(() => { if (!lastAssetMouseEvent) { @@ -438,7 +438,7 @@ {#if isShowDeleteConfirmation} (isShowDeleteConfirmation = false)} onConfirm={() => handlePromiseError(trashOrDelete(true))} /> @@ -480,7 +480,7 @@ {asset} selected={assetInteraction.hasSelectedAsset(asset.id)} selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} - focussed={assetInteraction.isFocussedAsset(asset)} + focussed={assetInteraction.isFocussedAsset(asset.id)} thumbnailWidth={layout.width} thumbnailHeight={layout.height} /> diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts index c940e6cec7..39bf9968c9 100644 --- a/web/src/lib/stores/asset-interaction.svelte.ts +++ b/web/src/lib/stores/asset-interaction.svelte.ts @@ -4,43 +4,43 @@ import { SvelteSet } from 'svelte/reactivity'; import { fromStore } from 'svelte/store'; export class AssetInteraction { - readonly selectedAssets = new SvelteSet(); + selectedAssets = $state([]); hasSelectedAsset(assetId: string) { - return [...this.selectedAssets.values()].some((asset) => asset.id === assetId); + return this.selectedAssets.some((asset) => asset.id === assetId); } - readonly selectedGroup = new SvelteSet(); - assetSelectionCandidates = $state(new SvelteSet()); + selectedGroup = new SvelteSet(); + assetSelectionCandidates = $state([]); hasSelectionCandidate(assetId: string) { - return [...this.assetSelectionCandidates.values()].some((asset) => asset.id === assetId); + return this.assetSelectionCandidates.some((asset) => asset.id === assetId); } assetSelectionStart = $state(null); focussedAssetId = $state(null); - - selectionActive = $derived(this.selectedAssets.size > 0); - selectedAssetsArray = $derived([...this.selectedAssets]); + selectionActive = $derived(this.selectedAssets.length > 0); private user = fromStore(user); private userId = $derived(this.user.current?.id); - isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed)); - isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived)); - isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite)); - isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId)); + isAllTrashed = $derived(this.selectedAssets.every((asset) => asset.isTrashed)); + isAllArchived = $derived(this.selectedAssets.every((asset) => asset.isArchived)); + isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite)); + isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId)); selectAsset(asset: AssetResponseDto) { - this.selectedAssets.add(asset); + if (!this.hasSelectedAsset(asset.id)) { + this.selectedAssets.push(asset); + } } selectAssets(assets: AssetResponseDto[]) { for (const asset of assets) { - this.selectedAssets.add(asset); + this.selectAsset(asset); } } - removeAssetFromMultiselectGroup(asset: AssetResponseDto) { - const selectedAsset = [...this.selectedAssets.values()].find((a) => a.id === asset.id); - if (selectedAsset) { - this.selectedAssets.delete(selectedAsset); + removeAssetFromMultiselectGroup(assetId: string) { + const index = this.selectedAssets.findIndex((a) => a.id == assetId); + if (index !== -1) { + this.selectedAssets.splice(index, 1); } } @@ -57,24 +57,24 @@ export class AssetInteraction { } setAssetSelectionCandidates(assets: AssetResponseDto[]) { - this.assetSelectionCandidates = new SvelteSet(assets); + this.assetSelectionCandidates = assets; } clearAssetSelectionCandidates() { - this.assetSelectionCandidates.clear(); + this.assetSelectionCandidates = []; } clearMultiselect() { // Multi-selection - this.selectedAssets.clear(); + this.selectedAssets = []; this.selectedGroup.clear(); // Range selection - this.assetSelectionCandidates.clear(); + this.assetSelectionCandidates = []; this.assetSelectionStart = null; } - isFocussedAsset(asset: AssetResponseDto) { - return this.focussedAssetId === asset.id; + isFocussedAsset(assetId: string) { + return this.focussedAssetId === assetId; } } diff --git a/web/src/lib/stores/assets-store.svelte.ts b/web/src/lib/stores/assets-store.svelte.ts index 249de56a84..3d6f77a06b 100644 --- a/web/src/lib/stores/assets-store.svelte.ts +++ b/web/src/lib/stores/assets-store.svelte.ts @@ -57,6 +57,13 @@ function updateObject(target: any, source: any): boolean { return updated; } +export function assetSnapshot(asset: AssetResponseDto) { + return $state.snapshot(asset); +} + +export function assetsSnapshot(assets: AssetResponseDto[]) { + return assets.map((a) => $state.snapshot(a)); +} class IntersectingAsset { // --- public --- readonly #group: AssetDateGroup; @@ -284,9 +291,11 @@ export class AssetBucket { get lastDateGroup() { return this.dateGroups.at(-1); } + getFirstAsset() { return this.dateGroups[0]?.getFirstAsset(); } + getAssets() { // eslint-disable-next-line unicorn/no-array-reduce return this.dateGroups.reduce( diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 82112b6dbf..830f2bbfd9 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -5,7 +5,7 @@ import { NotificationType, notificationController } from '$lib/components/shared import { AppRoute } from '$lib/constants'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; -import { 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'; import { preferences } from '$lib/stores/user.store'; import { downloadRequest, getKey, withError } from '$lib/utils'; @@ -367,7 +367,7 @@ export const getAssetType = (type: AssetTypeEnum) => { } }; -export const getSelectedAssets = (assets: Set, user: UserResponseDto | null): string[] => { +export const getSelectedAssets = (assets: AssetResponseDto[], user: UserResponseDto | null): string[] => { 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; @@ -474,15 +474,10 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: await assetStore.loadBucket(bucket.bucketDate); if (!get(isSelectingAllAssets)) { + assetInteraction.clearMultiselect(); break; // Cancelled } - assetInteraction.selectAssets(bucket.getAssets().map((a) => $state.snapshot(a))); - - // We use setTimeout to allow the UI to update. Otherwise, this may - // cause a long delay between the start of 'select all' and the - // effective update of the UI, depending on the number of assets - // to select - await delay(0); + assetInteraction.selectAssets(assetsSnapshot(bucket.getAssets())); } } catch (error) { const $t = get(t); 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 603250aecc..95c3d59b1c 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 @@ -249,7 +249,7 @@ album = await getAlbumInfo({ id: album.id, withoutAssets: true }); }; const handleAddAssets = async () => { - const assetIds = timelineInteraction.selectedAssetsArray.map((asset) => asset.id); + const assetIds = timelineInteraction.selectedAssets.map((asset) => asset.id); try { const results = await addAssetsToAlbum({ @@ -364,7 +364,7 @@ }; const updateThumbnailUsingCurrentSelection = async () => { - if (assetInteraction.selectedAssets.size === 1) { + if (assetInteraction.selectedAssets.length === 1) { const [firstAsset] = assetInteraction.selectedAssets; assetInteraction.clearMultiselect(); await updateThumbnail(firstAsset.id); @@ -479,7 +479,7 @@ {#if assetInteraction.isAllUserOwned} - {#if assetInteraction.selectedAssets.size === 1} + {#if assetInteraction.selectedAssets.length === 1} {/snippet} 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 c41ae7d85c..7bb0b81a42 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 @@ -149,7 +149,7 @@ }); const handleUnmerge = () => { - assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id)); + assetStore.removeAssets(assetInteraction.selectedAssets.map((a) => a.id)); assetInteraction.clearMultiselect(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -368,7 +368,7 @@ {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} a.id)} + assetIds={assetInteraction.selectedAssets.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 131df3126d..7b53c056a0 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -39,7 +39,7 @@ const assetInteraction = new AssetInteraction(); - let selectedAssets = $derived(assetInteraction.selectedAssetsArray); + let selectedAssets = $derived(assetInteraction.selectedAssets); let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack); let isLinkActionAvailable = $derived.by(() => { const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; @@ -97,7 +97,7 @@ > - {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected} + {#if assetInteraction.selectedAssets.length > 1 || isAssetStackSelected} assetStore.removeAssets(assetIds)} @@ -107,7 +107,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 9861cb0add..9955784a4f 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 @@ -85,7 +85,7 @@ } if (assetInteraction.selectionActive) { - assetInteraction.selectedAssets.clear(); + assetInteraction.selectedAssets = []; return; } if (!$preventRaceConditionSearchBar) {