Compare commits

...

34 commits
main ... 4779

Author SHA1 Message Date
Fynn Petersen-Frey
14ce849f90 update generated Isar code 2023-11-13 18:52:16 +01:00
martabal
49502f3230
fix: tests 2023-11-13 17:44:06 +01:00
martabal
fdf4f93208
fix: mobile 2023-11-13 17:12:32 +01:00
martabal
5d6bfa5a3e
merge main 2023-11-13 17:11:46 +01:00
martabal
5c37b27fcf
pr feedback 2023-11-13 17:01:48 +01:00
martabal
0ed5dc869a
pr feedback 2023-11-13 17:00:29 +01:00
martabal
ac9e2cd316
pr feedback 2023-11-13 16:46:30 +01:00
martabal
af0f2f005b
fix: e2e test 2023-11-13 12:41:20 +01:00
shalong-tanwen
543ff6f7fd conflict changes 2023-11-13 16:47:09 +05:30
martabal
f249a3761b
chore: regenerate api 2023-11-13 12:07:19 +01:00
martabal
b96f04efec
merge main 2023-11-13 11:59:47 +01:00
shalong-tanwen
1404b6441d conflict changes 2023-11-08 19:31:34 +05:30
martabal
91f8297a61
chore: regenerate api 2023-11-08 14:48:50 +01:00
martabal
97206faadb
merge main 2023-11-08 14:47:58 +01:00
shalong-tanwen
e3d6f7adb3 feat(mobile): user avatar colors 2023-11-06 01:30:22 +05:30
martabal
000e1f17c5
merge main 2023-11-05 18:33:30 +01:00
martabal
ee4120c5f7
merge main 2023-11-05 18:32:07 +01:00
martabal
6faa597aaf
fix: tests 2023-11-04 17:14:24 +01:00
martabal
21210ca297
fix: svelte file in correct folder 2023-11-04 15:47:09 +01:00
martabal
11f1ade8f2
pr feedback 2023-11-04 15:39:48 +01:00
martabal
40c1bfa27b
merge main 2023-11-04 15:23:49 +01:00
martabal
6c2bf550bc
chore: regenerate api 2023-11-04 15:22:37 +01:00
martabal
f28c369c16
merge main 2023-11-04 15:21:58 +01:00
martabal
fb9b854bf1
merge main 2023-11-04 15:21:33 +01:00
martabal
fe9348e049
remove autoColor from UserAvatar 2023-11-01 22:57:55 +01:00
martabal
8a6af72588
fix: do not use fix height and width 2023-11-01 21:20:29 +01:00
martabal
610b03d16c
pr feedback 2023-11-01 21:02:34 +01:00
martabal
3d6eadb595
fix: tests 2023-11-01 20:32:03 +01:00
martabal
68ebcf218d
fix: tests 2023-11-01 20:23:11 +01:00
martabal
46d640b7a1
pr feedback 2023-11-01 20:13:21 +01:00
martabal
839cc4f3f8
fix: tests 2023-11-01 19:14:46 +01:00
martabal
4f77d06592
feat: random avatar color on user creation 2023-11-01 19:06:35 +01:00
martabal
8c1e782f8f
fix: tests 2023-11-01 18:52:23 +01:00
martabal
cda2ff3d1f
feat: user avatar color 2023-11-01 18:22:49 +01:00
57 changed files with 1205 additions and 138 deletions

View file

@ -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.

View file

@ -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(

View file

@ -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,
), ),
); );
}), }),

View file

@ -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.name)
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,76 @@ 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 {
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);
}
}
}

View file

@ -17,53 +17,59 @@ const UserSchema = CollectionSchema(
name: r'User', name: r'User',
id: -7838171048429979076, id: -7838171048429979076,
properties: { properties: {
r'email': PropertySchema( r'avatarColor': PropertySchema(
id: 0, id: 0,
name: r'avatarColor',
type: IsarType.string,
enumMap: _UseravatarColorEnumValueMap,
),
r'email': PropertySchema(
id: 1,
name: r'email', name: r'email',
type: IsarType.string, type: IsarType.string,
), ),
r'id': PropertySchema( r'id': PropertySchema(
id: 1, id: 2,
name: r'id', name: r'id',
type: IsarType.string, type: IsarType.string,
), ),
r'inTimeline': PropertySchema( r'inTimeline': PropertySchema(
id: 2, id: 3,
name: r'inTimeline', name: r'inTimeline',
type: IsarType.bool, type: IsarType.bool,
), ),
r'isAdmin': PropertySchema( r'isAdmin': PropertySchema(
id: 3, id: 4,
name: r'isAdmin', name: r'isAdmin',
type: IsarType.bool, type: IsarType.bool,
), ),
r'isPartnerSharedBy': PropertySchema( r'isPartnerSharedBy': PropertySchema(
id: 4, id: 5,
name: r'isPartnerSharedBy', name: r'isPartnerSharedBy',
type: IsarType.bool, type: IsarType.bool,
), ),
r'isPartnerSharedWith': PropertySchema( r'isPartnerSharedWith': PropertySchema(
id: 5, id: 6,
name: r'isPartnerSharedWith', name: r'isPartnerSharedWith',
type: IsarType.bool, type: IsarType.bool,
), ),
r'memoryEnabled': PropertySchema( r'memoryEnabled': PropertySchema(
id: 6, id: 7,
name: r'memoryEnabled', name: r'memoryEnabled',
type: IsarType.bool, type: IsarType.bool,
), ),
r'name': PropertySchema( r'name': PropertySchema(
id: 7, id: 8,
name: r'name', name: r'name',
type: IsarType.string, type: IsarType.string,
), ),
r'profileImagePath': PropertySchema( r'profileImagePath': PropertySchema(
id: 8, id: 9,
name: r'profileImagePath', name: r'profileImagePath',
type: IsarType.string, type: IsarType.string,
), ),
r'updatedAt': PropertySchema( r'updatedAt': PropertySchema(
id: 9, id: 10,
name: r'updatedAt', name: r'updatedAt',
type: IsarType.dateTime, type: IsarType.dateTime,
) )
@ -117,6 +123,7 @@ int _userEstimateSize(
Map<Type, List<int>> allOffsets, Map<Type, List<int>> allOffsets,
) { ) {
var bytesCount = offsets.last; var bytesCount = offsets.last;
bytesCount += 3 + object.avatarColor.name.length * 3;
bytesCount += 3 + object.email.length * 3; bytesCount += 3 + object.email.length * 3;
bytesCount += 3 + object.id.length * 3; bytesCount += 3 + object.id.length * 3;
bytesCount += 3 + object.name.length * 3; bytesCount += 3 + object.name.length * 3;
@ -130,16 +137,17 @@ void _userSerialize(
List<int> offsets, List<int> offsets,
Map<Type, List<int>> allOffsets, Map<Type, List<int>> allOffsets,
) { ) {
writer.writeString(offsets[0], object.email); writer.writeString(offsets[0], object.avatarColor.name);
writer.writeString(offsets[1], object.id); writer.writeString(offsets[1], object.email);
writer.writeBool(offsets[2], object.inTimeline); writer.writeString(offsets[2], object.id);
writer.writeBool(offsets[3], object.isAdmin); writer.writeBool(offsets[3], object.inTimeline);
writer.writeBool(offsets[4], object.isPartnerSharedBy); writer.writeBool(offsets[4], object.isAdmin);
writer.writeBool(offsets[5], object.isPartnerSharedWith); writer.writeBool(offsets[5], object.isPartnerSharedBy);
writer.writeBool(offsets[6], object.memoryEnabled); writer.writeBool(offsets[6], object.isPartnerSharedWith);
writer.writeString(offsets[7], object.name); writer.writeBool(offsets[7], object.memoryEnabled);
writer.writeString(offsets[8], object.profileImagePath); writer.writeString(offsets[8], object.name);
writer.writeDateTime(offsets[9], object.updatedAt); writer.writeString(offsets[9], object.profileImagePath);
writer.writeDateTime(offsets[10], object.updatedAt);
} }
User _userDeserialize( User _userDeserialize(
@ -149,16 +157,19 @@ User _userDeserialize(
Map<Type, List<int>> allOffsets, Map<Type, List<int>> allOffsets,
) { ) {
final object = User( final object = User(
email: reader.readString(offsets[0]), avatarColor:
id: reader.readString(offsets[1]), _UseravatarColorValueEnumMap[reader.readStringOrNull(offsets[0])] ??
inTimeline: reader.readBoolOrNull(offsets[2]) ?? false, AvatarColorEnum.primary,
isAdmin: reader.readBool(offsets[3]), email: reader.readString(offsets[1]),
isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false, id: reader.readString(offsets[2]),
isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false, inTimeline: reader.readBoolOrNull(offsets[3]) ?? false,
memoryEnabled: reader.readBoolOrNull(offsets[6]) ?? true, isAdmin: reader.readBool(offsets[4]),
name: reader.readString(offsets[7]), isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false,
profileImagePath: reader.readStringOrNull(offsets[8]) ?? '', isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false,
updatedAt: reader.readDateTime(offsets[9]), memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true,
name: reader.readString(offsets[8]),
profileImagePath: reader.readStringOrNull(offsets[9]) ?? '',
updatedAt: reader.readDateTime(offsets[10]),
); );
return object; return object;
} }
@ -171,30 +182,58 @@ P _userDeserializeProp<P>(
) { ) {
switch (propertyId) { switch (propertyId) {
case 0: case 0:
return (reader.readString(offset)) as P; return (_UseravatarColorValueEnumMap[reader.readStringOrNull(offset)] ??
AvatarColorEnum.primary) as P;
case 1: case 1:
return (reader.readString(offset)) as P; return (reader.readString(offset)) as P;
case 2: case 2:
return (reader.readBoolOrNull(offset) ?? false) as P; return (reader.readString(offset)) as P;
case 3: case 3:
return (reader.readBool(offset)) as P;
case 4:
return (reader.readBoolOrNull(offset) ?? false) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 4:
return (reader.readBool(offset)) as P;
case 5: case 5:
return (reader.readBoolOrNull(offset) ?? false) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 6: case 6:
return (reader.readBoolOrNull(offset) ?? true) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 7: case 7:
return (reader.readString(offset)) as P; return (reader.readBoolOrNull(offset) ?? true) as P;
case 8: case 8:
return (reader.readStringOrNull(offset) ?? '') as P; return (reader.readString(offset)) as P;
case 9: case 9:
return (reader.readStringOrNull(offset) ?? '') as P;
case 10:
return (reader.readDateTime(offset)) as P; return (reader.readDateTime(offset)) as P;
default: default:
throw IsarError('Unknown property with id $propertyId'); throw IsarError('Unknown property with id $propertyId');
} }
} }
const _UseravatarColorEnumValueMap = {
r'primary': r'primary',
r'pink': r'pink',
r'red': r'red',
r'yellow': r'yellow',
r'blue': r'blue',
r'green': r'green',
r'purple': r'purple',
r'orange': r'orange',
r'gray': r'gray',
r'amber': r'amber',
};
const _UseravatarColorValueEnumMap = {
r'primary': AvatarColorEnum.primary,
r'pink': AvatarColorEnum.pink,
r'red': AvatarColorEnum.red,
r'yellow': AvatarColorEnum.yellow,
r'blue': AvatarColorEnum.blue,
r'green': AvatarColorEnum.green,
r'purple': AvatarColorEnum.purple,
r'orange': AvatarColorEnum.orange,
r'gray': AvatarColorEnum.gray,
r'amber': AvatarColorEnum.amber,
};
Id _userGetId(User object) { Id _userGetId(User object) {
return object.isarId; return object.isarId;
} }
@ -382,6 +421,136 @@ extension UserQueryWhere on QueryBuilder<User, User, QWhereClause> {
} }
extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> { extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
QueryBuilder<User, User, QAfterFilterCondition> avatarColorEqualTo(
AvatarColorEnum value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorGreaterThan(
AvatarColorEnum value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorLessThan(
AvatarColorEnum value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorBetween(
AvatarColorEnum lower,
AvatarColorEnum upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'avatarColor',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'avatarColor',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'avatarColor',
value: '',
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'avatarColor',
value: '',
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> emailEqualTo( QueryBuilder<User, User, QAfterFilterCondition> emailEqualTo(
String value, { String value, {
bool caseSensitive = true, bool caseSensitive = true,
@ -1167,6 +1336,18 @@ extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {
} }
extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> { extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
QueryBuilder<User, User, QAfterSortBy> sortByAvatarColor() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'avatarColor', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByAvatarColorDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'avatarColor', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByEmail() { QueryBuilder<User, User, QAfterSortBy> sortByEmail() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'email', Sort.asc); return query.addSortBy(r'email', Sort.asc);
@ -1289,6 +1470,18 @@ extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
} }
extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> { extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
QueryBuilder<User, User, QAfterSortBy> thenByAvatarColor() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'avatarColor', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByAvatarColorDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'avatarColor', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByEmail() { QueryBuilder<User, User, QAfterSortBy> thenByEmail() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'email', Sort.asc); return query.addSortBy(r'email', Sort.asc);
@ -1423,6 +1616,13 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
} }
extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> { extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
QueryBuilder<User, User, QDistinct> distinctByAvatarColor(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'avatarColor', caseSensitive: caseSensitive);
});
}
QueryBuilder<User, User, QDistinct> distinctByEmail( QueryBuilder<User, User, QDistinct> distinctByEmail(
{bool caseSensitive = true}) { {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
@ -1496,6 +1696,12 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
}); });
} }
QueryBuilder<User, AvatarColorEnum, QQueryOperations> avatarColorProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'avatarColor');
});
}
QueryBuilder<User, String, QQueryOperations> emailProperty() { QueryBuilder<User, String, QQueryOperations> emailProperty() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'email'); return query.addPropertyName(r'email');

