refactor: add to album (#26366)

This commit is contained in:
Jason Rasmussen
2026-02-19 15:27:30 -05:00
committed by GitHub
parent 8eec3c810e
commit 1d11106dd0
23 changed files with 202 additions and 242 deletions
@@ -1,6 +1,6 @@
import type { AssetAction } from '$lib/constants'; import type { AssetAction } from '$lib/constants';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; 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 = { type ActionMap = {
[AssetAction.ARCHIVE]: { asset: TimelineAsset }; [AssetAction.ARCHIVE]: { asset: TimelineAsset };
@@ -8,7 +8,6 @@ type ActionMap = {
[AssetAction.TRASH]: { asset: TimelineAsset }; [AssetAction.TRASH]: { asset: TimelineAsset };
[AssetAction.DELETE]: { asset: TimelineAsset }; [AssetAction.DELETE]: { asset: TimelineAsset };
[AssetAction.RESTORE]: { asset: TimelineAsset }; [AssetAction.RESTORE]: { asset: TimelineAsset };
[AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto };
[AssetAction.STACK]: { stack: StackResponseDto }; [AssetAction.STACK]: { stack: StackResponseDto };
[AssetAction.UNSTACK]: { assets: TimelineAsset[] }; [AssetAction.UNSTACK]: { assets: TimelineAsset[] };
[AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto }; [AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto };
@@ -1,44 +0,0 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import type { OnAction } from '$lib/components/asset-viewer/actions/action';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AssetAction } from '$lib/constants';
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
import { addAssetsToAlbum, addAssetsToAlbums } from '$lib/utils/asset-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk';
import { modalManager } from '@immich/ui';
import { mdiImageAlbum } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
asset: AssetResponseDto;
onAction: OnAction;
}
let { asset, onAction }: Props = $props();
const onClick = async () => {
const albums = await modalManager.show(AlbumPickerModal, {});
if (!albums || albums.length === 0) {
return;
}
if (albums.length === 1) {
const album = albums[0];
await addAssetsToAlbum(album.id, [asset.id]);
onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album });
} else {
await addAssetsToAlbums(
albums.map((a) => a.id),
[asset.id],
);
onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album: albums[0] });
}
};
</script>
<svelte:document use:shortcut={{ shortcut: { key: 'l' }, onShortcut: onClick }} />
<MenuOption icon={mdiImageAlbum} text={$t('add_to_album')} {onClick} />
@@ -3,7 +3,6 @@
import ActionButton from '$lib/components/ActionButton.svelte'; import ActionButton from '$lib/components/ActionButton.svelte';
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte'; import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action'; 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 AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte';
import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte'; import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte';
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte'; import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
@@ -102,6 +101,7 @@
Unfavorite, Unfavorite,
PlayMotionPhoto, PlayMotionPhoto,
StopMotionPhoto, StopMotionPhoto,
AddToAlbum,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
Copy, Copy,
@@ -129,6 +129,7 @@
Unfavorite, Unfavorite,
PlayMotionPhoto, PlayMotionPhoto,
StopMotionPhoto, StopMotionPhoto,
AddToAlbum,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
Copy, Copy,
@@ -181,14 +182,12 @@
<ActionMenuItem action={Download} /> <ActionMenuItem action={Download} />
<ActionMenuItem action={DownloadOriginal} /> <ActionMenuItem action={DownloadOriginal} />
{#if !isLocked} {#if !isLocked && asset.isTrashed}
{#if asset.isTrashed} <RestoreAction {asset} {onAction} />
<RestoreAction {asset} {onAction} />
{:else}
<AddToAlbumAction {asset} {onAction} />
{/if}
{/if} {/if}
<ActionMenuItem action={AddToAlbum} />
{#if isOwner} {#if isOwner}
<AddToStackAction {asset} {stack} {onAction} /> <AddToStackAction {asset} {stack} {onAction} />
{#if stack} {#if stack}
@@ -167,9 +167,7 @@
}), }),
); );
if (!sharedLink) { await onAlbumAddAssets();
await handleGetAllAlbums();
}
}); });
onDestroy(() => { onDestroy(() => {
@@ -182,7 +180,7 @@
syncAssetViewerOpenClass(false); syncAssetViewerOpenClass(false);
}); });
const handleGetAllAlbums = async () => { const onAlbumAddAssets = async () => {
if (authManager.isSharedLink) { if (authManager.isSharedLink) {
return; return;
} }
@@ -303,10 +301,6 @@
}; };
const handleAction = async (action: Action) => { const handleAction = async (action: Action) => {
switch (action.type) { switch (action.type) {
case AssetAction.ADD_TO_ALBUM: {
await handleGetAllAlbums();
break;
}
case AssetAction.DELETE: case AssetAction.DELETE:
case AssetAction.TRASH: { case AssetAction.TRASH: {
eventManager.emit('AssetsDelete', [asset.id]); eventManager.emit('AssetsDelete', [asset.id]);
@@ -369,7 +363,7 @@
const refresh = async () => { const refresh = async () => {
await refreshStack(); await refreshStack();
await handleGetAllAlbums(); await onAlbumAddAssets();
ocrManager.clear(); ocrManager.clear();
if (!sharedLink) { if (!sharedLink) {
if (previewStackedAsset) { if (previewStackedAsset) {
@@ -441,7 +435,7 @@
</script> </script>
<CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag]} /> <CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag]} />
<OnEvents {onAssetReplace} {onAssetUpdate} /> <OnEvents {onAssetReplace} {onAssetUpdate} {onAlbumAddAssets} />
<svelte:document bind:fullscreenElement /> <svelte:document bind:fullscreenElement />
@@ -10,7 +10,6 @@
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -25,6 +24,7 @@
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getAssetBulkActions } from '$lib/services/asset.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte'; import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte';
@@ -34,7 +34,7 @@
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';
import { fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; import { fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
import { AssetMediaSize, AssetTypeEnum, getAssetInfo } from '@immich/sdk'; import { AssetMediaSize, AssetTypeEnum, getAssetInfo } from '@immich/sdk';
import { IconButton, toastManager } from '@immich/ui'; import { ActionButton, IconButton, toastManager } from '@immich/ui';
import { import {
mdiCardsOutline, mdiCardsOutline,
mdiChevronDown, mdiChevronDown,
@@ -328,6 +328,7 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => cancelMultiselect(assetInteraction)} clearSelect={() => cancelMultiselect(assetInteraction)}
> >
{@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CreateSharedLink /> <CreateSharedLink />
<IconButton <IconButton
shape="round" shape="round"
@@ -338,7 +339,7 @@
onclick={handleSelectAll} onclick={handleSelectAll}
/> />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} /> <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
@@ -1,54 +0,0 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
import type { OnAddToAlbum } from '$lib/utils/actions';
import { addAssetsToAlbum, addAssetsToAlbums } from '$lib/utils/asset-utils';
import { getAssetControlContext } from '$lib/utils/context';
import { IconButton, modalManager } from '@immich/ui';
import { mdiImageAlbum, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
onAddToAlbum?: OnAddToAlbum;
menuItem?: boolean;
}
let { onAddToAlbum = () => {}, menuItem = false }: Props = $props();
const { getAssets } = getAssetControlContext();
const onClick = async () => {
const albums = await modalManager.show(AlbumPickerModal, {});
if (!albums || albums.length === 0) {
return;
}
const assetIds = [...getAssets()].map(({ id }) => id);
if (albums.length === 1) {
const album = albums[0];
await addAssetsToAlbum(album.id, assetIds);
onAddToAlbum(assetIds, album.id);
} else {
await addAssetsToAlbums(
albums.map(({ id }) => id),
assetIds,
);
onAddToAlbum(assetIds, albums[0].id);
}
};
</script>
{#if menuItem}
<MenuOption {onClick} text={$t('add_to_album')} icon={mdiImageAlbum} shortcut={{ key: 'l' }} />
{/if}
{#if !menuItem}
<IconButton
shape="round"
color="secondary"
variant="ghost"
icon={mdiPlus}
aria-label={$t('add_to_album')}
onclick={onClick}
/>
{/if}
-1
View File
@@ -6,7 +6,6 @@ export enum AssetAction {
TRASH = 'trash', TRASH = 'trash',
DELETE = 'delete', DELETE = 'delete',
RESTORE = 'restore', RESTORE = 'restore',
ADD_TO_ALBUM = 'add-to-album',
STACK = 'stack', STACK = 'stack',
UNSTACK = 'unstack', UNSTACK = 'unstack',
SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset', SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset',
+1 -1
View File
@@ -39,7 +39,7 @@ export type Events = {
AssetEditsApplied: [string]; AssetEditsApplied: [string];
AssetsTag: [string[]]; AssetsTag: [string[]];
AlbumAddAssets: []; AlbumAddAssets: [{ assetIds: string[]; albumIds: string[] }];
AlbumUpdate: [AlbumResponseDto]; AlbumUpdate: [AlbumResponseDto];
AlbumDelete: [AlbumResponseDto]; AlbumDelete: [AlbumResponseDto];
AlbumShare: []; AlbumShare: [];
@@ -0,0 +1,27 @@
<script lang="ts">
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
import { addAssetsToAlbums } from '$lib/services/album.service';
import { type AlbumResponseDto } from '@immich/sdk';
type Props = {
assetIds: string[];
onClose: () => void;
};
const { assetIds, onClose }: Props = $props();
const handleClose = async (albums?: AlbumResponseDto[]) => {
const albumIds = (albums ?? []).map(({ id }) => id);
if (albumIds.length === 0) {
onClose();
return;
}
const success = await addAssetsToAlbums(albumIds, assetIds, { notify: true });
if (success) {
onClose();
}
};
</script>
<AlbumPickerModal onClose={handleClose} />
+74 -8
View File
@@ -1,6 +1,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ToastAction from '$lib/components/ToastAction.svelte'; import ToastAction from '$lib/components/ToastAction.svelte';
import { AlbumPageViewMode } from '$lib/constants'; import { AlbumPageViewMode } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import AlbumAddUsersModal from '$lib/modals/AlbumAddUsersModal.svelte'; 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 { createAlbumAndRedirect } from '$lib/utils/album-utils';
import { downloadArchive } from '$lib/utils/asset-utils'; import { downloadArchive } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { import {
addAssetsToAlbum, addAssetsToAlbum as addToAlbum,
addAssetsToAlbums as addToAlbums,
addUsersToAlbum, addUsersToAlbum,
AlbumUserRole, AlbumUserRole,
BulkIdErrorReason,
deleteAlbum, deleteAlbum,
removeUserFromAlbum, removeUserFromAlbum,
updateAlbumInfo, updateAlbumInfo,
updateAlbumUser, updateAlbumUser,
type AlbumResponseDto, type AlbumResponseDto,
type AlbumsAddAssetsResponseDto,
type BulkIdResponseDto,
type UpdateAlbumDto, type UpdateAlbumDto,
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
@@ -86,7 +92,12 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse
color: 'primary', color: 'primary',
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
$if: () => assets.length > 0, $if: () => assets.length > 0,
onAction: () => addAssets(album, assets), onAction: () =>
addAssetsToAlbums(
[album.id],
assets.map(({ id }) => id),
{ notify: true },
).then(() => undefined),
}; };
const Upload: ActionItem = { const Upload: ActionItem = {
@@ -100,18 +111,73 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse
return { AddAssets, Upload }; 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 $t = await getFormatter();
const assetIds = assets.map(({ id }) => id);
try { 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; if (albumIds.length > 1) {
toastManager.success($t('assets_added_count', { values: { count } })); const results = await addToAlbums({ ...authManager.params, albumsAddAssetsDto: { albumIds, assetIds } });
eventManager.emit('AlbumAddAssets'); if (notify) {
notifyAddToAlbums($t, albumIds, assetIds, results);
}
}
eventManager.emit('AlbumAddAssets', { assetIds, albumIds });
return true;
} catch (error) { } catch (error) {
handleError(error, $t('errors.error_adding_assets_to_album')); 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 },
}),
);
} }
}; };
+19 -1
View File
@@ -2,6 +2,7 @@ import { ProjectionType } from '$lib/constants';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-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 AssetTagModal from '$lib/modals/AssetTagModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { user as authUser, preferences } from '$lib/stores/user.store'; import { user as authUser, preferences } from '$lib/stores/user.store';
@@ -42,6 +43,7 @@ import {
mdiMagnifyPlusOutline, mdiMagnifyPlusOutline,
mdiMotionPauseOutline, mdiMotionPauseOutline,
mdiMotionPlayOutline, mdiMotionPlayOutline,
mdiPlus,
mdiShareVariantOutline, mdiShareVariantOutline,
mdiTagPlusOutline, mdiTagPlusOutline,
mdiTune, mdiTune,
@@ -59,6 +61,13 @@ export const getAssetBulkActions = ($t: MessageFormatter, ctx: AssetControlConte
ctx.clearSelect(); ctx.clearSelect();
}; };
const AddToAlbum: ActionItem = {
title: $t('add_to_album'),
icon: mdiPlus,
shortcuts: [{ key: 'l' }],
onAction: () => modalManager.show(AssetAddToAlbumModal, { assetIds }),
};
const RefreshFacesJob: ActionItem = { const RefreshFacesJob: ActionItem = {
title: $t('refresh_faces'), title: $t('refresh_faces'),
icon: mdiHeadSyncOutline, icon: mdiHeadSyncOutline,
@@ -84,7 +93,7 @@ export const getAssetBulkActions = ($t: MessageFormatter, ctx: AssetControlConte
$if: () => isAllVideos, $if: () => isAllVideos,
}; };
return { RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob }; return { AddToAlbum, RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob };
}; };
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => { export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
@@ -161,6 +170,14 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
shortcuts: [{ key: 'f' }], 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 = { const Offline: ActionItem = {
title: $t('asset_offline'), title: $t('asset_offline'),
icon: mdiAlertOutline, icon: mdiAlertOutline,
@@ -260,6 +277,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
Unfavorite, Unfavorite,
PlayMotionPhoto, PlayMotionPhoto,
StopMotionPhoto, StopMotionPhoto,
AddToAlbum,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
Copy, Copy,
@@ -1,5 +1,6 @@
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import type { AssetControlContext } from '$lib/types';
import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk'; import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk';
import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { fromStore } from 'svelte/store'; import { fromStore } from 'svelte/store';
@@ -22,6 +23,14 @@ export class AssetInteraction {
private user = fromStore<UserAdminResponseDto | undefined>(user); private user = fromStore<UserAdminResponseDto | undefined>(user);
private userId = $derived(this.user.current?.id); 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)); isAllTrashed = $derived(this.selectedAssets.every((asset) => asset.isTrashed));
isAllArchived = $derived(this.selectedAssets.every((asset) => asset.visibility === AssetVisibility.Archive)); isAllArchived = $derived(this.selectedAssets.every((asset) => asset.visibility === AssetVisibility.Archive));
isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite)); isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
-76
View File
@@ -1,10 +1,8 @@
import { goto } from '$app/navigation';
import ToastAction from '$lib/components/ToastAction.svelte'; import ToastAction from '$lib/components/ToastAction.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { downloadManager } from '$lib/managers/download-manager.svelte'; import { downloadManager } from '$lib/managers/download-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { downloadRequest, withError } from '$lib/utils'; import { downloadRequest, withError } from '$lib/utils';
@@ -13,10 +11,7 @@ import { getFormatter } from '$lib/utils/i18n';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { asQueryString } from '$lib/utils/shared-links'; import { asQueryString } from '$lib/utils/shared-links';
import { import {
addAssetsToAlbum as addAssets,
addAssetsToAlbums as addToAlbums,
AssetVisibility, AssetVisibility,
BulkIdErrorReason,
bulkTagAssets, bulkTagAssets,
createStack, createStack,
deleteAssets, deleteAssets,
@@ -41,77 +36,6 @@ import { t } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { handleError } from './handle-error'; 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 ({ export const tagAssets = async ({
assetIds, assetIds,
tagIds, tagIds,
+2 -2
View File
@@ -1,10 +1,10 @@
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { uploadManager } from '$lib/managers/upload-manager.svelte'; import { uploadManager } from '$lib/managers/upload-manager.svelte';
import { addAssetsToAlbums } from '$lib/services/album.service';
import { uploadAssetsStore } from '$lib/stores/upload'; import { uploadAssetsStore } from '$lib/stores/upload';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { UploadState } from '$lib/types'; import { UploadState } from '$lib/types';
import { uploadRequest } from '$lib/utils'; import { uploadRequest } from '$lib/utils';
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
import { ExecutorQueue } from '$lib/utils/executor-queue'; import { ExecutorQueue } from '$lib/utils/executor-queue';
import { asQueryString } from '$lib/utils/shared-links'; import { asQueryString } from '$lib/utils/shared-links';
import { import {
@@ -213,7 +213,7 @@ async function fileUploader({
if (albumId) { if (albumId) {
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') }); 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') }); uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') });
} }
@@ -14,7 +14,6 @@
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -45,6 +44,7 @@
handleDownloadAlbum, handleDownloadAlbum,
} from '$lib/services/album.service'; } from '$lib/services/album.service';
import { getGlobalActions } from '$lib/services/app.service'; import { getGlobalActions } from '$lib/services/app.service';
import { getAssetBulkActions } from '$lib/services/asset.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
@@ -438,9 +438,11 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
{@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
{#if assetInteraction.isAllUserOwned} {#if assetInteraction.isAllUserOwned}
<FavoriteAction <FavoriteAction
removeFavorite={assetInteraction.isAllFavorite} removeFavorite={assetInteraction.isAllFavorite}
@@ -2,7 +2,6 @@
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte'; import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte';
import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte'; import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte';
@@ -15,8 +14,10 @@
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte'; import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
import { ActionButton, CommandPaletteDefaultProvider } from '@immich/ui';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -64,13 +65,15 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
{@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<ArchiveAction <ArchiveAction
unarchive unarchive
onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))} onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))}
/> />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
<FavoriteAction <FavoriteAction
removeFavorite={assetInteraction.isAllFavorite} removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))}
@@ -2,7 +2,6 @@
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -17,8 +16,10 @@
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import Timeline from '$lib/components/timeline/Timeline.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { ActionButton, CommandPaletteDefaultProvider } from '@immich/ui';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -68,10 +69,12 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
{@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<FavoriteAction removeFavorite onFavorite={(assetIds) => timelineManager.removeAssets(assetIds)} /> <FavoriteAction removeFavorite onFavorite={(assetIds) => timelineManager.removeAssets(assetIds)} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem />
@@ -8,7 +8,6 @@
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
import Sidebar from '$lib/components/sidebar/sidebar.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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -27,10 +26,9 @@
import { foldersStore } from '$lib/stores/folders.svelte'; import { foldersStore } from '$lib/stores/folders.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';
import { getAssetControlContext } from '$lib/utils/context';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { joinPaths } from '$lib/utils/tree-utils'; 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 { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiSelectAll } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -119,8 +117,8 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => cancelMultiselect(assetInteraction)} clearSelect={() => cancelMultiselect(assetInteraction)}
> >
{@const Actions = getAssetBulkActions($t, getAssetControlContext())} {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink /> <CreateSharedLink />
<IconButton <IconButton
shape="round" shape="round"
@@ -130,7 +128,7 @@
icon={mdiSelectAll} icon={mdiSelectAll}
onclick={handleSelectAllAssets} onclick={handleSelectAllAssets}
/> />
<AddToAlbum onAddToAlbum={() => cancelMultiselect(assetInteraction)} /> <ActionButton action={Actions.AddToAlbum} />
<FavoriteAction <FavoriteAction
removeFavorite={assetInteraction.isAllFavorite} removeFavorite={assetInteraction.isAllFavorite}
onFavorite={function handleFavoriteUpdate(ids, isFavorite) { onFavorite={function handleFavoriteUpdate(ids, isFavorite) {
@@ -1,17 +1,18 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; 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 CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte';
import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte'; import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte';
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import Timeline from '$lib/components/timeline/Timeline.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getAssetBulkActions } from '$lib/services/asset.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
import { ActionButton, CommandPaletteDefaultProvider } from '@immich/ui';
import { mdiArrowLeft } from '@mdi/js'; import { mdiArrowLeft } from '@mdi/js';
import type { PageData } from './$types';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props { interface Props {
data: PageData; data: PageData;
@@ -44,8 +45,10 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
{@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink /> <CreateSharedLink />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
<DownloadAction /> <DownloadAction />
</AssetSelectControlBar> </AssetSelectControlBar>
{:else} {:else}
@@ -12,7 +12,6 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; 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 MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -31,6 +30,7 @@
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte'; import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getAssetBulkActions } from '$lib/services/asset.service';
import { getPersonActions } from '$lib/services/person.service'; import { getPersonActions } from '$lib/services/person.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
@@ -40,7 +40,15 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { isExternalUrl } from '$lib/utils/navigation'; import { isExternalUrl } from '$lib/utils/navigation';
import { AssetVisibility, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk'; 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 { mdiAccountBoxOutline, mdiAccountMultipleCheckOutline, mdiArrowLeft, mdiDotsVertical } from '@mdi/js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -455,9 +463,11 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
{@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
<FavoriteAction <FavoriteAction
removeFavorite={assetInteraction.isAllFavorite} removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))}
@@ -4,7 +4,6 @@
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -36,12 +35,11 @@
type OnLink, type OnLink,
type OnUnlink, type OnUnlink,
} from '$lib/utils/actions'; } from '$lib/utils/actions';
import { getAssetControlContext } from '$lib/utils/context';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { getAltText } from '$lib/utils/thumbnail-util'; import { getAltText } from '$lib/utils/thumbnail-util';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
import { ImageCarousel } from '@immich/ui'; import { ActionButton, CommandPaletteDefaultProvider, ImageCarousel } from '@immich/ui';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -130,11 +128,12 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
{@const Actions = getAssetBulkActions($t, getAssetControlContext())} {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
{#if isAllUserOwned} {#if isAllUserOwned}
<FavoriteAction <FavoriteAction
@@ -2,11 +2,11 @@
import { afterNavigate, goto } from '$app/navigation'; import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte'; import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.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 GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte'; import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -28,7 +28,6 @@
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';
import { getAssetControlContext } from '$lib/utils/context';
import { parseUtcDate } from '$lib/utils/date-time'; import { parseUtcDate } from '$lib/utils/date-time';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
@@ -43,7 +42,7 @@
searchSmart, searchSmart,
type SmartSearchDto, type SmartSearchDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { Icon, IconButton, LoadingSpinner } from '@immich/ui'; import { ActionButton, CommandPaletteDefaultProvider, Icon, IconButton, LoadingSpinner } from '@immich/ui';
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiSelectAll } from '@mdi/js'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiSelectAll } from '@mdi/js';
import { tick, untrack } from 'svelte'; import { tick, untrack } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -232,7 +231,7 @@
return tagNames.join(', '); return tagNames.join(', ');
} }
const onAddToAlbum = (assetIds: string[]) => { const onAlbumAddAssets = ({ assetIds }: { assetIds: string[] }) => {
cancelMultiselect(assetInteraction); cancelMultiselect(assetInteraction);
if (terms.isNotInAlbum.toString() == 'true') { if (terms.isNotInAlbum.toString() == 'true') {
@@ -248,6 +247,8 @@
<svelte:window bind:scrollY /> <svelte:window bind:scrollY />
<OnEvents {onAlbumAddAssets} />
{#if terms} {#if terms}
<section <section
id="search-chips" id="search-chips"
@@ -328,7 +329,8 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => cancelMultiselect(assetInteraction)} clearSelect={() => cancelMultiselect(assetInteraction)}
> >
{@const Actions = getAssetBulkActions($t, getAssetControlContext())} {@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink /> <CreateSharedLink />
<IconButton <IconButton
@@ -339,7 +341,7 @@
icon={mdiSelectAll} icon={mdiSelectAll}
onclick={handleSelectAll} onclick={handleSelectAll}
/> />
<AddToAlbum {onAddToAlbum} /> <ActionButton action={Actions.AddToAlbum} />
{#if isAllUserOwned} {#if isAllUserOwned}
<FavoriteAction <FavoriteAction
removeFavorite={assetInteraction.isAllFavorite} removeFavorite={assetInteraction.isAllFavorite}
@@ -354,7 +356,7 @@
/> />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<AddToAlbum menuItem {onAddToAlbum} /> <ActionMenuItem action={Actions.AddToAlbum} />
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
@@ -9,7 +9,6 @@
import Sidebar from '$lib/components/sidebar/sidebar.svelte'; import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import Timeline from '$lib/components/timeline/Timeline.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 ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
@@ -25,12 +24,13 @@
import SkipLink from '$lib/elements/SkipLink.svelte'; import SkipLink from '$lib/elements/SkipLink.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getAssetBulkActions } from '$lib/services/asset.service';
import { getTagActions } from '$lib/services/tag.service'; import { getTagActions } from '$lib/services/tag.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { joinPaths, TreeNode } from '$lib/utils/tree-utils'; import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
import { getAllTags, type TagResponseDto } from '@immich/sdk'; 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 { mdiDotsVertical, mdiTag, mdiTagMultiple } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -120,9 +120,11 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
{@const Actions = getAssetBulkActions($t, assetInteraction.asControlContext())}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<AddToAlbum /> <ActionButton action={Actions.AddToAlbum} />
<FavoriteAction <FavoriteAction
removeFavorite={assetInteraction.isAllFavorite} removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))}