diff --git a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte index d1bdeb33e..e017b973a 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte @@ -17,14 +17,14 @@ import { showDeleteModal } from '#lib/stores/preferences.store.ts'; import { handlePromiseError } from '#lib/utils.ts'; import { deleteAssets } from '#lib/utils/actions.ts'; - import { archiveAssets, getNextAsset, getPreviousAsset, navigateToAsset } from '#lib/utils/asset-utils.ts'; + import { getNextAsset, getPreviousAsset, navigateToAsset } from '#lib/utils/asset-utils.ts'; import { moveFocus } from '#lib/utils/focus-util.ts'; import { handleError } from '#lib/utils/handle-error.ts'; import { getJustifiedLayoutFromAssets } from '#lib/utils/layout-utils.ts'; import { navigate } from '#lib/utils/navigation.ts'; import { isTimelineAsset, toTimelineAsset } from '#lib/utils/timeline-util.ts'; import { TUNABLES } from '#lib/utils/tunables.ts'; - import { AssetVisibility, type AssetResponseDto } from '@immich/sdk'; + import { type AssetResponseDto } from '@immich/sdk'; import { modalManager } from '@immich/ui'; import { debounce } from 'lodash-es'; import { t } from 'svelte-i18n'; @@ -206,17 +206,6 @@ assetInteraction.clear(); }; - const toggleArchive = async () => { - const ids = await archiveAssets( - assetInteraction.assets, - assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive, - ); - if (ids) { - assets = assets.filter((asset) => !ids.includes(asset.id)); - assetInteraction.clear(); - } - }; - const focusNextAsset = () => moveFocus((element) => element.dataset.thumbnailFocusContainer !== undefined, 'next'); const focusPreviousAsset = () => moveFocus((element) => element.dataset.thumbnailFocusContainer !== undefined, 'previous'); @@ -260,7 +249,6 @@ shortcuts.push( { shortcut: { key: 'Delete' }, onShortcut: onDelete }, { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, - { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive }, ); } } diff --git a/web/src/lib/components/timeline/actions/ArchiveAction.svelte b/web/src/lib/components/timeline/actions/ArchiveAction.svelte deleted file mode 100644 index 36c008730..000000000 --- a/web/src/lib/components/timeline/actions/ArchiveAction.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - -{#if menuItem} - -{/if} - -{#if !menuItem} - {#if loading} - {}} - /> - {:else} - - {/if} -{/if} diff --git a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte index 3daaf8cc6..2e1b938b8 100644 --- a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte +++ b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte @@ -7,7 +7,6 @@ } from '#lib/components/timeline/actions/focus-actions.ts'; import type { AssetMultiSelectManager } from '#lib/managers/asset-multi-select-manager.svelte.ts'; import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts'; - import { eventManager } from '#lib/managers/event-manager.svelte.ts'; import { featureFlagsManager } from '#lib/managers/feature-flags-manager.svelte.ts'; import { TimelineManager } from '#lib/managers/timeline-manager/timeline-manager.svelte.ts'; import type { TimelineAsset } from '#lib/managers/timeline-manager/types.ts'; @@ -21,8 +20,7 @@ import { searchStore } from '#lib/stores/search.svelte.ts'; import { handlePromiseError } from '#lib/utils.ts'; import { deleteAssets } from '#lib/utils/actions.ts'; - import { archiveAssets, selectAllAssets } from '#lib/utils/asset-utils.ts'; - import { AssetVisibility } from '@immich/sdk'; + import { selectAllAssets } from '#lib/utils/asset-utils.ts'; import { isModalOpen, modalManager } from '@immich/ui'; type Props = { @@ -64,14 +62,6 @@ onEscape?.(); }; - const toggleArchive = async () => { - const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive; - const ids = await archiveAssets(assetInteraction.assets, visibility); - timelineManager.update(ids, (asset) => (asset.visibility = visibility)); - eventManager.emit('AssetsArchive', ids); - assetInteraction.clear(); - }; - const onSelectStart = (e: Event) => { if (!searchStore.isSearchEnabled && assetInteraction.selectionActive && keyboardManager.shift) { e.preventDefault(); @@ -135,7 +125,6 @@ { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, { shortcut: { key: 'D', ctrl: true }, onShortcut: () => assetInteraction.clear() }, { shortcut: { key: 's' }, onShortcut: () => onStackAssets() }, - { shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive }, ); } else { // conflicting shortcuts diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 9a761ae29..522dcfe07 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -25,7 +25,14 @@ import { type TimelineDateTime, type TimelineYearMonth, } from '#lib/utils/timeline-util.ts'; -import { AssetOrder, AssetOrderBy, getAssetInfo, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; +import { + AssetOrder, + AssetOrderBy, + AssetVisibility, + getAssetInfo, + getTimeBuckets, + type AssetResponseDto, +} from '@immich/sdk'; import { clamp, isEqual } from 'lodash-es'; import { SvelteDate, SvelteSet } from 'svelte/reactivity'; import { isMismatched, updateObject } from './internal/utils.svelte'; @@ -122,7 +129,14 @@ export class TimelineManager extends VirtualScrollManager { this.upsertAssets([timelineAsset]); } }, - AssetsUnarchive: (assets) => this.upsertAssets(assets), + AssetsArchive: (assetIds) => this.update(assetIds, (asset) => (asset.visibility = AssetVisibility.Archive)), + AssetsUnarchive: (assets) => { + if (this.#options.visibility === AssetVisibility.Archive) { + this.removeAssets(assets.map((asset) => asset.id)); + } else { + this.upsertAssets(assets); + } + }, AlbumRemoveAssets: ({ assetIds, albumIds }) => { if (this.#options.albumId && albumIds.includes(this.#options.albumId)) { this.removeAssets(assetIds); diff --git a/web/src/lib/services/asset.service.ts b/web/src/lib/services/asset.service.ts index df25f9060..1f18eedf3 100644 --- a/web/src/lib/services/asset.service.ts +++ b/web/src/lib/services/asset.service.ts @@ -3,6 +3,7 @@ import { assetMultiSelectManager } from '#lib/managers/asset-multi-select-manage import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts'; import { authManager } from '#lib/managers/auth-manager.svelte.ts'; import { eventManager } from '#lib/managers/event-manager.svelte.ts'; +import type { TimelineAsset } from '#lib/managers/timeline-manager/types.ts'; import AssetAddToAlbumModal from '#lib/modals/AssetAddToAlbumModal.svelte'; import AssetTagModal from '#lib/modals/AssetTagModal.svelte'; import ProfileImageCropperModal from '#lib/modals/ProfileImageCropperModal.svelte'; @@ -25,6 +26,7 @@ import { getAssetInfo, runAssetJobs, updateAsset, + updateAssets, type AlbumResponseDto, type AssetJobsDto, type AssetResponseDto, @@ -33,6 +35,8 @@ import { modalManager, toastManager, type ActionItem } from '@immich/ui'; import { mdiAccountCircleOutline, mdiAlertOutline, + mdiArchiveArrowDownOutline, + mdiArchiveArrowUpOutline, mdiCogRefreshOutline, mdiCompare, mdiContentCopy, @@ -78,6 +82,19 @@ export const getAssetBulkActions = ($t: MessageFormatter, album?: AlbumResponseD onAction: () => modalManager.show(AssetAddToAlbumModal, { assetIds }), }; + const Archive: ActionItem = { + title: $t('to_archive'), + icon: mdiArchiveArrowDownOutline, + shortcuts: [{ key: 'a', shift: true }], + $if: () => assetMultiSelectManager.isAllUserOwned && !assetMultiSelectManager.isAllArchived, + onAction: async () => { + const assets = ownedAssets.filter((asset) => asset.visibility !== AssetVisibility.Archive); + if (await handleSetVisibility(assets, AssetVisibility.Archive)) { + assetMultiSelectManager.clear(); + } + }, + }; + const CreateSharedLink: ActionItem = { title: $t('share'), icon: mdiShareVariantOutline, @@ -104,6 +121,18 @@ export const getAssetBulkActions = ($t: MessageFormatter, album?: AlbumResponseD shortcuts: { key: 't' }, }; + const Unarchive: ActionItem = { + title: $t('unarchive'), + icon: mdiArchiveArrowUpOutline, + shortcuts: [{ key: 'a', shift: true }], + $if: () => assetMultiSelectManager.isAllUserOwned && assetMultiSelectManager.isAllArchived, + onAction: async () => { + if (await handleSetVisibility(ownedAssets, AssetVisibility.Timeline)) { + assetMultiSelectManager.clear(); + } + }, + }; + const RefreshFacesJob: ActionItem = { title: $t('refresh_faces'), icon: mdiHeadSyncOutline, @@ -131,9 +160,11 @@ export const getAssetBulkActions = ($t: MessageFormatter, album?: AlbumResponseD return { AddToAlbum, + Archive, CreateSharedLink, RemoveFromAlbum, Tag, + Unarchive, RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, @@ -500,6 +531,60 @@ export const handleTagAssets = async (assetIds: string[], tagIds: string[]) => { } }; +const handleSetVisibility = async (assets: TimelineAsset[], visibility: AssetVisibility) => { + const $t = await getFormatter(); + const ids = assets.map((asset) => asset.id); + + try { + await updateAssets({ assetBulkUpdateDto: { ids, visibility } }); + + if (visibility === AssetVisibility.Archive) { + toastManager.primary({ + description: $t('archived_count', { values: { count: assets.length } }), + button: (close) => ({ + label: $t('undo'), + onclick: () => { + close(); + void undoArchiveAssets(assets); + }, + }), + }); + eventManager.emit('AssetsArchive', ids); + } else { + // Timeline + toastManager.primary($t('unarchived_count', { values: { count: assets.length } })); + eventManager.emit('AssetsUnarchive', assets); + } + } catch (error) { + handleError( + error, + $t('errors.unable_to_archive_unarchive', { values: { archived: visibility === AssetVisibility.Archive } }), + ); + return false; + } + + return true; +}; + +const undoArchiveAssets = async (assets: TimelineAsset[]) => { + const $t = await getFormatter(); + try { + const ids = assets.map((a) => a.id); + await updateAssets({ + assetBulkUpdateDto: { ids, visibility: AssetVisibility.Timeline }, + }); + + for (const asset of assets) { + asset.visibility = AssetVisibility.Timeline; + } + toastManager.success($t('unarchived_count', { values: { count: assets.length } })); + eventManager.emit('AssetsUnarchive', assets); + eventManager.emit('AssetsUndoArchive', assets); + } catch (error) { + handleError(error, $t('errors.unable_to_archive_unarchive', { values: { archived: false } })); + } +}; + const handleBulkRemoveAssetsFromAlbum = async (assetIds: string[], album: AlbumResponseDto) => { const $t = await getFormatter(); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index f39bd251b..a1e33d72d 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -337,32 +337,6 @@ const undoArchiveAssets = async (assets: TimelineAsset[]) => { } }; -export const archiveAssets = async (assets: TimelineAsset[], visibility: AssetVisibility) => { - const ids = assets.map(({ id }) => id); - const $t = get(t); - - try { - if (ids.length > 0) { - await updateAssets({ - assetBulkUpdateDto: { ids, visibility }, - }); - } - - if (visibility === AssetVisibility.Archive) { - showUndoArchiveToast($t('archived_count', { values: { count: ids.length } }), assets); - } else { - toastManager.primary($t('unarchived_count', { values: { count: ids.length } })); - } - } catch (error) { - handleError( - error, - $t('errors.unable_to_archive_unarchive', { values: { archived: visibility === AssetVisibility.Archive } }), - ); - } - - return ids; -}; - export const delay = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; 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 61143b6ab..f8fb67347 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 @@ -11,7 +11,6 @@ import MenuOption from '#lib/components/shared-components/context-menu/MenuOption.svelte'; import ControlAppBar from '#lib/components/shared-components/ControlAppBar.svelte'; import UserAvatar from '#lib/components/shared-components/UserAvatar.svelte'; - import ArchiveAction from '#lib/components/timeline/actions/ArchiveAction.svelte'; import ChangeDate from '#lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDescription from '#lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeLocation from '#lib/components/timeline/actions/ChangeLocationAction.svelte'; @@ -415,11 +414,8 @@ - timelineManager.update(ids, (asset) => (asset.visibility = visibility))} - /> + + {/if} {#if assetMultiSelectManager.assets.length === 1} 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 13ee96640..f8276e5e8 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 @@ -2,7 +2,6 @@ import UserPageLayout from '#lib/components/layouts/UserPageLayout.svelte'; import ButtonContextMenu from '#lib/components/shared-components/context-menu/ButtonContextMenu.svelte'; import EmptyPlaceholder from '#lib/components/shared-components/EmptyPlaceholder.svelte'; - import ArchiveAction from '#lib/components/timeline/actions/ArchiveAction.svelte'; import DeleteAssets from '#lib/components/timeline/actions/DeleteAssetsAction.svelte'; import DownloadAction from '#lib/components/timeline/actions/DownloadAction.svelte'; import FavoriteAction from '#lib/components/timeline/actions/FavoriteAction.svelte'; @@ -63,10 +62,7 @@ {const Actions = $derived(getAssetBulkActions($t))} - timelineManager.update(ids, (asset) => (asset.visibility = visibility))} - /> + diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 1e9131d10..cf07c1c17 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -3,7 +3,6 @@ import UserPageLayout from '#lib/components/layouts/UserPageLayout.svelte'; import ButtonContextMenu from '#lib/components/shared-components/context-menu/ButtonContextMenu.svelte'; import EmptyPlaceholder from '#lib/components/shared-components/EmptyPlaceholder.svelte'; - import ArchiveAction from '#lib/components/timeline/actions/ArchiveAction.svelte'; import ChangeDate from '#lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDescription from '#lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeLocation from '#lib/components/timeline/actions/ChangeLocationAction.svelte'; @@ -75,11 +74,8 @@ - timelineManager.update(ids, (asset) => (asset.visibility = visibility))} - /> + + {/snippet} - + @@ -168,7 +167,8 @@ - + + diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/MapTimelinePanel.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/MapTimelinePanel.svelte index 18de77a56..df26d7e16 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/MapTimelinePanel.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/MapTimelinePanel.svelte @@ -2,7 +2,6 @@ import ActionMenuItem from '#lib/components/ActionMenuItem.svelte'; import ButtonContextMenu from '#lib/components/shared-components/context-menu/ButtonContextMenu.svelte'; import type { SelectionBBox } from '#lib/components/shared-components/map/types.ts'; - import ArchiveAction from '#lib/components/timeline/actions/ArchiveAction.svelte'; import ChangeDate from '#lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDescription from '#lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeLocation from '#lib/components/timeline/actions/ChangeLocationAction.svelte'; @@ -146,11 +145,8 @@ - timelineManager.update(ids, (asset) => (asset.visibility = visibility))} - /> + + + + {#if assetMultiSelectManager.selectionActive}
@@ -299,7 +301,7 @@ - + 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 a32fdf477..4b82c65dc 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 @@ -8,7 +8,6 @@ import ButtonContextMenu from '#lib/components/shared-components/context-menu/ButtonContextMenu.svelte'; import MenuOption from '#lib/components/shared-components/context-menu/MenuOption.svelte'; import ControlAppBar from '#lib/components/shared-components/ControlAppBar.svelte'; - import ArchiveAction from '#lib/components/timeline/actions/ArchiveAction.svelte'; import ChangeDate from '#lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDescription from '#lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeLocation from '#lib/components/timeline/actions/ChangeLocationAction.svelte'; @@ -500,14 +499,8 @@ - - timelineManager.update(ids, (asset) => { - asset.visibility = visibility; - })} - /> + + - timelineManager.update(ids, (asset) => (asset.visibility = visibility))} - /> + + - timelineManager.update(ids, (asset) => (asset.visibility = visibility))} - /> + + { + if (!terms.visibility || terms.visibility === AssetVisibility.Timeline) { + const assetIdSet = new Set(assetIds); + searchResultAssets = searchResultAssets.filter((asset) => !assetIdSet.has(asset.id)); + } + }; + + // TODO: add back in opposite case + const onAssetsUnarchive = (assets: TimelineAsset[]) => { + if (terms.visibility === AssetVisibility.Archive) { + const assetIdSet = new Set(assets.map((asset) => asset.id)); + searchResultAssets = searchResultAssets.filter((asset) => !assetIdSet.has(asset.id)); + } + }; + function getObjectKeys(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]; } @@ -254,7 +268,7 @@ - + {#if searchTermKeys.length > 0}
@@ -377,7 +391,8 @@ - + + 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 f329853a2..422968461 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 @@ -10,7 +10,6 @@ import Sidebar from '#lib/components/sidebar/Sidebar.svelte'; import AssetSelectControlBar from '#lib/components/timeline/AssetSelectControlBar.svelte'; import Timeline from '#lib/components/timeline/Timeline.svelte'; - import ArchiveAction from '#lib/components/timeline/actions/ArchiveAction.svelte'; import ChangeDate from '#lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDescription from '#lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeLocation from '#lib/components/timeline/actions/ChangeLocationAction.svelte'; @@ -134,10 +133,8 @@ - timelineManager.update(ids, (asset) => (asset.visibility = visibility))} - /> + +