refactor: adjust favorite, delete, and archive actions for timeline

- Pass TimelineManager instance to timeline action components
  instead of callbacks
- Move asset update logic (delete, archive, favorite) into
  action components
This commit is contained in:
midzelis 2025-09-29 00:18:56 +00:00
parent e5fce47c0c
commit 98ab224791
12 changed files with 74 additions and 104 deletions

View file

@ -161,7 +161,24 @@ export class NotificationService extends BaseService {
const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]); const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]);
if (asset) { if (asset) {
this.eventRepository.clientSend('on_asset_update', userId, mapAsset(asset)); // need to specify authDto to this mapAsset request, because it tries to prevent
// leaking information PR#7580 which expects a userId in the auth options object
this.eventRepository.clientSend(
'on_asset_update',
userId,
mapAsset(asset, {
auth: {
user: {
id: userId,
isAdmin: false,
name: '',
email: '',
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
},
},
}),
);
} }
} }

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { OnArchive } from '$lib/utils/actions'; import type { OnArchive } from '$lib/utils/actions';
import { archiveAssets } from '$lib/utils/asset-utils'; import { archiveAssets } from '$lib/utils/asset-utils';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
@ -12,9 +13,10 @@
onArchive?: OnArchive; onArchive?: OnArchive;
menuItem?: boolean; menuItem?: boolean;
unarchive?: boolean; unarchive?: boolean;
manager?: TimelineManager;
} }
let { onArchive, menuItem = false, unarchive = false }: Props = $props(); let { onArchive, menuItem = false, unarchive = false, manager }: Props = $props();
let text = $derived(unarchive ? $t('unarchive') : $t('to_archive')); let text = $derived(unarchive ? $t('unarchive') : $t('to_archive'));
let icon = $derived(unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline); let icon = $derived(unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline);
@ -24,12 +26,13 @@
const { clearSelect, getOwnedAssets } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleArchive = async () => { const handleArchive = async () => {
const isArchived = unarchive ? AssetVisibility.Timeline : AssetVisibility.Archive; const visibility = unarchive ? AssetVisibility.Timeline : AssetVisibility.Archive;
const assets = [...getOwnedAssets()].filter((asset) => asset.visibility !== isArchived); const assets = [...getOwnedAssets()].filter((asset) => asset.visibility !== visibility);
loading = true; loading = true;
const ids = await archiveAssets(assets, isArchived as AssetVisibility); const ids = await archiveAssets(assets, visibility as AssetVisibility);
if (ids) { if (ids) {
onArchive?.(ids, isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline); manager?.updateAssetOperation(ids, (asset) => ((asset.visibility = visibility), void 0));
onArchive?.(ids, visibility ? AssetVisibility.Archive : AssetVisibility.Timeline);
clearSelect(); clearSelect();
} }
loading = false; loading = false;

View file

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte'; import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions'; import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions';
import { IconButton } from '@immich/ui'; import { IconButton } from '@immich/ui';
@ -9,13 +11,14 @@
import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
interface Props { interface Props {
onAssetDelete: OnDelete; onAssetDelete?: OnDelete;
onUndoDelete?: OnUndoDelete | undefined; onUndoDelete?: OnUndoDelete;
menuItem?: boolean; menuItem?: boolean;
force?: boolean; force?: boolean;
manager?: TimelineManager;
} }
let { onAssetDelete, onUndoDelete = undefined, menuItem = false, force = !$featureFlags.trash }: Props = $props(); let { onAssetDelete, onUndoDelete, menuItem = false, force = !$featureFlags.trash, manager }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
@ -36,7 +39,12 @@
const handleDelete = async () => { const handleDelete = async () => {
loading = true; loading = true;
const assets = [...getOwnedAssets()]; const assets = [...getOwnedAssets()];
await deleteAssets(force, onAssetDelete, assets, onUndoDelete); const undo = (assets: TimelineAsset[]) => {
manager?.addAssets(assets);
onUndoDelete?.(assets);
};
await deleteAssets(force, onAssetDelete, assets, undo);
manager?.removeAssets(assets.map((asset) => asset.id));
clearSelect(); clearSelect();
isShowConfirmation = false; isShowConfirmation = false;
loading = false; loading = false;

View file

@ -5,6 +5,7 @@
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { OnFavorite } from '$lib/utils/actions'; import type { OnFavorite } from '$lib/utils/actions';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { updateAssets } from '@immich/sdk'; import { updateAssets } from '@immich/sdk';
@ -16,9 +17,10 @@
onFavorite?: OnFavorite; onFavorite?: OnFavorite;
menuItem?: boolean; menuItem?: boolean;
removeFavorite: boolean; removeFavorite: boolean;
manager?: TimelineManager;
} }
let { onFavorite, menuItem = false, removeFavorite }: Props = $props(); let { onFavorite, menuItem = false, removeFavorite, manager }: Props = $props();
let text = $derived(removeFavorite ? $t('remove_from_favorites') : $t('to_favorite')); let text = $derived(removeFavorite ? $t('remove_from_favorites') : $t('to_favorite'));
let icon = $derived(removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline); let icon = $derived(removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline);
@ -39,11 +41,7 @@
if (ids.length > 0) { if (ids.length > 0) {
await updateAssets({ assetBulkUpdateDto: { ids, isFavorite } }); await updateAssets({ assetBulkUpdateDto: { ids, isFavorite } });
} }
manager?.updateAssetOperation(ids, (asset) => ((asset.isFavorite = isFavorite), void 0));
for (const asset of assets) {
asset.isFavorite = isFavorite;
}
onFavorite?.(ids, isFavorite); onFavorite?.(ids, isFavorite);
notificationController.show({ notificationController.show({

View file

@ -129,7 +129,7 @@ export class DayGroup {
const asset = this.viewerAssets[index].asset!; const asset = this.viewerAssets[index].asset!;
const oldTime = { ...asset.localDateTime }; const oldTime = { ...asset.localDateTime };
let { remove } = operation(asset); let { remove } = operation(asset) ?? { remove: false };
const newTime = asset.localDateTime; const newTime = asset.localDateTime;
if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) { if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) {
const { year, month, day } = newTime; const { year, month, day } = newTime;

View file

@ -35,7 +35,7 @@ export type TimelineAsset = {
longitude?: number | null; longitude?: number | null;
}; };
export type AssetOperation = (asset: TimelineAsset) => { remove: boolean }; export type AssetOperation = (asset: TimelineAsset) => { remove: boolean } | void;
export type MoveAsset = { asset: TimelineAsset; date: TimelineDate }; export type MoveAsset = { asset: TimelineAsset; date: TimelineDate };

View file

@ -21,15 +21,15 @@ export type OnSetVisibility = (ids: string[]) => void;
export const deleteAssets = async ( export const deleteAssets = async (
force: boolean, force: boolean,
onAssetDelete: OnDelete, onAssetDelete: OnDelete | undefined,
assets: TimelineAsset[], assets: TimelineAsset[],
onUndoDelete: OnUndoDelete | undefined = undefined, onUndoDelete: OnUndoDelete | undefined,
) => { ) => {
const $t = get(t); const $t = get(t);
try { try {
const ids = assets.map((a) => a.id); const ids = assets.map((a) => a.id);
await deleteBulk({ assetBulkDeleteDto: { ids, force } }); await deleteBulk({ assetBulkDeleteDto: { ids, force } });
onAssetDelete(ids); onAssetDelete?.(ids);
notificationController.show({ notificationController.show({
message: force message: force

View file

@ -34,7 +34,6 @@
import { AlbumPageViewMode, AppRoute } from '$lib/constants'; import { AlbumPageViewMode, AppRoute } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte'; import { activityManager } from '$lib/managers/activity-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte'; import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte'; import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import AlbumUsersModal from '$lib/modals/AlbumUsersModal.svelte'; import AlbumUsersModal from '$lib/modals/AlbumUsersModal.svelte';
@ -262,18 +261,15 @@
} }
}; };
const handleSetVisibility = (assetIds: string[]) => { const handleSetVisibility = () => {
timelineManager.removeAssets(assetIds);
assetInteraction.clearMultiselect(); assetInteraction.clearMultiselect();
}; };
const handleRemoveAssets = async (assetIds: string[]) => { const handleRemoveAssets = async () => {
timelineManager.removeAssets(assetIds);
await refreshAlbum(); await refreshAlbum();
}; };
const handleUndoRemoveAssets = async (assets: TimelineAsset[]) => { const handleUndoRemoveAssets = async () => {
timelineManager.addAssets(assets);
await refreshAlbum(); await refreshAlbum();
}; };
@ -572,14 +568,7 @@
<AddToAlbum shared /> <AddToAlbum shared />
</ButtonContextMenu> </ButtonContextMenu>
{#if assetInteraction.isAllUserOwned} {#if assetInteraction.isAllUserOwned}
<FavoriteAction <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} manager={timelineManager}></FavoriteAction>
removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) =>
timelineManager.updateAssetOperation(ids, (asset) => {
asset.isFavorite = isFavorite;
return { remove: false };
})}
></FavoriteAction>
{/if} {/if}
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')} offset={{ x: 175, y: 25 }}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')} offset={{ x: 175, y: 25 }}>
<DownloadAction menuItem filename="{album.albumName}.zip" /> <DownloadAction menuItem filename="{album.albumName}.zip" />
@ -594,7 +583,7 @@
onClick={() => updateThumbnailUsingCurrentSelection()} onClick={() => updateThumbnailUsingCurrentSelection()}
/> />
{/if} {/if}
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} manager={timelineManager} />
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} /> <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
{/if} {/if}
@ -606,7 +595,12 @@
<RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} /> <RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} />
{/if} {/if}
{#if assetInteraction.isAllUserOwned} {#if assetInteraction.isAllUserOwned}
<DeleteAssets menuItem onAssetDelete={handleRemoveAssets} onUndoDelete={handleUndoRemoveAssets} /> <DeleteAssets
menuItem
onAssetDelete={handleRemoveAssets}
onUndoDelete={handleUndoRemoveAssets}
manager={timelineManager}
/>
{/if} {/if}
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>

View file

@ -65,32 +65,18 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
<ArchiveAction <ArchiveAction unarchive manager={timelineManager} />
unarchive
onArchive={(ids, visibility) =>
timelineManager.updateAssetOperation(ids, (asset) => {
asset.visibility = visibility;
return { remove: false };
})}
/>
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</ButtonContextMenu> </ButtonContextMenu>
<FavoriteAction <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} manager={timelineManager} />
removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) =>
timelineManager.updateAssetOperation(ids, (asset) => {
asset.isFavorite = isFavorite;
return { remove: false };
})}
/>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} /> <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets menuItem onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)} /> <DeleteAssets menuItem manager={timelineManager} />
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>
{/if} {/if}