View file

@ -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 {

View file

@ -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,

View file

@ -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,15 @@ 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, 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),

View file

@ -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

View file

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

View file

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

View file

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

View file

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

14
mobile/openapi/doc/UserAvatarColor.md generated Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ part of openapi.api;
class PartnerResponseDto { class PartnerResponseDto {
/// Returns a new [PartnerResponseDto] instance. /// Returns a new [PartnerResponseDto] instance.
PartnerResponseDto({ PartnerResponseDto({
required this.avatarColor,
required this.createdAt, required this.createdAt,
required this.deletedAt, required this.deletedAt,
required this.email, required this.email,
@ -29,6 +30,8 @@ class PartnerResponseDto {
required this.updatedAt, required this.updatedAt,
}); });
UserAvatarColor avatarColor;
DateTime createdAt; DateTime createdAt;
DateTime? deletedAt; DateTime? deletedAt;
@ -71,6 +74,7 @@ class PartnerResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is PartnerResponseDto && bool operator ==(Object other) => identical(this, other) || other is PartnerResponseDto &&
other.avatarColor == avatarColor &&
other.createdAt == createdAt && other.createdAt == createdAt &&
other.deletedAt == deletedAt && other.deletedAt == deletedAt &&
other.email == email && other.email == email &&
@ -89,6 +93,7 @@ class PartnerResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor.hashCode) +
(createdAt.hashCode) + (createdAt.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) + (deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) + (email.hashCode) +
@ -105,10 +110,11 @@ class PartnerResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'PartnerResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.deletedAt != null) { if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
@ -154,6 +160,7 @@ class PartnerResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return PartnerResponseDto( return PartnerResponseDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
createdAt: mapDateTime(json, r'createdAt', '')!, createdAt: mapDateTime(json, r'createdAt', '')!,
deletedAt: mapDateTime(json, r'deletedAt', ''), deletedAt: mapDateTime(json, r'deletedAt', ''),
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
@ -215,6 +222,7 @@ class PartnerResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'avatarColor',
'createdAt', 'createdAt',
'deletedAt', 'deletedAt',
'email', 'email',

View file

@ -13,6 +13,7 @@ part of openapi.api;
class UpdateUserDto { class UpdateUserDto {
/// Returns a new [UpdateUserDto] instance. /// Returns a new [UpdateUserDto] instance.
UpdateUserDto({ UpdateUserDto({
this.avatarColor,
this.email, this.email,
this.externalPath, this.externalPath,
required this.id, required this.id,
@ -24,6 +25,14 @@ class UpdateUserDto {
this.storageLabel, this.storageLabel,
}); });
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
UserAvatarColor? avatarColor;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@ -92,6 +101,7 @@ class UpdateUserDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto && bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.externalPath == externalPath && other.externalPath == externalPath &&
other.id == id && other.id == id &&
@ -105,6 +115,7 @@ class UpdateUserDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) + (email == null ? 0 : email!.hashCode) +
(externalPath == null ? 0 : externalPath!.hashCode) + (externalPath == null ? 0 : externalPath!.hashCode) +
(id.hashCode) + (id.hashCode) +
@ -116,10 +127,15 @@ class UpdateUserDto {
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @override
String toString() => 'UpdateUserDto[email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.email != null) { if (this.email != null) {
json[r'email'] = this.email; json[r'email'] = this.email;
} else { } else {
@ -172,6 +188,7 @@ class UpdateUserDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UpdateUserDto( return UpdateUserDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'), email: mapValueOfType<String>(json, r'email'),
externalPath: mapValueOfType<String>(json, r'externalPath'), externalPath: mapValueOfType<String>(json, r'externalPath'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,

View file

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

View file

@ -13,12 +13,15 @@ part of openapi.api;
class UserDto { class UserDto {
/// Returns a new [UserDto] instance. /// Returns a new [UserDto] instance.
UserDto({ UserDto({
required this.avatarColor,
required this.email, required this.email,
required this.id, required this.id,
required this.name, required this.name,
required this.profileImagePath, required this.profileImagePath,
}); });
UserAvatarColor avatarColor;
String email; String email;
String id; String id;
@ -29,6 +32,7 @@ class UserDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserDto && bool operator ==(Object other) => identical(this, other) || other is UserDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.id == id && other.id == id &&
other.name == name && other.name == name &&
@ -37,16 +41,18 @@ class UserDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor.hashCode) +
(email.hashCode) + (email.hashCode) +
(id.hashCode) + (id.hashCode) +
(name.hashCode) + (name.hashCode) +
(profileImagePath.hashCode); (profileImagePath.hashCode);
@override @override
String toString() => 'UserDto[email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]'; String toString() => 'UserDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor;
json[r'email'] = this.email; json[r'email'] = this.email;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'name'] = this.name; json[r'name'] = this.name;
@ -62,6 +68,7 @@ class UserDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserDto( return UserDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
@ -113,6 +120,7 @@ class UserDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'avatarColor',
'email', 'email',
'id', 'id',
'name', 'name',

View file

@ -13,6 +13,7 @@ part of openapi.api;
class UserResponseDto { class UserResponseDto {
/// Returns a new [UserResponseDto] instance. /// Returns a new [UserResponseDto] instance.
UserResponseDto({ UserResponseDto({
required this.avatarColor,
required this.createdAt, required this.createdAt,
required this.deletedAt, required this.deletedAt,
required this.email, required this.email,
@ -28,6 +29,8 @@ class UserResponseDto {
required this.updatedAt, required this.updatedAt,
}); });
UserAvatarColor avatarColor;
DateTime createdAt; DateTime createdAt;
DateTime? deletedAt; DateTime? deletedAt;
@ -62,6 +65,7 @@ class UserResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserResponseDto && bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
other.avatarColor == avatarColor &&
other.createdAt == createdAt && other.createdAt == createdAt &&
other.deletedAt == deletedAt && other.deletedAt == deletedAt &&
other.email == email && other.email == email &&
@ -79,6 +83,7 @@ class UserResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor.hashCode) +
(createdAt.hashCode) + (createdAt.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) + (deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) + (email.hashCode) +
@ -94,10 +99,11 @@ class UserResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'UserResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.deletedAt != null) { if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
@ -138,6 +144,7 @@ class UserResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserResponseDto( return UserResponseDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
createdAt: mapDateTime(json, r'createdAt', '')!, createdAt: mapDateTime(json, r'createdAt', '')!,
deletedAt: mapDateTime(json, r'deletedAt', ''), deletedAt: mapDateTime(json, r'deletedAt', ''),
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
@ -198,6 +205,7 @@ class UserResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'avatarColor',
'createdAt', 'createdAt',
'deletedAt', 'deletedAt',
'email', 'email',

View file

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

View file

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

View file

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

View file

@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for UserAvatarColor
void main() {
group('test UserAvatarColor', () {
});
}

View file

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

View file

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

View file

@ -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,10 @@
}, },
"PartnerResponseDto": { "PartnerResponseDto": {
"properties": { "properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor",
"nullable": true
},
"createdAt": { "createdAt": {
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
@ -7682,6 +7709,7 @@
} }
}, },
"required": [ "required": [
"avatarColor",
"id", "id",
"name", "name",
"email", "email",
@ -9140,6 +9168,9 @@
}, },
"UpdateUserDto": { "UpdateUserDto": {
"properties": { "properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor"
},
"email": { "email": {
"type": "string" "type": "string"
}, },
@ -9202,8 +9233,27 @@
], ],
"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",
"nullable": true
},
"email": { "email": {
"type": "string" "type": "string"
}, },
@ -9218,6 +9268,7 @@
} }
}, },
"required": [ "required": [
"avatarColor",
"id", "id",
"name", "name",
"email", "email",
@ -9227,6 +9278,10 @@
}, },
"UserResponseDto": { "UserResponseDto": {
"properties": { "properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor",
"nullable": true
},
"createdAt": { "createdAt": {
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
@ -9274,6 +9329,7 @@
} }
}, },
"required": [ "required": [
"avatarColor",
"id", "id",
"name", "name",
"email", "email",

View file

@ -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',

View file

@ -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,
}, },
}; };

View file

@ -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;
} }

View file

@ -1,10 +1,28 @@
import { UserEntity } from '@app/infra/entities'; import { UserAvatarColor, UserEntity } from '@app/infra/entities';
import { Optional } from '@nestjs/common';
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)
@Optional()
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor!: UserAvatarColor | null;
} }
export class UserResponseDto extends UserDto { export class UserResponseDto extends UserDto {
@ -25,6 +43,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),
}; };
}; };

