mirror of
https://github.com/immich-app/immich.git
synced 2026-07-01 10:35:13 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 698b96d597 |
+3
-1
@@ -33,7 +33,7 @@
|
|||||||
"add_to_albums": "Add to albums",
|
"add_to_albums": "Add to albums",
|
||||||
"add_to_albums_count": "Add to albums ({count})",
|
"add_to_albums_count": "Add to albums ({count})",
|
||||||
"add_to_bottom_bar": "Add to",
|
"add_to_bottom_bar": "Add to",
|
||||||
"add_upload_to_stack": "Add upload to stack",
|
"add_upload_to_stack": "Upload and {isStack, select, true {add to stack} other {create stack}}",
|
||||||
"add_url": "Add URL",
|
"add_url": "Add URL",
|
||||||
"added_to_archive": "Added to archive",
|
"added_to_archive": "Added to archive",
|
||||||
"added_to_favorites": "Added to favorites",
|
"added_to_favorites": "Added to favorites",
|
||||||
@@ -1733,6 +1733,7 @@
|
|||||||
"removed_from_archive": "Removed from archive",
|
"removed_from_archive": "Removed from archive",
|
||||||
"removed_from_favorites": "Removed from favorites",
|
"removed_from_favorites": "Removed from favorites",
|
||||||
"removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites",
|
"removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites",
|
||||||
|
"removed_from_stack": "Removed asset from stack",
|
||||||
"removed_memory": "Removed memory",
|
"removed_memory": "Removed memory",
|
||||||
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
@@ -2008,6 +2009,7 @@
|
|||||||
"source": "Source",
|
"source": "Source",
|
||||||
"stack": "Stack",
|
"stack": "Stack",
|
||||||
"stack_action_prompt": "{count} stacked",
|
"stack_action_prompt": "{count} stacked",
|
||||||
|
"stack_created": "Stack created",
|
||||||
"stack_duplicates": "Stack duplicates",
|
"stack_duplicates": "Stack duplicates",
|
||||||
"stack_selected_photos": "Stack selected photos",
|
"stack_selected_photos": "Stack selected photos",
|
||||||
"stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}",
|
"stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
const stackSelectedThumbnailSize = 65;
|
const stackSelectedThumbnailSize = 65;
|
||||||
|
|
||||||
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
||||||
let stack: StackResponseDto | null = $state(null);
|
let stack: StackResponseDto | undefined = $state();
|
||||||
|
|
||||||
const asset = $derived(previewStackedAsset ?? cursor.current);
|
const asset = $derived(previewStackedAsset ?? cursor.current);
|
||||||
const nextAsset = $derived(cursor.nextAsset);
|
const nextAsset = $derived(cursor.nextAsset);
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!stack?.assets.some(({ id }) => id === asset.id)) {
|
if (!stack?.assets.some(({ id }) => id === asset.id)) {
|
||||||
stack = null;
|
stack = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,6 +149,22 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onStackCreate = (createdStack: StackResponseDto) => {
|
||||||
|
if (createdStack.assets.map((a) => a.id).includes(asset.id)) {
|
||||||
|
stack = createdStack;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStackUpdate = (updatedStack: StackResponseDto) => {
|
||||||
|
if (stack?.id === updatedStack.id) {
|
||||||
|
stack = updatedStack;
|
||||||
|
if (!stack.assets.map((a) => a.id).includes(asset.id)) {
|
||||||
|
// current asset was removed from stack, go to primary
|
||||||
|
cursor.current = stack.assets[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
syncAssetViewerOpenClass(true);
|
syncAssetViewerOpenClass(true);
|
||||||
const slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
const slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
||||||
@@ -319,18 +335,6 @@
|
|||||||
eventManager.emit('AssetsDelete', [asset.id]);
|
eventManager.emit('AssetsDelete', [asset.id]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AssetAction.REMOVE_ASSET_FROM_STACK: {
|
|
||||||
stack = action.stack;
|
|
||||||
if (stack) {
|
|
||||||
cursor.current = stack.assets[0];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AssetAction.STACK:
|
|
||||||
case AssetAction.SET_STACK_PRIMARY_ASSET: {
|
|
||||||
stack = action.stack;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AssetAction.SET_PERSON_FEATURED_PHOTO: {
|
case AssetAction.SET_PERSON_FEATURED_PHOTO: {
|
||||||
const assetInfo = await getAssetInfo({ id: asset.id });
|
const assetInfo = await getAssetInfo({ id: asset.id });
|
||||||
cursor.current = { ...asset, people: assetInfo.people };
|
cursor.current = { ...asset, people: assetInfo.people };
|
||||||
@@ -347,10 +351,6 @@
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AssetAction.UNSTACK: {
|
|
||||||
closeViewer();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +475,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag, TagPeople]} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag, TagPeople]} />
|
||||||
<OnEvents {onAssetUpdate} />
|
<OnEvents {onAssetUpdate} {onStackCreate} onStackDelete={() => closeViewer()} {onStackUpdate} />
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
bind:fullscreenElement
|
bind:fullscreenElement
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 AddToStackAction from '$lib/components/asset-viewer/actions/AddToStackAction.svelte';
|
|
||||||
import ArchiveAction from '$lib/components/asset-viewer/actions/ArchiveAction.svelte';
|
import ArchiveAction from '$lib/components/asset-viewer/actions/ArchiveAction.svelte';
|
||||||
import DeleteAction from '$lib/components/asset-viewer/actions/DeleteAction.svelte';
|
import DeleteAction from '$lib/components/asset-viewer/actions/DeleteAction.svelte';
|
||||||
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/KeepThisDeleteOthers.svelte';
|
|
||||||
import RatingAction from '$lib/components/asset-viewer/actions/RatingAction.svelte';
|
import RatingAction from '$lib/components/asset-viewer/actions/RatingAction.svelte';
|
||||||
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/RemoveAssetFromStack.svelte';
|
|
||||||
import RestoreAction from '$lib/components/asset-viewer/actions/RestoreAction.svelte';
|
import RestoreAction from '$lib/components/asset-viewer/actions/RestoreAction.svelte';
|
||||||
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/SetPersonFeaturedAction.svelte';
|
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/SetPersonFeaturedAction.svelte';
|
||||||
import SetStackPrimaryAsset from '$lib/components/asset-viewer/actions/SetStackPrimaryAsset.svelte';
|
|
||||||
import SetVisibilityAction from '$lib/components/asset-viewer/actions/SetVisibilityAction.svelte';
|
import SetVisibilityAction from '$lib/components/asset-viewer/actions/SetVisibilityAction.svelte';
|
||||||
import UnstackAction from '$lib/components/asset-viewer/actions/UnstackAction.svelte';
|
|
||||||
import LoadingDots from '$lib/components/LoadingDots.svelte';
|
import LoadingDots from '$lib/components/LoadingDots.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
||||||
import RemoveFromAlbumAction from '$lib/components/timeline/actions/RemoveFromAlbumAction.svelte';
|
import RemoveFromAlbumAction from '$lib/components/timeline/actions/RemoveFromAlbumAction.svelte';
|
||||||
@@ -21,6 +16,7 @@
|
|||||||
import { getAlbumAssetActions } from '$lib/services/album.service';
|
import { getAlbumAssetActions } from '$lib/services/album.service';
|
||||||
import { getGlobalActions } from '$lib/services/app.service';
|
import { getGlobalActions } from '$lib/services/app.service';
|
||||||
import { getAssetActions } from '$lib/services/asset.service';
|
import { getAssetActions } from '$lib/services/asset.service';
|
||||||
|
import { getStackActions } from '$lib/services/stack.service';
|
||||||
import { getSharedLink, withoutIcons } from '$lib/utils';
|
import { getSharedLink, withoutIcons } from '$lib/utils';
|
||||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
@@ -40,7 +36,7 @@
|
|||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
album?: AlbumResponseDto | null;
|
album?: AlbumResponseDto | null;
|
||||||
person?: PersonResponseDto | null;
|
person?: PersonResponseDto | null;
|
||||||
stack?: StackResponseDto | null;
|
stack?: StackResponseDto;
|
||||||
preAction: PreAction;
|
preAction: PreAction;
|
||||||
onAction: OnAction;
|
onAction: OnAction;
|
||||||
onUndoDelete?: OnUndoDelete;
|
onUndoDelete?: OnUndoDelete;
|
||||||
@@ -54,7 +50,7 @@
|
|||||||
asset,
|
asset,
|
||||||
album = null,
|
album = null,
|
||||||
person = null,
|
person = null,
|
||||||
stack = null,
|
stack,
|
||||||
preAction,
|
preAction,
|
||||||
onAction,
|
onAction,
|
||||||
onUndoDelete = undefined,
|
onUndoDelete = undefined,
|
||||||
@@ -86,6 +82,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Actions = $derived(getAssetActions($t, { ...asset, stackPrimaryAssetId: stack?.primaryAssetId }));
|
const Actions = $derived(getAssetActions($t, { ...asset, stackPrimaryAssetId: stack?.primaryAssetId }));
|
||||||
|
const StackActions = $derived(getStackActions($t, stack, asset));
|
||||||
const sharedLink = getSharedLink();
|
const sharedLink = getSharedLink();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -150,19 +147,12 @@
|
|||||||
<RemoveFromAlbumAction {album} onRemove={onRemoveFromAlbum} assetIds={[asset.id]} menuItem />
|
<RemoveFromAlbumAction {album} onRemove={onRemoveFromAlbum} assetIds={[asset.id]} menuItem />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isOwner}
|
<ActionMenuItem action={StackActions.AddUploads} />
|
||||||
<AddToStackAction {asset} {stack} {onAction} />
|
<ActionMenuItem action={StackActions.Unstack} />
|
||||||
{#if stack}
|
<ActionMenuItem action={StackActions.KeepThisDeleteOthers} />
|
||||||
<UnstackAction {stack} {onAction} />
|
<ActionMenuItem action={StackActions.SetPrimaryAsset} />
|
||||||
<KeepThisDeleteOthersAction {stack} {asset} {onAction} />
|
<ActionMenuItem action={StackActions.RemoveAsset} />
|
||||||
{#if stack?.primaryAssetId !== asset.id}
|
|
||||||
<SetStackPrimaryAsset {stack} {asset} {onAction} />
|
|
||||||
{#if stack?.assets?.length > 2}
|
|
||||||
<RemoveAssetFromStack {asset} {stack} {onAction} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#if album}
|
{#if album}
|
||||||
{@const { SetCover } = getAlbumAssetActions($t, album, asset)}
|
{@const { SetCover } = getAlbumAssetActions($t, album, asset)}
|
||||||
<ActionMenuItem action={SetCover} />
|
<ActionMenuItem action={SetCover} />
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
|
||||||
import { AssetAction } from '$lib/constants';
|
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
|
||||||
import { createStack, type AssetResponseDto, type StackResponseDto } from '@immich/sdk';
|
|
||||||
import { mdiUploadMultiple } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { OnAction } from './action';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
asset: AssetResponseDto;
|
|
||||||
stack: StackResponseDto | null;
|
|
||||||
onAction: OnAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { asset, stack, onAction }: Props = $props();
|
|
||||||
|
|
||||||
const handleAddUploadToStack = async () => {
|
|
||||||
const newAssetIds = await openFileUploadDialog({ multiple: true });
|
|
||||||
// Including the old stacks primary asset ID ensures that all assets of the
|
|
||||||
// old stack are automatically included in the new stack.
|
|
||||||
const primaryAssetId = stack?.primaryAssetId ?? asset.id;
|
|
||||||
|
|
||||||
// First asset in the list will become the new primary asset.
|
|
||||||
const assetIds = [primaryAssetId, ...newAssetIds];
|
|
||||||
|
|
||||||
const newStack = await createStack({
|
|
||||||
stackCreateDto: {
|
|
||||||
assetIds,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onAction({ type: AssetAction.STACK, stack: newStack });
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<MenuOption icon={mdiUploadMultiple} onClick={handleAddUploadToStack} text={$t('add_upload_to_stack')} />
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
|
||||||
import { AssetAction } from '$lib/constants';
|
|
||||||
import { keepThisDeleteOthers } from '$lib/utils/asset-utils';
|
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
|
||||||
import type { AssetResponseDto, StackResponseDto } from '@immich/sdk';
|
|
||||||
import { modalManager } from '@immich/ui';
|
|
||||||
import { mdiPinOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { OnAction } from './action';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
stack: StackResponseDto;
|
|
||||||
asset: AssetResponseDto;
|
|
||||||
onAction: OnAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { stack, asset, onAction }: Props = $props();
|
|
||||||
|
|
||||||
const handleKeepThisDeleteOthers = async () => {
|
|
||||||
const isConfirmed = await modalManager.showDialog({
|
|
||||||
title: $t('keep_this_delete_others'),
|
|
||||||
prompt: $t('confirm_keep_this_delete_others'),
|
|
||||||
confirmText: $t('delete_others'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isConfirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keptAsset = await keepThisDeleteOthers(asset, stack);
|
|
||||||
if (keptAsset) {
|
|
||||||
onAction({ type: AssetAction.UNSTACK, assets: [toTimelineAsset(keptAsset)] });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<MenuOption icon={mdiPinOutline} onClick={handleKeepThisDeleteOthers} text={$t('keep_this_delete_others')} />
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
|
||||||
|
|
||||||
import { AssetAction } from '$lib/constants';
|
|
||||||
import { removeAssetFromStack, type AssetResponseDto, type StackResponseDto } from '@immich/sdk';
|
|
||||||
import { mdiImageMinusOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { OnAction } from './action';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
asset: AssetResponseDto;
|
|
||||||
stack: StackResponseDto;
|
|
||||||
onAction: OnAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { asset, stack, onAction }: Props = $props();
|
|
||||||
|
|
||||||
const handleRemoveFromStack = async () => {
|
|
||||||
await removeAssetFromStack({
|
|
||||||
id: stack.id,
|
|
||||||
assetId: asset.id,
|
|
||||||
});
|
|
||||||
const updatedStack = {
|
|
||||||
...stack,
|
|
||||||
assets: stack.assets.filter((a) => a.id !== asset.id),
|
|
||||||
};
|
|
||||||
onAction({ type: AssetAction.REMOVE_ASSET_FROM_STACK, stack: updatedStack, asset });
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<MenuOption icon={mdiImageMinusOutline} onClick={handleRemoveFromStack} text={$t('viewer_remove_from_stack')} />
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
|
||||||
|
|
||||||
import { AssetAction } from '$lib/constants';
|
|
||||||
import { updateStack, type AssetResponseDto, type StackResponseDto } from '@immich/sdk';
|
|
||||||
import { mdiImageCheckOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { OnAction } from './action';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
stack: StackResponseDto;
|
|
||||||
asset: AssetResponseDto;
|
|
||||||
onAction: OnAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { stack, asset, onAction }: Props = $props();
|
|
||||||
|
|
||||||
const handleSetPrimaryAsset = async () => {
|
|
||||||
const updatedStack = await updateStack({ id: stack.id, stackUpdateDto: { primaryAssetId: asset.id } });
|
|
||||||
if (updatedStack) {
|
|
||||||
onAction({ type: AssetAction.SET_STACK_PRIMARY_ASSET, stack: updatedStack });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<MenuOption icon={mdiImageCheckOutline} onClick={handleSetPrimaryAsset} text={$t('set_stack_primary_asset')} />
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
|
||||||
import { AssetAction } from '$lib/constants';
|
|
||||||
import { deleteStack } from '$lib/utils/asset-utils';
|
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
|
||||||
import type { StackResponseDto } from '@immich/sdk';
|
|
||||||
import { mdiImageOffOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { OnAction } from './action';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
stack: StackResponseDto;
|
|
||||||
onAction: OnAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { stack, onAction }: Props = $props();
|
|
||||||
|
|
||||||
const handleUnstack = async () => {
|
|
||||||
const unstackedAssets = await deleteStack([stack.id]);
|
|
||||||
if (unstackedAssets) {
|
|
||||||
onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets.map((asset) => toTimelineAsset(asset)) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<MenuOption icon={mdiImageOffOutline} onClick={handleUnstack} text={$t('unstack')} />
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AssetResponseDto, PersonResponseDto, StackResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto, PersonResponseDto } from '@immich/sdk';
|
||||||
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';
|
||||||
|
|
||||||
@@ -8,10 +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.STACK]: { stack: StackResponseDto };
|
|
||||||
[AssetAction.UNSTACK]: { assets: TimelineAsset[] };
|
|
||||||
[AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto };
|
|
||||||
[AssetAction.REMOVE_ASSET_FROM_STACK]: { stack: StackResponseDto | null; asset: AssetResponseDto };
|
|
||||||
[AssetAction.SET_VISIBILITY_LOCKED]: { asset: TimelineAsset };
|
[AssetAction.SET_VISIBILITY_LOCKED]: { asset: TimelineAsset };
|
||||||
[AssetAction.SET_VISIBILITY_TIMELINE]: { asset: TimelineAsset };
|
[AssetAction.SET_VISIBILITY_TIMELINE]: { asset: TimelineAsset };
|
||||||
[AssetAction.SET_PERSON_FEATURED_PHOTO]: { asset: AssetResponseDto; person: PersonResponseDto };
|
[AssetAction.SET_PERSON_FEATURED_PHOTO]: { asset: AssetResponseDto; person: PersonResponseDto };
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
|
||||||
import { navigateToAsset } from '$lib/utils/asset-utils';
|
import { navigateToAsset } from '$lib/utils/asset-utils';
|
||||||
import { handleErrorAsync } from '$lib/utils/handle-error';
|
import { handleErrorAsync } from '$lib/utils/handle-error';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
@@ -150,52 +149,6 @@
|
|||||||
timelineManager.upsertAssets([action.asset]);
|
timelineManager.upsertAssets([action.asset]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case AssetAction.STACK: {
|
|
||||||
updateStackedAssetInTimeline(timelineManager, {
|
|
||||||
stack: action.stack,
|
|
||||||
toDeleteIds: action.stack.assets
|
|
||||||
.filter((asset) => asset.id !== action.stack.primaryAssetId)
|
|
||||||
.map((asset) => asset.id),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case AssetAction.UNSTACK: {
|
|
||||||
updateUnstackedAssetInTimeline(timelineManager, action.assets);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AssetAction.REMOVE_ASSET_FROM_STACK: {
|
|
||||||
timelineManager.upsertAssets([toTimelineAsset(action.asset)]);
|
|
||||||
if (action.stack) {
|
|
||||||
//Have to unstack then restack assets in timeline in order to update the stack count in the timeline.
|
|
||||||
updateUnstackedAssetInTimeline(
|
|
||||||
timelineManager,
|
|
||||||
action.stack.assets.map((asset) => toTimelineAsset(asset)),
|
|
||||||
);
|
|
||||||
updateStackedAssetInTimeline(timelineManager, {
|
|
||||||
stack: action.stack,
|
|
||||||
toDeleteIds: action.stack.assets
|
|
||||||
.filter((asset) => asset.id !== action.stack?.primaryAssetId)
|
|
||||||
.map((asset) => asset.id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AssetAction.SET_STACK_PRIMARY_ASSET: {
|
|
||||||
//Have to unstack then restack assets in timeline in order for the currently removed new primary asset to be made visible.
|
|
||||||
updateUnstackedAssetInTimeline(
|
|
||||||
timelineManager,
|
|
||||||
action.stack.assets.map((asset) => toTimelineAsset(asset)),
|
|
||||||
);
|
|
||||||
updateStackedAssetInTimeline(timelineManager, {
|
|
||||||
stack: action.stack,
|
|
||||||
toDeleteIds: action.stack.assets
|
|
||||||
.filter((asset) => asset.id !== action.stack.primaryAssetId)
|
|
||||||
.map((asset) => asset.id),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +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 type { OnStack, OnUnstack } from '$lib/utils/actions';
|
|
||||||
import { deleteStack, stackAssets } from '$lib/utils/asset-utils';
|
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
|
||||||
import { mdiImageMultipleOutline, mdiImageOffOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
unstack?: boolean;
|
|
||||||
onStack: OnStack | undefined;
|
|
||||||
onUnstack: OnUnstack | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { unstack = false, onStack, onUnstack }: Props = $props();
|
|
||||||
|
|
||||||
const handleStack = async () => {
|
|
||||||
const result = await stackAssets(assetMultiSelectManager.ownedAssets);
|
|
||||||
onStack?.(result);
|
|
||||||
assetMultiSelectManager.clear();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnstack = async () => {
|
|
||||||
const selectedAssets = assetMultiSelectManager.ownedAssets;
|
|
||||||
if (selectedAssets.length !== 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { stack } = selectedAssets[0];
|
|
||||||
if (!stack) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const unstackedAssets = await deleteStack([stack.id]);
|
|
||||||
if (unstackedAssets) {
|
|
||||||
onUnstack?.(unstackedAssets.map((a) => toTimelineAsset(a)));
|
|
||||||
}
|
|
||||||
assetMultiSelectManager.clear();
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if unstack}
|
|
||||||
<MenuOption text={$t('unstack')} icon={mdiImageOffOutline} onClick={handleUnstack} />
|
|
||||||
{:else}
|
|
||||||
<MenuOption text={$t('stack')} icon={mdiImageMultipleOutline} onClick={handleStack} />
|
|
||||||
{/if}
|
|
||||||
@@ -15,12 +15,13 @@
|
|||||||
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
|
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
|
import { handleStack } from '$lib/services/stack.service';
|
||||||
import { keyboardManager } from '$lib/stores/keyboard-manager.svelte';
|
import { keyboardManager } from '$lib/stores/keyboard-manager.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
import { deleteAssets } from '$lib/utils/actions';
|
||||||
import { archiveAssets, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, selectAllAssets } from '$lib/utils/asset-utils';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { isModalOpen, modalManager } from '@immich/ui';
|
import { isModalOpen, modalManager } from '@immich/ui';
|
||||||
|
|
||||||
@@ -59,10 +60,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onStackAssets = async () => {
|
const onStackAssets = async () => {
|
||||||
const result = await stackAssets(assetInteraction.assets);
|
await handleStack(assetInteraction.assets.map((asset) => asset.id));
|
||||||
|
|
||||||
updateStackedAssetInTimeline(timelineManager, result);
|
|
||||||
|
|
||||||
onEscape?.();
|
onEscape?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ export enum AssetAction {
|
|||||||
TRASH = 'trash',
|
TRASH = 'trash',
|
||||||
DELETE = 'delete',
|
DELETE = 'delete',
|
||||||
RESTORE = 'restore',
|
RESTORE = 'restore',
|
||||||
STACK = 'stack',
|
|
||||||
UNSTACK = 'unstack',
|
|
||||||
SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset',
|
|
||||||
REMOVE_ASSET_FROM_STACK = 'remove-asset-from-stack',
|
|
||||||
SET_VISIBILITY_LOCKED = 'set-visibility-locked',
|
SET_VISIBILITY_LOCKED = 'set-visibility-locked',
|
||||||
SET_VISIBILITY_TIMELINE = 'set-visibility-timeline',
|
SET_VISIBILITY_TIMELINE = 'set-visibility-timeline',
|
||||||
SET_PERSON_FEATURED_PHOTO = 'set-person-featured-photo',
|
SET_PERSON_FEATURED_PHOTO = 'set-person-featured-photo',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
QueueResponseDto,
|
QueueResponseDto,
|
||||||
ReleaseEventV1,
|
ReleaseEventV1,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
|
StackResponseDto,
|
||||||
SystemConfigDto,
|
SystemConfigDto,
|
||||||
TagResponseDto,
|
TagResponseDto,
|
||||||
UserAdminResponseDto,
|
UserAdminResponseDto,
|
||||||
@@ -61,6 +62,11 @@ export type Events = {
|
|||||||
SharedLinkUpdate: [SharedLinkResponseDto];
|
SharedLinkUpdate: [SharedLinkResponseDto];
|
||||||
SharedLinkDelete: [SharedLinkResponseDto];
|
SharedLinkDelete: [SharedLinkResponseDto];
|
||||||
|
|
||||||
|
StackCreate: [StackResponseDto];
|
||||||
|
/** Unstacked, with assets to handle */
|
||||||
|
StackDelete: [{ id: string; assets: AssetResponseDto[] }];
|
||||||
|
StackUpdate: [StackResponseDto];
|
||||||
|
|
||||||
TagCreate: [TagResponseDto];
|
TagCreate: [TagResponseDto];
|
||||||
TagUpdate: [TagResponseDto];
|
TagUpdate: [TagResponseDto];
|
||||||
TagDelete: [TreeNode];
|
TagDelete: [TreeNode];
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
retrieveRange as retrieveRangeUtil,
|
retrieveRange as retrieveRangeUtil,
|
||||||
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||||
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
|
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
|
||||||
|
import { updateStackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
||||||
import {
|
import {
|
||||||
@@ -115,6 +116,23 @@ export class TimelineManager extends VirtualScrollManager {
|
|||||||
this.#unsubscribes.push(
|
this.#unsubscribes.push(
|
||||||
eventManager.on({
|
eventManager.on({
|
||||||
AssetUpdate: (asset: AssetResponseDto) => this.#updateAssets([toTimelineAsset(asset)]),
|
AssetUpdate: (asset: AssetResponseDto) => this.#updateAssets([toTimelineAsset(asset)]),
|
||||||
|
StackCreate: (stack) => updateStackedAssetInTimeline(this, stack),
|
||||||
|
StackDelete: ({ assets }) => {
|
||||||
|
this.update(
|
||||||
|
assets.map((asset) => asset.id),
|
||||||
|
(asset) => (asset.stack = null),
|
||||||
|
);
|
||||||
|
this.upsertAssets(assets.map((asset) => toTimelineAsset(asset)));
|
||||||
|
},
|
||||||
|
StackUpdate: (stack) => {
|
||||||
|
// unstack and re-stack
|
||||||
|
this.update(
|
||||||
|
stack.assets.map((asset) => asset.id),
|
||||||
|
(asset) => (asset.stack = null),
|
||||||
|
);
|
||||||
|
this.upsertAssets(stack.assets.map((asset) => toTimelineAsset(asset)));
|
||||||
|
updateStackedAssetInTimeline(this, stack);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import {
|
||||||
|
createStack,
|
||||||
|
deleteAssets,
|
||||||
|
deleteStacks,
|
||||||
|
getStack,
|
||||||
|
removeAssetFromStack,
|
||||||
|
updateStack,
|
||||||
|
type AssetResponseDto,
|
||||||
|
type AssetStackResponseDto,
|
||||||
|
type StackResponseDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
|
import {
|
||||||
|
mdiImageCheckOutline,
|
||||||
|
mdiImageMinusOutline,
|
||||||
|
mdiImageMultipleOutline,
|
||||||
|
mdiImageOffOutline,
|
||||||
|
mdiPinOutline,
|
||||||
|
mdiUploadMultiple,
|
||||||
|
} from '@mdi/js';
|
||||||
|
import { type MessageFormatter } from 'svelte-i18n';
|
||||||
|
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||||
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
|
||||||
|
export const getStackBulkActions = ($t: MessageFormatter) => {
|
||||||
|
const Stack: ActionItem = {
|
||||||
|
title: $t('stack'),
|
||||||
|
icon: mdiImageMultipleOutline,
|
||||||
|
$if: () => assetMultiSelectManager.ownedAssets.length > 1,
|
||||||
|
onAction: () => handleStack(assetMultiSelectManager.ownedAssets.map((asset) => asset.id)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Unstack: ActionItem = {
|
||||||
|
title: $t('unstack'),
|
||||||
|
icon: mdiImageOffOutline,
|
||||||
|
$if: () => assetMultiSelectManager.ownedAssets.every((asset) => !!asset.stack),
|
||||||
|
onAction: () => handleDeleteStacks(assetMultiSelectManager.ownedAssets.map(({ stack }) => stack!)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { Stack, Unstack };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStackActions = ($t: MessageFormatter, stack: StackResponseDto | undefined, asset: AssetResponseDto) => {
|
||||||
|
const authUser = authManager.authenticated ? authManager.user : undefined;
|
||||||
|
const isAssetOwner = !!(authUser && authUser.id === asset.ownerId);
|
||||||
|
const validStack = !!stack && isAssetOwner;
|
||||||
|
|
||||||
|
const AddUploads: ActionItem = {
|
||||||
|
title: $t('add_upload_to_stack', { values: { isStack: !!stack } }),
|
||||||
|
icon: mdiUploadMultiple,
|
||||||
|
$if: () => isAssetOwner,
|
||||||
|
onAction: () => handleAddUploadToStack(stack, asset),
|
||||||
|
};
|
||||||
|
|
||||||
|
const KeepThisDeleteOthers: ActionItem = {
|
||||||
|
title: $t('keep_this_delete_others'),
|
||||||
|
icon: mdiPinOutline,
|
||||||
|
$if: () => validStack,
|
||||||
|
onAction: () => handleKeepThisDeleteOthers(stack!, asset),
|
||||||
|
};
|
||||||
|
|
||||||
|
const RemoveAsset: ActionItem = {
|
||||||
|
title: $t('viewer_remove_from_stack'),
|
||||||
|
icon: mdiImageMinusOutline,
|
||||||
|
$if: () => validStack && stack?.primaryAssetId !== asset.id && stack?.assets?.length > 2,
|
||||||
|
onAction: () => handleRemoveFromStack(stack!, asset),
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetPrimaryAsset: ActionItem = {
|
||||||
|
title: $t('set_stack_primary_asset'),
|
||||||
|
icon: mdiImageCheckOutline,
|
||||||
|
$if: () => validStack && stack!.primaryAssetId !== asset.id,
|
||||||
|
onAction: () => handleSetPrimaryAsset(stack!, asset),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Unstack: ActionItem = {
|
||||||
|
title: $t('unstack'),
|
||||||
|
icon: mdiImageOffOutline,
|
||||||
|
$if: () => validStack,
|
||||||
|
onAction: () => handleDeleteStack(stack!),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { AddUploads, KeepThisDeleteOthers, RemoveAsset, SetPrimaryAsset, Unstack };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddUploadToStack = async (stack: StackResponseDto | undefined, asset: AssetResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
const newAssetIds = await openFileUploadDialog({ multiple: true });
|
||||||
|
// Including the old stack's primary asset ID ensures that all assets of the
|
||||||
|
// old stack are automatically included in the new stack.
|
||||||
|
const primaryAssetId = stack?.primaryAssetId ?? asset.id;
|
||||||
|
|
||||||
|
// First asset in the list will become the new primary asset.
|
||||||
|
const assetIds = [primaryAssetId, ...newAssetIds];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stack = await createStack({ stackCreateDto: { assetIds } });
|
||||||
|
|
||||||
|
toastManager.primary($t('stack_created'));
|
||||||
|
eventManager.emit('StackCreate', stack);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.failed_to_stack_assets'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeepThisDeleteOthers = async (stack: StackResponseDto, asset: AssetResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
const isConfirmed = await modalManager.showDialog({
|
||||||
|
title: $t('keep_this_delete_others'),
|
||||||
|
prompt: $t('confirm_keep_this_delete_others'),
|
||||||
|
confirmText: $t('delete_others'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const assetsToDeleteIds = stack.assets.filter((a) => a.id !== asset.id).map((asset) => asset.id);
|
||||||
|
await deleteAssets({ assetBulkDeleteDto: { ids: assetsToDeleteIds } });
|
||||||
|
await deleteStacks({ bulkIdsDto: { ids: [stack.id] } });
|
||||||
|
|
||||||
|
toastManager.primary($t('kept_this_deleted_others', { values: { count: assetsToDeleteIds.length } }));
|
||||||
|
eventManager.emit('StackDelete', { id: stack.id, assets: [asset] });
|
||||||
|
eventManager.emit('AssetUpdate', { ...asset, stack: null });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.failed_to_keep_this_delete_others'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromStack = async (stack: StackResponseDto, asset: AssetResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeAssetFromStack({ id: stack.id, assetId: asset.id });
|
||||||
|
const updatedStack = {
|
||||||
|
...stack,
|
||||||
|
assets: stack.assets.filter((a) => a.id !== asset.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
toastManager.primary($t('removed_from_stack'));
|
||||||
|
eventManager.emit('AssetUpdate', asset); // todo: check if this re-inserts into timeline
|
||||||
|
eventManager.emit('StackUpdate', updatedStack);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.failed_to_unstack_assets'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetPrimaryAsset = async (stack: StackResponseDto, asset: AssetResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedStack = await updateStack({ id: stack.id, stackUpdateDto: { primaryAssetId: asset.id } });
|
||||||
|
|
||||||
|
// todo: toast?
|
||||||
|
eventManager.emit('StackUpdate', updatedStack);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.something_went_wrong'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleStack = async (assetIds: string[]) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stack = await createStack({ stackCreateDto: { assetIds } });
|
||||||
|
|
||||||
|
toastManager.primary({
|
||||||
|
description: $t('stacked_assets_count', { values: { count: stack.assets.length } }),
|
||||||
|
button: {
|
||||||
|
label: $t('view_stack'),
|
||||||
|
onclick: () => navigate({ targetRoute: 'current', assetId: stack.primaryAssetId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
eventManager.emit('StackCreate', stack);
|
||||||
|
assetMultiSelectManager.clear();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.failed_to_stack_assets'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleDeleteStack = async (stack: StackResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteStacks({ bulkIdsDto: { ids: [stack.id] } });
|
||||||
|
|
||||||
|
const assetIds = stack.assets.map((asset) => asset.id);
|
||||||
|
|
||||||
|
toastManager.primary($t('unstacked_assets_count', { values: { count: assetIds.length } }));
|
||||||
|
eventManager.emit('StackDelete', stack);
|
||||||
|
|
||||||
|
return assetIds;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.failed_to_unstack_assets'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteStacks = async (assetStacks: AssetStackResponseDto[]) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stacks = await Promise.all(assetStacks.map((stack) => getStack(stack)));
|
||||||
|
await Promise.all(stacks.map((stack) => handleDeleteStack(stack)));
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.failed_to_unstack_assets'));
|
||||||
|
}
|
||||||
|
assetMultiSelectManager.clear();
|
||||||
|
};
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets } from '@immich/sdk';
|
import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets, type StackResponseDto } from '@immich/sdk';
|
||||||
import { toastManager } from '@immich/ui';
|
import { toastManager } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
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 type { StackResponse } from '$lib/utils/asset-utils';
|
|
||||||
import { handleError } from './handle-error';
|
import { handleError } from './handle-error';
|
||||||
|
|
||||||
export type OnDelete = (assetIds: string[]) => void;
|
export type OnDelete = (assetIds: string[]) => void;
|
||||||
@@ -15,8 +14,6 @@ export type OnUnlink = (assets: { still: TimelineAsset; motion: TimelineAsset })
|
|||||||
export type OnAddToAlbum = (ids: string[], albumId: string) => void;
|
export type OnAddToAlbum = (ids: string[], albumId: string) => void;
|
||||||
export type OnArchive = (ids: string[], visibility: AssetVisibility) => void;
|
export type OnArchive = (ids: string[], visibility: AssetVisibility) => void;
|
||||||
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
||||||
export type OnStack = (result: StackResponse) => void;
|
|
||||||
export type OnUnstack = (assets: TimelineAsset[]) => void;
|
|
||||||
export type OnSetVisibility = (ids: string[]) => void;
|
export type OnSetVisibility = (ids: string[]) => void;
|
||||||
|
|
||||||
export const deleteAssets = async (
|
export const deleteAssets = async (
|
||||||
@@ -62,43 +59,18 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse
|
|||||||
/**
|
/**
|
||||||
* Update the asset stack state in the asset store based on the provided stack response.
|
* Update the asset stack state in the asset store based on the provided stack response.
|
||||||
* This function updates the stack information so that the icon is shown for the primary asset
|
* This function updates the stack information so that the icon is shown for the primary asset
|
||||||
* and removes any assets from the timeline that are marked for deletion.
|
* and removes the non-primary assets from the timeline.
|
||||||
*
|
|
||||||
* @param {TimelineManager} timelineManager - The timeline manager to update.
|
|
||||||
* @param {StackResponse} stackResponse - The stack response containing the stack and assets to delete.
|
|
||||||
*/
|
*/
|
||||||
export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { stack, toDeleteIds }: StackResponse) {
|
export function updateStackedAssetInTimeline(timelineManager: TimelineManager, stack: StackResponseDto) {
|
||||||
if (stack != undefined) {
|
timelineManager.update([stack.primaryAssetId], (asset) => {
|
||||||
timelineManager.update(
|
asset.stack = {
|
||||||
[stack.primaryAssetId],
|
|
||||||
(asset) =>
|
|
||||||
(asset.stack = {
|
|
||||||
id: stack.id,
|
id: stack.id,
|
||||||
primaryAssetId: stack.primaryAssetId,
|
primaryAssetId: stack.primaryAssetId,
|
||||||
assetCount: stack.assets.length,
|
assetCount: stack.assets.length,
|
||||||
}),
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
timelineManager.removeAssets(
|
||||||
|
stack.assets.filter((asset) => asset.id !== stack.primaryAssetId).map((asset) => asset.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
timelineManager.removeAssets(toDeleteIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the timeline manager to reflect the unstacked state of assets.
|
|
||||||
* This function updates the stack property of each asset to undefined, effectively unstacking them.
|
|
||||||
* It also adds the unstacked assets back to the timeline manager.
|
|
||||||
*
|
|
||||||
* @param timelineManager - The timeline manager to update.
|
|
||||||
* @param assets - The array of asset response DTOs to update in the timeline manager.
|
|
||||||
*/
|
|
||||||
export function updateUnstackedAssetInTimeline(timelineManager: TimelineManager, assets: TimelineAsset[]) {
|
|
||||||
timelineManager.update(
|
|
||||||
assets.map((asset) => asset.id),
|
|
||||||
(asset) => {
|
|
||||||
asset.stack = null;
|
|
||||||
return { remove: false };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
timelineManager.upsertAssets(assets);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
bulkTagAssets,
|
bulkTagAssets,
|
||||||
createStack,
|
|
||||||
deleteAssets,
|
|
||||||
deleteStacks,
|
|
||||||
getBaseUrl,
|
getBaseUrl,
|
||||||
getDownloadInfo,
|
getDownloadInfo,
|
||||||
getStack,
|
|
||||||
untagAssets,
|
untagAssets,
|
||||||
updateAsset,
|
updateAsset,
|
||||||
updateAssets,
|
updateAssets,
|
||||||
@@ -14,7 +10,6 @@ import {
|
|||||||
type AssetTypeEnum,
|
type AssetTypeEnum,
|
||||||
type DownloadInfoDto,
|
type DownloadInfoDto,
|
||||||
type ExifResponseDto,
|
type ExifResponseDto,
|
||||||
type StackResponseDto,
|
|
||||||
type UserResponseDto,
|
type UserResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { toastManager } from '@immich/ui';
|
import { toastManager } from '@immich/ui';
|
||||||
@@ -281,84 +276,6 @@ export const getOwnedAssetsWithWarning = (assets: TimelineAsset[], user: UserRes
|
|||||||
return ids;
|
return ids;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StackResponse = {
|
|
||||||
stack?: StackResponseDto;
|
|
||||||
toDeleteIds: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stackAssets = async (assets: { id: string }[], showNotification = true): Promise<StackResponse> => {
|
|
||||||
if (assets.length < 2) {
|
|
||||||
return { stack: undefined, toDeleteIds: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const $t = get(t);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stack = await createStack({ stackCreateDto: { assetIds: assets.map(({ id }) => id) } });
|
|
||||||
if (showNotification) {
|
|
||||||
toastManager.primary({
|
|
||||||
description: $t('stacked_assets_count', { values: { count: stack.assets.length } }),
|
|
||||||
button: {
|
|
||||||
label: $t('view_stack'),
|
|
||||||
onclick: () => navigate({ targetRoute: 'current', assetId: stack.primaryAssetId }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
stack,
|
|
||||||
toDeleteIds: assets.slice(1).map((asset) => asset.id),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.failed_to_stack_assets'));
|
|
||||||
return { stack: undefined, toDeleteIds: [] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteStack = async (stackIds: string[]) => {
|
|
||||||
const ids = [...new Set(stackIds)];
|
|
||||||
if (ids.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $t = get(t);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stacks = await Promise.all(ids.map((id) => getStack({ id })));
|
|
||||||
const count = stacks.reduce((sum, stack) => sum + stack.assets.length, 0);
|
|
||||||
|
|
||||||
await deleteStacks({ bulkIdsDto: { ids: [...ids] } });
|
|
||||||
|
|
||||||
toastManager.primary($t('unstacked_assets_count', { values: { count } }));
|
|
||||||
|
|
||||||
const assets = stacks.flatMap((stack) => stack.assets);
|
|
||||||
for (const asset of assets) {
|
|
||||||
asset.stack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return assets;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.failed_to_unstack_assets'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: StackResponseDto) => {
|
|
||||||
const $t = get(t);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const assetsToDeleteIds = stack.assets.filter((asset) => asset.id !== keepAsset.id).map((asset) => asset.id);
|
|
||||||
await deleteAssets({ assetBulkDeleteDto: { ids: assetsToDeleteIds } });
|
|
||||||
await deleteStacks({ bulkIdsDto: { ids: [stack.id] } });
|
|
||||||
|
|
||||||
toastManager.primary($t('kept_this_deleted_others', { values: { count: assetsToDeleteIds.length } }));
|
|
||||||
|
|
||||||
keepAsset.stack = null;
|
|
||||||
return keepAsset;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.failed_to_keep_this_delete_others'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const selectAllAssets = async (timelineManager: TimelineManager, assetInteraction: AssetMultiSelectManager) => {
|
export const selectAllAssets = async (timelineManager: TimelineManager, assetInteraction: AssetMultiSelectManager) => {
|
||||||
if (assetInteraction.selectAll) {
|
if (assetInteraction.selectAll) {
|
||||||
// Selection is already ongoing
|
// Selection is already ongoing
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
import LinkLivePhotoAction from '$lib/components/timeline/actions/LinkLivePhotoAction.svelte';
|
import LinkLivePhotoAction from '$lib/components/timeline/actions/LinkLivePhotoAction.svelte';
|
||||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||||
import StackAction from '$lib/components/timeline/actions/StackAction.svelte';
|
|
||||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
import TagAction from '$lib/components/timeline/actions/TagAction.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';
|
||||||
@@ -22,13 +21,9 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.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 { getAssetBulkActions } from '$lib/services/asset.service';
|
||||||
|
import { getStackBulkActions } from '$lib/services/stack.service';
|
||||||
import { mapSettings } from '$lib/stores/preferences.store';
|
import { mapSettings } from '$lib/stores/preferences.store';
|
||||||
import {
|
import { type OnLink, type OnUnlink } from '$lib/utils/actions';
|
||||||
updateStackedAssetInTimeline,
|
|
||||||
updateUnstackedAssetInTimeline,
|
|
||||||
type OnLink,
|
|
||||||
type OnUnlink,
|
|
||||||
} from '$lib/utils/actions';
|
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { ActionButton, CloseButton, CommandPaletteDefaultProvider, Icon } from '@immich/ui';
|
import { ActionButton, CloseButton, CommandPaletteDefaultProvider, Icon } from '@immich/ui';
|
||||||
import { mdiDotsVertical, mdiImageMultiple } from '@mdi/js';
|
import { mdiDotsVertical, mdiImageMultiple } from '@mdi/js';
|
||||||
@@ -46,7 +41,6 @@
|
|||||||
|
|
||||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||||
let selectedAssets = $derived(assetMultiSelectManager.assets);
|
let selectedAssets = $derived(assetMultiSelectManager.assets);
|
||||||
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
|
|
||||||
let isLinkActionAvailable = $derived.by(() => {
|
let isLinkActionAvailable = $derived.by(() => {
|
||||||
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
||||||
const isLivePhotoCandidate =
|
const isLivePhotoCandidate =
|
||||||
@@ -85,6 +79,7 @@
|
|||||||
visibility: $mapSettings.includeArchived ? undefined : AssetVisibility.Timeline,
|
visibility: $mapSettings.includeArchived ? undefined : AssetVisibility.Timeline,
|
||||||
isFavorite: $mapSettings.onlyFavorites || undefined,
|
isFavorite: $mapSettings.onlyFavorites || undefined,
|
||||||
withPartners: $mapSettings.withPartners || undefined,
|
withPartners: $mapSettings.withPartners || undefined,
|
||||||
|
withStacked: true,
|
||||||
assetFilter: selectedClusterIds,
|
assetFilter: selectedClusterIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,12 +108,14 @@
|
|||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
assetInteraction={assetMultiSelectManager}
|
assetInteraction={assetMultiSelectManager}
|
||||||
showArchiveIcon
|
showArchiveIcon
|
||||||
|
withStacked
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{@const Actions = getAssetBulkActions($t)}
|
||||||
|
{@const StackActions = getStackBulkActions($t)}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
@@ -135,13 +132,8 @@
|
|||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
{#if assetMultiSelectManager.assets.length > 1 || isAssetStackSelected}
|
<ActionMenuItem action={StackActions.Stack} />
|
||||||
<StackAction
|
<ActionMenuItem action={StackActions.Unstack} />
|
||||||
unstack={isAssetStackSelected}
|
|
||||||
onStack={(result) => updateStackedAssetInTimeline(timelineManager, result)}
|
|
||||||
onUnstack={(assets) => updateUnstackedAssetInTimeline(timelineManager, assets)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if isLinkActionAvailable}
|
{#if isLinkActionAvailable}
|
||||||
<LinkLivePhotoAction
|
<LinkLivePhotoAction
|
||||||
menuItem
|
menuItem
|
||||||
|
|||||||
+4
-1
@@ -481,7 +481,10 @@
|
|||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
menuItem
|
menuItem
|
||||||
unarchive={assetMultiSelectManager.isAllArchived}
|
unarchive={assetMultiSelectManager.isAllArchived}
|
||||||
onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))}
|
onArchive={(ids, visibility) =>
|
||||||
|
timelineManager.update(ids, (asset) => {
|
||||||
|
asset.visibility = visibility;
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
{#if authManager.preferences.tags.enabled && assetMultiSelectManager.isAllUserOwned}
|
{#if authManager.preferences.tags.enabled && assetMultiSelectManager.isAllUserOwned}
|
||||||
<TagAction menuItem />
|
<TagAction menuItem />
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
import LinkLivePhotoAction from '$lib/components/timeline/actions/LinkLivePhotoAction.svelte';
|
import LinkLivePhotoAction from '$lib/components/timeline/actions/LinkLivePhotoAction.svelte';
|
||||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||||
import StackAction from '$lib/components/timeline/actions/StackAction.svelte';
|
|
||||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
import TagAction from '$lib/components/timeline/actions/TagAction.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';
|
||||||
@@ -26,13 +25,9 @@
|
|||||||
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 { getAssetBulkActions } from '$lib/services/asset.service';
|
||||||
|
import { getStackBulkActions } from '$lib/services/stack.service';
|
||||||
import { getAssetMediaUrl, memoryLaneTitle } from '$lib/utils';
|
import { getAssetMediaUrl, memoryLaneTitle } from '$lib/utils';
|
||||||
import {
|
import { type OnLink, type OnUnlink } from '$lib/utils/actions';
|
||||||
updateStackedAssetInTimeline,
|
|
||||||
updateUnstackedAssetInTimeline,
|
|
||||||
type OnLink,
|
|
||||||
type OnUnlink,
|
|
||||||
} from '$lib/utils/actions';
|
|
||||||
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';
|
||||||
@@ -45,7 +40,6 @@
|
|||||||
const options = { visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true };
|
const options = { visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true };
|
||||||
|
|
||||||
let selectedAssets = $derived(assetMultiSelectManager.assets);
|
let selectedAssets = $derived(assetMultiSelectManager.assets);
|
||||||
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
|
|
||||||
let isLinkActionAvailable = $derived.by(() => {
|
let isLinkActionAvailable = $derived.by(() => {
|
||||||
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
||||||
const isLivePhotoCandidate =
|
const isLivePhotoCandidate =
|
||||||
@@ -114,6 +108,7 @@
|
|||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{@const Actions = getAssetBulkActions($t)}
|
||||||
|
{@const StackActions = getStackBulkActions($t)}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<CreateSharedLink />
|
<CreateSharedLink />
|
||||||
@@ -128,13 +123,8 @@
|
|||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
{#if assetMultiSelectManager.assets.length > 1 || isAssetStackSelected}
|
<ActionMenuItem action={StackActions.Stack} />
|
||||||
<StackAction
|
<ActionMenuItem action={StackActions.Unstack} />
|
||||||
unstack={isAssetStackSelected}
|
|
||||||
onStack={(result) => updateStackedAssetInTimeline(timelineManager, result)}
|
|
||||||
onUnstack={(assets) => updateUnstackedAssetInTimeline(timelineManager, assets)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if isLinkActionAvailable}
|
{#if isLinkActionAvailable}
|
||||||
<LinkLivePhotoAction
|
<LinkLivePhotoAction
|
||||||
menuItem
|
menuItem
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
import LinkLivePhotoAction from '$lib/components/timeline/actions/LinkLivePhotoAction.svelte';
|
import LinkLivePhotoAction from '$lib/components/timeline/actions/LinkLivePhotoAction.svelte';
|
||||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||||
import StackAction from '$lib/components/timeline/actions/StackAction.svelte';
|
|
||||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
import TagAction from '$lib/components/timeline/actions/TagAction.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';
|
||||||
@@ -24,18 +23,14 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.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 { getAssetBulkActions } from '$lib/services/asset.service';
|
||||||
import {
|
import { type OnLink, type OnUnlink } from '$lib/utils/actions';
|
||||||
updateStackedAssetInTimeline,
|
|
||||||
updateUnstackedAssetInTimeline,
|
|
||||||
type OnLink,
|
|
||||||
type OnUnlink,
|
|
||||||
} from '$lib/utils/actions';
|
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { AssetVisibility, AssetOrderBy } from '@immich/sdk';
|
import { AssetVisibility, AssetOrderBy } from '@immich/sdk';
|
||||||
import { ActionButton, CommandPaletteDefaultProvider } from '@immich/ui';
|
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';
|
||||||
|
import { getStackBulkActions } from '$lib/services/stack.service';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@@ -52,7 +47,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let selectedAssets = $derived(assetMultiSelectManager.assets);
|
let selectedAssets = $derived(assetMultiSelectManager.assets);
|
||||||
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
|
|
||||||
let isLinkActionAvailable = $derived.by(() => {
|
let isLinkActionAvailable = $derived.by(() => {
|
||||||
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
||||||
const isLivePhotoCandidate =
|
const isLivePhotoCandidate =
|
||||||
@@ -108,6 +102,7 @@
|
|||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{@const Actions = getAssetBulkActions($t)}
|
||||||
|
{@const StackActions = getStackBulkActions($t)}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<CreateSharedLink />
|
<CreateSharedLink />
|
||||||
@@ -122,13 +117,8 @@
|
|||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
{#if assetMultiSelectManager.assets.length > 1 || isAssetStackSelected}
|
<ActionMenuItem action={StackActions.Stack} />
|
||||||
<StackAction
|
<ActionMenuItem action={StackActions.Unstack} />
|
||||||
unstack={isAssetStackSelected}
|
|
||||||
onStack={(result) => updateStackedAssetInTimeline(timelineManager, result)}
|
|
||||||
onUnstack={(assets) => updateUnstackedAssetInTimeline(timelineManager, assets)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if isLinkActionAvailable}
|
{#if isLinkActionAvailable}
|
||||||
<LinkLivePhotoAction
|
<LinkLivePhotoAction
|
||||||
menuItem
|
menuItem
|
||||||
|
|||||||
Reference in New Issue
Block a user