View file

@ -71,7 +71,7 @@
assets={assetInteraction.selectedAssets} assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()} clearSelect={() => assetInteraction.clearMultiselect()}
> >
<FavoriteAction removeFavorite onFavorite={(assetIds) => timelineManager.removeAssets(assetIds)} /> <FavoriteAction removeFavorite manager={timelineManager} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {timelineManager} {assetInteraction} /> <SelectAllAssets {timelineManager} {assetInteraction} />
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
@ -83,20 +83,12 @@
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} manager={timelineManager} />
menuItem
unarchive={assetInteraction.isAllArchived}
onArchive={(assetIds) => timelineManager.removeAssets(assetIds)}
/>
{#if $preferences.tags.enabled} {#if $preferences.tags.enabled}
<TagAction menuItem /> <TagAction menuItem />
{/if} {/if}
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} /> <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets <DeleteAssets menuItem manager={timelineManager} />
menuItem
onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)}
onUndoDelete={(assets) => timelineManager.addAssets(assets)}
/>
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>
{/if} {/if}

View file

@ -349,15 +349,8 @@
} }
}; };
const handleDeleteAssets = async (assetIds: string[]) => { const handleDeleteAssets = async () => await updateAssetCount();
timelineManager.removeAssets(assetIds); const handleUndoDeleteAssets = async () => await updateAssetCount();
await updateAssetCount();
};
const handleUndoDeleteAssets = async (assets: TimelineAsset[]) => {
timelineManager.addAssets(assets);
await updateAssetCount();
};
let person = $derived(data.person); let person = $derived(data.person);
@ -511,14 +504,7 @@
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</ButtonContextMenu> </ButtonContextMenu>
<FavoriteAction <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} manager={timelineManager} />
removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) =>
timelineManager.updateAssetOperation(ids, (asset) => {
asset.isFavorite = isFavorite;
return { remove: false };
})}
/>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" /> <DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
<MenuOption <MenuOption
@ -529,19 +515,16 @@
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} manager={timelineManager} />
menuItem
unarchive={assetInteraction.isAllArchived}
onArchive={(assetIds) => timelineManager.removeAssets(assetIds)}
/>
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem /> <TagAction menuItem />
{/if} {/if}
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} /> <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets <DeleteAssets
menuItem menuItem
onAssetDelete={(assetIds) => handleDeleteAssets(assetIds)} manager={timelineManager}
onUndoDelete={(assets) => handleUndoDeleteAssets(assets)} onAssetDelete={handleDeleteAssets}
onUndoDelete={handleUndoDeleteAssets}
/> />
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>

View file

@ -118,14 +118,7 @@
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</ButtonContextMenu> </ButtonContextMenu>
<FavoriteAction <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} manager={timelineManager}></FavoriteAction>
removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) =>
timelineManager.updateAssetOperation(ids, (asset) => {
asset.isFavorite = isFavorite;
return { remove: false };
})}
></FavoriteAction>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
{#if assetInteraction.selectedAssets.length > 1 || isAssetStackSelected} {#if assetInteraction.selectedAssets.length > 1 || isAssetStackSelected}
@ -146,15 +139,11 @@
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction menuItem onArchive={(assetIds) => timelineManager.removeAssets(assetIds)} /> <ArchiveAction menuItem manager={timelineManager} />
{#if $preferences.tags.enabled} {#if $preferences.tags.enabled}
<TagAction menuItem /> <TagAction menuItem />
{/if} {/if}
<DeleteAssets <DeleteAssets menuItem manager={timelineManager} />
menuItem
onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)}
onUndoDelete={(assets) => timelineManager.addAssets(assets)}
/>
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} /> <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<hr /> <hr />
<AssetJobActions /> <AssetJobActions />