Allow to add meal items to a meal

This commit is contained in:
Roland Geider
2020-12-24 16:36:54 +01:00
parent b04c0a9a74
commit e4e4907782
10 changed files with 289 additions and 47 deletions

View File

@@ -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',

View File

@@ -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;

View File

@@ -7,24 +7,27 @@ part of 'meal_item.dart';
// **************************************************************************
MealItem _$MealItemFromJson(Map<String, dynamic> 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<String, dynamic>),
: Ingredient.fromJson(json['ingredient_obj'] as Map<String, dynamic>),
weightUnit: json['weight_unit'] == null
? null
: IngredientWeightUnit.fromJson(
json['weight_unit'] as Map<String, dynamic>),
amount: toNum(json['amount'] as String),
);
)
..ingredientId = json['ingredient'] as int
..meal = json['meal'] as int;
}
Map<String, dynamic> _$MealItemToJson(MealItem instance) => <String, dynamic>{
'id': instance.id,
'ingredient': instance.ingredient,
'ingredient': instance.ingredientId,
'ingredient_obj': instance.ingredientObj,
'meal': instance.meal,
'weight_unit': instance.weightUnit,
'amount': toString(instance.amount),
};

View File

@@ -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<Map<String, dynamic>> fetchAndSet(http.Client client, [String urlPath]) async {
Future<Map<String, dynamic>> 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: <String, String>{
'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',

View File

@@ -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<WeightEntry> loadedEntries = [];
for (final entry in data['results']) {
loadedEntries.add(WeightEntry.fromJson(entry));

View File

@@ -16,10 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<void> 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<NutritionalPlan> 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<MealItem> 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<Ingredient> 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<List> 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: <String, String>{
'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<dynamic>;
}
}

View File

@@ -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<FormState>();
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<NutritionalPlans>(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<NutritionalPlans>(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<NutritionalPlans>(context, listen: false)
.addMealIteam(mealItem, meal.id);
} on WgerHttpException catch (error) {
showHttpExceptionErrorDialog(error, context);
} catch (error) {
showErrorDialog(error, context);
}
Navigator.of(context).pop();
},
),
],
),
),
);
}
}

View File

@@ -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'),

View File

@@ -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:

View File

@@ -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: