mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Allow to add meal items to a meal
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
36
pubspec.lock
36
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user