diff --git a/lib/locale/locales.dart b/lib/locale/locales.dart index 53fdaedc..f1e78523 100644 --- a/lib/locale/locales.dart +++ b/lib/locale/locales.dart @@ -171,6 +171,22 @@ class AppLocalizations { ); } + String get ingredient { + return Intl.message( + 'Ingredient', + name: 'ingredient', + desc: 'An ingredient', + ); + } + + String get amount { + return Intl.message( + 'Amount', + name: 'amount', + desc: 'The amount (e.g. in grams) of an ingredient in a meal', + ); + } + String get newEntry { return Intl.message( 'New entry', diff --git a/lib/models/nutrition/meal_item.dart b/lib/models/nutrition/meal_item.dart index 90557ebf..3f2b2c03 100644 --- a/lib/models/nutrition/meal_item.dart +++ b/lib/models/nutrition/meal_item.dart @@ -30,18 +30,24 @@ class MealItem { @JsonKey(required: true) final int id; - @JsonKey(required: true) - final Ingredient ingredient; + @JsonKey(required: false, name: 'ingredient') + int ingredientId; - @JsonKey(required: true, name: 'weight_unit') - final IngredientWeightUnit weightUnit; + @JsonKey(required: false, name: 'ingredient_obj') + Ingredient ingredientObj; + + @JsonKey(required: false) + int meal; + + @JsonKey(required: false, name: 'weight_unit') + IngredientWeightUnit weightUnit; @JsonKey(required: true, fromJson: toNum, toJson: toString) - final num amount; + num amount; MealItem({ @required this.id, - @required this.ingredient, + @required this.ingredientObj, @required this.weightUnit, @required this.amount, }); @@ -68,22 +74,22 @@ class MealItem { final weight = this.weightUnit == null ? amount : amount * weightUnit.amount * weightUnit.grams; - out['energy'] += ingredient.energy * weight / 100; + out['energy'] += ingredientObj.energy * weight / 100; out['energyKj'] += out['energy'] * 4.184; - out['protein'] += ingredient.protein * weight / 100; - out['carbohydrates'] += ingredient.carbohydrates * weight / 100; - out['fat'] += ingredient.fat * weight / 100; + out['protein'] += ingredientObj.protein * weight / 100; + out['carbohydrates'] += ingredientObj.carbohydrates * weight / 100; + out['fat'] += ingredientObj.fat * weight / 100; - if (ingredient.fatSaturated != null) { - out['fat_saturated'] += ingredient.fatSaturated * weight / 100; + if (ingredientObj.fatSaturated != null) { + out['fat_saturated'] += ingredientObj.fatSaturated * weight / 100; } - if (ingredient.fibres != null) { - out['fibres'] += ingredient.fibres * weight / 100; + if (ingredientObj.fibres != null) { + out['fibres'] += ingredientObj.fibres * weight / 100; } - if (ingredient.sodium != null) { - out['sodium'] += ingredient.sodium * weight / 100; + if (ingredientObj.sodium != null) { + out['sodium'] += ingredientObj.sodium * weight / 100; } return out; diff --git a/lib/models/nutrition/meal_item.g.dart b/lib/models/nutrition/meal_item.g.dart index f6b61590..f821ee9d 100644 --- a/lib/models/nutrition/meal_item.g.dart +++ b/lib/models/nutrition/meal_item.g.dart @@ -7,24 +7,27 @@ part of 'meal_item.dart'; // ************************************************************************** MealItem _$MealItemFromJson(Map json) { - $checkKeys(json, - requiredKeys: const ['id', 'ingredient', 'weight_unit', 'amount']); + $checkKeys(json, requiredKeys: const ['id', 'amount']); return MealItem( id: json['id'] as int, - ingredient: json['ingredient'] == null + ingredientObj: json['ingredient_obj'] == null ? null - : Ingredient.fromJson(json['ingredient'] as Map), + : Ingredient.fromJson(json['ingredient_obj'] as Map), weightUnit: json['weight_unit'] == null ? null : IngredientWeightUnit.fromJson( json['weight_unit'] as Map), amount: toNum(json['amount'] as String), - ); + ) + ..ingredientId = json['ingredient'] as int + ..meal = json['meal'] as int; } Map _$MealItemToJson(MealItem instance) => { 'id': instance.id, - 'ingredient': instance.ingredient, + 'ingredient': instance.ingredientId, + 'ingredient_obj': instance.ingredientObj, + 'meal': instance.meal, 'weight_unit': instance.weightUnit, 'amount': toString(instance.amount), }; diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index 61b24393..d5267100 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -26,12 +26,12 @@ import 'package:wger/providers/auth.dart'; /// Base provider class. /// Provides a couple of comfort functions so we avoid a bit of boilerplate. class WgerBaseProvider { - String url; + String requestUrl; Auth auth; WgerBaseProvider(auth, urlPath) { this.auth = auth; - this.url = makeUrl(urlPath); + this.requestUrl = makeUrl(urlPath); } makeUrl(String path, [String id]) { @@ -41,18 +41,15 @@ class WgerBaseProvider { } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response - Future> fetchAndSet(http.Client client, [String urlPath]) async { + Future> fetch(http.Client client, [String urlPath]) async { if (client == null) { client = http.Client(); } - if (urlPath != null) { - url = makeUrl(urlPath); - } - // Send the request + requestUrl = urlPath == null ? makeUrl(urlPath) : urlPath; final response = await client.get( - url + '?ordering=-date', + requestUrl + '?ordering=-date', headers: { 'Authorization': 'Token ${auth.token}', 'User-Agent': 'wger Workout Manager App', @@ -76,11 +73,11 @@ class WgerBaseProvider { } if (urlPath != null) { - url = makeUrl(urlPath); + requestUrl = makeUrl(urlPath); } final response = await client.post( - url, + requestUrl, headers: { 'Authorization': 'Token ${auth.token}', 'Content-Type': 'application/json; charset=UTF-8', diff --git a/lib/providers/body_weight.dart b/lib/providers/body_weight.dart index 6960da88..2b0d1f5c 100644 --- a/lib/providers/body_weight.dart +++ b/lib/providers/body_weight.dart @@ -45,7 +45,7 @@ class BodyWeight extends WgerBaseProvider with ChangeNotifier { } // Process the response - final data = await fetchAndSet(client); + final data = await fetch(client); final List loadedEntries = []; for (final entry in data['results']) { loadedEntries.add(WeightEntry.fromJson(entry)); diff --git a/lib/providers/nutritional_plans.dart b/lib/providers/nutritional_plans.dart index 7f213654..9f5ff5c8 100644 --- a/lib/providers/nutritional_plans.dart +++ b/lib/providers/nutritional_plans.dart @@ -16,10 +16,14 @@ * along with this program. If not, see . */ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:wger/models/http_exception.dart'; +import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/meal.dart'; +import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/base_provider.dart'; @@ -28,6 +32,9 @@ class NutritionalPlans extends WgerBaseProvider with ChangeNotifier { static const nutritionalPlansUrl = 'nutritionplan'; static const nutritionalPlansInfoUrl = 'nutritionplaninfo'; static const mealUrl = 'meal'; + static const mealItemUrl = 'mealitem'; + static const ingredientUrl = 'ingredient'; + static const ingredientSearchUrl = 'ingredient/search'; String _url; Auth _auth; @@ -51,12 +58,22 @@ class NutritionalPlans extends WgerBaseProvider with ChangeNotifier { return _plans.firstWhere((plan) => plan.id == id); } + Meal findMealById(int id) { + for (var plan in _plans) { + var meal = plan.meals.firstWhere((plan) => plan.id == id, orElse: () {}); + if (meal != null) { + return meal; + } + } + return null; + } + Future fetchAndSetPlans({http.Client client}) async { if (client == null) { client = http.Client(); } - final data = await fetchAndSet(client, nutritionalPlansInfoUrl); + final data = await fetch(client, makeUrl(nutritionalPlansInfoUrl)); final List loadedPlans = []; for (final entry in data['results']) { loadedPlans.add(NutritionalPlan.fromJson(entry)); @@ -73,7 +90,7 @@ class NutritionalPlans extends WgerBaseProvider with ChangeNotifier { String url = makeUrl('nutritionplaninfo', planId.toString()); //fetchAndSet - final data = await fetchAndSet(client, 'nutritionplaninfo/$planId'); + final data = await fetch(client, 'nutritionplaninfo/$planId'); //final response = await http.get( // url, @@ -151,4 +168,57 @@ class NutritionalPlans extends WgerBaseProvider with ChangeNotifier { } existingMeal = null; } + + /// Adds a meal item to a meal + Future addMealIteam(MealItem mealItem, int mealId, {http.Client client}) async { + if (client == null) { + client = http.Client(); + } + + var meal = findMealById(mealId); + final data = await add(mealItem.toJson(), client, mealItemUrl); + + mealItem = MealItem.fromJson(data); + mealItem.ingredientObj = await fetchIngredient(mealItem.ingredientId); + meal.mealItems.add(mealItem); + notifyListeners(); + + return mealItem; + } + + /// Fetch and return an ingredient + Future fetchIngredient(int ingredientId, {http.Client client}) async { + if (client == null) { + client = http.Client(); + } + + // fetch and return + final data = await fetch(client, makeUrl(ingredientUrl, ingredientId.toString())); + return Ingredient.fromJson(data); + } + + /// Searches for an ingredient + Future searchIngredient(String name, {http.Client client}) async { + if (client == null) { + client = http.Client(); + } + + // Send the request + requestUrl = makeUrl(ingredientSearchUrl); + final response = await client.get( + requestUrl + '?term=$name', + headers: { + 'Authorization': 'Token ${auth.token}', + 'User-Agent': 'wger Workout Manager App', + }, + ); + + // Something wrong with our request + if (response.statusCode >= 400) { + throw WgerHttpException(response.body); + } + + // Process the response + return json.decode(utf8.decode(response.bodyBytes))['suggestions'] as List; + } } diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 6e59139c..2e8d70d6 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -17,12 +17,14 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/locale/locales.dart'; import 'package:wger/models/http_exception.dart'; import 'package:wger/models/nutrition/meal.dart'; +import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/providers/nutritional_plans.dart'; @@ -92,3 +94,117 @@ class MealForm extends StatelessWidget { ); } } + +class MealItemForm extends StatelessWidget { + Meal meal; + MealItem mealItem; + NutritionalPlan _plan; + + MealItemForm(meal, [mealItem]) { + this.meal = meal; + this.mealItem = mealItem ?? MealItem(); + } + + final _form = GlobalKey(); + final _ingredientController = TextEditingController(); + final _amountController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.all(20), + child: Form( + key: _form, + child: Column( + children: [ + TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: this._ingredientController, + decoration: InputDecoration(labelText: AppLocalizations.of(context).ingredient), + ), + suggestionsCallback: (pattern) async { + return await Provider.of(context, listen: false) + .searchIngredient(pattern); + }, + itemBuilder: (context, suggestion) { + return ListTile( + title: Text(suggestion['value']), + subtitle: Text(suggestion['data']['id'].toString()), + ); + }, + transitionBuilder: (context, suggestionsBox, controller) { + return suggestionsBox; + }, + onSuggestionSelected: (suggestion) { + print(suggestion); + mealItem.ingredientId = suggestion['data']['id']; + this._ingredientController.text = suggestion['value']; + }, + validator: (value) { + if (value.isEmpty) { + return 'Please select an ingredient'; + } + if (mealItem.ingredientId == null) { + return 'Please select an ingredient'; + } + return null; + }, + ), + /* + TextFormField( + decoration: InputDecoration(labelText: AppLocalizations.of(context).ingredient), + controller: _ingredientController, + onSaved: (newValue) async { + mealItem.ingredient = await Provider.of(context, listen: false) + .fetchIngredient(int.parse(newValue)); + print(mealItem.ingredient.name); + print('ppppppppppppppppppp'); + }, + onFieldSubmitted: (_) {}, + ), + + */ + TextFormField( + decoration: InputDecoration(labelText: AppLocalizations.of(context).amount), + controller: _amountController, + keyboardType: TextInputType.number, + onFieldSubmitted: (_) {}, + onSaved: (newValue) { + mealItem.amount = double.parse(newValue); + }, + validator: (value) { + try { + double.parse(value); + } catch (error) { + return 'Please enter a valid number'; + } + return null; + }, + ), + ElevatedButton( + child: Text(AppLocalizations.of(context).save), + onPressed: () async { + if (!_form.currentState.validate()) { + return; + } + _form.currentState.save(); + + try { + mealItem.meal = meal.id; + + Provider.of(context, listen: false) + .addMealIteam(mealItem, meal.id); + } on WgerHttpException catch (error) { + showHttpExceptionErrorDialog(error, context); + } catch (error) { + showErrorDialog(error, context); + } + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/nutrition/meal.dart b/lib/widgets/nutrition/meal.dart index c0f9167e..2bb6a44c 100644 --- a/lib/widgets/nutrition/meal.dart +++ b/lib/widgets/nutrition/meal.dart @@ -22,6 +22,8 @@ import 'package:wger/locale/locales.dart'; import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/providers/nutritional_plans.dart'; +import 'package:wger/widgets/core/bottom_sheet.dart'; +import 'package:wger/widgets/nutrition/forms.dart'; class MealWidget extends StatelessWidget { final Meal _meal; @@ -36,9 +38,14 @@ class MealWidget extends StatelessWidget { child: Card( child: Column( children: [ - Text('Plan ID: ${_meal.plan}'), DismissibleMealHeader(meal: _meal), ..._meal.mealItems.map((item) => MealItemWidget(item)).toList(), + OutlinedButton( + child: Text('Add ingredient'), + onPressed: () { + showFormBottomSheet(context, 'Add ingredient', MealItemForm(_meal)); + }, + ), ], ), ), @@ -62,11 +69,9 @@ class MealItemWidget extends StatelessWidget { children: [ TableRow( children: [ + Text('${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredientObj.name}'), Text( - '${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}', - ), - Text( - '${values["energy"].toStringAsFixed(0)} kcal / \n ${values["energyKj"].toStringAsFixed(0)} kJ'), + '${values["energy"].toStringAsFixed(0)} kcal / ${values["energyKj"].toStringAsFixed(0)} kJ'), Text('${values["protein"].toStringAsFixed(0)} g'), Text('${values["carbohydrates"].toStringAsFixed(0)} g'), Text('${values["fat"].toStringAsFixed(0)} g'), diff --git a/pubspec.lock b/pubspec.lock index 60cb90c9..58a7267d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -236,7 +236,28 @@ packages: name: flutter_calendar_carousel url: "https://pub.dartlang.org" source: hosted - version: "1.5.1" + version: "1.5.2" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.2" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" flutter_launcher_icons: dependency: "direct dev" description: @@ -254,6 +275,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.1" flutter_web_plugins: dependency: transitive description: flutter @@ -349,7 +377,7 @@ packages: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.5.1" logging: dependency: transitive description: @@ -468,7 +496,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" pool: dependency: transitive description: @@ -489,7 +517,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.2+2" + version: "4.3.2+3" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3fedc951..59d54c06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,15 +26,16 @@ dependencies: flutter_localizations: sdk: flutter - provider: ^4.3.2+1 + provider: ^4.3.2+3 intl: ^0.16.1 http: ^0.12.2 shared_preferences: ^0.5.12+4 - flutter_calendar_carousel: ^1.5.1 + flutter_calendar_carousel: ^1.5.2 cupertino_icons: ^1.0.0 - json_serializable: ^3.5.0 + json_serializable: ^3.5.1 url_launcher: ^5.7.10 charts_flutter: ^0.9.0 + flutter_typeahead: ^1.9.1 dev_dependencies: flutter_test: