recipe list and detail screens
This commit is contained in:
parent
5e60ea09ce
commit
6b4f588d5d
19
TODO
19
TODO
@ -1,5 +1,6 @@
|
|||||||
MAIN TASKS:
|
MAIN TASKS:
|
||||||
General/Framework:
|
General/Framework:
|
||||||
|
☐ create app icon
|
||||||
☐ show indicator and make all fields readonly if user somehow gets to a deleted record detail view
|
☐ show indicator and make all fields readonly if user somehow gets to a deleted record detail view
|
||||||
☐ clean up controllers (dispose method of each stateful widget)
|
☐ clean up controllers (dispose method of each stateful widget)
|
||||||
☐ account for deleted/disabled elements in dropdowns
|
☐ account for deleted/disabled elements in dropdowns
|
||||||
@ -8,14 +9,9 @@ MAIN TASKS:
|
|||||||
☐ implement component for durations
|
☐ implement component for durations
|
||||||
☐ change placement of delete and floating button because its very easy to accidentally hit delete
|
☐ change placement of delete and floating button because its very easy to accidentally hit delete
|
||||||
Recipe:
|
Recipe:
|
||||||
✔ add model for recipe @done(21-12-11 02:23)
|
✔ recipe list screen @done(21-12-11 22:01)
|
||||||
✔ add model for ingredient (relation betweeen recipe and meal) @done(21-12-11 02:23)
|
✔ recipe detail screen @done(21-12-11 22:01)
|
||||||
☐ recipe list screen
|
|
||||||
☐ recipe detail screen
|
|
||||||
☐ add functionality to create a meal from a recipe
|
☐ add functionality to create a meal from a recipe
|
||||||
Log Entry:
|
|
||||||
✔ give option to specify quantity @done(21-12-11 01:28)
|
|
||||||
✔ give option to pick meal from a different log entry (that doesn't have an associated bolus yet and within certain time span) @done(21-12-11 02:22)
|
|
||||||
Event Types:
|
Event Types:
|
||||||
☐ add colors as indicators for log entries (and later graphs in reports)
|
☐ add colors as indicators for log entries (and later graphs in reports)
|
||||||
Settings:
|
Settings:
|
||||||
@ -31,12 +27,14 @@ FUTURE TASKS:
|
|||||||
General/Framework:
|
General/Framework:
|
||||||
☐ setup objectbox sync server
|
☐ setup objectbox sync server
|
||||||
☐ add explanations to each section
|
☐ add explanations to each section
|
||||||
✔ find a better way to work with multiple glucose measurements @done(21-12-11 02:23)
|
|
||||||
☐ evaluate if some fields should be readonly instead of completely hidden
|
☐ evaluate if some fields should be readonly instead of completely hidden
|
||||||
☐ alternate languages
|
☐ alternate languages
|
||||||
☐ log hba1c
|
☐ log hba1c
|
||||||
Reports:
|
Reports:
|
||||||
☐ evaluate what type of reports there should be
|
☐ evaluate what type of reports there should be
|
||||||
|
☐ meal tweaking
|
||||||
|
☐ bolus tweaking
|
||||||
|
☐ daily graph (showing glucose curve, events, boli and meals)
|
||||||
Log Overview:
|
Log Overview:
|
||||||
☐ add pagination
|
☐ add pagination
|
||||||
☐ add filters
|
☐ add filters
|
||||||
@ -50,6 +48,11 @@ FUTURE TASKS:
|
|||||||
☐ option to switch theme
|
☐ option to switch theme
|
||||||
|
|
||||||
Archive:
|
Archive:
|
||||||
|
✔ add model for recipe @done(21-12-11 02:23) @project(MAIN TASKS.Recipe)
|
||||||
|
✔ add model for ingredient (relation betweeen recipe and meal) @done(21-12-11 02:23) @project(MAIN TASKS.Recipe)
|
||||||
|
✔ give option to specify quantity @done(21-12-11 01:28) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ give option to pick meal from a different log entry (that doesn't have an associated bolus yet and within certain time span) @done(21-12-11 02:22) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ find a better way to work with multiple glucose measurements @done(21-12-11 02:23) @project(FUTURE TASKS.General/Framework)
|
||||||
✔ make components rounder/nicer/closer to new material style @done(21-12-10 04:10) @project(MAIN TASKS.Layout)
|
✔ make components rounder/nicer/closer to new material style @done(21-12-10 04:10) @project(MAIN TASKS.Layout)
|
||||||
✔ make sure 'null' isn't shown in text fields @done(21-12-10 04:23) @project(MAIN TASKS.General/Framework)
|
✔ make sure 'null' isn't shown in text fields @done(21-12-10 04:23) @project(MAIN TASKS.General/Framework)
|
||||||
✔ hide details like accuracies etc when picking meals @done(21-12-10 06:12) @project(MAIN TASKS.General/Framework)
|
✔ hide details like accuracies etc when picking meals @done(21-12-10 06:12) @project(MAIN TASKS.General/Framework)
|
||||||
|
@ -19,6 +19,8 @@ import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
|||||||
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_list.dart';
|
import 'package:diameter/screens/meal/meal_source_list.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_detail.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_list.dart';
|
||||||
import 'package:diameter/settings.dart';
|
import 'package:diameter/settings.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/screens/accuracy_list.dart';
|
import 'package:diameter/screens/accuracy_list.dart';
|
||||||
@ -61,6 +63,8 @@ Future<void> main() async {
|
|||||||
Routes.accuracy: (context) => const AccuracyDetailScreen(),
|
Routes.accuracy: (context) => const AccuracyDetailScreen(),
|
||||||
Routes.meals: (context) => const MealListScreen(),
|
Routes.meals: (context) => const MealListScreen(),
|
||||||
Routes.meal: (context) => const MealDetailScreen(),
|
Routes.meal: (context) => const MealDetailScreen(),
|
||||||
|
Routes.recipes: (context) => const RecipeListScreen(),
|
||||||
|
Routes.recipe: (context) => const RecipeDetailScreen(),
|
||||||
Routes.mealCategories: (context) => const MealCategoryListScreen(),
|
Routes.mealCategories: (context) => const MealCategoryListScreen(),
|
||||||
Routes.mealCategory: (context) => const MealCategoryDetailScreen(),
|
Routes.mealCategory: (context) => const MealCategoryDetailScreen(),
|
||||||
Routes.mealPortionTypes: (context) =>
|
Routes.mealPortionTypes: (context) =>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/models/meal.dart';
|
import 'package:diameter/models/meal.dart';
|
||||||
import 'package:diameter/models/recipe.dart';
|
import 'package:diameter/models/recipe.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
import 'package:diameter/objectbox.g.dart' show Ingredient_, Recipe_;
|
import 'package:diameter/objectbox.g.dart' show Ingredient_, Recipe_;
|
||||||
|
|
||||||
@ -25,9 +26,54 @@ class Ingredient {
|
|||||||
required this.amount,
|
required this.amount,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
|
static Ingredient? get(int id) => box.get(id);
|
||||||
|
static void put(Ingredient ingredient) => box.put(ingredient);
|
||||||
|
static void putMany(List<Ingredient> ingredients) => box.putMany(ingredients);
|
||||||
|
|
||||||
static List<Ingredient> getAllForRecipe(int id) {
|
static List<Ingredient> getAllForRecipe(int id) {
|
||||||
QueryBuilder<Ingredient> builder = box.query(Ingredient_.deleted.equals(false));
|
QueryBuilder<Ingredient> builder =
|
||||||
|
box.query(Ingredient_.deleted.equals(false));
|
||||||
builder.link(Ingredient_.recipe, Recipe_.id.equals(id));
|
builder.link(Ingredient_.recipe, Recipe_.id.equals(id));
|
||||||
return builder.build().find();
|
return builder.build().find();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
static double? getCarbsRatioForRecipe(int id) {
|
||||||
|
double carbsSum = 0.0;
|
||||||
|
double totalWeight = 0.0;
|
||||||
|
|
||||||
|
List<Ingredient> ingredients = getAllForRecipe(id);
|
||||||
|
|
||||||
|
for (Ingredient ingredient in ingredients) {
|
||||||
|
if ((ingredient.ingredient.target?.carbsRatio ?? 0) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
totalWeight += ingredient.amount;
|
||||||
|
carbsSum +=
|
||||||
|
Utils.calculateCarbs(ingredient.ingredient.target!.carbsRatio!, ingredient.amount);
|
||||||
|
}
|
||||||
|
return totalWeight > 0
|
||||||
|
? Utils.calculateCarbsRatio(carbsSum, totalWeight)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double? getTotalWeightForRecipe(int id) {
|
||||||
|
double totalWeight = 0.0;
|
||||||
|
|
||||||
|
List<Ingredient> ingredients = getAllForRecipe(id);
|
||||||
|
|
||||||
|
for (Ingredient ingredient in ingredients) {
|
||||||
|
if (ingredient.ingredient.target?.carbsRatio == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
totalWeight += ingredient.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return ingredient.target?.value ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:diameter/models/ingredient.dart';
|
||||||
import 'package:diameter/models/meal.dart';
|
import 'package:diameter/models/meal.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
import 'package:diameter/objectbox.g.dart' show Recipe_;
|
import 'package:diameter/objectbox.g.dart' show Recipe_;
|
||||||
|
|
||||||
@ -12,38 +14,43 @@ class Recipe {
|
|||||||
int id;
|
int id;
|
||||||
bool deleted;
|
bool deleted;
|
||||||
String name;
|
String name;
|
||||||
double? carbsRatio;
|
double? servings;
|
||||||
double? portionSize;
|
|
||||||
double? carbsPerPortion;
|
|
||||||
int? delayedBolusDuration;
|
|
||||||
double? delayedBolusPercentage;
|
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
// relations
|
// relations
|
||||||
final portion = ToOne<Meal>();
|
final portion = ToOne<Meal>();
|
||||||
|
|
||||||
// constructor
|
// constructor
|
||||||
Recipe({
|
Recipe({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
this.deleted = false,
|
this.deleted = false,
|
||||||
this.name = '',
|
this.name = '',
|
||||||
this.carbsRatio,
|
this.servings,
|
||||||
this.portionSize,
|
|
||||||
this.carbsPerPortion,
|
|
||||||
this.delayedBolusDuration,
|
|
||||||
this.delayedBolusPercentage,
|
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
static Recipe? get(int id) => box.get(id);
|
static Recipe? get(int id) => box.get(id);
|
||||||
static void put(Recipe recipe) => box.put(recipe);
|
static void put(Recipe recipe) => box.put(recipe);
|
||||||
|
|
||||||
static List<Recipe> getAll() {
|
static List<Recipe> getAll() {
|
||||||
QueryBuilder<Recipe> builder = box.query(Recipe_.deleted.equals(false))..order(Recipe_.name);
|
QueryBuilder<Recipe> builder = box.query(Recipe_.deleted.equals(false))
|
||||||
|
..order(Recipe_.name);
|
||||||
return builder.build().find();
|
return builder.build().find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double? getCarbsPerPortion(int id) {
|
||||||
|
final servings = Recipe.get(id)?.servings;
|
||||||
|
final totalWeight = Ingredient.getTotalWeightForRecipe(id);
|
||||||
|
final carbsRatio = Ingredient.getCarbsRatioForRecipe(id);
|
||||||
|
|
||||||
|
if (servings != null && totalWeight != null && carbsRatio != null) {
|
||||||
|
final portionSize = totalWeight / servings;
|
||||||
|
return Utils.calculateCarbs(carbsRatio, portionSize);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static void remove(int id) {
|
static void remove(int id) {
|
||||||
final item = box.get(id);
|
final item = box.get(id);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
|
@ -21,6 +21,8 @@ import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
|||||||
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_list.dart';
|
import 'package:diameter/screens/meal/meal_source_list.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_detail.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_list.dart';
|
||||||
import 'package:diameter/settings.dart';
|
import 'package:diameter/settings.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -45,6 +47,10 @@ class Routes {
|
|||||||
static const List<String> logEventTypeRoutes = [logEventType, logEventTypes];
|
static const List<String> logEventTypeRoutes = [logEventType, logEventTypes];
|
||||||
static const String events = LogEventListScreen.routeName;
|
static const String events = LogEventListScreen.routeName;
|
||||||
|
|
||||||
|
static const String recipe = RecipeDetailScreen.routeName;
|
||||||
|
static const String recipes = RecipeListScreen.routeName;
|
||||||
|
static const List<String> recipeRoutes = [recipe, recipes];
|
||||||
|
|
||||||
static const String meal = MealDetailScreen.routeName;
|
static const String meal = MealDetailScreen.routeName;
|
||||||
static const String meals = MealListScreen.routeName;
|
static const String meals = MealListScreen.routeName;
|
||||||
static const List<String> mealRoutes = [meal, meals];
|
static const List<String> mealRoutes = [meal, meals];
|
||||||
@ -107,9 +113,17 @@ class _NavigationState extends State<Navigation> {
|
|||||||
},
|
},
|
||||||
selected: widget.currentLocation == Routes.events,
|
selected: widget.currentLocation == Routes.events,
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Recipes'),
|
||||||
|
leading: const Icon(Icons.local_dining),
|
||||||
|
onTap: () {
|
||||||
|
selectDestination(Routes.recipes);
|
||||||
|
},
|
||||||
|
selected: Routes.recipeRoutes.contains(widget.currentLocation),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Meals'),
|
title: const Text('Meals'),
|
||||||
leading: const Icon(Icons.restaurant),
|
leading: const Icon(Icons.dinner_dining),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
selectDestination(Routes.meals);
|
selectDestination(Routes.meals);
|
||||||
},
|
},
|
||||||
|
@ -943,7 +943,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "18:6497942314956341514",
|
"id": "18:6497942314956341514",
|
||||||
"lastPropertyId": "10:4370359747396560337",
|
"lastPropertyId": "11:8488657312300528492",
|
||||||
"name": "Recipe",
|
"name": "Recipe",
|
||||||
"flags": 2,
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
@ -963,31 +963,6 @@
|
|||||||
"name": "name",
|
"name": "name",
|
||||||
"type": 9
|
"type": 9
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "4:241621230513128588",
|
|
||||||
"name": "carbsRatio",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5:4678123663117222609",
|
|
||||||
"name": "portionSize",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "6:780211923138281722",
|
|
||||||
"name": "carbsPerPortion",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7:763575433624979013",
|
|
||||||
"name": "delayedBolusDuration",
|
|
||||||
"type": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "8:1225271130099322691",
|
|
||||||
"name": "delayedBolusPercentage",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "9:8593446427752839266",
|
"id": "9:8593446427752839266",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
@ -1000,6 +975,11 @@
|
|||||||
"flags": 520,
|
"flags": 520,
|
||||||
"indexId": "29:5110151182694376118",
|
"indexId": "29:5110151182694376118",
|
||||||
"relationTarget": "Meal"
|
"relationTarget": "Meal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "11:8488657312300528492",
|
||||||
|
"name": "servings",
|
||||||
|
"type": 8
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
@ -1085,7 +1065,12 @@
|
|||||||
3282706593658092097,
|
3282706593658092097,
|
||||||
596980591281311896,
|
596980591281311896,
|
||||||
3633551763915044903,
|
3633551763915044903,
|
||||||
2215708755581938580
|
2215708755581938580,
|
||||||
|
241621230513128588,
|
||||||
|
4678123663117222609,
|
||||||
|
780211923138281722,
|
||||||
|
763575433624979013,
|
||||||
|
1225271130099322691
|
||||||
],
|
],
|
||||||
"retiredRelationUids": [],
|
"retiredRelationUids": [],
|
||||||
"version": 1
|
"version": 1
|
||||||
|
@ -930,7 +930,7 @@ final _entities = <ModelEntity>[
|
|||||||
ModelEntity(
|
ModelEntity(
|
||||||
id: const IdUid(18, 6497942314956341514),
|
id: const IdUid(18, 6497942314956341514),
|
||||||
name: 'Recipe',
|
name: 'Recipe',
|
||||||
lastPropertyId: const IdUid(10, 4370359747396560337),
|
lastPropertyId: const IdUid(11, 8488657312300528492),
|
||||||
flags: 2,
|
flags: 2,
|
||||||
properties: <ModelProperty>[
|
properties: <ModelProperty>[
|
||||||
ModelProperty(
|
ModelProperty(
|
||||||
@ -948,31 +948,6 @@ final _entities = <ModelEntity>[
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
type: 9,
|
type: 9,
|
||||||
flags: 0),
|
flags: 0),
|
||||||
ModelProperty(
|
|
||||||
id: const IdUid(4, 241621230513128588),
|
|
||||||
name: 'carbsRatio',
|
|
||||||
type: 8,
|
|
||||||
flags: 0),
|
|
||||||
ModelProperty(
|
|
||||||
id: const IdUid(5, 4678123663117222609),
|
|
||||||
name: 'portionSize',
|
|
||||||
type: 8,
|
|
||||||
flags: 0),
|
|
||||||
ModelProperty(
|
|
||||||
id: const IdUid(6, 780211923138281722),
|
|
||||||
name: 'carbsPerPortion',
|
|
||||||
type: 8,
|
|
||||||
flags: 0),
|
|
||||||
ModelProperty(
|
|
||||||
id: const IdUid(7, 763575433624979013),
|
|
||||||
name: 'delayedBolusDuration',
|
|
||||||
type: 6,
|
|
||||||
flags: 0),
|
|
||||||
ModelProperty(
|
|
||||||
id: const IdUid(8, 1225271130099322691),
|
|
||||||
name: 'delayedBolusPercentage',
|
|
||||||
type: 8,
|
|
||||||
flags: 0),
|
|
||||||
ModelProperty(
|
ModelProperty(
|
||||||
id: const IdUid(9, 8593446427752839266),
|
id: const IdUid(9, 8593446427752839266),
|
||||||
name: 'notes',
|
name: 'notes',
|
||||||
@ -984,7 +959,12 @@ final _entities = <ModelEntity>[
|
|||||||
type: 11,
|
type: 11,
|
||||||
flags: 520,
|
flags: 520,
|
||||||
indexId: const IdUid(29, 5110151182694376118),
|
indexId: const IdUid(29, 5110151182694376118),
|
||||||
relationTarget: 'Meal')
|
relationTarget: 'Meal'),
|
||||||
|
ModelProperty(
|
||||||
|
id: const IdUid(11, 8488657312300528492),
|
||||||
|
name: 'servings',
|
||||||
|
type: 8,
|
||||||
|
flags: 0)
|
||||||
],
|
],
|
||||||
relations: <ModelRelation>[],
|
relations: <ModelRelation>[],
|
||||||
backlinks: <ModelBacklink>[]),
|
backlinks: <ModelBacklink>[]),
|
||||||
@ -1080,7 +1060,12 @@ ModelDefinition getObjectBoxModel() {
|
|||||||
3282706593658092097,
|
3282706593658092097,
|
||||||
596980591281311896,
|
596980591281311896,
|
||||||
3633551763915044903,
|
3633551763915044903,
|
||||||
2215708755581938580
|
2215708755581938580,
|
||||||
|
241621230513128588,
|
||||||
|
4678123663117222609,
|
||||||
|
780211923138281722,
|
||||||
|
763575433624979013,
|
||||||
|
1225271130099322691
|
||||||
],
|
],
|
||||||
retiredRelationUids: const [],
|
retiredRelationUids: const [],
|
||||||
modelVersion: 5,
|
modelVersion: 5,
|
||||||
@ -1922,17 +1907,13 @@ ModelDefinition getObjectBoxModel() {
|
|||||||
final nameOffset = fbb.writeString(object.name);
|
final nameOffset = fbb.writeString(object.name);
|
||||||
final notesOffset =
|
final notesOffset =
|
||||||
object.notes == null ? null : fbb.writeString(object.notes!);
|
object.notes == null ? null : fbb.writeString(object.notes!);
|
||||||
fbb.startTable(11);
|
fbb.startTable(12);
|
||||||
fbb.addInt64(0, object.id);
|
fbb.addInt64(0, object.id);
|
||||||
fbb.addBool(1, object.deleted);
|
fbb.addBool(1, object.deleted);
|
||||||
fbb.addOffset(2, nameOffset);
|
fbb.addOffset(2, nameOffset);
|
||||||
fbb.addFloat64(3, object.carbsRatio);
|
|
||||||
fbb.addFloat64(4, object.portionSize);
|
|
||||||
fbb.addFloat64(5, object.carbsPerPortion);
|
|
||||||
fbb.addInt64(6, object.delayedBolusDuration);
|
|
||||||
fbb.addFloat64(7, object.delayedBolusPercentage);
|
|
||||||
fbb.addOffset(8, notesOffset);
|
fbb.addOffset(8, notesOffset);
|
||||||
fbb.addInt64(9, object.portion.targetId);
|
fbb.addInt64(9, object.portion.targetId);
|
||||||
|
fbb.addFloat64(10, object.servings);
|
||||||
fbb.finish(fbb.endTable());
|
fbb.finish(fbb.endTable());
|
||||||
return object.id;
|
return object.id;
|
||||||
},
|
},
|
||||||
@ -1946,16 +1927,8 @@ ModelDefinition getObjectBoxModel() {
|
|||||||
const fb.BoolReader().vTableGet(buffer, rootOffset, 6, false),
|
const fb.BoolReader().vTableGet(buffer, rootOffset, 6, false),
|
||||||
name:
|
name:
|
||||||
const fb.StringReader().vTableGet(buffer, rootOffset, 8, ''),
|
const fb.StringReader().vTableGet(buffer, rootOffset, 8, ''),
|
||||||
carbsRatio: const fb.Float64Reader()
|
servings: const fb.Float64Reader()
|
||||||
.vTableGetNullable(buffer, rootOffset, 10),
|
.vTableGetNullable(buffer, rootOffset, 24),
|
||||||
portionSize: const fb.Float64Reader()
|
|
||||||
.vTableGetNullable(buffer, rootOffset, 12),
|
|
||||||
carbsPerPortion: const fb.Float64Reader()
|
|
||||||
.vTableGetNullable(buffer, rootOffset, 14),
|
|
||||||
delayedBolusDuration: const fb.Int64Reader()
|
|
||||||
.vTableGetNullable(buffer, rootOffset, 16),
|
|
||||||
delayedBolusPercentage: const fb.Float64Reader()
|
|
||||||
.vTableGetNullable(buffer, rootOffset, 18),
|
|
||||||
notes: const fb.StringReader()
|
notes: const fb.StringReader()
|
||||||
.vTableGetNullable(buffer, rootOffset, 20));
|
.vTableGetNullable(buffer, rootOffset, 20));
|
||||||
object.portion.targetId =
|
object.portion.targetId =
|
||||||
@ -2609,32 +2582,16 @@ class Recipe_ {
|
|||||||
/// see [Recipe.name]
|
/// see [Recipe.name]
|
||||||
static final name = QueryStringProperty<Recipe>(_entities[16].properties[2]);
|
static final name = QueryStringProperty<Recipe>(_entities[16].properties[2]);
|
||||||
|
|
||||||
/// see [Recipe.carbsRatio]
|
|
||||||
static final carbsRatio =
|
|
||||||
QueryDoubleProperty<Recipe>(_entities[16].properties[3]);
|
|
||||||
|
|
||||||
/// see [Recipe.portionSize]
|
|
||||||
static final portionSize =
|
|
||||||
QueryDoubleProperty<Recipe>(_entities[16].properties[4]);
|
|
||||||
|
|
||||||
/// see [Recipe.carbsPerPortion]
|
|
||||||
static final carbsPerPortion =
|
|
||||||
QueryDoubleProperty<Recipe>(_entities[16].properties[5]);
|
|
||||||
|
|
||||||
/// see [Recipe.delayedBolusDuration]
|
|
||||||
static final delayedBolusDuration =
|
|
||||||
QueryIntegerProperty<Recipe>(_entities[16].properties[6]);
|
|
||||||
|
|
||||||
/// see [Recipe.delayedBolusPercentage]
|
|
||||||
static final delayedBolusPercentage =
|
|
||||||
QueryDoubleProperty<Recipe>(_entities[16].properties[7]);
|
|
||||||
|
|
||||||
/// see [Recipe.notes]
|
/// see [Recipe.notes]
|
||||||
static final notes = QueryStringProperty<Recipe>(_entities[16].properties[8]);
|
static final notes = QueryStringProperty<Recipe>(_entities[16].properties[3]);
|
||||||
|
|
||||||
/// see [Recipe.portion]
|
/// see [Recipe.portion]
|
||||||
static final portion =
|
static final portion =
|
||||||
QueryRelationToOne<Recipe, Meal>(_entities[16].properties[9]);
|
QueryRelationToOne<Recipe, Meal>(_entities[16].properties[4]);
|
||||||
|
|
||||||
|
/// see [Recipe.servings]
|
||||||
|
static final servings =
|
||||||
|
QueryDoubleProperty<Recipe>(_entities[16].properties[5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [Ingredient] entity fields to define ObjectBox queries.
|
/// [Ingredient] entity fields to define ObjectBox queries.
|
||||||
|
@ -211,7 +211,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
isNew: _isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
onDiscard: (context) => Navigator.pushReplacementNamed(context, '/log'),
|
onDiscard: (context) => Navigator.pop(context),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
@ -392,7 +392,7 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
|||||||
controller: _amountController,
|
controller: _amountController,
|
||||||
label: 'Amount',
|
label: 'Amount',
|
||||||
suffix: _mealPortionType?.value,
|
suffix: _mealPortionType?.value,
|
||||||
min: 1,
|
min: 0,
|
||||||
onChanged: updateAmount,
|
onChanged: updateAmount,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
@ -158,8 +158,8 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
|
|||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
event.title ?? event.eventType.target?.value ?? '',
|
(event.title ?? event.eventType.target?.value ?? '').toUpperCase(),
|
||||||
// style: Theme.of(context).textTheme.subtitle2,
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
328
lib/screens/recipe/recipe_detail.dart
Normal file
328
lib/screens/recipe/recipe_detail.dart
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
import 'package:diameter/components/detail.dart';
|
||||||
|
import 'package:diameter/components/dialogs.dart';
|
||||||
|
import 'package:diameter/components/dropdown.dart';
|
||||||
|
import 'package:diameter/components/forms.dart';
|
||||||
|
import 'package:diameter/models/ingredient.dart';
|
||||||
|
import 'package:diameter/models/meal.dart';
|
||||||
|
import 'package:diameter/models/recipe.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecipeDetailScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/recipe';
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const RecipeDetailScreen({Key? key, this.id = 0}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RecipeDetailScreenState createState() => _RecipeDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeDetailScreenState extends State<RecipeDetailScreen> {
|
||||||
|
Recipe? _recipe;
|
||||||
|
List<Ingredient> _ingredients = [];
|
||||||
|
|
||||||
|
bool _isNew = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
final GlobalKey<FormState> _recipeForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
final _nameController = TextEditingController(text: '');
|
||||||
|
final _servingsController = TextEditingController(text: '');
|
||||||
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
final List<TextEditingController> _ingredientControllers = [];
|
||||||
|
final List<TextEditingController> _ingredientAmountControllers = [];
|
||||||
|
|
||||||
|
List<Meal> _meals = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
|
_meals = Meal.getAll();
|
||||||
|
|
||||||
|
if (_recipe != null) {
|
||||||
|
_nameController.text = _recipe!.name;
|
||||||
|
_servingsController.text = (_recipe!.servings ?? '').toString();
|
||||||
|
_notesController.text = _recipe!.notes ?? '';
|
||||||
|
|
||||||
|
if (_ingredients.isNotEmpty) {
|
||||||
|
for (Ingredient ingredient in _ingredients) {
|
||||||
|
_ingredientControllers.add(
|
||||||
|
TextEditingController(text: ingredient.ingredient.target?.value));
|
||||||
|
_ingredientAmountControllers
|
||||||
|
.add(TextEditingController(text: ingredient.amount.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_recipe = Recipe.get(widget.id);
|
||||||
|
_ingredients = Ingredient.getAllForRecipe(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _recipe == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddIngredient() {
|
||||||
|
final newIngredient = Ingredient(amount: 0);
|
||||||
|
setState(() {
|
||||||
|
newIngredient.recipe.target = _recipe;
|
||||||
|
_ingredients.add(newIngredient);
|
||||||
|
_ingredientControllers.add(TextEditingController(text: ''));
|
||||||
|
_ingredientAmountControllers.add(TextEditingController(text: ''));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSaveAction({bool close = false}) async {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
if (_recipeForm.currentState!.validate()) {
|
||||||
|
Recipe recipe = Recipe(
|
||||||
|
id: widget.id,
|
||||||
|
name: _nameController.text,
|
||||||
|
servings: double.tryParse(_servingsController.text),
|
||||||
|
notes: _notesController.text,
|
||||||
|
);
|
||||||
|
Recipe.put(recipe);
|
||||||
|
List<Ingredient> ingredients = _ingredients.map((ingredient) {
|
||||||
|
if (ingredient.id != 0 &&
|
||||||
|
(!ingredient.ingredient.hasValue || ingredient.amount == 0)) {
|
||||||
|
ingredient.deleted = true;
|
||||||
|
}
|
||||||
|
return ingredient;
|
||||||
|
}).toList();
|
||||||
|
ingredients.retainWhere((ingredient) {
|
||||||
|
return ingredient.id != 0 ||
|
||||||
|
(ingredient.amount > 0 && ingredient.ingredient.hasValue);
|
||||||
|
});
|
||||||
|
Ingredient.putMany(ingredients);
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
Navigator.pop(context, ['${_isNew ? 'New' : ''} Recipe Saved', recipe]);
|
||||||
|
} else {
|
||||||
|
if (_isNew) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => RecipeDetailScreen(id: recipe.id),
|
||||||
|
),
|
||||||
|
).then((result) => Navigator.pop(context, result));
|
||||||
|
} else {
|
||||||
|
reload(message: 'Recipe saved');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCancelAction() {
|
||||||
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
((_isNew &&
|
||||||
|
(_nameController.text != '' ||
|
||||||
|
_servingsController.text != '' ||
|
||||||
|
_notesController.text != '')) ||
|
||||||
|
(!_isNew &&
|
||||||
|
(_nameController.text != _recipe!.name ||
|
||||||
|
_servingsController.text !=
|
||||||
|
(_recipe!.servings ?? '').toString() ||
|
||||||
|
_notesController.text != (_recipe!.notes ?? ''))))) {
|
||||||
|
Dialogs.showCancelConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
isNew: _isNew,
|
||||||
|
onSave: handleSaveAction,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isNew ? 'New Recipe' : _recipe!.name),
|
||||||
|
),
|
||||||
|
drawer: const Navigation(currentLocation: RecipeDetailScreen.routeName),
|
||||||
|
body: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
FormWrapper(
|
||||||
|
formState: _recipeForm,
|
||||||
|
fields: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
NumberFormField(
|
||||||
|
controller: _servingsController,
|
||||||
|
label: 'Servings',
|
||||||
|
suffix: ' portions',
|
||||||
|
min: 0,
|
||||||
|
onChanged: (value) {
|
||||||
|
if ((value ?? 0) >= 0) {
|
||||||
|
setState(() {
|
||||||
|
_servingsController.text = (value ?? 0).toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onAddIngredient,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'INGREDIENTS',
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onAddIngredient,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
!_isNew && _ingredients.isNotEmpty
|
||||||
|
? ListBody(
|
||||||
|
children: _ingredients.map((item) {
|
||||||
|
final ingredient = item.ingredient.target;
|
||||||
|
final index = _ingredients.indexOf(item);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10.0, vertical: 5.0),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<Meal>(
|
||||||
|
controller: _ingredientControllers[index],
|
||||||
|
selectedItem: ingredient,
|
||||||
|
label: 'Meal Category',
|
||||||
|
items: _meals,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_ingredients[index]
|
||||||
|
.ingredient
|
||||||
|
.target = value;
|
||||||
|
_ingredientControllers[index].text =
|
||||||
|
value?.value ?? '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
ingredient == null
|
||||||
|
? const MealDetailScreen()
|
||||||
|
: MealDetailScreen(
|
||||||
|
id: ingredient.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
_ingredients[index].ingredient.target =
|
||||||
|
result?[1];
|
||||||
|
_ingredientControllers[index].text =
|
||||||
|
result?[1].value ?? '';
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(ingredient == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
controller:
|
||||||
|
_ingredientAmountControllers[index],
|
||||||
|
label: 'Amount',
|
||||||
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
|
min: 0,
|
||||||
|
onChanged: (value) {
|
||||||
|
if ((value ?? 0) >= 0) {
|
||||||
|
setState(() {
|
||||||
|
_ingredients[index].amount = value ?? 0;
|
||||||
|
_ingredientAmountControllers[index]
|
||||||
|
.text = (value ?? 0).toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(_isNew
|
||||||
|
? 'Save the Recipe in order to add ingredients!'
|
||||||
|
: 'You have not added any Ingredients yet!'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: _isSaving ? null : handleSaveAction,
|
||||||
|
onMiddleAction: _isSaving ? null : () => handleSaveAction(close: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
200
lib/screens/recipe/recipe_list.dart
Normal file
200
lib/screens/recipe/recipe_list.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import 'package:diameter/components/dialogs.dart';
|
||||||
|
import 'package:diameter/models/ingredient.dart';
|
||||||
|
import 'package:diameter/models/recipe.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecipeListScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/recipes';
|
||||||
|
|
||||||
|
const RecipeListScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RecipeListScreenState createState() => _RecipeListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeListScreenState extends State<RecipeListScreen> {
|
||||||
|
List<Recipe> _recipes = [];
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
setState(() {
|
||||||
|
_recipes = Recipe.getAll();
|
||||||
|
});
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDelete(Recipe recipe) {
|
||||||
|
Recipe.remove(recipe.id);
|
||||||
|
reload(message: 'Recipe deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeleteAction(Recipe recipe) async {
|
||||||
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
|
Dialogs.showConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
onConfirm: () => onDelete(recipe),
|
||||||
|
message: 'Are you sure you want to delete this Recipe?',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onDelete(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Recipes'), actions: <Widget>[
|
||||||
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
|
]),
|
||||||
|
drawer: const Navigation(currentLocation: RecipeListScreen.routeName),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _recipes.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _recipes.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final recipe = _recipes[index];
|
||||||
|
final carbsRatio =
|
||||||
|
Ingredient.getCarbsRatioForRecipe(recipe.id);
|
||||||
|
final carbsPerPortion = Recipe.getCarbsPerPortion(recipe.id);
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
RecipeDetailScreen(id: recipe.id),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
recipe.name.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(recipe.notes ?? ''),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.center,
|
||||||
|
children:
|
||||||
|
((carbsRatio ?? 0) > 0)
|
||||||
|
? [
|
||||||
|
Text(carbsRatio!.toString()),
|
||||||
|
const Text('% carbs',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.center,
|
||||||
|
children:
|
||||||
|
(recipe.servings != null)
|
||||||
|
? [
|
||||||
|
Text(recipe.servings!
|
||||||
|
.toStringAsPrecision(3)),
|
||||||
|
const Text('servings',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.center,
|
||||||
|
children:
|
||||||
|
((carbsPerPortion ?? 0) > 0)
|
||||||
|
? [
|
||||||
|
Text(carbsPerPortion!
|
||||||
|
.toStringAsPrecision(3)),
|
||||||
|
Text(
|
||||||
|
'${Settings.nutritionMeasurementSuffix} carbs per serving',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => handleDeleteAction(recipe),
|
||||||
|
icon: const Icon(Icons.delete,
|
||||||
|
color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text('You have not created any Recipes yet!'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const RecipeDetailScreen(),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -21,11 +21,11 @@ class Utils {
|
|||||||
|
|
||||||
static double calculateCarbsRatio(
|
static double calculateCarbsRatio(
|
||||||
double carbsPerPortion, double portionSize) {
|
double carbsPerPortion, double portionSize) {
|
||||||
return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2);
|
return portionSize > 0 ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static double calculatePortionSize(
|
static double calculatePortionSize(
|
||||||
double carbsRatio, double carbsPerPortion) {
|
double carbsRatio, double carbsPerPortion) {
|
||||||
return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2);
|
return carbsRatio > 0 ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2) : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user