Compare commits

...

2 Commits

Author SHA1 Message Date
Mees Frensel 1fe56700fa don't check and throw 2026-06-22 10:55:07 +02:00
Mees Frensel 653b17669b refactor: remove assets from album action 2026-06-19 13:57:56 +02:00
7 changed files with 91 additions and 85 deletions
@@ -71,7 +71,6 @@
onAction?: OnAction;
onUndoDelete?: OnUndoDelete;
onClose?: (assetId: string) => void;
onRemoveFromAlbum?: (assetIds: string[]) => void;
onRandom?: () => Promise<{ id: string } | undefined>;
}
@@ -87,7 +86,6 @@
onAction,
onUndoDelete,
onClose,
onRemoveFromAlbum,
onRandom,
}: Props = $props();
@@ -505,7 +503,6 @@
{onUndoDelete}
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
onClose={onClose ? () => onClose(stack?.primaryAssetId ?? asset.id) : undefined}
{onRemoveFromAlbum}
{playOriginalVideo}
{setPlayOriginalVideo}
/>
@@ -18,7 +18,6 @@
import LoadingDots from '$lib/components/LoadingDots.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
import RemoveFromAlbumAction from '$lib/components/timeline/actions/RemoveFromAlbumAction.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
@@ -51,7 +50,7 @@
interface Props {
asset: AssetResponseDto;
album?: AlbumResponseDto | null;
album?: AlbumResponseDto;
person?: PersonResponseDto | null;
stack?: StackResponseDto | null;
showSlideshow?: boolean;
@@ -60,14 +59,13 @@
onUndoDelete?: OnUndoDelete;
onPlaySlideshow: () => void;
onClose?: () => void;
onRemoveFromAlbum?: (assetIds: string[]) => void;
playOriginalVideo: boolean;
setPlayOriginalVideo: (value: boolean) => void;
}
let {
asset,
album = null,
album,
person = null,
stack = null,
showSlideshow = false,
@@ -76,13 +74,11 @@
onUndoDelete = undefined,
onPlaySlideshow,
onClose,
onRemoveFromAlbum,
playOriginalVideo = false,
setPlayOriginalVideo,
}: Props = $props();
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
const isAlbumOwner = $derived(authManager.authenticated && album?.albumUsers[0].user.id === authManager.user.id);
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
@@ -96,7 +92,7 @@
shortcuts: [{ key: 'Escape' }],
});
const Actions = $derived(getAssetActions($t, asset));
const Actions = $derived(getAssetActions($t, asset, album));
const sharedLink = getSharedLink();
</script>
@@ -159,9 +155,7 @@
{/if}
<ActionMenuItem action={Actions.AddToAlbum} />
{#if album && (isOwner || isAlbumOwner)}
<RemoveFromAlbumAction {album} onRemove={onRemoveFromAlbum} assetIds={[asset.id]} menuItem />
{/if}
<ActionMenuItem action={Actions.RemoveFromAlbum} />
{#if isOwner}
<AddToStackAction {asset} {stack} {onAction} />
@@ -1,6 +1,7 @@
<script lang="ts">
import type { Action } from '$lib/components/asset-viewer/actions/action';
import type { AssetCursor } from '$lib/components/asset-viewer/AssetViewer.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import { AssetAction } from '$lib/constants';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
@@ -106,7 +107,11 @@
});
};
const handleRemoveFromAlbum = async (assetIds: string[]) => {
const onAlbumRemoveAssets = async ({ assetIds, albumIds }: { assetIds: string[]; albumIds: string[] }) => {
if (!album || !albumIds.includes(album.id)) {
return;
}
timelineManager.removeAssets(assetIds);
if (!assetIds.includes(assetCursor.current.id)) {
@@ -234,6 +239,8 @@
});
</script>
<OnEvents {onAlbumRemoveAssets} />
{#await import('$lib/components/asset-viewer/AssetViewer.svelte') then { default: AssetViewer }}
<AssetViewer
{withStacked}
@@ -251,7 +258,6 @@
}}
onUndoDelete={handleUndoDelete}
onRandom={handleRandom}
onRemoveFromAlbum={handleRemoveFromAlbum}
onClose={handleClose}
/>
{/await}
@@ -1,61 +0,0 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { getAlbumInfo, removeAssetFromAlbum, type AlbumResponseDto } from '@immich/sdk';
import { IconButton, modalManager, toastManager } from '@immich/ui';
import { mdiDeleteOutline, mdiImageRemoveOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
album: AlbumResponseDto;
onRemove: ((assetIds: string[]) => void) | undefined;
assetIds?: string[];
menuItem?: boolean;
}
let { album = $bindable(), onRemove, assetIds, menuItem = false }: Props = $props();
const removeFromAlbum = async () => {
const ids = assetIds ?? assetMultiSelectManager.assets.map(({ id }) => id) ?? [];
const isConfirmed = await modalManager.showDialog({
prompt: $t('remove_assets_album_confirmation', { values: { count: ids.length } }),
});
if (!isConfirmed) {
return;
}
try {
const results = await removeAssetFromAlbum({
id: album.id,
bulkIdsDto: { ids },
});
album = await getAlbumInfo({ id: album.id });
onRemove?.(ids);
const count = results.filter(({ success }) => success).length;
toastManager.primary($t('assets_removed_count', { values: { count } }));
assetMultiSelectManager.clear();
} catch (error) {
handleError(error, $t('errors.error_removing_assets_from_album'));
}
};
</script>
{#if menuItem}
<MenuOption text={$t('remove_from_album')} icon={mdiImageRemoveOutline} onClick={removeFromAlbum} />
{:else}
<IconButton
shape="round"
color="secondary"
variant="ghost"
aria-label={$t('remove_from_album')}
icon={mdiDeleteOutline}
onclick={removeFromAlbum}
/>
{/if}
@@ -40,6 +40,7 @@ export type Events = {
AssetsTag: [string[]];
AlbumAddAssets: [{ assetIds: string[]; albumIds: string[] }];
AlbumRemoveAssets: [{ assetIds: string[]; albumIds: string[] }];
AlbumCreate: [AlbumResponseDto];
AlbumUpdate: [AlbumResponseDto];
AlbumDelete: [AlbumResponseDto];
+67 -3
View File
@@ -4,8 +4,10 @@ import {
AssetTypeEnum,
AssetVisibility,
getAssetInfo,
removeAssetFromAlbum,
runAssetJobs,
updateAsset,
type AlbumResponseDto,
type AssetJobsDto,
type AssetResponseDto,
} from '@immich/sdk';
@@ -22,6 +24,7 @@ import {
mdiHeart,
mdiHeartOutline,
mdiImageRefreshOutline,
mdiImageRemoveOutline,
mdiInformationOutline,
mdiMagnifyMinusOutline,
mdiMagnifyPlusOutline,
@@ -46,8 +49,9 @@ import { downloadUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
export const getAssetBulkActions = ($t: MessageFormatter) => {
export const getAssetBulkActions = ($t: MessageFormatter, album?: AlbumResponseDto) => {
const ownedAssets = assetMultiSelectManager.ownedAssets;
const isAlbumOwner = album?.albumUsers[0].user.id === authManager.user.id;
const onAction = async (name: AssetJobName) => {
await handleRunAssetJob({ name, assetIds: ownedAssets.map(({ id }) => id) });
@@ -62,6 +66,17 @@ export const getAssetBulkActions = ($t: MessageFormatter) => {
modalManager.show(AssetAddToAlbumModal, { assetIds: assetMultiSelectManager.assets.map((asset) => asset.id) }),
};
const RemoveFromAlbum: ActionItem = {
title: $t('remove_from_album'),
icon: mdiImageRemoveOutline,
$if: () => !!album && (isAlbumOwner || assetMultiSelectManager.isAllUserOwned),
onAction: () =>
handleBulkRemoveAssetsFromAlbum(
assetMultiSelectManager.assets.map((asset) => asset.id),
album!,
),
};
const RefreshFacesJob: ActionItem = {
title: $t('refresh_faces'),
icon: mdiHeadSyncOutline,
@@ -87,13 +102,21 @@ export const getAssetBulkActions = ($t: MessageFormatter) => {
$if: () => ownedAssets.every((asset) => asset.isVideo),
};
return { AddToAlbum, RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob };
return {
AddToAlbum,
RemoveFromAlbum,
RefreshFacesJob,
RefreshMetadataJob,
RegenerateThumbnailJob,
TranscodeVideoJob,
};
};
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto, album?: AlbumResponseDto) => {
const sharedLink = getSharedLink();
const authUser = authManager.authenticated ? authManager.user : undefined;
const isOwner = !!(authUser && authUser.id === asset.ownerId);
const isAlbumOwner = !!(authUser && authUser.id === album?.albumUsers[0].user.id);
const Share: ActionItem = {
title: $t('share'),
@@ -164,6 +187,13 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
onAction: () => modalManager.show(AssetAddToAlbumModal, { assetIds: [asset.id] }),
};
const RemoveFromAlbum: ActionItem = {
title: $t('remove_from_album'),
icon: mdiImageRemoveOutline,
$if: () => !!album && (isOwner || isAlbumOwner),
onAction: () => handleRemoveAssetsFromAlbum([asset.id], album!),
};
const Offline: ActionItem = {
title: $t('asset_offline'),
icon: mdiAlertOutline,
@@ -270,6 +300,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
PlayMotionPhoto,
StopMotionPhoto,
AddToAlbum,
RemoveFromAlbum,
ZoomIn,
ZoomOut,
Copy,
@@ -357,6 +388,39 @@ const handleUnfavorite = async (asset: AssetResponseDto) => {
}
};
const handleBulkRemoveAssetsFromAlbum = async (assetIds: string[], album: AlbumResponseDto) => {
const $t = await getFormatter();
const isConfirmed = await modalManager.showDialog({
prompt: $t('remove_assets_album_confirmation', { values: { count: assetIds.length } }),
});
if (!isConfirmed) {
return;
}
await handleRemoveAssetsFromAlbum(assetIds, album);
assetMultiSelectManager.clear();
};
const handleRemoveAssetsFromAlbum = async (assetIds: string[], album: AlbumResponseDto) => {
const $t = await getFormatter();
try {
const results = await removeAssetFromAlbum({
id: album.id,
bulkIdsDto: { ids: assetIds },
});
const count = results.filter(({ success }) => success).length;
toastManager.primary($t('assets_removed_count', { values: { count } }));
eventManager.emit('AlbumRemoveAssets', { assetIds, albumIds: [album.id] });
} catch (error) {
handleError(error, $t('errors.error_removing_assets_from_album'));
}
};
const getAssetJobMessage = ($t: MessageFormatter, job: AssetJobName) => {
const messages: Record<AssetJobName, string> = {
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
@@ -19,7 +19,6 @@
import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte';
import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte';
import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte';
import RemoveFromAlbum from '$lib/components/timeline/actions/RemoveFromAlbumAction.svelte';
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
@@ -78,6 +77,7 @@
import type { PageData } from './$types';
import AlbumDescription from './AlbumDescription.svelte';
import AlbumTitle from './AlbumTitle.svelte';
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
interface Props {
data: PageData;
@@ -156,6 +156,12 @@
assetMultiSelectManager.clear();
};
const onAlbumRemoveAssets = async ({ assetIds, albumIds }: { assetIds: string[]; albumIds: string[] }) => {
if (albumIds.includes(album.id)) {
await handleRemoveAssets(assetIds);
}
};
const handleRemoveAssets = async (assetIds: string[]) => {
timelineManager.removeAssets(assetIds);
await refreshAlbum();
@@ -206,7 +212,7 @@
}
});
let album = $derived(data.album);
let album = $state(data.album);
let albumId = $derived(album.id);
const containsEditors = $derived(album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor));
@@ -330,6 +336,7 @@
onSharedLinkDelete={refreshAlbum}
{onAlbumDelete}
{onAlbumAddAssets}
{onAlbumRemoveAssets}
{onAlbumShare}
{onAlbumUserUpdate}
onAlbumUserDelete={refreshAlbum}
@@ -453,7 +460,7 @@
{#if assetMultiSelectManager.selectionActive}
<AssetSelectControlBar>
{@const Actions = getAssetBulkActions($t)}
{@const Actions = getAssetBulkActions($t, album)}
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
<CreateSharedLink />
<SelectAllAssets {timelineManager} assetInteraction={assetMultiSelectManager} />
@@ -489,9 +496,7 @@
<TagAction menuItem />
{/if}
{#if isOwned || assetMultiSelectManager.isAllUserOwned}
<RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} />
{/if}
<ActionMenuItem action={Actions.RemoveFromAlbum} />
{#if assetMultiSelectManager.isAllUserOwned}
<DeleteAssets menuItem onAssetDelete={handleRemoveAssets} onUndoDelete={handleUndoRemoveAssets} />
{/if}