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))}