martabal преди 1 година
родител
ревизия
11f1ade8f2
променени са 28 файла, в които са добавени 916 реда и са изтрити 65 реда
  1. 139 0
      cli/src/api/open-api/api.ts
  2. 3 0
      mobile/openapi/.openapi-generator/FILES
  3. 2 0
      mobile/openapi/README.md
  4. 1 0
      mobile/openapi/doc/UpdateUserDto.md
  5. 51 0
      mobile/openapi/doc/UserApi.md
  6. 14 0
      mobile/openapi/doc/UserAvatarColor.md
  7. 1 0
      mobile/openapi/doc/UserDto.md
  8. 1 0
      mobile/openapi/doc/UserResponseDto.md
  9. 1 0
      mobile/openapi/lib/api.dart
  10. 33 0
      mobile/openapi/lib/api/user_api.dart
  11. 2 0
      mobile/openapi/lib/api_client.dart
  12. 3 0
      mobile/openapi/lib/api_helper.dart
  13. 18 1
      mobile/openapi/lib/model/update_user_dto.dart
  14. 109 0
      mobile/openapi/lib/model/user_avatar_color.dart
  15. 107 1
      mobile/openapi/lib/model/user_dto.dart
  16. 107 1
      mobile/openapi/lib/model/user_response_dto.dart
  17. 5 0
      mobile/openapi/test/update_user_dto_test.dart
  18. 5 0
      mobile/openapi/test/user_api_test.dart
  19. 21 0
      mobile/openapi/test/user_avatar_color_test.dart
  20. 5 0
      mobile/openapi/test/user_dto_test.dart
  21. 5 0
      mobile/openapi/test/user_response_dto_test.dart
  22. 74 1
      server/immich-openapi-specs.json
  23. 139 0
      web/src/api/open-api/api.ts
  24. 1 1
      web/src/lib/components/layouts/user-page-layout.svelte
  25. 57 3
      web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte
  26. 1 1
      web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
  27. 10 55
      web/src/lib/components/user-settings-page/user-profile-settings.svelte
  28. 1 1
      web/src/routes/(user)/user-settings/+page.svelte

+ 139 - 0
cli/src/api/open-api/api.ts

@@ -4255,6 +4255,12 @@ export interface UpdateTagDto {
  * @interface UpdateUserDto
  */
 export interface UpdateUserDto {
+    /**
+     * 
+     * @type {UserAvatarColor}
+     * @memberof UpdateUserDto
+     */
+    'avatarColor'?: UserAvatarColor;
     /**
      * 
      * @type {string}
@@ -4316,6 +4322,8 @@ export interface UpdateUserDto {
      */
     'storageLabel'?: string;
 }
+
+
 /**
  * 
  * @export
@@ -4359,12 +4367,40 @@ export interface UsageByUserDto {
      */
     'videos': number;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const UserAvatarColor = {
+    Primary: 'primary',
+    Pink: 'pink',
+    Red: 'red',
+    Yellow: 'yellow',
+    Blue: 'blue',
+    Green: 'green',
+    Purple: 'purple',
+    Orange: 'orange',
+    Gray: 'gray',
+    Amber: 'amber'
+} as const;
+
+export type UserAvatarColor = typeof UserAvatarColor[keyof typeof UserAvatarColor];
+
+
 /**
  * 
  * @export
  * @interface UserDto
  */
 export interface UserDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof UserDto
+     */
+    'avatarColor': UserDtoAvatarColorEnum;
     /**
      * 
      * @type {string}
@@ -4396,12 +4432,34 @@ export interface UserDto {
      */
     'profileImagePath': string;
 }
+
+export const UserDtoAvatarColorEnum = {
+    Primary: 'primary',
+    Pink: 'pink',
+    Red: 'red',
+    Yellow: 'yellow',
+    Blue: 'blue',
+    Green: 'green',
+    Purple: 'purple',
+    Orange: 'orange',
+    Gray: 'gray',
+    Amber: 'amber'
+} as const;
+
+export type UserDtoAvatarColorEnum = typeof UserDtoAvatarColorEnum[keyof typeof UserDtoAvatarColorEnum];
+
 /**
  * 
  * @export
  * @interface UserResponseDto
  */
 export interface UserResponseDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof UserResponseDto
+     */
+    'avatarColor': UserResponseDtoAvatarColorEnum;
     /**
      * 
      * @type {string}
@@ -4487,6 +4545,22 @@ export interface UserResponseDto {
      */
     'updatedAt': string;
 }
