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
30 changed files with 99 additions and 359 deletions
-3
View File
@@ -82,9 +82,6 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
cast: {
gCastEnabled: false,
},
recentlyAdded: {
sidebarWeb: false,
},
},
});
});
-1
View File
@@ -110,7 +110,6 @@ export async function enableTagsPreference(context: BrowserContext) {
download: { archiveSize: 4_294_967_296, includeEmbeddedVideos: false },
purchase: { showSupportBadge: true, hideBuyButtonUntil: '2100-02-12T00:00:00.000Z' },
cast: { gCastEnabled: false },
recentlyAdded: { sidebarWeb: false },
},
});
});
-1
View File
@@ -1718,7 +1718,6 @@
"recent_searches": "Recent searches",
"recently_added": "Recently added",
"recently_added_body": "Jump straight to everything you've added lately on a dedicated page.",
"recently_added_description": "Browse your assets sorted by when they were uploaded to Immich",
"recently_added_page_title": "Recently Added",
"recently_added_title": "Recently added",
"recently_taken": "Recently taken",
-1
View File
@@ -26,7 +26,6 @@ final Map<String, Map<String, Object?>> openApiPatches = {
'sharedLinks': SharedLinksResponse(enabled: true, sidebarWeb: false).toJson(),
'cast': CastResponse(gCastEnabled: false).toJson(),
'albums': {'defaultAssetOrder': 'desc'},
'recentlyAdded': RecentlyAddedResponse(sidebarWeb: false).toJson(),
},
'ServerConfigDto': {
'mapLightStyleUrl': 'https://tiles.immich.cloud/v1/style/light.json',
-2
View File
@@ -545,8 +545,6 @@ Class | Method | HTTP request | Description
- [RatingsUpdate](doc//RatingsUpdate.md)
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)
- [RecentlyAddedResponse](doc//RecentlyAddedResponse.md)
- [RecentlyAddedUpdate](doc//RecentlyAddedUpdate.md)
- [ReleaseChannel](doc//ReleaseChannel.md)
- [ReleaseEventV1](doc//ReleaseEventV1.md)
- [ReleaseType](doc//ReleaseType.md)
-2
View File
@@ -266,8 +266,6 @@ part 'model/ratings_response.dart';
part 'model/ratings_update.dart';
part 'model/reaction_level.dart';
part 'model/reaction_type.dart';
part 'model/recently_added_response.dart';
part 'model/recently_added_update.dart';
part 'model/release_channel.dart';
part 'model/release_event_v1.dart';
part 'model/release_type.dart';
-4
View File
@@ -577,10 +577,6 @@ class ApiClient {
return ReactionLevelTypeTransformer().decode(value);
case 'ReactionType':
return ReactionTypeTypeTransformer().decode(value);
case 'RecentlyAddedResponse':
return RecentlyAddedResponse.fromJson(value);
case 'RecentlyAddedUpdate':
return RecentlyAddedUpdate.fromJson(value);
case 'ReleaseChannel':
return ReleaseChannelTypeTransformer().decode(value);
case 'ReleaseEventV1':
-100
View File
@@ -1,100 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class RecentlyAddedResponse {
/// Returns a new [RecentlyAddedResponse] instance.
RecentlyAddedResponse({
required this.sidebarWeb,
});
/// Whether the recently added page appears in the web sidebar
bool sidebarWeb;
@override
bool operator ==(Object other) => identical(this, other) || other is RecentlyAddedResponse &&
other.sidebarWeb == sidebarWeb;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(sidebarWeb.hashCode);
@override
String toString() => 'RecentlyAddedResponse[sidebarWeb=$sidebarWeb]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'sidebarWeb'] = this.sidebarWeb;
return json;
}
/// Returns a new [RecentlyAddedResponse] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static RecentlyAddedResponse? fromJson(dynamic value) {
upgradeDto(value, "RecentlyAddedResponse");
if (value is Map) {
final json = value.cast<String, dynamic>();
return RecentlyAddedResponse(
sidebarWeb: mapValueOfType<bool>(json, r'sidebarWeb')!,
);
}
return null;
}
static List<RecentlyAddedResponse> listFromJson(dynamic json, {bool growable = false,}) {
final result = <RecentlyAddedResponse>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = RecentlyAddedResponse.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, RecentlyAddedResponse> mapFromJson(dynamic json) {
final map = <String, RecentlyAddedResponse>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = RecentlyAddedResponse.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of RecentlyAddedResponse-objects as value to a dart map
static Map<String, List<RecentlyAddedResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<RecentlyAddedResponse>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = RecentlyAddedResponse.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'sidebarWeb',
};
}
-108
View File
@@ -1,108 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class RecentlyAddedUpdate {
/// Returns a new [RecentlyAddedUpdate] instance.
RecentlyAddedUpdate({
this.sidebarWeb = const Optional.absent(),
});
/// Whether the recently added page appears in the web sidebar
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Optional<bool?> sidebarWeb;
@override
bool operator ==(Object other) => identical(this, other) || other is RecentlyAddedUpdate &&
other.sidebarWeb == sidebarWeb;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(sidebarWeb == null ? 0 : sidebarWeb!.hashCode);
@override
String toString() => 'RecentlyAddedUpdate[sidebarWeb=$sidebarWeb]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.sidebarWeb.isPresent) {
final value = this.sidebarWeb.value;
json[r'sidebarWeb'] = value;
}
return json;
}
/// Returns a new [RecentlyAddedUpdate] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static RecentlyAddedUpdate? fromJson(dynamic value) {
upgradeDto(value, "RecentlyAddedUpdate");
if (value is Map) {
final json = value.cast<String, dynamic>();
return RecentlyAddedUpdate(
sidebarWeb: json.containsKey(r'sidebarWeb') ? Optional.present(mapValueOfType<bool>(json, r'sidebarWeb')) : const Optional.absent(),
);
}
return null;
}
static List<RecentlyAddedUpdate> listFromJson(dynamic json, {bool growable = false,}) {
final result = <RecentlyAddedUpdate>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = RecentlyAddedUpdate.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, RecentlyAddedUpdate> mapFromJson(dynamic json) {
final map = <String, RecentlyAddedUpdate>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = RecentlyAddedUpdate.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of RecentlyAddedUpdate-objects as value to a dart map
static Map<String, List<RecentlyAddedUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<RecentlyAddedUpdate>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = RecentlyAddedUpdate.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
+1 -9
View File
@@ -22,7 +22,6 @@ class UserPreferencesResponseDto {
required this.people,
required this.purchase,
required this.ratings,
required this.recentlyAdded,
required this.sharedLinks,
required this.tags,
});
@@ -45,8 +44,6 @@ class UserPreferencesResponseDto {
RatingsResponse ratings;
RecentlyAddedResponse recentlyAdded;
SharedLinksResponse sharedLinks;
TagsResponse tags;
@@ -62,7 +59,6 @@ class UserPreferencesResponseDto {
other.people == people &&
other.purchase == purchase &&
other.ratings == ratings &&
other.recentlyAdded == recentlyAdded &&
other.sharedLinks == sharedLinks &&
other.tags == tags;
@@ -78,12 +74,11 @@ class UserPreferencesResponseDto {
(people.hashCode) +
(purchase.hashCode) +
(ratings.hashCode) +
(recentlyAdded.hashCode) +
(sharedLinks.hashCode) +
(tags.hashCode);
@override
String toString() => 'UserPreferencesResponseDto[albums=$albums, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, recentlyAdded=$recentlyAdded, sharedLinks=$sharedLinks, tags=$tags]';
String toString() => 'UserPreferencesResponseDto[albums=$albums, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -96,7 +91,6 @@ class UserPreferencesResponseDto {
json[r'people'] = this.people;
json[r'purchase'] = this.purchase;
json[r'ratings'] = this.ratings;
json[r'recentlyAdded'] = this.recentlyAdded;
json[r'sharedLinks'] = this.sharedLinks;
json[r'tags'] = this.tags;
return json;
@@ -120,7 +114,6 @@ class UserPreferencesResponseDto {
people: PeopleResponse.fromJson(json[r'people'])!,
purchase: PurchaseResponse.fromJson(json[r'purchase'])!,
ratings: RatingsResponse.fromJson(json[r'ratings'])!,
recentlyAdded: RecentlyAddedResponse.fromJson(json[r'recentlyAdded'])!,
sharedLinks: SharedLinksResponse.fromJson(json[r'sharedLinks'])!,
tags: TagsResponse.fromJson(json[r'tags'])!,
);
@@ -179,7 +172,6 @@ class UserPreferencesResponseDto {
'people',
'purchase',
'ratings',
'recentlyAdded',
'sharedLinks',
'tags',
};
+1 -17
View File
@@ -23,7 +23,6 @@ class UserPreferencesUpdateDto {
this.people = const Optional.absent(),
this.purchase = const Optional.absent(),
this.ratings = const Optional.absent(),
this.recentlyAdded = const Optional.absent(),
this.sharedLinks = const Optional.absent(),
this.tags = const Optional.absent(),
});
@@ -108,14 +107,6 @@ class UserPreferencesUpdateDto {
///
Optional<RatingsUpdate?> ratings;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Optional<RecentlyAddedUpdate?> recentlyAdded;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -144,7 +135,6 @@ class UserPreferencesUpdateDto {
other.people == people &&
other.purchase == purchase &&
other.ratings == ratings &&
other.recentlyAdded == recentlyAdded &&
other.sharedLinks == sharedLinks &&
other.tags == tags;
@@ -161,12 +151,11 @@ class UserPreferencesUpdateDto {
(people == null ? 0 : people!.hashCode) +
(purchase == null ? 0 : purchase!.hashCode) +
(ratings == null ? 0 : ratings!.hashCode) +
(recentlyAdded == null ? 0 : recentlyAdded!.hashCode) +
(sharedLinks == null ? 0 : sharedLinks!.hashCode) +
(tags == null ? 0 : tags!.hashCode);
@override
String toString() => 'UserPreferencesUpdateDto[albums=$albums, avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, recentlyAdded=$recentlyAdded, sharedLinks=$sharedLinks, tags=$tags]';
String toString() => 'UserPreferencesUpdateDto[albums=$albums, avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -210,10 +199,6 @@ class UserPreferencesUpdateDto {
final value = this.ratings.value;
json[r'ratings'] = value;
}
if (this.recentlyAdded.isPresent) {
final value = this.recentlyAdded.value;
json[r'recentlyAdded'] = value;
}
if (this.sharedLinks.isPresent) {
final value = this.sharedLinks.value;
json[r'sharedLinks'] = value;
@@ -244,7 +229,6 @@ class UserPreferencesUpdateDto {
people: json.containsKey(r'people') ? Optional.present(PeopleUpdate.fromJson(json[r'people'])) : const Optional.absent(),
purchase: json.containsKey(r'purchase') ? Optional.present(PurchaseUpdate.fromJson(json[r'purchase'])) : const Optional.absent(),
ratings: json.containsKey(r'ratings') ? Optional.present(RatingsUpdate.fromJson(json[r'ratings'])) : const Optional.absent(),
recentlyAdded: json.containsKey(r'recentlyAdded') ? Optional.present(RecentlyAddedUpdate.fromJson(json[r'recentlyAdded'])) : const Optional.absent(),
sharedLinks: json.containsKey(r'sharedLinks') ? Optional.present(SharedLinksUpdate.fromJson(json[r'sharedLinks'])) : const Optional.absent(),
tags: json.containsKey(r'tags') ? Optional.present(TagsUpdate.fromJson(json[r'tags'])) : const Optional.absent(),
);
-28
View File
@@ -21991,27 +21991,6 @@
],
"type": "string"
},
"RecentlyAddedResponse": {
"properties": {
"sidebarWeb": {
"description": "Whether the recently added page appears in the web sidebar",
"type": "boolean"
}
},
"required": [
"sidebarWeb"
],
"type": "object"
},
"RecentlyAddedUpdate": {
"properties": {
"sidebarWeb": {
"description": "Whether the recently added page appears in the web sidebar",
"type": "boolean"
}
},
"type": "object"
},
"ReleaseChannel": {
"description": "Release channel",
"enum": [
@@ -27546,9 +27525,6 @@
"ratings": {
"$ref": "#/components/schemas/RatingsResponse"
},
"recentlyAdded": {
"$ref": "#/components/schemas/RecentlyAddedResponse"
},
"sharedLinks": {
"$ref": "#/components/schemas/SharedLinksResponse"
},
@@ -27566,7 +27542,6 @@
"people",
"purchase",
"ratings",
"recentlyAdded",
"sharedLinks",
"tags"
],
@@ -27604,9 +27579,6 @@
"ratings": {
"$ref": "#/components/schemas/RatingsUpdate"
},
"recentlyAdded": {
"$ref": "#/components/schemas/RecentlyAddedUpdate"
},
"sharedLinks": {
"$ref": "#/components/schemas/SharedLinksUpdate"
},
-10
View File
@@ -342,10 +342,6 @@ export type RatingsResponse = {
/** Whether ratings are enabled */
enabled: boolean;
};
export type RecentlyAddedResponse = {
/** Whether the recently added page appears in the web sidebar */
sidebarWeb: boolean;
};
export type SharedLinksResponse = {
/** Whether shared links are enabled */
enabled: boolean;
@@ -368,7 +364,6 @@ export type UserPreferencesResponseDto = {
people: PeopleResponse;
purchase: PurchaseResponse;
ratings: RatingsResponse;
recentlyAdded: RecentlyAddedResponse;
sharedLinks: SharedLinksResponse;
tags: TagsResponse;
};
@@ -426,10 +421,6 @@ export type RatingsUpdate = {
/** Whether ratings are enabled */
enabled?: boolean;
};
export type RecentlyAddedUpdate = {
/** Whether the recently added page appears in the web sidebar */
sidebarWeb?: boolean;
};
export type SharedLinksUpdate = {
/** Whether shared links are enabled */
enabled?: boolean;
@@ -453,7 +444,6 @@ export type UserPreferencesUpdateDto = {
people?: PeopleUpdate;
purchase?: PurchaseUpdate;
ratings?: RatingsUpdate;
recentlyAdded?: RecentlyAddedUpdate;
sharedLinks?: SharedLinksUpdate;
tags?: TagsUpdate;
};
+10 -10
View File
@@ -786,8 +786,8 @@ importers:
specifier: workspace:*
version: link:../packages/sdk
'@immich/ui':
specifier: ^0.81.1
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))
specifier: ^0.83.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':
specifier: 0.4.0
version: 0.4.0
@@ -3250,8 +3250,8 @@ packages:
resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==}
hasBin: true
'@immich/ui@0.81.1':
resolution: {integrity: sha512-7g173hArs7OS5CfHUG+ZJVQp1iCvFZzBmm+f2uH1WChlFdcwty5DLilYrDBszWuPIvu8wTX2AXTneS/KYBCUxw==}
'@immich/ui@0.83.0':
resolution: {integrity: sha512-Xh3R3yhn8/Qyq8lGWIiVmturA+dHXtmW/h++AGb2UAwS/58qu7ssocO/HWIamaSpkgjPGotlWpPIpUJq1zh9FQ==}
peerDependencies:
'@sveltejs/kit': ^2.13.0
svelte: ^5.0.0
@@ -16111,7 +16111,7 @@ snapshots:
pg-connection-string: 2.13.0
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:
'@internationalized/date': 3.12.2
'@mdi/js': 7.4.47
@@ -21092,21 +21092,21 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
exiftool-vendored.exe@13.59.0:
optional: true
exiftool-vendored.exe@13.59.0: {}
exiftool-vendored.pl@13.59.0: {}
exiftool-vendored.pl@13.59.0:
optional: true
exiftool-vendored@35.21.0:
dependencies:
'@photostructure/tz-lookup': 11.5.0
'@types/luxon': 3.7.1
batch-cluster: 17.3.1
exiftool-vendored.pl: 13.59.0
exiftool-vendored.exe: 13.59.0
he: 1.2.0
luxon: 3.7.2
optionalDependencies:
exiftool-vendored.exe: 13.59.0
exiftool-vendored.pl: 13.59.0
expand-template@2.0.3:
optional: true
+1 -1
View File
@@ -66,4 +66,4 @@ injectWorkspacePackages: true
shamefullyHoist: false
verifyDepsBeforeRun: install
minimumReleaseAgeExclude:
- '@immich/ui@0.81.1'
- '@immich/ui@0.83.0'
-15
View File
@@ -98,13 +98,6 @@ const CastUpdateSchema = z
.optional()
.meta({ id: 'CastUpdate' });
const RecentlyAddedUpdateSchema = z
.object({
sidebarWeb: z.boolean().optional().describe('Whether the recently added page appears in the web sidebar'),
})
.optional()
.meta({ id: 'RecentlyAddedUpdate' });
const UserPreferencesUpdateSchema = z
.object({
albums: AlbumsUpdateSchema,
@@ -119,7 +112,6 @@ const UserPreferencesUpdateSchema = z
ratings: RatingsUpdateSchema,
sharedLinks: SharedLinksUpdateSchema,
tags: TagsUpdateSchema,
recentlyAdded: RecentlyAddedUpdateSchema,
})
.meta({ id: 'UserPreferencesUpdateDto' });
@@ -199,12 +191,6 @@ const CastResponseSchema = z
})
.meta({ id: 'CastResponse' });
const RecentlyAddedResponseSchema = z
.object({
sidebarWeb: z.boolean().describe('Whether the recently added page appears in the web sidebar'),
})
.meta({ id: 'RecentlyAddedResponse' });
const UserPreferencesResponseSchema = z
.object({
albums: AlbumsResponseSchema,
@@ -218,7 +204,6 @@ const UserPreferencesResponseSchema = z
download: DownloadResponseSchema,
purchase: PurchaseResponseSchema,
cast: CastResponseSchema,
recentlyAdded: RecentlyAddedResponseSchema,
})
.meta({ id: 'UserPreferencesResponseDto' });
-3
View File
@@ -619,9 +619,6 @@ export type UserPreferences = {
cast: {
gCastEnabled: boolean;
};
recentlyAdded: {
sidebarWeb: boolean;
};
};
export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = {
-3
View File
@@ -50,9 +50,6 @@ const getDefaultPreferences = (): UserPreferences => {
cast: {
gCastEnabled: false,
},
recentlyAdded: {
sidebarWeb: false,
},
};
};
+1 -1
View File
@@ -27,7 +27,7 @@
"@formatjs/icu-messageformat-parser": "^3.0.0",
"@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "workspace:*",
"@immich/ui": "^0.81.1",
"@immich/ui": "^0.83.0",
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
"@mdi/js": "^7.4.47",
"@noble/hashes": "^2.2.0",
@@ -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
@@ -101,9 +101,7 @@
{description}
</p>
{:else}
<div class="pb-2">
{@render descriptionSnippet?.()}
</div>
{@render descriptionSnippet?.()}
{/if}
{#if inputType !== SettingInputFieldType.PASSWORD}
@@ -31,7 +31,6 @@
mdiToolboxOutline,
mdiTrashCan,
mdiTrashCanOutline,
mdiUploadOutline,
} from '@mdi/js';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
@@ -84,14 +83,6 @@
<NavbarItem title={$t('tags')} href={Route.tags()} icon={{ icon: mdiTagMultipleOutline, flipped: true }} />
{/if}
{#if authManager.preferences.recentlyAdded.sidebarWeb}
<NavbarItem
title={$t('recently_added')}
href={Route.recentlyAdded()}
icon={{ icon: mdiUploadOutline, flipped: true }}
/>
{/if}
{#if authManager.preferences.folders.enabled && authManager.preferences.folders.sidebarWeb}
<NavbarItem title={$t('folders')} href={Route.folders()} icon={{ icon: mdiFolderOutline, flipped: true }} />
{/if}
@@ -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),
}),
);
}
+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 { 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,
+4 -1
View File
@@ -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
@@ -38,9 +38,6 @@
// Cast
let gCastEnabled = $state(authManager.preferences.cast?.gCastEnabled ?? false);
// Recently added
let recentlyAddedSidebar = $state(authManager.preferences.recentlyAdded?.sidebarWeb ?? false);
const handleSave = async () => {
try {
const response = await updateMyPreferences({
@@ -53,7 +50,6 @@
sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar },
tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar },
cast: { gCastEnabled },
recentlyAdded: { sidebarWeb: recentlyAddedSidebar },
},
});
@@ -174,14 +170,6 @@
</div>
</SettingAccordion>
<SettingAccordion key="recentlyAdded" title={$t('recently_added')} subtitle={$t('recently_added_description')}>
<div class="mt-4 flex flex-col gap-4 sm:ms-4">
<Field label={$t('sidebar')} description={$t('sidebar_display_description')}>
<Switch bind:checked={recentlyAddedSidebar} />
</Field>
</div>
</SettingAccordion>
<div class="mt-4 flex justify-end">
<Button shape="round" type="submit" size="small" onclick={() => handleSave()}>{$t('save')}</Button>
</div>
@@ -44,7 +44,4 @@ export const preferencesFactory = Sync.makeFactory<UserPreferencesResponseDto>({
enabled: false,
sidebarWeb: false,
},
recentlyAdded: {
sidebarWeb: false,
},
});