diff --git a/lib/core/utils/bounds/ranges_const.dart b/lib/core/utils/bounds/ranges_const.dart new file mode 100644 index 000000000..6a670e409 --- /dev/null +++ b/lib/core/utils/bounds/ranges_const.dart @@ -0,0 +1,28 @@ +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; + +class Ranges{ + static test(){ + } + //first_page + static const Duration maxDurationForBirthdayIntoTheFuture = Duration(days: -1); //values < 0 result a Date in the past + static const Duration maxAge = Duration(days: 365 * 130); + //second_page + static const double maxHeight = 300; + static const double minHeight = 30; + static const double maxWeight = 640; + static const double minWeight = 2; + static const bool cmAllowDecimalPlaces = false; + static const bool feetAllowDecimalPlaces = true; + static const bool kgAllowDecimalPlaces = true; + static const bool lbsAllowDecimalPlaces = true; + + //generated + static final RegExp cmRegExp = cmAllowDecimalPlaces ? RegExp(r'^[0-9]+([.,][0-9])?$') : RegExp(r'^[0-9]+$'); + static final RegExp feetRegExp = feetAllowDecimalPlaces ? RegExp(r'^[0-9]+([.,][0-9])?$') : RegExp(r'^[0-9]+$'); + static final RegExp kgRegExp = kgAllowDecimalPlaces ? RegExp(r'^[0-9]+([.,][0-9])?$') : RegExp(r'^[0-9]+$'); + static final RegExp lbsRegExp = lbsAllowDecimalPlaces ? RegExp(r'^[0-9]+([.,][0-9])?$') : RegExp(r'^[0-9]+$'); + static final double maxHeightInFeet = UnitCalc.cmToFeet(maxHeight); + static final double minHeightInFeet = UnitCalc.cmToFeet(minHeight); + static final double maxWeightInLbs = UnitCalc.kgToLbs(maxWeight); + static final double minWeightInLbs = UnitCalc.kgToLbs(minWeight); +} \ No newline at end of file diff --git a/lib/core/utils/bounds/validator.dart b/lib/core/utils/bounds/validator.dart new file mode 100644 index 000000000..344f29a8c --- /dev/null +++ b/lib/core/utils/bounds/validator.dart @@ -0,0 +1,72 @@ +import 'package:opennutritracker/core/utils/bounds/ranges_const.dart'; + +import '../calc/unit_calc.dart'; + +class ValueValidator{ + + static String? heightStringValidator(String? value, String wrongHeightLabel, {bool isImperial = false}){ + if(value == null) return wrongHeightLabel; + + if (isImperial) { + if (value.isEmpty || !Ranges.feetRegExp.hasMatch(value)) { + return wrongHeightLabel; + } else { + return null; + } + } else { + if (value.isEmpty || !Ranges.cmRegExp.hasMatch(value)) { + return wrongHeightLabel; + } else { + return null; + } + } + } + + static String? weightStringValidator(String? value, String wrongWeightLabel, {bool isImperial = false}){ + if(value == null) return wrongWeightLabel; + + if (isImperial) { + if (value.isEmpty || !Ranges.lbsRegExp.hasMatch(value)) { + return wrongWeightLabel; + } else { + return null; + } + } else { + if (value.isEmpty || !Ranges.kgRegExp.hasMatch(value)) { + return wrongWeightLabel; + } else { + return null; + } + } + } + + static double? parseHeightInCm(double? height, {bool isImperial = false}){ + if(height == null) return null; + bool isBelowMin = isImperial ? height < Ranges.minHeightInFeet : height < Ranges.minHeight; + bool isAboveMax = isImperial ? height > Ranges.maxHeightInFeet : height > Ranges.maxHeight; + + if (isBelowMin || isAboveMax) { + return null; + } + return !isImperial ? height : UnitCalc.feetToCm(height); + } + + static double? parseWeightInKg(double? weight, {bool isImperial = false}){ + if(weight == null) return null; + bool isBelowMin = isImperial ? weight < Ranges.minWeightInLbs : weight < Ranges.minWeight; + bool isAboveMax = isImperial ? weight > Ranges.maxWeightInLbs : weight > Ranges.maxWeight; + + if (isBelowMin || isAboveMax) { + return null; + } + return !isImperial ? weight : UnitCalc.lbsToKg(weight); + } + + static DateTime getFirstDate(){ + return DateTime.now().subtract(Ranges.maxAge); + } + + static DateTime getLastDate(){ + return DateTime.now().add(Ranges.maxDurationForBirthdayIntoTheFuture); + } +} \ No newline at end of file diff --git a/lib/features/onboarding/presentation/widgets/onboarding_first_page_body.dart b/lib/features/onboarding/presentation/widgets/onboarding_first_page_body.dart index 20af5b166..81dde409e 100644 --- a/lib/features/onboarding/presentation/widgets/onboarding_first_page_body.dart +++ b/lib/features/onboarding/presentation/widgets/onboarding_first_page_body.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; +import 'package:opennutritracker/core/utils/bounds/validator.dart'; import 'package:opennutritracker/features/onboarding/domain/entity/user_gender_selection_entity.dart'; import 'package:opennutritracker/generated/l10n.dart'; @@ -86,11 +86,12 @@ class _OnboardingFirstPageBodyState extends State { void onDateInputClicked() async { final pickedDate = await showDatePicker( context: context, - initialDate: DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime(2100)); + initialDate: DateTime.now().compareTo(ValueValidator.getLastDate()) >= 0 ? ValueValidator.getLastDate() : DateTime.now(), // !!! if merge with #206, use both + firstDate: ValueValidator.getFirstDate(), + lastDate: ValueValidator.getLastDate()); if (pickedDate != null) { - String formattedDate = DateFormat('yyyy-MM-dd').format(pickedDate); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + String formattedDate = localizations.formatCompactDate(pickedDate); setState(() { _selectedDate = pickedDate; _dateInput.text = formattedDate; @@ -106,11 +107,8 @@ class _OnboardingFirstPageBodyState extends State { } else if (_femaleSelected) { selectedGender = UserGenderSelectionEntity.genderFemale; } - - if (selectedGender != null && _selectedDate != null) { - widget.setPageContent(true, selectedGender, _selectedDate); - } else { - widget.setPageContent(false, null, null); - } + selectedGender != null && _selectedDate != null + ? widget.setPageContent(true, selectedGender, _selectedDate) + : widget.setPageContent(false, null, null); } } diff --git a/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart b/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart index 7b4ce05bf..945af6705 100644 --- a/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart +++ b/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; +import 'package:opennutritracker/core/utils/bounds/validator.dart'; import 'package:opennutritracker/generated/l10n.dart'; class OnboardingSecondPageBody extends StatefulWidget { @@ -40,13 +40,7 @@ class _OnboardingSecondPageBodyState extends State { key: _heightFormKey, child: TextFormField( onChanged: (text) { - if (_heightFormKey.currentState!.validate()) { - _parsedHeight = double.tryParse(text.replaceAll(',', '.')); - checkCorrectInput(); - } else { - _parsedHeight = null; - checkCorrectInput(); - } + _heightFormKey.currentState!.validate(); }, validator: validateHeight, decoration: InputDecoration( @@ -79,7 +73,7 @@ class _OnboardingSecondPageBodyState extends State { _isUnitSelected[i] = i == index; } _heightFormKey.currentState!.validate(); - checkCorrectInput(); + _weightFormKey.currentState!.validate(); }); }, children: [ @@ -104,12 +98,7 @@ class _OnboardingSecondPageBodyState extends State { key: _weightFormKey, child: TextFormField( onChanged: (text) { - if (_weightFormKey.currentState!.validate()) { - _parsedWeight = double.tryParse(text); - checkCorrectInput(); - } else { - checkCorrectInput(); - } + _weightFormKey.currentState!.validate(); }, validator: validateWeight, decoration: InputDecoration( @@ -139,7 +128,7 @@ class _OnboardingSecondPageBodyState extends State { _isUnitSelected[i] = i == index; } _weightFormKey.currentState!.validate(); - checkCorrectInput(); + _heightFormKey.currentState!.validate(); }); }, children: [ @@ -160,54 +149,30 @@ class _OnboardingSecondPageBodyState extends State { } String? validateHeight(String? value) { - if (value == null) return S.of(context).onboardingWrongHeightLabel; - - if (_isImperialSelected) { - // Regex for feet and inches - if (value.isEmpty || !RegExp(r'^[0-9]+([.,][0-9])?$').hasMatch(value)) { - return S.of(context).onboardingWrongHeightLabel; - } else { - return null; - } - } else { - // Regex for cm - if (value.isEmpty || !RegExp(r'^[0-9]+$').hasMatch(value)) { - return S.of(context).onboardingWrongHeightLabel; - } else { - return null; - } - } + return tryParseValidValue(value, + ValueValidator.heightStringValidator(value, S.of(context).onboardingWrongHeightLabel, isImperial: _isImperialSelected), + (String value, bool isImperial) => { + _parsedHeight = ValueValidator.parseHeightInCm(double.tryParse(value), isImperial: _isImperialSelected) + }); } String? validateWeight(String? value) { - if (value == null) return S.of(context).onboardingWrongWeightLabel; - if (value.isEmpty || !RegExp(r'^[0-9]').hasMatch(value)) { - return S.of(context).onboardingWrongHeightLabel; - } else { - return null; - } + return tryParseValidValue(value, + ValueValidator.weightStringValidator(value, S.of(context).onboardingWrongHeightLabel, isImperial: _isImperialSelected), + (String value, bool isImperial) => { + _parsedWeight = ValueValidator.parseWeightInKg(double.tryParse(value), isImperial: _isImperialSelected) + }); } - /// Check if the input is correct and update the button content - void checkCorrectInput() { - final isHeightValid = _heightFormKey.currentState?.validate() ?? false; - final isWeightValid = _weightFormKey.currentState?.validate() ?? false; - - if (isHeightValid && isWeightValid) { - if (_parsedHeight != null && _parsedWeight != null) { - final heightCm = _isImperialSelected - ? UnitCalc.feetToCm(_parsedHeight!) - : _parsedHeight!; - final weightKg = _isImperialSelected - ? UnitCalc.lbsToKg(_parsedWeight!) - : _parsedWeight!; - - widget.setButtonContent(true, heightCm, weightKg, _isImperialSelected); - } else { - widget.setButtonContent(false, null, null, _isImperialSelected); - } - } else { + String? tryParseValidValue(String? value, String? errorLabel, Function(String, bool) parseValue){ + if(errorLabel != null) { widget.setButtonContent(false, null, null, _isImperialSelected); + return errorLabel; } + parseValue(value!, _isImperialSelected); + _parsedWeight != null && _parsedHeight != null + ? widget.setButtonContent(true, _parsedHeight, _parsedWeight, _isImperialSelected) + : widget.setButtonContent(false, null, null, _isImperialSelected); + return null; } } diff --git a/lib/features/profile/presentation/widgets/set_weight_dialog.dart b/lib/features/profile/presentation/widgets/set_weight_dialog.dart index 6c2471f4b..897749da2 100644 --- a/lib/features/profile/presentation/widgets/set_weight_dialog.dart +++ b/lib/features/profile/presentation/widgets/set_weight_dialog.dart @@ -1,16 +1,49 @@ import 'package:flutter/material.dart'; import 'package:horizontal_picker/horizontal_picker.dart'; +import 'package:opennutritracker/core/utils/bounds/ranges_const.dart'; +import 'package:opennutritracker/core/utils/bounds/validator.dart'; import 'package:opennutritracker/generated/l10n.dart'; class SetWeightDialog extends StatelessWidget { - static const weightRangeKg = 50.0; - static const weightRangeLbs = 100.0; + static const divisionsPerLbs = 5; + static const divisionsPerKg = 10; + static const kgScrollBuffer = 2; + static const lbsScrollBuffer = 5; final double userWeight; final bool usesImperialUnits; + late final int divisions; + late final double maxWeight, minWeight; - const SetWeightDialog( - {super.key, required this.userWeight, required this.usesImperialUnits}); + + SetWeightDialog(this.userWeight, this.usesImperialUnits, {super.key}) + { + super.key; + divisions = calculateDivisions(); + } + + int calculateDivisions(){ + getCloserBound(); + return usesImperialUnits + ? ((maxWeight - minWeight) * divisionsPerLbs).round() + : ((maxWeight - minWeight) * divisionsPerKg).round(); + } + + void getCloserBound(){ + double initialMaxWeight = usesImperialUnits ? Ranges.maxWeightInLbs : Ranges.maxWeight; + double initialMinWeight = usesImperialUnits ? Ranges.minWeightInLbs : Ranges.minWeight; + initialMinWeight = initialMinWeight - (usesImperialUnits ? lbsScrollBuffer : kgScrollBuffer); + initialMaxWeight = initialMaxWeight + (usesImperialUnits ? lbsScrollBuffer : kgScrollBuffer); + if(initialMaxWeight - userWeight > userWeight - initialMinWeight){ + print("sehr leicht"); + minWeight = initialMinWeight; + maxWeight = userWeight + (userWeight - minWeight); + }else{ + print("sehr schwer"); + maxWeight = initialMaxWeight; + minWeight = userWeight - (maxWeight - userWeight); + } + } @override Widget build(BuildContext context) { @@ -23,14 +56,10 @@ class SetWeightDialog extends StatelessWidget { HorizontalPicker( height: 100, backgroundColor: Colors.transparent, - minValue: usesImperialUnits - ? userWeight - weightRangeLbs - : userWeight - weightRangeKg, - maxValue: usesImperialUnits - ? userWeight + weightRangeLbs - : userWeight + weightRangeKg, + minValue: minWeight, + maxValue: maxWeight, initialPosition: InitialPosition.center, - divisions: 1000, + divisions: divisions, suffix: usesImperialUnits ? S.of(context).lbsLabel : S.of(context).kgLabel, @@ -48,8 +77,10 @@ class SetWeightDialog extends StatelessWidget { child: Text(S.of(context).dialogCancelLabel)), TextButton( onPressed: () { - // TODO validate selected weight - Navigator.pop(context, selectedWeight); + double? weight = ValueValidator.parseWeightInKg(selectedWeight, isImperial: usesImperialUnits); + if(weight != null) Navigator.pop(context, weight); + else ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(S.of(context).onboardingWrongWeightLabel))); }, child: Text(S.of(context).dialogOKLabel)), ],