mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge pull request #594 from wger-project/recent-ingredient-suggestions-tweaks
Recent ingredient suggestions deduping and filtering
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<MealItem> get allMealItems {
|
||||
/// returns meal items across all meals
|
||||
/// deduped by the combination of amount and ingredient ID
|
||||
List<MealItem> get dedupMealItems {
|
||||
final List<MealItem> 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<Log> get dedupDiaryEntries {
|
||||
final out = <Log>[];
|
||||
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,
|
||||
|
||||
@@ -131,7 +131,7 @@ Widget MealItemForm(Meal meal, List<MealItem> 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<NutritionPlansProvider>(context, listen: false)
|
||||
.logIngredientToDiary(mealItem, plan.id!, dt);
|
||||
@@ -168,6 +168,7 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
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<IngredientForm> {
|
||||
});
|
||||
}
|
||||
|
||||
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<IngredientForm> {
|
||||
test: widget.test,
|
||||
selectIngredient: selectIngredient,
|
||||
unSelectIngredient: unSelectIngredient,
|
||||
updateSearchQuery: updateSearchQuery,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@@ -370,27 +380,26 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -39,11 +39,11 @@ enum viewMode {
|
||||
|
||||
class MealWidget extends StatefulWidget {
|
||||
final Meal _meal;
|
||||
final List<MealItem> _listMealItems;
|
||||
final List<MealItem> _recentMealItems;
|
||||
|
||||
const MealWidget(
|
||||
this._meal,
|
||||
this._listMealItems,
|
||||
this._recentMealItems,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -108,7 +108,7 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
FormScreen.routeName,
|
||||
arguments: FormScreenArguments(
|
||||
AppLocalizations.of(context).addIngredient,
|
||||
MealItemForm(widget._meal, widget._listMealItems),
|
||||
MealItemForm(widget._meal, widget._recentMealItems),
|
||||
hasListView: true,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<IngredientTypeahead> {
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
widget.updateSearchQuery(value);
|
||||
// unselect to start a new search
|
||||
widget.unSelectIngredient();
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user