+
+export const UserResponseDtoAvatarColorEnum = {
+    Primary: 'primary',
+    Pink: 'pink',
+    Red: 'red',
+    Yellow: 'yellow',
+    Blue: 'blue',
+    Green: 'green',
+    Purple: 'purple',
+    Orange: 'orange',
+    Gray: 'gray',
+    Amber: 'amber'
+} as const;
+
+export type UserResponseDtoAvatarColorEnum = typeof UserResponseDtoAvatarColorEnum[keyof typeof UserResponseDtoAvatarColorEnum];
+
 /**
  * 
  * @export
@@ -16158,6 +16232,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/user/profile-image`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {string} id 
@@ -16483,6 +16595,15 @@ export const UserApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async deleteProfileImage(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {string} id 
@@ -16580,6 +16701,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
         createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
             return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise<void> {
+            return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {UserApiDeleteUserRequest} requestParameters Request parameters.
@@ -16786,6 +16915,16 @@ export class UserApi extends BaseAPI {
         return UserApiFp(this.configuration).createUser(requestParameters.createUserDto, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof UserApi
+     */
+    public deleteProfileImage(options?: AxiosRequestConfig) {
+        return UserApiFp(this.configuration).deleteProfileImage(options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {UserApiDeleteUserRequest} requestParameters Request parameters.

+ 3 - 0
mobile/openapi/.openapi-generator/FILES

@@ -163,6 +163,7 @@ doc/UpdateTagDto.md
 doc/UpdateUserDto.md
 doc/UsageByUserDto.md
 doc/UserApi.md
+doc/UserAvatarColor.md
 doc/UserDto.md
 doc/UserResponseDto.md
 doc/ValidateAccessTokenResponseDto.md
@@ -337,6 +338,7 @@ lib/model/update_stack_parent_dto.dart
 lib/model/update_tag_dto.dart
 lib/model/update_user_dto.dart
 lib/model/usage_by_user_dto.dart
+lib/model/user_avatar_color.dart
 lib/model/user_dto.dart
 lib/model/user_response_dto.dart
 lib/model/validate_access_token_response_dto.dart
@@ -502,6 +504,7 @@ test/update_tag_dto_test.dart
 test/update_user_dto_test.dart
 test/usage_by_user_dto_test.dart
 test/user_api_test.dart
+test/user_avatar_color_test.dart
 test/user_dto_test.dart
 test/user_response_dto_test.dart
 test/validate_access_token_response_dto_test.dart

+ 2 - 0
mobile/openapi/README.md

@@ -193,6 +193,7 @@ Class | Method | HTTP request | Description
 *TagApi* | [**updateTag**](doc//TagApi.md#updatetag) | **PATCH** /tag/{id} | 
 *UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image | 
 *UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user | 
+*UserApi* | [**deleteProfileImage**](doc//UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image | 
 *UserApi* | [**deleteUser**](doc//UserApi.md#deleteuser) | **DELETE** /user/{id} | 
 *UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user | 
 *UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me | 
@@ -347,6 +348,7 @@ Class | Method | HTTP request | Description
  - [UpdateTagDto](doc//UpdateTagDto.md)
  - [UpdateUserDto](doc//UpdateUserDto.md)
  - [UsageByUserDto](doc//UsageByUserDto.md)
+ - [UserAvatarColor](doc//UserAvatarColor.md)
  - [UserDto](doc//UserDto.md)
  - [UserResponseDto](doc//UserResponseDto.md)
  - [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)

+ 1 - 0
mobile/openapi/doc/UpdateUserDto.md

@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
+**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) |  | [optional] 
 **email** | **String** |  | [optional] 
 **externalPath** | **String** |  | [optional] 
 **firstName** | **String** |  | [optional] 

+ 51 - 0
mobile/openapi/doc/UserApi.md

@@ -11,6 +11,7 @@ Method | HTTP request | Description
 ------------- | ------------- | -------------
 [**createProfileImage**](UserApi.md#createprofileimage) | **POST** /user/profile-image | 
 [**createUser**](UserApi.md#createuser) | **POST** /user | 
+[**deleteProfileImage**](UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image | 
 [**deleteUser**](UserApi.md#deleteuser) | **DELETE** /user/{id} | 
 [**getAllUsers**](UserApi.md#getallusers) | **GET** /user | 
 [**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me | 
@@ -130,6 +131,56 @@ Name | Type | Description  | Notes
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
+# **deleteProfileImage**
+> deleteProfileImage()
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure API key authorization: api_key
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = UserApi();
+
+try {
+    api_instance.deleteProfileImage();
+} catch (e) {
+    print('Exception when calling UserApi->deleteProfileImage: $e\n');
+}
+```
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+void (empty response body)
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: Not defined
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
 # **deleteUser**
 > UserResponseDto deleteUser(id)
 

+ 14 - 0
mobile/openapi/doc/UserAvatarColor.md

@@ -0,0 +1,14 @@
+# openapi.model.UserAvatarColor
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+

+ 1 - 0
mobile/openapi/doc/UserDto.md

@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
+**avatarColor** | **String** |  | 
 **email** | **String** |  | 
 **firstName** | **String** |  | 
 **id** | **String** |  | 

+ 1 - 0
mobile/openapi/doc/UserResponseDto.md

@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
+**avatarColor** | **String** |  | 
 **createdAt** | [**DateTime**](DateTime.md) |  | 
 **deletedAt** | [**DateTime**](DateTime.md) |  | 
 **email** | **String** |  | 

+ 1 - 0
mobile/openapi/lib/api.dart

@@ -189,6 +189,7 @@ part 'model/update_stack_parent_dto.dart';
 part 'model/update_tag_dto.dart';
 part 'model/update_user_dto.dart';
 part 'model/usage_by_user_dto.dart';
+part 'model/user_avatar_color.dart';
 part 'model/user_dto.dart';
 part 'model/user_response_dto.dart';
 part 'model/validate_access_token_response_dto.dart';

+ 33 - 0
mobile/openapi/lib/api/user_api.dart

@@ -120,6 +120,39 @@ class UserApi {
     return null;
   }
 
+  /// Performs an HTTP 'DELETE /user/profile-image' operation and returns the [Response].
+  Future<Response> deleteProfileImageWithHttpInfo() async {
+    // ignore: prefer_const_declarations
+    final path = r'/user/profile-image';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'DELETE',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  Future<void> deleteProfileImage() async {
+    final response = await deleteProfileImageWithHttpInfo();
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+  }
+
   /// Performs an HTTP 'DELETE /user/{id}' operation and returns the [Response].
   /// Parameters:
   ///

+ 2 - 0
mobile/openapi/lib/api_client.dart

@@ -467,6 +467,8 @@ class ApiClient {
           return UpdateUserDto.fromJson(value);
         case 'UsageByUserDto':
           return UsageByUserDto.fromJson(value);
+        case 'UserAvatarColor':
+          return UserAvatarColorTypeTransformer().decode(value);
         case 'UserDto':
           return UserDto.fromJson(value);
         case 'UserResponseDto':

+ 3 - 0
mobile/openapi/lib/api_helper.dart

@@ -121,6 +121,9 @@ String parameterToString(dynamic value) {
   if (value is TranscodePolicy) {
     return TranscodePolicyTypeTransformer().encode(value).toString();
   }
+  if (value is UserAvatarColor) {
+    return UserAvatarColorTypeTransformer().encode(value).toString();
+  }
   if (value is VideoCodec) {
     return VideoCodecTypeTransformer().encode(value).toString();
   }

+ 18 - 1
mobile/openapi/lib/model/update_user_dto.dart

@@ -13,6 +13,7 @@ part of openapi.api;
 class UpdateUserDto {
   /// Returns a new [UpdateUserDto] instance.
   UpdateUserDto({
+    this.avatarColor,
     this.email,
     this.externalPath,
     this.firstName,
@@ -25,6 +26,14 @@ class UpdateUserDto {
     this.storageLabel,
   });
 
+  ///
+  /// 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.
+  ///
+  UserAvatarColor? avatarColor;
+
   ///
   /// 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
@@ -101,6 +110,7 @@ class UpdateUserDto {
 
   @override
   bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
+     other.avatarColor == avatarColor &&
      other.email == email &&
      other.externalPath == externalPath &&
      other.firstName == firstName &&
@@ -115,6 +125,7 @@ class UpdateUserDto {
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
+    (avatarColor == null ? 0 : avatarColor!.hashCode) +
     (email == null ? 0 : email!.hashCode) +
     (externalPath == null ? 0 : externalPath!.hashCode) +
     (firstName == null ? 0 : firstName!.hashCode) +
@@ -127,10 +138,15 @@ class UpdateUserDto {
     (storageLabel == null ? 0 : storageLabel!.hashCode);
 
   @override
-  String toString() => 'UpdateUserDto[email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
+  String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
+    if (this.avatarColor != null) {
+      json[r'avatarColor'] = this.avatarColor;
+    } else {
+    //  json[r'avatarColor'] = null;
+    }
     if (this.email != null) {
       json[r'email'] = this.email;
     } else {
@@ -188,6 +204,7 @@ class UpdateUserDto {
       final json = value.cast<String, dynamic>();
 
       return UpdateUserDto(
+        avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
         email: mapValueOfType<String>(json, r'email'),
         externalPath: mapValueOfType<String>(json, r'externalPath'),
         firstName: mapValueOfType<String>(json, r'firstName'),

+ 109 - 0
mobile/openapi/lib/model/user_avatar_color.dart

@@ -0,0 +1,109 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// 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 UserAvatarColor {
+  /// Instantiate a new enum with the provided [value].
+  const UserAvatarColor._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const primary = UserAvatarColor._(r'primary');
+  static const pink = UserAvatarColor._(r'pink');
+  static const red = UserAvatarColor._(r'red');
+  static const yellow = UserAvatarColor._(r'yellow');
+  static const blue = UserAvatarColor._(r'blue');
+  static const green = UserAvatarColor._(r'green');
+  static const purple = UserAvatarColor._(r'purple');
+  static const orange = UserAvatarColor._(r'orange');
+  static const gray = UserAvatarColor._(r'gray');
+  static const amber = UserAvatarColor._(r'amber');
+
+  /// List of all possible values in this [enum][UserAvatarColor].
+  static const values = <UserAvatarColor>[
+    primary,
+    pink,
+    red,
+    yellow,
+    blue,
+    green,
+    purple,
+    orange,
+    gray,
+    amber,
+  ];
+
+  static UserAvatarColor? fromJson(dynamic value) => UserAvatarColorTypeTransformer().decode(value);
+
+  static List<UserAvatarColor>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <UserAvatarColor>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = UserAvatarColor.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [UserAvatarColor] to String,
+/// and [decode] dynamic data back to [UserAvatarColor].
+class UserAvatarColorTypeTransformer {
+  factory UserAvatarColorTypeTransformer() => _instance ??= const UserAvatarColorTypeTransformer._();
+
+  const UserAvatarColorTypeTransformer._();
+
+  String encode(UserAvatarColor data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a UserAvatarColor.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  UserAvatarColor? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data) {
+        case r'primary': return UserAvatarColor.primary;
+        case r'pink': return UserAvatarColor.pink;
+        case r'red': return UserAvatarColor.red;
+        case r'yellow': return UserAvatarColor.yellow;
+        case r'blue': return UserAvatarColor.blue;
+        case r'green': return UserAvatarColor.green;
+        case r'purple': return UserAvatarColor.purple;
+        case r'orange': return UserAvatarColor.orange;
+        case r'gray': return UserAvatarColor.gray;
+        case r'amber': return UserAvatarColor.amber;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [UserAvatarColorTypeTransformer] instance.
+  static UserAvatarColorTypeTransformer? _instance;
+}
+

+ 107 - 1
mobile/openapi/lib/model/user_dto.dart

@@ -13,6 +13,7 @@ part of openapi.api;
 class UserDto {
   /// Returns a new [UserDto] instance.
   UserDto({
+    required this.avatarColor,
     required this.email,
     required this.firstName,
     required this.id,
@@ -20,6 +21,8 @@ class UserDto {
     required this.profileImagePath,
   });
 
+  UserDtoAvatarColorEnum avatarColor;
+
   String email;
 
   String firstName;
@@ -32,6 +35,7 @@ class UserDto {
 
   @override
   bool operator ==(Object other) => identical(this, other) || other is UserDto &&
+     other.avatarColor == avatarColor &&
      other.email == email &&
      other.firstName == firstName &&
      other.id == id &&
@@ -41,6 +45,7 @@ class UserDto {
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
+    (avatarColor.hashCode) +
     (email.hashCode) +
     (firstName.hashCode) +
     (id.hashCode) +
@@ -48,10 +53,11 @@ class UserDto {
     (profileImagePath.hashCode);
 
   @override
-  String toString() => 'UserDto[email=$email, firstName=$firstName, id=$id, lastName=$lastName, profileImagePath=$profileImagePath]';
+  String toString() => 'UserDto[avatarColor=$avatarColor, email=$email, firstName=$firstName, id=$id, lastName=$lastName, profileImagePath=$profileImagePath]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
+      json[r'avatarColor'] = this.avatarColor;
       json[r'email'] = this.email;
       json[r'firstName'] = this.firstName;
       json[r'id'] = this.id;
@@ -68,6 +74,7 @@ class UserDto {
       final json = value.cast<String, dynamic>();
 
       return UserDto(
+        avatarColor: UserDtoAvatarColorEnum.fromJson(json[r'avatarColor'])!,
         email: mapValueOfType<String>(json, r'email')!,
         firstName: mapValueOfType<String>(json, r'firstName')!,
         id: mapValueOfType<String>(json, r'id')!,
@@ -120,6 +127,7 @@ class UserDto {
 
   /// The list of required keys that must be present in a JSON.
   static const requiredKeys = <String>{
+    'avatarColor',
     'email',
     'firstName',
     'id',
@@ -128,3 +136,101 @@ class UserDto {
   };
 }
 
+
+class UserDtoAvatarColorEnum {
+  /// Instantiate a new enum with the provided [value].
+  const UserDtoAvatarColorEnum._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const primary = UserDtoAvatarColorEnum._(r'primary');
+  static const pink = UserDtoAvatarColorEnum._(r'pink');
+  static const red = UserDtoAvatarColorEnum._(r'red');
+  static const yellow = UserDtoAvatarColorEnum._(r'yellow');
+  static const blue = UserDtoAvatarColorEnum._(r'blue');
+  static const green = UserDtoAvatarColorEnum._(r'green');
+  static const purple = UserDtoAvatarColorEnum._(r'purple');
+  static const orange = UserDtoAvatarColorEnum._(r'orange');
+  static const gray = UserDtoAvatarColorEnum._(r'gray');
+  static const amber = UserDtoAvatarColorEnum._(r'amber');
+
+  /// List of all possible values in this [enum][UserDtoAvatarColorEnum].
+  static const values = <UserDtoAvatarColorEnum>[
+    primary,
+    pink,
+    red,
+    yellow,
+    blue,
+    green,
+    purple,
+    orange,
+    gray,
+    amber,
+  ];
+
+  static UserDtoAvatarColorEnum? fromJson(dynamic value) => UserDtoAvatarColorEnumTypeTransformer().decode(value);
+
+  static List<UserDtoAvatarColorEnum>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <UserDtoAvatarColorEnum>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = UserDtoAvatarColorEnum.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [UserDtoAvatarColorEnum] to String,
+/// and [decode] dynamic data back to [UserDtoAvatarColorEnum].
+class UserDtoAvatarColorEnumTypeTransformer {
+  factory UserDtoAvatarColorEnumTypeTransformer() => _instance ??= const UserDtoAvatarColorEnumTypeTransformer._();
+
+  const UserDtoAvatarColorEnumTypeTransformer._();
+
+  String encode(UserDtoAvatarColorEnum data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a UserDtoAvatarColorEnum.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  UserDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data) {
+        case r'primary': return UserDtoAvatarColorEnum.primary;
+        case r'pink': return UserDtoAvatarColorEnum.pink;
+        case r'red': return UserDtoAvatarColorEnum.red;
+        case r'yellow': return UserDtoAvatarColorEnum.yellow;
+        case r'blue': return UserDtoAvatarColorEnum.blue;
+        case r'green': return UserDtoAvatarColorEnum.green;
+        case r'purple': return UserDtoAvatarColorEnum.purple;
+        case r'orange': return UserDtoAvatarColorEnum.orange;
+        case r'gray': return UserDtoAvatarColorEnum.gray;
+        case r'amber': return UserDtoAvatarColorEnum.amber;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [UserDtoAvatarColorEnumTypeTransformer] instance.
+  static UserDtoAvatarColorEnumTypeTransformer? _instance;
+}
+
+

+ 107 - 1
mobile/openapi/lib/model/user_response_dto.dart

@@ -13,6 +13,7 @@ part of openapi.api;
 class UserResponseDto {
   /// Returns a new [UserResponseDto] instance.
   UserResponseDto({
+    required this.avatarColor,
     required this.createdAt,
     required this.deletedAt,
     required this.email,
@@ -29,6 +30,8 @@ class UserResponseDto {
     required this.updatedAt,
   });
 
+  UserResponseDtoAvatarColorEnum avatarColor;
+
   DateTime createdAt;
 
   DateTime? deletedAt;
@@ -65,6 +68,7 @@ class UserResponseDto {
 
   @override
   bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
+     other.avatarColor == avatarColor &&
      other.createdAt == createdAt &&
      other.deletedAt == deletedAt &&
      other.email == email &&
@@ -83,6 +87,7 @@ class UserResponseDto {
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
+    (avatarColor.hashCode) +
     (createdAt.hashCode) +
     (deletedAt == null ? 0 : deletedAt!.hashCode) +
     (email.hashCode) +
@@ -99,10 +104,11 @@ class UserResponseDto {
     (updatedAt.hashCode);
 
   @override
-  String toString() => 'UserResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
+  String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
+      json[r'avatarColor'] = this.avatarColor;
       json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
     if (this.deletedAt != null) {
       json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
@@ -144,6 +150,7 @@ class UserResponseDto {
       final json = value.cast<String, dynamic>();
 
       return UserResponseDto(
+        avatarColor: UserResponseDtoAvatarColorEnum.fromJson(json[r'avatarColor'])!,
         createdAt: mapDateTime(json, r'createdAt', '')!,
         deletedAt: mapDateTime(json, r'deletedAt', ''),
         email: mapValueOfType<String>(json, r'email')!,
@@ -205,6 +212,7 @@ class UserResponseDto {
 
   /// The list of required keys that must be present in a JSON.
   static const requiredKeys = <String>{
+    'avatarColor',
     'createdAt',
     'deletedAt',
     'email',
@@ -221,3 +229,101 @@ class UserResponseDto {
   };
 }
 
+
+class UserResponseDtoAvatarColorEnum {
+  /// Instantiate a new enum with the provided [value].
+  const UserResponseDtoAvatarColorEnum._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const primary = UserResponseDtoAvatarColorEnum._(r'primary');
+  static const pink = UserResponseDtoAvatarColorEnum._(r'pink');
+  static const red = UserResponseDtoAvatarColorEnum._(r'red');
+  static const yellow = UserResponseDtoAvatarColorEnum._(r'yellow');
+  static const blue = UserResponseDtoAvatarColorEnum._(r'blue');
+  static const green = UserResponseDtoAvatarColorEnum._(r'green');
+  static const purple = UserResponseDtoAvatarColorEnum._(r'purple');
+  static const orange = UserResponseDtoAvatarColorEnum._(r'orange');
+  static const gray = UserResponseDtoAvatarColorEnum._(r'gray');
+  static const amber = UserResponseDtoAvatarColorEnum._(r'amber');
+
+  /// List of all possible values in this [enum][UserResponseDtoAvatarColorEnum].
+  static const values = <UserResponseDtoAvatarColorEnum>[
+    primary,
+    pink,
+    red,
+    yellow,
+    blue,
+    green,
+    purple,
+    orange,
+    gray,
+    amber,
+  ];
+
+  static UserResponseDtoAvatarColorEnum? fromJson(dynamic value) => UserResponseDtoAvatarColorEnumTypeTransformer().decode(value);
+
+  static List<UserResponseDtoAvatarColorEnum>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <UserResponseDtoAvatarColorEnum>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = UserResponseDtoAvatarColorEnum.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [UserResponseDtoAvatarColorEnum] to String,
+/// and [decode] dynamic data back to [UserResponseDtoAvatarColorEnum].
+class UserResponseDtoAvatarColorEnumTypeTransformer {
+  factory UserResponseDtoAvatarColorEnumTypeTransformer() => _instance ??= const UserResponseDtoAvatarColorEnumTypeTransformer._();
+
+  const UserResponseDtoAvatarColorEnumTypeTransformer._();
+
+  String encode(UserResponseDtoAvatarColorEnum data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a UserResponseDtoAvatarColorEnum.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  UserResponseDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data) {
+        case r'primary': return UserResponseDtoAvatarColorEnum.primary;
+        case r'pink': return UserResponseDtoAvatarColorEnum.pink;
+        case r'red': return UserResponseDtoAvatarColorEnum.red;
+        case r'yellow': return UserResponseDtoAvatarColorEnum.yellow;
+        case r'blue': return UserResponseDtoAvatarColorEnum.blue;
+        case r'green': return UserResponseDtoAvatarColorEnum.green;
+        case r'purple': return UserResponseDtoAvatarColorEnum.purple;
+        case r'orange': return UserResponseDtoAvatarColorEnum.orange;
+        case r'gray': return UserResponseDtoAvatarColorEnum.gray;
+        case r'amber': return UserResponseDtoAvatarColorEnum.amber;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [UserResponseDtoAvatarColorEnumTypeTransformer] instance.
+  static UserResponseDtoAvatarColorEnumTypeTransformer? _instance;
+}
+
+

+ 5 - 0
mobile/openapi/test/update_user_dto_test.dart

@@ -16,6 +16,11 @@ void main() {
   // final instance = UpdateUserDto();
 
   group('test UpdateUserDto', () {
+    // UserAvatarColor avatarColor
+    test('to test the property `avatarColor`', () async {
+      // TODO
+    });
+
     // String email
     test('to test the property `email`', () async {
       // TODO

+ 5 - 0
mobile/openapi/test/user_api_test.dart

@@ -27,6 +27,11 @@ void main() {
       // TODO
     });
 
+    //Future deleteProfileImage() async
+    test('test deleteProfileImage', () async {
+      // TODO
+    });
+
     //Future<UserResponseDto> deleteUser(String id) async
     test('test deleteUser', () async {
       // TODO

+ 21 - 0
mobile/openapi/test/user_avatar_color_test.dart

@@ -0,0 +1,21 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// 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
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for UserAvatarColor
+void main() {
+
+  group('test UserAvatarColor', () {
+
+  });
+
+}

+ 5 - 0
mobile/openapi/test/user_dto_test.dart

@@ -16,6 +16,11 @@ void main() {
   // final instance = UserDto();
 
   group('test UserDto', () {
+    // String avatarColor
+    test('to test the property `avatarColor`', () async {
+      // TODO
+    });
+
     // String email
     test('to test the property `email`', () async {
       // TODO

+ 5 - 0
mobile/openapi/test/user_response_dto_test.dart

@@ -16,6 +16,11 @@ void main() {
   // final instance = UserResponseDto();
 
   group('test UserResponseDto', () {
+    // String avatarColor
+    test('to test the property `avatarColor`', () async {
+      // TODO
+    });
+
     // DateTime createdAt
     test('to test the property `createdAt`', () async {
       // TODO

+ 74 - 1
server/immich-openapi-specs.json

@@ -5449,6 +5449,29 @@
       }
     },
     "/user/profile-image": {
+      "delete": {
+        "operationId": "deleteProfileImage",
+        "parameters": [],
+        "responses": {
+          "204": {
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "User"
+        ]
+      },
       "post": {
         "operationId": "createProfileImage",
         "parameters": [],
@@ -8963,6 +8986,9 @@
       },
       "UpdateUserDto": {
         "properties": {
+          "avatarColor": {
+            "$ref": "#/components/schemas/UserAvatarColor"
+          },
           "email": {
             "type": "string"
           },
@@ -9032,8 +9058,38 @@
         ],
         "type": "object"
       },
+      "UserAvatarColor": {
+        "enum": [
+          "primary",
+          "pink",
+          "red",
+          "yellow",
+          "blue",
+          "green",
+          "purple",
+          "orange",
+          "gray",
+          "amber"
+        ],
+        "type": "string"
+      },
       "UserDto": {
         "properties": {
+          "avatarColor": {
+            "enum": [
+              "primary",
+              "pink",
+              "red",
+              "yellow",
+              "blue",
+              "green",
+              "purple",
+              "orange",
+              "gray",
+              "amber"
+            ],
+            "type": "string"
+          },
           "email": {
             "type": "string"
           },
@@ -9055,12 +9111,28 @@
           "firstName",
           "lastName",
           "email",
-          "profileImagePath"
+          "profileImagePath",
+          "avatarColor"
         ],
         "type": "object"
       },
       "UserResponseDto": {
         "properties": {
+          "avatarColor": {
+            "enum": [
+              "primary",
+              "pink",
+              "red",
+              "yellow",
+              "blue",
+              "green",
+              "purple",
+              "orange",
+              "gray",
+              "amber"
+            ],
+            "type": "string"
+          },
           "createdAt": {
             "format": "date-time",
             "type": "string"
@@ -9116,6 +9188,7 @@
           "lastName",
           "email",
           "profileImagePath",
+          "avatarColor",
           "storageLabel",
           "externalPath",
           "shouldChangePassword",

+ 139 - 0
web/src/api/open-api/api.ts

@@ -4255,6 +4255,12 @@ export interface UpdateTagDto {
  * @interface UpdateUserDto
  */
 export interface UpdateUserDto {
+    /**
+     * 
+     * @type {UserAvatarColor}
+     * @memberof UpdateUserDto
+     */
+    'avatarColor'?: UserAvatarColor;
     /**
      * 
      * @type {string}
@@ -4316,6 +4322,8 @@ export interface UpdateUserDto {
      */
     'storageLabel'?: string;
 }
+
+
 /**
  * 
  * @export
@@ -4359,12 +4367,40 @@ export interface UsageByUserDto {
      */
     'videos': number;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const UserAvatarColor = {
+    Primary: 'primary',
+    Pink: 'pink',
+    Red: 'red',
+    Yellow: 'yellow',
+    Blue: 'blue',
+    Green: 'green',
+    Purple: 'purple',
+    Orange: 'orange',
+    Gray: 'gray',
+    Amber: 'amber'
+} as const;
+
+export type UserAvatarColor = typeof UserAvatarColor[keyof typeof UserAvatarColor];
+
+
 /**
  * 
  * @export
  * @interface UserDto
  */
 export interface UserDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof UserDto
+     */
+    'avatarColor': UserDtoAvatarColorEnum;
     /**
      * 
      * @type {string}
@@ -4396,12 +4432,34 @@ export interface UserDto {
      */
     'profileImagePath': string;
 }
+
+export const UserDtoAvatarColorEnum = {
+    Primary: 'primary',
+    Pink: 'pink',
+    Red: 'red',
+    Yellow: 'yellow',
+    Blue: 'blue',
+    Green: 'green',
+    Purple: 'purple',
+    Orange: 'orange',
+    Gray: 'gray',
+    Amber: 'amber'
+} as const;
+
+export type UserDtoAvatarColorEnum = typeof UserDtoAvatarColorEnum[keyof typeof UserDtoAvatarColorEnum];
+
 /**
  * 
  * @export
  * @interface UserResponseDto
  */
 export interface UserResponseDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof UserResponseDto
+     */
+    'avatarColor': UserResponseDtoAvatarColorEnum;
     /**
      * 
      * @type {string}
@@ -4487,6 +4545,22 @@ export interface UserResponseDto {
      */
     'updatedAt': string;
 }
+
+export const UserResponseDtoAvatarColorEnum = {
+    Primary: 'primary',
+    Pink: 'pink',
+    Red: 'red',
+    Yellow: 'yellow',
+    Blue: 'blue',
+    Green: 'green',
+    Purple: 'purple',
+    Orange: 'orange',
+    Gray: 'gray',
+    Amber: 'amber'
+} as const;
+
+export type UserResponseDtoAvatarColorEnum = typeof UserResponseDtoAvatarColorEnum[keyof typeof UserResponseDtoAvatarColorEnum];
+
 /**
  * 
  * @export
@@ -16158,6 +16232,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/user/profile-image`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {string} id 
@@ -16483,6 +16595,15 @@ export const UserApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async deleteProfileImage(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {string} id 
@@ -16580,6 +16701,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
         createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
             return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise<void> {
+            return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {UserApiDeleteUserRequest} requestParameters Request parameters.
@@ -16786,6 +16915,16 @@ export class UserApi extends BaseAPI {
         return UserApiFp(this.configuration).createUser(requestParameters.createUserDto, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof UserApi
+     */
+    public deleteProfileImage(options?: AxiosRequestConfig) {
+        return UserApiFp(this.configuration).deleteProfileImage(options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {UserApiDeleteUserRequest} requestParameters Request parameters.

+ 1 - 1
web/src/lib/components/layouts/user-page-layout.svelte

@@ -18,7 +18,7 @@
 
 <header>
   {#if !hideNavbar}
-    <NavigationBar bind:user {showUploadButton} on:uploadClicked={() => openFileUploadDialog()} />
+    <NavigationBar {user} {showUploadButton} on:uploadClicked={() => openFileUploadDialog()} />
   {/if}
 
   <slot name="header" />

+ 57 - 3
web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte

@@ -1,16 +1,49 @@
 <script lang="ts">
   import Button from '$lib/components/elements/buttons/button.svelte';
   import { AppRoute } from '$lib/constants';
-  import type { UserResponseDto } from '@api';
+  import { api, UserDtoAvatarColorEnum, type UserResponseDto } from '@api';
   import { createEventDispatcher } from 'svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import { fade } from 'svelte/transition';
   import UserAvatar from '../user-avatar.svelte';
-  import { mdiCog, mdiLogout } from '@mdi/js';
+  import { mdiCog, mdiLogout, mdiPencil } from '@mdi/js';
+  import AvatarSelector from '$lib/components/user-settings-page/avatar-selector.svelte';
+  import { notificationController, NotificationType } from '../notification/notification';
+  import { handleError } from '$lib/utils/handle-error';
 
   export let user: UserResponseDto;
 
+  let isShowSelectAvatar = false;
+
   const dispatch = createEventDispatcher();
+
+  const handleSaveProfile = async (color: UserDtoAvatarColorEnum) => {
+    try {
+      if (user.profileImagePath !== '') {
+        await api.userApi.deleteProfileImage();
+      }
+
+      const { data } = await api.userApi.updateUser({
+        updateUserDto: {
+          id: user.id,
+          email: user.email,
+          firstName: user.firstName,
+          lastName: user.lastName,
+          avatarColor: color,
+        },
+      });
+
+      user = data;
+      isShowSelectAvatar = false;
+
+      notificationController.show({
+        message: 'Saved profile',
+        type: NotificationType.Info,
+      });
+    } catch (error) {
+      handleError(error, 'Unable to save profile');
+    }
+  };
 </script>
 
 <div
@@ -22,8 +55,22 @@
   <div
     class="mx-4 mt-4 flex flex-col items-center justify-center gap-4 rounded-3xl bg-white p-4 dark:bg-immich-dark-primary/10"
   >
-    <UserAvatar size="xl" {user} />
+    <div class="relative">
+      {#key user}
+        <UserAvatar {user} size="xl" />
 
+        <div
+          class="absolute z-10 bottom-0 right-0 rounded-full w-6 h-6 border dark:border-immich-dark-primary bg-immich-primary"
+        >
+          <button
+            class="flex items-center justify-center w-full h-full text-white"
+            on:click={() => (isShowSelectAvatar = true)}
+          >
+            <Icon path={mdiPencil} />
+          </button>
+        </div>
+      {/key}
+    </div>
     <div>
       <p class="text-center text-lg font-medium text-immich-primary dark:text-immich-dark-primary">
         {user.firstName}
@@ -52,3 +99,10 @@
     >
   </div>
 </div>
+{#if isShowSelectAvatar}
+  <AvatarSelector
+    {user}
+    on:close={() => (isShowSelectAvatar = false)}
+    on:choose={({ detail: color }) => handleSaveProfile(color)}
+  />
+{/if}

+ 1 - 1
web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte

@@ -141,7 +141,7 @@
           {/if}
 
           {#if shouldShowAccountInfoPanel}
-            <AccountInfoPanel {user} on:logout={logOut} />
+            <AccountInfoPanel bind:user on:logout={logOut} />
           {/if}
         </div>
       </section>

+ 10 - 55
web/src/lib/components/user-settings-page/user-profile-settings.svelte

@@ -3,49 +3,27 @@
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
-  import { api, UserDtoAvatarColorEnum, UserResponseDto } from '@api';
+  import { api, UserResponseDto } from '@api';
   import { fade } from 'svelte/transition';
   import { handleError } from '../../utils/handle-error';
   import SettingInputField, { SettingInputFieldType } from '../admin-page/settings/setting-input-field.svelte';
   import Button from '../elements/buttons/button.svelte';
-  import { mdiPencil } from '@mdi/js';
-  import Icon from '../elements/icon.svelte';
-  import AvatarSelector from './avatar-selector.svelte';
-  import UserAvatar from '../shared-components/user-avatar.svelte';
 
   export let user: UserResponseDto;
 
-  let editingUser: UserResponseDto = user;
-  let forceShowColor = true;
-
-  let isShowSelectAvatar = false;
-
-  const handleChooseAvatarColor = (color: UserDtoAvatarColorEnum) => {
-    editingUser.avatarColor = color;
-    forceShowColor = false;
-    isShowSelectAvatar = false;
-  };
-
   const handleSaveProfile = async () => {
     try {
-      if (!forceShowColor) {
-        await api.userApi.deleteProfileImage();
-      }
-
       const { data } = await api.userApi.updateUser({
         updateUserDto: {
-          id: editingUser.id,
-          email: editingUser.email,
-          firstName: editingUser.firstName,
-          lastName: editingUser.lastName,
-          avatarColor: editingUser.avatarColor,
+          id: user.id,
+          email: user.email,
+          firstName: user.firstName,
+          lastName: user.lastName,
         },
       });
 
       Object.assign(user, data);
 
-      user = user;
-
       notificationController.show({
         message: 'Saved profile',
         type: NotificationType.Info,
@@ -60,25 +38,10 @@
   <div in:fade={{ duration: 500 }}>
     <form autocomplete="off" on:submit|preventDefault>
       <div class="ml-4 mt-4 flex flex-col gap-4">
-        <div class="relative self-center md:self-start">
-          {#key editingUser}
-            <UserAvatar user={editingUser} size="xxl" showProfileImage={forceShowColor} />
-          {/key}
-          <div
-            class="absolute z-10 bottom-0 right-0 rounded-full w-6 h-6 border dark:border-immich-dark-primary bg-immich-primary"
-          >
-            <button
-              class="flex items-center justify-center w-full h-full text-white"
-              on:click={() => (isShowSelectAvatar = true)}
-            >
-              <Icon path={mdiPencil} />
-            </button>
-          </div>
-        </div>
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
           label="USER ID"
-          bind:value={editingUser.id}
+          bind:value={user.id}
           disabled={true}
         />
 
@@ -87,14 +50,14 @@
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
           label="FIRST NAME"
-          bind:value={editingUser.firstName}
+          bind:value={user.firstName}
           required={true}
         />
 
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
           label="LAST NAME"
-          bind:value={editingUser.lastName}
+          bind:value={user.lastName}
           required={true}
         />
 
@@ -102,7 +65,7 @@
           inputType={SettingInputFieldType.TEXT}
           label="STORAGE LABEL"
           disabled={true}
-          value={editingUser.storageLabel || ''}
+          value={user.storageLabel || ''}
           required={false}
         />
 
@@ -110,7 +73,7 @@
           inputType={SettingInputFieldType.TEXT}
           label="EXTERNAL PATH"
           disabled={true}
-          value={editingUser.externalPath || ''}
+          value={user.externalPath || ''}
           required={false}
         />
 
@@ -121,11 +84,3 @@
     </form>
   </div>
 </section>
-
-{#if isShowSelectAvatar}
-  <AvatarSelector
-    user={editingUser}
-    on:close={() => (isShowSelectAvatar = false)}
-    on:choose={({ detail: color }) => handleChooseAvatarColor(color)}
-  />
-{/if}

+ 1 - 1
web/src/routes/(user)/user-settings/+page.svelte

@@ -12,7 +12,7 @@
 <UserPageLayout {user} title={data.meta.title}>
   <section class="mx-4 flex place-content-center">
     <div class="w-full max-w-3xl">
-      <UserSettingsList bind:user keys={data.keys} devices={data.devices} partners={data.partners} />
+      <UserSettingsList {user} keys={data.keys} devices={data.devices} partners={data.partners} />
     </div>
   </section>
 </UserPageLayout>