From a4bdf70582d2839d7be195be5f9cedc0da0e1a28 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 28 May 2024 11:54:55 +0200 Subject: [PATCH 1/2] consistent, improved deduplication behavior across mealitems and logs deduplicate by ingredient AND amount: so if you use the same ingredient, but in a different amount, the different amount will show up this increases the list of suggestions for editing meals, and decreases the list logging an ingredient (significantly, if you often log the same ingredient in the same amount) --- lib/models/nutrition/nutritional_plan.dart | 27 ++++++++++++++----- lib/widgets/nutrition/forms.dart | 2 +- lib/widgets/nutrition/meal.dart | 6 ++--- .../nutrition/nutritional_plan_detail.dart | 4 +-- .../nutritional_plan_model_test.dart | 2 +- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index 294c95c6..ce9b8069 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -191,16 +192,16 @@ class NutritionalPlan { return out; } - /// Helper that returns all meal items for the current plan - /// - /// Duplicated ingredients are removed - List get allMealItems { + /// returns meal items across all meals + /// deduped by the combination of amount and ingredient ID + List get dedupMealItems { final List out = []; for (final meal in meals) { for (final mealItem in meal.mealItems) { - final ingredientInList = out.where((e) => e.ingredientId == mealItem.ingredientId); + final found = out.firstWhereOrNull( + (e) => e.amount == mealItem.amount && e.ingredientId == mealItem.ingredientId); - if (ingredientInList.isEmpty) { + if (found == null) { out.add(mealItem); } } @@ -208,6 +209,20 @@ class NutritionalPlan { return out; } + /// returns diary entries + /// deduped by the combination of amount and ingredient ID + List get dedupDiaryEntries { + final out = []; + for (final log in diaryEntries) { + final found = + out.firstWhereOrNull((e) => e.amount == log.amount && e.ingredientId == log.ingredientId); + if (found == null) { + out.add(log); + } + } + return out; + } + Meal pseudoMealOthers(String name) { return Meal( id: PSEUDO_MEAL_ID, diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index c9d319e7..cfe4d06a 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -131,7 +131,7 @@ Widget MealItemForm(Meal meal, List recent, [String? barcode, bool? te Widget IngredientLogForm(NutritionalPlan plan) { return IngredientForm( - recent: plan.diaryEntries, + recent: plan.dedupDiaryEntries, onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { Provider.of(context, listen: false) .logIngredientToDiary(mealItem, plan.id!, dt); diff --git a/lib/widgets/nutrition/meal.dart b/lib/widgets/nutrition/meal.dart index 769f991f..e11b6e7c 100644 --- a/lib/widgets/nutrition/meal.dart +++ b/lib/widgets/nutrition/meal.dart @@ -39,11 +39,11 @@ enum viewMode { class MealWidget extends StatefulWidget { final Meal _meal; - final List _listMealItems; + final List _recentMealItems; const MealWidget( this._meal, - this._listMealItems, + this._recentMealItems, ); @override @@ -108,7 +108,7 @@ class _MealWidgetState extends State { FormScreen.routeName, arguments: FormScreenArguments( AppLocalizations.of(context).addIngredient, - MealItemForm(widget._meal, widget._listMealItems), + MealItemForm(widget._meal, widget._recentMealItems), hasListView: true, ), ); diff --git a/lib/widgets/nutrition/nutritional_plan_detail.dart b/lib/widgets/nutrition/nutritional_plan_detail.dart index 9feb631c..cf873904 100644 --- a/lib/widgets/nutrition/nutritional_plan_detail.dart +++ b/lib/widgets/nutrition/nutritional_plan_detail.dart @@ -56,11 +56,11 @@ class NutritionalPlanDetailWidget extends StatelessWidget { const SizedBox(height: 10), ..._nutritionalPlan.meals.map((meal) => MealWidget( meal, - _nutritionalPlan.allMealItems, + _nutritionalPlan.dedupMealItems, )), MealWidget( _nutritionalPlan.pseudoMealOthers('Other logs'), - _nutritionalPlan.allMealItems, + _nutritionalPlan.dedupMealItems, ), if (!_nutritionalPlan.onlyLogging) Padding( diff --git a/test/nutrition/nutritional_plan_model_test.dart b/test/nutrition/nutritional_plan_model_test.dart index 096e837e..8e3a2887 100644 --- a/test/nutrition/nutritional_plan_model_test.dart +++ b/test/nutrition/nutritional_plan_model_test.dart @@ -101,7 +101,7 @@ void main() { }); test('Test that the getter returns all meal items for a plan', () { - expect(plan.allMealItems, plan.meals[0].mealItems + plan.meals[1].mealItems); + expect(plan.dedupMealItems, plan.meals[0].mealItems + plan.meals[1].mealItems); }); }); } From 686fb449338ade4f5524ef6ca55ff1f9d1e2b284 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 28 May 2024 12:18:21 +0200 Subject: [PATCH 2/2] filter suggestions by search string --- lib/widgets/nutrition/forms.dart | 25 +++++++++++++++++-------- lib/widgets/nutrition/widgets.dart | 3 +++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index cfe4d06a..aa141250 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -168,6 +168,7 @@ class IngredientFormState extends State { final _dateController = TextEditingController(); // optional final _timeController = TextEditingController(); // optional final _mealItem = MealItem.empty(); + var _searchQuery = ''; // copy from typeahead. for filtering suggestions @override void initState() { @@ -201,10 +202,18 @@ class IngredientFormState extends State { }); } + void updateSearchQuery(String query) { + setState(() { + _searchQuery = query; + }); + } + @override Widget build(BuildContext context) { final String unit = AppLocalizations.of(context).g; - + final queryLower = _searchQuery.toLowerCase(); + final suggestions = + widget.recent.where((e) => e.ingredient.name.toLowerCase().contains(queryLower)).toList(); return Container( margin: const EdgeInsets.all(20), child: Form( @@ -218,6 +227,7 @@ class IngredientFormState extends State { test: widget.test, selectIngredient: selectIngredient, unSelectIngredient: unSelectIngredient, + updateSearchQuery: updateSearchQuery, ), Row( children: [ @@ -370,27 +380,26 @@ class IngredientFormState extends State { Navigator.of(context).pop(); }, ), - if (widget.recent.isNotEmpty) const SizedBox(height: 10.0), + if (suggestions.isNotEmpty) const SizedBox(height: 10.0), Container( padding: const EdgeInsets.all(10.0), child: Text(AppLocalizations.of(context).recentlyUsedIngredients), ), Expanded( child: ListView.builder( - itemCount: widget.recent.length, + itemCount: suggestions.length, shrinkWrap: true, itemBuilder: (context, index) { return Card( child: ListTile( onTap: () { - final ingredient = widget.recent[index].ingredient; - selectIngredient( - ingredient.id, ingredient.name, widget.recent[index].amount); + final ingredient = suggestions[index].ingredient; + selectIngredient(ingredient.id, ingredient.name, suggestions[index].amount); }, title: Text( - '${widget.recent[index].ingredient.name} (${widget.recent[index].amount.toStringAsFixed(0)}$unit)'), + '${suggestions[index].ingredient.name} (${suggestions[index].amount.toStringAsFixed(0)}$unit)'), subtitle: Text(getShortNutritionValues( - widget.recent[index].ingredient.nutritionalValues, context)), + suggestions[index].ingredient.nutritionalValues, context)), trailing: const Icon(Icons.copy), ), ); diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index 35d335c5..460c15a7 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -65,6 +65,7 @@ class IngredientTypeahead extends StatefulWidget { final Function(int id, String name, num? amount) selectIngredient; final Function() unSelectIngredient; + final Function(String query) updateSearchQuery; const IngredientTypeahead( this._ingredientIdController, @@ -74,6 +75,7 @@ class IngredientTypeahead extends StatefulWidget { this.barcode = '', required this.selectIngredient, required this.unSelectIngredient, + required this.updateSearchQuery, }); @override @@ -125,6 +127,7 @@ class _IngredientTypeaheadState extends State { return null; }, onChanged: (value) { + widget.updateSearchQuery(value); // unselect to start a new search widget.unSelectIngredient(); },