mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(web,server): user avatar color (#4779)
This commit is contained in:
parent
14c7187539
commit
d25a245049
119
cli/src/api/open-api/api.ts
generated
119
cli/src/api/open-api/api.ts
generated
@ -2355,6 +2355,12 @@ export interface OAuthConfigResponseDto {
|
|||||||
* @interface PartnerResponseDto
|
* @interface PartnerResponseDto
|
||||||
*/
|
*/
|
||||||
export interface PartnerResponseDto {
|
export interface PartnerResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof PartnerResponseDto
|
||||||
|
*/
|
||||||
|
'avatarColor': UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -2440,6 +2446,8 @@ export interface PartnerResponseDto {
|
|||||||
*/
|
*/
|
||||||
'updatedAt': string;
|
'updatedAt': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -4344,6 +4352,12 @@ export interface UpdateTagDto {
|
|||||||
* @interface UpdateUserDto
|
* @interface UpdateUserDto
|
||||||
*/
|
*/
|
||||||
export interface UpdateUserDto {
|
export interface UpdateUserDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof UpdateUserDto
|
||||||
|
*/
|
||||||
|
'avatarColor'?: UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4399,6 +4413,8 @@ export interface UpdateUserDto {
|
|||||||
*/
|
*/
|
||||||
'storageLabel'?: string;
|
'storageLabel'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -4436,12 +4452,40 @@ export interface UsageByUserDto {
|
|||||||
*/
|
*/
|
||||||
'videos': number;
|
'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
|
* @export
|
||||||
* @interface UserDto
|
* @interface UserDto
|
||||||
*/
|
*/
|
||||||
export interface UserDto {
|
export interface UserDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof UserDto
|
||||||
|
*/
|
||||||
|
'avatarColor': UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4467,12 +4511,20 @@ export interface UserDto {
|
|||||||
*/
|
*/
|
||||||
'profileImagePath': string;
|
'profileImagePath': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @interface UserResponseDto
|
* @interface UserResponseDto
|
||||||
*/
|
*/
|
||||||
export interface UserResponseDto {
|
export interface UserResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof UserResponseDto
|
||||||
|
*/
|
||||||
|
'avatarColor': UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4552,6 +4604,8 @@ export interface UserResponseDto {
|
|||||||
*/
|
*/
|
||||||
'updatedAt': string;
|
'updatedAt': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -16477,6 +16531,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
|
|||||||
options: localVarRequestOptions,
|
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
|
* @param {string} id
|
||||||
@ -16802,6 +16894,15 @@ export const UserApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
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
|
* @param {string} id
|
||||||
@ -16899,6 +17000,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
|
|||||||
createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
|
createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
|
||||||
return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath));
|
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.
|
* @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));
|
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.
|
* @param {UserApiDeleteUserRequest} requestParameters Request parameters.
|
||||||
|
@ -117,12 +117,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
buildOwnerInfo() {
|
buildOwnerInfo() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: owner != null
|
leading:
|
||||||
? UserCircleAvatar(
|
owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
|
||||||
user: owner,
|
|
||||||
useRandomBackgroundColor: true,
|
|
||||||
)
|
|
||||||
: const SizedBox(),
|
|
||||||
title: Text(
|
title: Text(
|
||||||
album.owner.value?.name ?? "",
|
album.owner.value?.name ?? "",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -151,7 +147,6 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
leading: UserCircleAvatar(
|
leading: UserCircleAvatar(
|
||||||
user: user,
|
user: user,
|
||||||
useRandomBackgroundColor: true,
|
|
||||||
radius: 22,
|
radius: 22,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
|
@ -217,7 +217,6 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
user: album.sharedUsers.toList()[index],
|
user: album.sharedUsers.toList()[index],
|
||||||
radius: 18,
|
radius: 18,
|
||||||
size: 36,
|
size: 36,
|
||||||
useRandomBackgroundColor: true,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/utils/hash.dart';
|
import 'package:immich_mobile/utils/hash.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
@ -16,6 +18,7 @@ class User {
|
|||||||
this.isPartnerSharedBy = false,
|
this.isPartnerSharedBy = false,
|
||||||
this.isPartnerSharedWith = false,
|
this.isPartnerSharedWith = false,
|
||||||
this.profileImagePath = '',
|
this.profileImagePath = '',
|
||||||
|
this.avatarColor = AvatarColorEnum.primary,
|
||||||
this.memoryEnabled = true,
|
this.memoryEnabled = true,
|
||||||
this.inTimeline = false,
|
this.inTimeline = false,
|
||||||
});
|
});
|
||||||
@ -32,6 +35,7 @@ class User {
|
|||||||
profileImagePath = dto.profileImagePath,
|
profileImagePath = dto.profileImagePath,
|
||||||
isAdmin = dto.isAdmin,
|
isAdmin = dto.isAdmin,
|
||||||
memoryEnabled = dto.memoriesEnabled ?? false,
|
memoryEnabled = dto.memoriesEnabled ?? false,
|
||||||
|
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||||
inTimeline = false;
|
inTimeline = false;
|
||||||
|
|
||||||
User.fromPartnerDto(PartnerResponseDto dto)
|
User.fromPartnerDto(PartnerResponseDto dto)
|
||||||
@ -44,6 +48,7 @@ class User {
|
|||||||
profileImagePath = dto.profileImagePath,
|
profileImagePath = dto.profileImagePath,
|
||||||
isAdmin = dto.isAdmin,
|
isAdmin = dto.isAdmin,
|
||||||
memoryEnabled = dto.memoriesEnabled ?? false,
|
memoryEnabled = dto.memoriesEnabled ?? false,
|
||||||
|
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||||
inTimeline = dto.inTimeline ?? false;
|
inTimeline = dto.inTimeline ?? false;
|
||||||
|
|
||||||
@Index(unique: true, replace: false, type: IndexType.hash)
|
@Index(unique: true, replace: false, type: IndexType.hash)
|
||||||
@ -55,6 +60,8 @@ class User {
|
|||||||
bool isPartnerSharedWith;
|
bool isPartnerSharedWith;
|
||||||
bool isAdmin;
|
bool isAdmin;
|
||||||
String profileImagePath;
|
String profileImagePath;
|
||||||
|
@Enumerated(EnumType.ordinal)
|
||||||
|
AvatarColorEnum avatarColor;
|
||||||
bool memoryEnabled;
|
bool memoryEnabled;
|
||||||
bool inTimeline;
|
bool inTimeline;
|
||||||
|
|
||||||
@ -68,6 +75,7 @@ class User {
|
|||||||
if (other is! User) return false;
|
if (other is! User) return false;
|
||||||
return id == other.id &&
|
return id == other.id &&
|
||||||
updatedAt.isAtSameMomentAs(other.updatedAt) &&
|
updatedAt.isAtSameMomentAs(other.updatedAt) &&
|
||||||
|
avatarColor == other.avatarColor &&
|
||||||
email == other.email &&
|
email == other.email &&
|
||||||
name == other.name &&
|
name == other.name &&
|
||||||
isPartnerSharedBy == other.isPartnerSharedBy &&
|
isPartnerSharedBy == other.isPartnerSharedBy &&
|
||||||
@ -88,7 +96,77 @@ class User {
|
|||||||
isPartnerSharedBy.hashCode ^
|
isPartnerSharedBy.hashCode ^
|
||||||
isPartnerSharedWith.hashCode ^
|
isPartnerSharedWith.hashCode ^
|
||||||
profileImagePath.hashCode ^
|
profileImagePath.hashCode ^
|
||||||
|
avatarColor.hashCode ^
|
||||||
isAdmin.hashCode ^
|
isAdmin.hashCode ^
|
||||||
memoryEnabled.hashCode ^
|
memoryEnabled.hashCode ^
|
||||||
inTimeline.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Binary file not shown.
@ -22,14 +22,12 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||||||
final user = Store.tryGet(StoreKey.currentUser);
|
final user = Store.tryGet(StoreKey.currentUser);
|
||||||
|
|
||||||
buildUserProfileImage() {
|
buildUserProfileImage() {
|
||||||
const immichImage = CircleAvatar(
|
if (user == null) {
|
||||||
radius: 20,
|
return const CircleAvatar(
|
||||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
radius: 20,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
);
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
if (authState.profileImagePath.isEmpty || user == null) {
|
|
||||||
return immichImage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final userImage = UserCircleAvatar(
|
final userImage = UserCircleAvatar(
|
||||||
@ -38,18 +36,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||||||
user: user,
|
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) {
|
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
@ -58,7 +44,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return immichImage;
|
return userImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
pickUserProfileImage() async {
|
pickUserProfileImage() async {
|
||||||
|
@ -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/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_dialog.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/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/routing/router.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.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 =
|
final bool isEnableAutoBackup =
|
||||||
backupState.backgroundBackup || backupState.autoBackup;
|
backupState.backgroundBackup || backupState.autoBackup;
|
||||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||||
AuthenticationState authState = ref.watch(authenticationProvider);
|
|
||||||
final user = Store.tryGet(StoreKey.currentUser);
|
final user = Store.tryGet(StoreKey.currentUser);
|
||||||
final isDarkTheme = context.isDarkTheme;
|
final isDarkTheme = context.isDarkTheme;
|
||||||
const widgetSize = 30.0;
|
const widgetSize = 30.0;
|
||||||
@ -55,7 +52,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
isLabelVisible: serverInfoState.isVersionMismatch,
|
isLabelVisible: serverInfoState.isVersionMismatch,
|
||||||
offset: const Offset(2, 2),
|
offset: const Offset(2, 2),
|
||||||
child: authState.profileImagePath.isEmpty || user == null
|
child: user == null
|
||||||
? const Icon(
|
? const Icon(
|
||||||
Icons.face_outlined,
|
Icons.face_outlined,
|
||||||
size: widgetSize,
|
size: widgetSize,
|
||||||
|
@ -3,7 +3,6 @@ import 'dart:math';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/store.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
||||||
@ -13,32 +12,17 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||||||
final User user;
|
final User user;
|
||||||
double radius;
|
double radius;
|
||||||
double size;
|
double size;
|
||||||
bool useRandomBackgroundColor;
|
|
||||||
|
|
||||||
UserCircleAvatar({
|
UserCircleAvatar({
|
||||||
super.key,
|
super.key,
|
||||||
this.radius = 22,
|
this.radius = 22,
|
||||||
this.size = 44,
|
this.size = 44,
|
||||||
this.useRandomBackgroundColor = false,
|
|
||||||
required this.user,
|
required this.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final randomColors = [
|
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
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],
|
|
||||||
];
|
|
||||||
|
|
||||||
final profileImageUrl =
|
final profileImageUrl =
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}';
|
'${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(),
|
user.name[0].toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: context.isDarkTheme ? Colors.black : Colors.white,
|
fontSize: 12,
|
||||||
|
color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: useRandomBackgroundColor
|
backgroundColor: user.avatarColor.toColor(),
|
||||||
? randomColors[Random().nextInt(randomColors.length)]
|
|
||||||
: context.primaryColor,
|
|
||||||
radius: radius,
|
radius: radius,
|
||||||
child: user.profileImagePath == ""
|
child: user.profileImagePath.isEmpty
|
||||||
? textIcon
|
? textIcon
|
||||||
: ClipRRect(
|
: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@ -166,6 +166,7 @@ doc/UpdateTagDto.md
|
|||||||
doc/UpdateUserDto.md
|
doc/UpdateUserDto.md
|
||||||
doc/UsageByUserDto.md
|
doc/UsageByUserDto.md
|
||||||
doc/UserApi.md
|
doc/UserApi.md
|
||||||
|
doc/UserAvatarColor.md
|
||||||
doc/UserDto.md
|
doc/UserDto.md
|
||||||
doc/UserResponseDto.md
|
doc/UserResponseDto.md
|
||||||
doc/ValidateAccessTokenResponseDto.md
|
doc/ValidateAccessTokenResponseDto.md
|
||||||
@ -343,6 +344,7 @@ lib/model/update_stack_parent_dto.dart
|
|||||||
lib/model/update_tag_dto.dart
|
lib/model/update_tag_dto.dart
|
||||||
lib/model/update_user_dto.dart
|
lib/model/update_user_dto.dart
|
||||||
lib/model/usage_by_user_dto.dart
|
lib/model/usage_by_user_dto.dart
|
||||||
|
lib/model/user_avatar_color.dart
|
||||||
lib/model/user_dto.dart
|
lib/model/user_dto.dart
|
||||||
lib/model/user_response_dto.dart
|
lib/model/user_response_dto.dart
|
||||||
lib/model/validate_access_token_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/update_user_dto_test.dart
|
||||||
test/usage_by_user_dto_test.dart
|
test/usage_by_user_dto_test.dart
|
||||||
test/user_api_test.dart
|
test/user_api_test.dart
|
||||||
|
test/user_avatar_color_test.dart
|
||||||
test/user_dto_test.dart
|
test/user_dto_test.dart
|
||||||
test/user_response_dto_test.dart
|
test/user_response_dto_test.dart
|
||||||
test/validate_access_token_response_dto_test.dart
|
test/validate_access_token_response_dto_test.dart
|
||||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/PartnerResponseDto.md
generated
BIN
mobile/openapi/doc/PartnerResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UpdateUserDto.md
generated
BIN
mobile/openapi/doc/UpdateUserDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UserApi.md
generated
BIN
mobile/openapi/doc/UserApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UserAvatarColor.md
generated
Normal file
BIN
mobile/openapi/doc/UserAvatarColor.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/UserDto.md
generated
BIN
mobile/openapi/doc/UserDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UserResponseDto.md
generated
BIN
mobile/openapi/doc/UserResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/user_api.dart
generated
BIN
mobile/openapi/lib/api/user_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/update_user_dto.dart
generated
BIN
mobile/openapi/lib/model/update_user_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_avatar_color.dart
generated
Normal file
BIN
mobile/openapi/lib/model/user_avatar_color.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/user_dto.dart
generated
BIN
mobile/openapi/lib/model/user_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/partner_response_dto_test.dart
generated
BIN
mobile/openapi/test/partner_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/update_user_dto_test.dart
generated
BIN
mobile/openapi/test/update_user_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/user_api_test.dart
generated
BIN
mobile/openapi/test/user_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/user_avatar_color_test.dart
generated
Normal file
BIN
mobile/openapi/test/user_avatar_color_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/user_dto_test.dart
generated
BIN
mobile/openapi/test/user_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/user_response_dto_test.dart
generated
BIN
mobile/openapi/test/user_response_dto_test.dart
generated
Binary file not shown.
@ -5578,6 +5578,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/user/profile-image": {
|
"/user/profile-image": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteProfileImage",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
]
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "createProfileImage",
|
"operationId": "createProfileImage",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
@ -7632,6 +7655,9 @@
|
|||||||
},
|
},
|
||||||
"PartnerResponseDto": {
|
"PartnerResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -7682,6 +7708,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"avatarColor",
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
@ -9140,6 +9167,9 @@
|
|||||||
},
|
},
|
||||||
"UpdateUserDto": {
|
"UpdateUserDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -9202,8 +9232,26 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"UserAvatarColor": {
|
||||||
|
"enum": [
|
||||||
|
"primary",
|
||||||
|
"pink",
|
||||||
|
"red",
|
||||||
|
"yellow",
|
||||||
|
"blue",
|
||||||
|
"green",
|
||||||
|
"purple",
|
||||||
|
"orange",
|
||||||
|
"gray",
|
||||||
|
"amber"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"UserDto": {
|
"UserDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -9218,6 +9266,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"avatarColor",
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
@ -9227,6 +9276,9 @@
|
|||||||
},
|
},
|
||||||
"UserResponseDto": {
|
"UserResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -9274,6 +9326,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"avatarColor",
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
@ -248,6 +248,7 @@ describe('AuthService', () => {
|
|||||||
userMock.getAdmin.mockResolvedValue(null);
|
userMock.getAdmin.mockResolvedValue(null);
|
||||||
userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: new Date('2021-01-01') } as UserEntity);
|
userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: new Date('2021-01-01') } as UserEntity);
|
||||||
await expect(sut.adminSignUp(dto)).resolves.toEqual({
|
await expect(sut.adminSignUp(dto)).resolves.toEqual({
|
||||||
|
avatarColor: expect.any(String),
|
||||||
id: 'admin',
|
id: 'admin',
|
||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
email: 'test@immich.com',
|
email: 'test@immich.com',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UserAvatarColor } from '@app/infra/entities';
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { authStub, newPartnerRepositoryMock, partnerStub } from '@test';
|
import { authStub, newPartnerRepositoryMock, partnerStub } from '@test';
|
||||||
import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories';
|
import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories';
|
||||||
@ -19,6 +20,7 @@ const responseDto = {
|
|||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
externalPath: null,
|
externalPath: null,
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
inTimeline: true,
|
inTimeline: true,
|
||||||
},
|
},
|
||||||
user1: <PartnerResponseDto>{
|
user1: <PartnerResponseDto>{
|
||||||
@ -35,6 +37,7 @@ const responseDto = {
|
|||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
externalPath: null,
|
externalPath: null,
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
inTimeline: true,
|
inTimeline: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { UserAvatarColor } from '@app/infra/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
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';
|
import { Optional, toEmail, toSanitized } from '../../domain.util';
|
||||||
|
|
||||||
export class UpdateUserDto {
|
export class UpdateUserDto {
|
||||||
@ -44,4 +45,9 @@ export class UpdateUserDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
memoriesEnabled?: boolean;
|
memoriesEnabled?: boolean;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor?: UserAvatarColor;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
export class UserDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
name!: string;
|
name!: string;
|
||||||
email!: string;
|
email!: string;
|
||||||
profileImagePath!: string;
|
profileImagePath!: string;
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor!: UserAvatarColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserResponseDto extends UserDto {
|
export class UserResponseDto extends UserDto {
|
||||||
@ -25,6 +41,7 @@ export const mapSimpleUser = (entity: UserEntity): UserDto => {
|
|||||||
email: entity.email,
|
email: entity.email,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
profileImagePath: entity.profileImagePath,
|
profileImagePath: entity.profileImagePath,
|
||||||
|
avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,7 +98,6 @@ export class UserCore {
|
|||||||
if (payload.storageLabel) {
|
if (payload.storageLabel) {
|
||||||
payload.storageLabel = sanitize(payload.storageLabel);
|
payload.storageLabel = sanitize(payload.storageLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userEntity = await this.userRepository.create(payload);
|
const userEntity = await this.userRepository.create(payload);
|
||||||
await this.libraryRepository.create({
|
await this.libraryRepository.create({
|
||||||
owner: { id: userEntity.id } as UserEntity,
|
owner: { id: userEntity.id } as UserEntity,
|
||||||
|
@ -323,17 +323,52 @@ describe(UserService.name, () => {
|
|||||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||||
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||||
|
|
||||||
await sut.createProfileImage(userStub.admin, file);
|
await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(BadRequestException);
|
||||||
|
|
||||||
expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, { profileImagePath: file.path });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the user profile could not be updated with the new image', async () => {
|
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;
|
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||||
|
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||||
userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
|
userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
|
||||||
|
|
||||||
await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
|
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', () => {
|
describe('getUserProfileImage', () => {
|
||||||
|
@ -93,10 +93,23 @@ export class UserService {
|
|||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
fileInfo: Express.Multer.File,
|
fileInfo: Express.Multer.File,
|
||||||
): Promise<CreateProfileImageResponseDto> {
|
): Promise<CreateProfileImageResponseDto> {
|
||||||
|
const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false });
|
||||||
const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path });
|
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);
|
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteProfileImage(authUser: AuthUserDto): Promise<void> {
|
||||||
|
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<ImmichReadStream> {
|
async getProfileImage(id: string): Promise<ImmichReadStream> {
|
||||||
const user = await this.findOrFail(id, {});
|
const user = await this.findOrFail(id, {});
|
||||||
if (!user.profileImagePath) {
|
if (!user.profileImagePath) {
|
||||||
@ -111,7 +124,7 @@ export class UserService {
|
|||||||
throw new BadRequestException('Admin account does not exist');
|
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, '');
|
const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, '');
|
||||||
|
|
||||||
await this.userCore.updateUser(admin, admin.id, { password });
|
await this.userCore.updateUser(admin, admin.id, { password });
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
SignUpDto,
|
SignUpDto,
|
||||||
UserResponseDto,
|
UserResponseDto,
|
||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
|
mapUser,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
@ -71,7 +72,7 @@ export class AuthController {
|
|||||||
@Post('change-password')
|
@Post('change-password')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
|
changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
|
||||||
return this.service.changePassword(authUser, dto);
|
return this.service.changePassword(authUser, dto).then(mapUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
|
@ -13,6 +13,8 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
Header,
|
Header,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
@ -54,6 +56,12 @@ export class UserController {
|
|||||||
return this.service.create(createUserDto);
|
return this.service.create(createUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete('profile-image')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
deleteProfileImage(@AuthUser() authUser: AuthUserDto): Promise<void> {
|
||||||
|
return this.service.deleteProfileImage(authUser);
|
||||||
|
}
|
||||||
|
|
||||||
@AdminRoute()
|
@AdminRoute()
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||||
|
@ -10,6 +10,19 @@ import {
|
|||||||
import { AssetEntity } from './asset.entity';
|
import { AssetEntity } from './asset.entity';
|
||||||
import { TagEntity } from './tag.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')
|
@Entity('users')
|
||||||
export class UserEntity {
|
export class UserEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -18,6 +31,9 @@ export class UserEntity {
|
|||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
avatarColor!: UserAvatarColor | null;
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
isAdmin!: boolean;
|
isAdmin!: boolean;
|
||||||
|
|
||||||
|
14
server/src/infra/migrations/1699889987493-AddAvatarColor.ts
Normal file
14
server/src/infra/migrations/1699889987493-AddAvatarColor.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddAvatarColor1699889987493 implements MigrationInterface {
|
||||||
|
name = 'AddAvatarColor1699889987493'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "avatarColor" character varying`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarColor"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,6 +18,7 @@ const password = 'Password123';
|
|||||||
const email = 'admin@immich.app';
|
const email = 'admin@immich.app';
|
||||||
|
|
||||||
const adminSignupResponse = {
|
const adminSignupResponse = {
|
||||||
|
avatarColor: expect.any(String),
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: 'Immich Admin',
|
name: 'Immich Admin',
|
||||||
email: 'admin@immich.app',
|
email: 'admin@immich.app',
|
||||||
|
9
server/test/fixtures/user.stub.ts
vendored
9
server/test/fixtures/user.stub.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import { UserEntity } from '@app/infra/entities';
|
import { UserAvatarColor, UserEntity } from '@app/infra/entities';
|
||||||
import { authStub } from './auth.stub';
|
import { authStub } from './auth.stub';
|
||||||
|
|
||||||
export const userStub = {
|
export const userStub = {
|
||||||
@ -17,6 +17,7 @@ export const userStub = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
}),
|
}),
|
||||||
user1: Object.freeze<UserEntity>({
|
user1: Object.freeze<UserEntity>({
|
||||||
...authStub.user1,
|
...authStub.user1,
|
||||||
@ -33,6 +34,7 @@ export const userStub = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
}),
|
}),
|
||||||
user2: Object.freeze<UserEntity>({
|
user2: Object.freeze<UserEntity>({
|
||||||
...authStub.user2,
|
...authStub.user2,
|
||||||
@ -49,6 +51,7 @@ export const userStub = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
}),
|
}),
|
||||||
storageLabel: Object.freeze<UserEntity>({
|
storageLabel: Object.freeze<UserEntity>({
|
||||||
...authStub.user1,
|
...authStub.user1,
|
||||||
@ -65,6 +68,7 @@ export const userStub = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
}),
|
}),
|
||||||
externalPath1: Object.freeze<UserEntity>({
|
externalPath1: Object.freeze<UserEntity>({
|
||||||
...authStub.user1,
|
...authStub.user1,
|
||||||
@ -81,6 +85,7 @@ export const userStub = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
}),
|
}),
|
||||||
externalPath2: Object.freeze<UserEntity>({
|
externalPath2: Object.freeze<UserEntity>({
|
||||||
...authStub.user1,
|
...authStub.user1,
|
||||||
@ -97,6 +102,7 @@ export const userStub = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
}),
|
}),
|
||||||
profilePath: Object.freeze<UserEntity>({
|
profilePath: Object.freeze<UserEntity>({
|
||||||
...authStub.user1,
|
...authStub.user1,
|
||||||
@ -113,5 +119,6 @@ export const userStub = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
119
web/src/api/open-api/api.ts
generated
119
web/src/api/open-api/api.ts
generated
@ -2355,6 +2355,12 @@ export interface OAuthConfigResponseDto {
|
|||||||
* @interface PartnerResponseDto
|
* @interface PartnerResponseDto
|
||||||
*/
|
*/
|
||||||
export interface PartnerResponseDto {
|
export interface PartnerResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof PartnerResponseDto
|
||||||
|
*/
|
||||||
|
'avatarColor': UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -2440,6 +2446,8 @@ export interface PartnerResponseDto {
|
|||||||
*/
|
*/
|
||||||
'updatedAt': string;
|
'updatedAt': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -4344,6 +4352,12 @@ export interface UpdateTagDto {
|
|||||||
* @interface UpdateUserDto
|
* @interface UpdateUserDto
|
||||||
*/
|
*/
|
||||||
export interface UpdateUserDto {
|
export interface UpdateUserDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof UpdateUserDto
|
||||||
|
*/
|
||||||
|
'avatarColor'?: UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4399,6 +4413,8 @@ export interface UpdateUserDto {
|
|||||||
*/
|
*/
|
||||||
'storageLabel'?: string;
|
'storageLabel'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -4436,12 +4452,40 @@ export interface UsageByUserDto {
|
|||||||
*/
|
*/
|
||||||
'videos': number;
|
'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
|
* @export
|
||||||
* @interface UserDto
|
* @interface UserDto
|
||||||
*/
|
*/
|
||||||
export interface UserDto {
|
export interface UserDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof UserDto
|
||||||
|
*/
|
||||||
|
'avatarColor': UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4467,12 +4511,20 @@ export interface UserDto {
|
|||||||
*/
|
*/
|
||||||
'profileImagePath': string;
|
'profileImagePath': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @interface UserResponseDto
|
* @interface UserResponseDto
|
||||||
*/
|
*/
|
||||||
export interface UserResponseDto {
|
export interface UserResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UserAvatarColor}
|
||||||
|
* @memberof UserResponseDto
|
||||||
|
*/
|
||||||
|
'avatarColor': UserAvatarColor;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4552,6 +4604,8 @@ export interface UserResponseDto {
|
|||||||
*/
|
*/
|
||||||
'updatedAt': string;
|
'updatedAt': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -16477,6 +16531,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
|
|||||||
options: localVarRequestOptions,
|
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
|
* @param {string} id
|
||||||
@ -16802,6 +16894,15 @@ export const UserApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
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
|
* @param {string} id
|
||||||
@ -16899,6 +17000,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
|
|||||||
createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
|
createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
|
||||||
return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath));
|
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.
|
* @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));
|
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.
|
* @param {UserApiDeleteUserRequest} requestParameters Request parameters.
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
<section class="immich-scrollbar max-h-[400px] overflow-y-auto pb-4">
|
<section class="immich-scrollbar max-h-[400px] overflow-y-auto pb-4">
|
||||||
<div class="flex w-full place-items-center justify-between gap-4 p-5">
|
<div class="flex w-full place-items-center justify-between gap-4 p-5">
|
||||||
<div class="flex place-items-center gap-4">
|
<div class="flex place-items-center gap-4">
|
||||||
<UserAvatar user={album.owner} size="md" autoColor />
|
<UserAvatar user={album.owner} size="md" />
|
||||||
<p class="text-sm font-medium">{album.owner.name}</p>
|
<p class="text-sm font-medium">{album.owner.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -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"
|
class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<div class="flex place-items-center gap-4">
|
<div class="flex place-items-center gap-4">
|
||||||
<UserAvatar {user} size="md" autoColor />
|
<UserAvatar {user} size="md" />
|
||||||
<p class="text-sm font-medium">{user.name}</p>
|
<p class="text-sm font-medium">{user.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
on:click={() => handleUnselect(user)}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<UserAvatar {user} size="sm" autoColor />
|
<UserAvatar {user} size="sm" />
|
||||||
<p class="text-xs font-medium">{user.name}</p>
|
<p class="text-xs font-medium">{user.name}</p>
|
||||||
</button>
|
</button>
|
||||||
{/key}
|
{/key}
|
||||||
@ -94,7 +94,7 @@
|
|||||||
>✓</span
|
>✓</span
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<UserAvatar {user} size="md" autoColor />
|
<UserAvatar {user} size="md" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
|
@ -333,7 +333,7 @@
|
|||||||
<p class="text-sm">SHARED BY</p>
|
<p class="text-sm">SHARED BY</p>
|
||||||
<div class="flex gap-4 pt-4">
|
<div class="flex gap-4 pt-4">
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar user={asset.owner} size="md" autoColor />
|
<UserAvatar user={asset.owner} size="md" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-auto mt-auto">
|
<div class="mb-auto mt-auto">
|
||||||
|
@ -1,16 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import type { UserResponseDto } from '@api';
|
import { api, UserAvatarColor, type UserResponseDto } from '@api';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import UserAvatar from '../user-avatar.svelte';
|
import UserAvatar from '../user-avatar.svelte';
|
||||||
import { mdiCog, mdiLogout } from '@mdi/js';
|
import { mdiCog, mdiLogout, mdiPencil } from '@mdi/js';
|
||||||
|
import { notificationController, NotificationType } from '../notification/notification';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import AvatarSelector from './avatar-selector.svelte';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
let isShowSelectAvatar = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const handleSaveProfile = async (color: UserAvatarColor) => {
|
||||||
|
try {
|
||||||
|
if (user.profileImagePath !== '') {
|
||||||
|
await api.userApi.deleteProfileImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.userApi.updateUser({
|
||||||
|
updateUserDto: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
avatarColor: color,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
user = data;
|
||||||
|
isShowSelectAvatar = false;
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: 'Saved profile',
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to save profile');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -22,8 +54,22 @@
|
|||||||
<div
|
<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"
|
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>
|
<div>
|
||||||
<p class="text-center text-lg font-medium text-immich-primary dark:text-immich-dark-primary">
|
<p class="text-center text-lg font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||||
{user.name}
|
{user.name}
|
||||||
@ -51,3 +97,10 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if isShowSelectAvatar}
|
||||||
|
<AvatarSelector
|
||||||
|
{user}
|
||||||
|
on:close={() => (isShowSelectAvatar = false)}
|
||||||
|
on:choose={({ detail: color }) => handleSaveProfile(color)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { mdiClose } from '@mdi/js';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { UserAvatarColor, UserResponseDto } from '@api';
|
||||||
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import FullScreenModal from '../full-screen-modal.svelte';
|
||||||
|
import UserAvatar from '../user-avatar.svelte';
|
||||||
|
|
||||||
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FullScreenModal on:clickOutside={() => dispatch('close')} on:escape={() => dispatch('close')}>
|
||||||
|
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
|
||||||
|
<div
|
||||||
|
class=" rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary text-sm">
|
||||||
|
SELECT AVATAR COLOR
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center p-4 mt-4">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
|
{#each colors as color}
|
||||||
|
<button on:click={() => dispatch('choose', color)}>
|
||||||
|
<UserAvatar {user} {color} size="xl" showProfileImage={false} />
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FullScreenModal>
|
@ -124,7 +124,9 @@
|
|||||||
on:mouseleave={() => (shouldShowAccountInfo = false)}
|
on:mouseleave={() => (shouldShowAccountInfo = false)}
|
||||||
on:click={() => (shouldShowAccountInfoPanel = !shouldShowAccountInfoPanel)}
|
on:click={() => (shouldShowAccountInfoPanel = !shouldShowAccountInfoPanel)}
|
||||||
>
|
>
|
||||||
<UserAvatar {user} size="lg" showTitle={false} interactive />
|
{#key user}
|
||||||
|
<UserAvatar {user} size="lg" showTitle={false} interactive />
|
||||||
|
{/key}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if shouldShowAccountInfo && !shouldShowAccountInfoPanel}
|
{#if shouldShowAccountInfo && !shouldShowAccountInfoPanel}
|
||||||
@ -139,7 +141,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if shouldShowAccountInfoPanel}
|
{#if shouldShowAccountInfoPanel}
|
||||||
<AccountInfoPanel {user} on:logout={logOut} />
|
<AccountInfoPanel bind:user on:logout={logOut} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,35 +1,40 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
export type Color = 'primary' | 'pink' | 'red' | 'yellow' | 'blue' | 'green';
|
export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
|
||||||
export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { imageLoad } from '$lib/utils/image-load';
|
import { imageLoad } from '$lib/utils/image-load';
|
||||||
import { api } from '@api';
|
import { UserAvatarColor, api } from '@api';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
profileImagePath: string;
|
profileImagePath: string;
|
||||||
|
avatarColor: UserAvatarColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let user: User;
|
export let user: User;
|
||||||
export let color: Color = 'primary';
|
export let color: UserAvatarColor = user.avatarColor;
|
||||||
export let size: Size = 'full';
|
export let size: Size = 'full';
|
||||||
export let rounded = true;
|
export let rounded = true;
|
||||||
export let interactive = false;
|
export let interactive = false;
|
||||||
export let showTitle = true;
|
export let showTitle = true;
|
||||||
export let autoColor = false;
|
export let showProfileImage = true;
|
||||||
|
|
||||||
let showFallback = true;
|
let showFallback = true;
|
||||||
|
|
||||||
const colorClasses: Record<Color, string> = {
|
const colorClasses: Record<UserAvatarColor, string> = {
|
||||||
primary: 'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
|
primary: 'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
|
||||||
pink: 'bg-pink-400 text-immich-bg',
|
pink: 'bg-pink-400 text-immich-bg',
|
||||||
red: 'bg-red-500 text-immich-bg',
|
red: 'bg-red-500 text-immich-bg',
|
||||||
yellow: 'bg-yellow-500 text-immich-bg',
|
yellow: 'bg-yellow-500 text-immich-bg',
|
||||||
blue: 'bg-blue-500 text-immich-bg',
|
blue: 'bg-blue-500 text-immich-bg',
|
||||||
green: 'bg-green-600 text-immich-bg',
|
green: 'bg-green-600 text-immich-bg',
|
||||||
|
purple: 'bg-purple-600 text-immich-bg',
|
||||||
|
orange: 'bg-orange-600 text-immich-bg',
|
||||||
|
gray: 'bg-gray-600 text-immich-bg',
|
||||||
|
amber: 'bg-amber-600 text-immich-bg',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<Size, string> = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
@ -37,18 +42,12 @@
|
|||||||
sm: 'w-7 h-7',
|
sm: 'w-7 h-7',
|
||||||
md: 'w-10 h-10',
|
md: 'w-10 h-10',
|
||||||
lg: 'w-12 h-12',
|
lg: 'w-12 h-12',
|
||||||
xl: 'w-20 h-20',
|
xl: 'w-16 h-16',
|
||||||
|
xxl: 'w-24 h-24',
|
||||||
|
xxxl: 'w-28 h-28',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get color based on the user UUID.
|
$: colorClass = colorClasses[color];
|
||||||
function getUserColor() {
|
|
||||||
const seed = parseInt(user.id.split('-')[0], 16);
|
|
||||||
const colors = Object.keys(colorClasses).filter((color) => color !== 'primary') as Color[];
|
|
||||||
const randomIndex = seed % colors.length;
|
|
||||||
return colors[randomIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
$: colorClass = colorClasses[autoColor ? getUserColor() : color];
|
|
||||||
$: sizeClass = sizeClasses[size];
|
$: sizeClass = sizeClasses[size];
|
||||||
$: title = `${user.name} (${user.email})`;
|
$: title = `${user.name} (${user.email})`;
|
||||||
$: interactiveClass = interactive
|
$: interactiveClass = interactive
|
||||||
@ -61,7 +60,7 @@
|
|||||||
class:rounded-full={rounded}
|
class:rounded-full={rounded}
|
||||||
title={showTitle ? title : undefined}
|
title={showTitle ? title : undefined}
|
||||||
>
|
>
|
||||||
{#if user.profileImagePath}
|
{#if showProfileImage && user.profileImagePath}
|
||||||
<img
|
<img
|
||||||
src={api.getProfileImageUrl(user.id)}
|
src={api.getProfileImageUrl(user.id)}
|
||||||
alt="Profile image of {title}"
|
alt="Profile image of {title}"
|
||||||
@ -74,12 +73,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if showFallback}
|
{#if showFallback}
|
||||||
<span
|
<span
|
||||||
class="flex h-full w-full select-none items-center justify-center"
|
class="flex h-full w-full select-none items-center justify-center font-medium"
|
||||||
class:text-xs={size === 'sm'}
|
class:text-xs={size === 'sm'}
|
||||||
class:text-lg={size === 'lg'}
|
class:text-lg={size === 'lg'}
|
||||||
class:text-xl={size === 'xl'}
|
class:text-xl={size === 'xl'}
|
||||||
class:font-medium={!autoColor}
|
class:text-2xl={size === 'xxl'}
|
||||||
class:font-semibold={autoColor}
|
class:text-3xl={size === 'xxxl'}
|
||||||
>
|
>
|
||||||
{(user.name[0] || '').toUpperCase()}
|
{(user.name[0] || '').toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
>✓</span
|
>✓</span
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<UserAvatar {user} size="lg" autoColor />
|
<UserAvatar {user} size="lg" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
|
@ -113,7 +113,7 @@
|
|||||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 mt-6 bg-slate-50 dark:bg-gray-900 p-5">
|
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 mt-6 bg-slate-50 dark:bg-gray-900 p-5">
|
||||||
<div class="flex gap-4 rounded-lg pb-4 transition-all justify-between">
|
<div class="flex gap-4 rounded-lg pb-4 transition-all justify-between">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<UserAvatar user={partner.user} size="md" autoColor />
|
<UserAvatar user={partner.user} size="md" />
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||||
{partner.user.name}
|
{partner.user.name}
|
||||||
|
@ -603,13 +603,13 @@
|
|||||||
|
|
||||||
<!-- owner -->
|
<!-- owner -->
|
||||||
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||||
<UserAvatar user={album.owner} size="md" autoColor />
|
<UserAvatar user={album.owner} size="md" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- users -->
|
<!-- users -->
|
||||||
{#each album.sharedUsers as user (user.id)}
|
{#each album.sharedUsers as user (user.id)}
|
||||||
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||||
<UserAvatar {user} size="md" autoColor />
|
<UserAvatar {user} size="md" />
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
href="/partners/{partner.id}"
|
href="/partners/{partner.id}"
|
||||||
class="flex gap-4 rounded-lg px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
class="flex gap-4 rounded-lg px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<UserAvatar user={partner} size="lg" autoColor />
|
<UserAvatar user={partner} size="lg" />
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||||
{partner.name}
|
{partner.name}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { UserResponseDto } from '@api';
|
import { UserAvatarColor, type UserResponseDto } from '@api';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { Sync } from 'factory.ts';
|
import { Sync } from 'factory.ts';
|
||||||
|
|
||||||
@ -16,4 +16,5 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
|
|||||||
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
|
avatarColor: UserAvatarColor.Primary,
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user