View file

@ -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,

View file

@ -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 do nothing if the user has no profile image', async () => {
userMock.get.mockResolvedValue(userStub.admin);
await sut.deleteProfileImage(userStub.admin);
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', () => {

View file

@ -93,10 +93,25 @@ 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 !== '') {
const files = [oldpath];
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
}
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 !== '') {
await this.userRepository.update(authUser.id, { profileImagePath: '' });
const files = [user.profileImagePath];
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
}
return;
}
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) {

View file

@ -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> {

View file

@ -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;

View 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"`);
}
}

View file

@ -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',

View file

@ -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,
}), }),
}; };

View file

@ -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.

View file

@ -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>

View file

@ -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">

View file

@ -310,7 +310,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">

View file

@ -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}

View file

@ -0,0 +1,41 @@
<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"
>
<div class="flex px-2 pt-2 items-center">
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">
Select avatar color
</h1>
<div>
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
</div>
</div>
<div class="flex items-center justify-center p-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
{#each colors as color}
<div>
<button on:click={() => dispatch('choose', color)}>
<UserAvatar {user} {color} size="xl" showProfileImage={false} />
</button>
</div>
{/each}
</div>
</div>
</div>
</div>
</FullScreenModal>

View file

@ -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>

View file

@ -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> = {
@ -38,17 +43,11 @@
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-20 h-20',
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>

View file

@ -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">

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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,
}); });