mirror of
https://github.com/immich-app/immich.git
synced 2026-06-22 14:52:17 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fe56700fa | |||
| 653b17669b |
@@ -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];
|
||||
|
||||
@@ -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'),
|
||||
|
||||
+11
-6
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user