Commit 527f442

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-09-24 16:38:47
Implement preferred weight unit (#449)
* Implement weight unit * Implement preferred weight unit setting * Display weight unit setting * Use preferred weight unit wherever applicable
1 parent 40d3e4c
app/lib/features/input/add_bodyweight_dialoge.dart
@@ -1,5 +1,7 @@
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:health_data_store/health_data_store.dart';
 
@@ -18,7 +20,7 @@ class AddBodyweightDialoge extends StatelessWidget {
         autofocus: true,
         decoration: InputDecoration(
           labelText: AppLocalizations.of(context)!.weight,
-          suffix: const Text('kg'),
+          suffix: Text(context.select((Settings s) => s.weightUnit).name),
         ),
         inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9,.]'))],
         keyboardType: const TextInputType.numberWithOptions(decimal: true),
@@ -32,9 +34,12 @@ class AddBodyweightDialoge extends StatelessWidget {
           }
           return null;
         },
-        onFieldSubmitted: (text) {
+        onFieldSubmitted: (String text) {
           final value = double.tryParse(text);
-          if (value != null) Navigator.of(context).pop(Weight.kg(value));
+          if (value != null) {
+            final weight = context.read<Settings>().weightUnit.store(value);
+            Navigator.of(context).pop(weight);
+          }
         },
       ),
     ),
app/lib/features/measurement_list/weight_list.dart
@@ -1,6 +1,7 @@
 import 'package:blood_pressure_app/components/confirm_deletion_dialoge.dart';
 import 'package:blood_pressure_app/data_util/repository_builder.dart';
 import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:blood_pressure_app/model/weight_unit.dart';
 import 'package:flutter/material.dart';
 import 'package:health_data_store/health_data_store.dart';
 import 'package:intl/intl.dart';
