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

View File

@@ -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 };

View File

@@ -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} />

View File

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

View File

@@ -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 />

View File

@@ -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} />

View File

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

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',

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: [];

View File

@@ -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} />

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

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,

View File

@@ -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));

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,

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

View File

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

View File

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

View File

@@ -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 />

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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

View File

@@ -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 />

View File

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