From 5ae30e67883799d1f53a4382b5ac3df96b6e17a7 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 19 May 2024 08:52:20 +0200 Subject: [PATCH 1/8] cleaner way to show line of macros --- lib/widgets/dashboard/widgets.dart | 19 ++----------------- lib/widgets/nutrition/helpers.dart | 9 +++++++++ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 93297d4a..4d37b167 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -42,6 +42,7 @@ import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/measurements/forms.dart'; import 'package:wger/widgets/nutrition/charts.dart'; import 'package:wger/widgets/nutrition/forms.dart'; +import 'package:wger/widgets/nutrition/helpers.dart'; import 'package:wger/widgets/weight/forms.dart'; import 'package:wger/widgets/workouts/forms.dart'; @@ -83,23 +84,7 @@ class _DashboardNutritionWidgetState extends State { //textAlign: TextAlign.left, ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.min, - children: [ - MutedText( - '${AppLocalizations.of(context).energyShort} ${meal.plannedNutritionalValues.energy.toStringAsFixed(0)}${AppLocalizations.of(context).kcal}'), - const MutedText(' / '), - MutedText( - '${AppLocalizations.of(context).proteinShort} ${meal.plannedNutritionalValues.protein.toStringAsFixed(0)}${AppLocalizations.of(context).g}'), - const MutedText(' / '), - MutedText( - '${AppLocalizations.of(context).carbohydratesShort} ${meal.plannedNutritionalValues.carbohydrates.toStringAsFixed(0)}${AppLocalizations.of(context).g}'), - const MutedText(' / '), - MutedText( - '${AppLocalizations.of(context).fatShort} ${meal.plannedNutritionalValues.fat.toStringAsFixed(0)}${AppLocalizations.of(context).g} '), - ], - ), + MutedText(getShortNutritionValues(meal.plannedNutritionalValues, context)), IconButton( icon: const Icon(Icons.history_edu), color: wgerPrimaryButtonColor, diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 222e8765..766386b1 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -40,3 +40,12 @@ List getMutedNutritionalValues(NutritionalValues values, BuildContext co textAlign: TextAlign.right, ), ]; + +String getShortNutritionValues(NutritionalValues values, BuildContext context) { + final loc = AppLocalizations.of(context); + final e = '${loc.energyShort} ${loc.kcalValue(values.energy.toStringAsFixed(0))}'; + final p = '${loc.proteinShort} ${loc.gValue(values.protein.toStringAsFixed(0))}'; + final c = '${loc.carbohydratesShort} ${loc.gValue(values.carbohydrates.toStringAsFixed(0))}'; + final f = '${loc.fatShort} ${loc.gValue(values.fat.toStringAsFixed(0))}'; + return '$e / $p / $c / $f'; +} From ad4543f2e671205f8f6dc3642b0b1e12d92d70b2 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 19 May 2024 09:12:59 +0200 Subject: [PATCH 2/8] small refactor --- lib/models/nutrition/ingredient.dart | 14 ++++++++++++++ lib/models/nutrition/log.dart | 13 +------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/models/nutrition/ingredient.dart b/lib/models/nutrition/ingredient.dart index 59a3460f..87684c46 100644 --- a/lib/models/nutrition/ingredient.dart +++ b/lib/models/nutrition/ingredient.dart @@ -18,6 +18,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/models/nutrition/image.dart'; +import 'package:wger/models/nutrition/nutritional_values.dart'; part 'ingredient.g.dart'; @@ -91,4 +92,17 @@ class Ingredient { factory Ingredient.fromJson(Map json) => _$IngredientFromJson(json); Map toJson() => _$IngredientToJson(this); + + NutritionalValues get nutritionalValues { + return NutritionalValues.values( + energy * 1, + protein * 1, + carbohydrates * 1, + carbohydratesSugar * 1, + fat * 1, + fatSaturated * 1, + fibres * 1, + sodium * 1, + ); + } } diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index 730763cc..ac4aa845 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -83,21 +83,10 @@ class Log { /// Calculations NutritionalValues get nutritionalValues { // This is already done on the server. It might be better to read it from there. - final out = NutritionalValues(); - //final weight = amount; final weight = weightUnitObj == null ? amount : amount * weightUnitObj!.amount * weightUnitObj!.grams; - out.energy = ingredient.energy * weight / 100; - out.protein = ingredient.protein * weight / 100; - out.carbohydrates = ingredient.carbohydrates * weight / 100; - out.carbohydratesSugar = ingredient.carbohydratesSugar * weight / 100; - out.fat = ingredient.fat * weight / 100; - out.fatSaturated = ingredient.fatSaturated * weight / 100; - out.fibres = ingredient.fibres * weight / 100; - out.sodium = ingredient.sodium * weight / 100; - - return out; + return ingredient.nutritionalValues / (100 / weight); } } From 63d37175c4377f75b3564e15936a2b12ff69c385 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 19 May 2024 09:35:07 +0200 Subject: [PATCH 3/8] show reminder of macro's on ingredient tiles --- lib/widgets/nutrition/forms.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 6bf3fcb0..2280380c 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -28,6 +28,7 @@ import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/screens/nutritional_plan_screen.dart'; +import 'package:wger/widgets/nutrition/helpers.dart'; import 'package:wger/widgets/nutrition/widgets.dart'; class MealForm extends StatelessWidget { @@ -210,8 +211,10 @@ class MealItemForm extends StatelessWidget { _mealItem.ingredientId = _listMealItems[index].ingredientId; _mealItem.amount = _listMealItems[index].amount; }, - title: Text(_listMealItems[index].ingredient.name), - subtitle: Text('${_listMealItems[index].amount.toStringAsFixed(0)}$unit'), + title: Text( + '${_listMealItems[index].ingredient.name} (${_listMealItems[index].amount.toStringAsFixed(0)}$unit)'), + subtitle: Text(getShortNutritionValues( + _listMealItems[index].ingredient.nutritionalValues, context)), trailing: const Icon(Icons.copy), ), ); @@ -376,8 +379,10 @@ class IngredientLogForm extends StatelessWidget { _mealItem.ingredientId = diaryEntries[index].ingredientId; _mealItem.amount = diaryEntries[index].amount; }, - title: Text(_plan.diaryEntries[index].ingredient.name), - subtitle: Text('${diaryEntries[index].amount.toStringAsFixed(0)}$unit'), + title: Text( + '${diaryEntries[index].ingredient.name} (${diaryEntries[index].amount.toStringAsFixed(0)}$unit)'), + subtitle: Text(getShortNutritionValues( + diaryEntries[index].ingredient.nutritionalValues, context)), trailing: const Icon(Icons.copy), ), ); From 1c84d918348135cb48777c75c40f5ceeef38afa5 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 09:24:07 +0200 Subject: [PATCH 4/8] convert form to stateful, show ingredient preview --- lib/widgets/nutrition/forms.dart | 109 ++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 25 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 2280380c..a3dc37ca 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -23,6 +23,7 @@ import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/ui.dart'; +import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; @@ -228,19 +229,27 @@ class MealItemForm extends StatelessWidget { } } -class IngredientLogForm extends StatelessWidget { - late MealItem _mealItem; +class IngredientLogForm extends StatefulWidget { final NutritionalPlan _plan; + const IngredientLogForm(this._plan); + @override + State createState() => _IngredientLogFormState(); +} + +class _IngredientLogFormState extends State { final _form = GlobalKey(); final _ingredientController = TextEditingController(); final _ingredientIdController = TextEditingController(); final _amountController = TextEditingController(); final _dateController = TextEditingController(); final _timeController = TextEditingController(); + final _mealItem = MealItem.empty(); - IngredientLogForm(this._plan) { - _mealItem = MealItem.empty(); + bool validIngredientId = false; + @override + void initState() { + super.initState(); final now = DateTime.now(); _dateController.text = toDate(now)!; _timeController.text = timeToString(TimeOfDay.fromDateTime(now))!; @@ -248,7 +257,7 @@ class IngredientLogForm extends StatelessWidget { @override Widget build(BuildContext context) { - final diaryEntries = _plan.diaryEntries; + final diaryEntries = widget._plan.diaryEntries; final String unit = AppLocalizations.of(context).g; return Container( @@ -261,25 +270,32 @@ class IngredientLogForm extends StatelessWidget { _ingredientIdController, _ingredientController, ), - TextFormField( - decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), - controller: _amountController, - keyboardType: TextInputType.number, - onFieldSubmitted: (_) {}, - onSaved: (newValue) { - _mealItem.amount = double.parse(newValue!); - }, - validator: (value) { - try { - double.parse(value!); - } catch (error) { - return AppLocalizations.of(context).enterValidNumber; - } - return null; - }, - ), Row( children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), + controller: _amountController, + keyboardType: TextInputType.number, + onFieldSubmitted: (_) {}, + onChanged: (value) { + setState(() { + _mealItem.amount = double.tryParse(value) ?? _mealItem.amount; + }); + }, + onSaved: (value) { + _mealItem.amount = double.parse(value!); + }, + validator: (value) { + try { + double.parse(value!); + } catch (error) { + return AppLocalizations.of(context).enterValidNumber; + } + return null; + }, + ), + ), Expanded( child: TextFormField( readOnly: true, @@ -337,6 +353,46 @@ class IngredientLogForm extends StatelessWidget { ), ], ), + if (validIngredientId) + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + 'Macros preview', + style: Theme.of(context).textTheme.titleMedium, + ), + FutureBuilder( + future: Provider.of(context, listen: false) + .fetchIngredient(_mealItem.ingredientId), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + _mealItem.ingredient = snapshot.data!; + return Padding( + padding: const EdgeInsets.only(top: 16), + child: + Text(getShortNutritionValues(_mealItem.nutritionalValues, context)), + ); + } else if (snapshot.hasError) { + return Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + 'Ingredient lookup error: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + ), + ); + } else { + return const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(), + ); + } + }, + ), + ], + ), + ), ElevatedButton( child: Text(AppLocalizations.of(context).save), onPressed: () async { @@ -351,7 +407,7 @@ class IngredientLogForm extends StatelessWidget { final tod = stringToTime(_timeController.text); date = DateTime(date.year, date.month, date.day, tod.hour, tod.minute); Provider.of(context, listen: false) - .logIngredientToDiary(_mealItem, _plan.id!, date); + .logIngredientToDiary(_mealItem, widget._plan.id!, date); } on WgerHttpException catch (error) { showHttpExceptionErrorDialog(error, context); } catch (error) { @@ -376,8 +432,11 @@ class IngredientLogForm extends StatelessWidget { _ingredientController.text = diaryEntries[index].ingredient.name; _ingredientIdController.text = diaryEntries[index].ingredient.id.toString(); _amountController.text = diaryEntries[index].amount.toStringAsFixed(0); - _mealItem.ingredientId = diaryEntries[index].ingredientId; - _mealItem.amount = diaryEntries[index].amount; + setState(() { + _mealItem.ingredientId = diaryEntries[index].ingredientId; + _mealItem.amount = diaryEntries[index].amount; + validIngredientId = true; + }); }, title: Text( '${diaryEntries[index].ingredient.name} (${diaryEntries[index].amount.toStringAsFixed(0)}$unit)'), From 58122d40b5373a2a7c6c72a48f28178901531b4b Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 11:06:14 +0200 Subject: [PATCH 5/8] get ready to support showing images in the ingredient preview note: the image doesn't seem to be set yet, but at least the UI is now ready for it --- lib/widgets/nutrition/forms.dart | 6 +++--- lib/widgets/nutrition/meal.dart | 28 ++-------------------------- lib/widgets/nutrition/widgets.dart | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index a3dc37ca..763d9dfd 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -368,9 +368,9 @@ class _IngredientLogFormState extends State { builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { _mealItem.ingredient = snapshot.data!; - return Padding( - padding: const EdgeInsets.only(top: 16), - child: + return ListTile( + leading: IngredientAvatar(ingredient: _mealItem.ingredient), + title: Text(getShortNutritionValues(_mealItem.nutritionalValues, context)), ); } else if (snapshot.hasError) { diff --git a/lib/widgets/nutrition/meal.dart b/lib/widgets/nutrition/meal.dart index 3048bed0..253fde07 100644 --- a/lib/widgets/nutrition/meal.dart +++ b/lib/widgets/nutrition/meal.dart @@ -20,13 +20,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; -import 'package:wger/helpers/misc.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/screens/form_screen.dart'; -import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/nutrition/charts.dart'; import 'package:wger/widgets/nutrition/forms.dart'; import 'package:wger/widgets/nutrition/helpers.dart'; @@ -208,18 +206,7 @@ class MealItemWidget extends StatelessWidget { final values = _item.nutritionalValues; return ListTile( - leading: _item.ingredient.image != null - ? GestureDetector( - child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredient.image!.image)), - onTap: () async { - if (_item.ingredient.image!.objectUrl != '') { - return launchURL(_item.ingredient.image!.objectUrl, context); - } else { - return; - } - }, - ) - : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)), + leading: IngredientAvatar(ingredient: _item.ingredient), title: Text( '${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}', overflow: TextOverflow.ellipsis, @@ -273,18 +260,7 @@ class LogDiaryItemWidget extends StatelessWidget { final values = _item.nutritionalValues; return ListTile( - leading: _item.ingredient.image != null - ? GestureDetector( - child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredient.image!.image)), - onTap: () async { - if (_item.ingredient.image!.objectUrl != '') { - return launchURL(_item.ingredient.image!.objectUrl, context); - } else { - return; - } - }, - ) - : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)), + leading: IngredientAvatar(ingredient: _item.ingredient), title: Text( '${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}', overflow: TextOverflow.ellipsis, diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index c307bb0e..0cc2d24c 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -25,9 +25,11 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; +import 'package:wger/helpers/misc.dart'; import 'package:wger/helpers/platform.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/models/exercises/ingredient_api.dart'; +import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/providers/nutrition.dart'; @@ -335,3 +337,23 @@ class NutritionDiaryEntry extends StatelessWidget { ); } } + +class IngredientAvatar extends StatelessWidget { + final Ingredient ingredient; + + const IngredientAvatar({super.key, required this.ingredient}); + + @override + Widget build(BuildContext context) { + return ingredient.image != null + ? GestureDetector( + child: CircleAvatar(backgroundImage: NetworkImage(ingredient.image!.image)), + onTap: () async { + if (ingredient.image!.objectUrl != '') { + return launchURL(ingredient.image!.objectUrl, context); + } + }, + ) + : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)); + } +} From 74002350ba036d445cbb46127304133e1d5b9dbb Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 13:16:27 +0200 Subject: [PATCH 6/8] unify IngredientLogForm and MealItemForm --- lib/widgets/nutrition/forms.dart | 304 +++++++----------- .../nutritional_meal_item_form_test.dart | 6 +- 2 files changed, 122 insertions(+), 188 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 763d9dfd..5d4b21d5 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -24,6 +24,7 @@ import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/models/nutrition/ingredient.dart'; +import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; @@ -115,135 +116,57 @@ class MealForm extends StatelessWidget { } } -class MealItemForm extends StatelessWidget { - final Meal _meal; - late final MealItem _mealItem; - final List _listMealItems; - late String _barcode; - late bool _test; - - final _form = GlobalKey(); - final _ingredientIdController = TextEditingController(); - final _ingredientController = TextEditingController(); - final _amountController = TextEditingController(); - - MealItemForm(this._meal, this._listMealItems, [mealItem, code, test]) { - _mealItem = mealItem ?? MealItem.empty(); - _test = test ?? false; - _barcode = code ?? ''; - _mealItem.mealId = _meal.id!; - } - - TextEditingController get ingredientIdController => _ingredientIdController; - - MealItem get mealItem => _mealItem; - - @override - Widget build(BuildContext context) { - final String unit = AppLocalizations.of(context).g; - return Container( - margin: const EdgeInsets.all(20), - child: Form( - key: _form, - child: Column( - children: [ - IngredientTypeahead( - _ingredientIdController, - _ingredientController, - barcode: _barcode, - test: _test, - ), - TextFormField( - key: const Key('field-weight'), - decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), - controller: _amountController, - keyboardType: TextInputType.number, - onFieldSubmitted: (_) {}, - onSaved: (newValue) { - _mealItem.amount = double.parse(newValue!); - }, - validator: (value) { - try { - double.parse(value!); - } catch (error) { - return AppLocalizations.of(context).enterValidNumber; - } - return null; - }, - ), - ElevatedButton( - key: const Key(SUBMIT_BUTTON_KEY_NAME), - child: Text(AppLocalizations.of(context).save), - onPressed: () async { - if (!_form.currentState!.validate()) { - return; - } - _form.currentState!.save(); - _mealItem.ingredientId = int.parse(_ingredientIdController.text); - - try { - Provider.of(context, listen: false) - .addMealItem(_mealItem, _meal); - } on WgerHttpException catch (error) { - showHttpExceptionErrorDialog(error, context); - } catch (error) { - showErrorDialog(error, context); - } - Navigator.of(context).pop(); - }, - ), - if (_listMealItems.isNotEmpty) const SizedBox(height: 10.0), - Container( - padding: const EdgeInsets.all(10.0), - child: Text(AppLocalizations.of(context).recentlyUsedIngredients), - ), - Expanded( - child: ListView.builder( - itemCount: _listMealItems.length, - shrinkWrap: true, - itemBuilder: (context, index) { - return Card( - child: ListTile( - onTap: () { - _ingredientController.text = _listMealItems[index].ingredient.name; - _ingredientIdController.text = - _listMealItems[index].ingredient.id.toString(); - _amountController.text = _listMealItems[index].amount.toStringAsFixed(0); - _mealItem.ingredientId = _listMealItems[index].ingredientId; - _mealItem.amount = _listMealItems[index].amount; - }, - title: Text( - '${_listMealItems[index].ingredient.name} (${_listMealItems[index].amount.toStringAsFixed(0)}$unit)'), - subtitle: Text(getShortNutritionValues( - _listMealItems[index].ingredient.nutritionalValues, context)), - trailing: const Icon(Icons.copy), - ), - ); - }, - ), - ) - ], - ), - ), - ); - } +Widget MealItemForm(Meal meal, List recent, [String? barcode, bool? test]) { + return IngredientForm( + // TODO we use planId 0 here cause we don't have one and we don't need it I think? + recent: recent.map((e) => Log.fromMealItem(e, 0, e.mealId)).toList(), + onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { + mealItem.mealId = meal.id!; + Provider.of(context, listen: false).addMealItem(mealItem, meal); + }, + barcode: barcode ?? '', + test: test ?? false, + withDate: false); } -class IngredientLogForm extends StatefulWidget { - final NutritionalPlan _plan; - const IngredientLogForm(this._plan); - - @override - State createState() => _IngredientLogFormState(); +Widget IngredientLogForm(NutritionalPlan plan) { + return IngredientForm( + recent: plan.diaryEntries, + onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { + Provider.of(context, listen: false) + .logIngredientToDiary(mealItem, plan.id!, dt); + }, + withDate: true); } -class _IngredientLogFormState extends State { +/// IngredientForm is a form that lets the user pick an ingredient (and amount) to +/// log to the diary or to add to a meal. +class IngredientForm extends StatefulWidget { + final Function(BuildContext context, MealItem mealItem, DateTime? dt) onSave; + final List recent; + final bool withDate; + final String barcode; + final bool test; + + const IngredientForm({ + required this.recent, + required this.onSave, + required this.withDate, + this.barcode = '', + this.test = false, + }); + + @override + State createState() => _IngredientFormState(); +} + +class _IngredientFormState extends State { final _form = GlobalKey(); final _ingredientController = TextEditingController(); final _ingredientIdController = TextEditingController(); final _amountController = TextEditingController(); - final _dateController = TextEditingController(); - final _timeController = TextEditingController(); + final _dateController = TextEditingController(); // optional + final _timeController = TextEditingController(); // optional final _mealItem = MealItem.empty(); bool validIngredientId = false; @@ -255,9 +178,12 @@ class _IngredientLogFormState extends State { _timeController.text = timeToString(TimeOfDay.fromDateTime(now))!; } + TextEditingController get ingredientIdController => _ingredientIdController; + + MealItem get mealItem => _mealItem; + @override Widget build(BuildContext context) { - final diaryEntries = widget._plan.diaryEntries; final String unit = AppLocalizations.of(context).g; return Container( @@ -269,11 +195,14 @@ class _IngredientLogFormState extends State { IngredientTypeahead( _ingredientIdController, _ingredientController, + barcode: widget.barcode, + test: widget.test, ), Row( children: [ Expanded( child: TextFormField( + key: const Key('field-weight'), // needed ? decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), controller: _amountController, keyboardType: TextInputType.number, @@ -296,61 +225,63 @@ class _IngredientLogFormState extends State { }, ), ), - Expanded( - child: TextFormField( - readOnly: true, - // Stop keyboard from appearing - decoration: InputDecoration( - labelText: AppLocalizations.of(context).date, - // suffixIcon: const Icon(Icons.calendar_today), - ), - enableInteractiveSelection: false, - controller: _dateController, - onTap: () async { - // Show Date Picker Here - final pickedDate = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(DateTime.now().year - 10), - lastDate: DateTime.now(), - ); - - if (pickedDate != null) { - _dateController.text = toDate(pickedDate)!; - } - }, - onSaved: (newValue) { - _dateController.text = newValue!; - }, - ), - ), - Expanded( - child: TextFormField( - key: const Key('field-time'), - decoration: InputDecoration( - labelText: AppLocalizations.of(context).time, - //suffixIcon: const Icon(Icons.punch_clock) - ), - controller: _timeController, - onTap: () async { + if (widget.withDate) + Expanded( + child: TextFormField( + readOnly: true, // Stop keyboard from appearing - FocusScope.of(context).requestFocus(FocusNode()); + decoration: InputDecoration( + labelText: AppLocalizations.of(context).date, + // suffixIcon: const Icon(Icons.calendar_today), + ), + enableInteractiveSelection: false, + controller: _dateController, + onTap: () async { + // Show Date Picker Here + final pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(DateTime.now().year - 10), + lastDate: DateTime.now(), + ); - // Open time picker - final pickedTime = await showTimePicker( - context: context, - initialTime: stringToTime(_timeController.text), - ); - if (pickedTime != null) { - _timeController.text = timeToString(pickedTime)!; - } - }, - onSaved: (newValue) { - _timeController.text = newValue!; - }, - onFieldSubmitted: (_) {}, + if (pickedDate != null) { + _dateController.text = toDate(pickedDate)!; + } + }, + onSaved: (newValue) { + _dateController.text = newValue!; + }, + ), + ), + if (widget.withDate) + Expanded( + child: TextFormField( + key: const Key('field-time'), + decoration: InputDecoration( + labelText: AppLocalizations.of(context).time, + //suffixIcon: const Icon(Icons.punch_clock) + ), + controller: _timeController, + onTap: () async { + // Stop keyboard from appearing + FocusScope.of(context).requestFocus(FocusNode()); + + // Open time picker + final pickedTime = await showTimePicker( + context: context, + initialTime: stringToTime(_timeController.text), + ); + if (pickedTime != null) { + _timeController.text = timeToString(pickedTime)!; + } + }, + onSaved: (newValue) { + _timeController.text = newValue!; + }, + onFieldSubmitted: (_) {}, + ), ), - ), ], ), if (validIngredientId) @@ -394,6 +325,9 @@ class _IngredientLogFormState extends State { ), ), ElevatedButton( + key: const Key( + SUBMIT_BUTTON_KEY_NAME), // needed? mealItemForm had it, but not ingredientlogform + child: Text(AppLocalizations.of(context).save), onPressed: () async { if (!_form.currentState!.validate()) { @@ -406,8 +340,7 @@ class _IngredientLogFormState extends State { var date = DateTime.parse(_dateController.text); final tod = stringToTime(_timeController.text); date = DateTime(date.year, date.month, date.day, tod.hour, tod.minute); - Provider.of(context, listen: false) - .logIngredientToDiary(_mealItem, widget._plan.id!, date); + widget.onSave(context, _mealItem, date); } on WgerHttpException catch (error) { showHttpExceptionErrorDialog(error, context); } catch (error) { @@ -416,32 +349,33 @@ class _IngredientLogFormState extends State { Navigator.of(context).pop(); }, ), - if (diaryEntries.isNotEmpty) const SizedBox(height: 10.0), + if (widget.recent.isNotEmpty) const SizedBox(height: 10.0), Container( padding: const EdgeInsets.all(10.0), child: Text(AppLocalizations.of(context).recentlyUsedIngredients), ), Expanded( child: ListView.builder( - itemCount: diaryEntries.length, + itemCount: widget.recent.length, shrinkWrap: true, itemBuilder: (context, index) { return Card( child: ListTile( onTap: () { - _ingredientController.text = diaryEntries[index].ingredient.name; - _ingredientIdController.text = diaryEntries[index].ingredient.id.toString(); - _amountController.text = diaryEntries[index].amount.toStringAsFixed(0); + _ingredientController.text = widget.recent[index].ingredient.name; + _ingredientIdController.text = + widget.recent[index].ingredient.id.toString(); + _amountController.text = widget.recent[index].amount.toStringAsFixed(0); setState(() { - _mealItem.ingredientId = diaryEntries[index].ingredientId; - _mealItem.amount = diaryEntries[index].amount; + _mealItem.ingredientId = widget.recent[index].ingredientId; + _mealItem.amount = widget.recent[index].amount; validIngredientId = true; }); }, title: Text( - '${diaryEntries[index].ingredient.name} (${diaryEntries[index].amount.toStringAsFixed(0)}$unit)'), + '${widget.recent[index].ingredient.name} (${widget.recent[index].amount.toStringAsFixed(0)}$unit)'), subtitle: Text(getShortNutritionValues( - diaryEntries[index].ingredient.nutritionalValues, context)), + widget.recent[index].ingredient.nutritionalValues, context)), trailing: const Icon(Icons.copy), ), ); diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 50561722..2a751a6d 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -101,7 +101,7 @@ void main() { home: Scaffold( body: Scrollable( viewportBuilder: (BuildContext context, ViewportOffset position) => - MealItemForm(meal, const [], null, code, test), + MealItemForm(meal, const [], code, test), ), ), routes: { @@ -213,7 +213,7 @@ void main() { testWidgets('confirm found ingredient dialog', (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final MealItemForm formScreen = tester.widget(find.byType(MealItemForm)); + final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); @@ -293,7 +293,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final MealItemForm formScreen = tester.widget(find.byType(MealItemForm)); + final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); From d4d298ad9e9f639097f26e71b6773de7a0acd493 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 14:13:00 +0200 Subject: [PATCH 7/8] fix tests --- lib/widgets/nutrition/forms.dart | 9 ++++++--- test/nutrition/nutritional_meal_item_form_test.dart | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 5d4b21d5..f5b2e7c1 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -157,10 +157,10 @@ class IngredientForm extends StatefulWidget { }); @override - State createState() => _IngredientFormState(); + State createState() => IngredientFormState(); } -class _IngredientFormState extends State { +class IngredientFormState extends State { final _form = GlobalKey(); final _ingredientController = TextEditingController(); final _ingredientIdController = TextEditingController(); @@ -209,7 +209,10 @@ class _IngredientFormState extends State { onFieldSubmitted: (_) {}, onChanged: (value) { setState(() { - _mealItem.amount = double.tryParse(value) ?? _mealItem.amount; + final v = double.tryParse(value); + if (v != null) { + _mealItem.amount = v; + } }); }, onSaved: (value) { diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 2a751a6d..aa739ba1 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -213,7 +213,7 @@ void main() { testWidgets('confirm found ingredient dialog', (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); + final IngredientFormState formState = tester.state(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); @@ -223,7 +223,7 @@ void main() { await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); await tester.pumpAndSettle(); - expect(formScreen.ingredientIdController.text, '1'); + expect(formState.ingredientIdController.text, '1'); }); testWidgets('close found ingredient dialog', (WidgetTester tester) async { @@ -293,7 +293,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); + final IngredientFormState formState = tester.state(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); @@ -303,7 +303,7 @@ void main() { await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); await tester.pumpAndSettle(); - expect(formScreen.ingredientIdController.text, '1'); + expect(formState.ingredientIdController.text, '1'); await tester.enterText(find.byKey(const Key('field-weight')), '2'); await tester.pumpAndSettle(); @@ -313,7 +313,7 @@ void main() { await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME))); await tester.pumpAndSettle(); - expect(formScreen.mealItem.amount, 2); + expect(formState.mealItem.amount, 2); verify(mockNutrition.addMealItem(any, meal1)); }); From eeec221499152e8c74f1e431a06c80d8cbfef85b Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 14:22:10 +0200 Subject: [PATCH 8/8] add note --- lib/models/nutrition/meal_item.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/models/nutrition/meal_item.dart b/lib/models/nutrition/meal_item.dart index d484283f..4bf10e94 100644 --- a/lib/models/nutrition/meal_item.dart +++ b/lib/models/nutrition/meal_item.dart @@ -72,6 +72,7 @@ class MealItem { Map toJson() => _$MealItemToJson(this); /// Calculations + /// TODO why does this not consider weightUnitObj ? should we do the same as Log.nutritionalValues here? NutritionalValues get nutritionalValues { // This is already done on the server. It might be better to read it from there. final out = NutritionalValues();