diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index cc88438a..513b19da 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -785,7 +785,7 @@ "@settingsCacheDescription": {}, "settingsCacheDeletedSnackbar": "Zwischenspeicher erfolgreich gelöscht", "@settingsCacheDeletedSnackbar": {}, - "lb": "Pfund", + "lb": "lb", "@lb": { "description": "Generated entry for translation for server strings" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8618567a..eb55d59e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -167,6 +167,7 @@ "@rirNotUsed": { "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" }, + "useMetric": "Use metric units for body weight", "weightUnit": "Weight unit", "@weightUnit": {}, "repetitionUnit": "Repetition unit", diff --git a/lib/models/user/profile.dart b/lib/models/user/profile.dart index 5bb5335b..ed7cb174 100644 --- a/lib/models/user/profile.dart +++ b/lib/models/user/profile.dart @@ -31,6 +31,11 @@ class Profile { @JsonKey(required: true, name: 'is_trustworthy') bool isTrustworthy; + @JsonKey(required: true, name: 'weight_unit') + String weightUnitStr; + + bool get isMetric => weightUnitStr == 'kg'; + @JsonKey(required: true) String email; @@ -39,9 +44,11 @@ class Profile { required this.emailVerified, required this.isTrustworthy, required this.email, + required this.weightUnitStr, }); // Boilerplate factory Profile.fromJson(Map json) => _$ProfileFromJson(json); + Map toJson() => _$ProfileToJson(this); } diff --git a/lib/models/user/profile.g.dart b/lib/models/user/profile.g.dart index fc05b9cf..1bc975c2 100644 --- a/lib/models/user/profile.g.dart +++ b/lib/models/user/profile.g.dart @@ -9,13 +9,14 @@ part of 'profile.dart'; Profile _$ProfileFromJson(Map json) { $checkKeys( json, - requiredKeys: const ['username', 'email_verified', 'is_trustworthy', 'email'], + requiredKeys: const ['username', 'email_verified', 'is_trustworthy', 'weight_unit', 'email'], ); return Profile( username: json['username'] as String, emailVerified: json['email_verified'] as bool, isTrustworthy: json['is_trustworthy'] as bool, email: json['email'] as String, + weightUnitStr: json['weight_unit'] as String, ); } @@ -23,5 +24,6 @@ Map _$ProfileToJson(Profile instance) => { 'username': instance.username, 'email_verified': instance.emailVerified, 'is_trustworthy': instance.isTrustworthy, + 'weight_unit': instance.weightUnitStr, 'email': instance.email, }; diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 6b265658..d3acd084 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -27,6 +27,7 @@ import 'package:wger/models/workouts/workout_plan.dart'; import 'package:wger/providers/body_weight.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/providers/nutrition.dart'; +import 'package:wger/providers/user.dart'; import 'package:wger/providers/workout_plans.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/screens/gym_mode.dart'; @@ -243,6 +244,7 @@ class _DashboardWeightWidgetState extends State { @override Widget build(BuildContext context) { + final profile = context.read().profile; weightEntriesData = Provider.of(context, listen: false); return Consumer( @@ -267,9 +269,13 @@ class _DashboardWeightWidgetState extends State { children: [ SizedBox( height: 200, - child: MeasurementChartWidgetFl(weightEntriesData.items - .map((e) => MeasurementChartEntry(e.weight, e.date)) - .toList()), + child: MeasurementChartWidgetFl( + weightEntriesData.items + .map((e) => MeasurementChartEntry(e.weight, e.date)) + .toList(), + unit: profile!.isMetric + ? AppLocalizations.of(context).kg + : AppLocalizations.of(context).lb), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/widgets/user/forms.dart b/lib/widgets/user/forms.dart index 87907f8e..5f4a58cd 100644 --- a/lib/widgets/user/forms.dart +++ b/lib/widgets/user/forms.dart @@ -23,14 +23,26 @@ import 'package:wger/models/user/profile.dart'; import 'package:wger/providers/user.dart'; import 'package:wger/theme/theme.dart'; -class UserProfileForm extends StatelessWidget { +class UserProfileForm extends StatefulWidget { late final Profile _profile; - final _form = GlobalKey(); - final emailController = TextEditingController(); UserProfileForm(Profile profile) { _profile = profile; - emailController.text = _profile.email; + } + + @override + State createState() => _UserProfileFormState(); +} + +class _UserProfileFormState extends State { + final _form = GlobalKey(); + + final emailController = TextEditingController(); + + @override + void initState() { + super.initState(); + emailController.text = widget._profile.email; } @override @@ -42,16 +54,29 @@ class UserProfileForm extends StatelessWidget { ListTile( leading: const Icon(Icons.person, color: wgerPrimaryColor), title: Text(AppLocalizations.of(context).username), - subtitle: Text(_profile.username), + subtitle: Text(widget._profile.username), + ), + SwitchListTile( + title: Text(AppLocalizations.of(context).useMetric), + subtitle: Text(widget._profile.weightUnitStr), + value: widget._profile.isMetric, + onChanged: (_) { + setState(() { + widget._profile.weightUnitStr = widget._profile.isMetric + ? AppLocalizations.of(context).lb + : AppLocalizations.of(context).kg; + }); + }, + dense: true, ), ListTile( leading: const Icon(Icons.email_rounded, color: wgerPrimaryColor), title: TextFormField( decoration: InputDecoration( - labelText: _profile.emailVerified + labelText: widget._profile.emailVerified ? AppLocalizations.of(context).verifiedEmail : AppLocalizations.of(context).unVerifiedEmail, - suffixIcon: _profile.emailVerified + suffixIcon: widget._profile.emailVerified ? const Icon( Icons.check_circle, color: Colors.green, @@ -60,7 +85,7 @@ class UserProfileForm extends StatelessWidget { controller: emailController, keyboardType: TextInputType.emailAddress, onSaved: (newValue) { - _profile.email = newValue!; + widget._profile.email = newValue!; }, validator: (value) { if (value!.isNotEmpty && !value.contains('@')) { @@ -70,11 +95,11 @@ class UserProfileForm extends StatelessWidget { }, ), ), - if (!_profile.emailVerified) + if (!widget._profile.emailVerified) OutlinedButton( onPressed: () async { // Email is already verified - if (_profile.emailVerified) { + if (widget._profile.emailVerified) { return; } @@ -83,7 +108,7 @@ class UserProfileForm extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - AppLocalizations.of(context).verifiedEmailInfo(_profile.email), + AppLocalizations.of(context).verifiedEmailInfo(widget._profile.email), ), ), ); @@ -91,9 +116,6 @@ class UserProfileForm extends StatelessWidget { child: Text(AppLocalizations.of(context).verify), ), ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: wgerPrimaryButtonColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50))), onPressed: () async { // Validate and save the current values to the weightEntry final isValid = _form.currentState!.validate(); diff --git a/lib/widgets/weight/entries_list.dart b/lib/widgets/weight/entries_list.dart index 8377ec5b..bf42303b 100644 --- a/lib/widgets/weight/entries_list.dart +++ b/lib/widgets/weight/entries_list.dart @@ -21,6 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:wger/providers/body_weight.dart'; +import 'package:wger/providers/user.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/screens/measurement_categories_screen.dart'; import 'package:wger/widgets/measurements/charts.dart'; @@ -29,6 +30,7 @@ import 'package:wger/widgets/weight/forms.dart'; class WeightEntriesList extends StatelessWidget { @override Widget build(BuildContext context) { + final profile = context.read().profile; final weightProvider = Provider.of(context, listen: false); return Column( @@ -37,7 +39,11 @@ class WeightEntriesList extends StatelessWidget { padding: const EdgeInsets.all(15), height: 220, child: MeasurementChartWidgetFl( - weightProvider.items.map((e) => MeasurementChartEntry(e.weight, e.date)).toList()), + weightProvider.items.map((e) => MeasurementChartEntry(e.weight, e.date)).toList(), + unit: profile!.isMetric + ? AppLocalizations.of(context).kg + : AppLocalizations.of(context).lb, + ), ), TextButton( onPressed: () => Navigator.pushNamed( diff --git a/test/weight/weight_screen_test.dart b/test/weight/weight_screen_test.dart index 10bd75bd..575867d3 100644 --- a/test/weight/weight_screen_test.dart +++ b/test/weight/weight_screen_test.dart @@ -23,32 +23,43 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; import 'package:wger/providers/body_weight.dart'; +import 'package:wger/providers/user.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/screens/weight_screen.dart'; import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/weight/forms.dart'; import '../../test_data/body_weight.dart'; +import '../../test_data/profile.dart'; import 'weight_screen_test.mocks.dart'; -@GenerateMocks([BodyWeightProvider]) +@GenerateMocks([BodyWeightProvider, UserProvider]) void main() { - var mockWeightProvider = MockBodyWeightProvider(); + late MockBodyWeightProvider mockWeightProvider; + late MockUserProvider mockUserProvider; - Widget createWeightScreen({locale = 'en'}) { + setUp(() { mockWeightProvider = MockBodyWeightProvider(); when(mockWeightProvider.items).thenReturn(getWeightEntries()); - return ChangeNotifierProvider( - create: (context) => mockWeightProvider, - child: MaterialApp( - locale: Locale(locale), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: WeightScreen(), - routes: { - FormScreen.routeName: (_) => FormScreen(), - }, + mockUserProvider = MockUserProvider(); + when(mockUserProvider.profile).thenReturn(tProfile1); + }); + + Widget createWeightScreen({locale = 'en'}) { + return ChangeNotifierProvider( + create: (context) => mockUserProvider, + child: ChangeNotifierProvider( + create: (context) => mockWeightProvider, + child: MaterialApp( + locale: Locale(locale), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: WeightScreen(), + routes: { + FormScreen.routeName: (_) => FormScreen(), + }, + ), ), ); } diff --git a/test/weight/weight_screen_test.mocks.dart b/test/weight/weight_screen_test.mocks.dart index 530a9aeb..e42d7e68 100644 --- a/test/weight/weight_screen_test.mocks.dart +++ b/test/weight/weight_screen_test.mocks.dart @@ -8,8 +8,10 @@ import 'dart:ui' as _i6; import 'package:mockito/mockito.dart' as _i1; import 'package:wger/models/body_weight/weight_entry.dart' as _i3; +import 'package:wger/models/user/profile.dart' as _i8; import 'package:wger/providers/base_provider.dart' as _i2; import 'package:wger/providers/body_weight.dart' as _i4; +import 'package:wger/providers/user.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -192,3 +194,111 @@ class MockBodyWeightProvider extends _i1.Mock implements _i4.BodyWeightProvider returnValueForMissingStub: null, ); } + +/// A class which mocks [UserProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUserProvider extends _i1.Mock implements _i7.UserProvider { + MockUserProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get baseProvider => (super.noSuchMethod( + Invocation.getter(#baseProvider), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#baseProvider), + ), + ) as _i2.WgerBaseProvider); + + @override + set profile(_i8.Profile? _profile) => super.noSuchMethod( + Invocation.setter( + #profile, + _profile, + ), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future fetchAndSetProfile() => (super.noSuchMethod( + Invocation.method( + #fetchAndSetProfile, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future saveProfile() => (super.noSuchMethod( + Invocation.method( + #saveProfile, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future verifyEmail() => (super.noSuchMethod( + Invocation.method( + #verifyEmail, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + void addListener(_i6.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i6.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/test_data/profile.dart b/test_data/profile.dart index f908c858..f7e053f9 100644 --- a/test_data/profile.dart +++ b/test_data/profile.dart @@ -5,4 +5,5 @@ final tProfile1 = Profile( emailVerified: true, isTrustworthy: true, email: 'admin@google.com', + weightUnitStr: 'kg', );