Skip to content

Commit fb57a98

Browse files
authored
feat: Add user profile pictures (#510)
1 parent b0810de commit fb57a98

File tree

5 files changed

+135
-29
lines changed

5 files changed

+135
-29
lines changed

kitchenowl/lib/cubits/settings_user_cubit.dart

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import 'package:flutter/widgets.dart';
55
import 'package:flutter_bloc/flutter_bloc.dart';
66
import 'package:kitchenowl/cubits/auth_cubit.dart';
77
import 'package:kitchenowl/enums/update_enum.dart';
8+
import 'package:kitchenowl/helpers/named_bytearray.dart';
89
import 'package:kitchenowl/models/token.dart';
910
import 'package:kitchenowl/models/user.dart';
1011
import 'package:kitchenowl/services/api/api_service.dart';
1112

1213
class SettingsUserCubit extends Cubit<SettingsUserState> {
1314
final int? userId;
1415
SettingsUserCubit(this.userId, [User? initialUserData])
15-
: super(SettingsUserState(initialUserData, false, UpdateEnum.unchanged)) {
16+
: super(SettingsUserState(user: initialUserData)) {
1617
refresh();
1718
}
1819

@@ -26,27 +27,34 @@ class SettingsUserCubit extends Cubit<SettingsUserState> {
2627

2728
Future<void> updateUser({
2829
required BuildContext context,
29-
String? name,
3030
String? username,
3131
String? password,
3232
String? email,
3333
}) async {
3434
if (state.user == null) return;
35+
String? image;
36+
if (state.image != null) {
37+
image = state.image!.isEmpty
38+
? ''
39+
: await ApiService.getInstance().uploadBytes(state.image!);
40+
}
3541
bool res = false;
3642
res = userId != null
3743
? await ApiService.getInstance().updateUserById(
3844
userId!,
39-
name: name,
45+
name: state.name,
4046
password: password,
4147
email: email,
48+
image: image,
4249
admin: (state.setAdmin != state.user!.serverAdmin)
4350
? state.setAdmin
4451
: null,
4552
)
4653
: await ApiService.getInstance().updateUser(
47-
name: name,
54+
name: state.name,
4855
password: password,
4956
email: email,
57+
image: image,
5058
);
5159
if (res) {
5260
emit(state.copyWith(updateState: UpdateEnum.updated));
@@ -57,10 +65,18 @@ class SettingsUserCubit extends Cubit<SettingsUserState> {
5765
}
5866
}
5967

68+
void setName(String name) {
69+
emit(state.copyWith(name: name));
70+
}
71+
6072
void setAdmin(bool newAdmin) {
6173
emit(state.copyWith(setAdmin: newAdmin));
6274
}
6375

76+
void setImage(NamedByteArray image) {
77+
emit(state.copyWith(image: image));
78+
}
79+
6480
Future<String?> addLongLivedToken(String name) async {
6581
final token = await ApiService.getInstance().createLongLivedToken(name);
6682
if (token != null) refresh();
@@ -94,22 +110,38 @@ class SettingsUserCubit extends Cubit<SettingsUserState> {
94110

95111
class SettingsUserState extends Equatable {
96112
final User? user;
113+
114+
final String? name;
115+
final NamedByteArray? image;
97116
final bool setAdmin;
98117
final UpdateEnum updateState;
99118

100-
const SettingsUserState(this.user, this.setAdmin, this.updateState);
119+
const SettingsUserState({
120+
this.user,
121+
this.name,
122+
this.setAdmin = false,
123+
this.updateState = UpdateEnum.unchanged,
124+
this.image,
125+
});
101126

102127
@override
103-
List<Object?> get props => [user, setAdmin, updateState];
128+
List<Object?> get props => [user, name, setAdmin, updateState, image];
104129

105130
SettingsUserState copyWith({
106131
User? user,
132+
String? name,
107133
bool? setAdmin,
108134
UpdateEnum? updateState,
135+
NamedByteArray? image,
109136
}) =>
110137
SettingsUserState(
111-
user ?? this.user,
112-
setAdmin ?? this.setAdmin,
113-
updateState ?? this.updateState,
138+
user: user ?? this.user,
139+
name: name ?? this.name,
140+
setAdmin: setAdmin ?? this.setAdmin,
141+
updateState: updateState ?? this.updateState,
142+
image: image ?? this.image,
114143
);
144+
145+
bool hasChanges() =>
146+
name != null && (user == null || user!.name != name) || image != null;
115147
}

kitchenowl/lib/models/user.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class User extends Model {
5656
name,
5757
email,
5858
emailVerified,
59+
image,
5960
username,
6061
serverAdmin,
6162
tokens,

kitchenowl/lib/pages/settings_user_page.dart

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:kitchenowl/pages/settings_user_email_page.dart';
1111
import 'package:kitchenowl/pages/settings_user_linked_accounts_page.dart';
1212
import 'package:kitchenowl/pages/settings_user_password_page.dart';
1313
import 'package:kitchenowl/pages/settings_user_sessions_page.dart';
14+
import 'package:kitchenowl/widgets/user_image_selector.dart';
1415

1516
class SettingsUserPage extends StatefulWidget {
1617
final User? user;
@@ -131,21 +132,12 @@ class _SettingsUserPageState extends State<SettingsUserPage> {
131132
BlocBuilder<SettingsUserCubit, SettingsUserState>(
132133
bloc: cubit,
133134
builder: (context, state) => Center(
134-
child: CircleAvatar(
135-
foregroundImage: state.user?.image?.isEmpty ?? true
136-
? null
137-
: getImageProvider(
138-
context,
139-
state.user!.image!,
140-
),
141-
radius: 45,
142-
child: nameController.text.isNotEmpty
143-
? Text(
144-
nameController.text.substring(0, 1),
145-
textScaler: MediaQuery.textScalerOf(context)
146-
.clamp(minScaleFactor: 2),
147-
)
148-
: null,
135+
child: UserImageSelector(
136+
name: nameController.text,
137+
originalImage: state.user?.image,
138+
image: state.image,
139+
tooltip: AppLocalizations.of(context)!.imageSelect,
140+
setImage: cubit.setImage,
149141
),
150142
),
151143
),
@@ -190,6 +182,7 @@ class _SettingsUserPageState extends State<SettingsUserPage> {
190182
decoration: InputDecoration(
191183
labelText: AppLocalizations.of(context)!.name,
192184
),
185+
onChanged: cubit.setName,
193186
),
194187
if (cubit.userId == null && App.isDefaultServer) ...[
195188
const SizedBox(height: 8),
@@ -226,12 +219,17 @@ class _SettingsUserPageState extends State<SettingsUserPage> {
226219
const SizedBox(height: 8),
227220
Padding(
228221
padding: const EdgeInsets.only(bottom: 24),
229-
child: LoadingElevatedButton(
230-
onPressed: () => cubit.updateUser(
231-
context: context,
232-
name: nameController.text,
222+
child: BlocBuilder<SettingsUserCubit, SettingsUserState>(
223+
bloc: cubit,
224+
builder: (context, state) => LoadingElevatedButton(
225+
onPressed: state.hasChanges() &&
226+
(state.name?.isNotEmpty ?? true)
227+
? () => cubit.updateUser(
228+
context: context,
229+
)
230+
: null,
231+
child: Text(AppLocalizations.of(context)!.save),
233232
),
234-
child: Text(AppLocalizations.of(context)!.save),
235233
),
236234
),
237235
const Divider(),

kitchenowl/lib/services/api/user.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ extension UserApi on ApiService {
4343
}
4444

4545
Future<bool> updateUser({
46+
String? image,
4647
String? name,
4748
String? password,
4849
String? email,
@@ -53,6 +54,7 @@ extension UserApi on ApiService {
5354
if (name != null) 'name': name,
5455
if (password != null) 'password': password,
5556
if (email != null) 'email': email,
57+
if (image != null) 'photo': image,
5658
};
5759

5860
final res = await post(baseRoute, jsonEncode(body));
@@ -62,6 +64,7 @@ extension UserApi on ApiService {
6264

6365
Future<bool> updateUserById(
6466
int userId, {
67+
String? image,
6568
String? name,
6669
String? password,
6770
String? email,
@@ -74,6 +77,7 @@ extension UserApi on ApiService {
7477
if (password != null) 'password': password,
7578
if (email != null) 'email': email,
7679
if (admin != null) 'admin': admin,
80+
if (image != null) 'photo': image,
7781
};
7882

7983
final res = await post('$baseRoute/$userId', jsonEncode(body));
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:kitchenowl/helpers/named_bytearray.dart';
3+
import 'package:kitchenowl/kitchenowl.dart';
4+
5+
class UserImageSelector extends StatelessWidget {
6+
final NamedByteArray? image;
7+
final String? originalImage;
8+
final void Function(NamedByteArray) setImage;
9+
final String? tooltip;
10+
final String? name;
11+
12+
const UserImageSelector({
13+
super.key,
14+
this.image,
15+
this.originalImage,
16+
required this.setImage,
17+
this.tooltip,
18+
this.name,
19+
});
20+
21+
@override
22+
Widget build(BuildContext context) {
23+
return CircleAvatar(
24+
backgroundImage: hasDominantImage() ? getDominantImage(context)! : null,
25+
radius: 45,
26+
child: IconButton(
27+
icon: hasDominantImage()
28+
? const Icon(Icons.edit)
29+
: const Icon(Icons.add_photo_alternate_rounded),
30+
tooltip: tooltip ?? AppLocalizations.of(context)!.imageSelect,
31+
color: hasDominantImage()
32+
? Theme.of(context).colorScheme.secondary
33+
: Theme.of(context).colorScheme.onSecondary,
34+
onPressed: () async {
35+
NamedByteArray? file = await selectFile(
36+
context: context,
37+
title: tooltip ?? AppLocalizations.of(context)!.imageSelect,
38+
deleteOption: hasDominantImage(),
39+
);
40+
if (file != null) {
41+
setImage(file);
42+
}
43+
},
44+
),
45+
);
46+
}
47+
48+
bool hasDominantImage() {
49+
if (image != null && image!.isNotEmpty) {
50+
return true;
51+
} else if (image == null && (originalImage?.isNotEmpty ?? false)) {
52+
return true;
53+
} else {
54+
return false;
55+
}
56+
}
57+
58+
ImageProvider<Object>? getDominantImage(BuildContext context) {
59+
if (image != null && image!.isNotEmpty) {
60+
return MemoryImage(image!.bytes);
61+
} else if (originalImage?.isNotEmpty ?? false) {
62+
return getImageProvider(
63+
context,
64+
originalImage!,
65+
maxWidth: MediaQuery.of(context).size.width.toInt(),
66+
);
67+
} else {
68+
return null;
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)