feat(train): weights in pounds too, the person's unit or their region's (ADR 0030, D64)
Every weight is still kept in kilograms. It is shown and entered in the person's unit, chosen in Your body (Weight in: kg | lb, the profile's new weightUnit), else the region's: pounds in the US, Liberia and Myanmar. The entry takes 44 to 880 lb, the same 20 to 400 kg. A weight left untouched in Your body is saved exactly as it was, not rounded through pounds. The other figures stay metric. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3eb757c921
commit
396e010949
15 changed files with 451 additions and 32 deletions
File diff suppressed because one or more lines are too long
|
|
@ -751,5 +751,11 @@
|
||||||
"loopDetection": "Training selbst erkennen",
|
"loopDetection": "Training selbst erkennen",
|
||||||
"loopDetectionHint": "Das Band bemerkt ein Training, das du nicht aufgezeichnet hast, und es kommt als Training herein.",
|
"loopDetectionHint": "Das Band bemerkt ein Training, das du nicht aufgezeichnet hast, und es kommt als Training herein.",
|
||||||
"loopDetectionMinimum": "Behält Trainings ab",
|
"loopDetectionMinimum": "Behält Trainings ab",
|
||||||
"loopDetectionMinutes": "{minutes} Min."
|
"loopDetectionMinutes": "{minutes} Min.",
|
||||||
|
"bodyWeightPounds": "{lb} lb",
|
||||||
|
"weightInvalidPounds": "Ein Gewicht zwischen 44 und 880 lb",
|
||||||
|
"profileWeightUnitPounds": "lb",
|
||||||
|
"profileWeightIn": "Gewicht in",
|
||||||
|
"profileWeightKilograms": "Kilogramm",
|
||||||
|
"profileWeightPounds": "Pfund"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,13 +263,21 @@
|
||||||
},
|
},
|
||||||
"workoutMapUnavailable": "The map can't be loaded now.",
|
"workoutMapUnavailable": "The map can't be loaded now.",
|
||||||
"workoutMapAbout": "About the map",
|
"workoutMapAbout": "About the map",
|
||||||
"@workoutMapAbout": {"description": "Title of the explanation behind the question mark on a workout's page (D63): what the map's source sees."},
|
"@workoutMapAbout": {
|
||||||
|
"description": "Title of the explanation behind the question mark on a workout's page (D63): what the map's source sees."
|
||||||
|
},
|
||||||
"workoutShowMap": "Show the map",
|
"workoutShowMap": "Show the map",
|
||||||
"@workoutShowMap": {"description": "Tooltip of the chevron that lowers a workout's sheet, the page pulled up, so that the map shows again (D63)."},
|
"@workoutShowMap": {
|
||||||
|
"description": "Tooltip of the chevron that lowers a workout's sheet, the page pulled up, so that the map shows again (D63)."
|
||||||
|
},
|
||||||
"mapCredits": "Map credits",
|
"mapCredits": "Map credits",
|
||||||
"@mapCredits": {"description": "Tooltip of the button over a map that shows whose map and data it is."},
|
"@mapCredits": {
|
||||||
|
"description": "Tooltip of the button over a map that shows whose map and data it is."
|
||||||
|
},
|
||||||
"routeHelpTitle": "Planning a route",
|
"routeHelpTitle": "Planning a route",
|
||||||
"@routeHelpTitle": {"description": "Title of the explanation behind the question mark over the route planner's map (D63)."},
|
"@routeHelpTitle": {
|
||||||
|
"description": "Title of the explanation behind the question mark over the route planner's map (D63)."
|
||||||
|
},
|
||||||
"workoutNotFound": "This workout is gone.",
|
"workoutNotFound": "This workout is gone.",
|
||||||
"recordHeartRate": "Heart rate",
|
"recordHeartRate": "Heart rate",
|
||||||
"recordNoStrap": "No strap",
|
"recordNoStrap": "No strap",
|
||||||
|
|
@ -2122,5 +2130,21 @@
|
||||||
"type": "int"
|
"type": "int"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"bodyWeightPounds": "{lb} lb",
|
||||||
|
"@bodyWeightPounds": {
|
||||||
|
"placeholders": {
|
||||||
|
"lb": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weightInvalidPounds": "A weight between 44 and 880 lb",
|
||||||
|
"profileWeightUnitPounds": "lb",
|
||||||
|
"profileWeightIn": "Weight in",
|
||||||
|
"@profileWeightIn": {
|
||||||
|
"description": "Above the choice between kilograms and pounds in Your body."
|
||||||
|
},
|
||||||
|
"profileWeightKilograms": "Kilograms",
|
||||||
|
"profileWeightPounds": "Pounds"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4611,6 +4611,42 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'{minutes} min'**
|
/// **'{minutes} min'**
|
||||||
String loopDetectionMinutes(int minutes);
|
String loopDetectionMinutes(int minutes);
|
||||||
|
|
||||||
|
/// No description provided for @bodyWeightPounds.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{lb} lb'**
|
||||||
|
String bodyWeightPounds(String lb);
|
||||||
|
|
||||||
|
/// No description provided for @weightInvalidPounds.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A weight between 44 and 880 lb'**
|
||||||
|
String get weightInvalidPounds;
|
||||||
|
|
||||||
|
/// No description provided for @profileWeightUnitPounds.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'lb'**
|
||||||
|
String get profileWeightUnitPounds;
|
||||||
|
|
||||||
|
/// Above the choice between kilograms and pounds in Your body.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Weight in'**
|
||||||
|
String get profileWeightIn;
|
||||||
|
|
||||||
|
/// No description provided for @profileWeightKilograms.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Kilograms'**
|
||||||
|
String get profileWeightKilograms;
|
||||||
|
|
||||||
|
/// No description provided for @profileWeightPounds.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pounds'**
|
||||||
|
String get profileWeightPounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -2978,4 +2978,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String loopDetectionMinutes(int minutes) {
|
String loopDetectionMinutes(int minutes) {
|
||||||
return '$minutes Min.';
|
return '$minutes Min.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String bodyWeightPounds(String lb) {
|
||||||
|
return '$lb lb';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get weightInvalidPounds => 'Ein Gewicht zwischen 44 und 880 lb';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightUnitPounds => 'lb';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightIn => 'Gewicht in';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightKilograms => 'Kilogramm';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightPounds => 'Pfund';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2965,6 +2965,26 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String loopDetectionMinutes(int minutes) {
|
String loopDetectionMinutes(int minutes) {
|
||||||
return '$minutes min';
|
return '$minutes min';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String bodyWeightPounds(String lb) {
|
||||||
|
return '$lb lb';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get weightInvalidPounds => 'A weight between 44 and 880 lb';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightUnitPounds => 'lb';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightIn => 'Weight in';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightKilograms => 'Kilograms';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileWeightPounds => 'Pounds';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ ByteData encodeProfile(ProfileContent content) => ByteData.sublistView(
|
||||||
'trainingBackground': ?content.trainingBackground?.name,
|
'trainingBackground': ?content.trainingBackground?.name,
|
||||||
'typicalDay': ?content.typicalDay?.name,
|
'typicalDay': ?content.typicalDay?.name,
|
||||||
'sleepGoalMinutes': ?content.sleepGoal?.inMinutes,
|
'sleepGoalMinutes': ?content.sleepGoal?.inMinutes,
|
||||||
|
'weightUnit': ?content.weightUnit?.name,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -59,6 +60,7 @@ ProfileContent? decodeProfile(ByteData payload) {
|
||||||
.asNameMap()[json['trainingBackground']],
|
.asNameMap()[json['trainingBackground']],
|
||||||
typicalDay: TypicalDay.values.asNameMap()[json['typicalDay']],
|
typicalDay: TypicalDay.values.asNameMap()[json['typicalDay']],
|
||||||
sleepGoal: sleep == null ? null : Duration(minutes: sleep),
|
sleepGoal: sleep == null ? null : Duration(minutes: sleep),
|
||||||
|
weightUnit: WeightUnit.values.asNameMap()[json['weightUnit']],
|
||||||
);
|
);
|
||||||
} on Object catch (error) {
|
} on Object catch (error) {
|
||||||
log('A profile not read: ${error.runtimeType}', name: 'Profile');
|
log('A profile not read: ${error.runtimeType}', name: 'Profile');
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,29 @@ enum TypicalDay() {
|
||||||
mostlyMoving,
|
mostlyMoving,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a weight is shown and typed in (decided 4 Nov 2026); kept in
|
||||||
|
/// kilograms whatever it is.
|
||||||
|
enum WeightUnit() {
|
||||||
|
kilograms,
|
||||||
|
pounds;
|
||||||
|
|
||||||
|
/// A pound in kilograms, as defined.
|
||||||
|
static const poundInKilograms = 0.45359237;
|
||||||
|
|
||||||
|
/// Where the person chose none: pounds where the app's region weighs in
|
||||||
|
/// them (the United States, Liberia, Myanmar), kilograms elsewhere.
|
||||||
|
static WeightUnit ofRegion(String? countryCode) =>
|
||||||
|
const {'US', 'LR', 'MM'}.contains(countryCode) ? pounds : kilograms;
|
||||||
|
|
||||||
|
/// [kilograms] in this unit.
|
||||||
|
double fromKilograms(double kilograms) =>
|
||||||
|
this == pounds ? kilograms / poundInKilograms : kilograms;
|
||||||
|
|
||||||
|
/// [value], in this unit, in kilograms.
|
||||||
|
double toKilograms(double value) =>
|
||||||
|
this == pounds ? value * poundInKilograms : value;
|
||||||
|
}
|
||||||
|
|
||||||
/// The figures of the person's body (D33), each null where they gave none.
|
/// The figures of the person's body (D33), each null where they gave none.
|
||||||
final class const ProfileContent({
|
final class const ProfileContent({
|
||||||
final Sex? sex,
|
final Sex? sex,
|
||||||
|
|
@ -40,7 +63,16 @@ final class const ProfileContent({
|
||||||
|
|
||||||
/// How long they mean to sleep a night, 5 to 11 hours.
|
/// How long they mean to sleep a night, 5 to 11 hours.
|
||||||
final Duration? sleepGoal,
|
final Duration? sleepGoal,
|
||||||
|
|
||||||
|
/// What their weight is shown in; their region's ([WeightUnit.ofRegion])
|
||||||
|
/// where they chose none.
|
||||||
|
final WeightUnit? weightUnit,
|
||||||
}) with Equatable {
|
}) with Equatable {
|
||||||
|
/// What their weight is shown in, in the region [countryCode] where they
|
||||||
|
/// chose none.
|
||||||
|
WeightUnit weightUnitIn(String? countryCode) =>
|
||||||
|
weightUnit ?? WeightUnit.ofRegion(countryCode);
|
||||||
|
|
||||||
/// The same figures with the weight [kilograms], as a weight measured
|
/// The same figures with the weight [kilograms], as a weight measured
|
||||||
/// last sets it (D64).
|
/// last sets it (D64).
|
||||||
ProfileContent withWeight(double kilograms) => ProfileContent(
|
ProfileContent withWeight(double kilograms) => ProfileContent(
|
||||||
|
|
@ -54,6 +86,7 @@ final class const ProfileContent({
|
||||||
trainingBackground: trainingBackground,
|
trainingBackground: trainingBackground,
|
||||||
typicalDay: typicalDay,
|
typicalDay: typicalDay,
|
||||||
sleepGoal: sleepGoal,
|
sleepGoal: sleepGoal,
|
||||||
|
weightUnit: weightUnit,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Their age on [day], in whole years; null without a birth date.
|
/// Their age on [day], in whole years; null without a birth date.
|
||||||
|
|
@ -80,6 +113,7 @@ final class const ProfileContent({
|
||||||
trainingBackground,
|
trainingBackground,
|
||||||
typicalDay,
|
typicalDay,
|
||||||
sleepGoal,
|
sleepGoal,
|
||||||
|
weightUnit,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
|
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/profile/widget/profile_screen.dart';
|
import 'package:halkyon_train_flutter/src/feature/profile/widget/profile_screen.dart';
|
||||||
|
import 'package:halkyon_train_flutter/src/feature/weight/widget/weight_card.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
|
@ -58,7 +58,12 @@ class _BodyCardState() extends State<BodyCard> {
|
||||||
final locale = Localizations.localeOf(context).toString();
|
final locale = Localizations.localeOf(context).toString();
|
||||||
final figures = [
|
final figures = [
|
||||||
if (profile.weightKg case final kg?)
|
if (profile.weightKg case final kg?)
|
||||||
l10n.bodyWeight(NumberFormat('0.#', locale).format(kg)),
|
weightText(
|
||||||
|
l10n,
|
||||||
|
locale,
|
||||||
|
kg,
|
||||||
|
weightUnitOf(context, profile.weightUnit),
|
||||||
|
),
|
||||||
if (profile.restingHeartRate case final bpm?) l10n.bodyResting(bpm),
|
if (profile.restingHeartRate case final bpm?) l10n.bodyResting(bpm),
|
||||||
if (profile.maxHeartRate case final bpm?) l10n.bodyMax(bpm),
|
if (profile.maxHeartRate case final bpm?) l10n.bodyMax(bpm),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/profile/bloc/profile_cubit.dart';
|
import 'package:halkyon_train_flutter/src/feature/profile/bloc/profile_cubit.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
|
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/recording/widget/recording_texts.dart';
|
import 'package:halkyon_train_flutter/src/feature/recording/widget/recording_texts.dart';
|
||||||
|
import 'package:halkyon_train_flutter/src/feature/weight/widget/weight_card.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
@ -95,11 +96,61 @@ class _ProfileFormState() extends State<ProfileForm> {
|
||||||
late TypicalDay? _day = widget.profile.typicalDay;
|
late TypicalDay? _day = widget.profile.typicalDay;
|
||||||
late Duration? _sleep = widget.profile.sleepGoal;
|
late Duration? _sleep = widget.profile.sleepGoal;
|
||||||
late final _height = _text(widget.profile.heightCm);
|
late final _height = _text(widget.profile.heightCm);
|
||||||
|
|
||||||
|
/// What the weight is typed in; the region's until [didChangeDependencies]
|
||||||
|
/// where the person chose none.
|
||||||
|
late WeightUnit _unit = widget.profile.weightUnit ?? WeightUnit.kilograms;
|
||||||
|
late final bool _unitChosen = widget.profile.weightUnit != null;
|
||||||
late final _weight = _text(widget.profile.weightKg);
|
late final _weight = _text(widget.profile.weightKg);
|
||||||
|
|
||||||
|
/// The weight in kilograms as it was before it was shown in [_unit],
|
||||||
|
/// rounded, and what it was shown as: kept while the field is untouched,
|
||||||
|
/// so that a weight passing through pounds comes back as it was.
|
||||||
|
late double? _exactKilograms = widget.profile.weightKg;
|
||||||
|
late String _shownWeight = _weight.text;
|
||||||
|
|
||||||
|
/// The weight in the field, in kilograms.
|
||||||
|
double? get _weightKilograms => _weight.text == _shownWeight
|
||||||
|
? _exactKilograms
|
||||||
|
: switch (_number(_weight.text)) {
|
||||||
|
final typed? => _unit.toKilograms(typed),
|
||||||
|
null => null,
|
||||||
|
};
|
||||||
late final _resting = _text(widget.profile.restingHeartRate);
|
late final _resting = _text(widget.profile.restingHeartRate);
|
||||||
late final _max = _text(widget.profile.maxHeartRate);
|
late final _max = _text(widget.profile.maxHeartRate);
|
||||||
late final _vo2 = _text(widget.profile.vo2Max);
|
late final _vo2 = _text(widget.profile.vo2Max);
|
||||||
|
|
||||||
|
var _regionApplied = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (!_unitChosen && !_regionApplied) {
|
||||||
|
_regionApplied = true;
|
||||||
|
_setUnit(weightUnitOf(context, null), chosen: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the weight typed so far in [unit] from now on.
|
||||||
|
void _setUnit(WeightUnit unit, {bool chosen = true}) {
|
||||||
|
final kilograms = _weightKilograms;
|
||||||
|
_unit = unit;
|
||||||
|
if (chosen) {
|
||||||
|
_chosenUnit = unit;
|
||||||
|
}
|
||||||
|
if (kilograms != null) {
|
||||||
|
_weight.text = _figure(unit.fromKilograms(kilograms));
|
||||||
|
_exactKilograms = kilograms;
|
||||||
|
_shownWeight = _weight.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unit the person picked here; null until they do.
|
||||||
|
late WeightUnit? _chosenUnit = widget.profile.weightUnit;
|
||||||
|
|
||||||
|
static String _figure(double value) =>
|
||||||
|
value.toStringAsFixed(1).replaceFirst(RegExp(r'\.0$'), '');
|
||||||
|
|
||||||
bool _shows(ProfileField field) => widget.only?.contains(field) ?? true;
|
bool _shows(ProfileField field) => widget.only?.contains(field) ?? true;
|
||||||
|
|
||||||
bool get _needed => widget.only != null;
|
bool get _needed => widget.only != null;
|
||||||
|
|
@ -166,15 +217,48 @@ class _ProfileFormState() extends State<ProfileForm> {
|
||||||
unit: l10n.profileHeightUnit,
|
unit: l10n.profileHeightUnit,
|
||||||
range: (100, 250),
|
range: (100, 250),
|
||||||
),
|
),
|
||||||
if (_shows(ProfileField.weight))
|
if (_shows(ProfileField.weight)) ...[
|
||||||
_Figure(
|
_Figure(
|
||||||
controller: _weight,
|
controller: _weight,
|
||||||
needed: _needed,
|
needed: _needed,
|
||||||
label: l10n.profileWeight,
|
label: l10n.profileWeight,
|
||||||
unit: l10n.profileWeightUnit,
|
unit: switch (_unit) {
|
||||||
range: (20, 300),
|
WeightUnit.kilograms => l10n.profileWeightUnit,
|
||||||
|
WeightUnit.pounds => l10n.profileWeightUnitPounds,
|
||||||
|
},
|
||||||
|
range: switch (_unit) {
|
||||||
|
WeightUnit.kilograms => (20, 300),
|
||||||
|
WeightUnit.pounds => (44, 660),
|
||||||
|
},
|
||||||
decimal: true,
|
decimal: true,
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(l10n.profileWeightIn)),
|
||||||
|
SegmentedButton<WeightUnit>(
|
||||||
|
showSelectedIcon: false,
|
||||||
|
segments: [
|
||||||
|
ButtonSegment(
|
||||||
|
value: WeightUnit.kilograms,
|
||||||
|
label: Text(l10n.profileWeightUnit),
|
||||||
|
tooltip: l10n.profileWeightKilograms,
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: WeightUnit.pounds,
|
||||||
|
label: Text(l10n.profileWeightUnitPounds),
|
||||||
|
tooltip: l10n.profileWeightPounds,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_unit},
|
||||||
|
onSelectionChanged: (units) =>
|
||||||
|
setState(() => _setUnit(units.single)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
if (_shows(ProfileField.restingHeartRate))
|
if (_shows(ProfileField.restingHeartRate))
|
||||||
_Figure(
|
_Figure(
|
||||||
controller: _resting,
|
controller: _resting,
|
||||||
|
|
@ -280,13 +364,17 @@ class _ProfileFormState() extends State<ProfileForm> {
|
||||||
sex: _sex,
|
sex: _sex,
|
||||||
birthDate: _birthDate,
|
birthDate: _birthDate,
|
||||||
heightCm: whole(_height),
|
heightCm: whole(_height),
|
||||||
weightKg: _number(_weight.text),
|
weightKg: _weightKilograms,
|
||||||
restingHeartRate: whole(_resting),
|
restingHeartRate: whole(_resting),
|
||||||
maxHeartRate: whole(_max),
|
maxHeartRate: whole(_max),
|
||||||
vo2Max: whole(_vo2),
|
vo2Max: whole(_vo2),
|
||||||
trainingBackground: _background,
|
trainingBackground: _background,
|
||||||
typicalDay: _day,
|
typicalDay: _day,
|
||||||
sleepGoal: _sleep,
|
sleepGoal: _sleep,
|
||||||
|
// Kept as it was where the form doesn't show the weight.
|
||||||
|
weightUnit: _shows(ProfileField.weight)
|
||||||
|
? _chosenUnit
|
||||||
|
: widget.profile.weightUnit,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/profile/data/profile_repository.dart';
|
import 'package:halkyon_train_flutter/src/feature/profile/data/profile_repository.dart';
|
||||||
|
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/weight/data/weight_repository.dart';
|
import 'package:halkyon_train_flutter/src/feature/weight/data/weight_repository.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
|
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
|
||||||
|
|
||||||
|
|
@ -13,12 +14,16 @@ final class const WeightsState({
|
||||||
|
|
||||||
/// Those that open on this device.
|
/// Those that open on this device.
|
||||||
final List<StoredWeight> weights = const [],
|
final List<StoredWeight> weights = const [],
|
||||||
|
|
||||||
|
/// What the person chose to see weights in, from their profile; null
|
||||||
|
/// where they chose none (their region's then) or it doesn't open here.
|
||||||
|
final WeightUnit? unit,
|
||||||
}) with Equatable {
|
}) with Equatable {
|
||||||
/// The weight measured last; null without one.
|
/// The weight measured last; null without one.
|
||||||
WeightContent? get latest => weights.lastOrNull?.content;
|
WeightContent? get latest => weights.lastOrNull?.content;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [loaded, weights];
|
List<Object?> get props => [loaded, weights, unit];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the person's weights, keeps one they enter and deletes one; a
|
/// Reads the person's weights, keeps one they enter and deletes one; a
|
||||||
|
|
@ -31,9 +36,11 @@ class WeightsCubit(
|
||||||
this : super(const WeightsState());
|
this : super(const WeightsState());
|
||||||
|
|
||||||
StreamSubscription<void>? _changes;
|
StreamSubscription<void>? _changes;
|
||||||
|
StreamSubscription<void>? _profileChanges;
|
||||||
|
|
||||||
Future<void> open() async {
|
Future<void> open() async {
|
||||||
_changes = _weights.changes.listen((_) => unawaited(_read()));
|
_changes = _weights.changes.listen((_) => unawaited(_read()));
|
||||||
|
_profileChanges = _profile.changes.listen((_) => unawaited(_read()));
|
||||||
await _read();
|
await _read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +61,7 @@ class WeightsCubit(
|
||||||
|
|
||||||
Future<void> _read() async {
|
Future<void> _read() async {
|
||||||
final all = await _weights.all();
|
final all = await _weights.all();
|
||||||
|
final profile = await _profile.read();
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
emit(
|
emit(
|
||||||
WeightsState(
|
WeightsState(
|
||||||
|
|
@ -62,6 +70,7 @@ class WeightsCubit(
|
||||||
for (final weight in all)
|
for (final weight in all)
|
||||||
if (weight.content != null) weight,
|
if (weight.content != null) weight,
|
||||||
],
|
],
|
||||||
|
unit: profile?.weightUnit,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +79,7 @@ class WeightsCubit(
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _changes?.cancel();
|
await _changes?.cancel();
|
||||||
|
await _profileChanges?.cancel();
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/weight/bloc/weights_cubit.dart';
|
import 'package:halkyon_train_flutter/src/feature/weight/bloc/weights_cubit.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
|
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/workout_chart.dart';
|
import 'package:halkyon_train_flutter/src/feature/workout/widget/workout_chart.dart';
|
||||||
|
|
@ -9,9 +10,28 @@ import 'package:intl/intl.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// [kilograms] with one decimal where it has one, in [locale]'s way.
|
/// [kilograms] in [unit], with one decimal where it has one, in [locale]'s
|
||||||
String kilogramsText(AppLocalizations l10n, String locale, double kilograms) =>
|
/// way.
|
||||||
l10n.bodyWeight(NumberFormat('0.#', locale).format(kilograms));
|
String weightText(
|
||||||
|
AppLocalizations l10n,
|
||||||
|
String locale,
|
||||||
|
double kilograms,
|
||||||
|
WeightUnit unit,
|
||||||
|
) {
|
||||||
|
final figure = NumberFormat(
|
||||||
|
'0.#',
|
||||||
|
locale,
|
||||||
|
).format(unit.fromKilograms(kilograms));
|
||||||
|
return switch (unit) {
|
||||||
|
WeightUnit.kilograms => l10n.bodyWeight(figure),
|
||||||
|
WeightUnit.pounds => l10n.bodyWeightPounds(figure),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What weights are shown in for [context]: the person's choice ([chosen]),
|
||||||
|
/// else their region's.
|
||||||
|
WeightUnit weightUnitOf(BuildContext context, WeightUnit? chosen) =>
|
||||||
|
chosen ?? WeightUnit.ofRegion(Localizations.localeOf(context).countryCode);
|
||||||
|
|
||||||
/// The person's weight on Profile's Progress (ADR 0030, D64): the latest
|
/// The person's weight on Profile's Progress (ADR 0030, D64): the latest
|
||||||
/// and when, its line over time (held, each weight and its day), a weight
|
/// and when, its line over time (held, each weight and its day), a weight
|
||||||
|
|
@ -23,6 +43,7 @@ class const WeightCard({super.key}) extends StatelessWidget {
|
||||||
final locale = Localizations.localeOf(context).toString();
|
final locale = Localizations.localeOf(context).toString();
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final state = context.watch<WeightsCubit>().state;
|
final state = context.watch<WeightsCubit>().state;
|
||||||
|
final unit = weightUnitOf(context, state.unit);
|
||||||
final latest = state.latest;
|
final latest = state.latest;
|
||||||
final day = DateFormat.yMMMd(locale);
|
final day = DateFormat.yMMMd(locale);
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
|
|
@ -60,7 +81,7 @@ class const WeightCard({super.key}) extends StatelessWidget {
|
||||||
)
|
)
|
||||||
else ...[
|
else ...[
|
||||||
Text(
|
Text(
|
||||||
kilogramsText(l10n, locale, latest.kilograms),
|
weightText(l10n, locale, latest.kilograms, unit),
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
),
|
),
|
||||||
|
|
@ -79,10 +100,12 @@ class const WeightCard({super.key}) extends StatelessWidget {
|
||||||
for (final weight in state.weights)
|
for (final weight in state.weights)
|
||||||
(
|
(
|
||||||
x: weight.content!.at.millisecondsSinceEpoch / 1000,
|
x: weight.content!.at.millisecondsSinceEpoch / 1000,
|
||||||
value: weight.content!.kilograms,
|
value: unit.fromKilograms(weight.content!.kilograms),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
label: (kilograms) => kilogramsText(l10n, locale, kilograms),
|
// The line's values are already in [unit].
|
||||||
|
label: (value) =>
|
||||||
|
weightText(l10n, locale, unit.toKilograms(value), unit),
|
||||||
xLabel: (seconds) => day.format(
|
xLabel: (seconds) => day.format(
|
||||||
DateTime.fromMillisecondsSinceEpoch(
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
(seconds * 1000).round(),
|
(seconds * 1000).round(),
|
||||||
|
|
@ -115,7 +138,9 @@ class const WeightsScreen({super.key}) extends StatelessWidget {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final locale = Localizations.localeOf(context).toString();
|
final locale = Localizations.localeOf(context).toString();
|
||||||
final when = DateFormat.yMMMd(locale).add_Hm();
|
final when = DateFormat.yMMMd(locale).add_Hm();
|
||||||
final weights = context.watch<WeightsCubit>().state.weights.reversed;
|
final state = context.watch<WeightsCubit>().state;
|
||||||
|
final unit = weightUnitOf(context, state.unit);
|
||||||
|
final weights = state.weights.reversed;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l10n.weightTitle),
|
title: Text(l10n.weightTitle),
|
||||||
|
|
@ -132,7 +157,7 @@ class const WeightsScreen({super.key}) extends StatelessWidget {
|
||||||
for (final weight in weights)
|
for (final weight in weights)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
kilogramsText(l10n, locale, weight.content!.kilograms),
|
weightText(l10n, locale, weight.content!.kilograms, unit),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
[
|
[
|
||||||
|
|
@ -157,16 +182,18 @@ class const WeightsScreen({super.key}) extends StatelessWidget {
|
||||||
/// keeps it.
|
/// keeps it.
|
||||||
Future<void> showWeightDialog(BuildContext context) async {
|
Future<void> showWeightDialog(BuildContext context) async {
|
||||||
final cubit = context.read<WeightsCubit>();
|
final cubit = context.read<WeightsCubit>();
|
||||||
|
final unit = weightUnitOf(context, cubit.state.unit);
|
||||||
final entered = await showDialog<(double, DateTime)>(
|
final entered = await showDialog<(double, DateTime)>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const _WeightDialog(),
|
builder: (context) => _WeightDialog(unit: unit),
|
||||||
);
|
);
|
||||||
if (entered case (final kilograms, final at)) {
|
if (entered case (final kilograms, final at)) {
|
||||||
await cubit.add(kilograms, at);
|
await cubit.add(kilograms, at);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class const _WeightDialog() extends StatefulWidget {
|
class const _WeightDialog({required final WeightUnit unit})
|
||||||
|
extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
State<_WeightDialog> createState() => _WeightDialogState();
|
State<_WeightDialog> createState() => _WeightDialogState();
|
||||||
}
|
}
|
||||||
|
|
@ -182,12 +209,15 @@ class _WeightDialogState() extends State<_WeightDialog> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The weight typed, a decimal comma taken as a point; null for one not
|
/// The weight typed in the dialog's unit, in kilograms, a decimal comma
|
||||||
/// a person's.
|
/// taken as a point; null for one not a person's (20 to 400 kg).
|
||||||
double? get _kilograms => switch (double.tryParse(
|
double? get _kilograms => switch (double.tryParse(
|
||||||
_field.text.trim().replaceAll(',', '.'),
|
_field.text.trim().replaceAll(',', '.'),
|
||||||
)) {
|
)) {
|
||||||
final kg? when kg >= 20 && kg <= 400 => kg,
|
final typed? => switch (widget.unit.toKilograms(typed)) {
|
||||||
|
final kg when kg >= 19.95 && kg <= 400 => kg,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -235,8 +265,16 @@ class _WeightDialogState() extends State<_WeightDialog> {
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.weightField,
|
labelText: l10n.weightField,
|
||||||
suffixText: 'kg',
|
suffixText: switch (widget.unit) {
|
||||||
errorText: _invalid ? l10n.weightInvalid : null,
|
WeightUnit.kilograms => l10n.profileWeightUnit,
|
||||||
|
WeightUnit.pounds => l10n.profileWeightUnitPounds,
|
||||||
|
},
|
||||||
|
errorText: _invalid
|
||||||
|
? switch (widget.unit) {
|
||||||
|
WeightUnit.kilograms => l10n.weightInvalid,
|
||||||
|
WeightUnit.pounds => l10n.weightInvalidPounds,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _save(),
|
onSubmitted: (_) => _save(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,13 @@ void main() {
|
||||||
/// Starts Train on a new install ([usedBefore] false), where nobody is
|
/// Starts Train on a new install ([usedBefore] false), where nobody is
|
||||||
/// signed in, or on one that chose its instance before, signed in unless
|
/// signed in, or on one that chose its instance before, signed in unless
|
||||||
/// [signedOut].
|
/// [signedOut].
|
||||||
|
/// Puts the test in a region weighing in kilograms (D64): the tests' own,
|
||||||
|
/// en_US, weighs in pounds.
|
||||||
|
void inKilograms(WidgetTester tester) {
|
||||||
|
tester.platformDispatcher.localesTestValue = const [Locale('en', 'GB')];
|
||||||
|
addTearDown(tester.platformDispatcher.clearLocalesTestValue);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> start(
|
Future<void> start(
|
||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
bool usedBefore = false,
|
bool usedBefore = false,
|
||||||
|
|
@ -1114,9 +1121,64 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testWidgets("Your body shows the weight in the region's unit until one is "
|
||||||
|
'picked, and one left alone stays as it was (D64)', (tester) async {
|
||||||
|
await tester.runAsync(
|
||||||
|
() => profile.save(const ProfileContent(weightKg: 64.5)),
|
||||||
|
);
|
||||||
|
await openBody(tester);
|
||||||
|
final weight = find.widgetWithText(TextFormField, 'Weight');
|
||||||
|
final units = find.byType(SegmentedButton<WeightUnit>);
|
||||||
|
final form = find
|
||||||
|
.descendant(
|
||||||
|
of: find.byType(ProfileForm),
|
||||||
|
matching: find.byType(Scrollable),
|
||||||
|
)
|
||||||
|
.first;
|
||||||
|
Future<void> save() async {
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Save'),
|
||||||
|
300,
|
||||||
|
scrollable: form,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Save'));
|
||||||
|
await tester.runAsync(pumpEventQueue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// en_US weighs in pounds; 64.5 kg is 142.2 lb, rounded.
|
||||||
|
expect(
|
||||||
|
find.descendant(of: weight, matching: find.text('142.2')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
await save();
|
||||||
|
expect(
|
||||||
|
await tester.runAsync(profile.read),
|
||||||
|
const ProfileContent(weightKg: 64.5),
|
||||||
|
reason: 'neither rounded through pounds nor a unit picked',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Your body'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(of: units, matching: find.text('kg')),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
find.descendant(of: weight, matching: find.text('64.5')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
await save();
|
||||||
|
expect(
|
||||||
|
await tester.runAsync(profile.read),
|
||||||
|
const ProfileContent(weightKg: 64.5, weightUnit: WeightUnit.kilograms),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('each figure is optional; those given are kept', (
|
testWidgets('each figure is optional; those given are kept', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
inKilograms(tester);
|
||||||
await openBody(tester);
|
await openBody(tester);
|
||||||
|
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
|
|
@ -1194,6 +1256,7 @@ void main() {
|
||||||
|
|
||||||
testWidgets("Profile's card shows the figures given, opens Your body, and "
|
testWidgets("Profile's card shows the figures given, opens Your body, and "
|
||||||
'follows a change (D60)', (tester) async {
|
'follows a change (D60)', (tester) async {
|
||||||
|
inKilograms(tester);
|
||||||
await tester.runAsync(
|
await tester.runAsync(
|
||||||
() => profile.save(
|
() => profile.save(
|
||||||
const ProfileContent(
|
const ProfileContent(
|
||||||
|
|
@ -1234,6 +1297,7 @@ void main() {
|
||||||
|
|
||||||
testWidgets('a weight entered on Progress shows on its card and in Your '
|
testWidgets('a weight entered on Progress shows on its card and in Your '
|
||||||
'body, and goes again from All (D64)', (tester) async {
|
'body, and goes again from All (D64)', (tester) async {
|
||||||
|
inKilograms(tester);
|
||||||
await start(tester, usedBefore: true, profile: profile);
|
await start(tester, usedBefore: true, profile: profile);
|
||||||
await openProgress(tester);
|
await openProgress(tester);
|
||||||
await tester.runAsync(pumpEventQueue);
|
await tester.runAsync(pumpEventQueue);
|
||||||
|
|
@ -1283,6 +1347,43 @@ void main() {
|
||||||
expect(find.text('71.4 kg'), findsNothing);
|
expect(find.text('71.4 kg'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('a person who weighs in pounds sees and enters pounds, and '
|
||||||
|
'kilograms are kept (D64)', (tester) async {
|
||||||
|
await tester.runAsync(
|
||||||
|
() => profile.save(
|
||||||
|
const ProfileContent(weightKg: 70, weightUnit: WeightUnit.pounds),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await start(tester, usedBefore: true, profile: profile);
|
||||||
|
await openProgress(tester);
|
||||||
|
await tester.runAsync(pumpEventQueue);
|
||||||
|
await tester.pump();
|
||||||
|
final card = find.byType(WeightCard);
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(of: card, matching: find.byTooltip('Add weight')),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(find.byType(TextField), '30');
|
||||||
|
await tester.tap(find.text('Save'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('A weight between 44 and 880 lb'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '165');
|
||||||
|
await tester.tap(find.text('Save'));
|
||||||
|
await tester.runAsync(pumpEventQueue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.runAsync(pumpEventQueue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
find.descendant(of: card, matching: find.text('165 lb')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
final saved = await tester.runAsync(profile.read);
|
||||||
|
expect(saved?.weightKg, closeTo(74.84, 0.01));
|
||||||
|
expect(saved?.weightUnit, WeightUnit.pounds);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets("without figures, Profile's card says what they're for", (
|
testWidgets("without figures, Profile's card says what they're for", (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
@ -1447,6 +1548,7 @@ void main() {
|
||||||
|
|
||||||
testWidgets('what the profile lacks is asked alone and needed; then '
|
testWidgets('what the profile lacks is asked alone and needed; then '
|
||||||
'kept, and the band set up', (tester) async {
|
'kept, and the band set up', (tester) async {
|
||||||
|
inKilograms(tester);
|
||||||
await tester.runAsync(
|
await tester.runAsync(
|
||||||
() => profile.save(
|
() => profile.save(
|
||||||
ProfileContent(
|
ProfileContent(
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ void main() {
|
||||||
trainingBackground: TrainingBackground.frequent,
|
trainingBackground: TrainingBackground.frequent,
|
||||||
typicalDay: TypicalDay.mostlyStanding,
|
typicalDay: TypicalDay.mostlyStanding,
|
||||||
sleepGoal: const Duration(hours: 7, minutes: 30),
|
sleepGoal: const Duration(hours: 7, minutes: 30),
|
||||||
|
weightUnit: WeightUnit.pounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
group('its payload', () {
|
group('its payload', () {
|
||||||
|
|
@ -66,6 +67,32 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('the weight unit', () {
|
||||||
|
test("is the person's choice, else their region's: pounds in the US, "
|
||||||
|
'Liberia and Myanmar', () {
|
||||||
|
expect(WeightUnit.ofRegion('US'), WeightUnit.pounds);
|
||||||
|
expect(WeightUnit.ofRegion('MM'), WeightUnit.pounds);
|
||||||
|
expect(WeightUnit.ofRegion('GB'), WeightUnit.kilograms);
|
||||||
|
expect(WeightUnit.ofRegion(null), WeightUnit.kilograms);
|
||||||
|
expect(
|
||||||
|
const ProfileContent(weightUnit: WeightUnit.kilograms)
|
||||||
|
.weightUnitIn('US'),
|
||||||
|
WeightUnit.kilograms,
|
||||||
|
);
|
||||||
|
expect(const ProfileContent().weightUnitIn('US'), WeightUnit.pounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('converts both ways, kilograms kept as they are', () {
|
||||||
|
expect(WeightUnit.pounds.fromKilograms(0.45359237), closeTo(1, 1e-12));
|
||||||
|
expect(WeightUnit.pounds.toKilograms(165), closeTo(74.8428, 1e-4));
|
||||||
|
expect(WeightUnit.kilograms.toKilograms(70), 70);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('goes with a new weight', () {
|
||||||
|
expect(full.withWeight(60).weightUnit, WeightUnit.pounds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('the age counts whole years, the birthday itself included', () {
|
test('the age counts whole years, the birthday itself included', () {
|
||||||
final born = ProfileContent(birthDate: DateTime.utc(1990, 4, 12));
|
final born = ProfileContent(birthDate: DateTime.utc(1990, 4, 12));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1663,9 +1663,16 @@ Strava. What was found, 27 Oct:
|
||||||
band's and the zones' figures work from. Health Connect's weight records are read
|
band's and the zones' figures work from. Health Connect's weight records are read
|
||||||
in when switched on in its screen (its own permission, asked as the others), each
|
in when switched on in its screen (its own permission, asked as the others), each
|
||||||
record once (its id kept, also after deletion), not Train's own, and not one
|
record once (its id kept, also after deletion), not Train's own, and not one
|
||||||
within 10 minutes and 0.1 kg of a weight entered here. **In kilograms alone**:
|
within 10 minutes and 0.1 kg of a weight entered here. **In kilograms or pounds**
|
||||||
Train is metric throughout and has no units setting yet; pounds come with one,
|
(the developer's call, 4 Nov 2026, weight alone): every weight is kept in
|
||||||
for every figure at once.
|
kilograms, and shown and entered in the person's unit, *Your body*'s *Weight in*
|
||||||
|
(kg | lb, the profile's `weightUnit`), else their region's (pounds in the US,
|
||||||
|
Liberia and Myanmar, `WeightUnit.ofRegion`, from the app's locale); a pound is
|
||||||
|
0.45359237 kg, the entry's range the same 20 to 400 kg (44 to 880 lb). A weight
|
||||||
|
left untouched in *Your body* is saved as it was, not rounded through pounds.
|
||||||
|
The other figures stay metric; a units setting for all of them would come on
|
||||||
|
its own. A profile saved on Train 0.2.0, which doesn't know the field, loses
|
||||||
|
the unit, and the region's applies again until it is picked anew.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue