1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

perf(web): asset delete (#7555)

* perf(web): asset delete

* update asset delete on search page

* don't use arrow function in class
This commit is contained in:
Michel Heusschen 2024-03-02 01:49:31 +01:00 committed by GitHub
parent 7303fab9d9
commit a4e6c43823
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 40 additions and 51 deletions

View File

@ -72,7 +72,7 @@
const trashOrDelete = async (force: boolean = false) => { const trashOrDelete = async (force: boolean = false) => {
isShowDeleteConfirmation = false; isShowDeleteConfirmation = false;
await deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets); await deleteAssets(!(isTrashEnabled && !force), (assetIds) => assetStore.removeAssets(assetIds), idsSelectedAssets);
assetInteractionStore.clearMultiselect(); assetInteractionStore.clearMultiselect();
}; };
@ -169,7 +169,7 @@
(await handleNext()) || (await handlePrevious()) || handleClose(); (await handleNext()) || (await handlePrevious()) || handleClose();
// delete after find the next one // delete after find the next one
assetStore.removeAsset(asset.id); assetStore.removeAssets([asset.id]);
break; break;
} }
@ -414,7 +414,7 @@
<slot name="empty" /> <slot name="empty" />
{/if} {/if}
<section id="virtual-timeline" style:height={$assetStore.timelineHeight + 'px'}> <section id="virtual-timeline" style:height={$assetStore.timelineHeight + 'px'}>
{#each $assetStore.buckets as bucket, bucketIndex (bucketIndex)} {#each $assetStore.buckets as bucket (bucket.bucketDate)}
<IntersectionObserver <IntersectionObserver
on:intersected={intersectedHandler} on:intersected={intersectedHandler}
on:hidden={() => assetStore.cancelBucket(bucket)} on:hidden={() => assetStore.cancelBucket(bucket)}

View File

@ -59,14 +59,14 @@ interface DeleteAsset {
value: string; value: string;
} }
interface TrashAsset { interface TrashAssets {
type: 'trash'; type: 'trash';
value: string; value: string[];
} }
export const photoViewer = writable<HTMLImageElement | null>(null); export const photoViewer = writable<HTMLImageElement | null>(null);
type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAsset; type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets;
export class AssetStore { export class AssetStore {
private store$ = writable(this); private store$ = writable(this);
@ -105,7 +105,7 @@ export class AssetStore {
this.addPendingChanges({ type: 'add', value: asset }); this.addPendingChanges({ type: 'add', value: asset });
}), }),
websocketEvents.on('on_asset_trash', (ids) => { websocketEvents.on('on_asset_trash', (ids) => {
this.addPendingChanges(...ids.map((id): TrashAsset => ({ type: 'trash', value: id }))); this.addPendingChanges({ type: 'trash', value: ids });
}), }),
websocketEvents.on('on_asset_update', (asset) => { websocketEvents.on('on_asset_update', (asset) => {
this.addPendingChanges({ type: 'update', value: asset }); this.addPendingChanges({ type: 'update', value: asset });
@ -137,13 +137,13 @@ export class AssetStore {
case 'trash': { case 'trash': {
if (!this.options.isTrashed) { if (!this.options.isTrashed) {
this.removeAsset(value); this.removeAssets(value);
} }
break; break;
} }
case 'delete': { case 'delete': {
this.removeAsset(value); this.removeAssets([value]);
break; break;
} }
} }
@ -363,7 +363,7 @@ export class AssetStore {
const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt; const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt;
if (recalculate) { if (recalculate) {
this.removeAsset(asset.id); this.removeAssets([asset.id]);
this.addAssetToBucket(_asset); this.addAssetToBucket(_asset);
return; return;
} }
@ -373,33 +373,29 @@ export class AssetStore {
} }
removeAssets(ids: string[]) { removeAssets(ids: string[]) {
// TODO: this could probably be more efficient const idSet = new Set(ids);
for (const id of ids) { this.assets = this.assets.filter((asset) => !idSet.has(asset.id));
this.removeAsset(id);
}
}
removeAsset(id: string) { // Iterate in reverse to allow array splicing.
this.assets = this.assets.filter((asset) => asset.id !== id); for (let index = this.buckets.length - 1; index >= 0; index--) {
delete this.assetToBucket[id];
for (let index = 0; index < this.buckets.length; index++) {
const bucket = this.buckets[index]; const bucket = this.buckets[index];
for (let index_ = 0; index_ < bucket.assets.length; index_++) { for (let index_ = bucket.assets.length - 1; index_ >= 0; index_--) {
const asset = bucket.assets[index_]; const asset = bucket.assets[index_];
if (asset.id !== id) { if (!idSet.has(asset.id)) {
continue; continue;
} }
bucket.assets.splice(index_, 1); bucket.assets.splice(index_, 1);
if (bucket.assets.length === 0) { bucket.bucketCount = bucket.assets.length;
if (bucket.bucketCount === 0) {
this.buckets.splice(index, 1); this.buckets.splice(index, 1);
} }
this.emit(true); delete this.assetToBucket[asset.id];
return;
} }
} }
this.emit(false);
} }
async getPreviousAssetId(assetId: string): Promise<string | null> { async getPreviousAssetId(assetId: string): Promise<string | null> {

View File

@ -2,7 +2,7 @@ import { notificationController, NotificationType } from '$lib/components/shared
import { deleteAssets as deleteBulk } from '@immich/sdk'; import { deleteAssets as deleteBulk } from '@immich/sdk';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
export type OnDelete = (assetId: string) => void; export type OnDelete = (assetIds: string[]) => void;
export type OnRestore = (ids: string[]) => void; export type OnRestore = (ids: string[]) => void;
export type OnArchive = (ids: string[], isArchived: boolean) => void; export type OnArchive = (ids: string[], isArchived: boolean) => void;
export type OnFavorite = (ids: string[], favorite: boolean) => void; export type OnFavorite = (ids: string[], favorite: boolean) => void;
@ -11,9 +11,7 @@ export type OnStack = (ids: string[]) => void;
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
try { try {
await deleteBulk({ assetBulkDeleteDto: { ids, force } }); await deleteBulk({ assetBulkDeleteDto: { ids, force } });
for (const id of ids) { onAssetDelete(ids);
onAssetDelete(id);
}
notificationController.show({ notificationController.show({
message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`, message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`,

View File

@ -328,12 +328,6 @@
} }
}; };
const handleRemoveAssets = (assetIds: string[]) => {
for (const assetId of assetIds) {
assetStore.removeAsset(assetId);
}
};
const handleCloseSelectAssets = () => { const handleCloseSelectAssets = () => {
viewMode = ViewMode.VIEW; viewMode = ViewMode.VIEW;
timelineInteractionStore.clearMultiselect(); timelineInteractionStore.clearMultiselect();
@ -434,10 +428,10 @@
{/if} {/if}
<DownloadAction menuItem filename="{album.albumName}.zip" /> <DownloadAction menuItem filename="{album.albumName}.zip" />
{#if isOwned || isAllUserOwned} {#if isOwned || isAllUserOwned}
<RemoveFromAlbum menuItem bind:album onRemove={(assetIds) => handleRemoveAssets(assetIds)} /> <RemoveFromAlbum menuItem bind:album onRemove={(assetIds) => assetStore.removeAssets(assetIds)} />
{/if} {/if}
{#if isAllUserOwned} {#if isAllUserOwned}
<DeleteAssets menuItem onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} /> <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
{/if} {/if}

View File

@ -28,14 +28,14 @@
{#if $isMultiSelectState} {#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<ArchiveAction unarchive onArchive={(ids) => assetStore.removeAssets(ids)} /> <ArchiveAction unarchive onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {assetStore} {assetInteractionStore} /> <SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={mdiPlus} title="Add"> <AssetSelectContextMenu icon={mdiPlus} title="Add">
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</AssetSelectContextMenu> </AssetSelectContextMenu>
<DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} /> <DeleteAssets onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
<AssetSelectContextMenu icon={mdiDotsVertical} title="Add"> <AssetSelectContextMenu icon={mdiDotsVertical} title="Add">
<DownloadAction menuItem /> <DownloadAction menuItem />
<FavoriteAction menuItem removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> <FavoriteAction menuItem removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />

View File

@ -31,17 +31,17 @@
<!-- Multiselection mode app bar --> <!-- Multiselection mode app bar -->
{#if $isMultiSelectState} {#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<FavoriteAction removeFavorite onFavorite={(ids) => assetStore.removeAssets(ids)} /> <FavoriteAction removeFavorite onFavorite={(assetIds) => assetStore.removeAssets(assetIds)} />
<CreateSharedLink /> <CreateSharedLink />
<SelectAllAssets {assetStore} {assetInteractionStore} /> <SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={mdiPlus} title="Add"> <AssetSelectContextMenu icon={mdiPlus} title="Add">
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</AssetSelectContextMenu> </AssetSelectContextMenu>
<DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} /> <DeleteAssets onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
<AssetSelectContextMenu icon={mdiDotsVertical} title="Menu"> <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
<DownloadAction menuItem /> <DownloadAction menuItem />
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => assetStore.removeAssets(ids)} /> <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
</AssetSelectContextMenu> </AssetSelectContextMenu>

View File

@ -443,11 +443,11 @@
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</AssetSelectContextMenu> </AssetSelectContextMenu>
<DeleteAssets onAssetDelete={(assetId) => $assetStore.removeAsset(assetId)} /> <DeleteAssets onAssetDelete={(assetIds) => $assetStore.removeAssets(assetIds)} />
<AssetSelectContextMenu icon={mdiDotsVertical} title="Add"> <AssetSelectContextMenu icon={mdiDotsVertical} title="Add">
<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" /> <DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
<FavoriteAction menuItem removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> <FavoriteAction menuItem removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => $assetStore.removeAssets(ids)} /> <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => $assetStore.removeAssets(assetIds)} />
<MenuOption text="Fix incorrect match" on:click={handleReassignAssets} /> <MenuOption text="Fix incorrect match" on:click={handleReassignAssets} />
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />

View File

@ -61,14 +61,14 @@
</AssetSelectContextMenu> </AssetSelectContextMenu>
<DeleteAssets <DeleteAssets
on:escape={() => (handleEscapeKey = true)} on:escape={() => (handleEscapeKey = true)}
onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)}
/> />
<AssetSelectContextMenu icon={mdiDotsVertical} title="Menu"> <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
<FavoriteAction menuItem removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> <FavoriteAction menuItem removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
<DownloadAction menuItem /> <DownloadAction menuItem />
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} /> <ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
{#if $selectedAssets.size > 1} {#if $selectedAssets.size > 1}
<StackAction onStack={(ids) => assetStore.removeAssets(ids)} /> <StackAction onStack={(assetIds) => assetStore.removeAssets(assetIds)} />
{/if} {/if}
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />

View File

@ -95,8 +95,9 @@
$: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived); $: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived);
$: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite); $: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite);
const onAssetDelete = (assetId: string) => { const onAssetDelete = (assetIds: string[]) => {
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => a.id !== assetId); const assetIdSet = new Set(assetIds);
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
}; };
const handleSelectAll = () => { const handleSelectAll = () => {
selectedAssets = new Set(searchResultAssets); selectedAssets = new Set(searchResultAssets);

View File

@ -65,8 +65,8 @@
{#if $isMultiSelectState} {#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<SelectAllAssets {assetStore} {assetInteractionStore} /> <SelectAllAssets {assetStore} {assetInteractionStore} />
<DeleteAssets force onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} /> <DeleteAssets force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
<RestoreAssets onRestore={(ids) => assetStore.removeAssets(ids)} /> <RestoreAssets onRestore={(assetIds) => assetStore.removeAssets(assetIds)} />
</AssetSelectControlBar> </AssetSelectControlBar>
{/if} {/if}