Skip to content

Commit e733bc4

Browse files
authored
Merge pull request #498 from wger-project/feature/body-weight-unit
Use the user's preferred unit for body weight
2 parents d478660 + d4e4f33 commit e733bc4

File tree

10 files changed

+199
-33
lines changed

10 files changed

+199
-33
lines changed

lib/l10n/app_de.arb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@
785785
"@settingsCacheDescription": {},
786786
"settingsCacheDeletedSnackbar": "Zwischenspeicher erfolgreich gelöscht",
787787
"@settingsCacheDeletedSnackbar": {},
788-
"lb": "Pfund",
788+
"lb": "lb",
789789
"@lb": {
790790
"description": "Generated entry for translation for server strings"
791791
}

lib/l10n/app_en.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
"@rirNotUsed": {
168168
"description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log"
169169
},
170+
"useMetric": "Use metric units for body weight",
170171
"weightUnit": "Weight unit",
171172
"@weightUnit": {},
172173
"repetitionUnit": "Repetition unit",

lib/models/user/profile.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ class Profile {
3131
@JsonKey(required: true, name: 'is_trustworthy')
3232
bool isTrustworthy;
3333

34+
@JsonKey(required: true, name: 'weight_unit')
35+
String weightUnitStr;
36+
37+
bool get isMetric => weightUnitStr == 'kg';
38+
3439
@JsonKey(required: true)
3540
String email;
3641

@@ -39,9 +44,11 @@ class Profile {
3944
required this.emailVerified,
4045
required this.isTrustworthy,
4146
required this.email,
47+
required this.weightUnitStr,
4248
});
4349

4450
// Boilerplate
4551
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
52+
4653
Map<String, dynamic> toJson() => _$ProfileToJson(this);
4754
}

lib/models/user/profile.g.dart

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/widgets/dashboard/widgets.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'package:wger/models/workouts/workout_plan.dart';
2727
import 'package:wger/providers/body_weight.dart';
2828
import 'package:wger/providers/measurement.dart';
2929
import 'package:wger/providers/nutrition.dart';
30+
import 'package:wger/providers/user.dart';
3031
import 'package:wger/providers/workout_plans.dart';
3132
import 'package:wger/screens/form_screen.dart';
3233
import 'package:wger/screens/gym_mode.dart';
@@ -243,6 +244,7 @@ class _DashboardWeightWidgetState extends State<DashboardWeightWidget> {
243244

244245
@override
245246
Widget build(BuildContext context) {
247+
final profile = context.read<UserProvider>().profile;
246248
weightEntriesData = Provider.of<BodyWeightProvider>(context, listen: false);
247249

248250
return Consumer<BodyWeightProvider>(
@@ -267,9 +269,13 @@ class _DashboardWeightWidgetState extends State<DashboardWeightWidget> {
267269
children: [
268270
SizedBox(
269271
height: 200,
270-
child: MeasurementChartWidgetFl(weightEntriesData.items
271-
.map((e) => MeasurementChartEntry(e.weight, e.date))
272-
.toList()),
272+
child: MeasurementChartWidgetFl(
273+
weightEntriesData.items
274+
.map((e) => MeasurementChartEntry(e.weight, e.date))
275+
.toList(),
276+
unit: profile!.isMetric
277+
? AppLocalizations.of(context).kg
278+
: AppLocalizations.of(context).lb),
273279
),
274280
Row(
275281
mainAxisAlignment: MainAxisAlignment.spaceBetween,

lib/widgets/user/forms.dart

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,26 @@ import 'package:wger/models/user/profile.dart';
2323
import 'package:wger/providers/user.dart';
2424
import 'package:wger/theme/theme.dart';
2525

26-
class UserProfileForm extends StatelessWidget {
26+
class UserProfileForm extends StatefulWidget {
2727
late final Profile _profile;
28-
final _form = GlobalKey<FormState>();
29-
final emailController = TextEditingController();
3028

3129
UserProfileForm(Profile profile) {
3230
_profile = profile;
33-
emailController.text = _profile.email;
31+
}
32+
33+
@override
34+
State<UserProfileForm> createState() => _UserProfileFormState();
35+
}
36+
37+
class _UserProfileFormState extends State<UserProfileForm> {
38+
final _form = GlobalKey<FormState>();
39+
40+
final emailController = TextEditingController();
41+
42+
@override
43+
void initState() {
44+
super.initState();
45+
emailController.text = widget._profile.email;
3446
}
3547

3648
@override
@@ -42,16 +54,29 @@ class UserProfileForm extends StatelessWidget {
4254
ListTile(
4355
leading: const Icon(Icons.person, color: wgerPrimaryColor),
4456
title: Text(AppLocalizations.of(context).username),
45-
subtitle: Text(_profile.username),
57+
subtitle: Text(widget._profile.username),
58+
),
59+
SwitchListTile(
60+
title: Text(AppLocalizations.of(context).useMetric),
61+
subtitle: Text(widget._profile.weightUnitStr),
62+
value: widget._profile.isMetric,
63+
onChanged: (_) {
64+
setState(() {
65+
widget._profile.weightUnitStr = widget._profile.isMetric
66+
? AppLocalizations.of(context).lb
67+
: AppLocalizations.of(context).kg;
68+
});
69+
},
70+
dense: true,
4671
),
4772
ListTile(
4873
leading: const Icon(Icons.email_rounded, color: wgerPrimaryColor),
4974
title: TextFormField(
5075
decoration: InputDecoration(
51-
labelText: _profile.emailVerified
76+
labelText: widget._profile.emailVerified
5277
? AppLocalizations.of(context).verifiedEmail
5378
: AppLocalizations.of(context).unVerifiedEmail,
54-
suffixIcon: _profile.emailVerified
79+
suffixIcon: widget._profile.emailVerified
5580
? const Icon(
5681
Icons.check_circle,
5782
color: Colors.green,
@@ -60,7 +85,7 @@ class UserProfileForm extends StatelessWidget {
6085
controller: emailController,
6186
keyboardType: TextInputType.emailAddress,
6287
onSaved: (newValue) {
63-
_profile.email = newValue!;
88+
widget._profile.email = newValue!;
6489
},
6590
validator: (value) {
6691
if (value!.isNotEmpty && !value.contains('@')) {
@@ -70,11 +95,11 @@ class UserProfileForm extends StatelessWidget {
7095
},
7196
),
7297
),
73-
if (!_profile.emailVerified)
98+
if (!widget._profile.emailVerified)
7499
OutlinedButton(
75100
onPressed: () async {
76101
// Email is already verified
77-
if (_profile.emailVerified) {
102+
if (widget._profile.emailVerified) {
78103
return;
79104
}
80105

@@ -83,17 +108,14 @@ class UserProfileForm extends StatelessWidget {
83108
ScaffoldMessenger.of(context).showSnackBar(
84109
SnackBar(
85110
content: Text(
86-
AppLocalizations.of(context).verifiedEmailInfo(_profile.email),
111+
AppLocalizations.of(context).verifiedEmailInfo(widget._profile.email),
87112
),
88113
),
89114
);
90115
},
91116
child: Text(AppLocalizations.of(context).verify),
92117
),
93118
ElevatedButton(
94-
style: ElevatedButton.styleFrom(
95-
backgroundColor: wgerPrimaryButtonColor,
96-
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50))),
97119
onPressed: () async {
98120
// Validate and save the current values to the weightEntry
99121
final isValid = _form.currentState!.validate();

lib/widgets/weight/entries_list.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2121
import 'package:intl/intl.dart';
2222
import 'package:provider/provider.dart';
2323
import 'package:wger/providers/body_weight.dart';
24+
import 'package:wger/providers/user.dart';
2425
import 'package:wger/screens/form_screen.dart';
2526
import 'package:wger/screens/measurement_categories_screen.dart';
2627
import 'package:wger/widgets/measurements/charts.dart';
@@ -29,6 +30,7 @@ import 'package:wger/widgets/weight/forms.dart';
2930
class WeightEntriesList extends StatelessWidget {
3031
@override
3132
Widget build(BuildContext context) {
33+
final profile = context.read<UserProvider>().profile;
3234
final weightProvider = Provider.of<BodyWeightProvider>(context, listen: false);
3335

3436
return Column(
@@ -37,7 +39,11 @@ class WeightEntriesList extends StatelessWidget {
3739
padding: const EdgeInsets.all(15),
3840
height: 220,
3941
child: MeasurementChartWidgetFl(
40-
weightProvider.items.map((e) => MeasurementChartEntry(e.weight, e.date)).toList()),
42+
weightProvider.items.map((e) => MeasurementChartEntry(e.weight, e.date)).toList(),
43+
unit: profile!.isMetric
44+
? AppLocalizations.of(context).kg
45+
: AppLocalizations.of(context).lb,
46+
),
4147
),
4248
TextButton(
4349
onPressed: () => Navigator.pushNamed(

test/weight/weight_screen_test.dart

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,43 @@ import 'package:mockito/annotations.dart';
2323
import 'package:mockito/mockito.dart';
2424
import 'package:provider/provider.dart';
2525
import 'package:wger/providers/body_weight.dart';
26+
import 'package:wger/providers/user.dart';
2627
import 'package:wger/screens/form_screen.dart';
2728
import 'package:wger/screens/weight_screen.dart';
2829
import 'package:wger/widgets/measurements/charts.dart';
2930
import 'package:wger/widgets/weight/forms.dart';
3031

3132
import '../../test_data/body_weight.dart';
33+
import '../../test_data/profile.dart';
3234
import 'weight_screen_test.mocks.dart';
3335

34-
@GenerateMocks([BodyWeightProvider])
36+
@GenerateMocks([BodyWeightProvider, UserProvider])
3537
void main() {
36-
var mockWeightProvider = MockBodyWeightProvider();
38+
late MockBodyWeightProvider mockWeightProvider;
39+
late MockUserProvider mockUserProvider;
3740

38-
Widget createWeightScreen({locale = 'en'}) {
41+
setUp(() {
3942
mockWeightProvider = MockBodyWeightProvider();
4043
when(mockWeightProvider.items).thenReturn(getWeightEntries());
4144

42-
return ChangeNotifierProvider<BodyWeightProvider>(
43-
create: (context) => mockWeightProvider,
44-
child: MaterialApp(
45-
locale: Locale(locale),
46-
localizationsDelegates: AppLocalizations.localizationsDelegates,
47-
supportedLocales: AppLocalizations.supportedLocales,
48-
home: WeightScreen(),
49-
routes: {
50-
FormScreen.routeName: (_) => FormScreen(),
51-
},
45+
mockUserProvider = MockUserProvider();
46+
when(mockUserProvider.profile).thenReturn(tProfile1);
47+
});
48+
49+
Widget createWeightScreen({locale = 'en'}) {
50+
return ChangeNotifierProvider<UserProvider>(
51+
create: (context) => mockUserProvider,
52+
child: ChangeNotifierProvider<BodyWeightProvider>(
53+
create: (context) => mockWeightProvider,
54+
child: MaterialApp(
55+
locale: Locale(locale),
56+
localizationsDelegates: AppLocalizations.localizationsDelegates,
57+
supportedLocales: AppLocalizations.supportedLocales,
58+
home: WeightScreen(),
59+
routes: {
60+
FormScreen.routeName: (_) => FormScreen(),
61+
},
62+
),
5263
),
5364
);
5465
}

0 commit comments

Comments
 (0)