diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 8fb2c1b3d1..4eb5e12288 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -2355,6 +2355,12 @@ export interface OAuthConfigResponseDto { * @interface PartnerResponseDto */ export interface PartnerResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof PartnerResponseDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -2440,6 +2446,8 @@ export interface PartnerResponseDto { */ 'updatedAt': string; } + + /** * * @export @@ -4344,6 +4352,12 @@ export interface UpdateTagDto { * @interface UpdateUserDto */ export interface UpdateUserDto { + /** + * + * @type {UserAvatarColor} + * @memberof UpdateUserDto + */ + 'avatarColor'?: UserAvatarColor; /** * * @type {string} @@ -4399,6 +4413,8 @@ export interface UpdateUserDto { */ 'storageLabel'?: string; } + + /** * * @export @@ -4436,12 +4452,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 {UserAvatarColor} + * @memberof UserDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -4467,12 +4511,20 @@ export interface UserDto { */ 'profileImagePath': string; } + + /** * * @export * @interface UserResponseDto */ export interface UserResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof UserResponseDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -4552,6 +4604,8 @@ export interface UserResponseDto { */ 'updatedAt': string; } + + /** * * @export @@ -16477,6 +16531,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise => { + 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 @@ -16802,6 +16894,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> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id @@ -16899,6 +17000,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath)); + }, /** * * @param {UserApiDeleteUserRequest} requestParameters Request parameters. @@ -17105,6 +17214,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. diff --git a/mobile/lib/modules/album/views/album_options_part.dart b/mobile/lib/modules/album/views/album_options_part.dart index 8d45d36b4f..5e5a3a6b86 100644 --- a/mobile/lib/modules/album/views/album_options_part.dart +++ b/mobile/lib/modules/album/views/album_options_part.dart @@ -117,12 +117,8 @@ class AlbumOptionsPage extends HookConsumerWidget { buildOwnerInfo() { return ListTile( - leading: owner != null - ? UserCircleAvatar( - user: owner, - useRandomBackgroundColor: true, - ) - : const SizedBox(), + leading: + owner != null ? UserCircleAvatar(user: owner) : const SizedBox(), title: Text( album.owner.value?.name ?? "", style: const TextStyle( @@ -151,7 +147,6 @@ class AlbumOptionsPage extends HookConsumerWidget { return ListTile( leading: UserCircleAvatar( user: user, - useRandomBackgroundColor: true, radius: 22, ), title: Text( diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index b123363286..bcb32b8359 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -217,7 +217,6 @@ class AlbumViewerPage extends HookConsumerWidget { user: album.sharedUsers.toList()[index], radius: 18, size: 36, - useRandomBackgroundColor: true, ), ); }), diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart index e9e5004da0..094509c897 100644 --- a/mobile/lib/shared/models/user.dart +++ b/mobile/lib/shared/models/user.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:isar/isar.dart'; @@ -16,6 +18,7 @@ class User { this.isPartnerSharedBy = false, this.isPartnerSharedWith = false, this.profileImagePath = '', + this.avatarColor = AvatarColorEnum.primary, this.memoryEnabled = true, this.inTimeline = false, }); @@ -32,6 +35,7 @@ class User { profileImagePath = dto.profileImagePath, isAdmin = dto.isAdmin, memoryEnabled = dto.memoriesEnabled ?? false, + avatarColor = dto.avatarColor.toAvatarColor(), inTimeline = false; User.fromPartnerDto(PartnerResponseDto dto) @@ -44,6 +48,7 @@ class User { profileImagePath = dto.profileImagePath, isAdmin = dto.isAdmin, memoryEnabled = dto.memoriesEnabled ?? false, + avatarColor = dto.avatarColor.toAvatarColor(), inTimeline = dto.inTimeline ?? false; @Index(unique: true, replace: false, type: IndexType.hash) @@ -55,6 +60,8 @@ class User { bool isPartnerSharedWith; bool isAdmin; String profileImagePath; + @Enumerated(EnumType.ordinal) + AvatarColorEnum avatarColor; bool memoryEnabled; bool inTimeline; @@ -68,6 +75,7 @@ class User { if (other is! User) return false; return id == other.id && updatedAt.isAtSameMomentAs(other.updatedAt) && + avatarColor == other.avatarColor && email == other.email && name == other.name && isPartnerSharedBy == other.isPartnerSharedBy && @@ -88,7 +96,77 @@ class User { isPartnerSharedBy.hashCode ^ isPartnerSharedWith.hashCode ^ profileImagePath.hashCode ^ + avatarColor.hashCode ^ isAdmin.hashCode ^ memoryEnabled.hashCode ^ inTimeline.hashCode; } + +enum AvatarColorEnum { + // do not change this order or reuse indices for other purposes, adding is OK + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, +} + +extension AvatarColorEnumHelper on UserAvatarColor { + AvatarColorEnum toAvatarColor() { + switch (this) { + case UserAvatarColor.primary: + return AvatarColorEnum.primary; + case UserAvatarColor.pink: + return AvatarColorEnum.pink; + case UserAvatarColor.red: + return AvatarColorEnum.red; + case UserAvatarColor.yellow: + return AvatarColorEnum.yellow; + case UserAvatarColor.blue: + return AvatarColorEnum.blue; + case UserAvatarColor.green: + return AvatarColorEnum.green; + case UserAvatarColor.purple: + return AvatarColorEnum.purple; + case UserAvatarColor.orange: + return AvatarColorEnum.orange; + case UserAvatarColor.gray: + return AvatarColorEnum.gray; + case UserAvatarColor.amber: + return AvatarColorEnum.amber; + } + return AvatarColorEnum.primary; + } +} + +extension AvatarColorToColorHelper on AvatarColorEnum { + Color toColor([bool isDarkTheme = false]) { + switch (this) { + case AvatarColorEnum.primary: + return isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF); + case AvatarColorEnum.pink: + return const Color.fromARGB(255, 244, 114, 182); + case AvatarColorEnum.red: + return const Color.fromARGB(255, 239, 68, 68); + case AvatarColorEnum.yellow: + return const Color.fromARGB(255, 234, 179, 8); + case AvatarColorEnum.blue: + return const Color.fromARGB(255, 59, 130, 246); + case AvatarColorEnum.green: + return const Color.fromARGB(255, 22, 163, 74); + case AvatarColorEnum.purple: + return const Color.fromARGB(255, 147, 51, 234); + case AvatarColorEnum.orange: + return const Color.fromARGB(255, 234, 88, 12); + case AvatarColorEnum.gray: + return const Color.fromARGB(255, 75, 85, 99); + case AvatarColorEnum.amber: + return const Color.fromARGB(255, 217, 119, 6); + } + } +} diff --git a/mobile/lib/shared/models/user.g.dart b/mobile/lib/shared/models/user.g.dart index 82420a1753..0b2605b949 100644 Binary files a/mobile/lib/shared/models/user.g.dart and b/mobile/lib/shared/models/user.g.dart differ diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart index c4951e3393..abb81ca895 100644 --- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart @@ -22,14 +22,12 @@ class AppBarProfileInfoBox extends HookConsumerWidget { final user = Store.tryGet(StoreKey.currentUser); buildUserProfileImage() { - const immichImage = CircleAvatar( - radius: 20, - backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), - backgroundColor: Colors.transparent, - ); - - if (authState.profileImagePath.isEmpty || user == null) { - return immichImage; + if (user == null) { + return const CircleAvatar( + radius: 20, + backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), + backgroundColor: Colors.transparent, + ); } final userImage = UserCircleAvatar( @@ -38,18 +36,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget { user: user, ); - if (uploadProfileImageStatus == UploadProfileStatus.idle) { - return authState.profileImagePath.isNotEmpty ? userImage : immichImage; - } - - if (uploadProfileImageStatus == UploadProfileStatus.success) { - return userImage; - } - - if (uploadProfileImageStatus == UploadProfileStatus.failure) { - return immichImage; - } - if (uploadProfileImageStatus == UploadProfileStatus.loading) { return const SizedBox( height: 40, @@ -58,7 +44,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); } - return immichImage; + return userImage; } pickUserProfileImage() async { diff --git a/mobile/lib/shared/ui/immich_app_bar.dart b/mobile/lib/shared/ui/immich_app_bar.dart index bbf5a48a00..144adc7876 100644 --- a/mobile/lib/shared/ui/immich_app_bar.dart +++ b/mobile/lib/shared/ui/immich_app_bar.dart @@ -4,8 +4,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_dialog.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; -import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; -import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; @@ -26,7 +24,6 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; final ServerInfo serverInfoState = ref.watch(serverInfoProvider); - AuthenticationState authState = ref.watch(authenticationProvider); final user = Store.tryGet(StoreKey.currentUser); final isDarkTheme = context.isDarkTheme; const widgetSize = 30.0; @@ -55,7 +52,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { alignment: Alignment.bottomRight, isLabelVisible: serverInfoState.isVersionMismatch, offset: const Offset(2, 2), - child: authState.profileImagePath.isEmpty || user == null + child: user == null ? const Icon( Icons.face_outlined, size: widgetSize, diff --git a/mobile/lib/shared/ui/user_circle_avatar.dart b/mobile/lib/shared/ui/user_circle_avatar.dart index 5920758a7f..1f6ef15f50 100644 --- a/mobile/lib/shared/ui/user_circle_avatar.dart +++ b/mobile/lib/shared/ui/user_circle_avatar.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/ui/transparent_image.dart'; @@ -13,32 +12,17 @@ class UserCircleAvatar extends ConsumerWidget { final User user; double radius; double size; - bool useRandomBackgroundColor; UserCircleAvatar({ super.key, this.radius = 22, this.size = 44, - this.useRandomBackgroundColor = false, required this.user, }); @override Widget build(BuildContext context, WidgetRef ref) { - final randomColors = [ - Colors.red[200], - Colors.blue[200], - Colors.green[200], - Colors.yellow[200], - Colors.purple[200], - Colors.orange[200], - Colors.pink[200], - Colors.teal[200], - Colors.indigo[200], - Colors.cyan[200], - Colors.brown[200], - ]; - + bool isDarkTheme = Theme.of(context).brightness == Brightness.dark; final profileImageUrl = '${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}'; @@ -46,15 +30,16 @@ class UserCircleAvatar extends ConsumerWidget { user.name[0].toUpperCase(), style: TextStyle( fontWeight: FontWeight.bold, - color: context.isDarkTheme ? Colors.black : Colors.white, + fontSize: 12, + color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary + ? Colors.black + : Colors.white, ), ); return CircleAvatar( - backgroundColor: useRandomBackgroundColor - ? randomColors[Random().nextInt(randomColors.length)] - : context.primaryColor, + backgroundColor: user.avatarColor.toColor(), radius: radius, - child: user.profileImagePath == "" + child: user.profileImagePath.isEmpty ? textIcon : ClipRRect( borderRadius: BorderRadius.circular(50), diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index c4f9679768..9ca8335169 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -166,6 +166,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 @@ -343,6 +344,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 @@ -511,6 +513,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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 551169a993..4d54d41c39 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/PartnerResponseDto.md b/mobile/openapi/doc/PartnerResponseDto.md index f7133bbf7e..574b96f8df 100644 Binary files a/mobile/openapi/doc/PartnerResponseDto.md and b/mobile/openapi/doc/PartnerResponseDto.md differ diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index ffbe11253a..567bc43ebb 100644 Binary files a/mobile/openapi/doc/UpdateUserDto.md and b/mobile/openapi/doc/UpdateUserDto.md differ diff --git a/mobile/openapi/doc/UserApi.md b/mobile/openapi/doc/UserApi.md index fb88d53bdd..6d2a028da4 100644 Binary files a/mobile/openapi/doc/UserApi.md and b/mobile/openapi/doc/UserApi.md differ diff --git a/mobile/openapi/doc/UserAvatarColor.md b/mobile/openapi/doc/UserAvatarColor.md new file mode 100644 index 0000000000..a07350de12 Binary files /dev/null and b/mobile/openapi/doc/UserAvatarColor.md differ diff --git a/mobile/openapi/doc/UserDto.md b/mobile/openapi/doc/UserDto.md index c8b750a1d3..7e5770f840 100644 Binary files a/mobile/openapi/doc/UserDto.md and b/mobile/openapi/doc/UserDto.md differ diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index ddf7c574c6..93f9aa62a3 100644 Binary files a/mobile/openapi/doc/UserResponseDto.md and b/mobile/openapi/doc/UserResponseDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index ed0b51f88d..92bfa3f81d 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/user_api.dart b/mobile/openapi/lib/api/user_api.dart index 23f25492ce..26ab3dcd0d 100644 Binary files a/mobile/openapi/lib/api/user_api.dart and b/mobile/openapi/lib/api/user_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index c03a469ae3..4c24967ec5 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index f391314482..ddb16df0c3 100644 Binary files a/mobile/openapi/lib/api_helper.dart and b/mobile/openapi/lib/api_helper.dart differ diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 5ccef68d85..6e1776b266 100644 Binary files a/mobile/openapi/lib/model/partner_response_dto.dart and b/mobile/openapi/lib/model/partner_response_dto.dart differ diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index 9d381cd4b7..d0e46e7f5d 100644 Binary files a/mobile/openapi/lib/model/update_user_dto.dart and b/mobile/openapi/lib/model/update_user_dto.dart differ diff --git a/mobile/openapi/lib/model/user_avatar_color.dart b/mobile/openapi/lib/model/user_avatar_color.dart new file mode 100644 index 0000000000..075f58d3a5 Binary files /dev/null and b/mobile/openapi/lib/model/user_avatar_color.dart differ diff --git a/mobile/openapi/lib/model/user_dto.dart b/mobile/openapi/lib/model/user_dto.dart index de26cc8f24..ad39f84e03 100644 Binary files a/mobile/openapi/lib/model/user_dto.dart and b/mobile/openapi/lib/model/user_dto.dart differ diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 73b30881f8..11a182b6b7 100644 Binary files a/mobile/openapi/lib/model/user_response_dto.dart and b/mobile/openapi/lib/model/user_response_dto.dart differ diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart index 495762d3b7..50ac1d8050 100644 Binary files a/mobile/openapi/test/partner_response_dto_test.dart and b/mobile/openapi/test/partner_response_dto_test.dart differ diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 039a1a2446..0b4cc0b65d 100644 Binary files a/mobile/openapi/test/update_user_dto_test.dart and b/mobile/openapi/test/update_user_dto_test.dart differ diff --git a/mobile/openapi/test/user_api_test.dart b/mobile/openapi/test/user_api_test.dart index 86c33c7e0e..26ebf3d7ed 100644 Binary files a/mobile/openapi/test/user_api_test.dart and b/mobile/openapi/test/user_api_test.dart differ diff --git a/mobile/openapi/test/user_avatar_color_test.dart b/mobile/openapi/test/user_avatar_color_test.dart new file mode 100644 index 0000000000..83480b580f Binary files /dev/null and b/mobile/openapi/test/user_avatar_color_test.dart differ diff --git a/mobile/openapi/test/user_dto_test.dart b/mobile/openapi/test/user_dto_test.dart index e0866d12dc..20229ff65b 100644 Binary files a/mobile/openapi/test/user_dto_test.dart and b/mobile/openapi/test/user_dto_test.dart differ diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index 28830a73fd..aa0717e74d 100644 Binary files a/mobile/openapi/test/user_response_dto_test.dart and b/mobile/openapi/test/user_response_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index b776e4b28c..bcc301ee2e 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -5578,6 +5578,29 @@ } }, "/user/profile-image": { + "delete": { + "operationId": "deleteProfileImage", + "parameters": [], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "User" + ] + }, "post": { "operationId": "createProfileImage", "parameters": [], @@ -7632,6 +7655,9 @@ }, "PartnerResponseDto": { "properties": { + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, "createdAt": { "format": "date-time", "type": "string" @@ -7682,6 +7708,7 @@ } }, "required": [ + "avatarColor", "id", "name", "email", @@ -9140,6 +9167,9 @@ }, "UpdateUserDto": { "properties": { + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, "email": { "type": "string" }, @@ -9202,8 +9232,26 @@ ], "type": "object" }, + "UserAvatarColor": { + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" + ], + "type": "string" + }, "UserDto": { "properties": { + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, "email": { "type": "string" }, @@ -9218,6 +9266,7 @@ } }, "required": [ + "avatarColor", "id", "name", "email", @@ -9227,6 +9276,9 @@ }, "UserResponseDto": { "properties": { + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, "createdAt": { "format": "date-time", "type": "string" @@ -9274,6 +9326,7 @@ } }, "required": [ + "avatarColor", "id", "name", "email", diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index f915883a5b..a815e22d1f 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -248,6 +248,7 @@ describe('AuthService', () => { userMock.getAdmin.mockResolvedValue(null); userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: new Date('2021-01-01') } as UserEntity); await expect(sut.adminSignUp(dto)).resolves.toEqual({ + avatarColor: expect.any(String), id: 'admin', createdAt: new Date('2021-01-01'), email: 'test@immich.com', diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index 6eaa87ea05..c632cc8da3 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -1,3 +1,4 @@ +import { UserAvatarColor } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; import { authStub, newPartnerRepositoryMock, partnerStub } from '@test'; import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories'; @@ -19,6 +20,7 @@ const responseDto = { updatedAt: new Date('2021-01-01'), externalPath: null, memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, inTimeline: true, }, user1: { @@ -35,6 +37,7 @@ const responseDto = { updatedAt: new Date('2021-01-01'), externalPath: null, memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, inTimeline: true, }, }; diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts index 4f05498da9..a71c0e21a5 100644 --- a/server/src/domain/user/dto/update-user.dto.ts +++ b/server/src/domain/user/dto/update-user.dto.ts @@ -1,6 +1,7 @@ +import { UserAvatarColor } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator'; import { Optional, toEmail, toSanitized } from '../../domain.util'; export class UpdateUserDto { @@ -44,4 +45,9 @@ export class UpdateUserDto { @Optional() @IsBoolean() memoriesEnabled?: boolean; + + @Optional() + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor?: UserAvatarColor; } diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index fd9a121678..7b6aef1910 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -1,10 +1,26 @@ -import { UserEntity } from '@app/infra/entities'; +import { UserAvatarColor, UserEntity } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; + +export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => { + const values = Object.values(UserAvatarColor); + const randomIndex = Math.floor( + user.email + .split('') + .map((letter) => letter.charCodeAt(0)) + .reduce((a, b) => a + b, 0) % values.length, + ); + return values[randomIndex] as UserAvatarColor; +}; export class UserDto { id!: string; name!: string; email!: string; profileImagePath!: string; + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor!: UserAvatarColor; } export class UserResponseDto extends UserDto { @@ -25,6 +41,7 @@ export const mapSimpleUser = (entity: UserEntity): UserDto => { email: entity.email, name: entity.name, profileImagePath: entity.profileImagePath, + avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity), }; }; diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index 431caa8e1f..c8e77a5004 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -98,7 +98,6 @@ export class UserCore { if (payload.storageLabel) { payload.storageLabel = sanitize(payload.storageLabel); } - const userEntity = await this.userRepository.create(payload); await this.libraryRepository.create({ owner: { id: userEntity.id } as UserEntity, diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index 94f919178f..04b4206cab 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -323,17 +323,52 @@ describe(UserService.name, () => { const file = { path: '/profile/path' } as Express.Multer.File; userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); - await sut.createProfileImage(userStub.admin, file); - - expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, { profileImagePath: file.path }); + await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(BadRequestException); }); it('should throw an error if the user profile could not be updated with the new image', async () => { const file = { path: '/profile/path' } as Express.Multer.File; + userMock.get.mockResolvedValue(userStub.profilePath); userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error')); await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(InternalServerErrorException); }); + + it('should delete the previous profile image', async () => { + const file = { path: '/profile/path' } as Express.Multer.File; + userMock.get.mockResolvedValue(userStub.profilePath); + const files = [userStub.profilePath.profileImagePath]; + userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); + + await sut.createProfileImage(userStub.admin, file); + await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); + }); + + it('should not delete the profile image if it has not been set', async () => { + const file = { path: '/profile/path' } as Express.Multer.File; + userMock.get.mockResolvedValue(userStub.admin); + userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); + + await sut.createProfileImage(userStub.admin, file); + expect(jobMock.queue).not.toHaveBeenCalled(); + }); + }); + + describe('deleteProfileImage', () => { + it('should send an http error has no profile image', async () => { + userMock.get.mockResolvedValue(userStub.admin); + + await expect(sut.deleteProfileImage(userStub.admin)).rejects.toBeInstanceOf(BadRequestException); + expect(jobMock.queue).not.toHaveBeenCalled(); + }); + + it('should delete the profile image if user has one', async () => { + userMock.get.mockResolvedValue(userStub.profilePath); + const files = [userStub.profilePath.profileImagePath]; + + await sut.deleteProfileImage(userStub.admin); + await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); + }); }); describe('getUserProfileImage', () => { diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index a155d401df..3232a6f94d 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -93,10 +93,23 @@ export class UserService { authUser: AuthUserDto, fileInfo: Express.Multer.File, ): Promise { + const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false }); const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path }); + if (oldpath !== '') { + await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); + } return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath); } + async deleteProfileImage(authUser: AuthUserDto): Promise { + const user = await this.findOrFail(authUser.id, { withDeleted: false }); + if (user.profileImagePath === '') { + throw new BadRequestException("Can't delete a missing profile Image"); + } + await this.userRepository.update(authUser.id, { profileImagePath: '' }); + await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } }); + } + async getProfileImage(id: string): Promise { const user = await this.findOrFail(id, {}); if (!user.profileImagePath) { @@ -111,7 +124,7 @@ export class UserService { throw new BadRequestException('Admin account does not exist'); } - const providedPassword = await ask(admin); + const providedPassword = await ask(mapUser(admin)); const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, ''); await this.userCore.updateUser(admin, admin.id, { password }); diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/immich/controllers/auth.controller.ts index ae48a78ebc..dda546cf04 100644 --- a/server/src/immich/controllers/auth.controller.ts +++ b/server/src/immich/controllers/auth.controller.ts @@ -12,6 +12,7 @@ import { SignUpDto, UserResponseDto, ValidateAccessTokenResponseDto, + mapUser, } from '@app/domain'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; @@ -71,7 +72,7 @@ export class AuthController { @Post('change-password') @HttpCode(HttpStatus.OK) changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise { - return this.service.changePassword(authUser, dto); + return this.service.changePassword(authUser, dto).then(mapUser); } @Post('logout') diff --git a/server/src/immich/controllers/user.controller.ts b/server/src/immich/controllers/user.controller.ts index 92b3fdcc0c..1772fb5481 100644 --- a/server/src/immich/controllers/user.controller.ts +++ b/server/src/immich/controllers/user.controller.ts @@ -13,6 +13,8 @@ import { Delete, Get, Header, + HttpCode, + HttpStatus, Param, Post, Put, @@ -54,6 +56,12 @@ export class UserController { return this.service.create(createUserDto); } + @Delete('profile-image') + @HttpCode(HttpStatus.NO_CONTENT) + deleteProfileImage(@AuthUser() authUser: AuthUserDto): Promise { + return this.service.deleteProfileImage(authUser); + } + @AdminRoute() @Delete(':id') deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/infra/entities/user.entity.ts b/server/src/infra/entities/user.entity.ts index 83f23bef60..5a0a6afd6c 100644 --- a/server/src/infra/entities/user.entity.ts +++ b/server/src/infra/entities/user.entity.ts @@ -10,6 +10,19 @@ import { import { AssetEntity } from './asset.entity'; import { TagEntity } from './tag.entity'; +export enum UserAvatarColor { + PRIMARY = 'primary', + PINK = 'pink', + RED = 'red', + YELLOW = 'yellow', + BLUE = 'blue', + GREEN = 'green', + PURPLE = 'purple', + ORANGE = 'orange', + GRAY = 'gray', + AMBER = 'amber', +} + @Entity('users') export class UserEntity { @PrimaryGeneratedColumn('uuid') @@ -18,6 +31,9 @@ export class UserEntity { @Column({ default: '' }) name!: string; + @Column({ type: 'varchar', nullable: true }) + avatarColor!: UserAvatarColor | null; + @Column({ default: false }) isAdmin!: boolean; diff --git a/server/src/infra/migrations/1699889987493-AddAvatarColor.ts b/server/src/infra/migrations/1699889987493-AddAvatarColor.ts new file mode 100644 index 0000000000..b075a5d2af --- /dev/null +++ b/server/src/infra/migrations/1699889987493-AddAvatarColor.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAvatarColor1699889987493 implements MigrationInterface { + name = 'AddAvatarColor1699889987493' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "avatarColor" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarColor"`); + } + +} diff --git a/server/test/e2e/auth.e2e-spec.ts b/server/test/e2e/auth.e2e-spec.ts index eb47a87255..97a8551ff4 100644 --- a/server/test/e2e/auth.e2e-spec.ts +++ b/server/test/e2e/auth.e2e-spec.ts @@ -18,6 +18,7 @@ const password = 'Password123'; const email = 'admin@immich.app'; const adminSignupResponse = { + avatarColor: expect.any(String), id: expect.any(String), name: 'Immich Admin', email: 'admin@immich.app', diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index b528a107f3..c070a6769d 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -1,4 +1,4 @@ -import { UserEntity } from '@app/infra/entities'; +import { UserAvatarColor, UserEntity } from '@app/infra/entities'; import { authStub } from './auth.stub'; export const userStub = { @@ -17,6 +17,7 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), user1: Object.freeze({ ...authStub.user1, @@ -33,6 +34,7 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), user2: Object.freeze({ ...authStub.user2, @@ -49,6 +51,7 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), storageLabel: Object.freeze({ ...authStub.user1, @@ -65,6 +68,7 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), externalPath1: Object.freeze({ ...authStub.user1, @@ -81,6 +85,7 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), externalPath2: Object.freeze({ ...authStub.user1, @@ -97,6 +102,7 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), profilePath: Object.freeze({ ...authStub.user1, @@ -113,5 +119,6 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), }; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 8fb2c1b3d1..4eb5e12288 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -2355,6 +2355,12 @@ export interface OAuthConfigResponseDto { * @interface PartnerResponseDto */ export interface PartnerResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof PartnerResponseDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -2440,6 +2446,8 @@ export interface PartnerResponseDto { */ 'updatedAt': string; } + + /** * * @export @@ -4344,6 +4352,12 @@ export interface UpdateTagDto { * @interface UpdateUserDto */ export interface UpdateUserDto { + /** + * + * @type {UserAvatarColor} + * @memberof UpdateUserDto + */ + 'avatarColor'?: UserAvatarColor; /** * * @type {string} @@ -4399,6 +4413,8 @@ export interface UpdateUserDto { */ 'storageLabel'?: string; } + + /** * * @export @@ -4436,12 +4452,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 {UserAvatarColor} + * @memberof UserDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -4467,12 +4511,20 @@ export interface UserDto { */ 'profileImagePath': string; } + + /** * * @export * @interface UserResponseDto */ export interface UserResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof UserResponseDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -4552,6 +4604,8 @@ export interface UserResponseDto { */ 'updatedAt': string; } + + /** * * @export @@ -16477,6 +16531,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise => { + 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 @@ -16802,6 +16894,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> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id @@ -16899,6 +17000,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath)); + }, /** * * @param {UserApiDeleteUserRequest} requestParameters Request parameters. @@ -17105,6 +17214,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. diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index 668946d837..b92f20028e 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -77,7 +77,7 @@
- +

{album.owner.name}

@@ -90,7 +90,7 @@ class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700" >
- +

{user.name}

diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 0b4818ad5f..33b5494fcc 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -71,7 +71,7 @@ on:click={() => handleUnselect(user)} class="flex place-items-center gap-1 rounded-full border border-gray-400 p-1 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700" > - +

{user.name}

{/key} @@ -94,7 +94,7 @@ >✓ {:else} - + {/if}
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 70d76ca1aa..8c36df2d8b 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -333,7 +333,7 @@

SHARED BY

- +
diff --git a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte index 6a135066f9..27b7423bbd 100644 --- a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte @@ -1,16 +1,48 @@
- +
+ {#key user} + +
+ +
+ {/key} +

{user.name} @@ -51,3 +97,10 @@ >

+{#if isShowSelectAvatar} + (isShowSelectAvatar = false)} + on:choose={({ detail: color }) => handleSaveProfile(color)} + /> +{/if} diff --git a/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte b/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte new file mode 100644 index 0000000000..6d7e395785 --- /dev/null +++ b/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte @@ -0,0 +1,39 @@ + + + dispatch('close')} on:escape={() => dispatch('close')}> +
+
+
+

+ SELECT AVATAR COLOR +

+
+ dispatch('close')} /> +
+
+
+
+ {#each colors as color} + + {/each} +
+
+
+
+
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index c72803b54c..2bb95495fa 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -124,7 +124,9 @@ on:mouseleave={() => (shouldShowAccountInfo = false)} on:click={() => (shouldShowAccountInfoPanel = !shouldShowAccountInfoPanel)} > - + {#key user} + + {/key} {#if shouldShowAccountInfo && !shouldShowAccountInfoPanel} @@ -139,7 +141,7 @@ {/if} {#if shouldShowAccountInfoPanel} - + {/if}
diff --git a/web/src/lib/components/shared-components/user-avatar.svelte b/web/src/lib/components/shared-components/user-avatar.svelte index 07e65d94aa..a77a5f2bb1 100644 --- a/web/src/lib/components/shared-components/user-avatar.svelte +++ b/web/src/lib/components/shared-components/user-avatar.svelte @@ -1,35 +1,40 @@