mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
feat(web): undo archive from toast (#27061)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
This commit is contained in:
parent
90dc328c22
commit
fe3eb7d865
10 changed files with 91 additions and 20 deletions
|
|
@ -14,6 +14,7 @@
|
|||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { getAssetActions } from '$lib/services/asset.service';
|
||||
import { faceManager } from '$lib/stores/face.svelte';
|
||||
import { ocrManager } from '$lib/stores/ocr.svelte';
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { navigateToAsset } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
|
|
@ -149,6 +151,15 @@
|
|||
}
|
||||
};
|
||||
|
||||
const onAssetsUndoArchive = async (assets: TimelineAsset[]) => {
|
||||
if (assets.length === 0) {
|
||||
return;
|
||||
}
|
||||
const restoredAsset = assets[0];
|
||||
await assetViewerManager.setAssetId(restoredAsset.id);
|
||||
await navigate({ targetRoute: 'current', assetId: restoredAsset.id });
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
syncAssetViewerOpenClass(true);
|
||||
const slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
||||
|
|
@ -475,7 +486,7 @@
|
|||
</script>
|
||||
|
||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag, TagPeople]} />
|
||||
<OnEvents {onAssetUpdate} />
|
||||
<OnEvents {onAssetUpdate} {onAssetsUndoArchive} />
|
||||
|
||||
<svelte:document
|
||||
bind:fullscreenElement
|
||||
|
|
|
|||
|
|
@ -309,12 +309,12 @@
|
|||
untrack(() => map?.jumpTo({ center, zoom }));
|
||||
});
|
||||
|
||||
const onAssetsDelete = async () => {
|
||||
const onAssetsChanged = async () => {
|
||||
mapMarkers = await loadMapMarkers();
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onAssetsDelete} />
|
||||
<OnEvents onAssetsDelete={onAssetsChanged} onAssetsArchive={onAssetsChanged} onAssetsUnarchive={onAssetsChanged} />
|
||||
|
||||
<!-- We handle style loading ourselves so we set style blank here -->
|
||||
<MapLibre
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type {
|
|||
UserAdminResponseDto,
|
||||
WorkflowResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||
|
||||
|
|
@ -35,6 +36,8 @@ export type Events = {
|
|||
|
||||
AssetUpdate: [AssetResponseDto];
|
||||
AssetsArchive: [string[]];
|
||||
AssetsUnarchive: [TimelineAsset[]];
|
||||
AssetsUndoArchive: [TimelineAsset[]];
|
||||
AssetsDelete: [string[]];
|
||||
AssetEditsApplied: [string];
|
||||
AssetsTag: [string[]];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AssetOrder, getAssetInfo, getTimeBuckets, AssetOrderBy, type AssetResponseDto } from '@immich/sdk';
|
||||
import { AssetOrder, AssetOrderBy, getAssetInfo, getTimeBuckets, type AssetResponseDto } from '@immich/sdk';
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
|
||||
import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||
|
|
@ -114,7 +114,15 @@ export class TimelineManager extends VirtualScrollManager {
|
|||
|
||||
this.#unsubscribes.push(
|
||||
eventManager.on({
|
||||
AssetUpdate: (asset: AssetResponseDto) => this.#updateAssets([toTimelineAsset(asset)]),
|
||||
AssetUpdate: (asset: AssetResponseDto) => {
|
||||
const timelineAsset = toTimelineAsset(asset);
|
||||
if (this.#options.albumId || this.#options.personId) {
|
||||
this.#updateAssets([timelineAsset]);
|
||||
} else {
|
||||
this.upsertAssets([timelineAsset]);
|
||||
}
|
||||
},
|
||||
AssetsUnarchive: (assets) => this.upsertAssets(assets),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { get } from 'svelte/store';
|
|||
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { downloadBlob, downloadRequest, withError } from '$lib/utils';
|
||||
|
|
@ -31,6 +32,7 @@ import { getByteUnitString } from '$lib/utils/byte-units';
|
|||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { asQueryString } from '$lib/utils/shared-links';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { handleError } from './handle-error';
|
||||
|
||||
export const tagAssets = async ({
|
||||
|
|
@ -400,7 +402,12 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
|
|||
});
|
||||
|
||||
asset.isArchived = data.isArchived;
|
||||
toastManager.primary(asset.isArchived ? $t(`added_to_archive`) : $t(`removed_from_archive`));
|
||||
if (asset.isArchived) {
|
||||
const timelineAsset = toTimelineAsset(asset);
|
||||
showUndoArchiveToast($t('added_to_archive'), [timelineAsset]);
|
||||
} else {
|
||||
toastManager.primary($t('removed_from_archive'));
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_remove_archive', { values: { archived: asset.isArchived } }));
|
||||
}
|
||||
|
|
@ -408,7 +415,45 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
|
|||
return asset;
|
||||
};
|
||||
|
||||
export const archiveAssets = async (assets: { id: string }[], visibility: AssetVisibility) => {
|
||||
const showUndoArchiveToast = (description: string, assets: TimelineAsset[]) => {
|
||||
const $t = get(t);
|
||||
toastManager.primary({
|
||||
description,
|
||||
button: (close) => ({
|
||||
label: $t('undo'),
|
||||
onclick: () => {
|
||||
close();
|
||||
void undoArchiveAssets(assets);
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const undoArchiveAssets = async (assets: TimelineAsset[]) => {
|
||||
const $t = get(t);
|
||||
try {
|
||||
const ids = assets.map((a) => a.id);
|
||||
if (ids.length > 0) {
|
||||
await updateAssets({
|
||||
assetBulkUpdateDto: {
|
||||
ids,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const asset of assets) {
|
||||
asset.visibility = AssetVisibility.Timeline;
|
||||
}
|
||||
eventManager.emit('AssetsUnarchive', assets);
|
||||
eventManager.emit('AssetsUndoArchive', assets);
|
||||
toastManager.success($t('unarchived_count', { values: { count: assets.length } }));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_archive_unarchive', { values: { archived: false } }));
|
||||
}
|
||||
};
|
||||
|
||||
export const archiveAssets = async (assets: TimelineAsset[], visibility: AssetVisibility) => {
|
||||
const ids = assets.map(({ id }) => id);
|
||||
const $t = get(t);
|
||||
|
||||
|
|
@ -419,11 +464,11 @@ export const archiveAssets = async (assets: { id: string }[], visibility: AssetV
|
|||
});
|
||||
}
|
||||
|
||||
toastManager.primary(
|
||||
visibility === AssetVisibility.Archive
|
||||
? $t('archived_count', { values: { count: ids.length } })
|
||||
: $t('unarchived_count', { values: { count: ids.length } }),
|
||||
);
|
||||
if (visibility === AssetVisibility.Archive) {
|
||||
showUndoArchiveToast($t('archived_count', { values: { count: ids.length } }), assets);
|
||||
} else {
|
||||
toastManager.primary($t('unarchived_count', { values: { count: ids.length } }));
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
|
|
|
|||
|
|
@ -165,7 +165,10 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
|
|||
const people = assetResponse.people?.map((person) => person.name) || [];
|
||||
|
||||
const localDateTime = fromISODateTimeUTCToObject(assetResponse.localDateTime);
|
||||
const fileCreatedAt = fromISODateTimeToObject(assetResponse.fileCreatedAt, assetResponse.exifInfo?.timeZone ?? 'UTC');
|
||||
// Keep this consistent with the bucket loader (getTimes), which stores fileCreatedAt as UTC
|
||||
// components. The timeline sorts assets within a day by fileCreatedAt, so a mismatched
|
||||
// representation here would place re-inserted assets (e.g. undo archive) in the wrong spot.
|
||||
const fileCreatedAt = fromISODateTimeUTCToObject(assetResponse.fileCreatedAt);
|
||||
const createdAt = fromISODateTimeUTCToObject(assetResponse.createdAt);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@
|
|||
onPersonAssetDelete={handlePersonAssetDelete}
|
||||
onAssetsDelete={updateAssetCount}
|
||||
onAssetsArchive={updateAssetCount}
|
||||
onAssetsUnarchive={updateAssetCount}
|
||||
/>
|
||||
|
||||
<main
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue