diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index c2a2bb05..da61afc6 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -169,7 +169,6 @@ class IngredientFormState extends State { final _timeController = TextEditingController(); // optional final _mealItem = MealItem.empty(); - bool validIngredientId = false; @override void initState() { super.initState(); @@ -182,6 +181,26 @@ class IngredientFormState extends State { MealItem get mealItem => _mealItem; + void selectIngredient(int id, String name, num? amount) { + setState(() { + _mealItem.ingredientId = id; + _ingredientController.text = name; + _ingredientIdController.text = id.toString(); + if (amount != null) { + _amountController.text = amount.toStringAsFixed(0); + _mealItem.amount = amount; + } + }); + } + +// note: does not reset text search and amount inputs + void unSelectIngredient() { + setState(() { + _mealItem.ingredientId = 0; + _ingredientIdController.text = ''; + }); + } + @override Widget build(BuildContext context) { final String unit = AppLocalizations.of(context).g; @@ -197,6 +216,8 @@ class IngredientFormState extends State { _ingredientController, barcode: widget.barcode, test: widget.test, + selectIngredient: selectIngredient, + unSelectIngredient: unSelectIngredient, ), Row( children: [ @@ -287,7 +308,7 @@ class IngredientFormState extends State { ), ], ), - if (validIngredientId) + if (ingredientIdController.text.isNotEmpty && _amountController.text.isNotEmpty) Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -302,10 +323,9 @@ class IngredientFormState extends State { builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { _mealItem.ingredient = snapshot.data!; - return ListTile( - leading: IngredientAvatar(ingredient: _mealItem.ingredient), - title: - Text(getShortNutritionValues(_mealItem.nutritionalValues, context)), + return MealItemTile( + ingredient: _mealItem.ingredient, + nutritionalValues: _mealItem.nutritionalValues, ); } else if (snapshot.hasError) { return Padding( @@ -328,9 +348,7 @@ class IngredientFormState extends State { ), ), ElevatedButton( - key: const Key( - SUBMIT_BUTTON_KEY_NAME), // needed? mealItemForm had it, but not ingredientlogform - + key: const Key(SUBMIT_BUTTON_KEY_NAME), child: Text(AppLocalizations.of(context).save), onPressed: () async { if (!_form.currentState!.validate()) { @@ -365,15 +383,9 @@ class IngredientFormState extends State { return Card( child: ListTile( onTap: () { - _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 = widget.recent[index].ingredientId; - _mealItem.amount = widget.recent[index].amount; - validIngredientId = true; - }); + final ingredient = widget.recent[index].ingredient; + selectIngredient( + ingredient.id, ingredient.name, widget.recent[index].amount); }, title: Text( '${widget.recent[index].ingredient.name} (${widget.recent[index].amount.toStringAsFixed(0)}$unit)'), diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index 0cc2d24c..35d335c5 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -32,6 +32,7 @@ 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/models/nutrition/nutritional_values.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/nutrition/helpers.dart'; @@ -62,12 +63,17 @@ class IngredientTypeahead extends StatefulWidget { final bool? test; final bool showScanner; + final Function(int id, String name, num? amount) selectIngredient; + final Function() unSelectIngredient; + const IngredientTypeahead( this._ingredientIdController, this._ingredientController, { this.showScanner = true, this.test = false, this.barcode = '', + required this.selectIngredient, + required this.unSelectIngredient, }); @override @@ -118,6 +124,10 @@ class _IngredientTypeaheadState extends State { } return null; }, + onChanged: (value) { + // unselect to start a new search + widget.unSelectIngredient(); + }, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), labelText: AppLocalizations.of(context).searchIngredient, @@ -126,7 +136,8 @@ class _IngredientTypeaheadState extends State { ); }, suggestionsCallback: (pattern) { - if (pattern == '') { + // don't do search if user has already loaded a specific item + if (pattern == '' || widget._ingredientIdController.text.isNotEmpty) { return null; } @@ -151,10 +162,7 @@ class _IngredientTypeaheadState extends State { child: child, ), onSelected: (suggestion) { - //SuggestionsController.of(context).; - - widget._ingredientIdController.text = suggestion.data.id.toString(); - widget._ingredientController.text = suggestion.value; + widget.selectIngredient(suggestion.data.id, suggestion.value, null); }, ), if (Localizations.localeOf(context).languageCode != LANGUAGE_SHORT_ENGLISH) @@ -188,6 +196,7 @@ class _IngredientTypeaheadState extends State { } final result = await Provider.of(context, listen: false) .searchIngredientWithCode(barcode); + // TODO: show spinner... if (!mounted) { return; } @@ -198,14 +207,22 @@ class _IngredientTypeaheadState extends State { builder: (ctx) => AlertDialog( key: const Key('found-dialog'), title: Text(AppLocalizations.of(context).productFound), - content: Text(AppLocalizations.of(context).productFoundDescription(result.name)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context).productFoundDescription(result.name)), + MealItemTile( + ingredient: result, + nutritionalValues: result.nutritionalValues, + ), + ], + ), actions: [ TextButton( key: const Key('found-dialog-confirm-button'), child: Text(MaterialLocalizations.of(context).continueButtonLabel), onPressed: () { - widget._ingredientController.text = result.name; - widget._ingredientIdController.text = result.id.toString(); + widget.selectIngredient(result.id, result.name, null); Navigator.of(ctx).pop(); }, ), @@ -357,3 +374,23 @@ class IngredientAvatar extends StatelessWidget { : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)); } } + +class MealItemTile extends StatelessWidget { + final Ingredient ingredient; + final NutritionalValues nutritionalValues; + + const MealItemTile({ + super.key, + required this.ingredient, + required this.nutritionalValues, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: IngredientAvatar(ingredient: ingredient), + title: Text(getShortNutritionValues(nutritionalValues, context)), + // subtitle: Text(ingredient.id.toString()), + ); + } +} diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 03770c7c..72c03594 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -305,6 +305,11 @@ void main() { expect(formState.ingredientIdController.text, '1'); + // once ID and weight are set, it'll fetchIngredient and show macros preview + when(mockNutrition.fetchIngredient(1)).thenAnswer((_) => Future.value( + Ingredient.fromJson(jsonDecode(fixture('nutrition/ingredient_59887_response.json'))), + )); + await tester.enterText(find.byKey(const Key('field-weight')), '2'); await tester.pumpAndSettle();