@@ -17,6 +18,7 @@ class WeightList extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final format = DateFormat(context.select<Settings, String>((s) => s.dateFormatString));
+    final weightUnit = context.select((Settings s) => s.weightUnit);
     return RepositoryBuilder<BodyweightRecord, BodyweightRepository>(
       rangeType: rangeType,
       onData: (context, records) {
@@ -24,7 +26,7 @@ class WeightList extends StatelessWidget {
         return ListView.builder(
           itemCount: records.length,
           itemBuilder: (context, idx) => ListTile(
-            title: Text(_buildWeightText(records[idx].weight)),
+            title: Text(_buildWeightText(weightUnit, records[idx].weight)),
             subtitle: Text(format.format(records[idx].time)),
             trailing: IconButton(
               icon: const Icon(Icons.delete),
@@ -41,12 +43,12 @@ class WeightList extends StatelessWidget {
     );
   }
 
-  String _buildWeightText(Weight w) {
-    String weightStr = w.kg.toStringAsFixed(2);
+  String _buildWeightText(WeightUnit u, Weight w) {
+    String weightStr = u.extract(w).toStringAsFixed(2);
     if (weightStr.endsWith('0')) weightStr = weightStr.substring(0, weightStr.length - 1);
     if (weightStr.endsWith('0')) weightStr = weightStr.substring(0, weightStr.length - 1);
     if (weightStr.endsWith('.')) weightStr = weightStr.substring(0, weightStr.length - 1);
-    // TODO: preferred weight unit
-    return '$weightStr kg';
+
+    return '$weightStr ${u.name}';
   }
 }
app/lib/l10n/app_en.arb
@@ -522,5 +522,9 @@
   "weight": "Weight",
   "@weight": {},
   "enterWeight": "Enter weight",
-  "@enterWeight": {}
+  "@enterWeight": {},
+  "preferredWeightUnit": "Preferred weight unit",
+  "@preferredWeightUnit": {
+    "description": "Setting for the unit the app will use for displaying weight"
+  }
 }
\ No newline at end of file
app/lib/model/blood_pressure/pressure_unit.dart
@@ -1,4 +1,3 @@
-
 import 'package:health_data_store/health_data_store.dart';
 
 /// A unit blood pressure can be in.
@@ -19,11 +18,7 @@ enum PressureUnit {
   static PressureUnit? decode(int? encoded) => switch(encoded) {
     0 => PressureUnit.mmHg,
     1 => PressureUnit.kPa,
-    null => null,
-    _ => (){
-      assert(false);
-      return null;
-    }(),
+    _ => null,
   };
 
   /// Converts a value to a [Pressure] of this [PressureUnit].
app/lib/model/storage/settings_store.dart
@@ -6,6 +6,7 @@ import 'package:blood_pressure_app/model/blood_pressure/medicine/medicine.dart';
 import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
 import 'package:blood_pressure_app/model/horizontal_graph_line.dart';
 import 'package:blood_pressure_app/model/storage/convert_util.dart';
+import 'package:blood_pressure_app/model/weight_unit.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 
@@ -50,6 +51,7 @@ class Settings extends ChangeNotifier {
     int? highestMedIndex,
     bool? bleInput,
     bool? weightInput,
+    WeightUnit? weightUnit,
   }) {
     if (accentColor != null) _accentColor = accentColor;
     if (sysColor != null) _sysColor = sysColor;
@@ -78,6 +80,7 @@ class Settings extends ChangeNotifier {
     if (knownBleDev != null) _knownBleDev = knownBleDev;
     if (bleInput != null) _bleInput = bleInput;
     if (weightInput != null) _weightInput = weightInput;
+    if (weightUnit != null) _weightUnit = weightUnit;
     _language = language; // No check here, as null is the default as well.
   }
 
@@ -113,6 +116,8 @@ class Settings extends ChangeNotifier {
       knownBleDev: ConvertUtil.parseList<String>(map['knownBleDev']),
       bleInput: ConvertUtil.parseBool(map['bleInput']),
       weightInput: ConvertUtil.parseBool(map['weightInput']),
+      preferredPressureUnit: PressureUnit.decode(ConvertUtil.parseInt(map['preferredPressureUnit'])),
+      weightUnit: WeightUnit.deserialize(ConvertUtil.parseInt(map['weightUnit'])),
     );
 
     // update
@@ -161,6 +166,7 @@ class Settings extends ChangeNotifier {
     'knownBleDev': knownBleDev,
     'bleInput': bleInput,
     'weightInput': weightInput,
+    'weightUnit': weightUnit.serialized,
   };
 
   /// Serialize the object to a restoreable string.
@@ -197,6 +203,7 @@ class Settings extends ChangeNotifier {
     _medications.addAll(other._medications);
     _highestMedIndex = other._highestMedIndex;
     _weightInput = other._weightInput;
+    _weightUnit = other._weightUnit;
     notifyListeners();
   }
 
@@ -444,6 +451,14 @@ class Settings extends ChangeNotifier {
   int _highestMedIndex = 0;
   /// Total amount of medicines created.
   int get highestMedIndex => _highestMedIndex;
+
+  WeightUnit _weightUnit = WeightUnit.kg;
+  /// Preferred unit for bodyweight.
+  WeightUnit get weightUnit => _weightUnit;
+  set weightUnit(WeightUnit value) {
+    _weightUnit = value;
+    notifyListeners();
+  }
   
 // When adding fields notice the checklist at the top.
 }
@@ -452,14 +467,9 @@ class Settings extends ChangeNotifier {
 /// [ConvertUtil.parseThemeMode].
 extension Serialization on ThemeMode {
   /// Turns enum into a restoreable integer.
-  int serialize() {
-    switch(this) {
-      case ThemeMode.system:
-        return 0;
-      case ThemeMode.dark:
-        return 1;
-      case ThemeMode.light:
-        return 2;
-    }
-  }
+  int serialize() => switch(this) {
+    ThemeMode.system =>  0,
+    ThemeMode.dark => 1,
+    ThemeMode.light => 2,
+  };
 }
app/lib/model/weight_unit.dart
@@ -0,0 +1,42 @@
+import 'package:health_data_store/health_data_store.dart';
+
+/// A unit [Weight] can be in.
+enum WeightUnit {
+  /// Kilograms, SI unit
+  kg,
+
+  /// Pounds, Defined by the Units of Measurement Regulations 1994
+  lbs,
+
+  /// Stone, the imperial unit of mass
+  st;
+
+  /// Restore from [serialized].
+  static WeightUnit? deserialize(int? value) => switch(value) {
+    0 => WeightUnit.kg,
+    1 => WeightUnit.lbs,
+    2 => WeightUnit.st,
+    _ => null,
+  };
+
+  /// Create a [WeightUnit.deserialize]able number.
+  int get serialized => switch(this) {
+    WeightUnit.kg => 0,
+    WeightUnit.lbs => 1,
+    WeightUnit.st => 2,
+  };
+
+  /// Create a [Weight] from a double in this unit.
+  Weight store(double value) => switch(this) {
+    WeightUnit.kg => Weight.kg(value),
+    WeightUnit.lbs => Weight.kg(value * 2.2046226218488),
+    WeightUnit.st => Weight.kg(value * 6.350),
+  };
+
+  /// Extract a weight to the preferred unit.
+  double extract(Weight w) => switch(this) {
+    WeightUnit.kg => w.kg,
+    WeightUnit.lbs => w.kg / 2.2046226218488,
+    WeightUnit.st => w.kg / 6.350,
+  };
+}
app/lib/screens/settings_screen.dart
@@ -25,6 +25,7 @@ import 'package:blood_pressure_app/model/storage/db/file_settings_loader.dart';
 import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
 import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
 import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:blood_pressure_app/model/weight_unit.dart';
 import 'package:blood_pressure_app/platform_integration/platform_client.dart';
 import 'package:file_picker/file_picker.dart';
 import 'package:flutter/material.dart';
@@ -299,6 +300,22 @@ class SettingsPage extends StatelessWidget {
                 onChanged: (value) {
                   settings.weightInput = value;
                 },),
+              if (settings.weightInput)
+                DropDownListTile<WeightUnit?>(
+                  leading: const Icon(Icons.language),
+                  title: Text(localizations.preferredWeightUnit),
+                  value: settings.weightUnit,
+                  items: [
+                    for (final u in WeightUnit.values)
+                      DropdownMenuItem(
+                        value: u,
+                        child: Text(u.name),
+                      ),
+                  ],
+                  onChanged: (WeightUnit? value) {
+                    if (value != null) settings.weightUnit = value;
+                  },
+                ),
             ],),
             TitledColumn(
               title: Text(localizations.data),
app/test/features/input/add_bodyweight_dialoge_test.dart
@@ -1,4 +1,6 @@
 import 'package:blood_pressure_app/features/input/add_bodyweight_dialoge.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:blood_pressure_app/model/weight_unit.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -46,4 +48,21 @@ void main() {
 
     expect(res, Weight.kg(123.45));
   });
+  testWidgets('respects preferred weight unit', (tester) async {
+    Weight? res;
+    await tester.pumpWidget(materialApp(Builder(
+      builder: (context) => GestureDetector(
+        onTap: () async => res = await showDialog<Weight>(context: context, builder: (_) => const AddBodyweightDialoge()),
+        child: const Text('X'),
+      ),
+    ), settings: Settings(weightUnit: WeightUnit.st)));
+    await tester.tap(find.text('X'));
+    await tester.pumpAndSettle();
+
+    await tester.enterText(find.byType(TextFormField), '123.45');
+    await tester.testTextInput.receiveAction(TextInputAction.done);
+    await tester.pumpAndSettle();
+
+    expect(res, WeightUnit.st.store(123.45));
+  });
 }
app/test/features/input/add_measurement_dialoge_test.dart
@@ -16,11 +16,7 @@ import '../settings/tiles/color_picker_list_tile_test.dart';
 void main() {
   group('AddEntryDialoge', () {
     testWidgets('should show everything on initial page', (tester) async {
-      await tester.pumpWidget(materialApp(
-        AddEntryDialoge(
-          availableMeds: [],
-        ),
-      ),);
+      await tester.pumpWidget(materialApp(const AddEntryDialoge(availableMeds: [])));
       expect(tester.takeException(), isNull);
 
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
@@ -37,7 +33,7 @@ void main() {
           initialRecord: mockEntryPos(
             DateTime.now(), 123, 56, 43, 'Test note', Colors.teal,
           ),
-          availableMeds: [],
+          availableMeds: const [],
         ),
       ),);
       await tester.pumpAndSettle();
@@ -149,7 +145,7 @@ void main() {
       );
       await tester.pumpWidget(materialApp(
         AddEntryDialoge(
-          availableMeds: [],
+          availableMeds: const [],
         ),
         settings: settings,
       ),);
app/test/features/measurement_list/weight_list_test.dart
@@ -1,6 +1,7 @@
 import 'package:blood_pressure_app/features/measurement_list/weight_list.dart';
 import 'package:blood_pressure_app/model/storage/interval_store.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:blood_pressure_app/model/weight_unit.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -102,4 +103,21 @@ void main() {
     expect(find.text(localizations.confirmDelete), findsNothing);
     expect(find.text('123 kg'), findsNothing);
   });
+  testWidgets('respects confirm weight unit setting', (tester) async {
+    final interval = IntervalStorage();
+    interval.changeStepSize(TimeStep.lifetime);
+    final repo = MockBodyweightRepository();
+    await repo.add(BodyweightRecord(time: DateTime(2001), weight: Weight.kg(123.0)));
+
+    await tester.pumpWidget(appBase(
+      weightRepo: repo,
+      intervallStoreManager: IntervalStoreManager(interval, IntervalStorage(), IntervalStorage()),
+      settings: Settings(weightUnit: WeightUnit.lbs),
+      const WeightList(rangeType: IntervalStoreManagerLocation.mainPage),
+    ));
+    await tester.pumpAndSettle();
+
+    expect(find.text('123 kg'), findsNothing);
+    expect(find.text('55.79 lbs'), findsOneWidget);
+  });
 }
app/test/model/json_serialization_test.dart
@@ -1,4 +1,5 @@
 
+import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
 import 'package:blood_pressure_app/model/export_import/column.dart';
 import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
 import 'package:blood_pressure_app/model/horizontal_graph_line.dart';
@@ -8,6 +9,7 @@ import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart'
 import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
 import 'package:blood_pressure_app/model/storage/interval_store.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:blood_pressure_app/model/weight_unit.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:health_data_store/health_data_store.dart';
@@ -97,6 +99,8 @@ void main() {
         knownBleDev: ['a', 'b'],
         bleInput: false,
         weightInput: true,
+        weightUnit: WeightUnit.st,
+        preferredPressureUnit: PressureUnit.kPa,
       );
       final fromJson = Settings.fromJson(initial.toJson());
 
@@ -126,6 +130,8 @@ void main() {
       expect(initial.knownBleDev, fromJson.knownBleDev);
       expect(initial.bleInput, fromJson.bleInput);
       expect(initial.weightInput, fromJson.weightInput);
+      expect(initial.preferredPressureUnit, fromJson.preferredPressureUnit);
+      expect(initial.weightUnit, fromJson.weightUnit);
 
       expect(initial.toJson(), fromJson.toJson());
     });
app/test/model/weight_unit_test.dart
@@ -0,0 +1,16 @@
+import 'package:blood_pressure_app/model/weight_unit.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+void main() {
+  test('converts all units to kg', () {
+    expect(WeightUnit.kg.store(72.34).kg, closeTo(72.34, 0.01));
+    expect(WeightUnit.lbs.store(32.812872).kg, closeTo(72.34, 0.01));
+    expect(WeightUnit.st.store(11.3916).kg, closeTo(72.34, 0.01));
+  });
+  test('converts kg to all units', () {
+    expect(WeightUnit.kg.extract(Weight.kg(72.34)), closeTo(72.34, 0.01));
+    expect(WeightUnit.lbs.extract(Weight.kg(72.34)), closeTo(32.812872, 0.00001));
+    expect(WeightUnit.st.extract(Weight.kg(72.34)), closeTo(11.3916, 0.001));
+  });
+}
app/pubspec.lock
@@ -5,23 +5,23 @@ packages:
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
+      sha256: f6dbf021f4b214d85c79822912c5fcd142a2c4869f01222ad371bc51f9f1c356
       url: "https://pub.dev"
     source: hosted
-    version: "73.0.0"
+    version: "74.0.0"
   _macros:
     dependency: transitive
     description: dart
     source: sdk
-    version: "0.3.2"
+    version: "0.3.3"
   analyzer:
     dependency: transitive
     description:
       name: analyzer
-      sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
+      sha256: f7e8caf82f2d3190881d81012606effdf8a38e6c1ab9e30947149733065f817c
       url: "https://pub.dev"
     source: hosted
-    version: "6.8.0"
+    version: "6.9.0"
   app_settings:
     dependency: "direct main"
     description:
@@ -532,10 +532,10 @@ packages:
     dependency: transitive
     description:
       name: macros
-      sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+      sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
       url: "https://pub.dev"
     source: hosted
-    version: "0.1.2-main.4"
+    version: "0.1.3-main.0"
   markdown:
     dependency: transitive
     description: