Compare commits

...

27 Commits

Author SHA1 Message Date
Yaros 1139c96cbc Merge branch 'main' into feat/undo-archive 2026-07-01 18:32:50 +02:00
Yaros 2f5b78d163 refactor: remove toast variable 2026-07-01 18:32:38 +02:00
Yaros 58d1373a04 chore: undo unrelated button change 2026-06-27 15:36:28 +02:00
Yaros 0b68e1376a chore: bump immich/ui to 0.83.0 2026-06-27 15:34:54 +02:00
Yaros 7cbd9eada9 Merge branch 'main' into feat/undo-archive 2026-06-27 15:24:14 +02:00
Yaros 4b05d02769 Merge branch 'main' into feat/undo-archive 2026-06-27 15:23:39 +02:00
Yaros 18d0991b61 chore: format asset-utils 2026-06-25 13:04:52 +02:00
Yaros fa29dc08f9 chore(web): use immich/ui 0.82.0 2026-06-24 22:06:26 +02:00
Yaros fdcd6c7671 Merge branch 'main' into feat/undo-archive 2026-06-24 21:41:01 +02:00
Yaros 44fdaa47d3 refactor: remove undotoast component 2026-06-23 20:06:03 +02:00
Yaros 604d100bb8 chore: format undotoast 2026-06-23 19:24:55 +02:00
Yaros ae022fd90a fix: file placement & undo toast 2026-06-23 19:22:33 +02:00
Yaros afa433b3ad refactor: moved AssetsUndoArchive handler 2026-06-22 17:33:54 +02:00
Yaros 7cdc252384 refactor: use eventmanager for onundoarchive 2026-06-11 21:46:27 +02:00
Yaros 3c43b7240b fix(web): ignore unknown assets in album timelines 2026-06-08 19:12:57 +02:00
Yaros b966940264 chore: formatting 2026-06-08 19:02:47 +02:00
Yaros 72cca30272 feat: go back to asset on undo archive in viewer 2026-06-08 19:00:09 +02:00
Yaros bcd01a2464 fix: restore from asset viewer 2026-06-08 18:53:42 +02:00
Yaros a5d8415c5f fix(web): correct timeline position on undo 2026-06-08 18:48:10 +02:00
Yaros 74e9ec4872 fix: import conflicts 2026-06-08 18:29:44 +02:00
Yaros 411487dfd7 Merge branch 'main' into feat/undo-archive 2026-06-08 18:13:43 +02:00
Yaros 524b191ccc refactor: use event-manager 2026-03-27 13:55:42 +01:00
Yaros 075f7d507b refactor: use primary toast 2026-03-26 19:20:11 +01:00
Yaros c4df4d7852 Merge branch 'main' into feat/undo-archive 2026-03-26 19:11:36 +01:00
Yaros 0eaa2c3419 refactor: remove ternary 2026-03-20 17:43:22 +01:00
Yaros 554e7b28a2 refactor: remove unnecessary checks 2026-03-20 17:13:18 +01:00
Yaros 167aad7ac2 feat(web): undo archive from toast 2026-03-19 22:13:34 +01:00
10 changed files with 96 additions and 25 deletions
+10 -10
View File
@@ -786,8 +786,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../packages/sdk version: link:../packages/sdk
'@immich/ui': '@immich/ui':
specifier: ^0.81.1 specifier: ^0.83.0
version: 0.81.1(@sveltejs/kit@2.65.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.61.0))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0)) version: 0.83.0(@sveltejs/kit@2.65.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.61.0))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0))
'@mapbox/mapbox-gl-rtl-text': '@mapbox/mapbox-gl-rtl-text':
specifier: 0.4.0 specifier: 0.4.0
version: 0.4.0 version: 0.4.0
@@ -3250,8 +3250,8 @@ packages:
resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==} resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==}
hasBin: true hasBin: true
'@immich/ui@0.81.1': '@immich/ui@0.83.0':
resolution: {integrity: sha512-7g173hArs7OS5CfHUG+ZJVQp1iCvFZzBmm+f2uH1WChlFdcwty5DLilYrDBszWuPIvu8wTX2AXTneS/KYBCUxw==} resolution: {integrity: sha512-Xh3R3yhn8/Qyq8lGWIiVmturA+dHXtmW/h++AGb2UAwS/58qu7ssocO/HWIamaSpkgjPGotlWpPIpUJq1zh9FQ==}
peerDependencies: peerDependencies:
'@sveltejs/kit': ^2.13.0 '@sveltejs/kit': ^2.13.0
svelte: ^5.0.0 svelte: ^5.0.0
@@ -16111,7 +16111,7 @@ snapshots:
pg-connection-string: 2.13.0 pg-connection-string: 2.13.0
postgres: 3.4.9 postgres: 3.4.9
'@immich/ui@0.81.1(@sveltejs/kit@2.65.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.61.0))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0))': '@immich/ui@0.83.0(@sveltejs/kit@2.65.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.61.0))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.0))':
dependencies: dependencies:
'@internationalized/date': 3.12.2 '@internationalized/date': 3.12.2
'@mdi/js': 7.4.47 '@mdi/js': 7.4.47
@@ -21092,21 +21092,21 @@ snapshots:
signal-exit: 3.0.7 signal-exit: 3.0.7
strip-final-newline: 2.0.0 strip-final-newline: 2.0.0
exiftool-vendored.exe@13.59.0: exiftool-vendored.exe@13.59.0: {}
optional: true
exiftool-vendored.pl@13.59.0: {} exiftool-vendored.pl@13.59.0:
optional: true
exiftool-vendored@35.21.0: exiftool-vendored@35.21.0:
dependencies: dependencies:
'@photostructure/tz-lookup': 11.5.0 '@photostructure/tz-lookup': 11.5.0
'@types/luxon': 3.7.1 '@types/luxon': 3.7.1
batch-cluster: 17.3.1 batch-cluster: 17.3.1
exiftool-vendored.pl: 13.59.0 exiftool-vendored.exe: 13.59.0
he: 1.2.0 he: 1.2.0
luxon: 3.7.2 luxon: 3.7.2
optionalDependencies: optionalDependencies:
exiftool-vendored.exe: 13.59.0 exiftool-vendored.pl: 13.59.0
expand-template@2.0.3: expand-template@2.0.3:
optional: true optional: true
+1 -1
View File
@@ -66,4 +66,4 @@ injectWorkspacePackages: true
shamefullyHoist: false shamefullyHoist: false
verifyDepsBeforeRun: install verifyDepsBeforeRun: install
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- '@immich/ui@0.81.1' - '@immich/ui@0.83.0'
+1 -1
View File
@@ -27,7 +27,7 @@
"@formatjs/icu-messageformat-parser": "^3.0.0", "@formatjs/icu-messageformat-parser": "^3.0.0",
"@immich/justified-layout-wasm": "^0.4.3", "@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "workspace:*", "@immich/sdk": "workspace:*",
"@immich/ui": "^0.81.1", "@immich/ui": "^0.83.0",
"@mapbox/mapbox-gl-rtl-text": "0.4.0", "@mapbox/mapbox-gl-rtl-text": "0.4.0",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@noble/hashes": "^2.2.0", "@noble/hashes": "^2.2.0",
@@ -14,6 +14,7 @@
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte'; import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte';
import { eventManager } from '$lib/managers/event-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 { getAssetActions } from '$lib/services/asset.service';
import { faceManager } from '$lib/stores/face.svelte'; import { faceManager } from '$lib/stores/face.svelte';
import { ocrManager } from '$lib/stores/ocr.svelte'; import { ocrManager } from '$lib/stores/ocr.svelte';
@@ -23,6 +24,7 @@
import type { OnUndoDelete } from '$lib/utils/actions'; import type { OnUndoDelete } from '$lib/utils/actions';
import { navigateToAsset } from '$lib/utils/asset-utils'; import { navigateToAsset } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { navigate } from '$lib/utils/navigation';
import { InvocationTracker } from '$lib/utils/invocationTracker'; import { InvocationTracker } from '$lib/utils/invocationTracker';
import { SlideshowHistory } from '$lib/utils/slideshow-history'; import { SlideshowHistory } from '$lib/utils/slideshow-history';
import { toTimelineAsset } from '$lib/utils/timeline-util'; 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(() => { onMount(() => {
syncAssetViewerOpenClass(true); syncAssetViewerOpenClass(true);
const slideshowStateUnsubscribe = slideshowState.subscribe((value) => { const slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
@@ -475,7 +486,7 @@
</script> </script>
<CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag, TagPeople]} /> <CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag, TagPeople]} />
<OnEvents {onAssetUpdate} /> <OnEvents {onAssetUpdate} {onAssetsUndoArchive} />
<svelte:document <svelte:document
bind:fullscreenElement bind:fullscreenElement
@@ -309,12 +309,12 @@
untrack(() => map?.jumpTo({ center, zoom })); untrack(() => map?.jumpTo({ center, zoom }));
}); });
const onAssetsDelete = async () => { const onAssetsChanged = async () => {
mapMarkers = await loadMapMarkers(); mapMarkers = await loadMapMarkers();
}; };
</script> </script>
<OnEvents {onAssetsDelete} /> <OnEvents onAssetsDelete={onAssetsChanged} onAssetsArchive={onAssetsChanged} onAssetsUnarchive={onAssetsChanged} />
<!-- We handle style loading ourselves so we set style blank here --> <!-- We handle style loading ourselves so we set style blank here -->
<MapLibre <MapLibre
@@ -16,6 +16,7 @@ import type {
UserAdminResponseDto, UserAdminResponseDto,
WorkflowResponseDto, WorkflowResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte'; import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
import type { TreeNode } from '$lib/utils/tree-utils'; import type { TreeNode } from '$lib/utils/tree-utils';
@@ -35,6 +36,8 @@ export type Events = {
AssetUpdate: [AssetResponseDto]; AssetUpdate: [AssetResponseDto];
AssetsArchive: [string[]]; AssetsArchive: [string[]];
AssetsUnarchive: [TimelineAsset[]];
AssetsUndoArchive: [TimelineAsset[]];
AssetsDelete: [string[]]; AssetsDelete: [string[]];
AssetEditsApplied: [string]; AssetEditsApplied: [string];
AssetsTag: [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 { clamp, isEqual } from 'lodash-es';
import { SvelteDate, SvelteSet } from 'svelte/reactivity'; import { SvelteDate, SvelteSet } from 'svelte/reactivity';
import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte'; import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
@@ -114,7 +114,15 @@ 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) => {
const timelineAsset = toTimelineAsset(asset);
if (this.#options.albumId || this.#options.personId) {
this.#updateAssets([timelineAsset]);
} else {
this.upsertAssets([timelineAsset]);
}
},
AssetsUnarchive: (assets) => this.upsertAssets(assets),
}), }),
); );
} }
+52 -7
View File
@@ -24,6 +24,7 @@ import { get } from 'svelte/store';
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { downloadManager } from '$lib/managers/download-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 { 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 { downloadBlob, downloadRequest, withError } from '$lib/utils'; 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 { getFormatter } from '$lib/utils/i18n';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { asQueryString } from '$lib/utils/shared-links'; import { asQueryString } from '$lib/utils/shared-links';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
export const tagAssets = async ({ export const tagAssets = async ({
@@ -400,7 +402,12 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
}); });
asset.isArchived = data.isArchived; 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) { } catch (error) {
handleError(error, $t('errors.unable_to_add_remove_archive', { values: { archived: asset.isArchived } })); 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; 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 ids = assets.map(({ id }) => id);
const $t = get(t); const $t = get(t);
@@ -419,11 +464,11 @@ export const archiveAssets = async (assets: { id: string }[], visibility: AssetV
}); });
} }
toastManager.primary( if (visibility === AssetVisibility.Archive) {
visibility === AssetVisibility.Archive showUndoArchiveToast($t('archived_count', { values: { count: ids.length } }), assets);
? $t('archived_count', { values: { count: ids.length } }) } else {
: $t('unarchived_count', { values: { count: ids.length } }), toastManager.primary($t('unarchived_count', { values: { count: ids.length } }));
); }
} catch (error) { } catch (error) {
handleError( handleError(
error, error,
+4 -1
View File
@@ -165,7 +165,10 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
const people = assetResponse.people?.map((person) => person.name) || []; const people = assetResponse.people?.map((person) => person.name) || [];
const localDateTime = fromISODateTimeUTCToObject(assetResponse.localDateTime); 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); const createdAt = fromISODateTimeUTCToObject(assetResponse.createdAt);
return { return {
@@ -329,6 +329,7 @@
onPersonAssetDelete={handlePersonAssetDelete} onPersonAssetDelete={handlePersonAssetDelete}
onAssetsDelete={updateAssetCount} onAssetsDelete={updateAssetCount}
onAssetsArchive={updateAssetCount} onAssetsArchive={updateAssetCount}
onAssetsUnarchive={updateAssetCount}
/> />
<main <main