Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class _OnboardingSecondPageBodyState extends State<OnboardingSecondPageBody> {
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;
return S.of(context).onboardingWrongWeightLabel;
} else {
return null;
}
Expand Down
40 changes: 40 additions & 0 deletions lib/features/profile/presentation/utils/profile_picker_bounds.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:math';

const _heightRangeCm = 100.0;
const _heightRangeFt = 10.0;
const _weightRangeKg = 50.0;
const _weightRangeLbs = 100.0;
const _minHeight = 1.0;
const _minWeight = 1.0;

double minSelectableHeight(double userHeight, bool usesImperialUnits) {
final range = usesImperialUnits ? _heightRangeFt : _heightRangeCm;
return max(_minHeight, userHeight - range);
}

double maxSelectableHeight(double userHeight, bool usesImperialUnits) {
final range = usesImperialUnits ? _heightRangeFt : _heightRangeCm;
final clampedMin = minSelectableHeight(userHeight, usesImperialUnits);
final rawMax = userHeight + range;
return max(clampedMin + range, rawMax);
}

double clampHeightSelection(double selectedHeight, double minHeight) {
return max(minHeight, selectedHeight);
}

double minSelectableWeight(double userWeight, bool usesImperialUnits) {
final range = usesImperialUnits ? _weightRangeLbs : _weightRangeKg;
return max(_minWeight, userWeight - range);
}

double maxSelectableWeight(double userWeight, bool usesImperialUnits) {
final range = usesImperialUnits ? _weightRangeLbs : _weightRangeKg;
final minWeight = minSelectableWeight(userWeight, usesImperialUnits);
final candidateMaxWeight = userWeight + range;
return max(minWeight + range, candidateMaxWeight);
}

double clampWeightSelection(double selectedWeight, double minWeight) {
return max(minWeight, selectedWeight);
}
31 changes: 13 additions & 18 deletions lib/features/profile/presentation/widgets/set_height_dialog.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:horizontal_picker/horizontal_picker.dart';
import 'package:opennutritracker/features/profile/presentation/utils/profile_picker_bounds.dart';
import 'package:opennutritracker/generated/l10n.dart';

class SetHeightDialog extends StatefulWidget {
static const _heightRangeCM = 100.0;
static const _heightRangeFt = 10.0;

final double userHeight;
final bool usesImperialUnits;

Expand All @@ -30,13 +28,10 @@ class _SetHeightDialogState extends State<SetHeightDialog> {

@override
Widget build(BuildContext context) {
final minValue = widget.usesImperialUnits
? widget.userHeight - SetHeightDialog._heightRangeFt
: widget.userHeight - SetHeightDialog._heightRangeCM;

final maxValue = widget.usesImperialUnits
? widget.userHeight + SetHeightDialog._heightRangeFt
: widget.userHeight + SetHeightDialog._heightRangeCM;
final minHeight =
minSelectableHeight(widget.userHeight, widget.usesImperialUnits);
final maxHeight =
maxSelectableHeight(widget.userHeight, widget.usesImperialUnits);

return AlertDialog(
title: Text(S.of(context).selectHeightDialogLabel),
Expand All @@ -47,17 +42,15 @@ class _SetHeightDialogState extends State<SetHeightDialog> {
HorizontalPicker(
height: 100,
backgroundColor: Colors.transparent,
// Prevent negative minimum height
minValue: minValue < 0 ? 1 : minValue, // setting it to 1, because 0 triggers zero-division error
maxValue: maxValue,
minValue: minHeight,
maxValue: maxHeight,
divisions: 400,
suffix: widget.usesImperialUnits
? S.of(context).ftLabel
: S.of(context).cmLabel,
onChanged: (value) {
setState(() {
// Prevent negative height values
selectedHeight = value < 0 ? 1 : value;
selectedHeight = value;
});
},
),
Expand All @@ -74,12 +67,14 @@ class _SetHeightDialogState extends State<SetHeightDialog> {
),
TextButton(
onPressed: () {
// TODO validate selected height
Navigator.pop(context, selectedHeight);
Navigator.pop(
context,
clampHeightSelection(selectedHeight, minHeight),
);
},
child: Text(S.of(context).dialogOKLabel),
),
],
);
}
}
}
26 changes: 12 additions & 14 deletions lib/features/profile/presentation/widgets/set_weight_dialog.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:horizontal_picker/horizontal_picker.dart';
import 'package:opennutritracker/features/profile/presentation/utils/profile_picker_bounds.dart';
import 'package:opennutritracker/generated/l10n.dart';

class SetWeightDialog extends StatefulWidget {
static const weightRangeKg = 50.0;
static const weightRangeLbs = 100.0;

final double userWeight;
final bool usesImperialUnits;

Expand All @@ -30,13 +28,10 @@ class _SetWeightDialogState extends State<SetWeightDialog> {

@override
Widget build(BuildContext context) {
final minValue = widget.usesImperialUnits
? widget.userWeight - SetWeightDialog.weightRangeLbs
: widget.userWeight - SetWeightDialog.weightRangeKg;

final maxValue = widget.usesImperialUnits
? widget.userWeight + SetWeightDialog.weightRangeLbs
: widget.userWeight + SetWeightDialog.weightRangeKg;
final minWeight =
minSelectableWeight(widget.userWeight, widget.usesImperialUnits);
final maxWeight =
maxSelectableWeight(widget.userWeight, widget.usesImperialUnits);

return AlertDialog(
title: Text(S.of(context).selectWeightDialogLabel),
Expand All @@ -47,16 +42,16 @@ class _SetWeightDialogState extends State<SetWeightDialog> {
HorizontalPicker(
height: 100,
backgroundColor: Colors.transparent,
minValue: minValue < 0 ? 0 : minValue, // 👈 no negative minimum
maxValue: maxValue,
minValue: minWeight,
maxValue: maxWeight,
initialPosition: InitialPosition.center,
divisions: 1000,
suffix: widget.usesImperialUnits
? S.of(context).lbsLabel
: S.of(context).kgLabel,
onChanged: (value) {
setState(() {
selectedWeight = value < 0 ? 0 : value; // 👈 no negative values
selectedWeight = value;
});
},
),
Expand All @@ -73,7 +68,10 @@ class _SetWeightDialogState extends State<SetWeightDialog> {
),
TextButton(
onPressed: () {
Navigator.pop(context, selectedWeight);
Navigator.pop(
context,
clampWeightSelection(selectedWeight, minWeight),
);
},
child: Text(S.of(context).dialogOKLabel),
),
Expand Down
46 changes: 46 additions & 0 deletions test/unit_test/profile_picker_bounds_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:opennutritracker/features/profile/presentation/utils/profile_picker_bounds.dart';

void main() {
group('profile picker bounds', () {
test('metric height minimum is clamped to 1', () {
expect(minSelectableHeight(80, false), 1);
});

test('imperial height minimum is clamped to 1', () {
expect(minSelectableHeight(5, true), 1);
});

test('metric height maximum keeps expected range', () {
expect(maxSelectableHeight(170, false), 270);
});

test('weight minimum is clamped to 1 for metric and imperial', () {
expect(minSelectableWeight(40, false), 1);
expect(minSelectableWeight(80, true), 1);
});

test('weight maximum keeps expected range', () {
expect(maxSelectableWeight(75, false), 125);
expect(maxSelectableWeight(140, true), 240);
});

Comment thread
simonoppowa marked this conversation as resolved.
test('extreme negative persisted values still produce a valid metric range', () {
expect(
maxSelectableHeight(-200, false) >= minSelectableHeight(-200, false),
isTrue,
);
expect(
maxSelectableWeight(-200, false) >= minSelectableWeight(-200, false),
isTrue,
);
});

test('selected values are clamped to computed minimum', () {
expect(clampHeightSelection(0.5, 1), 1);
expect(clampWeightSelection(0.5, 1), 1);
expect(clampHeightSelection(180, 1), 180);
expect(clampWeightSelection(80, 1), 80);
});
});
}
Loading