From 1909f7caabb8c3f3792627382e9a1649de55c8af Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 20 Jun 2024 18:59:08 +0300 Subject: [PATCH 01/16] popup ingredient details in typeahead suggestions and recently used --- lib/widgets/nutrition/forms.dart | 80 +++++++++++++++++++++++++++++- lib/widgets/nutrition/widgets.dart | 7 +++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index f6ac8d53..9f66e591 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -27,10 +27,12 @@ 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_goals.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/macro_nutrients_table.dart'; import 'package:wger/widgets/nutrition/nutrition_tiles.dart'; import 'package:wger/widgets/nutrition/widgets.dart'; @@ -447,7 +449,19 @@ class IngredientFormState extends State { suggestions[index].ingredient.nutritionalValues, context, )), - trailing: const Icon(Icons.copy), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.info_outline), + onPressed: () { + showIngredientDetails(context, suggestions[index].ingredient.id); + }, + ), + const SizedBox(width: 5), + const Icon(Icons.copy), + ], + ), ), ); }, @@ -460,6 +474,70 @@ class IngredientFormState extends State { } } +void showIngredientDetails(BuildContext context, int id, {String? image}) { + // when loading recently used ingredients, we never get an image :'( + // we also don't get an image when querying the API + // however, the typeahead suggestion does get an image, so we allow passing it... + + final url = context.read().baseProvider.auth.serverUrl; + + showDialog( + context: context, + builder: (context) => FutureBuilder( + future: Future.delayed(const Duration(seconds: 5), () { + return Provider.of(context, listen: false).fetchIngredient(id); + }), + builder: (BuildContext context, AsyncSnapshot snapshot) { + Ingredient? ingredient; + NutritionalGoals? goals; + + if (snapshot.hasData) { + ingredient = snapshot.data!; + goals = NutritionalGoals( + energy: ingredient.nutritionalValues.energy, + protein: ingredient.nutritionalValues.protein, + carbohydrates: ingredient.nutritionalValues.carbohydrates, + carbohydratesSugar: ingredient.nutritionalValues.carbohydratesSugar, + fat: ingredient.nutritionalValues.fat, + fatSaturated: ingredient.nutritionalValues.fatSaturated, + fiber: ingredient.nutritionalValues.fiber, + sodium: ingredient.nutritionalValues.sodium, + ); + } + return AlertDialog( + title: (snapshot.hasData) ? Text(ingredient!.name) : null, + content: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (image != null) + CircleAvatar(backgroundImage: NetworkImage(url! + image), radius: 128), + if (image != null) const SizedBox(height: 12), + if (snapshot.hasError) + Text( + 'Ingredient lookup error: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + ), + if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), + if (snapshot.hasData) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 400), + child: MacronutrientsTable( + nutritionalGoals: goals!, + plannedValuesPercentage: goals.energyPercentage(), + nutritionalGoalsGperKg: null, + ), + ), + ], + ), + ), + ); + }, + ), + ); +} + enum GoalType { meals('From meals'), basic('Basic'), diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index 0ee46fc5..e6fc8cf2 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -31,6 +31,7 @@ import 'package:wger/models/exercises/ingredient_api.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/widgets/core/core.dart'; +import 'package:wger/widgets/nutrition/forms.dart'; import 'package:wger/widgets/nutrition/nutrition_tiles.dart'; class ScanReader extends StatelessWidget { @@ -159,6 +160,12 @@ class _IngredientTypeaheadState extends State { ), title: Text(suggestion.value), // subtitle: Text(suggestion.data.id.toString()), + trailing: IconButton( + icon: const Icon(Icons.info_outline), + onPressed: () { + showIngredientDetails(context, suggestion.data.id, image: suggestion.data.image); + }, + ), ); }, transitionBuilder: (context, animation, child) => FadeTransition( From b8f8bc4e4a30c071f1cb942a395c5ddbd916cb6d Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 20 Jun 2024 19:10:03 +0300 Subject: [PATCH 02/16] remove test sleep --- lib/widgets/nutrition/forms.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 9f66e591..90641449 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -484,9 +484,7 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { showDialog( context: context, builder: (context) => FutureBuilder( - future: Future.delayed(const Duration(seconds: 5), () { - return Provider.of(context, listen: false).fetchIngredient(id); - }), + future: Provider.of(context, listen: false).fetchIngredient(id), builder: (BuildContext context, AsyncSnapshot snapshot) { Ingredient? ingredient; NutritionalGoals? goals; From de1b7e2229a07e0497782b3d12d9b8014800d2ae Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 20 Jun 2024 19:12:10 +0300 Subject: [PATCH 03/16] cleanup --- lib/widgets/nutrition/forms.dart | 64 ---------------------------- lib/widgets/nutrition/helpers.dart | 67 ++++++++++++++++++++++++++++++ lib/widgets/nutrition/widgets.dart | 2 +- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 90641449..23928bac 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -27,12 +27,10 @@ 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_goals.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/macro_nutrients_table.dart'; import 'package:wger/widgets/nutrition/nutrition_tiles.dart'; import 'package:wger/widgets/nutrition/widgets.dart'; @@ -474,68 +472,6 @@ class IngredientFormState extends State { } } -void showIngredientDetails(BuildContext context, int id, {String? image}) { - // when loading recently used ingredients, we never get an image :'( - // we also don't get an image when querying the API - // however, the typeahead suggestion does get an image, so we allow passing it... - - final url = context.read().baseProvider.auth.serverUrl; - - showDialog( - context: context, - builder: (context) => FutureBuilder( - future: Provider.of(context, listen: false).fetchIngredient(id), - builder: (BuildContext context, AsyncSnapshot snapshot) { - Ingredient? ingredient; - NutritionalGoals? goals; - - if (snapshot.hasData) { - ingredient = snapshot.data!; - goals = NutritionalGoals( - energy: ingredient.nutritionalValues.energy, - protein: ingredient.nutritionalValues.protein, - carbohydrates: ingredient.nutritionalValues.carbohydrates, - carbohydratesSugar: ingredient.nutritionalValues.carbohydratesSugar, - fat: ingredient.nutritionalValues.fat, - fatSaturated: ingredient.nutritionalValues.fatSaturated, - fiber: ingredient.nutritionalValues.fiber, - sodium: ingredient.nutritionalValues.sodium, - ); - } - return AlertDialog( - title: (snapshot.hasData) ? Text(ingredient!.name) : null, - content: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (image != null) - CircleAvatar(backgroundImage: NetworkImage(url! + image), radius: 128), - if (image != null) const SizedBox(height: 12), - if (snapshot.hasError) - Text( - 'Ingredient lookup error: ${snapshot.error}', - style: const TextStyle(color: Colors.red), - ), - if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), - if (snapshot.hasData) - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 400), - child: MacronutrientsTable( - nutritionalGoals: goals!, - plannedValuesPercentage: goals.energyPercentage(), - nutritionalGoalsGperKg: null, - ), - ), - ], - ), - ), - ); - }, - ), - ); -} - enum GoalType { meals('From meals'), basic('Basic'), diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 39ac478c..6fdb758d 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -19,9 +19,14 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/meal.dart'; +import 'package:wger/models/nutrition/nutritional_goals.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/macro_nutrients_table.dart'; List getNutritionColumnNames(BuildContext context) => [ AppLocalizations.of(context).energy, @@ -95,3 +100,65 @@ String getKcalConsumedVsPlanned(Meal meal, BuildContext context) { return '${consumed.toStringAsFixed(0)} / ${planned.toStringAsFixed(0)} ${loc.kcal}'; } + +void showIngredientDetails(BuildContext context, int id, {String? image}) { + // when loading recently used ingredients, we never get an image :'( + // we also don't get an image when querying the API + // however, the typeahead suggestion does get an image, so we allow passing it... + + final url = context.read().baseProvider.auth.serverUrl; + + showDialog( + context: context, + builder: (context) => FutureBuilder( + future: Provider.of(context, listen: false).fetchIngredient(id), + builder: (BuildContext context, AsyncSnapshot snapshot) { + Ingredient? ingredient; + NutritionalGoals? goals; + + if (snapshot.hasData) { + ingredient = snapshot.data!; + goals = NutritionalGoals( + energy: ingredient.nutritionalValues.energy, + protein: ingredient.nutritionalValues.protein, + carbohydrates: ingredient.nutritionalValues.carbohydrates, + carbohydratesSugar: ingredient.nutritionalValues.carbohydratesSugar, + fat: ingredient.nutritionalValues.fat, + fatSaturated: ingredient.nutritionalValues.fatSaturated, + fiber: ingredient.nutritionalValues.fiber, + sodium: ingredient.nutritionalValues.sodium, + ); + } + return AlertDialog( + title: (snapshot.hasData) ? Text(ingredient!.name) : null, + content: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (image != null) + CircleAvatar(backgroundImage: NetworkImage(url! + image), radius: 128), + if (image != null) const SizedBox(height: 12), + if (snapshot.hasError) + Text( + 'Ingredient lookup error: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + ), + if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), + if (snapshot.hasData) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 400), + child: MacronutrientsTable( + nutritionalGoals: goals!, + plannedValuesPercentage: goals.energyPercentage(), + nutritionalGoalsGperKg: null, + ), + ), + ], + ), + ), + ); + }, + ), + ); +} diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index e6fc8cf2..ba3dc0b6 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -31,7 +31,7 @@ import 'package:wger/models/exercises/ingredient_api.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/widgets/core/core.dart'; -import 'package:wger/widgets/nutrition/forms.dart'; +import 'package:wger/widgets/nutrition/helpers.dart'; import 'package:wger/widgets/nutrition/nutrition_tiles.dart'; class ScanReader extends StatelessWidget { From 54fb7c1c6ca663b7922889336bf30dce9f702918 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 20 Jun 2024 19:26:19 +0300 Subject: [PATCH 04/16] cleanup --- lib/models/nutrition/nutritional_values.dart | 15 +++++++++++++++ lib/widgets/nutrition/helpers.dart | 13 ++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/models/nutrition/nutritional_values.dart b/lib/models/nutrition/nutritional_values.dart index b3dabef0..fee6c370 100644 --- a/lib/models/nutrition/nutritional_values.dart +++ b/lib/models/nutrition/nutritional_values.dart @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import 'package:wger/models/nutrition/nutritional_goals.dart'; + class NutritionalValues { double energy = 0; double protein = 0; @@ -114,6 +116,19 @@ class NutritionalValues { return 'e: $energy, p: $protein, c: $carbohydrates, cS: $carbohydratesSugar, f: $fat, fS: $fatSaturated, fi: $fiber, s: $sodium'; } + NutritionalGoals toGoals() { + return NutritionalGoals( + energy: energy, + protein: protein, + carbohydrates: carbohydrates, + carbohydratesSugar: carbohydratesSugar, + fat: fat, + fatSaturated: fatSaturated, + fiber: fiber, + sodium: sodium, + ); + } + @override //ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => Object.hash( diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 6fdb758d..b828055e 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -117,17 +117,8 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { NutritionalGoals? goals; if (snapshot.hasData) { - ingredient = snapshot.data!; - goals = NutritionalGoals( - energy: ingredient.nutritionalValues.energy, - protein: ingredient.nutritionalValues.protein, - carbohydrates: ingredient.nutritionalValues.carbohydrates, - carbohydratesSugar: ingredient.nutritionalValues.carbohydratesSugar, - fat: ingredient.nutritionalValues.fat, - fatSaturated: ingredient.nutritionalValues.fatSaturated, - fiber: ingredient.nutritionalValues.fiber, - sodium: ingredient.nutritionalValues.sodium, - ); + ingredient = snapshot.data; + goals = ingredient!.nutritionalValues.toGoals(); } return AlertDialog( title: (snapshot.hasData) ? Text(ingredient!.name) : null, From 395d40ce9ae6c6f27840f730274bb45e383a93c0 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Wed, 3 Jul 2024 20:58:45 +0300 Subject: [PATCH 05/16] dart fix --- lib/widgets/nutrition/helpers.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index b828055e..8879b661 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; From 2d27281a1d40a1b1f3efbed1544218a43cc809ef Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Wed, 3 Jul 2024 21:09:37 +0300 Subject: [PATCH 06/16] don't show g-per-bodyweight-kg on ingredient details popups --- lib/widgets/nutrition/helpers.dart | 2 +- lib/widgets/nutrition/macro_nutrients_table.dart | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 8879b661..27e45761 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -141,7 +141,7 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { child: MacronutrientsTable( nutritionalGoals: goals!, plannedValuesPercentage: goals.energyPercentage(), - nutritionalGoalsGperKg: null, + showGperKg: false, ), ), ], diff --git a/lib/widgets/nutrition/macro_nutrients_table.dart b/lib/widgets/nutrition/macro_nutrients_table.dart index 4d81c081..c5bc5c4b 100644 --- a/lib/widgets/nutrition/macro_nutrients_table.dart +++ b/lib/widgets/nutrition/macro_nutrients_table.dart @@ -7,13 +7,15 @@ class MacronutrientsTable extends StatelessWidget { super.key, required this.nutritionalGoals, required this.plannedValuesPercentage, - required this.nutritionalGoalsGperKg, + this.nutritionalGoalsGperKg, + this.showGperKg = true, }); static const double tablePadding = 7; final NutritionalGoals nutritionalGoals; final NutritionalGoals plannedValuesPercentage; final NutritionalGoals? nutritionalGoalsGperKg; + final bool showGperKg; @override Widget build(BuildContext context) { @@ -42,7 +44,8 @@ class MacronutrientsTable extends StatelessWidget { ), Text(goal != null ? valFn(goal.toStringAsFixed(0)) : '', textAlign: TextAlign.right), Text(pct != null ? pct.toStringAsFixed(1) : '', textAlign: TextAlign.right), - Text(perkg != null ? perkg.toStringAsFixed(1) : '', textAlign: TextAlign.right), + if (showGperKg) + Text(perkg != null ? perkg.toStringAsFixed(1) : '', textAlign: TextAlign.right), ], ); } @@ -62,7 +65,7 @@ class MacronutrientsTable extends StatelessWidget { columnHeader(true, loc.macronutrients), columnHeader(false, loc.total), columnHeader(false, loc.percentEnergy), - columnHeader(false, loc.gPerBodyKg), + if (showGperKg) columnHeader(false, loc.gPerBodyKg), ], ), macroRow(0, false, loc.energy, (NutritionalGoals ng) => ng.energy), From d06ecabadf8f4fcecf6ce1dac43785ae6ecb843c Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 11 Jul 2024 20:43:22 +0300 Subject: [PATCH 07/16] use ingredientinfo api, for image + new source and remote id fields also update fixtures accordingly --- lib/models/nutrition/ingredient.dart | 18 ++++++ lib/models/nutrition/ingredient.g.dart | 9 +++ lib/providers/nutrition.dart | 5 +- lib/screens/home_tabs_screen.dart | 6 +- .../nutrition/ingredient_10065_response.json | 18 ------ .../nutrition/ingredient_58300_response.json | 18 ------ .../nutrition/ingredient_59887_response.json | 18 ------ .../nutrition/ingredientinfo_10065.json | 56 +++++++++++++++++ .../nutrition/ingredientinfo_58300.json | 56 +++++++++++++++++ .../nutrition/ingredientinfo_59887.json | 56 +++++++++++++++++ .../nutrition/ingredientinfo_right_code.json | 63 +++++++++++++++++++ ...de.json => ingredientinfo_wrong_code.json} | 0 ...nutritional_plan_info_detail_response.json | 9 +++ .../search_ingredient_right_code.json | 25 -------- test/nutrition/nutrition_provider_test.dart | 6 +- .../nutritional_meal_item_form_test.dart | 17 ++--- test_data/nutritional_plans.dart | 21 +++++++ 17 files changed, 307 insertions(+), 94 deletions(-) delete mode 100644 test/fixtures/nutrition/ingredient_10065_response.json delete mode 100644 test/fixtures/nutrition/ingredient_58300_response.json delete mode 100644 test/fixtures/nutrition/ingredient_59887_response.json create mode 100644 test/fixtures/nutrition/ingredientinfo_10065.json create mode 100644 test/fixtures/nutrition/ingredientinfo_58300.json create mode 100644 test/fixtures/nutrition/ingredientinfo_59887.json create mode 100644 test/fixtures/nutrition/ingredientinfo_right_code.json rename test/fixtures/nutrition/{search_ingredient_wrong_code.json => ingredientinfo_wrong_code.json} (100%) delete mode 100644 test/fixtures/nutrition/search_ingredient_right_code.json diff --git a/lib/models/nutrition/ingredient.dart b/lib/models/nutrition/ingredient.dart index 3acf8624..1a0981f8 100644 --- a/lib/models/nutrition/ingredient.dart +++ b/lib/models/nutrition/ingredient.dart @@ -24,9 +24,24 @@ part 'ingredient.g.dart'; @JsonSerializable() class Ingredient { + // fields returned by django api that we ignore here: + // uuid, last_updated, last_imported, weight_units, language + // all license fields + @JsonKey(required: true) final int id; + // some ingredients don't have these 3 fields set. E.g. USDA entries that + // have been removed upstream, or manually added ingredients. + @JsonKey(required: true, name: 'remote_id') + final String? remoteId; + + @JsonKey(required: true, name: 'source_name') + final String? sourceName; + + @JsonKey(required: true, name: 'source_url') + final String? sourceUrl; + /// Barcode of the product @JsonKey(required: true) final String? code; @@ -73,6 +88,9 @@ class Ingredient { IngredientImage? image; Ingredient({ + required this.remoteId, + required this.sourceName, + required this.sourceUrl, required this.id, required this.code, required this.name, diff --git a/lib/models/nutrition/ingredient.g.dart b/lib/models/nutrition/ingredient.g.dart index 92df4273..f3f21074 100644 --- a/lib/models/nutrition/ingredient.g.dart +++ b/lib/models/nutrition/ingredient.g.dart @@ -11,6 +11,9 @@ Ingredient _$IngredientFromJson(Map json) { json, requiredKeys: const [ 'id', + 'remote_id', + 'source_name', + 'source_url', 'code', 'name', 'created', @@ -25,6 +28,9 @@ Ingredient _$IngredientFromJson(Map json) { ], ); return Ingredient( + remoteId: json['remote_id'] as String?, + sourceName: json['source_name'] as String?, + sourceUrl: json['source_url'] as String?, id: (json['id'] as num).toInt(), code: json['code'] as String?, name: json['name'] as String, @@ -46,6 +52,9 @@ Ingredient _$IngredientFromJson(Map json) { Map _$IngredientToJson(Ingredient instance) => { 'id': instance.id, + 'remote_id': instance.remoteId, + 'source_name': instance.sourceName, + 'source_url': instance.sourceUrl, 'code': instance.code, 'name': instance.name, 'created': instance.created.toIso8601String(), diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index b9806449..f6bd5af9 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -39,6 +39,7 @@ class NutritionPlansProvider with ChangeNotifier { static const _mealPath = 'meal'; static const _mealItemPath = 'mealitem'; static const _ingredientPath = 'ingredient'; + static const _ingredientInfoPath = 'ingredientinfo'; static const _ingredientSearchPath = 'ingredient/search'; static const _nutritionDiaryPath = 'nutritiondiary'; static const _ingredientImagePath = 'ingredient-image'; @@ -298,7 +299,7 @@ class NutritionPlansProvider with ChangeNotifier { // Get ingredient from the server and save to cache } on StateError { final data = await baseProvider.fetch( - baseProvider.makeUrl(_ingredientPath, id: ingredientId), + baseProvider.makeUrl(_ingredientInfoPath, id: ingredientId), ); ingredient = Ingredient.fromJson(data); _ingredients.add(ingredient); @@ -370,7 +371,7 @@ class NutritionPlansProvider with ChangeNotifier { // Send the request final data = await baseProvider.fetch( - baseProvider.makeUrl(_ingredientPath, query: {'code': code}), + baseProvider.makeUrl(_ingredientInfoPath, query: {'code': code}), ); if (data['count'] == 0) { diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 9a4c3f25..765703be 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -94,7 +94,7 @@ class _HomeTabsScreenState extends State with SingleTickerProvid exercisesProvider.fetchAndSetInitialData(), ]); } catch (e) { - log('fire! fire!'); + log('Exception loading base data'); log(e.toString()); } @@ -109,7 +109,7 @@ class _HomeTabsScreenState extends State with SingleTickerProvid measurementProvider.fetchAndSetAllCategoriesAndEntries(), ]); } catch (e) { - log('fire! fire!'); + log('Exception loading plans, weight, measurements and gallery'); log(e.toString()); } @@ -121,7 +121,7 @@ class _HomeTabsScreenState extends State with SingleTickerProvid await nutritionPlansProvider.fetchAndSetPlanFull(plan.id!); } } catch (e) { - log('fire! fire!'); + log('Exception loading current nutritional plan'); log(e.toString()); } diff --git a/test/fixtures/nutrition/ingredient_10065_response.json b/test/fixtures/nutrition/ingredient_10065_response.json deleted file mode 100644 index 6653d12a..00000000 --- a/test/fixtures/nutrition/ingredient_10065_response.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": 10065, - "code": "0043647440020", - "name": "'Old Times' Orange Fine Cut Marmalade", - "created": "2020-12-20T01:00:00+01:00", - "last_update": "2022-08-09T10:23:11+02:00", - "energy": 269, - "protein": "0.000", - "carbohydrates": "67.000", - "carbohydrates_sugar": "66.000", - "fat": "0.000", - "fat_saturated": "0.000", - "fiber": null, - "sodium": "0.000", - "license": 5, - "license_author": "Open Food Facts", - "language": 2 -} \ No newline at end of file diff --git a/test/fixtures/nutrition/ingredient_58300_response.json b/test/fixtures/nutrition/ingredient_58300_response.json deleted file mode 100644 index 7ff1f8f6..00000000 --- a/test/fixtures/nutrition/ingredient_58300_response.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": 58300, - "code": "4071800000992", - "name": "1688 Mehrkorn", - "created": "2020-12-20T01:00:00+02:00", - "last_update": "2022-08-09T18:55:00+02:00", - "energy": 229, - "protein": "7.680", - "carbohydrates": "35.700", - "carbohydrates_sugar": "2.320", - "fat": "4.820", - "fat_saturated": "0.714", - "fiber": "6.960", - "sodium": "0.000", - "license": 5, - "license_author": "Open Food Facts", - "language": 2 -} \ No newline at end of file diff --git a/test/fixtures/nutrition/ingredient_59887_response.json b/test/fixtures/nutrition/ingredient_59887_response.json deleted file mode 100644 index 2287c32b..00000000 --- a/test/fixtures/nutrition/ingredient_59887_response.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": 59887, - "code": "4311501354155", - "name": "Baked Beans", - "created": "2020-12-20T23:10:54+01:00", - "last_update": "2022-08-09T13:32:41+01:00", - "energy": 86, - "protein": "4.400", - "carbohydrates": "11.000", - "carbohydrates_sugar": "5.300", - "fat": "0.100", - "fat_saturated": "0.000", - "fiber": "5.000", - "sodium": "0.480", - "license": 5, - "license_author": "Open Food Facts", - "language": 1 -} \ No newline at end of file diff --git a/test/fixtures/nutrition/ingredientinfo_10065.json b/test/fixtures/nutrition/ingredientinfo_10065.json new file mode 100644 index 00000000..b10e4e9e --- /dev/null +++ b/test/fixtures/nutrition/ingredientinfo_10065.json @@ -0,0 +1,56 @@ +{ + "id": 10065, + "uuid": "cd134608-3fbf-4d54-bf9d-ecc56e4271f2", + "remote_id": "0043647440020", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/0043647440020.json", + "code": "0043647440020", + "name": "'Old Times' Orange Fine Cut Marmalade", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-06-07T06:04:05.211180+02:00", + "last_imported": "2020-12-20T14:44:55.854000+01:00", + "energy": 269, + "protein": "0.000", + "carbohydrates": "67.000", + "carbohydrates_sugar": "66.000", + "fat": "0.000", + "fat_saturated": "0.000", + "fiber": null, + "sodium": "0.000", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 7545, + "uuid": "c7a43097-da62-4a35-95f4-fe466e68cbc6", + "ingredient_id": 10065, + "ingredient_uuid": "cd134608-3fbf-4d54-bf9d-ecc56e4271f2", + "image": "https://wger.de/media/ingredients/10065/c7a43097-da62-4a35-95f4-fe466e68cbc6.jpg", + "created": "2023-04-21T20:13:01.136749+02:00", + "last_update": "2023-04-21T20:13:01.151137+02:00", + "size": 43397, + "width": 219, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=0043647440020&id=2", + "license_author": "tacinte", + "license_author_url": "https://world.openfoodfacts.org/photographer/tacinte", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "'Old Times' Orange Fine Cut Marmalade", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" +} \ No newline at end of file diff --git a/test/fixtures/nutrition/ingredientinfo_58300.json b/test/fixtures/nutrition/ingredientinfo_58300.json new file mode 100644 index 00000000..9e02c618 --- /dev/null +++ b/test/fixtures/nutrition/ingredientinfo_58300.json @@ -0,0 +1,56 @@ +{ + "id": 58300, + "uuid": "151a8d44-735e-4f9b-9e22-7dc4c604154a", + "remote_id": "4071800000992", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/4071800000992.json", + "code": "4071800000992", + "name": "1688 Mehrkorn", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-06-07T06:06:48.259950+02:00", + "last_imported": "2020-12-20T14:46:58.971000+01:00", + "energy": 229, + "protein": "7.680", + "carbohydrates": "35.700", + "carbohydrates_sugar": "2.320", + "fat": "4.820", + "fat_saturated": "0.714", + "fiber": "6.960", + "sodium": "0.000", + "weight_units": [], + "language": { + "id": 1, + "short_name": "de", + "full_name": "Deutsch", + "full_name_en": "German" + }, + "image": { + "id": 10862, + "uuid": "a5b6766f-b606-451c-b806-24eb3a8597ea", + "ingredient_id": 58300, + "ingredient_uuid": "151a8d44-735e-4f9b-9e22-7dc4c604154a", + "image": "https://wger.de/media/ingredients/58300/a5b6766f-b606-451c-b806-24eb3a8597ea.jpg", + "created": "2023-05-08T11:37:13.876341+02:00", + "last_update": "2023-05-08T11:37:13.891457+02:00", + "size": 33617, + "width": 400, + "height": 225, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=4071800000992&id=15", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "1688 Mehrkorn", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" +} \ No newline at end of file diff --git a/test/fixtures/nutrition/ingredientinfo_59887.json b/test/fixtures/nutrition/ingredientinfo_59887.json new file mode 100644 index 00000000..6c18b0fa --- /dev/null +++ b/test/fixtures/nutrition/ingredientinfo_59887.json @@ -0,0 +1,56 @@ +{ + "id": 59887, + "uuid": "2efa101b-d6f7-4755-838d-b3bb5c483b02", + "remote_id": "4311501354155", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/4311501354155.json", + "code": "4311501354155", + "name": "Baked Beans", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-06-07T06:40:23.364866+02:00", + "last_imported": "2020-12-20T14:47:03.728000+01:00", + "energy": 86, + "protein": "4.400", + "carbohydrates": "11.000", + "carbohydrates_sugar": "5.300", + "fat": "0.600", + "fat_saturated": "0.100", + "fiber": "9.100", + "sodium": "0.260", + "weight_units": [], + "language": { + "id": 1, + "short_name": "de", + "full_name": "Deutsch", + "full_name_en": "German" + }, + "image": { + "id": 1718, + "uuid": "493bc593-4813-4bae-b92a-a4e1fbee5e44", + "ingredient_id": 59887, + "ingredient_uuid": "2efa101b-d6f7-4755-838d-b3bb5c483b02", + "image": "https://wger.de/media/ingredients/59887/493bc593-4813-4bae-b92a-a4e1fbee5e44.jpg", + "created": "2023-04-09T01:45:50.874157+02:00", + "last_update": "2023-04-09T01:45:50.911258+02:00", + "size": 27752, + "width": 286, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=4311501354155&id=6", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Baked Beans", + "license_object_url": "https://world.openfoodfacts.org/product/4311501354155/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" +} \ No newline at end of file diff --git a/test/fixtures/nutrition/ingredientinfo_right_code.json b/test/fixtures/nutrition/ingredientinfo_right_code.json new file mode 100644 index 00000000..ee46233a --- /dev/null +++ b/test/fixtures/nutrition/ingredientinfo_right_code.json @@ -0,0 +1,63 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 19733, + "uuid": "354c6050-5654-41bc-8823-4fd27b54fb59", + "remote_id": "3068320105222", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/3068320105222.json", + "code": "3068320105222", + "name": "Badoit zest citron vert sans sucres", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-06-07T06:39:29.032545+02:00", + "last_imported": "2020-12-20T14:45:31.370000+01:00", + "energy": 1, + "protein": "0.000", + "carbohydrates": "0.000", + "carbohydrates_sugar": "0.000", + "fat": "0.000", + "fat_saturated": "0.000", + "fiber": null, + "sodium": "0.020", + "weight_units": [], + "language": { + "id": 12, + "short_name": "fr", + "full_name": "Fran\u00e7ais", + "full_name_en": "French" + }, + "image": { + "id": 26949, + "uuid": "5177e2d1-b03a-42d9-bdf1-2dd3edad8210", + "ingredient_id": 19733, + "ingredient_uuid": "354c6050-5654-41bc-8823-4fd27b54fb59", + "image": "https://wger.de/media/ingredients/19733/5177e2d1-b03a-42d9-bdf1-2dd3edad8210.jpg", + "created": "2023-12-04T10:48:19.019477+01:00", + "last_update": "2023-12-04T10:48:19.062343+01:00", + "size": 13302, + "width": 112, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=3068320105222&id=11", + "license_author": "hungergames", + "license_author_url": "https://world.openfoodfacts.org/photographer/hungergames", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Badoit zest citron vert sans sucres", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + } + ] +} diff --git a/test/fixtures/nutrition/search_ingredient_wrong_code.json b/test/fixtures/nutrition/ingredientinfo_wrong_code.json similarity index 100% rename from test/fixtures/nutrition/search_ingredient_wrong_code.json rename to test/fixtures/nutrition/ingredientinfo_wrong_code.json diff --git a/test/fixtures/nutrition/nutritional_plan_info_detail_response.json b/test/fixtures/nutrition/nutritional_plan_info_detail_response.json index 233d2e1a..ba07b705 100644 --- a/test/fixtures/nutrition/nutritional_plan_info_detail_response.json +++ b/test/fixtures/nutrition/nutritional_plan_info_detail_response.json @@ -50,6 +50,9 @@ "ingredient": 59887, "ingredient_obj": { "id": 59887, + "remote_id": 123456789, + "source_name": "Open Food Facts", + "source_url" : "https://world.openfoodfacts.org/ingredients/123456789", "code": "4311501354155", "name": "Baked Beans", "created": "2020-12-20", @@ -88,6 +91,9 @@ "ingredient": 58300, "ingredient_obj": { "id": 58300, + "remote_id": 123456788, + "source_name": "Open Food Facts", + "source_url" : "https://world.openfoodfacts.org/ingredients/123456788", "code": "4071800000992", "name": "1688 Mehrkorn", "created": "2020-12-20", @@ -141,6 +147,9 @@ "ingredient": 10065, "ingredient_obj": { "id": 10065, + "remote_id": 123456787, + "source_name": "Open Food Facts", + "source_url" : "https://world.openfoodfacts.org/ingredients/123456787", "code": "0043647440020", "name": "'Old Times' Orange Fine Cut Marmalade", "created": "2020-12-20", diff --git a/test/fixtures/nutrition/search_ingredient_right_code.json b/test/fixtures/nutrition/search_ingredient_right_code.json deleted file mode 100644 index 01a5ae39..00000000 --- a/test/fixtures/nutrition/search_ingredient_right_code.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "count": 1, - "next": null, - "previous": null, - "results": [ - { - "id": 9436, - "code": "0013087245950", - "name": " Gâteau double chocolat ", - "creation_date": "2020-12-20", - "update_date": "2020-12-20", - "energy": 360, - "protein": "5.000", - "carbohydrates": "45.000", - "carbohydrates_sugar": "27.000", - "fat": "18.000", - "fat_saturated": "4.500", - "fiber": "2.000", - "sodium": "0.356", - "license": 5, - "license_author": "Open Food Facts", - "language": 12 - } - ] -} \ No newline at end of file diff --git a/test/nutrition/nutrition_provider_test.dart b/test/nutrition/nutrition_provider_test.dart index 8f3f4f95..39ed669a 100644 --- a/test/nutrition/nutrition_provider_test.dart +++ b/test/nutrition/nutrition_provider_test.dart @@ -31,13 +31,13 @@ void main() { fixture('nutrition/nutrition_diary_response.json'), )['results']; final Map ingredient59887Response = jsonDecode( - fixture('nutrition/ingredient_59887_response.json'), + fixture('nutrition/ingredientinfo_59887.json'), ); final Map ingredient10065Response = jsonDecode( - fixture('nutrition/ingredient_10065_response.json'), + fixture('nutrition/ingredientinfo_10065.json'), ); final Map ingredient58300Response = jsonDecode( - fixture('nutrition/ingredient_58300_response.json'), + fixture('nutrition/ingredientinfo_58300.json'), ); final ingredientList = [ diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 2ed57b98..9ea69f77 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -26,6 +26,9 @@ import 'nutritional_plan_form_test.mocks.dart'; void main() { final ingredient = Ingredient( + remoteId: '1', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/1', id: 1, code: '123456787', name: 'Water', @@ -52,20 +55,20 @@ void main() { var plan1 = NutritionalPlan.empty(); var meal1 = Meal(); - final Uri tUriRightCode = Uri.parse('https://localhost/api/v2/ingredient/?code=123'); - final Uri tUriEmptyCode = Uri.parse('https://localhost/api/v2/ingredient/?code="%20"'); - final Uri tUriBadCode = Uri.parse('https://localhost/api/v2/ingredient/?code=222'); + final Uri tUriRightCode = Uri.parse('https://localhost/api/v2/ingredientinfo/?code=123'); + final Uri tUriEmptyCode = Uri.parse('https://localhost/api/v2/ingredientinfo/?code="%20"'); + final Uri tUriBadCode = Uri.parse('https://localhost/api/v2/ingredientinfo/?code=222'); when(client.get(tUriRightCode, headers: anyNamed('headers'))).thenAnswer( - (_) => Future.value(http.Response(fixture('nutrition/search_ingredient_right_code.json'), 200)), + (_) => Future.value(http.Response(fixture('nutrition/ingredientinfo_right_code.json'), 200)), ); when(client.get(tUriEmptyCode, headers: anyNamed('headers'))).thenAnswer( - (_) => Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200)), + (_) => Future.value(http.Response(fixture('nutrition/ingredientinfo_wrong_code.json'), 200)), ); when(client.get(tUriBadCode, headers: anyNamed('headers'))).thenAnswer( - (_) => Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200)), + (_) => Future.value(http.Response(fixture('nutrition/ingredientinfo_wrong_code.json'), 200)), ); setUp(() { @@ -307,7 +310,7 @@ void main() { // 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'))), + Ingredient.fromJson(jsonDecode(fixture('nutrition/ingredientinfo_59887.json'))), )); await tester.enterText(find.byKey(const Key('field-weight')), '2'); diff --git a/test_data/nutritional_plans.dart b/test_data/nutritional_plans.dart index 6cfd2de0..5e77e792 100644 --- a/test_data/nutritional_plans.dart +++ b/test_data/nutritional_plans.dart @@ -26,6 +26,9 @@ import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; final ingredient1 = Ingredient( + remoteId: '1', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/1', id: 1, code: '123456787', name: 'Water', @@ -40,6 +43,9 @@ final ingredient1 = Ingredient( sodium: 0.5, ); final ingredient2 = Ingredient( + remoteId: '2', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/2', id: 2, code: '123456788', name: 'Burger soup', @@ -54,6 +60,9 @@ final ingredient2 = Ingredient( sodium: 0, ); final ingredient3 = Ingredient( + remoteId: '3', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/3', id: 3, code: '123456789', name: 'Broccoli cake', @@ -68,6 +77,9 @@ final ingredient3 = Ingredient( sodium: 10, ); final muesli = Ingredient( + remoteId: '1', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/1', id: 1, code: '123456787', name: 'Müsli', @@ -82,6 +94,9 @@ final muesli = Ingredient( sodium: 0.5, ); final milk = Ingredient( + remoteId: '1', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/1', id: 1, code: '123456787', name: 'Milk', @@ -96,6 +111,9 @@ final milk = Ingredient( sodium: 0.5, ); final apple = Ingredient( + remoteId: '1', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/1', id: 1, code: '123456787', name: 'Apple', @@ -110,6 +128,9 @@ final apple = Ingredient( sodium: 0.5, ); final cake = Ingredient( + remoteId: '1', + sourceName: 'Built-in testdata', + sourceUrl: 'https://example.com/ingredient/1', id: 1, code: '111111111', name: 'Lemon CAke', From 7e7fa387d5133c940672530b92578b1a6a46c0f5 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 11 Jul 2024 21:17:15 +0300 Subject: [PATCH 08/16] todo's --- lib/providers/nutrition.dart | 7 +++++-- lib/widgets/nutrition/forms.dart | 3 ++- lib/widgets/nutrition/helpers.dart | 1 + lib/widgets/nutrition/widgets.dart | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index f6bd5af9..532617d5 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -132,6 +132,7 @@ class NutritionPlansProvider with ChangeNotifier { try { plan = findById(planId); } on NoSuchEntryException { + // TODO: remove this useless call, because we will fetch all details below plan = await fetchAndSetPlanSparse(planId); } @@ -145,6 +146,7 @@ class NutritionPlansProvider with ChangeNotifier { final List mealItems = []; final meal = Meal.fromJson(mealData); + // TODO: we should add these ingredients to the ingredient cache for (final mealItemData in mealData['meal_items']) { final mealItem = MealItem.fromJson(mealItemData); @@ -376,9 +378,10 @@ class NutritionPlansProvider with ChangeNotifier { if (data['count'] == 0) { return null; - } else { - return Ingredient.fromJson(data['results'][0]); } + // TODO we should probably add it to ingredient cache. + // TODO: we could also use the ingredient cache for code searches + return Ingredient.fromJson(data['results'][0]); } /// Log meal to nutrition diary diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 23928bac..afd70036 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -353,7 +353,7 @@ class IngredientFormState extends State { child: Column( children: [ Text( - 'Macros preview', + 'Macros preview', // TODO fix l10n style: Theme.of(context).textTheme.titleMedium, ), FutureBuilder( @@ -367,6 +367,7 @@ class IngredientFormState extends State { ) { if (snapshot.hasData) { _mealItem.ingredient = snapshot.data!; + // here it could be nice to put the button to the popup return MealItemValuesTile( ingredient: _mealItem.ingredient, nutritionalValues: _mealItem.nutritionalValues, diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 27e45761..3fc47ca5 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -107,6 +107,7 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { final url = context.read().baseProvider.auth.serverUrl; +// TODO: display source name and source URL for an ingredient in the UI showDialog( context: context, builder: (context) => FutureBuilder( diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index ba3dc0b6..ed79f111 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -224,6 +224,7 @@ class _IngredientTypeaheadState extends State { mainAxisSize: MainAxisSize.min, children: [ Text(AppLocalizations.of(context).productFoundDescription(result.name)), + // TODO replace with full view instead of small popup MealItemValuesTile( ingredient: result, nutritionalValues: result.nutritionalValues, From 2ef68ce24d41b39a57b1d0c73a65b67db9a95512 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 11:12:47 +0300 Subject: [PATCH 09/16] cleanup --- lib/providers/nutrition.dart | 2 -- test/nutrition/nutrition_provider_test.dart | 1 - 2 files changed, 3 deletions(-) diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 532617d5..9c88eee7 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -38,11 +38,9 @@ class NutritionPlansProvider with ChangeNotifier { static const _nutritionalPlansInfoPath = 'nutritionplaninfo'; static const _mealPath = 'meal'; static const _mealItemPath = 'mealitem'; - static const _ingredientPath = 'ingredient'; static const _ingredientInfoPath = 'ingredientinfo'; static const _ingredientSearchPath = 'ingredient/search'; static const _nutritionDiaryPath = 'nutritiondiary'; - static const _ingredientImagePath = 'ingredient-image'; final WgerBaseProvider baseProvider; List _plans = []; diff --git a/test/nutrition/nutrition_provider_test.dart b/test/nutrition/nutrition_provider_test.dart index 39ed669a..ae1a747f 100644 --- a/test/nutrition/nutrition_provider_test.dart +++ b/test/nutrition/nutrition_provider_test.dart @@ -19,7 +19,6 @@ void main() { const String planInfoUrl = 'nutritionplaninfo'; const String planUrl = 'nutritionplan'; const String diaryUrl = 'nutritiondiary'; - const String ingredientUrl = 'ingredient'; final Map nutritionalPlanInfoResponse = jsonDecode( fixture('nutrition/nutritional_plan_info_detail_response.json'), From fea019f51cd9f528c65174f1c795390bd2e11392 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 12:35:23 +0300 Subject: [PATCH 10/16] show source name and link to source, when possible in popup --- lib/widgets/nutrition/helpers.dart | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 3fc47ca5..422616a5 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:wger/helpers/misc.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/nutritional_goals.dart'; @@ -105,9 +106,8 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { // we also don't get an image when querying the API // however, the typeahead suggestion does get an image, so we allow passing it... - final url = context.read().baseProvider.auth.serverUrl; + final serverURL = context.read().baseProvider.auth.serverUrl; -// TODO: display source name and source URL for an ingredient in the UI showDialog( context: context, builder: (context) => FutureBuilder( @@ -115,10 +115,16 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { builder: (BuildContext context, AsyncSnapshot snapshot) { Ingredient? ingredient; NutritionalGoals? goals; + String? source; + String? url; if (snapshot.hasData) { ingredient = snapshot.data; goals = ingredient!.nutritionalValues.toGoals(); + source = ingredient.sourceName ?? 'unknown'; + url = ingredient.remoteId == null + ? null + : 'https://world.openfoodfacts.org/product/${ingredient.remoteId}'; } return AlertDialog( title: (snapshot.hasData) ? Text(ingredient!.name) : null, @@ -128,7 +134,7 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { mainAxisSize: MainAxisSize.min, children: [ if (image != null) - CircleAvatar(backgroundImage: NetworkImage(url! + image), radius: 128), + CircleAvatar(backgroundImage: NetworkImage(serverURL! + image), radius: 128), if (image != null) const SizedBox(height: 12), if (snapshot.hasError) Text( @@ -145,6 +151,15 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { showGperKg: false, ), ), + if (snapshot.hasData && url == null) Text('Source: ${source!}'), + if (snapshot.hasData && url != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: InkWell( + child: Text('Source: ${source!}'), + onTap: () => launchURL(url!, context), + ), + ), ], ), ), From 5fa08894ce76d3926bbd46482038ed7f5de709f2 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 12:36:25 +0300 Subject: [PATCH 11/16] update nutritional plan info detail response fixture with new plan --- ...nutritional_plan_info_detail_response.json | 2605 +++++++++++++++-- 1 file changed, 2402 insertions(+), 203 deletions(-) diff --git a/test/fixtures/nutrition/nutritional_plan_info_detail_response.json b/test/fixtures/nutrition/nutritional_plan_info_detail_response.json index ba07b705..3c1f0c5e 100644 --- a/test/fixtures/nutrition/nutritional_plan_info_detail_response.json +++ b/test/fixtures/nutrition/nutritional_plan_info_detail_response.json @@ -1,214 +1,2413 @@ { - "id": 1, - "language": { - "id": 1, - "short_name": "de", - "full_name": "Deutsch" - }, - "creation_date": "2022-08-09", - "only_logging": false, - "goal_energy": null, - "goal_protein": null, - "goal_carbohydrates": null, - "goal_fat": null, - "goal_fiber": null, - "description": "", - "get_nutritional_values": { - "total": { - "energy": 853.0, - "protein": 12.08, - "carbohydrates": 180.7, - "carbohydrates_sugar": 139.62, - "fat": 4.92, - "fat_saturated": 0.71, - "fiber": 11.96, - "sodium": 0.48, - "energy_kilojoule": 3568.95 - }, - "percent": { - "protein": 5.66, - "carbohydrates": 84.74, - "fat": 5.19 - }, - "per_kg": { - "protein": 0.0, - "carbohydrates": 0.0, - "fat": 0.0 - } - }, - "meals": [ - { - "id": 1, - "plan": 1, - "order": 1, - "time": "08:00:00", - "name": "Frühstück", - "meal_items": [ + "id": 92547, + "creation_date": "2024-04-01", + "description": "first real cut", + "only_logging": false, + "goal_energy": 1600, + "goal_protein": 150, + "goal_carbohydrates": null, + "goal_fat": 70, + "goal_fiber": 50, + "meals": [ { - "id": 1, - "meal": 1, - "ingredient": 59887, - "ingredient_obj": { - "id": 59887, - "remote_id": 123456789, - "source_name": "Open Food Facts", - "source_url" : "https://world.openfoodfacts.org/ingredients/123456789", - "code": "4311501354155", - "name": "Baked Beans", - "created": "2020-12-20", - "last_update": "2022-08-09", - "energy": 86, - "protein": "4.400", - "carbohydrates": "11.000", - "carbohydrates_sugar": "5.300", - "fat": "0.100", - "fat_saturated": "0.000", - "fiber": "5.000", - "sodium": "0.480", - "license": { - "id": 5, - "full_name": "Open Data Commons Open Database License", - "short_name": "ODbL", - "url": "https://opendatacommons.org/licenses/odbl/" - }, - "license_author": "Open Food Facts", - "ingredientweightunit_set": [], - "language": { - "id": 1, - "short_name": "de", - "full_name": "Deutsch" + "id": 218388, + "plan": 92547, + "order": 1, + "time": "09:00:00", + "name": "eggs: 3 boiled", + "meal_items": [ + { + "id": 497742, + "meal": 218388, + "ingredient": 118, + "ingredient_obj": { + "id": 118, + "uuid": "ebcd2af9-682c-4f7b-bfaa-e9597b183625", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Egg, whole, cooked, hard-boiled", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T09:06:53.675883+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 155, + "protein": "12.580", + "carbohydrates": "1.120", + "carbohydrates_sugar": "1.120", + "fat": "10.610", + "fat_saturated": "3.267", + "fiber": "0.000", + "sodium": "0.124", + "weight_units": [ + { + "gram": 136, + "amount": "1.00", + "unit": { + "id": 60, + "name": "cup, chopped", + "language": 2 + } + }, + { + "gram": 8, + "amount": "1.00", + "unit": { + "id": 2, + "name": "tbsp", + "language": 2 + } + }, + { + "gram": 50, + "amount": "1.00", + "unit": { + "id": 57, + "name": "large", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "180.00" + } + ], + "nutritional_values": { + "energy": 279.0, + "protein": 22.644, + "carbohydrates": 2.016, + "carbohydrates_sugar": 2.016, + "fat": 19.098, + "fat_saturated": 5.8806, + "fiber": null, + "sodium": 0.2232 } - }, - "weight_unit": null, - "weight_unit_obj": null, - "image": null, - "order": 1, - "amount": "100.00" }, { - "id": 2, - "meal": 1, - "ingredient": 58300, - "ingredient_obj": { - "id": 58300, - "remote_id": 123456788, - "source_name": "Open Food Facts", - "source_url" : "https://world.openfoodfacts.org/ingredients/123456788", - "code": "4071800000992", - "name": "1688 Mehrkorn", - "created": "2020-12-20", - "last_update": "2022-08-09", - "energy": 229, - "protein": "7.680", - "carbohydrates": "35.700", - "carbohydrates_sugar": "2.320", - "fat": "4.820", - "fat_saturated": "0.714", - "fiber": "6.960", - "sodium": "0.000", - "license": { - "id": 5, - "full_name": "Open Data Commons Open Database License", - "short_name": "ODbL", - "url": "https://opendatacommons.org/licenses/odbl/" - }, - "license_author": "Open Food Facts", - "ingredientweightunit_set": [], - "language": { - "id": 2, - "short_name": "en", - "full_name": "English" + "id": 218580, + "plan": 92547, + "order": 1, + "time": "09:09:00", + "name": "Tim Ferris eggs", + "meal_items": [ + { + "id": 497926, + "meal": 218580, + "ingredient": 113, + "ingredient_obj": { + "id": 113, + "uuid": "6e0e7258-f2e5-4c5f-9e38-abfa5c77cbf5", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Egg, white, raw, fresh", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T09:06:53.660011+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 52, + "protein": "10.900", + "carbohydrates": "0.730", + "carbohydrates_sugar": "0.710", + "fat": "0.170", + "fat_saturated": "0.000", + "fiber": "0.000", + "sodium": "0.166", + "weight_units": [ + { + "gram": 243, + "amount": "1.00", + "unit": { + "id": 1, + "name": "cup", + "language": 2 + } + }, + { + "gram": 33, + "amount": "1.00", + "unit": { + "id": 57, + "name": "large", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "150.00" + }, + { + "id": 497927, + "meal": 218580, + "ingredient": 87821, + "ingredient_obj": { + "id": 87821, + "uuid": "fd2b54d8-8c2f-4cf4-8f4b-57ebad5d8619", + "remote_id": "01137637", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/01137637.json", + "code": "01137637", + "name": "Semi Skimmed Milk", + "created": "2024-01-25T11:12:04.028000+01:00", + "last_update": "2024-05-28T20:46:45.266271+02:00", + "last_imported": "2024-01-25T11:12:04.028000+01:00", + "energy": 50, + "protein": "3.600", + "carbohydrates": "4.800", + "carbohydrates_sugar": "4.800", + "fat": "1.800", + "fat_saturated": "1.100", + "fiber": "0.500", + "sodium": "0.044", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 46522, + "uuid": "c231fef8-11c7-47ac-ba0a-f96f81b9127d", + "ingredient_id": 87821, + "ingredient_uuid": "fd2b54d8-8c2f-4cf4-8f4b-57ebad5d8619", + "image": "https://wger.de/media/ingredients/87821/c231fef8-11c7-47ac-ba0a-f96f81b9127d.jpg", + "created": "2024-03-27T11:48:40.865557+01:00", + "last_update": "2024-03-27T11:48:40.869835+01:00", + "size": 35092, + "width": 297, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=01137637&id=1", + "license_author": "kyzh", + "license_author_url": "https://world.openfoodfacts.org/photographer/kyzh", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Semi Skimmed Milk", + "license_object_url": "https://world.openfoodfacts.org/product/01137637/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 46522, + "uuid": "c231fef8-11c7-47ac-ba0a-f96f81b9127d", + "ingredient_id": 87821, + "ingredient_uuid": "fd2b54d8-8c2f-4cf4-8f4b-57ebad5d8619", + "image": "https://wger.de/media/ingredients/87821/c231fef8-11c7-47ac-ba0a-f96f81b9127d.jpg", + "created": "2024-03-27T11:48:40.865557+01:00", + "last_update": "2024-03-27T11:48:40.869835+01:00", + "size": 35092, + "width": 297, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=01137637&id=1", + "license_author": "kyzh", + "license_author_url": "https://world.openfoodfacts.org/photographer/kyzh", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "40.00" + } + ], + "nutritional_values": { + "energy": 98.0, + "protein": 17.79, + "carbohydrates": 3.015, + "carbohydrates_sugar": 2.985, + "fat": 0.975, + "fat_saturated": 0.44, + "fiber": 0.2, + "sodium": 0.2666 } - }, - "weight_unit": null, - "weight_unit_obj": null, - "image": { - "id": 3, - "uuid": "778fe5cb-a5d9-42e5-9df2-2a47022b720f", - "ingredient_id": 58300, - "ingredient_uuid": "60e1f479-9af1-4bd4-b2a8-1ebe8974258e", - "image": "http://localhost:8000/media/ingredients/60e1f479-9af1-4bd4-b2a8-1ebe8974258e/778fe5cb-a5d9-42e5-9df2-2a47022b720f.jpg", - "last_update": "2022-08-09T15:31:46.833918+02:00", - "size": 33617, - "source_url": "", - "license": 1, - "license_author": "kiliweb", - "license_title": "Photo", - "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=4388844120334&id=2", - "license_author_url": "https://world.openfoodfacts.org/photographer/twoflower", - "license_derivative_source_url": "" - }, - "order": 1, - "amount": "100.00" }, { - "id": 3, - "meal": 1, - "ingredient": 10065, - "ingredient_obj": { - "id": 10065, - "remote_id": 123456787, - "source_name": "Open Food Facts", - "source_url" : "https://world.openfoodfacts.org/ingredients/123456787", - "code": "0043647440020", - "name": "'Old Times' Orange Fine Cut Marmalade", - "created": "2020-12-20", - "last_update": "2022-08-09", - "energy": 269, - "protein": "0.000", - "carbohydrates": "67.000", - "carbohydrates_sugar": "66.000", - "fat": "0.000", - "fat_saturated": "0.000", - "fiber": null, - "sodium": "0.000", - "license": { - "id": 5, - "full_name": "Open Data Commons Open Database License", - "short_name": "ODbL", - "url": "https://opendatacommons.org/licenses/odbl/" - }, - "license_author": "Open Food Facts", - "ingredientweightunit_set": [], - "language": { - "id": 2, - "short_name": "en", - "full_name": "English" + "id": 218399, + "plan": 92547, + "order": 1, + "time": "09:10:00", + "name": "Tim Ferris eggs with \ud83d\udc14", + "meal_items": [ + { + "id": 497752, + "meal": 218399, + "ingredient": 113, + "ingredient_obj": { + "id": 113, + "uuid": "6e0e7258-f2e5-4c5f-9e38-abfa5c77cbf5", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Egg, white, raw, fresh", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T09:06:53.660011+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 52, + "protein": "10.900", + "carbohydrates": "0.730", + "carbohydrates_sugar": "0.710", + "fat": "0.170", + "fat_saturated": "0.000", + "fiber": "0.000", + "sodium": "0.166", + "weight_units": [ + { + "gram": 243, + "amount": "1.00", + "unit": { + "id": 1, + "name": "cup", + "language": 2 + } + }, + { + "gram": 33, + "amount": "1.00", + "unit": { + "id": 57, + "name": "large", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "150.00" + }, + { + "id": 497754, + "meal": 218399, + "ingredient": 62302, + "ingredient_obj": { + "id": 62302, + "uuid": "824965f8-8be9-4544-b798-eac8425a5a6a", + "remote_id": "5018374066428", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/5018374066428.json", + "code": "5018374066428", + "name": "Semi-skimmed milk", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-05-28T20:46:45.347791+02:00", + "last_imported": "2020-12-20T14:47:08.877000+01:00", + "energy": 50, + "protein": "3.600", + "carbohydrates": "4.800", + "carbohydrates_sugar": "4.800", + "fat": "1.800", + "fat_saturated": "1.100", + "fiber": "0.000", + "sodium": "0.040", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 3749, + "uuid": "b8cb30ff-0930-4320-a24a-eeebd806906e", + "ingredient_id": 62302, + "ingredient_uuid": "824965f8-8be9-4544-b798-eac8425a5a6a", + "image": "https://wger.de/media/ingredients/62302/b8cb30ff-0930-4320-a24a-eeebd806906e.jpg", + "created": "2023-04-10T17:33:11.342989+02:00", + "last_update": "2023-04-10T17:33:11.348083+02:00", + "size": 23429, + "width": 400, + "height": 300, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5018374066428&id=1", + "license_author": "openfoodfacts-contributors", + "license_author_url": "https://world.openfoodfacts.org/photographer/openfoodfacts-contributors", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Semi-skimmed milk", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 3749, + "uuid": "b8cb30ff-0930-4320-a24a-eeebd806906e", + "ingredient_id": 62302, + "ingredient_uuid": "824965f8-8be9-4544-b798-eac8425a5a6a", + "image": "https://wger.de/media/ingredients/62302/b8cb30ff-0930-4320-a24a-eeebd806906e.jpg", + "created": "2023-04-10T17:33:11.342989+02:00", + "last_update": "2023-04-10T17:33:11.348083+02:00", + "size": 23429, + "width": 400, + "height": 300, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5018374066428&id=1", + "license_author": "openfoodfacts-contributors", + "license_author_url": "https://world.openfoodfacts.org/photographer/openfoodfacts-contributors", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "40.00" + }, + { + "id": 497755, + "meal": 218399, + "ingredient": 144977, + "ingredient_obj": { + "id": 144977, + "uuid": "23b66a85-98ab-484f-9bb8-efd3f0257171", + "remote_id": "20802301", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/20802301.json", + "code": "20802301", + "name": "Roast chicken breast slices", + "created": "2024-01-25T11:19:37.262000+01:00", + "last_update": "2024-06-07T15:45:16.330879+02:00", + "last_imported": "2024-01-25T11:19:37.262000+01:00", + "energy": 148, + "protein": "28.200", + "carbohydrates": "0.400", + "carbohydrates_sugar": "0.400", + "fat": "3.400", + "fat_saturated": "0.800", + "fiber": "0.500", + "sodium": "0.224", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 28255, + "uuid": "a6d25fab-6917-42a1-b337-d14edd0349ff", + "ingredient_id": 144977, + "ingredient_uuid": "23b66a85-98ab-484f-9bb8-efd3f0257171", + "image": "https://wger.de/media/ingredients/144977/a6d25fab-6917-42a1-b337-d14edd0349ff.jpg", + "created": "2024-01-27T00:44:34.128137+01:00", + "last_update": "2024-01-27T00:44:34.132572+01:00", + "size": 22997, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=20802301&id=3", + "license_author": "alia", + "license_author_url": "https://world.openfoodfacts.org/photographer/alia", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Roast chicken breast slices", + "license_object_url": "https://world.openfoodfacts.org/product/20802301/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 28255, + "uuid": "a6d25fab-6917-42a1-b337-d14edd0349ff", + "ingredient_id": 144977, + "ingredient_uuid": "23b66a85-98ab-484f-9bb8-efd3f0257171", + "image": "https://wger.de/media/ingredients/144977/a6d25fab-6917-42a1-b337-d14edd0349ff.jpg", + "created": "2024-01-27T00:44:34.128137+01:00", + "last_update": "2024-01-27T00:44:34.132572+01:00", + "size": 22997, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=20802301&id=3", + "license_author": "alia", + "license_author_url": "https://world.openfoodfacts.org/photographer/alia", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "100.00" + } + ], + "nutritional_values": { + "energy": 246.0, + "protein": 45.99, + "carbohydrates": 3.415, + "carbohydrates_sugar": 3.385, + "fat": 4.375, + "fat_saturated": 1.24, + "fiber": 0.5, + "sodium": 0.489 + } + }, + { + "id": 218581, + "plan": 92547, + "order": 1, + "time": "09:11:00", + "name": "Tim Ferris eggs with \ud83e\udd66", + "meal_items": [ + { + "id": 497928, + "meal": 218581, + "ingredient": 113, + "ingredient_obj": { + "id": 113, + "uuid": "6e0e7258-f2e5-4c5f-9e38-abfa5c77cbf5", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Egg, white, raw, fresh", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T09:06:53.660011+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 52, + "protein": "10.900", + "carbohydrates": "0.730", + "carbohydrates_sugar": "0.710", + "fat": "0.170", + "fat_saturated": "0.000", + "fiber": "0.000", + "sodium": "0.166", + "weight_units": [ + { + "gram": 243, + "amount": "1.00", + "unit": { + "id": 1, + "name": "cup", + "language": 2 + } + }, + { + "gram": 33, + "amount": "1.00", + "unit": { + "id": 57, + "name": "large", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "150.00" + }, + { + "id": 497929, + "meal": 218581, + "ingredient": 78953, + "ingredient_obj": { + "id": 78953, + "uuid": "3d3cff79-2f11-456a-9e27-5a4cab39373f", + "remote_id": "00357937", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/00357937.json", + "code": "00357937", + "name": "Semi Skimmed Milk", + "created": "2023-02-08T01:00:00+01:00", + "last_update": "2024-05-28T20:46:45.271685+02:00", + "last_imported": "2023-02-08T21:45:47.332000+01:00", + "energy": 50, + "protein": "3.600", + "carbohydrates": "4.800", + "carbohydrates_sugar": "4.800", + "fat": "1.800", + "fat_saturated": "1.100", + "fiber": "0.500", + "sodium": "0.044", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 3752, + "uuid": "fc78e749-d2b2-4d89-be50-99eb27194ea3", + "ingredient_id": 78953, + "ingredient_uuid": "3d3cff79-2f11-456a-9e27-5a4cab39373f", + "image": "https://wger.de/media/ingredients/78953/fc78e749-d2b2-4d89-be50-99eb27194ea3.jpg", + "created": "2023-04-10T17:33:11.648421+02:00", + "last_update": "2023-04-10T17:33:11.660500+02:00", + "size": 40380, + "width": 297, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=00357937&id=1", + "license_author": "kyzh", + "license_author_url": "https://world.openfoodfacts.org/photographer/kyzh", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Semi Skimmed Milk", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 3752, + "uuid": "fc78e749-d2b2-4d89-be50-99eb27194ea3", + "ingredient_id": 78953, + "ingredient_uuid": "3d3cff79-2f11-456a-9e27-5a4cab39373f", + "image": "https://wger.de/media/ingredients/78953/fc78e749-d2b2-4d89-be50-99eb27194ea3.jpg", + "created": "2023-04-10T17:33:11.648421+02:00", + "last_update": "2023-04-10T17:33:11.660500+02:00", + "size": 40380, + "width": 297, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=00357937&id=1", + "license_author": "kyzh", + "license_author_url": "https://world.openfoodfacts.org/photographer/kyzh", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "40.00" + }, + { + "id": 497930, + "meal": 218581, + "ingredient": 2858, + "ingredient_obj": { + "id": 2858, + "uuid": "45e2dc8f-5ad4-4cad-94cf-5cdb7b13e941", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Broccoli, raw", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T07:14:54.191525+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 34, + "protein": "2.820", + "carbohydrates": "6.640", + "carbohydrates_sugar": "1.700", + "fat": "0.370", + "fat_saturated": "0.039", + "fiber": "2.600", + "sodium": "0.033", + "weight_units": [ + { + "gram": 91, + "amount": "1.00", + "unit": { + "id": 652, + "name": "cup chopped", + "language": 2 + } + }, + { + "gram": 608, + "amount": "1.00", + "unit": { + "id": 653, + "name": "bunch", + "language": 2 + } + }, + { + "gram": 31, + "amount": "1.00", + "unit": { + "id": 633, + "name": "spear (about 5\" long)", + "language": 2 + } + }, + { + "gram": 151, + "amount": "1.00", + "unit": { + "id": 532, + "name": "stalk", + "language": 2 + } + }, + { + "gram": 44, + "amount": "0.50", + "unit": { + "id": 155, + "name": "cup, chopped or diced", + "language": 2 + } + }, + { + "gram": 148, + "amount": "1.00", + "unit": { + "id": 412, + "name": "NLEA serving", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "100.00" + }, + { + "id": 497963, + "meal": 218581, + "ingredient": 113, + "ingredient_obj": { + "id": 113, + "uuid": "6e0e7258-f2e5-4c5f-9e38-abfa5c77cbf5", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Egg, white, raw, fresh", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T09:06:53.660011+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 52, + "protein": "10.900", + "carbohydrates": "0.730", + "carbohydrates_sugar": "0.710", + "fat": "0.170", + "fat_saturated": "0.000", + "fiber": "0.000", + "sodium": "0.166", + "weight_units": [ + { + "gram": 243, + "amount": "1.00", + "unit": { + "id": 1, + "name": "cup", + "language": 2 + } + }, + { + "gram": 33, + "amount": "1.00", + "unit": { + "id": 57, + "name": "large", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "900.00" + } + ], + "nutritional_values": { + "energy": 600.0, + "protein": 118.71, + "carbohydrates": 16.225, + "carbohydrates_sugar": 11.075, + "fat": 2.875, + "fat_saturated": 0.479, + "fiber": 2.8, + "sodium": 1.7936 + } + }, + { + "id": 218595, + "plan": 92547, + "order": 1, + "time": "09:12:00", + "name": "Tim Ferris eggs with \ud83d\udc1f", + "meal_items": [ + { + "id": 497942, + "meal": 218595, + "ingredient": 113, + "ingredient_obj": { + "id": 113, + "uuid": "6e0e7258-f2e5-4c5f-9e38-abfa5c77cbf5", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Egg, white, raw, fresh", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T09:06:53.660011+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 52, + "protein": "10.900", + "carbohydrates": "0.730", + "carbohydrates_sugar": "0.710", + "fat": "0.170", + "fat_saturated": "0.000", + "fiber": "0.000", + "sodium": "0.166", + "weight_units": [ + { + "gram": 243, + "amount": "1.00", + "unit": { + "id": 1, + "name": "cup", + "language": 2 + } + }, + { + "gram": 33, + "amount": "1.00", + "unit": { + "id": 57, + "name": "large", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "150.00" + }, + { + "id": 497943, + "meal": 218595, + "ingredient": 81287, + "ingredient_obj": { + "id": 81287, + "uuid": "8c076cad-0298-412a-8ab6-28951dfccdcb", + "remote_id": "5052910873354", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/5052910873354.json", + "code": "5052910873354", + "name": "Smoked Salmon Slices", + "created": "2023-02-08T01:00:00+01:00", + "last_update": "2024-05-28T20:46:53.652408+02:00", + "last_imported": "2023-02-08T21:46:25.205000+01:00", + "energy": 185, + "protein": "22.300", + "carbohydrates": "1.500", + "carbohydrates_sugar": "0.300", + "fat": "9.900", + "fat_saturated": "1.700", + "fiber": "0.400", + "sodium": "1.360", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 6553, + "uuid": "32f7a231-fd39-4b13-a723-792ce5d0472f", + "ingredient_id": 81287, + "ingredient_uuid": "8c076cad-0298-412a-8ab6-28951dfccdcb", + "image": "https://wger.de/media/ingredients/81287/32f7a231-fd39-4b13-a723-792ce5d0472f.jpg", + "created": "2023-04-16T20:17:01.216005+02:00", + "last_update": "2023-04-16T20:17:01.237703+02:00", + "size": 30230, + "width": 400, + "height": 387, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5052910873354&id=6", + "license_author": "roccait", + "license_author_url": "https://world.openfoodfacts.org/photographer/roccait", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Smoked Salmon Slices", + "license_object_url": "https://world.openfoodfacts.org/product/5052910873354/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 6553, + "uuid": "32f7a231-fd39-4b13-a723-792ce5d0472f", + "ingredient_id": 81287, + "ingredient_uuid": "8c076cad-0298-412a-8ab6-28951dfccdcb", + "image": "https://wger.de/media/ingredients/81287/32f7a231-fd39-4b13-a723-792ce5d0472f.jpg", + "created": "2023-04-16T20:17:01.216005+02:00", + "last_update": "2023-04-16T20:17:01.237703+02:00", + "size": 30230, + "width": 400, + "height": 387, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5052910873354&id=6", + "license_author": "roccait", + "license_author_url": "https://world.openfoodfacts.org/photographer/roccait", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "40.00" + }, + { + "id": 497944, + "meal": 218595, + "ingredient": 87821, + "ingredient_obj": { + "id": 87821, + "uuid": "fd2b54d8-8c2f-4cf4-8f4b-57ebad5d8619", + "remote_id": "01137637", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/01137637.json", + "code": "01137637", + "name": "Semi Skimmed Milk", + "created": "2024-01-25T11:12:04.028000+01:00", + "last_update": "2024-05-28T20:46:45.266271+02:00", + "last_imported": "2024-01-25T11:12:04.028000+01:00", + "energy": 50, + "protein": "3.600", + "carbohydrates": "4.800", + "carbohydrates_sugar": "4.800", + "fat": "1.800", + "fat_saturated": "1.100", + "fiber": "0.500", + "sodium": "0.044", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 46522, + "uuid": "c231fef8-11c7-47ac-ba0a-f96f81b9127d", + "ingredient_id": 87821, + "ingredient_uuid": "fd2b54d8-8c2f-4cf4-8f4b-57ebad5d8619", + "image": "https://wger.de/media/ingredients/87821/c231fef8-11c7-47ac-ba0a-f96f81b9127d.jpg", + "created": "2024-03-27T11:48:40.865557+01:00", + "last_update": "2024-03-27T11:48:40.869835+01:00", + "size": 35092, + "width": 297, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=01137637&id=1", + "license_author": "kyzh", + "license_author_url": "https://world.openfoodfacts.org/photographer/kyzh", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Semi Skimmed Milk", + "license_object_url": "https://world.openfoodfacts.org/product/01137637/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 46522, + "uuid": "c231fef8-11c7-47ac-ba0a-f96f81b9127d", + "ingredient_id": 87821, + "ingredient_uuid": "fd2b54d8-8c2f-4cf4-8f4b-57ebad5d8619", + "image": "https://wger.de/media/ingredients/87821/c231fef8-11c7-47ac-ba0a-f96f81b9127d.jpg", + "created": "2024-03-27T11:48:40.865557+01:00", + "last_update": "2024-03-27T11:48:40.869835+01:00", + "size": 35092, + "width": 297, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=01137637&id=1", + "license_author": "kyzh", + "license_author_url": "https://world.openfoodfacts.org/photographer/kyzh", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "10.00" + } + ], + "nutritional_values": { + "energy": 157.0, + "protein": 25.63, + "carbohydrates": 2.175, + "carbohydrates_sugar": 1.665, + "fat": 4.395, + "fat_saturated": 0.79, + "fiber": 0.21, + "sodium": 0.7974 + } + }, + { + "id": 218400, + "plan": 92547, + "order": 1, + "time": "10:26:00", + "name": "yoghurt blueberries nuts", + "meal_items": [ + { + "id": 497756, + "meal": 218400, + "ingredient": 64952, + "ingredient_obj": { + "id": 64952, + "uuid": "0e92d0de-e0b7-4399-9615-605e2280eccb", + "remote_id": "5412971116072", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/5412971116072.json", + "code": "5412971116072", + "name": "Yaourt bio nature", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-05-28T20:48:23.954186+02:00", + "last_imported": "2020-12-20T14:47:17.136000+01:00", + "energy": 73, + "protein": "4.200", + "carbohydrates": "5.400", + "carbohydrates_sugar": "5.100", + "fat": "3.800", + "fat_saturated": "2.400", + "fiber": "0.000", + "sodium": "0.000", + "weight_units": [], + "language": { + "id": 12, + "short_name": "fr", + "full_name": "Fran\u00e7ais", + "full_name_en": "French" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Yaourt bio nature", + "license_object_url": "https://world.openfoodfacts.org/product/5412971116072/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "200.00" + }, + { + "id": 497757, + "meal": 218400, + "ingredient": 2185, + "ingredient_obj": { + "id": 2185, + "uuid": "9e83662b-d578-45a0-a83a-33ab7c34deec", + "remote_id": null, + "source_name": null, + "source_url": null, + "code": null, + "name": "Blueberries, raw", + "created": "2013-04-14T02:00:00+02:00", + "last_update": "2024-06-07T07:03:23.961236+02:00", + "last_imported": "2020-12-20T14:44:45.874000+01:00", + "energy": 57, + "protein": "0.740", + "carbohydrates": "14.490", + "carbohydrates_sugar": "9.960", + "fat": "0.330", + "fat_saturated": "0.028", + "fiber": "2.400", + "sodium": "0.001", + "weight_units": [ + { + "gram": 148, + "amount": "1.00", + "unit": { + "id": 1, + "name": "cup", + "language": 2 + } + }, + { + "gram": 68, + "amount": "50.00", + "unit": { + "id": 434, + "name": "berries", + "language": 2 + } + } + ], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 1, + "full_name": " Creative Commons Attribution Share Alike 3", + "short_name": "CC-BY-SA 3", + "url": "https://creativecommons.org/licenses/by-sa/3.0/deed.en" + }, + "license_title": "", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "100.00" + }, + { + "id": 497758, + "meal": 218400, + "ingredient": 87105, + "ingredient_obj": { + "id": 87105, + "uuid": "62ac5e74-613e-4625-99d7-80614e7577c3", + "remote_id": "0029000016651", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/0029000016651.json", + "code": "0029000016651", + "name": "Mixed Nuts", + "created": "2024-01-25T11:11:45.406000+01:00", + "last_update": "2024-05-28T20:43:31.490793+02:00", + "last_imported": "2024-01-25T11:11:45.406000+01:00", + "energy": 607, + "protein": "21.400", + "carbohydrates": "21.400", + "carbohydrates_sugar": "3.570", + "fat": "53.600", + "fat_saturated": "5.360", + "fiber": "7.140", + "sodium": "0.321", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 43939, + "uuid": "879d670f-3a5c-4337-ab77-c04f6fe24160", + "ingredient_id": 87105, + "ingredient_uuid": "62ac5e74-613e-4625-99d7-80614e7577c3", + "image": "https://wger.de/media/ingredients/87105/879d670f-3a5c-4337-ab77-c04f6fe24160.jpg", + "created": "2024-03-16T17:44:47.379441+01:00", + "last_update": "2024-03-16T17:44:47.382729+01:00", + "size": 34984, + "width": 375, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=0029000016651&id=3", + "license_author": "openfoodfacts-contributors", + "license_author_url": "https://world.openfoodfacts.org/photographer/openfoodfacts-contributors", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Mixed Nuts", + "license_object_url": "https://world.openfoodfacts.org/product/0029000016651/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 43939, + "uuid": "879d670f-3a5c-4337-ab77-c04f6fe24160", + "ingredient_id": 87105, + "ingredient_uuid": "62ac5e74-613e-4625-99d7-80614e7577c3", + "image": "https://wger.de/media/ingredients/87105/879d670f-3a5c-4337-ab77-c04f6fe24160.jpg", + "created": "2024-03-16T17:44:47.379441+01:00", + "last_update": "2024-03-16T17:44:47.382729+01:00", + "size": 34984, + "width": 375, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=0029000016651&id=3", + "license_author": "openfoodfacts-contributors", + "license_author_url": "https://world.openfoodfacts.org/photographer/openfoodfacts-contributors", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "20.00" + } + ], + "nutritional_values": { + "energy": 324.4, + "protein": 13.42, + "carbohydrates": 29.57, + "carbohydrates_sugar": 20.874, + "fat": 18.65, + "fat_saturated": 5.9, + "fiber": 3.828, + "sodium": 0.0652 + } + }, + { + "id": 218389, + "plan": 92547, + "order": 1, + "time": "11:50:00", + "name": "high protein yogurt nuts", + "meal_items": [ + { + "id": 497745, + "meal": 218389, + "ingredient": 138913, + "ingredient_obj": { + "id": 138913, + "uuid": "5434d4ab-72fa-4604-9b1e-cce64a882f04", + "remote_id": "5057373546791", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/5057373546791.json", + "code": "5057373546791", + "name": "Walnuts", + "created": "2024-01-25T11:17:59.017000+01:00", + "last_update": "2024-05-28T20:48:15.419389+02:00", + "last_imported": "2024-01-25T11:17:59.017000+01:00", + "energy": 701, + "protein": "14.700", + "carbohydrates": "3.300", + "carbohydrates_sugar": "2.600", + "fat": "68.500", + "fat_saturated": "7.500", + "fiber": "6.300", + "sodium": "0.004", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 43151, + "uuid": "dbab2f97-ca63-4773-b919-173469b4bcf7", + "ingredient_id": 138913, + "ingredient_uuid": "5434d4ab-72fa-4604-9b1e-cce64a882f04", + "image": "https://wger.de/media/ingredients/138913/dbab2f97-ca63-4773-b919-173469b4bcf7.jpg", + "created": "2024-03-14T10:34:08.718851+01:00", + "last_update": "2024-03-14T10:34:08.723808+01:00", + "size": 29795, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5057373546791&id=1", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Walnuts", + "license_object_url": "https://world.openfoodfacts.org/product/5057373546791/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 43151, + "uuid": "dbab2f97-ca63-4773-b919-173469b4bcf7", + "ingredient_id": 138913, + "ingredient_uuid": "5434d4ab-72fa-4604-9b1e-cce64a882f04", + "image": "https://wger.de/media/ingredients/138913/dbab2f97-ca63-4773-b919-173469b4bcf7.jpg", + "created": "2024-03-14T10:34:08.718851+01:00", + "last_update": "2024-03-14T10:34:08.723808+01:00", + "size": 29795, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5057373546791&id=1", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "10.00" + }, + { + "id": 497743, + "meal": 218389, + "ingredient": 82217, + "ingredient_obj": { + "id": 82217, + "uuid": "f95baf0f-34bf-4e48-91ec-304fa07b008e", + "remote_id": "3033491482953", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/3033491482953.json", + "code": "3033491482953", + "name": "Hipro Myrtille", + "created": "2023-02-08T01:00:00+01:00", + "last_update": "2024-06-07T10:33:10.329620+02:00", + "last_imported": "2023-02-08T21:46:51.078000+01:00", + "energy": 53, + "protein": "9.300", + "carbohydrates": "3.600", + "carbohydrates_sugar": "3.400", + "fat": "0.100", + "fat_saturated": "0.100", + "fiber": null, + "sodium": "0.044", + "weight_units": [], + "language": { + "id": 12, + "short_name": "fr", + "full_name": "Fran\u00e7ais", + "full_name_en": "French" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Hipro Myrtille", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "160.00" + } + ], + "nutritional_values": { + "energy": 154.9, + "protein": 16.35, + "carbohydrates": 6.09, + "carbohydrates_sugar": 5.7, + "fat": 7.01, + "fat_saturated": 0.91, + "fiber": 0.63, + "sodium": 0.0708 + } + }, + { + "id": 218406, + "plan": 92547, + "order": 1, + "time": "13:03:00", + "name": "shake", + "meal_items": [ + { + "id": 497975, + "meal": 218406, + "ingredient": 851132, + "ingredient_obj": { + "id": 851132, + "uuid": "6dac4f98-6f16-4178-a242-a1715f93f592", + "remote_id": "5060469986142", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/5060469986142.json", + "code": "5060469986142", + "name": "Gold standard 100% whey", + "created": "2024-05-28T21:58:20.781632+02:00", + "last_update": "2024-06-07T10:06:12.041050+02:00", + "last_imported": "2024-05-28T21:58:20.781654+02:00", + "energy": 364, + "protein": "76.000", + "carbohydrates": "4.700", + "carbohydrates_sugar": "2.300", + "fat": "4.100", + "fat_saturated": "1.500", + "fiber": null, + "sodium": "0.384", + "weight_units": [], + "language": { + "id": 12, + "short_name": "fr", + "full_name": "Fran\u00e7ais", + "full_name_en": "French" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Gold standard 100% whey", + "license_object_url": "https://world.openfoodfacts.org/product/5060469986142/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "32.00" + }, + { + "id": 497976, + "meal": 218406, + "ingredient": 646879, + "ingredient_obj": { + "id": 646879, + "uuid": "11eb021b-0336-4a67-bf88-0ad297a155bf", + "remote_id": "5060245605397", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/5060245605397.json", + "code": "5060245605397", + "name": "Micronized Creatine Powder Optimum Nutrition", + "created": "2024-05-28T21:48:50.375606+02:00", + "last_update": "2024-06-07T12:10:36.904254+02:00", + "last_imported": "2024-05-28T21:48:50.375626+02:00", + "energy": 1, + "protein": "1.000", + "carbohydrates": "1.000", + "carbohydrates_sugar": "1.000", + "fat": "1.000", + "fat_saturated": "1.000", + "fiber": null, + "sodium": "0.400", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Micronized Creatine Powder Optimum Nutrition", + "license_object_url": "https://world.openfoodfacts.org/product/5060245605397/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "3.40" + }, + { + "id": 497977, + "meal": 218406, + "ingredient": 713856, + "ingredient_obj": { + "id": 713856, + "uuid": "719133d6-86de-4152-931e-d1e254e2fb46", + "remote_id": "8710400280507", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/8710400280507.json", + "code": "8710400280507", + "name": "Biologische Halfvolle Melk", + "created": "2024-05-28T21:52:18.372468+02:00", + "last_update": "2024-06-07T06:56:39.915224+02:00", + "last_imported": "2024-05-28T21:52:18.372489+02:00", + "energy": 48, + "protein": "3.500", + "carbohydrates": "5.000", + "carbohydrates_sugar": "5.000", + "fat": "1.500", + "fat_saturated": "1.000", + "fiber": "0.000", + "sodium": "0.040", + "weight_units": [], + "language": { + "id": 6, + "short_name": "nl", + "full_name": "Nederlands", + "full_name_en": "Dutch" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Biologische Halfvolle Melk", + "license_object_url": "https://world.openfoodfacts.org/product/8710400280507/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "400.00" + } + ], + "nutritional_values": { + "energy": 308.514, + "protein": 38.354, + "carbohydrates": 21.538, + "carbohydrates_sugar": 20.77, + "fat": 7.346, + "fat_saturated": 4.514, + "fiber": null, + "sodium": 0.29648 + } + }, + { + "id": 218379, + "plan": 92547, + "order": 1, + "time": "14:56:00", + "name": "large chicken salad", + "meal_items": [ + { + "id": 497735, + "meal": 218379, + "ingredient": 10476, + "ingredient_obj": { + "id": 10476, + "uuid": "3fbe2a38-df9d-43b1-8dd1-8673921d0d52", + "remote_id": "0062356500061", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/0062356500061.json", + "code": "0062356500061", + "name": "Chick Peas", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-06-07T07:45:38.479702+02:00", + "last_imported": "2020-12-20T14:45:01.005000+01:00", + "energy": 110, + "protein": "7.000", + "carbohydrates": "18.000", + "carbohydrates_sugar": "0.000", + "fat": "2.500", + "fat_saturated": "0.000", + "fiber": "4.000", + "sodium": "0.130", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 9639, + "uuid": "f31fb1a7-48d9-47b3-95cc-abbfcfd90453", + "ingredient_id": 10476, + "ingredient_uuid": "3fbe2a38-df9d-43b1-8dd1-8673921d0d52", + "image": "https://wger.de/media/ingredients/10476/f31fb1a7-48d9-47b3-95cc-abbfcfd90453.jpg", + "created": "2023-05-04T21:38:29.025461+02:00", + "last_update": "2023-05-04T21:38:29.031300+02:00", + "size": 43623, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=0062356500061&id=4", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Chick Peas", + "license_object_url": "https://world.openfoodfacts.org/product/0062356500061/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 9639, + "uuid": "f31fb1a7-48d9-47b3-95cc-abbfcfd90453", + "ingredient_id": 10476, + "ingredient_uuid": "3fbe2a38-df9d-43b1-8dd1-8673921d0d52", + "image": "https://wger.de/media/ingredients/10476/f31fb1a7-48d9-47b3-95cc-abbfcfd90453.jpg", + "created": "2023-05-04T21:38:29.025461+02:00", + "last_update": "2023-05-04T21:38:29.031300+02:00", + "size": 43623, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=0062356500061&id=4", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "400.00" + }, + { + "id": 497736, + "meal": 218379, + "ingredient": 74571, + "ingredient_obj": { + "id": 74571, + "uuid": "b2520e85-aef9-4d9b-aaf8-03fab0709a12", + "remote_id": "8424536935711", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/8424536935711.json", + "code": "8424536935711", + "name": "Extra Virgin Olive Oil", + "created": "2020-12-20T01:00:00+01:00", + "last_update": "2024-05-28T20:39:56.771038+02:00", + "last_imported": "2020-12-20T14:47:44.111000+01:00", + "energy": 822, + "protein": "0.000", + "carbohydrates": "0.000", + "carbohydrates_sugar": "0.000", + "fat": "91.300", + "fat_saturated": "13.300", + "fiber": null, + "sodium": "0.000", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 1238, + "uuid": "222af4eb-7de8-44cb-9190-1f0bbeecb0de", + "ingredient_id": 74571, + "ingredient_uuid": "b2520e85-aef9-4d9b-aaf8-03fab0709a12", + "image": "https://wger.de/media/ingredients/74571/222af4eb-7de8-44cb-9190-1f0bbeecb0de.jpg", + "created": "2023-04-08T21:17:10.004631+02:00", + "last_update": "2023-04-08T21:17:10.019407+02:00", + "size": 14624, + "width": 135, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=8424536935711&id=1", + "license_author": "bcd4e6", + "license_author_url": "https://world.openfoodfacts.org/photographer/bcd4e6", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Extra Virgin Olive Oil", + "license_object_url": "", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 1238, + "uuid": "222af4eb-7de8-44cb-9190-1f0bbeecb0de", + "ingredient_id": 74571, + "ingredient_uuid": "b2520e85-aef9-4d9b-aaf8-03fab0709a12", + "image": "https://wger.de/media/ingredients/74571/222af4eb-7de8-44cb-9190-1f0bbeecb0de.jpg", + "created": "2023-04-08T21:17:10.004631+02:00", + "last_update": "2023-04-08T21:17:10.019407+02:00", + "size": 14624, + "width": 135, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=8424536935711&id=1", + "license_author": "bcd4e6", + "license_author_url": "https://world.openfoodfacts.org/photographer/bcd4e6", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "24.00" + }, + { + "id": 497738, + "meal": 218379, + "ingredient": 143410, + "ingredient_obj": { + "id": 143410, + "uuid": "29a56924-53ae-4b5c-8eaf-59cfc6d49f26", + "remote_id": "5010236157817", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/5010236157817.json", + "code": "5010236157817", + "name": "Chicken Breast Chunks", + "created": "2024-01-25T11:19:21.262000+01:00", + "last_update": "2024-06-07T07:46:04.640970+02:00", + "last_imported": "2024-01-25T11:19:21.262000+01:00", + "energy": 101, + "protein": "21.100", + "carbohydrates": "1.300", + "carbohydrates_sugar": "0.400", + "fat": "1.300", + "fat_saturated": "0.300", + "fiber": null, + "sodium": "0.520", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 28240, + "uuid": "7151eeac-0b91-4f7b-894a-9558b212253a", + "ingredient_id": 143410, + "ingredient_uuid": "29a56924-53ae-4b5c-8eaf-59cfc6d49f26", + "image": "https://wger.de/media/ingredients/143410/7151eeac-0b91-4f7b-894a-9558b212253a.jpg", + "created": "2024-01-27T00:44:30.525349+01:00", + "last_update": "2024-01-27T00:44:30.529392+01:00", + "size": 32104, + "width": 290, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5010236157817&id=6", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Chicken Breast Chunks", + "license_object_url": "https://world.openfoodfacts.org/product/5010236157817/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 28240, + "uuid": "7151eeac-0b91-4f7b-894a-9558b212253a", + "ingredient_id": 143410, + "ingredient_uuid": "29a56924-53ae-4b5c-8eaf-59cfc6d49f26", + "image": "https://wger.de/media/ingredients/143410/7151eeac-0b91-4f7b-894a-9558b212253a.jpg", + "created": "2024-01-27T00:44:30.525349+01:00", + "last_update": "2024-01-27T00:44:30.529392+01:00", + "size": 32104, + "width": 290, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=5010236157817&id=6", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "250.00" + }, + { + "id": 497739, + "meal": 218379, + "ingredient": 87997, + "ingredient_obj": { + "id": 87997, + "uuid": "f8e364d9-5e88-4903-89b9-72b380db30e7", + "remote_id": "03201985", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/03201985.json", + "code": "03201985", + "name": "Avocado", + "created": "2024-01-25T11:12:05.189000+01:00", + "last_update": "2024-06-07T06:36:02.313359+02:00", + "last_imported": "2024-01-25T11:12:05.189000+01:00", + "energy": 198, + "protein": "1.900", + "carbohydrates": "1.900", + "carbohydrates_sugar": "0.500", + "fat": "19.500", + "fat_saturated": "4.100", + "fiber": "3.400", + "sodium": "0.040", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 30571, + "uuid": "c8c81017-1736-4353-8bfc-570c35dffee0", + "ingredient_id": 87997, + "ingredient_uuid": "f8e364d9-5e88-4903-89b9-72b380db30e7", + "image": "https://wger.de/media/ingredients/87997/c8c81017-1736-4353-8bfc-570c35dffee0.jpg", + "created": "2024-01-30T12:33:24.644167+01:00", + "last_update": "2024-01-30T12:33:24.651083+01:00", + "size": 20549, + "width": 400, + "height": 253, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=03201985&id=3", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Avocado", + "license_object_url": "https://world.openfoodfacts.org/product/03201985/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 30571, + "uuid": "c8c81017-1736-4353-8bfc-570c35dffee0", + "ingredient_id": 87997, + "ingredient_uuid": "f8e364d9-5e88-4903-89b9-72b380db30e7", + "image": "https://wger.de/media/ingredients/87997/c8c81017-1736-4353-8bfc-570c35dffee0.jpg", + "created": "2024-01-30T12:33:24.644167+01:00", + "last_update": "2024-01-30T12:33:24.651083+01:00", + "size": 20549, + "width": 400, + "height": 253, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=03201985&id=3", + "license_author": "kiliweb", + "license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "140.00" + }, + { + "id": 497740, + "meal": 218379, + "ingredient": 144759, + "ingredient_obj": { + "id": 144759, + "uuid": "e39259ac-b16d-4e7b-a216-6fa26f7f39c8", + "remote_id": "10016503", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/10016503.json", + "code": "10016503", + "name": "Little Gem Lettuce", + "created": "2024-01-25T11:19:35.115000+01:00", + "last_update": "2024-06-07T11:41:34.613207+02:00", + "last_imported": "2024-01-25T11:19:35.115000+01:00", + "energy": 14, + "protein": "1.200", + "carbohydrates": "1.400", + "carbohydrates_sugar": "1.400", + "fat": "0.100", + "fat_saturated": "0.000", + "fiber": "1.500", + "sodium": "0.040", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 44756, + "uuid": "42de8a04-d3c3-4a7c-96dc-b200c3e8fb0b", + "ingredient_id": 144759, + "ingredient_uuid": "e39259ac-b16d-4e7b-a216-6fa26f7f39c8", + "image": "https://wger.de/media/ingredients/144759/42de8a04-d3c3-4a7c-96dc-b200c3e8fb0b.jpg", + "created": "2024-03-18T19:39:16.972948+01:00", + "last_update": "2024-03-18T19:39:16.977094+01:00", + "size": 33161, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=10016503&id=3", + "license_author": "alia", + "license_author_url": "https://world.openfoodfacts.org/photographer/alia", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Little Gem Lettuce", + "license_object_url": "https://world.openfoodfacts.org/product/10016503/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 44756, + "uuid": "42de8a04-d3c3-4a7c-96dc-b200c3e8fb0b", + "ingredient_id": 144759, + "ingredient_uuid": "e39259ac-b16d-4e7b-a216-6fa26f7f39c8", + "image": "https://wger.de/media/ingredients/144759/42de8a04-d3c3-4a7c-96dc-b200c3e8fb0b.jpg", + "created": "2024-03-18T19:39:16.972948+01:00", + "last_update": "2024-03-18T19:39:16.977094+01:00", + "size": 33161, + "width": 300, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=10016503&id=3", + "license_author": "alia", + "license_author_url": "https://world.openfoodfacts.org/photographer/alia", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "100.00" + } + ], + "nutritional_values": { + "energy": 1180.98, + "protein": 84.61, + "carbohydrates": 79.31, + "carbohydrates_sugar": 3.1, + "fat": 62.562, + "fat_saturated": 9.682, + "fiber": 22.26, + "sodium": 1.916 + } + }, + { + "id": 218623, + "plan": 92547, + "order": 1, + "time": "18:58:00", + "name": "mashed veggies", + "meal_items": [ + { + "id": 497970, + "meal": 218623, + "ingredient": 1848437, + "ingredient_obj": { + "id": 1848437, + "uuid": "3c883d34-3386-4b2f-94d1-850b2d214706", + "remote_id": "8710400114321", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/8710400114321.json", + "code": "8710400114321", + "name": "Roerbak Proven\u00e7aals", + "created": "2024-05-29T00:02:01.342527+02:00", + "last_update": "2024-06-07T15:51:07.136637+02:00", + "last_imported": "2024-05-29T00:02:01.342558+02:00", + "energy": 26, + "protein": "1.000", + "carbohydrates": "4.000", + "carbohydrates_sugar": "3.000", + "fat": "0.200", + "fat_saturated": "0.100", + "fiber": "2.000", + "sodium": null, + "weight_units": [], + "language": { + "id": 6, + "short_name": "nl", + "full_name": "Nederlands", + "full_name_en": "Dutch" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Roerbak Proven\u00e7aals", + "license_object_url": "https://world.openfoodfacts.org/product/8710400114321/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "800.00" + }, + { + "id": 497971, + "meal": 218623, + "ingredient": 128579, + "ingredient_obj": { + "id": 128579, + "uuid": "0e950cd8-52ae-4c8a-a965-3eb6d3dc7fff", + "remote_id": "8710243960505", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/8710243960505.json", + "code": "8710243960505", + "name": "Old Amsterdam", + "created": "2024-01-25T11:16:52.393000+01:00", + "last_update": "2024-06-07T12:55:08.619988+02:00", + "last_imported": "2024-01-25T11:16:52.393000+01:00", + "energy": 431, + "protein": "29.000", + "carbohydrates": "0.000", + "carbohydrates_sugar": "0.000", + "fat": "35.000", + "fat_saturated": "23.300", + "fiber": "0.000", + "sodium": "0.920", + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Old Amsterdam", + "license_object_url": "https://world.openfoodfacts.org/product/8710243960505/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "70.00" + }, + { + "id": 497972, + "meal": 218623, + "ingredient": 108766, + "ingredient_obj": { + "id": 108766, + "uuid": "bbf3325e-bc52-4528-9090-642e867dc826", + "remote_id": "3560070910366", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/3560070910366.json", + "code": "3560070910366", + "name": "Huile d'olive vierge extra", + "created": "2024-01-25T11:14:31.939000+01:00", + "last_update": "2024-06-07T10:39:34.526066+02:00", + "last_imported": "2024-01-25T11:14:31.939000+01:00", + "energy": 819, + "protein": "0.000", + "carbohydrates": "0.000", + "carbohydrates_sugar": "0.000", + "fat": "91.000", + "fat_saturated": "17.000", + "fiber": null, + "sodium": "0.000", + "weight_units": [], + "language": { + "id": 12, + "short_name": "fr", + "full_name": "Fran\u00e7ais", + "full_name_en": "French" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Huile d'olive vierge extra", + "license_object_url": "https://world.openfoodfacts.org/product/3560070910366/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "13.00" + }, + { + "id": 497973, + "meal": 218623, + "ingredient": 2414156, + "ingredient_obj": { + "id": 2414156, + "uuid": "d2213346-a009-4271-a4ed-f634386a32dc", + "remote_id": "7627536579409", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/7627536579409.json", + "code": "7627536579409", + "name": "Labneh", + "created": "2024-05-29T00:27:37.713378+02:00", + "last_update": "2024-06-07T11:18:27.248284+02:00", + "last_imported": "2024-05-29T00:27:37.713404+02:00", + "energy": 201, + "protein": "7.100", + "carbohydrates": "5.000", + "carbohydrates_sugar": "3.500", + "fat": "17.000", + "fat_saturated": "9.500", + "fiber": null, + "sodium": null, + "weight_units": [], + "language": { + "id": 2, + "short_name": "en", + "full_name": "English", + "full_name_en": "English" + }, + "image": { + "id": 63200, + "uuid": "0c753278-76cb-4007-a25a-e20973d394a7", + "ingredient_id": 2414156, + "ingredient_uuid": "d2213346-a009-4271-a4ed-f634386a32dc", + "image": "https://wger.de/media/ingredients/2414156/0c753278-76cb-4007-a25a-e20973d394a7.jpg", + "created": "2024-05-31T19:03:37.025279+02:00", + "last_update": "2024-05-31T19:03:37.029406+02:00", + "size": 21737, + "width": 333, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=7627536579409&id=1", + "license_author": "foodvisor", + "license_author_url": "https://world.openfoodfacts.org/photographer/foodvisor", + "license_derivative_source_url": "" + }, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Labneh", + "license_object_url": "https://world.openfoodfacts.org/product/7627536579409/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": { + "id": 63200, + "uuid": "0c753278-76cb-4007-a25a-e20973d394a7", + "ingredient_id": 2414156, + "ingredient_uuid": "d2213346-a009-4271-a4ed-f634386a32dc", + "image": "https://wger.de/media/ingredients/2414156/0c753278-76cb-4007-a25a-e20973d394a7.jpg", + "created": "2024-05-31T19:03:37.025279+02:00", + "last_update": "2024-05-31T19:03:37.029406+02:00", + "size": 21737, + "width": 333, + "height": 400, + "license": 1, + "license_title": "Photo", + "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=7627536579409&id=1", + "license_author": "foodvisor", + "license_author_url": "https://world.openfoodfacts.org/photographer/foodvisor", + "license_derivative_source_url": "" + }, + "order": 1, + "amount": "20.00" + }, + { + "id": 497974, + "meal": 218623, + "ingredient": 1073786, + "ingredient_obj": { + "id": 1073786, + "uuid": "5d5ec791-2094-4d3d-b6ee-8a4a1e09e545", + "remote_id": "8718906812154", + "source_name": "Open Food Facts", + "source_url": "https://world.openfoodfacts.org/api/v2/product/8718906812154.json", + "code": "8718906812154", + "name": "Gezoute roomboter", + "created": "2024-05-28T22:07:10.768295+02:00", + "last_update": "2024-06-07T10:00:39.414484+02:00", + "last_imported": "2024-05-28T22:07:10.768320+02:00", + "energy": 735, + "protein": "0.600", + "carbohydrates": "1.000", + "carbohydrates_sugar": "1.000", + "fat": "81.000", + "fat_saturated": "56.000", + "fiber": null, + "sodium": "0.400", + "weight_units": [], + "language": { + "id": 12, + "short_name": "fr", + "full_name": "Fran\u00e7ais", + "full_name_en": "French" + }, + "image": null, + "license": { + "id": 5, + "full_name": "Open Data Commons Open Database License", + "short_name": "ODbL", + "url": "https://opendatacommons.org/licenses/odbl/" + }, + "license_title": "Gezoute roomboter", + "license_object_url": "https://world.openfoodfacts.org/product/8718906812154/", + "license_author": "", + "license_author_url": "", + "license_derivative_source_url": "" + }, + "weight_unit": null, + "weight_unit_obj": null, + "image": null, + "order": 1, + "amount": "14.00" + } + ], + "nutritional_values": { + "energy": 759.27, + "protein": 29.804, + "carbohydrates": 33.14, + "carbohydrates_sugar": 24.84, + "fat": 52.67, + "fat_saturated": 29.06, + "fiber": 16.0, + "sodium": 0.7 + } + }, + { + "id": 218613, + "plan": 92547, + "order": 1, + "time": "22:58:00", + "name": "empty meal", + "meal_items": [], + "nutritional_values": { + "energy": 0.0, + "protein": 0.0, + "carbohydrates": 0.0, + "carbohydrates_sugar": null, + "fat": 0.0, + "fat_saturated": null, + "fiber": null, + "sodium": null } - }, - "weight_unit": null, - "weight_unit_obj": null, - "image": { - "id": 5, - "uuid": "81c07bef-02ea-47ba-a18e-d7388dc2bc01", - "ingredient_id": 10065, - "ingredient_uuid": "b0c47fa4-3898-4705-b8f5-184877d50b78", - "image": "http://localhost:8000/media/ingredients/b0c47fa4-3898-4705-b8f5-184877d50b78/81c07bef-02ea-47ba-a18e-d7388dc2bc01.jpg", - "last_update": "2022-08-09T15:45:58.511169+02:00", - "size": 43397, - "source_url": "", - "license": 1, - "license_author": "tacinte", - "license_title": "Photo", - "license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=4388844120334&id=2", - "license_author_url": "https://world.openfoodfacts.org/photographer/twoflower", - "license_derivative_source_url": "" - }, - "order": 1, - "amount": "200.00" } - ], - "get_nutritional_values": { - "energy": 853.0, - "protein": 12.08, - "carbohydrates": 180.7, - "carbohydrates_sugar": 139.62, - "fat": 4.92, - "fat_saturated": 0.71, - "fiber": 11.96, - "sodium": 0.48, - "energy_kilojoule": 3568.95 - } - } - ] + ] } From d99ca4160f4efbf11b88dfa0837f995787219f59 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 13:48:55 +0300 Subject: [PATCH 12/16] ingredient popup: fix image rendering --- lib/widgets/nutrition/forms.dart | 6 +++++- lib/widgets/nutrition/helpers.dart | 9 +-------- lib/widgets/nutrition/widgets.dart | 6 +++++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index afd70036..8ecbf517 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -454,7 +454,11 @@ class IngredientFormState extends State { IconButton( icon: const Icon(Icons.info_outline), onPressed: () { - showIngredientDetails(context, suggestions[index].ingredient.id); + showIngredientDetails( + context, + suggestions[index].ingredient.id, + image: suggestions[index].ingredient.image?.image, + ); }, ), const SizedBox(width: 5), diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 422616a5..20dfb7d7 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -102,12 +102,6 @@ String getKcalConsumedVsPlanned(Meal meal, BuildContext context) { } void showIngredientDetails(BuildContext context, int id, {String? image}) { - // when loading recently used ingredients, we never get an image :'( - // we also don't get an image when querying the API - // however, the typeahead suggestion does get an image, so we allow passing it... - - final serverURL = context.read().baseProvider.auth.serverUrl; - showDialog( context: context, builder: (context) => FutureBuilder( @@ -133,8 +127,7 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (image != null) - CircleAvatar(backgroundImage: NetworkImage(serverURL! + image), radius: 128), + if (image != null) CircleAvatar(backgroundImage: NetworkImage(image), radius: 128), if (image != null) const SizedBox(height: 12), if (snapshot.hasError) Text( diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index ed79f111..ae3f57d6 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -163,7 +163,11 @@ class _IngredientTypeaheadState extends State { trailing: IconButton( icon: const Icon(Icons.info_outline), onPressed: () { - showIngredientDetails(context, suggestion.data.id, image: suggestion.data.image); + showIngredientDetails( + context, + suggestion.data.id, + image: suggestion.data.image != null ? url! + suggestion.data.image! : null, + ); }, ), ); From 1ae3f33edd9e191cf5e9d61b6c08c6fc0d252803 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 14:10:45 +0300 Subject: [PATCH 13/16] use ingredient.license_object_url to link to human readable page --- lib/models/nutrition/ingredient.dart | 6 +++++- lib/models/nutrition/ingredient.g.dart | 3 +++ lib/widgets/nutrition/helpers.dart | 13 +++++-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/models/nutrition/ingredient.dart b/lib/models/nutrition/ingredient.dart index 1a0981f8..a8d4b565 100644 --- a/lib/models/nutrition/ingredient.dart +++ b/lib/models/nutrition/ingredient.dart @@ -26,7 +26,7 @@ part 'ingredient.g.dart'; class Ingredient { // fields returned by django api that we ignore here: // uuid, last_updated, last_imported, weight_units, language - // all license fields + // most license fields @JsonKey(required: true) final int id; @@ -42,6 +42,9 @@ class Ingredient { @JsonKey(required: true, name: 'source_url') final String? sourceUrl; + @JsonKey(required: true, name: 'license_object_url') + final String? licenseObjectURl; + /// Barcode of the product @JsonKey(required: true) final String? code; @@ -91,6 +94,7 @@ class Ingredient { required this.remoteId, required this.sourceName, required this.sourceUrl, + this.licenseObjectURl, required this.id, required this.code, required this.name, diff --git a/lib/models/nutrition/ingredient.g.dart b/lib/models/nutrition/ingredient.g.dart index f3f21074..6c820420 100644 --- a/lib/models/nutrition/ingredient.g.dart +++ b/lib/models/nutrition/ingredient.g.dart @@ -14,6 +14,7 @@ Ingredient _$IngredientFromJson(Map json) { 'remote_id', 'source_name', 'source_url', + 'license_object_url', 'code', 'name', 'created', @@ -31,6 +32,7 @@ Ingredient _$IngredientFromJson(Map json) { remoteId: json['remote_id'] as String?, sourceName: json['source_name'] as String?, sourceUrl: json['source_url'] as String?, + licenseObjectURl: json['license_object_url'] as String?, id: (json['id'] as num).toInt(), code: json['code'] as String?, name: json['name'] as String, @@ -55,6 +57,7 @@ Map _$IngredientToJson(Ingredient instance) => 'remote_id': instance.remoteId, 'source_name': instance.sourceName, 'source_url': instance.sourceUrl, + 'license_object_url': instance.licenseObjectURl, 'code': instance.code, 'name': instance.name, 'created': instance.created.toIso8601String(), diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 20dfb7d7..a6659fbd 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -110,15 +110,11 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { Ingredient? ingredient; NutritionalGoals? goals; String? source; - String? url; if (snapshot.hasData) { ingredient = snapshot.data; goals = ingredient!.nutritionalValues.toGoals(); source = ingredient.sourceName ?? 'unknown'; - url = ingredient.remoteId == null - ? null - : 'https://world.openfoodfacts.org/product/${ingredient.remoteId}'; } return AlertDialog( title: (snapshot.hasData) ? Text(ingredient!.name) : null, @@ -131,7 +127,7 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { if (image != null) const SizedBox(height: 12), if (snapshot.hasError) Text( - 'Ingredient lookup error: ${snapshot.error}', + 'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}', style: const TextStyle(color: Colors.red), ), if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), @@ -144,13 +140,14 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { showGperKg: false, ), ), - if (snapshot.hasData && url == null) Text('Source: ${source!}'), - if (snapshot.hasData && url != null) + if (snapshot.hasData && ingredient!.licenseObjectURl == null) + Text('Source: ${source!}'), + if (snapshot.hasData && ingredient!.licenseObjectURl != null) Padding( padding: const EdgeInsets.only(top: 12), child: InkWell( child: Text('Source: ${source!}'), - onTap: () => launchURL(url!, context), + onTap: () => launchURL(ingredient!.licenseObjectURl!, context), ), ), ], From 4a87c0b5f1b03c044d41ab4364273774cb9cceee Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 16:31:31 +0300 Subject: [PATCH 14/16] put popup button also in preview section --- lib/widgets/nutrition/forms.dart | 1 - lib/widgets/nutrition/nutrition_tiles.dart | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 8ecbf517..8e4e6d76 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -367,7 +367,6 @@ class IngredientFormState extends State { ) { if (snapshot.hasData) { _mealItem.ingredient = snapshot.data!; - // here it could be nice to put the button to the popup return MealItemValuesTile( ingredient: _mealItem.ingredient, nutritionalValues: _mealItem.nutritionalValues, diff --git a/lib/widgets/nutrition/nutrition_tiles.dart b/lib/widgets/nutrition/nutrition_tiles.dart index 3fd05979..ecb10aae 100644 --- a/lib/widgets/nutrition/nutrition_tiles.dart +++ b/lib/widgets/nutrition/nutrition_tiles.dart @@ -13,7 +13,7 @@ import 'package:wger/widgets/nutrition/nutrition_tile.dart'; import 'package:wger/widgets/nutrition/widgets.dart'; /// a NutritionTitle showing an ingredient, with its -/// avatar and nutritional values +/// avatar, nutritional values and button to popup its details class MealItemValuesTile extends StatelessWidget { final Ingredient ingredient; final NutritionalValues nutritionalValues; @@ -29,6 +29,16 @@ class MealItemValuesTile extends StatelessWidget { return NutritionTile( leading: IngredientAvatar(ingredient: ingredient), title: Text(getShortNutritionValues(nutritionalValues, context)), + trailing: IconButton( + icon: const Icon(Icons.info_outline), + onPressed: () { + showIngredientDetails( + context, + ingredient.id, + image: ingredient.image?.image, + ); + }, + ), ); } } From 47a5f4a07f2c5e4ed824d9261dd9717614025903 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 16:44:40 +0300 Subject: [PATCH 15/16] ingredient popup: scroll and shrink image when appropriate --- lib/widgets/nutrition/helpers.dart | 72 +++++++++++++++++------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index a6659fbd..302d3df3 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -116,41 +116,51 @@ void showIngredientDetails(BuildContext context, int id, {String? image}) { goals = ingredient!.nutritionalValues.toGoals(); source = ingredient.sourceName ?? 'unknown'; } + var radius = 100.0; + final height = MediaQuery.sizeOf(context).height; + final width = MediaQuery.sizeOf(context).width; + final smallest = height < width ? height : width; + if (smallest < 400) { + radius = smallest / 4; + } return AlertDialog( title: (snapshot.hasData) ? Text(ingredient!.name) : null, - content: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (image != null) CircleAvatar(backgroundImage: NetworkImage(image), radius: 128), - if (image != null) const SizedBox(height: 12), - if (snapshot.hasError) - Text( - 'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}', - style: const TextStyle(color: Colors.red), - ), - if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), - if (snapshot.hasData) - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 400), - child: MacronutrientsTable( - nutritionalGoals: goals!, - plannedValuesPercentage: goals.energyPercentage(), - showGperKg: false, + content: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (image != null) + CircleAvatar(backgroundImage: NetworkImage(image), radius: radius), + if (image != null) const SizedBox(height: 12), + if (snapshot.hasError) + Text( + 'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}', + style: const TextStyle(color: Colors.red), ), - ), - if (snapshot.hasData && ingredient!.licenseObjectURl == null) - Text('Source: ${source!}'), - if (snapshot.hasData && ingredient!.licenseObjectURl != null) - Padding( - padding: const EdgeInsets.only(top: 12), - child: InkWell( - child: Text('Source: ${source!}'), - onTap: () => launchURL(ingredient!.licenseObjectURl!, context), + if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), + if (snapshot.hasData) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 400), + child: MacronutrientsTable( + nutritionalGoals: goals!, + plannedValuesPercentage: goals.energyPercentage(), + showGperKg: false, + ), ), - ), - ], + if (snapshot.hasData && ingredient!.licenseObjectURl == null) + Text('Source: ${source!}'), + if (snapshot.hasData && ingredient!.licenseObjectURl != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: InkWell( + child: Text('Source: ${source!}'), + onTap: () => launchURL(ingredient!.licenseObjectURl!, context), + ), + ), + ], + ), ), ), ); From dc1f2202922a5cdd50e14778d48027c5559abeaf Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 12 Jul 2024 18:00:45 +0300 Subject: [PATCH 16/16] refactor ingredient details dialog: - give it close/continue buttons to load into selection - always use image property from loaded ingredient this is a bit slower, but: * more consistent (no need to support absolute vs relative URL's separately) * cleaner (no need to pass it thru explicitly) * more future proof: we will get rid of the dedicated /ingredient/search endpoint which gives us images before we load the full ingredient. in the future we will simply load the ingredients, completely, all at once. * allows for easier code reuse with barcode scan result dialog barcode scan result dialog: - show image and detailed nutrition table - support a loading spinner - simplify error handling - deduplicate code between found & not found - share code with ingredient details dialog --- lib/widgets/nutrition/forms.dart | 20 +- lib/widgets/nutrition/helpers.dart | 64 +----- lib/widgets/nutrition/ingredient_dialogs.dart | 199 ++++++++++++++++++ lib/widgets/nutrition/nutrition_tiles.dart | 1 - lib/widgets/nutrition/widgets.dart | 96 ++------- .../nutritional_meal_item_form_test.dart | 44 ++-- 6 files changed, 255 insertions(+), 169 deletions(-) create mode 100644 lib/widgets/nutrition/ingredient_dialogs.dart diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 8e4e6d76..b518a8e3 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -430,16 +430,18 @@ class IngredientFormState extends State { itemCount: suggestions.length, shrinkWrap: true, itemBuilder: (context, index) { + void select() { + final ingredient = suggestions[index].ingredient; + selectIngredient( + ingredient.id, + ingredient.name, + suggestions[index].amount, + ); + } + return Card( child: ListTile( - onTap: () { - final ingredient = suggestions[index].ingredient; - selectIngredient( - ingredient.id, - ingredient.name, - suggestions[index].amount, - ); - }, + onTap: select, title: Text( '${suggestions[index].ingredient.name} (${suggestions[index].amount.toStringAsFixed(0)}$unit)', ), @@ -456,7 +458,7 @@ class IngredientFormState extends State { showIngredientDetails( context, suggestions[index].ingredient.id, - image: suggestions[index].ingredient.image?.image, + select: select, ); }, ), diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 302d3df3..f4f6d3f7 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -19,14 +19,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; -import 'package:wger/helpers/misc.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/meal.dart'; -import 'package:wger/models/nutrition/nutritional_goals.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/macro_nutrients_table.dart'; +import 'package:wger/widgets/nutrition/ingredient_dialogs.dart'; List getNutritionColumnNames(BuildContext context) => [ AppLocalizations.of(context).energy, @@ -101,69 +99,13 @@ String getKcalConsumedVsPlanned(Meal meal, BuildContext context) { return '${consumed.toStringAsFixed(0)} / ${planned.toStringAsFixed(0)} ${loc.kcal}'; } -void showIngredientDetails(BuildContext context, int id, {String? image}) { +void showIngredientDetails(BuildContext context, int id, {void Function()? select}) { showDialog( context: context, builder: (context) => FutureBuilder( future: Provider.of(context, listen: false).fetchIngredient(id), builder: (BuildContext context, AsyncSnapshot snapshot) { - Ingredient? ingredient; - NutritionalGoals? goals; - String? source; - - if (snapshot.hasData) { - ingredient = snapshot.data; - goals = ingredient!.nutritionalValues.toGoals(); - source = ingredient.sourceName ?? 'unknown'; - } - var radius = 100.0; - final height = MediaQuery.sizeOf(context).height; - final width = MediaQuery.sizeOf(context).width; - final smallest = height < width ? height : width; - if (smallest < 400) { - radius = smallest / 4; - } - return AlertDialog( - title: (snapshot.hasData) ? Text(ingredient!.name) : null, - content: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (image != null) - CircleAvatar(backgroundImage: NetworkImage(image), radius: radius), - if (image != null) const SizedBox(height: 12), - if (snapshot.hasError) - Text( - 'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}', - style: const TextStyle(color: Colors.red), - ), - if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), - if (snapshot.hasData) - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 400), - child: MacronutrientsTable( - nutritionalGoals: goals!, - plannedValuesPercentage: goals.energyPercentage(), - showGperKg: false, - ), - ), - if (snapshot.hasData && ingredient!.licenseObjectURl == null) - Text('Source: ${source!}'), - if (snapshot.hasData && ingredient!.licenseObjectURl != null) - Padding( - padding: const EdgeInsets.only(top: 12), - child: InkWell( - child: Text('Source: ${source!}'), - onTap: () => launchURL(ingredient!.licenseObjectURl!, context), - ), - ), - ], - ), - ), - ), - ); + return IngredientDetails(snapshot, select: select); }, ), ); diff --git a/lib/widgets/nutrition/ingredient_dialogs.dart b/lib/widgets/nutrition/ingredient_dialogs.dart new file mode 100644 index 00000000..e636e22c --- /dev/null +++ b/lib/widgets/nutrition/ingredient_dialogs.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:wger/helpers/misc.dart'; +import 'package:wger/models/nutrition/ingredient.dart'; +import 'package:wger/models/nutrition/nutritional_goals.dart'; +import 'package:wger/widgets/nutrition/macro_nutrients_table.dart'; + +Widget ingredientImage(String url, BuildContext context) { + var radius = 100.0; + final height = MediaQuery.sizeOf(context).height; + final width = MediaQuery.sizeOf(context).width; + final smallest = height < width ? height : width; + if (smallest < 400) { + radius = smallest / 4; + } + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: CircleAvatar(backgroundImage: NetworkImage(url), radius: radius), + ); +} + +class IngredientDetails extends StatelessWidget { + final AsyncSnapshot snapshot; + final void Function()? select; + const IngredientDetails(this.snapshot, {super.key, this.select}); + + @override + Widget build(BuildContext context) { + Ingredient? ingredient; + NutritionalGoals? goals; + String? source; + + if (snapshot.hasData) { + ingredient = snapshot.data; + goals = ingredient!.nutritionalValues.toGoals(); + source = ingredient.sourceName ?? 'unknown'; + } + + return AlertDialog( + title: (snapshot.hasData) ? Text(ingredient!.name) : null, + content: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (snapshot.hasError) + Text( + 'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}', + style: const TextStyle(color: Colors.red), + ), + if (ingredient?.image?.image != null) + ingredientImage(ingredient!.image!.image, context), + if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(), + if (snapshot.hasData) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 400), + child: MacronutrientsTable( + nutritionalGoals: goals!, + plannedValuesPercentage: goals.energyPercentage(), + showGperKg: false, + ), + ), + if (snapshot.hasData && ingredient!.licenseObjectURl == null) + Text('Source: ${source!}'), + if (snapshot.hasData && ingredient!.licenseObjectURl != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: InkWell( + child: Text('Source: ${source!}'), + onTap: () => launchURL(ingredient!.licenseObjectURl!, context), + ), + ), + ], + ), + ), + ), + actions: [ + if (snapshot.hasData && select != null) + TextButton( + key: const Key('ingredient-details-continue-button'), + child: Text(MaterialLocalizations.of(context).continueButtonLabel), + onPressed: () { + select!(); + Navigator.of(context).pop(); + }, + ), + TextButton( + key: const Key('ingredient-details-close-button'), + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} + +class IngredientScanResultDialog extends StatelessWidget { + final AsyncSnapshot snapshot; + final String barcode; + final Function(int id, String name, num? amount) selectIngredient; + + const IngredientScanResultDialog(this.snapshot, this.barcode, this.selectIngredient, {super.key}); + + @override + Widget build(BuildContext context) { + Ingredient? ingredient; + NutritionalGoals? goals; + String? title; + String? source; + + if (snapshot.connectionState == ConnectionState.done) { + ingredient = snapshot.data; + title = ingredient != null + ? AppLocalizations.of(context).productFound + : AppLocalizations.of(context).productNotFound; + if (ingredient != null) { + goals = ingredient.nutritionalValues.toGoals(); + source = ingredient.sourceName ?? 'unknown'; + } + } + return AlertDialog( + key: const Key('ingredient-scan-result-dialog'), + title: title != null ? Text(title) : null, + content: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (snapshot.hasError) + Text( + 'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}', + style: const TextStyle(color: Colors.red), + ), + if (snapshot.connectionState == ConnectionState.done && ingredient == null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + AppLocalizations.of(context).productNotFoundDescription(barcode), + ), + ), + if (ingredient != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: + Text(AppLocalizations.of(context).productFoundDescription(ingredient.name)), + ), + if (ingredient?.image?.image != null) + ingredientImage(ingredient!.image!.image, context), + if (snapshot.connectionState != ConnectionState.done && !snapshot.hasError) + const CircularProgressIndicator(), + if (goals != null) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 400), + child: MacronutrientsTable( + nutritionalGoals: goals, + plannedValuesPercentage: goals.energyPercentage(), + showGperKg: false, + ), + ), + if (ingredient != null && ingredient.licenseObjectURl == null) + Text('Source: ${source!}'), + if (ingredient?.licenseObjectURl != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: InkWell( + child: Text('Source: ${source!}'), + onTap: () => launchURL(ingredient!.licenseObjectURl!, context), + ), + ), + ], + ), + ), + ), + actions: [ + if (ingredient != null) // if barcode matched + TextButton( + key: const Key('ingredient-scan-result-dialog-confirm-button'), + child: Text(MaterialLocalizations.of(context).continueButtonLabel), + onPressed: () { + selectIngredient(ingredient!.id, ingredient.name, null); + Navigator.of(context).pop(); + }, + ), + // if didn't match, or we're still waiting + TextButton( + key: const Key('ingredient-scan-result-dialog-close-button'), + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/lib/widgets/nutrition/nutrition_tiles.dart b/lib/widgets/nutrition/nutrition_tiles.dart index ecb10aae..dc23fbff 100644 --- a/lib/widgets/nutrition/nutrition_tiles.dart +++ b/lib/widgets/nutrition/nutrition_tiles.dart @@ -35,7 +35,6 @@ class MealItemValuesTile extends StatelessWidget { showIngredientDetails( context, ingredient.id, - image: ingredient.image?.image, ); }, ), diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index ae3f57d6..00afff5b 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -32,7 +32,7 @@ import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/nutrition/helpers.dart'; -import 'package:wger/widgets/nutrition/nutrition_tiles.dart'; +import 'package:wger/widgets/nutrition/ingredient_dialogs.dart'; class ScanReader extends StatelessWidget { const ScanReader(); @@ -166,7 +166,9 @@ class _IngredientTypeaheadState extends State { showIngredientDetails( context, suggestion.data.id, - image: suggestion.data.image != null ? url! + suggestion.data.image! : null, + select: () { + widget.selectIngredient(suggestion.data.id, suggestion.value, null); + }, ); }, ), @@ -204,88 +206,24 @@ class _IngredientTypeaheadState extends State { if (!widget.test!) { barcode = await readerscan(context); } - - if (barcode.isNotEmpty) { - if (!mounted) { - return; - } - final result = await Provider.of( - context, - listen: false, - ).searchIngredientWithCode(barcode); - // TODO: show spinner... - if (!mounted) { - return; - } - - if (result != null) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - key: const Key('found-dialog'), - title: Text(AppLocalizations.of(context).productFound), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(AppLocalizations.of(context).productFoundDescription(result.name)), - // TODO replace with full view instead of small popup - MealItemValuesTile( - ingredient: result, - nutritionalValues: result.nutritionalValues, - ), - ], - ), - actions: [ - TextButton( - key: const Key('found-dialog-confirm-button'), - child: Text(MaterialLocalizations.of(context).continueButtonLabel), - onPressed: () { - widget.selectIngredient(result.id, result.name, null); - Navigator.of(ctx).pop(); - }, - ), - TextButton( - key: const Key('found-dialog-close-button'), - child: Text( - MaterialLocalizations.of(context).closeButtonLabel, - ), - onPressed: () { - Navigator.of(ctx).pop(); - }, - ), - ], - ), - ); - } else { - //nothing is matching barcode - showDialog( - context: context, - builder: (ctx) => AlertDialog( - key: const Key('notFound-dialog'), - title: Text(AppLocalizations.of(context).productNotFound), - content: Text( - AppLocalizations.of(context).productNotFoundDescription(barcode), - ), - actions: [ - TextButton( - key: const Key('notFound-dialog-close-button'), - child: Text( - MaterialLocalizations.of(context).closeButtonLabel, - ), - onPressed: () { - Navigator.of(ctx).pop(); - }, - ), - ], - ), - ); - } - } } catch (e) { if (mounted) { showErrorDialog(e, context); } } + if (!mounted) { + return; + } + showDialog( + context: context, + builder: (context) => FutureBuilder( + future: Provider.of(context, listen: false) + .searchIngredientWithCode(barcode), + builder: (BuildContext context, AsyncSnapshot snapshot) { + return IngredientScanResultDialog(snapshot, barcode, widget.selectIngredient); + }, + ), + ); }, ); } diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 9ea69f77..ba5de3f0 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; +import 'package:network_image_mock/network_image_mock.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/exercises/ingredient_api.dart'; @@ -127,13 +128,15 @@ void main() { }); group('Test the AlertDialogs for scanning result', () { + // TODO: why do we need to support empty barcodes? testWidgets('with empty code', (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '', true)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byType(AlertDialog), findsNothing); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')), findsNothing); }); testWidgets('with correct code', (WidgetTester tester) async { @@ -142,7 +145,8 @@ void main() { await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('found-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')), findsOneWidget); }); testWidgets('with incorrect code', (WidgetTester tester) async { @@ -151,7 +155,8 @@ void main() { await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('notFound-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')), findsNothing); }); }); @@ -221,9 +226,9 @@ void main() { await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('found-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); - await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); + await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button'))); await tester.pumpAndSettle(); expect(formState.ingredientIdController.text, '1'); @@ -235,12 +240,12 @@ void main() { await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('found-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); - await tester.tap(find.byKey(const Key('found-dialog-close-button'))); + await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-close-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('found-dialog')), findsNothing); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsNothing); }); }); @@ -264,9 +269,9 @@ void main() { await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('found-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); - await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); + await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME))); @@ -275,15 +280,16 @@ void main() { expect(find.text('Please enter a valid number'), findsOneWidget); }); +//TODO: isn't this test just a duplicate of the above one? can be removed? testWidgets('save ingredient with incorrect weight input type', (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('found-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); - await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); + await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME))); @@ -301,22 +307,22 @@ void main() { await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('found-dialog')), findsOneWidget); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget); - await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); + await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button'))); await tester.pumpAndSettle(); expect(formState.ingredientIdController.text, '1'); - // once ID and weight are set, it'll fetchIngredient and show macros preview + await tester.enterText(find.byKey(const Key('field-weight')), '2'); + + // once ID and weight are set, it'll fetchIngredient and show macros preview and ingredient image when(mockNutrition.fetchIngredient(1)).thenAnswer((_) => Future.value( Ingredient.fromJson(jsonDecode(fixture('nutrition/ingredientinfo_59887.json'))), )); + await mockNetworkImagesFor(() => tester.pumpAndSettle()); - await tester.enterText(find.byKey(const Key('field-weight')), '2'); - await tester.pumpAndSettle(); - - expect(find.byKey(const Key('found-dialog')), findsNothing); + expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsNothing); await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME))); await tester.pumpAndSettle();