From 1d11106dd09cc07033dd95b59f4f1d82fe1942bb Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 19 Feb 2026 15:27:30 -0500 Subject: [PATCH] refactor: add to album (#26366) --- .../components/asset-viewer/actions/action.ts | 3 +- .../actions/add-to-album-action.svelte | 44 ---------- .../asset-viewer/asset-viewer-nav-bar.svelte | 13 ++- .../asset-viewer/asset-viewer.svelte | 14 +--- .../memory-page/memory-viewer.svelte | 7 +- .../timeline/actions/AddToAlbumAction.svelte | 54 ------------ web/src/lib/constants.ts | 1 - web/src/lib/managers/event-manager.svelte.ts | 2 +- .../lib/modals/AssetAddToAlbumModal.svelte | 27 ++++++ web/src/lib/services/album.service.ts | 82 +++++++++++++++++-- web/src/lib/services/asset.service.ts | 20 ++++- .../lib/stores/asset-interaction.svelte.ts | 9 ++ web/src/lib/utils/asset-utils.ts | 76 ----------------- web/src/lib/utils/file-uploader.ts | 4 +- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 7 +- .../[[assetId=id]]/+page.svelte | 7 +- .../[[assetId=id]]/+page.svelte | 10 +-- .../[[assetId=id]]/+page.svelte | 9 +- .../[[assetId=id]]/+page.svelte | 16 +++- .../(user)/photos/[[assetId=id]]/+page.svelte | 9 +- .../[[assetId=id]]/+page.svelte | 16 ++-- .../[[assetId=id]]/+page.svelte | 8 +- 23 files changed, 202 insertions(+), 242 deletions(-) delete mode 100644 web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte delete mode 100644 web/src/lib/components/timeline/actions/AddToAlbumAction.svelte create mode 100644 web/src/lib/modals/AssetAddToAlbumModal.svelte diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts index 812047e350..df57d73a7e 100644 --- a/web/src/lib/components/asset-viewer/actions/action.ts +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -1,6 +1,6 @@ import type { AssetAction } from '$lib/constants'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; -import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto, StackResponseDto } from '@immich/sdk'; +import type { AssetResponseDto, PersonResponseDto, StackResponseDto } from '@immich/sdk'; type ActionMap = { [AssetAction.ARCHIVE]: { asset: TimelineAsset }; @@ -8,7 +8,6 @@ type ActionMap = { [AssetAction.TRASH]: { asset: TimelineAsset }; [AssetAction.DELETE]: { asset: TimelineAsset }; [AssetAction.RESTORE]: { asset: TimelineAsset }; - [AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto }; [AssetAction.STACK]: { stack: StackResponseDto }; [AssetAction.UNSTACK]: { assets: TimelineAsset[] }; [AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto }; diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte deleted file mode 100644 index cf8ba15024..0000000000 --- a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - - 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 93ce2f01e3..e3b03c3a7b 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 @@ -3,7 +3,6 @@ import ActionButton from '$lib/components/ActionButton.svelte'; import ActionMenuItem from '$lib/components/ActionMenuItem.svelte'; import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action'; - import AddToAlbumAction from '$lib/components/asset-viewer/actions/add-to-album-action.svelte'; import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte'; import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte'; import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte'; @@ -102,6 +101,7 @@ Unfavorite, PlayMotionPhoto, StopMotionPhoto, + AddToAlbum, ZoomIn, ZoomOut, Copy, @@ -129,6 +129,7 @@ Unfavorite, PlayMotionPhoto, StopMotionPhoto, + AddToAlbum, ZoomIn, ZoomOut, Copy, @@ -181,14 +182,12 @@ - {#if !isLocked} - {#if asset.isTrashed} - - {:else} - - {/if} + {#if !isLocked && asset.isTrashed} + {/if} + + {#if isOwner} {#if stack} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 8811d46ff4..c011a5e466 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -167,9 +167,7 @@ }), ); - if (!sharedLink) { - await handleGetAllAlbums(); - } + await onAlbumAddAssets(); }); onDestroy(() => { @@ -182,7 +180,7 @@ syncAssetViewerOpenClass(false); }); - const handleGetAllAlbums = async () => { + const onAlbumAddAssets = async () => { if (authManager.isSharedLink) { return; } @@ -303,10 +301,6 @@ }; const handleAction = async (action: Action) => { switch (action.type) { - case AssetAction.ADD_TO_ALBUM: { - await handleGetAllAlbums(); - break; - } case AssetAction.DELETE: case AssetAction.TRASH: { eventManager.emit('AssetsDelete', [asset.id]); @@ -369,7 +363,7 @@ const refresh = async () => { await refreshStack(); - await handleGetAllAlbums(); + await onAlbumAddAssets(); ocrManager.clear(); if (!sharedLink) { if (previewStackedAsset) { @@ -441,7 +435,7 @@ - + diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 20d43f1974..49fb7fa6b9 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -10,7 +10,6 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; - import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.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'; @@ -25,6 +24,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import { Route } from '$lib/route'; + import { getAssetBulkActions } from '$lib/services/asset.service'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte'; @@ -34,7 +34,7 @@ import { cancelMultiselect } from '$lib/utils/asset-utils'; import { fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetMediaSize, AssetTypeEnum, getAssetInfo } from '@immich/sdk'; - import { IconButton, toastManager } from '@immich/ui'; + import { ActionButton, IconButton, toastManager } from '@immich/ui'; import { mdiCardsOutline, mdiChevronDown, @@ -328,6 +328,7 @@ assets={assetInteraction.selectedAssets} clearSelect={() => cancelMultiselect(assetInteraction)} > + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} - + diff --git a/web/src/lib/components/timeline/actions/AddToAlbumAction.svelte b/web/src/lib/components/timeline/actions/AddToAlbumAction.svelte deleted file mode 100644 index 6dce0ce084..0000000000 --- a/web/src/lib/components/timeline/actions/AddToAlbumAction.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - -{#if menuItem} - -{/if} - -{#if !menuItem} - -{/if} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 30ae796136..389ebbefab 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -6,7 +6,6 @@ export enum AssetAction { TRASH = 'trash', DELETE = 'delete', RESTORE = 'restore', - ADD_TO_ALBUM = 'add-to-album', STACK = 'stack', UNSTACK = 'unstack', SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset', diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index ead2ffe4b0..b161356a68 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -39,7 +39,7 @@ export type Events = { AssetEditsApplied: [string]; AssetsTag: [string[]]; - AlbumAddAssets: []; + AlbumAddAssets: [{ assetIds: string[]; albumIds: string[] }]; AlbumUpdate: [AlbumResponseDto]; AlbumDelete: [AlbumResponseDto]; AlbumShare: []; diff --git a/web/src/lib/modals/AssetAddToAlbumModal.svelte b/web/src/lib/modals/AssetAddToAlbumModal.svelte new file mode 100644 index 0000000000..b35c125d08 --- /dev/null +++ b/web/src/lib/modals/AssetAddToAlbumModal.svelte @@ -0,0 +1,27 @@ + + + diff --git a/web/src/lib/services/album.service.ts b/web/src/lib/services/album.service.ts index ac0a1045b3..05e0fdb78d 100644 --- a/web/src/lib/services/album.service.ts +++ b/web/src/lib/services/album.service.ts @@ -1,6 +1,7 @@ import { goto } from '$app/navigation'; import ToastAction from '$lib/components/ToastAction.svelte'; import { AlbumPageViewMode } from '$lib/constants'; +import { authManager } from '$lib/managers/auth-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import AlbumAddUsersModal from '$lib/modals/AlbumAddUsersModal.svelte'; @@ -11,17 +12,22 @@ import { user } from '$lib/stores/user.store'; import { createAlbumAndRedirect } from '$lib/utils/album-utils'; import { downloadArchive } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; + import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; import { - addAssetsToAlbum, + addAssetsToAlbum as addToAlbum, + addAssetsToAlbums as addToAlbums, addUsersToAlbum, AlbumUserRole, + BulkIdErrorReason, deleteAlbum, removeUserFromAlbum, updateAlbumInfo, updateAlbumUser, type AlbumResponseDto, + type AlbumsAddAssetsResponseDto, + type BulkIdResponseDto, type UpdateAlbumDto, type UserResponseDto, } from '@immich/sdk'; @@ -86,7 +92,12 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse color: 'primary', icon: mdiPlusBoxOutline, $if: () => assets.length > 0, - onAction: () => addAssets(album, assets), + onAction: () => + addAssetsToAlbums( + [album.id], + assets.map(({ id }) => id), + { notify: true }, + ).then(() => undefined), }; const Upload: ActionItem = { @@ -100,18 +111,73 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse return { AddAssets, Upload }; }; -const addAssets = async (album: AlbumResponseDto, assets: TimelineAsset[]) => { +export const addAssetsToAlbums = async (albumIds: string[], assetIds: string[], { notify }: { notify: boolean }) => { const $t = await getFormatter(); - const assetIds = assets.map(({ id }) => id); try { - const results = await addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: assetIds } }); + if (albumIds.length === 1) { + const albumId = albumIds[0]; + const results = await addToAlbum({ ...authManager.params, id: albumId, bulkIdsDto: { ids: assetIds } }); + if (notify) { + notifyAddToAlbum($t, albumId, assetIds, results); + } + } - const count = results.filter(({ success }) => success).length; - toastManager.success($t('assets_added_count', { values: { count } })); - eventManager.emit('AlbumAddAssets'); + if (albumIds.length > 1) { + const results = await addToAlbums({ ...authManager.params, albumsAddAssetsDto: { albumIds, assetIds } }); + if (notify) { + notifyAddToAlbums($t, albumIds, assetIds, results); + } + } + + eventManager.emit('AlbumAddAssets', { assetIds, albumIds }); + return true; } catch (error) { handleError(error, $t('errors.error_adding_assets_to_album')); + return false; + } +}; + +const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: string[], results: BulkIdResponseDto[]) => { + const successCount = results.filter(({ success }) => success).length; + const duplicateCount = results.filter(({ error }) => error === 'duplicate').length; + let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } }); + if (successCount > 0) { + description = $t('assets_added_to_album_count', { values: { count: successCount } }); + } else if (duplicateCount > 0) { + description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } }); + } + + toastManager.custom( + { + component: ToastAction, + props: { + title: $t('info'), + color: 'primary', + description, + button: { text: $t('view_album'), color: 'primary', onClick: () => goto(Route.viewAlbum({ id: albumId })) }, + }, + }, + { timeout: 5000 }, + ); +}; + +const notifyAddToAlbums = ( + $t: MessageFormatter, + albumIds: string[], + assetIds: string[], + results: AlbumsAddAssetsResponseDto, +) => { + if (results.error === BulkIdErrorReason.Duplicate) { + toastManager.info($t('assets_were_part_of_albums_count', { values: { count: assetIds.length } })); + } else if (results.error) { + toastManager.warning($t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } })); + } else { + toastManager.success( + $t('assets_added_to_albums_count', { + values: { albumTotal: albumIds.length, assetTotal: assetIds.length }, + }), + ); } }; diff --git a/web/src/lib/services/asset.service.ts b/web/src/lib/services/asset.service.ts index f9b33d5687..bbe4d9301b 100644 --- a/web/src/lib/services/asset.service.ts +++ b/web/src/lib/services/asset.service.ts @@ -2,6 +2,7 @@ import { ProjectionType } from '$lib/constants'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; +import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte'; import AssetTagModal from '$lib/modals/AssetTagModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import { user as authUser, preferences } from '$lib/stores/user.store'; @@ -42,6 +43,7 @@ import { mdiMagnifyPlusOutline, mdiMotionPauseOutline, mdiMotionPlayOutline, + mdiPlus, mdiShareVariantOutline, mdiTagPlusOutline, mdiTune, @@ -59,6 +61,13 @@ export const getAssetBulkActions = ($t: MessageFormatter, ctx: AssetControlConte ctx.clearSelect(); }; + const AddToAlbum: ActionItem = { + title: $t('add_to_album'), + icon: mdiPlus, + shortcuts: [{ key: 'l' }], + onAction: () => modalManager.show(AssetAddToAlbumModal, { assetIds }), + }; + const RefreshFacesJob: ActionItem = { title: $t('refresh_faces'), icon: mdiHeadSyncOutline, @@ -84,7 +93,7 @@ export const getAssetBulkActions = ($t: MessageFormatter, ctx: AssetControlConte $if: () => isAllVideos, }; - return { RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob }; + return { AddToAlbum, RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob }; }; export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => { @@ -161,6 +170,14 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) = shortcuts: [{ key: 'f' }], }; + const AddToAlbum: ActionItem = { + title: $t('add_to_album'), + icon: mdiPlus, + shortcuts: [{ key: 'l' }], + $if: () => asset.visibility !== AssetVisibility.Locked && !asset.isTrashed, + onAction: () => modalManager.show(AssetAddToAlbumModal, { assetIds: [asset.id] }), + }; + const Offline: ActionItem = { title: $t('asset_offline'), icon: mdiAlertOutline, @@ -260,6 +277,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) = Unfavorite, PlayMotionPhoto, StopMotionPhoto, + AddToAlbum, ZoomIn, ZoomOut, Copy, diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts index 817354e619..48c8080269 100644 --- a/web/src/lib/stores/asset-interaction.svelte.ts +++ b/web/src/lib/stores/asset-interaction.svelte.ts @@ -1,5 +1,6 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { user } from '$lib/stores/user.store'; +import type { AssetControlContext } from '$lib/types'; import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk'; import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { fromStore } from 'svelte/store'; @@ -22,6 +23,14 @@ export class AssetInteraction { private user = fromStore(user); private userId = $derived(this.user.current?.id); + asControlContext(): AssetControlContext { + return { + getOwnedAssets: () => this.selectedAssets.filter((asset) => asset.ownerId === this.userId), + getAssets: () => this.selectedAssets, + clearSelect: () => this.clearMultiselect(), + }; + } + 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)); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index fc3911e45b..73a6965dd9 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -1,10 +1,8 @@ -import { goto } from '$app/navigation'; import ToastAction from '$lib/components/ToastAction.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { downloadManager } from '$lib/managers/download-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; -import { Route } from '$lib/route'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { preferences } from '$lib/stores/user.store'; import { downloadRequest, withError } from '$lib/utils'; @@ -13,10 +11,7 @@ import { getFormatter } from '$lib/utils/i18n'; import { navigate } from '$lib/utils/navigation'; import { asQueryString } from '$lib/utils/shared-links'; import { - addAssetsToAlbum as addAssets, - addAssetsToAlbums as addToAlbums, AssetVisibility, - BulkIdErrorReason, bulkTagAssets, createStack, deleteAssets, @@ -41,77 +36,6 @@ import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; import { handleError } from './handle-error'; -export const addAssetsToAlbum = async (albumId: string, assetIds: string[], showNotification = true) => { - const result = await addAssets({ - ...authManager.params, - id: albumId, - bulkIdsDto: { - ids: assetIds, - }, - }); - const count = result.filter(({ success }) => success).length; - const duplicateErrorCount = result.filter(({ error }) => error === 'duplicate').length; - const $t = get(t); - - if (showNotification) { - let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } }); - if (count > 0) { - description = $t('assets_added_to_album_count', { values: { count } }); - } else if (duplicateErrorCount > 0) { - description = $t('assets_were_part_of_album_count', { values: { count: duplicateErrorCount } }); - } - toastManager.custom( - { - component: ToastAction, - props: { - title: $t('info'), - color: 'primary', - description, - button: { - text: $t('view_album'), - color: 'primary', - onClick() { - return goto(Route.viewAlbum({ id: albumId })); - }, - }, - }, - }, - { timeout: 5000 }, - ); - } -}; - -export const addAssetsToAlbums = async (albumIds: string[], assetIds: string[], showNotification = true) => { - const result = await addToAlbums({ - ...authManager.params, - albumsAddAssetsDto: { - albumIds, - assetIds, - }, - }); - - if (!showNotification) { - return result; - } - - if (showNotification) { - const $t = get(t); - - if (result.error === BulkIdErrorReason.Duplicate) { - toastManager.info($t('assets_were_part_of_albums_count', { values: { count: assetIds.length } })); - return result; - } - if (result.error) { - toastManager.warning($t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } })); - return result; - } - toastManager.success( - $t('assets_added_to_albums_count', { values: { albumTotal: albumIds.length, assetTotal: assetIds.length } }), - ); - return result; - } -}; - export const tagAssets = async ({ assetIds, tagIds, diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 8558244cfb..e33022eb37 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -1,10 +1,10 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; import { uploadManager } from '$lib/managers/upload-manager.svelte'; +import { addAssetsToAlbums } from '$lib/services/album.service'; import { uploadAssetsStore } from '$lib/stores/upload'; import { user } from '$lib/stores/user.store'; import { UploadState } from '$lib/types'; import { uploadRequest } from '$lib/utils'; -import { addAssetsToAlbum } from '$lib/utils/asset-utils'; import { ExecutorQueue } from '$lib/utils/executor-queue'; import { asQueryString } from '$lib/utils/shared-links'; import { @@ -213,7 +213,7 @@ async function fileUploader({ if (albumId) { uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') }); - await addAssetsToAlbum(albumId, [responseData.id], false); + await addAssetsToAlbums([albumId], [responseData.id], { notify: false }); uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') }); } 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 38817650c1..b9e5e166dd 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 @@ -14,7 +14,6 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; - import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.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'; @@ -45,6 +44,7 @@ handleDownloadAlbum, } from '$lib/services/album.service'; import { getGlobalActions } from '$lib/services/app.service'; + import { getAssetBulkActions } from '$lib/services/asset.service'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; @@ -438,9 +438,11 @@ assets={assetInteraction.selectedAssets} clearSelect={() => assetInteraction.clearMultiselect()} > + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + - + {#if assetInteraction.isAllUserOwned} assetInteraction.clearMultiselect()} > + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + timelineManager.update(ids, (asset) => (asset.visibility = visibility))} /> - + timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} 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 74993cb64b..b13146aab6 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 @@ -2,7 +2,6 @@ import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; - import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.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'; @@ -17,8 +16,10 @@ import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; + import { getAssetBulkActions } from '$lib/services/asset.service'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { preferences } from '$lib/stores/user.store'; + import { ActionButton, CommandPaletteDefaultProvider } from '@immich/ui'; import { mdiDotsVertical } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -68,10 +69,12 @@ assets={assetInteraction.selectedAssets} clearSelect={() => assetInteraction.clearMultiselect()} > + {@const Actions = getAssetBulkActions($t, assetInteraction.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 c9ac99d10f..3cafdcbc5b 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 @@ -8,7 +8,6 @@ import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import Sidebar from '$lib/components/sidebar/sidebar.svelte'; - import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.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'; @@ -27,10 +26,9 @@ import { foldersStore } from '$lib/stores/folders.svelte'; import { preferences } from '$lib/stores/user.store'; import { cancelMultiselect } from '$lib/utils/asset-utils'; - import { getAssetControlContext } from '$lib/utils/context'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { joinPaths } from '$lib/utils/tree-utils'; - import { IconButton, Text } from '@immich/ui'; + import { ActionButton, CommandPaletteDefaultProvider, IconButton, Text } from '@immich/ui'; import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiSelectAll } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -119,8 +117,8 @@ assets={assetInteraction.selectedAssets} clearSelect={() => cancelMultiselect(assetInteraction)} > - {@const Actions = getAssetBulkActions($t, getAssetControlContext())} - + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + - cancelMultiselect(assetInteraction)} /> + import { goto } from '$app/navigation'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; - import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte'; import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte'; import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte'; import { Route } from '$lib/route'; + import { getAssetBulkActions } from '$lib/services/asset.service'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetVisibility } from '@immich/sdk'; + import { ActionButton, CommandPaletteDefaultProvider } from '@immich/ui'; import { mdiArrowLeft } from '@mdi/js'; - import type { PageData } from './$types'; import { t } from 'svelte-i18n'; + import type { PageData } from './$types'; interface Props { data: PageData; @@ -44,8 +45,10 @@ assets={assetInteraction.selectedAssets} clearSelect={() => assetInteraction.clearMultiselect()} > + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + - + {:else} 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 3c18b866c1..d28847068b 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 @@ -12,7 +12,6 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; - import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.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'; @@ -31,6 +30,7 @@ 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { locale } from '$lib/stores/preferences.store'; @@ -40,7 +40,15 @@ import { handleError } from '$lib/utils/handle-error'; import { isExternalUrl } from '$lib/utils/navigation'; import { AssetVisibility, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk'; - import { ContextMenuButton, LoadingSpinner, modalManager, toastManager, type ActionItem } from '@immich/ui'; + import { + ActionButton, + CommandPaletteDefaultProvider, + ContextMenuButton, + LoadingSpinner, + modalManager, + toastManager, + type ActionItem, + } from '@immich/ui'; import { mdiAccountBoxOutline, mdiAccountMultipleCheckOutline, mdiArrowLeft, mdiDotsVertical } from '@mdi/js'; import { DateTime } from 'luxon'; import { onMount } from 'svelte'; @@ -455,9 +463,11 @@ assets={assetInteraction.selectedAssets} clearSelect={() => assetInteraction.clearMultiselect()} > + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + - + timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index bef36d5602..dd2080a831 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -4,7 +4,6 @@ import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; - import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.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'; @@ -36,12 +35,11 @@ type OnLink, type OnUnlink, } from '$lib/utils/actions'; - import { getAssetControlContext } from '$lib/utils/context'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { getAltText } from '$lib/utils/thumbnail-util'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetVisibility } from '@immich/sdk'; - import { ImageCarousel } from '@immich/ui'; + import { ActionButton, CommandPaletteDefaultProvider, ImageCarousel } from '@immich/ui'; import { mdiDotsVertical } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -130,11 +128,12 @@ assets={assetInteraction.selectedAssets} clearSelect={() => assetInteraction.clearMultiselect()} > - {@const Actions = getAssetBulkActions($t, getAssetControlContext())} + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + - + {#if isAllUserOwned} { + const onAlbumAddAssets = ({ assetIds }: { assetIds: string[] }) => { cancelMultiselect(assetInteraction); if (terms.isNotInAlbum.toString() == 'true') { @@ -248,6 +247,8 @@ + + {#if terms}
cancelMultiselect(assetInteraction)} > - {@const Actions = getAssetBulkActions($t, getAssetControlContext())} + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + - + {#if isAllUserOwned} - + 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 868f23bf55..fefd8dd032 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 @@ -9,7 +9,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 AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.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'; @@ -25,12 +24,13 @@ import SkipLink from '$lib/elements/SkipLink.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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { preferences, user } from '$lib/stores/user.store'; import { joinPaths, TreeNode } from '$lib/utils/tree-utils'; import { getAllTags, type TagResponseDto } from '@immich/sdk'; - import { Text } from '@immich/ui'; + import { ActionButton, CommandPaletteDefaultProvider, Text } from '@immich/ui'; import { mdiDotsVertical, mdiTag, mdiTagMultiple } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -120,9 +120,11 @@ assets={assetInteraction.selectedAssets} clearSelect={() => assetInteraction.clearMultiselect()} > + {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())} + - + timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))}