mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge pull request #585 from wger-project/show-macros-on-ingredients-tiles
Add energy and kcal values to ingredient logging/adding forms
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/models/nutrition/image.dart';
|
||||
import 'package:wger/models/nutrition/nutritional_values.dart';
|
||||
|
||||
part 'ingredient.g.dart';
|
||||
|
||||
@@ -91,4 +92,17 @@ class Ingredient {
|
||||
factory Ingredient.fromJson(Map<String, dynamic> json) => _$IngredientFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$IngredientToJson(this);
|
||||
|
||||
NutritionalValues get nutritionalValues {
|
||||
return NutritionalValues.values(
|
||||
energy * 1,
|
||||
protein * 1,
|
||||
carbohydrates * 1,
|
||||
carbohydratesSugar * 1,
|
||||
fat * 1,
|
||||
fatSaturated * 1,
|
||||
fibres * 1,
|
||||
sodium * 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,21 +83,10 @@ class Log {
|
||||
/// Calculations
|
||||
NutritionalValues get nutritionalValues {
|
||||
// This is already done on the server. It might be better to read it from there.
|
||||
final out = NutritionalValues();
|
||||
|
||||
//final weight = amount;
|
||||
final weight =
|
||||
weightUnitObj == null ? amount : amount * weightUnitObj!.amount * weightUnitObj!.grams;
|
||||
|
||||
out.energy = ingredient.energy * weight / 100;
|
||||
out.protein = ingredient.protein * weight / 100;
|
||||
out.carbohydrates = ingredient.carbohydrates * weight / 100;
|
||||
out.carbohydratesSugar = ingredient.carbohydratesSugar * weight / 100;
|
||||
out.fat = ingredient.fat * weight / 100;
|
||||
out.fatSaturated = ingredient.fatSaturated * weight / 100;
|
||||
out.fibres = ingredient.fibres * weight / 100;
|
||||
out.sodium = ingredient.sodium * weight / 100;
|
||||
|
||||
return out;
|
||||
return ingredient.nutritionalValues / (100 / weight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ class MealItem {
|
||||
Map<String, dynamic> toJson() => _$MealItemToJson(this);
|
||||
|
||||
/// Calculations
|
||||
/// TODO why does this not consider weightUnitObj ? should we do the same as Log.nutritionalValues here?
|
||||
NutritionalValues get nutritionalValues {
|
||||
// This is already done on the server. It might be better to read it from there.
|
||||
final out = NutritionalValues();
|
||||
|
||||
@@ -42,6 +42,7 @@ import 'package:wger/widgets/measurements/charts.dart';
|
||||
import 'package:wger/widgets/measurements/forms.dart';
|
||||
import 'package:wger/widgets/nutrition/charts.dart';
|
||||
import 'package:wger/widgets/nutrition/forms.dart';
|
||||
import 'package:wger/widgets/nutrition/helpers.dart';
|
||||
import 'package:wger/widgets/weight/forms.dart';
|
||||
import 'package:wger/widgets/workouts/forms.dart';
|
||||
|
||||
@@ -83,23 +84,7 @@ class _DashboardNutritionWidgetState extends State<DashboardNutritionWidget> {
|
||||
//textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).energyShort} ${meal.plannedNutritionalValues.energy.toStringAsFixed(0)}${AppLocalizations.of(context).kcal}'),
|
||||
const MutedText(' / '),
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).proteinShort} ${meal.plannedNutritionalValues.protein.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
|
||||
const MutedText(' / '),
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).carbohydratesShort} ${meal.plannedNutritionalValues.carbohydrates.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
|
||||
const MutedText(' / '),
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).fatShort} ${meal.plannedNutritionalValues.fat.toStringAsFixed(0)}${AppLocalizations.of(context).g} '),
|
||||
],
|
||||
),
|
||||
MutedText(getShortNutritionValues(meal.plannedNutritionalValues, context)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.history_edu),
|
||||
color: wgerPrimaryButtonColor,
|
||||
|
||||
@@ -23,11 +23,14 @@ import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
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_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/widgets.dart';
|
||||
|
||||
class MealForm extends StatelessWidget {
|
||||
@@ -113,23 +116,66 @@ class MealForm extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class MealItemForm extends StatelessWidget {
|
||||
final Meal _meal;
|
||||
late final MealItem _mealItem;
|
||||
final List<MealItem> _listMealItems;
|
||||
late String _barcode;
|
||||
late bool _test;
|
||||
Widget MealItemForm(Meal meal, List<MealItem> recent, [String? barcode, bool? test]) {
|
||||
return IngredientForm(
|
||||
// TODO we use planId 0 here cause we don't have one and we don't need it I think?
|
||||
recent: recent.map((e) => Log.fromMealItem(e, 0, e.mealId)).toList(),
|
||||
onSave: (BuildContext context, MealItem mealItem, DateTime? dt) {
|
||||
mealItem.mealId = meal.id!;
|
||||
Provider.of<NutritionPlansProvider>(context, listen: false).addMealItem(mealItem, meal);
|
||||
},
|
||||
barcode: barcode ?? '',
|
||||
test: test ?? false,
|
||||
withDate: false);
|
||||
}
|
||||
|
||||
Widget IngredientLogForm(NutritionalPlan plan) {
|
||||
return IngredientForm(
|
||||
recent: plan.diaryEntries,
|
||||
onSave: (BuildContext context, MealItem mealItem, DateTime? dt) {
|
||||
Provider.of<NutritionPlansProvider>(context, listen: false)
|
||||
.logIngredientToDiary(mealItem, plan.id!, dt);
|
||||
},
|
||||
withDate: true);
|
||||
}
|
||||
|
||||
/// IngredientForm is a form that lets the user pick an ingredient (and amount) to
|
||||
/// log to the diary or to add to a meal.
|
||||
class IngredientForm extends StatefulWidget {
|
||||
final Function(BuildContext context, MealItem mealItem, DateTime? dt) onSave;
|
||||
final List<Log> recent;
|
||||
final bool withDate;
|
||||
final String barcode;
|
||||
final bool test;
|
||||
|
||||
const IngredientForm({
|
||||
required this.recent,
|
||||
required this.onSave,
|
||||
required this.withDate,
|
||||
this.barcode = '',
|
||||
this.test = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<IngredientForm> createState() => IngredientFormState();
|
||||
}
|
||||
|
||||
class IngredientFormState extends State<IngredientForm> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
final _ingredientIdController = TextEditingController();
|
||||
final _ingredientController = TextEditingController();
|
||||
final _ingredientIdController = TextEditingController();
|
||||
final _amountController = TextEditingController();
|
||||
final _dateController = TextEditingController(); // optional
|
||||
final _timeController = TextEditingController(); // optional
|
||||
final _mealItem = MealItem.empty();
|
||||
|
||||
MealItemForm(this._meal, this._listMealItems, [mealItem, code, test]) {
|
||||
_mealItem = mealItem ?? MealItem.empty();
|
||||
_test = test ?? false;
|
||||
_barcode = code ?? '';
|
||||
_mealItem.mealId = _meal.id!;
|
||||
bool validIngredientId = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
_dateController.text = toDate(now)!;
|
||||
_timeController.text = timeToString(TimeOfDay.fromDateTime(now))!;
|
||||
}
|
||||
|
||||
TextEditingController get ingredientIdController => _ingredientIdController;
|
||||
@@ -139,114 +185,6 @@ class MealItemForm extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String unit = AppLocalizations.of(context).g;
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: _form,
|
||||
child: Column(
|
||||
children: [
|
||||
IngredientTypeahead(
|
||||
_ingredientIdController,
|
||||
_ingredientController,
|
||||
barcode: _barcode,
|
||||
test: _test,
|
||||
),
|
||||
TextFormField(
|
||||
key: const Key('field-weight'),
|
||||
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
onFieldSubmitted: (_) {},
|
||||
onSaved: (newValue) {
|
||||
_mealItem.amount = double.parse(newValue!);
|
||||
},
|
||||
validator: (value) {
|
||||
try {
|
||||
double.parse(value!);
|
||||
} catch (error) {
|
||||
return AppLocalizations.of(context).enterValidNumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
key: const Key(SUBMIT_BUTTON_KEY_NAME),
|
||||
child: Text(AppLocalizations.of(context).save),
|
||||
onPressed: () async {
|
||||
if (!_form.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
_form.currentState!.save();
|
||||
_mealItem.ingredientId = int.parse(_ingredientIdController.text);
|
||||
|
||||
try {
|
||||
Provider.of<NutritionPlansProvider>(context, listen: false)
|
||||
.addMealItem(_mealItem, _meal);
|
||||
} on WgerHttpException catch (error) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
} catch (error) {
|
||||
showErrorDialog(error, context);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
if (_listMealItems.isNotEmpty) const SizedBox(height: 10.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Text(AppLocalizations.of(context).recentlyUsedIngredients),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _listMealItems.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
_ingredientController.text = _listMealItems[index].ingredient.name;
|
||||
_ingredientIdController.text =
|
||||
_listMealItems[index].ingredient.id.toString();
|
||||
_amountController.text = _listMealItems[index].amount.toStringAsFixed(0);
|
||||
_mealItem.ingredientId = _listMealItems[index].ingredientId;
|
||||
_mealItem.amount = _listMealItems[index].amount;
|
||||
},
|
||||
title: Text(_listMealItems[index].ingredient.name),
|
||||
subtitle: Text('${_listMealItems[index].amount.toStringAsFixed(0)}$unit'),
|
||||
trailing: const Icon(Icons.copy),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IngredientLogForm extends StatelessWidget {
|
||||
late MealItem _mealItem;
|
||||
final NutritionalPlan _plan;
|
||||
|
||||
final _form = GlobalKey<FormState>();
|
||||
final _ingredientController = TextEditingController();
|
||||
final _ingredientIdController = TextEditingController();
|
||||
final _amountController = TextEditingController();
|
||||
final _dateController = TextEditingController();
|
||||
final _timeController = TextEditingController();
|
||||
|
||||
IngredientLogForm(this._plan) {
|
||||
_mealItem = MealItem.empty();
|
||||
final now = DateTime.now();
|
||||
_dateController.text = toDate(now)!;
|
||||
_timeController.text = timeToString(TimeOfDay.fromDateTime(now))!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final diaryEntries = _plan.diaryEntries;
|
||||
final String unit = AppLocalizations.of(context).g;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
@@ -257,84 +195,142 @@ class IngredientLogForm extends StatelessWidget {
|
||||
IngredientTypeahead(
|
||||
_ingredientIdController,
|
||||
_ingredientController,
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
onFieldSubmitted: (_) {},
|
||||
onSaved: (newValue) {
|
||||
_mealItem.amount = double.parse(newValue!);
|
||||
},
|
||||
validator: (value) {
|
||||
try {
|
||||
double.parse(value!);
|
||||
} catch (error) {
|
||||
return AppLocalizations.of(context).enterValidNumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
barcode: widget.barcode,
|
||||
test: widget.test,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
// Stop keyboard from appearing
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).date,
|
||||
// suffixIcon: const Icon(Icons.calendar_today),
|
||||
),
|
||||
enableInteractiveSelection: false,
|
||||
controller: _dateController,
|
||||
onTap: () async {
|
||||
// Show Date Picker Here
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(DateTime.now().year - 10),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
if (pickedDate != null) {
|
||||
_dateController.text = toDate(pickedDate)!;
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_dateController.text = newValue!;
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
key: const Key('field-time'),
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).time,
|
||||
//suffixIcon: const Icon(Icons.punch_clock)
|
||||
),
|
||||
controller: _timeController,
|
||||
onTap: () async {
|
||||
// Stop keyboard from appearing
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
|
||||
// Open time picker
|
||||
final pickedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: stringToTime(_timeController.text),
|
||||
);
|
||||
if (pickedTime != null) {
|
||||
_timeController.text = timeToString(pickedTime)!;
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_timeController.text = newValue!;
|
||||
},
|
||||
key: const Key('field-weight'), // needed ?
|
||||
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
onFieldSubmitted: (_) {},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
final v = double.tryParse(value);
|
||||
if (v != null) {
|
||||
_mealItem.amount = v;
|
||||
}
|
||||
});
|
||||
},
|
||||
onSaved: (value) {
|
||||
_mealItem.amount = double.parse(value!);
|
||||
},
|
||||
validator: (value) {
|
||||
try {
|
||||
double.parse(value!);
|
||||
} catch (error) {
|
||||
return AppLocalizations.of(context).enterValidNumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.withDate)
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
// Stop keyboard from appearing
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).date,
|
||||
// suffixIcon: const Icon(Icons.calendar_today),
|
||||
),
|
||||
enableInteractiveSelection: false,
|
||||
controller: _dateController,
|
||||
onTap: () async {
|
||||
// Show Date Picker Here
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(DateTime.now().year - 10),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
if (pickedDate != null) {
|
||||
_dateController.text = toDate(pickedDate)!;
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_dateController.text = newValue!;
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.withDate)
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
key: const Key('field-time'),
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).time,
|
||||
//suffixIcon: const Icon(Icons.punch_clock)
|
||||
),
|
||||
controller: _timeController,
|
||||
onTap: () async {
|
||||
// Stop keyboard from appearing
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
|
||||
// Open time picker
|
||||
final pickedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: stringToTime(_timeController.text),
|
||||
);
|
||||
if (pickedTime != null) {
|
||||
_timeController.text = timeToString(pickedTime)!;
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_timeController.text = newValue!;
|
||||
},
|
||||
onFieldSubmitted: (_) {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (validIngredientId)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Macros preview',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
FutureBuilder<Ingredient>(
|
||||
future: Provider.of<NutritionPlansProvider>(context, listen: false)
|
||||
.fetchIngredient(_mealItem.ingredientId),
|
||||
builder: (BuildContext context, AsyncSnapshot<Ingredient> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
_mealItem.ingredient = snapshot.data!;
|
||||
return ListTile(
|
||||
leading: IngredientAvatar(ingredient: _mealItem.ingredient),
|
||||
title:
|
||||
Text(getShortNutritionValues(_mealItem.nutritionalValues, context)),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
'Ingredient lookup error: ${snapshot.error}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
key: const Key(
|
||||
SUBMIT_BUTTON_KEY_NAME), // needed? mealItemForm had it, but not ingredientlogform
|
||||
|
||||
child: Text(AppLocalizations.of(context).save),
|
||||
onPressed: () async {
|
||||
if (!_form.currentState!.validate()) {
|
||||
@@ -347,8 +343,7 @@ class IngredientLogForm extends StatelessWidget {
|
||||
var date = DateTime.parse(_dateController.text);
|
||||
final tod = stringToTime(_timeController.text);
|
||||
date = DateTime(date.year, date.month, date.day, tod.hour, tod.minute);
|
||||
Provider.of<NutritionPlansProvider>(context, listen: false)
|
||||
.logIngredientToDiary(_mealItem, _plan.id!, date);
|
||||
widget.onSave(context, _mealItem, date);
|
||||
} on WgerHttpException catch (error) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
} catch (error) {
|
||||
@@ -357,27 +352,33 @@ class IngredientLogForm extends StatelessWidget {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
if (diaryEntries.isNotEmpty) const SizedBox(height: 10.0),
|
||||
if (widget.recent.isNotEmpty) const SizedBox(height: 10.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Text(AppLocalizations.of(context).recentlyUsedIngredients),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: diaryEntries.length,
|
||||
itemCount: widget.recent.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
_ingredientController.text = diaryEntries[index].ingredient.name;
|
||||
_ingredientIdController.text = diaryEntries[index].ingredient.id.toString();
|
||||
_amountController.text = diaryEntries[index].amount.toStringAsFixed(0);
|
||||
_mealItem.ingredientId = diaryEntries[index].ingredientId;
|
||||
_mealItem.amount = diaryEntries[index].amount;
|
||||
_ingredientController.text = widget.recent[index].ingredient.name;
|
||||
_ingredientIdController.text =
|
||||
widget.recent[index].ingredient.id.toString();
|
||||
_amountController.text = widget.recent[index].amount.toStringAsFixed(0);
|
||||
setState(() {
|
||||
_mealItem.ingredientId = widget.recent[index].ingredientId;
|
||||
_mealItem.amount = widget.recent[index].amount;
|
||||
validIngredientId = true;
|
||||
});
|
||||
},
|
||||
title: Text(_plan.diaryEntries[index].ingredient.name),
|
||||
subtitle: Text('${diaryEntries[index].amount.toStringAsFixed(0)}$unit'),
|
||||
title: Text(
|
||||
'${widget.recent[index].ingredient.name} (${widget.recent[index].amount.toStringAsFixed(0)}$unit)'),
|
||||
subtitle: Text(getShortNutritionValues(
|
||||
widget.recent[index].ingredient.nutritionalValues, context)),
|
||||
trailing: const Icon(Icons.copy),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -40,3 +40,12 @@ List<Widget> getMutedNutritionalValues(NutritionalValues values, BuildContext co
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
];
|
||||
|
||||
String getShortNutritionValues(NutritionalValues values, BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
final e = '${loc.energyShort} ${loc.kcalValue(values.energy.toStringAsFixed(0))}';
|
||||
final p = '${loc.proteinShort} ${loc.gValue(values.protein.toStringAsFixed(0))}';
|
||||
final c = '${loc.carbohydratesShort} ${loc.gValue(values.carbohydrates.toStringAsFixed(0))}';
|
||||
final f = '${loc.fatShort} ${loc.gValue(values.fat.toStringAsFixed(0))}';
|
||||
return '$e / $p / $c / $f';
|
||||
}
|
||||
|
||||
@@ -20,13 +20,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/misc.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/providers/nutrition.dart';
|
||||
import 'package:wger/screens/form_screen.dart';
|
||||
import 'package:wger/widgets/core/core.dart';
|
||||
import 'package:wger/widgets/nutrition/charts.dart';
|
||||
import 'package:wger/widgets/nutrition/forms.dart';
|
||||
import 'package:wger/widgets/nutrition/helpers.dart';
|
||||
@@ -208,18 +206,7 @@ class MealItemWidget extends StatelessWidget {
|
||||
final values = _item.nutritionalValues;
|
||||
|
||||
return ListTile(
|
||||
leading: _item.ingredient.image != null
|
||||
? GestureDetector(
|
||||
child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredient.image!.image)),
|
||||
onTap: () async {
|
||||
if (_item.ingredient.image!.objectUrl != '') {
|
||||
return launchURL(_item.ingredient.image!.objectUrl, context);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)),
|
||||
leading: IngredientAvatar(ingredient: _item.ingredient),
|
||||
title: Text(
|
||||
'${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -273,18 +260,7 @@ class LogDiaryItemWidget extends StatelessWidget {
|
||||
final values = _item.nutritionalValues;
|
||||
|
||||
return ListTile(
|
||||
leading: _item.ingredient.image != null
|
||||
? GestureDetector(
|
||||
child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredient.image!.image)),
|
||||
onTap: () async {
|
||||
if (_item.ingredient.image!.objectUrl != '') {
|
||||
return launchURL(_item.ingredient.image!.objectUrl, context);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)),
|
||||
leading: IngredientAvatar(ingredient: _item.ingredient),
|
||||
title: Text(
|
||||
'${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -25,9 +25,11 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/misc.dart';
|
||||
import 'package:wger/helpers/platform.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/models/exercises/ingredient_api.dart';
|
||||
import 'package:wger/models/nutrition/ingredient.dart';
|
||||
import 'package:wger/models/nutrition/log.dart';
|
||||
import 'package:wger/models/nutrition/nutritional_plan.dart';
|
||||
import 'package:wger/providers/nutrition.dart';
|
||||
@@ -335,3 +337,23 @@ class NutritionDiaryEntry extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IngredientAvatar extends StatelessWidget {
|
||||
final Ingredient ingredient;
|
||||
|
||||
const IngredientAvatar({super.key, required this.ingredient});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ingredient.image != null
|
||||
? GestureDetector(
|
||||
child: CircleAvatar(backgroundImage: NetworkImage(ingredient.image!.image)),
|
||||
onTap: () async {
|
||||
if (ingredient.image!.objectUrl != '') {
|
||||
return launchURL(ingredient.image!.objectUrl, context);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ void main() {
|
||||
home: Scaffold(
|
||||
body: Scrollable(
|
||||
viewportBuilder: (BuildContext context, ViewportOffset position) =>
|
||||
MealItemForm(meal, const [], null, code, test),
|
||||
MealItemForm(meal, const [], code, test),
|
||||
),
|
||||
),
|
||||
routes: {
|
||||
@@ -213,7 +213,7 @@ void main() {
|
||||
testWidgets('confirm found ingredient dialog', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
|
||||
|
||||
final MealItemForm formScreen = tester.widget(find.byType(MealItemForm));
|
||||
final IngredientFormState formState = tester.state(find.byType(IngredientForm));
|
||||
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -223,7 +223,7 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(formScreen.ingredientIdController.text, '1');
|
||||
expect(formState.ingredientIdController.text, '1');
|
||||
});
|
||||
|
||||
testWidgets('close found ingredient dialog', (WidgetTester tester) async {
|
||||
@@ -293,7 +293,7 @@ void main() {
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
|
||||
|
||||
final MealItemForm formScreen = tester.widget(find.byType(MealItemForm));
|
||||
final IngredientFormState formState = tester.state(find.byType(IngredientForm));
|
||||
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -303,7 +303,7 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(formScreen.ingredientIdController.text, '1');
|
||||
expect(formState.ingredientIdController.text, '1');
|
||||
|
||||
await tester.enterText(find.byKey(const Key('field-weight')), '2');
|
||||
await tester.pumpAndSettle();
|
||||
@@ -313,7 +313,7 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(formScreen.mealItem.amount, 2);
|
||||
expect(formState.mealItem.amount, 2);
|
||||
|
||||
verify(mockNutrition.addMealItem(any, meal1));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user