diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index f2e8f9d67b..a8b48993c9 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -54,18 +54,7 @@ $: isOwner = $user && asset.ownerId === $user?.id; - type MenuItemEvent = - | 'addToAlbum' - | 'restoreAsset' - | 'addToSharedAlbum' - | 'asProfileImage' - | 'setAsAlbumCover' - | 'download' - | 'playSlideShow' - | 'runJob' - | 'unstack'; - - const dispatch = createEventDispatcher<{ + type EventTypes = { back: void; stopMotionPhoto: void; playMotionPhoto: void; @@ -83,7 +72,9 @@ playSlideShow: void; unstack: void; showShareModal: void; - }>(); + }; + + const dispatch = createEventDispatcher(); let contextMenuPosition = { x: 0, y: 0 }; let isShowAssetOptions = false; @@ -98,7 +89,7 @@ dispatch('runJob', name); }; - const onMenuClick = (eventName: MenuItemEvent) => { + const onMenuClick = (eventName: keyof EventTypes) => { isShowAssetOptions = false; dispatch(eventName); }; @@ -258,7 +249,7 @@ /> {/if} dispatch('toggleArchive')} + on:click={() => onMenuClick('toggleArchive')} icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline} text={asset.isArchived ? $t('unarchive') : $t('archive')} /> diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index e309d0dbd0..b7da50e69a 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -12,7 +12,13 @@ import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; import { user } from '$lib/stores/user.store'; import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils'; - import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile, unstackAssets } from '$lib/utils/asset-utils'; + import { + addAssetsToAlbum, + addAssetsToNewAlbum, + downloadFile, + unstackAssets, + toggleArchive, + } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { shortcuts } from '$lib/actions/shortcut'; import { SlideshowHistory } from '$lib/utils/slideshow-history'; @@ -433,24 +439,10 @@ } }; - const toggleArchive = async () => { - try { - const data = await updateAsset({ - id: asset.id, - updateAssetDto: { - isArchived: !asset.isArchived, - }, - }); - - asset.isArchived = data.isArchived; - dispatch('action', { type: data.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: data }); - - notificationController.show({ - type: NotificationType.Info, - message: asset.isArchived ? `Added to archive` : `Removed from archive`, - }); - } catch (error) { - handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`); + const toggleAssetArchive = async () => { + const updatedAsset = await toggleArchive(asset); + if (updatedAsset) { + dispatch('action', { type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: asset }); } }; @@ -550,7 +542,7 @@ navigateAsset('previous') }, { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, { shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) }, @@ -594,7 +586,7 @@ on:addToSharedAlbum={() => openAlbumPicker(true)} on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} - on:toggleArchive={toggleArchive} + on:toggleArchive={toggleAssetArchive} on:asProfileImage={() => (isShowProfileImageCrop = true)} on:setAsAlbumCover={handleUpdateThumbnail} on:runJob={({ detail: job }) => handleRunJob(job)} diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte index 969dbf3533..e6384d8569 100644 --- a/web/src/lib/components/photos-page/actions/archive-action.svelte +++ b/web/src/lib/components/photos-page/actions/archive-action.svelte @@ -1,15 +1,10 @@ diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 3535967886..7f56192ce7 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -18,7 +18,7 @@ import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import AssetDateGroup from './asset-date-group.svelte'; - import { stackAssets } from '$lib/utils/asset-utils'; + import { archiveAssets, stackAssets } from '$lib/utils/asset-utils'; import DeleteAssetDialog from './delete-asset-dialog.svelte'; import { handlePromiseError } from '$lib/utils'; import { selectAllAssets } from '$lib/utils/asset-utils'; @@ -48,6 +48,7 @@ $: timelineY = element?.scrollTop || 0; $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0; $: idsSelectedAssets = [...$selectedAssets].map(({ id }) => id); + $: isAllArchived = [...$selectedAssets].every((asset) => asset.isArchived); $: { if (isEmpty) { assetInteractionStore.clearMultiselect(); @@ -106,6 +107,14 @@ } }; + const toggleArchive = async () => { + const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived); + if (ids) { + assetStore.removeAssets(ids); + deselectAllAssets(); + } + }; + const focusElement = () => { if (document.activeElement === document.body) { element.focus(); @@ -132,6 +141,7 @@ { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() }, { shortcut: { key: 's' }, onShortcut: () => onStackAssets() }, + { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive }, ); } diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index e57bdbdd3a..2095f1eb96 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -14,6 +14,7 @@ import { getAssetInfo, getBaseUrl, getDownloadInfo, + updateAsset, updateAssets, type AlbumResponseDto, type AssetResponseDto, @@ -23,6 +24,7 @@ import { type UserResponseDto, } from '@immich/sdk'; import { DateTime } from 'luxon'; +import { t as translate } from 'svelte-i18n'; import { get } from 'svelte/store'; import { handleError } from './handle-error'; @@ -397,6 +399,53 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt } }; +export const toggleArchive = async (asset: AssetResponseDto) => { + try { + const data = await updateAsset({ + id: asset.id, + updateAssetDto: { + isArchived: !asset.isArchived, + }, + }); + + asset.isArchived = data.isArchived; + + notificationController.show({ + type: NotificationType.Info, + message: asset.isArchived ? `Added to archive` : `Removed from archive`, + }); + } catch (error) { + handleError(error, `Unable to ${asset.isArchived ? `remove asset from` : `add asset to`} archive`); + } + + return asset; +}; + +export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean) => { + const isArchived = archive; + const ids = assets.map(({ id }) => id); + + try { + if (ids.length > 0) { + await updateAssets({ assetBulkUpdateDto: { ids, isArchived } }); + } + + for (const asset of assets) { + asset.isArchived = isArchived; + } + + const t = get(translate); + notificationController.show({ + message: `${isArchived ? t('archived') : t('unarchived')} ${ids.length}`, + type: NotificationType.Info, + }); + } catch (error) { + handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`); + } + + return ids; +}; + export const delay = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); };