mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Add chart for percentage of logged nutritional values for meals
This commit is contained in:
@@ -89,9 +89,9 @@ Widget createNutritionalPlanScreen({locale = 'en'}) {
|
||||
);
|
||||
|
||||
// Add logs
|
||||
plan.logs.add(Log.fromMealItem(mealItem1, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.logs.add(Log.fromMealItem(mealItem2, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.logs.add(Log.fromMealItem(mealItem3, 1, 1, DateTime(2021, 6, 10)));
|
||||
plan.diaryEntries.add(Log.fromMealItem(mealItem1, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.diaryEntries.add(Log.fromMealItem(mealItem2, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.diaryEntries.add(Log.fromMealItem(mealItem3, 1, 1, DateTime(2021, 6, 10)));
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
|
||||
@@ -336,10 +336,6 @@
|
||||
"@energyShort": {
|
||||
"description": "The first letter or short name of the word 'Energy', used in overviews"
|
||||
},
|
||||
"kcal": "kcal",
|
||||
"@kcal": {
|
||||
"description": "Energy in a meal in kilocalories, kcal"
|
||||
},
|
||||
"macronutrients": "Macronutrients",
|
||||
"@macronutrients": {},
|
||||
"planned": "Planned",
|
||||
@@ -350,6 +346,7 @@
|
||||
"@logged": {
|
||||
"description": "Header for the column of 'logged' nutritional values, i.e. what was eaten"
|
||||
},
|
||||
"loggedToday": "LoggedToday",
|
||||
"weekAverage": "7 day average",
|
||||
"@weekAverage": {
|
||||
"description": "Header for the column of '7 day average' nutritional values, i.e. what was logged last week"
|
||||
@@ -366,6 +363,18 @@
|
||||
"@total": {
|
||||
"description": "Label used for total sums of e.g. calories or similar"
|
||||
},
|
||||
"kcal": "kcal",
|
||||
"@kcal": {
|
||||
"description": "Energy in a meal in kilocalories, kcal"
|
||||
},
|
||||
"kcalValue": "{value} kcal",
|
||||
"@kcalValue": {
|
||||
"description": "A value in kcal, e.g. 500 kcal",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"value": {}
|
||||
}
|
||||
},
|
||||
"kJ": "kJ",
|
||||
"@kJ": {
|
||||
"description": "Energy in a meal in kilo joules, kJ"
|
||||
@@ -374,6 +383,22 @@
|
||||
"@g": {
|
||||
"description": "Abbreviation for gram"
|
||||
},
|
||||
"gValue": "{value} g",
|
||||
"@gValue": {
|
||||
"description": "A value in grams, e.g. 5 g",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"value": {}
|
||||
}
|
||||
},
|
||||
"percentValue": "{value} %",
|
||||
"@percentValue": {
|
||||
"description": "A value in percent, e.g. 10 %",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"value": {}
|
||||
}
|
||||
},
|
||||
"protein": "Protein",
|
||||
"@protein": {},
|
||||
"proteinShort": "P",
|
||||
|
||||
@@ -12,7 +12,7 @@ part of 'exercise_api.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
ExerciseApiData _$ExerciseApiDataFromJson(Map<String, dynamic> json) {
|
||||
return _ExerciseBaseData.fromJson(json);
|
||||
|
||||
@@ -12,7 +12,7 @@ part of 'ingredient_api.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
IngredientApiSearchDetails _$IngredientApiSearchDetailsFromJson(Map<String, dynamic> json) {
|
||||
return _IngredientApiSearchDetails.fromJson(json);
|
||||
|
||||
@@ -69,7 +69,7 @@ class Log {
|
||||
|
||||
Log.fromMealItem(MealItem mealItem, this.planId, this.mealId, [DateTime? dateTime]) {
|
||||
ingredientId = mealItem.ingredientId;
|
||||
ingredientObj = mealItem.ingredientObj;
|
||||
ingredientObj = mealItem.ingredient;
|
||||
weightUnitId = mealItem.weightUnitId;
|
||||
datetime = dateTime ?? DateTime.now();
|
||||
amount = mealItem.amount;
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/helpers/misc.dart';
|
||||
import 'package:wger/models/nutrition/log.dart';
|
||||
import 'package:wger/models/nutrition/meal_item.dart';
|
||||
import 'package:wger/models/nutrition/nutritional_values.dart';
|
||||
|
||||
@@ -39,33 +40,32 @@ class Meal {
|
||||
@JsonKey(name: 'name')
|
||||
late String name;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false, name: 'meal_items', defaultValue: [])
|
||||
@JsonKey(includeFromJson: false, includeToJson: false, defaultValue: [])
|
||||
List<MealItem> mealItems = [];
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false, defaultValue: [])
|
||||
List<Log> diaryEntries = [];
|
||||
|
||||
Meal({
|
||||
this.id,
|
||||
int? plan,
|
||||
TimeOfDay? time,
|
||||
this.time,
|
||||
String? name,
|
||||
List<MealItem>? mealItems,
|
||||
List<Log>? diaryEntries,
|
||||
}) {
|
||||
if (plan != null) {
|
||||
planId = plan;
|
||||
}
|
||||
|
||||
this.mealItems = mealItems ?? [];
|
||||
|
||||
this.time = time ?? TimeOfDay.fromDateTime(clock.now());
|
||||
this.diaryEntries = diaryEntries ?? [];
|
||||
//this.time = time ?? TimeOfDay.fromDateTime(clock.now());
|
||||
this.name = name ?? '';
|
||||
}
|
||||
|
||||
// Boilerplate
|
||||
factory Meal.fromJson(Map<String, dynamic> json) => _$MealFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$MealToJson(this);
|
||||
|
||||
/// Calculations
|
||||
NutritionalValues get nutritionalValues {
|
||||
NutritionalValues get plannedNutritionalValues {
|
||||
// This is already done on the server. It might be better to read it from there.
|
||||
var out = NutritionalValues();
|
||||
|
||||
@@ -75,4 +75,20 @@ class Meal {
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Returns the logged nutritional values for today
|
||||
NutritionalValues get loggedNutritionalValuesToday {
|
||||
var out = NutritionalValues();
|
||||
|
||||
for (final item in diaryEntries.where((l) => l.datetime.isSameDayAs(DateTime.now()))) {
|
||||
out += item.nutritionalValues;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Boilerplate
|
||||
factory Meal.fromJson(Map<String, dynamic> json) => _$MealFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$MealToJson(this);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class MealItem {
|
||||
late int ingredientId;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
late Ingredient ingredientObj;
|
||||
late Ingredient ingredient;
|
||||
|
||||
@JsonKey(required: false, name: 'weight_unit')
|
||||
int? weightUnitId;
|
||||
@@ -59,7 +59,7 @@ class MealItem {
|
||||
this.mealId = mealId;
|
||||
}
|
||||
if (ingredient != null) {
|
||||
ingredientObj = ingredient;
|
||||
this.ingredient = ingredient;
|
||||
ingredientId = ingredient.id;
|
||||
}
|
||||
}
|
||||
@@ -79,14 +79,14 @@ class MealItem {
|
||||
//final weight = this.weightUnit == null ? amount : amount * weightUnit.amount * weightUnit.grams;
|
||||
final weight = amount;
|
||||
|
||||
out.energy = ingredientObj.energy * weight / 100;
|
||||
out.protein = ingredientObj.protein * weight / 100;
|
||||
out.carbohydrates = ingredientObj.carbohydrates * weight / 100;
|
||||
out.carbohydratesSugar = ingredientObj.carbohydratesSugar * weight / 100;
|
||||
out.fat = ingredientObj.fat * weight / 100;
|
||||
out.fatSaturated = ingredientObj.fatSaturated * weight / 100;
|
||||
out.fibres = ingredientObj.fibres * weight / 100;
|
||||
out.sodium = ingredientObj.sodium * weight / 100;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class NutritionalPlan {
|
||||
List<Meal> meals = [];
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false, defaultValue: [])
|
||||
List<Log> logs = [];
|
||||
List<Log> diaryEntries = [];
|
||||
|
||||
NutritionalPlan({
|
||||
this.id,
|
||||
@@ -70,10 +70,10 @@ class NutritionalPlan {
|
||||
this.goalCarbohydrates,
|
||||
this.goalFat,
|
||||
List<Meal>? meals,
|
||||
List<Log>? logs,
|
||||
List<Log>? diaryEntries,
|
||||
}) {
|
||||
this.meals = meals ?? [];
|
||||
this.logs = logs ?? [];
|
||||
this.diaryEntries = diaryEntries ?? [];
|
||||
}
|
||||
|
||||
NutritionalPlan.empty() {
|
||||
@@ -97,7 +97,7 @@ class NutritionalPlan {
|
||||
var out = NutritionalValues();
|
||||
|
||||
for (final meal in meals) {
|
||||
out += meal.nutritionalValues;
|
||||
out += meal.plannedNutritionalValues;
|
||||
}
|
||||
|
||||
return out;
|
||||
@@ -114,7 +114,7 @@ class NutritionalPlan {
|
||||
final currentDate = DateTime.now();
|
||||
final sevenDaysAgo = currentDate.subtract(const Duration(days: 7));
|
||||
|
||||
final entries = logs.where((obj) {
|
||||
final entries = diaryEntries.where((obj) {
|
||||
final DateTime objDate = obj.datetime;
|
||||
return objDate.isAfter(sevenDaysAgo) && objDate.isBefore(currentDate);
|
||||
}).toList();
|
||||
@@ -151,7 +151,7 @@ class NutritionalPlan {
|
||||
|
||||
Map<DateTime, NutritionalValues> get logEntriesValues {
|
||||
final out = <DateTime, NutritionalValues>{};
|
||||
for (final log in logs) {
|
||||
for (final log in diaryEntries) {
|
||||
final date = DateTime(log.datetime.year, log.datetime.month, log.datetime.day);
|
||||
|
||||
if (!out.containsKey(date)) {
|
||||
@@ -175,7 +175,7 @@ class NutritionalPlan {
|
||||
/// Returns the nutritional logs for the given date
|
||||
List<Log> getLogsForDate(DateTime date) {
|
||||
final List<Log> out = [];
|
||||
for (final log in logs) {
|
||||
for (final log in diaryEntries) {
|
||||
final dateKey = DateTime(date.year, date.month, date.day);
|
||||
final logKey = DateTime(log.datetime.year, log.datetime.month, log.datetime.day);
|
||||
|
||||
@@ -203,4 +203,14 @@ class NutritionalPlan {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Meal pseudoMealOthers(String name) {
|
||||
return Meal(
|
||||
id: -1,
|
||||
plan: id,
|
||||
name: name,
|
||||
time: null,
|
||||
diaryEntries: diaryEntries.where((e) => e.mealId == null).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,13 @@ NutritionalPlan _$NutritionalPlanFromJson(Map<String, dynamic> json) {
|
||||
);
|
||||
return NutritionalPlan(
|
||||
id: json['id'] as int?,
|
||||
description: json['description'] as String,
|
||||
creationDate: DateTime.parse(json['creation_date'] as String),
|
||||
onlyLogging: json['only_logging'] as bool? ?? false,
|
||||
goalEnergy: json['goal_energy'] as num?,
|
||||
goalProtein: json['goal_protein'] as num?,
|
||||
goalCarbohydrates: json['goal_carbohydrates'] as num?,
|
||||
goalFat: json['goal_fat'] as num?,
|
||||
description: json['description'] as String,
|
||||
creationDate: DateTime.parse(json['creation_date'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,9 +106,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
/// Fetches and sets all plans fully, i.e. with all corresponding child objects
|
||||
Future<void> fetchAndSetAllPlansFull() async {
|
||||
final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(_nutritionalPlansPath));
|
||||
for (final entry in data) {
|
||||
await fetchAndSetPlanFull(entry['id']);
|
||||
}
|
||||
await Future.wait(data.map((e) => fetchAndSetPlanFull(e['id'])).toList());
|
||||
}
|
||||
|
||||
/// Fetches and sets the given nutritional plan
|
||||
@@ -153,7 +151,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
final image = IngredientImage.fromJson(mealItemData['image']);
|
||||
ingredient.image = image;
|
||||
}
|
||||
mealItem.ingredientObj = ingredient;
|
||||
mealItem.ingredient = ingredient;
|
||||
mealItems.add(mealItem);
|
||||
}
|
||||
meal.mealItems = mealItems;
|
||||
@@ -163,6 +161,9 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
|
||||
// Logs
|
||||
await fetchAndSetLogs(plan);
|
||||
for (final meal in meals) {
|
||||
meal.diaryEntries = plan.diaryEntries.where((e) => e.mealId == meal.id).toList();
|
||||
}
|
||||
|
||||
// ... and done
|
||||
notifyListeners();
|
||||
@@ -255,7 +256,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
final data = await baseProvider.post(mealItem.toJson(), baseProvider.makeUrl(_mealItemPath));
|
||||
|
||||
mealItem = MealItem.fromJson(data);
|
||||
mealItem.ingredientObj = await fetchIngredient(mealItem.ingredientId);
|
||||
mealItem.ingredient = await fetchIngredient(mealItem.ingredientId);
|
||||
meal.mealItems.add(mealItem);
|
||||
notifyListeners();
|
||||
|
||||
@@ -385,7 +386,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
baseProvider.makeUrl(_nutritionDiaryPath),
|
||||
);
|
||||
log.id = data['id'];
|
||||
plan.logs.add(log);
|
||||
plan.diaryEntries.add(log);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -393,12 +394,12 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
/// Log custom ingredient to nutrition diary
|
||||
Future<void> logIngredientToDiary(MealItem mealItem, int planId, [DateTime? dateTime]) async {
|
||||
final plan = findById(planId);
|
||||
mealItem.ingredientObj = await fetchIngredient(mealItem.ingredientId);
|
||||
mealItem.ingredient = await fetchIngredient(mealItem.ingredientId);
|
||||
final Log log = Log.fromMealItem(mealItem, plan.id!, null, dateTime);
|
||||
|
||||
final data = await baseProvider.post(log.toJson(), baseProvider.makeUrl(_nutritionDiaryPath));
|
||||
log.id = data['id'];
|
||||
plan.logs.add(log);
|
||||
plan.diaryEntries.add(log);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -407,7 +408,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
await baseProvider.deleteRequest(_nutritionDiaryPath, logId);
|
||||
|
||||
final plan = findById(planId);
|
||||
plan.logs.removeWhere((element) => element.id == logId);
|
||||
plan.diaryEntries.removeWhere((element) => element.id == logId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -420,12 +421,12 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
),
|
||||
);
|
||||
|
||||
plan.logs = [];
|
||||
plan.diaryEntries = [];
|
||||
for (final logData in data) {
|
||||
final log = Log.fromJson(logData);
|
||||
final ingredient = await fetchIngredient(log.ingredientId);
|
||||
log.ingredientObj = ingredient;
|
||||
plan.logs.add(log);
|
||||
plan.diaryEntries.add(log);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -85,16 +85,16 @@ class _DashboardNutritionWidgetState extends State<DashboardNutritionWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).energyShort} ${meal.nutritionalValues.energy.toStringAsFixed(0)}${AppLocalizations.of(context).kcal}'),
|
||||
'${AppLocalizations.of(context).energyShort} ${meal.plannedNutritionalValues.energy.toStringAsFixed(0)}${AppLocalizations.of(context).kcal}'),
|
||||
const MutedText(' / '),
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).proteinShort} ${meal.nutritionalValues.protein.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
|
||||
'${AppLocalizations.of(context).proteinShort} ${meal.plannedNutritionalValues.protein.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
|
||||
const MutedText(' / '),
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).carbohydratesShort} ${meal.nutritionalValues.carbohydrates.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
|
||||
'${AppLocalizations.of(context).carbohydratesShort} ${meal.plannedNutritionalValues.carbohydrates.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
|
||||
const MutedText(' / '),
|
||||
MutedText(
|
||||
'${AppLocalizations.of(context).fatShort} ${meal.nutritionalValues.fat.toStringAsFixed(0)}${AppLocalizations.of(context).g} '),
|
||||
'${AppLocalizations.of(context).fatShort} ${meal.plannedNutritionalValues.fat.toStringAsFixed(0)}${AppLocalizations.of(context).g} '),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
@@ -127,7 +127,7 @@ class _DashboardNutritionWidgetState extends State<DashboardNutritionWidget> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.ingredientObj.name,
|
||||
item.ingredient.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -201,14 +201,11 @@ class NutritionalDiaryChartWidgetFlState extends State<NutritionalDiaryChartWidg
|
||||
if (value == meta.max) {
|
||||
return Container();
|
||||
}
|
||||
const style = TextStyle(
|
||||
fontSize: 10,
|
||||
);
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(
|
||||
meta.formattedValue,
|
||||
style: style,
|
||||
AppLocalizations.of(context).gValue(meta.formattedValue),
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -391,6 +388,167 @@ class NutritionalDiaryChartWidgetFlState extends State<NutritionalDiaryChartWidg
|
||||
}
|
||||
}
|
||||
|
||||
class MealDiaryBarChartWidget extends StatefulWidget {
|
||||
const MealDiaryBarChartWidget({
|
||||
super.key,
|
||||
required NutritionalValues logged,
|
||||
required NutritionalValues planned,
|
||||
}) : _logged = logged,
|
||||
_planned = planned;
|
||||
|
||||
final NutritionalValues _logged;
|
||||
final NutritionalValues _planned;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => MealDiaryBarChartWidgetState();
|
||||
}
|
||||
|
||||
class MealDiaryBarChartWidgetState extends State<MealDiaryBarChartWidget> {
|
||||
Widget bottomTitles(double value, TitleMeta meta) {
|
||||
String text;
|
||||
switch (value.toInt()) {
|
||||
case 0:
|
||||
text = AppLocalizations.of(context).protein;
|
||||
break;
|
||||
case 1:
|
||||
text = AppLocalizations.of(context).carbohydrates;
|
||||
break;
|
||||
case 2:
|
||||
text = AppLocalizations.of(context).fat;
|
||||
break;
|
||||
case 3:
|
||||
text = AppLocalizations.of(context).energy;
|
||||
break;
|
||||
default:
|
||||
text = '';
|
||||
break;
|
||||
}
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget leftTitles(double value, TitleMeta meta) => SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).percentValue(value.toStringAsFixed(0)),
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 2.5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final barsSpace = 1.0 * constraints.maxWidth / 400;
|
||||
final barsWidth = 10.0 * constraints.maxWidth / 400;
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.center,
|
||||
barTouchData: BarTouchData(
|
||||
enabled: false,
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 48,
|
||||
getTitlesWidget: bottomTitles,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: leftTitles,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
checkToShowHorizontalLine: (value) => value % 10 == 0,
|
||||
getDrawingHorizontalLine: (value) => const FlLine(
|
||||
color: Colors.black,
|
||||
strokeWidth: 1,
|
||||
),
|
||||
drawVerticalLine: false,
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: false,
|
||||
),
|
||||
groupsSpace: 30,
|
||||
// groupsSpace: barsSpace,
|
||||
barGroups: [
|
||||
BarChartGroupData(
|
||||
x: 3,
|
||||
barsSpace: barsSpace,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: widget._logged.energy / widget._planned.energy * 100,
|
||||
color: LIST_OF_COLORS3.first,
|
||||
width: barsWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
BarChartGroupData(
|
||||
x: 0,
|
||||
barsSpace: barsSpace,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: widget._logged.protein / widget._planned.protein * 100,
|
||||
color: LIST_OF_COLORS3.first,
|
||||
width: barsWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
BarChartGroupData(
|
||||
x: 1,
|
||||
barsSpace: barsSpace,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: widget._logged.carbohydrates / widget._planned.carbohydrates * 100,
|
||||
color: LIST_OF_COLORS3.first,
|
||||
width: barsWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
BarChartGroupData(
|
||||
x: 2,
|
||||
barsSpace: barsSpace,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: widget._logged.fat / widget._planned.fat * 100,
|
||||
color: LIST_OF_COLORS3.first,
|
||||
width: barsWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
// barGroups: getData(barsWidth, barsSpace),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlNutritionalDiaryChartWidget extends StatefulWidget {
|
||||
final NutritionalPlan _nutritionalPlan;
|
||||
|
||||
@@ -492,7 +650,7 @@ class FlNutritionalDiaryChartWidgetState extends State<FlNutritionalDiaryChartWi
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(
|
||||
'${meta.formattedValue} kcal',
|
||||
AppLocalizations.of(context).kcalValue(meta.formattedValue),
|
||||
style: style,
|
||||
),
|
||||
);
|
||||
@@ -516,7 +674,7 @@ class FlNutritionalDiaryChartWidgetState extends State<FlNutritionalDiaryChartWi
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${(rod.toY - 1).toStringAsFixed(0)} kcal',
|
||||
text: AppLocalizations.of(context).kcalValue((rod.toY - 1).toStringAsFixed(0)),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -203,14 +203,14 @@ class MealItemForm extends StatelessWidget {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
_ingredientController.text = _listMealItems[index].ingredientObj.name;
|
||||
_ingredientController.text = _listMealItems[index].ingredient.name;
|
||||
_ingredientIdController.text =
|
||||
_listMealItems[index].ingredientObj.id.toString();
|
||||
_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].ingredientObj.name),
|
||||
title: Text(_listMealItems[index].ingredient.name),
|
||||
subtitle: Text('${_listMealItems[index].amount.toStringAsFixed(0)}$unit'),
|
||||
trailing: const Icon(Icons.copy),
|
||||
),
|
||||
@@ -242,7 +242,7 @@ class IngredientLogForm extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final diaryEntries = _plan.logs;
|
||||
final diaryEntries = _plan.diaryEntries;
|
||||
final String unit = AppLocalizations.of(context).g;
|
||||
|
||||
return Container(
|
||||
@@ -338,7 +338,7 @@ class IngredientLogForm extends StatelessWidget {
|
||||
_mealItem.ingredientId = diaryEntries[index].ingredientId;
|
||||
_mealItem.amount = diaryEntries[index].amount;
|
||||
},
|
||||
title: Text(_plan.logs[index].ingredientObj.name),
|
||||
title: Text(_plan.diaryEntries[index].ingredientObj.name),
|
||||
subtitle: Text('${diaryEntries[index].amount.toStringAsFixed(0)}$unit'),
|
||||
trailing: const Icon(Icons.copy),
|
||||
),
|
||||
|
||||
@@ -28,22 +28,17 @@ List<Widget> getMutedNutritionalValues(NutritionalValues values, BuildContext co
|
||||
'${values.energy.toStringAsFixed(0)}'
|
||||
'${AppLocalizations.of(context).kcal}',
|
||||
),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).protein}: '
|
||||
'${values.protein.toStringAsFixed(0)}'
|
||||
'${AppLocalizations.of(context).g}',
|
||||
),
|
||||
Text('${AppLocalizations.of(context).protein}: '
|
||||
'${AppLocalizations.of(context).gValue(values.protein.toStringAsFixed(0))}'),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).carbohydrates}: '
|
||||
'${values.carbohydrates.toStringAsFixed(0)} '
|
||||
'${AppLocalizations.of(context).g} '
|
||||
'(${values.carbohydratesSugar.toStringAsFixed(0)} ${AppLocalizations.of(context).sugars})',
|
||||
'${AppLocalizations.of(context).gValue(values.carbohydrates.toStringAsFixed(0))} '
|
||||
'(${AppLocalizations.of(context).gValue(values.carbohydratesSugar.toStringAsFixed(0))} ${AppLocalizations.of(context).sugars})',
|
||||
),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).fat}: '
|
||||
'${values.fat.toStringAsFixed(0)}'
|
||||
'${AppLocalizations.of(context).g} '
|
||||
'(${values.fatSaturated.toStringAsFixed(0)} ${AppLocalizations.of(context).saturatedFat})',
|
||||
'${AppLocalizations.of(context).gValue(values.fat.toStringAsFixed(0))} '
|
||||
'(${AppLocalizations.of(context).gValue(values.fatSaturated.toStringAsFixed(0))} ${AppLocalizations.of(context).saturatedFat})',
|
||||
),
|
||||
];
|
||||
return out;
|
||||
|
||||
@@ -26,6 +26,7 @@ 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';
|
||||
|
||||
@@ -43,7 +44,7 @@ class MealWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MealWidgetState extends State<MealWidget> {
|
||||
bool _showingDetails = false;
|
||||
bool _showDetails = false;
|
||||
bool _editing = false;
|
||||
|
||||
void _toggleEditing() {
|
||||
@@ -54,7 +55,7 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
|
||||
void _toggleDetails() {
|
||||
setState(() {
|
||||
_showingDetails = !_showingDetails;
|
||||
_showDetails = !_showDetails;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,11 +68,12 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MealHeader(
|
||||
editing: _editing,
|
||||
toggleEditing: _toggleEditing,
|
||||
showingDetails: _showingDetails,
|
||||
toggleDetails: _toggleDetails,
|
||||
meal: widget._meal),
|
||||
editing: _editing,
|
||||
toggleEditing: _toggleEditing,
|
||||
showingDetails: _showDetails,
|
||||
toggleDetails: _toggleDetails,
|
||||
meal: widget._meal,
|
||||
),
|
||||
if (_editing)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
@@ -128,8 +130,19 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
],
|
||||
)),
|
||||
const Divider(),
|
||||
...widget._meal.mealItems
|
||||
.map((item) => MealItemWidget(item, _showingDetails, _editing)),
|
||||
...widget._meal.mealItems.map((item) => MealItemWidget(item, _showDetails, _editing)),
|
||||
Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).loggedToday,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (widget._meal.plannedNutritionalValues.energy != 0 &&
|
||||
widget._meal.loggedNutritionalValuesToday.energy != 0)
|
||||
MealDiaryBarChartWidget(
|
||||
planned: widget._meal.plannedNutritionalValues,
|
||||
logged: widget._meal.loggedNutritionalValuesToday,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -157,12 +170,12 @@ class MealItemWidget extends StatelessWidget {
|
||||
final values = _item.nutritionalValues;
|
||||
|
||||
return ListTile(
|
||||
leading: _item.ingredientObj.image != null
|
||||
leading: _item.ingredient.image != null
|
||||
? GestureDetector(
|
||||
child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredientObj.image!.image)),
|
||||
child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredient.image!.image)),
|
||||
onTap: () async {
|
||||
if (_item.ingredientObj.image!.objectUrl != '') {
|
||||
return launchURL(_item.ingredientObj.image!.objectUrl, context);
|
||||
if (_item.ingredient.image!.objectUrl != '') {
|
||||
return launchURL(_item.ingredient.image!.objectUrl, context);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -170,7 +183,7 @@ class MealItemWidget extends StatelessWidget {
|
||||
)
|
||||
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)),
|
||||
title: Text(
|
||||
'${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredientObj.name}',
|
||||
'${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Column(
|
||||
@@ -238,10 +251,11 @@ class MealHeader extends StatelessWidget {
|
||||
_meal.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
_meal.time!.format(context),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
)
|
||||
if (_meal.time != null)
|
||||
Text(
|
||||
_meal.time!.format(context),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
)
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
|
||||
@@ -52,7 +52,14 @@ class NutritionalPlanDetailWidget extends StatelessWidget {
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
const SizedBox(height: 10),
|
||||
..._nutritionalPlan.meals.map((meal) => MealWidget(meal, _nutritionalPlan.allMealItems)),
|
||||
..._nutritionalPlan.meals.map((meal) => MealWidget(
|
||||
meal,
|
||||
_nutritionalPlan.allMealItems,
|
||||
)),
|
||||
MealWidget(
|
||||
_nutritionalPlan.pseudoMealOthers('Other logs'),
|
||||
_nutritionalPlan.allMealItems,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ElevatedButton(
|
||||
@@ -211,7 +218,7 @@ class NutritionalPlanDetailWidget extends StatelessWidget {
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8.0)),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).planned} / ${AppLocalizations.of(context).logged} / ${AppLocalizations.of(context).weekAverage}',
|
||||
AppLocalizations.of(context).logged,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
@@ -252,7 +259,7 @@ class NutritionalPlanDetailWidget extends StatelessWidget {
|
||||
Text(
|
||||
AppLocalizations.of(context).nutritionalDiary,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
|
||||
@@ -38,7 +38,7 @@ void main() {
|
||||
test('Test the nutritionalValues method for meals', () {
|
||||
final meal = plan.meals.first;
|
||||
final values = NutritionalValues.values(518.75, 5.75, 17.5, 3.5, 29.0, 13.75, 49.5, 0.5);
|
||||
expect(meal.nutritionalValues, values);
|
||||
expect(meal.plannedNutritionalValues, values);
|
||||
});
|
||||
|
||||
test('Test that the getter returns all meal items for a plan', () {
|
||||
|
||||
@@ -71,19 +71,19 @@ NutritionalPlan getNutritionalPlan() {
|
||||
ingredientId: 1,
|
||||
amount: 100,
|
||||
);
|
||||
mealItem1.ingredientObj = ingredient1;
|
||||
mealItem1.ingredient = ingredient1;
|
||||
|
||||
final mealItem2 = MealItem(
|
||||
ingredientId: 2,
|
||||
amount: 75,
|
||||
);
|
||||
mealItem2.ingredientObj = ingredient2;
|
||||
mealItem2.ingredient = ingredient2;
|
||||
|
||||
final mealItem3 = MealItem(
|
||||
ingredientId: 3,
|
||||
amount: 300,
|
||||
);
|
||||
mealItem3.ingredientObj = ingredient3;
|
||||
mealItem3.ingredient = ingredient3;
|
||||
|
||||
final meal1 = Meal(
|
||||
id: 1,
|
||||
@@ -109,9 +109,9 @@ NutritionalPlan getNutritionalPlan() {
|
||||
plan.meals = [meal1, meal2];
|
||||
|
||||
// Add logs
|
||||
plan.logs.add(Log.fromMealItem(mealItem1, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.logs.add(Log.fromMealItem(mealItem2, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.logs.add(Log.fromMealItem(mealItem3, 1, 1, DateTime(2021, 6, 10)));
|
||||
plan.diaryEntries.add(Log.fromMealItem(mealItem1, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.diaryEntries.add(Log.fromMealItem(mealItem2, 1, 1, DateTime(2021, 6, 1)));
|
||||
plan.diaryEntries.add(Log.fromMealItem(mealItem3, 1, 1, DateTime(2021, 6, 10)));
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user