recipe list and detail screens
This commit is contained in:
parent
5e60ea09ce
commit
6b4f588d5d
19
TODO
19
TODO
@ -1,5 +1,6 @@
|
||||
MAIN TASKS:
|
||||
General/Framework:
|
||||
☐ create app icon
|
||||
☐ 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)
|
||||
☐ account for deleted/disabled elements in dropdowns
|
||||
@ -8,14 +9,9 @@ MAIN TASKS:
|
||||
☐ implement component for durations
|
||||
☐ change placement of delete and floating button because its very easy to accidentally hit delete
|
||||
Recipe:
|
||||
✔ add model for recipe @done(21-12-11 02:23)
|
||||
✔ add model for ingredient (relation betweeen recipe and meal) @done(21-12-11 02:23)
|
||||
☐ recipe list screen
|
||||
☐ recipe detail screen
|
||||
✔ recipe list screen @done(21-12-11 22:01)
|
||||
✔ recipe detail screen @done(21-12-11 22:01)
|
||||
☐ 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:
|
||||
☐ add colors as indicators for log entries (and later graphs in reports)
|
||||
Settings:
|
||||
@ -31,12 +27,14 @@ FUTURE TASKS:
|
||||
General/Framework:
|
||||
☐ setup objectbox sync server
|
||||
☐ 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
|
||||
☐ alternate languages
|
||||
☐ log hba1c
|
||||
Reports:
|
||||
☐ evaluate what type of reports there should be
|
||||
☐ meal tweaking
|
||||
☐ bolus tweaking
|
||||
☐ daily graph (showing glucose curve, events, boli and meals)
|
||||
Log Overview:
|
||||
☐ add pagination
|
||||
☐ add filters
|
||||
@ -50,6 +48,11 @@ FUTURE TASKS:
|
||||
☐ option to switch theme
|
||||
|
||||
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 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)
|
||||
|
@ -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_source_detail.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:flutter/material.dart';
|
||||
import 'package:diameter/screens/accuracy_list.dart';
|
||||
@ -61,6 +63,8 @@ Future<void> main() async {
|
||||
Routes.accuracy: (context) => const AccuracyDetailScreen(),
|
||||
Routes.meals: (context) => const MealListScreen(),
|
||||
Routes.meal: (context) => const MealDetailScreen(),
|
||||
Routes.recipes: (context) => const RecipeListScreen(),
|
||||
Routes.recipe: (context) => const RecipeDetailScreen(),
|
||||
Routes.mealCategories: (context) => const MealCategoryListScreen(),
|
||||
Routes.mealCategory: (context) => const MealCategoryDetailScreen(),
|
||||
Routes.mealPortionTypes: (context) =>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:diameter/main.dart';
|
||||
import 'package:diameter/models/meal.dart';
|
||||
import 'package:diameter/models/recipe.dart';
|
||||
import 'package:diameter/utils/utils.dart';
|
||||
import 'package:objectbox/objectbox.dart';
|
||||
import 'package:diameter/objectbox.g.dart' show Ingredient_, Recipe_;
|
||||
|
||||
@ -25,9 +26,54 @@ class Ingredient {
|
||||
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) {
|
||||
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));
|
||||
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/models/ingredient.dart';
|
||||
import 'package:diameter/models/meal.dart';
|
||||
import 'package:diameter/utils/utils.dart';
|
||||
import 'package:objectbox/objectbox.dart';
|
||||
import 'package:diameter/objectbox.g.dart' show Recipe_;
|
||||
|
||||
@ -12,38 +14,43 @@ class Recipe {
|
||||
int id;
|
||||
bool deleted;
|
||||
String name;
|
||||
double? carbsRatio;
|
||||
double? portionSize;
|
||||
double? carbsPerPortion;
|
||||
int? delayedBolusDuration;
|
||||
double? delayedBolusPercentage;
|
||||
double? servings;
|
||||
String? notes;
|
||||
|
||||
// relations
|
||||
final portion = ToOne<Meal>();
|
||||
|
||||
|
||||
// constructor
|
||||
Recipe({
|
||||
this.id = 0,
|
||||
this.deleted = false,
|
||||
this.name = '',
|
||||
this.carbsRatio,
|
||||
this.portionSize,
|
||||
this.carbsPerPortion,
|
||||
this.delayedBolusDuration,
|
||||
this.delayedBolusPercentage,
|
||||
this.servings,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
// methods
|
||||
static Recipe? get(int id) => box.get(id);
|
||||
static void put(Recipe recipe) => box.put(recipe);
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
final item = box.get(id);
|
||||
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_source_detail.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:flutter/material.dart';
|
||||
|
||||
@ -45,6 +47,10 @@ class Routes {
|
||||
static const List<String> logEventTypeRoutes = [logEventType, logEventTypes];
|
||||
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 meals = MealListScreen.routeName;
|
||||
static const List<String> mealRoutes = [meal, meals];
|
||||
@ -107,9 +113,17 @@ class _NavigationState extends State<Navigation> {
|
||||
},
|
||||
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(
|
||||
title: const Text('Meals'),
|
||||
leading: const Icon(Icons.restaurant),
|
||||
leading: const Icon(Icons.dinner_dining),
|
||||
onTap: () {
|
||||
selectDestination(Routes.meals);
|
||||
},
|
||||
|
@ -943,7 +943,7 @@
|
||||
},
|
||||
{
|
||||
"id": "18:6497942314956341514",
|
||||
"lastPropertyId": "10:4370359747396560337",
|
||||
"lastPropertyId": "11:8488657312300528492",
|
||||
"name": "Recipe",
|
||||
"flags": 2,
|
||||
"properties": [
|
||||
@ -963,31 +963,6 @@
|
||||
"name": "name",
|
||||
"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",
|
||||
"name": "notes",
|
||||
@ -1000,6 +975,11 @@
|
||||
"flags": 520,
|
||||
"indexId": "29:5110151182694376118",
|
||||
"relationTarget": "Meal"
|
||||
},
|
||||
{
|
||||
"id": "11:8488657312300528492",
|
||||
"name": "servings",
|
||||
"type": 8
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
@ -1085,7 +1065,12 @@
|
||||
3282706593658092097,
|
||||
596980591281311896,
|
||||
3633551763915044903,
|
||||
2215708755581938580
|
||||
2215708755581938580,
|
||||
241621230513128588,
|
||||
4678123663117222609,
|
||||
780211923138281722,
|
||||
763575433624979013,
|
||||
1225271130099322691
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
|
@ -930,7 +930,7 @@ final _entities = <ModelEntity>[
|
||||
ModelEntity(
|
||||
id: const IdUid(18, 6497942314956341514),
|
||||
name: 'Recipe',
|
||||
lastPropertyId: const IdUid(10, 4370359747396560337),
|
||||
lastPropertyId: const IdUid(11, 8488657312300528492),
|
||||
flags: 2,
|
||||
properties: <ModelProperty>[
|
||||
ModelProperty(
|
||||
@ -948,31 +948,6 @@ final _entities = <ModelEntity>[
|
||||
name: 'name',
|
||||
type: 9,
|
||||
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(
|
||||
id: const IdUid(9, 8593446427752839266),
|
||||
name: 'notes',
|
||||
@ -984,7 +959,12 @@ final _entities = <ModelEntity>[
|
||||
type: 11,
|
||||
flags: 520,
|
||||
indexId: const IdUid(29, 5110151182694376118),
|
||||
relationTarget: 'Meal')
|
||||
relationTarget: 'Meal'),
|
||||
ModelProperty(
|
||||
id: const IdUid(11, 8488657312300528492),
|
||||
name: 'servings',
|
||||
type: 8,
|
||||
flags: 0)
|
||||
],
|
||||
relations: <ModelRelation>[],
|
||||
backlinks: <ModelBacklink>[]),
|
||||
@ -1080,7 +1060,12 @@ ModelDefinition getObjectBoxModel() {
|
||||
3282706593658092097,
|
||||
596980591281311896,
|
||||
3633551763915044903,
|
||||
2215708755581938580
|
||||
2215708755581938580,
|
||||
241621230513128588,
|
||||
4678123663117222609,
|
||||
780211923138281722,
|
||||
763575433624979013,
|
||||
1225271130099322691
|
||||
],
|
||||
retiredRelationUids: const [],
|
||||
modelVersion: 5,
|
||||
@ -1922,17 +1907,13 @@ ModelDefinition getObjectBoxModel() {
|
||||
final nameOffset = fbb.writeString(object.name);
|
||||
final notesOffset =
|
||||
object.notes == null ? null : fbb.writeString(object.notes!);
|
||||
fbb.startTable(11);
|
||||
fbb.startTable(12);
|
||||
fbb.addInt64(0, object.id);
|
||||
fbb.addBool(1, object.deleted);
|
||||
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.addInt64(9, object.portion.targetId);
|
||||
fbb.addFloat64(10, object.servings);
|
||||
fbb.finish(fbb.endTable());
|
||||
return object.id;
|
||||
},
|
||||
@ -1946,16 +1927,8 @@ ModelDefinition getObjectBoxModel() {
|
||||
const fb.BoolReader().vTableGet(buffer, rootOffset, 6, false),
|
||||
name:
|
||||
const fb.StringReader().vTableGet(buffer, rootOffset, 8, ''),
|
||||
carbsRatio: const fb.Float64Reader()
|
||||
.vTableGetNullable(buffer, rootOffset, 10),
|
||||
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),
|
||||
servings: const fb.Float64Reader()
|
||||
.vTableGetNullable(buffer, rootOffset, 24),
|
||||
notes: const fb.StringReader()
|
||||
.vTableGetNullable(buffer, rootOffset, 20));
|
||||
object.portion.targetId =
|
||||
@ -2609,32 +2582,16 @@ class Recipe_ {
|
||||
/// see [Recipe.name]
|
||||
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]
|
||||
static final notes = QueryStringProperty<Recipe>(_entities[16].properties[8]);
|
||||
static final notes = QueryStringProperty<Recipe>(_entities[16].properties[3]);
|
||||
|
||||
/// see [Recipe.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.
|
||||
|
@ -211,7 +211,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
||||
context: context,
|
||||
isNew: _isNew,
|
||||
onSave: handleSaveAction,
|
||||
onDiscard: (context) => Navigator.pushReplacementNamed(context, '/log'),
|
||||
onDiscard: (context) => Navigator.pop(context),
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
|
@ -392,7 +392,7 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
||||
controller: _amountController,
|
||||
label: 'Amount',
|
||||
suffix: _mealPortionType?.value,
|
||||
min: 1,
|
||||
min: 0,
|
||||
onChanged: updateAmount,
|
||||
),
|
||||
Row(
|
||||
|
@ -158,8 +158,8 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
|
||||
const SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: Text(
|
||||
event.title ?? event.eventType.target?.value ?? '',
|
||||
// style: Theme.of(context).textTheme.subtitle2,
|
||||
(event.title ?? event.eventType.target?.value ?? '').toUpperCase(),
|
||||
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(
|
||||
double carbsPerPortion, double portionSize) {
|
||||
return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2);
|
||||
return portionSize > 0 ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2) : 0;
|
||||
}
|
||||
|
||||
static double calculatePortionSize(
|
||||
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