diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte index fe4f066a0e..ba47f8ca35 100644 --- a/web/src/lib/components/photos-page/actions/stack-action.svelte +++ b/web/src/lib/components/photos-page/actions/stack-action.svelte @@ -18,11 +18,9 @@ const handleStack = async () => { const selectedAssets = [...getOwnedAssets()]; - const ids = await stackAssets(selectedAssets); - if (ids) { - onStack?.(ids); - clearSelect(); - } + const result = await stackAssets(selectedAssets); + onStack?.(result); + clearSelect(); }; const handleUnstack = async () => { diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 0526c7bc75..6e1751b7b3 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -9,7 +9,7 @@ import { isSearchEnabled } from '$lib/stores/search.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { handlePromiseError } from '$lib/utils'; - import { deleteAssets } from '$lib/utils/actions'; + import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; import { navigate } from '$lib/utils/navigation'; import { type ScrubberListener } from '$lib/utils/timeline-util'; @@ -310,11 +310,11 @@ }; const onStackAssets = async () => { - const ids = await stackAssets(assetInteraction.selectedAssets); - if (ids) { - assetStore.removeAssets(ids); - onEscape(); - } + const result = await stackAssets(assetInteraction.selectedAssets); + + updateStackedAssetInTimeline(assetStore, result); + + onEscape(); }; const toggleArchive = async () => { @@ -411,7 +411,7 @@ } case AssetAction.UNSTACK: { - assetStore.addAssets(action.assets); + updateUnstackedAssetInTimeline(assetStore, action.assets); } } }; diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index d4715db729..472f55cbca 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -1,4 +1,6 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; +import type { AssetStore } from '$lib/stores/assets-store.svelte'; +import type { StackResponse } from '$lib/utils/asset-utils'; import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk'; import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; @@ -11,7 +13,7 @@ export type OnUnlink = (assets: { still: AssetResponseDto; motion: AssetResponse export type OnAddToAlbum = (ids: string[], albumId: string) => void; export type OnArchive = (ids: string[], isArchived: boolean) => void; export type OnFavorite = (ids: string[], favorite: boolean) => void; -export type OnStack = (ids: string[]) => void; +export type OnStack = (result: StackResponse) => void; export type OnUnstack = (assets: AssetResponseDto[]) => void; export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { @@ -30,3 +32,46 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: handleError(error, $t('errors.unable_to_delete_assets')); } }; + +/** + * Update the asset stack state in the asset store based on the provided stack response. + * This function updates the stack information so that the icon is shown for the primary asset + * and removes any assets from the timeline that are marked for deletion. + * + * @param {AssetStore} assetStore - The asset store to update. + * @param {StackResponse} stackResponse - The stack response containing the stack and assets to delete. + */ +export function updateStackedAssetInTimeline(assetStore: AssetStore, { stack, toDeleteIds }: StackResponse) { + if (stack != undefined) { + assetStore.updateAssetOperation([stack.primaryAssetId], (asset) => { + asset.stack = { + id: stack.id, + primaryAssetId: stack.primaryAssetId, + assetCount: stack.assets.length, + }; + return { remove: false }; + }); + + assetStore.removeAssets(toDeleteIds); + } +} + +/** + * Update the asset store to reflect the unstacked state of assets. + * This function updates the stack property of each asset to undefined, effectively unstacking them. + * It also adds the unstacked assets back to the asset store. + * + * @param assetStore - The asset store to update. + * @param assets - The array of asset response DTOs to update in the asset store. + */ +export function updateUnstackedAssetInTimeline(assetStore: AssetStore, assets: AssetResponseDto[]) { + assetStore.updateAssetOperation( + assets.map((asset) => asset.id), + (asset) => { + asset.stack = undefined; + return { remove: false }; + }, + ); + + assetStore.addAssets(assets); +} diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 830f2bbfd9..242515b02a 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -4,7 +4,6 @@ import type { InterpolationValues } from '$lib/components/i18n/format-message'; import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; -import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte'; import { downloadManager } from '$lib/stores/download'; import { preferences } from '$lib/stores/user.store'; @@ -12,6 +11,7 @@ import { downloadRequest, getKey, withError } from '$lib/utils'; import { createAlbum } from '$lib/utils/album-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; import { getFormatter } from '$lib/utils/i18n'; +import { navigate } from '$lib/utils/navigation'; import { addAssetsToAlbum as addAssets, createStack, @@ -381,9 +381,14 @@ export const getSelectedAssets = (assets: AssetResponseDto[], user: UserResponse return ids; }; -export const stackAssets = async (assets: AssetResponseDto[], showNotification = true) => { +export type StackResponse = { + stack?: StackResponseDto; + toDeleteIds: string[]; +}; + +export const stackAssets = async (assets: AssetResponseDto[], showNotification = true): Promise => { if (assets.length < 2) { - return false; + return { stack: undefined, toDeleteIds: [] }; } const $t = get(t); @@ -396,7 +401,7 @@ export const stackAssets = async (assets: AssetResponseDto[], showNotification = type: NotificationType.Info, button: { text: $t('view_stack'), - onClick: () => assetViewingStore.setAssetId(stack.primaryAssetId), + onClick: () => navigate({ targetRoute: 'current', assetId: stack.primaryAssetId }), }, }); } @@ -405,10 +410,13 @@ export const stackAssets = async (assets: AssetResponseDto[], showNotification = asset.stack = index === 0 ? { id: stack.id, assetCount: stack.assets.length, primaryAssetId: asset.id } : null; } - return assets.slice(1).map((asset) => asset.id); + return { + stack, + toDeleteIds: assets.slice(1).map((asset) => asset.id), + }; } catch (error) { handleError(error, $t('errors.failed_to_stack_assets')); - return false; + return { stack: undefined, toDeleteIds: [] }; } }; diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 7b53c056a0..34498a60a3 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -25,7 +25,12 @@ import { AssetStore } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { preferences, user } from '$lib/stores/user.store'; - import type { OnLink, OnUnlink } from '$lib/utils/actions'; + import { + updateStackedAssetInTimeline, + updateUnstackedAssetInTimeline, + type OnLink, + type OnUnlink, + } from '$lib/utils/actions'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { AssetTypeEnum } from '@immich/sdk'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; @@ -100,8 +105,8 @@ {#if assetInteraction.selectedAssets.length > 1 || isAssetStackSelected} assetStore.removeAssets(assetIds)} - onUnstack={(assets) => assetStore.addAssets(assets)} + onStack={(result) => updateStackedAssetInTimeline(assetStore, result)} + onUnstack={(assets) => updateUnstackedAssetInTimeline(assetStore, assets)} /> {/if} {#if isLinkActionAvailable}