import 'package:diameter/components/detail.dart'; import 'package:diameter/components/forms/boolean_form_field.dart'; import 'package:diameter/components/forms/number_form_field.dart'; import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/accuracy.dart'; import 'package:diameter/models/log_meal.dart'; import 'package:diameter/models/meal.dart'; import 'package:diameter/models/meal_category.dart'; import 'package:diameter/models/meal_portion_type.dart'; import 'package:diameter/models/meal_source.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/screens/accuracy_detail.dart'; import 'package:diameter/screens/meal/meal_category_detail.dart'; import 'package:diameter/screens/meal/meal_detail.dart'; import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; import 'package:diameter/screens/meal/meal_source_detail.dart'; import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; class LogMealDetailScreen extends StatefulWidget { static const String routeName = '/log-meal'; final int logEntryId; final int id; const LogMealDetailScreen({Key? key, this.logEntryId = 0, this.id = 0}) : super(key: key); @override _LogMealDetailScreenState createState() => _LogMealDetailScreenState(); } class _LogMealDetailScreenState extends State { LogMeal? _logMeal; bool _isNew = true; bool _isSaving = false; bool _isExpanded = false; bool _setManually = false; final GlobalKey _logMealForm = GlobalKey(); final ScrollController _scrollController = ScrollController(); double _amount = 1; final _valueController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: ''); final _portionSizeController = TextEditingController(text: ''); final _totalCarbsController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); Meal? _meal; MealSource? _mealSource; MealCategory? _mealCategory; MealPortionType? _mealPortionType; Accuracy? _portionSizeAccuracy; Accuracy? _carbsRatioAccuracy; final _mealController = TextEditingController(text: ''); final _mealSourceController = TextEditingController(text: ''); final _mealCategoryController = TextEditingController(text: ''); final _mealPortionTypeController = TextEditingController(text: ''); final _portionSizeAccuracyController = TextEditingController(text: ''); final _carbsRatioAccuracyController = TextEditingController(text: ''); final _amountController = TextEditingController(text: '1'); List _meals = []; List _mealCategories = []; List _mealPortionTypes = []; List _mealSources = []; List _portionSizeAccuracies = []; List _carbsRatioAccuracies = []; @override void initState() { super.initState(); reload(); _portionSizeAccuracies = Accuracy.getAllForPortionSize(); _carbsRatioAccuracies = Accuracy.getAllForCarbsRatio(); _meals = Meal.getAll(); _mealCategories = MealCategory.getAll(); _mealPortionTypes = MealPortionType.getAll(); _mealSources = MealSource.getAll(); if (widget.id != 0) { _valueController.text = _logMeal!.value; _carbsRatioController.text = (_logMeal!.carbsRatio ?? '').toString(); _portionSizeController.text = (_logMeal!.portionSize ?? '').toString(); _totalCarbsController.text = (_logMeal!.totalCarbs ?? '').toString(); _amountController.text = (_logMeal!.amount).toString(); _notesController.text = _logMeal!.notes ?? ''; _meal = _logMeal!.meal.target; _mealController.text = (_meal ?? '').toString(); _mealSource = _logMeal!.mealSource.target; _mealSourceController.text = (_mealSource ?? '').toString(); _mealCategory = _logMeal!.mealCategory.target; _mealCategoryController.text = (_mealCategory ?? '').toString(); _mealPortionType = _logMeal!.mealPortionType.target; _mealPortionTypeController.text = (_mealPortionType ?? '').toString(); _portionSizeAccuracy = _logMeal!.portionSizeAccuracy.target; _portionSizeAccuracyController.text = (_portionSizeAccuracy ?? '').toString(); _carbsRatioAccuracy = _logMeal!.carbsRatioAccuracy.target; _carbsRatioAccuracyController.text = (_carbsRatioAccuracy ?? '').toString(); } } @override void dispose() { _scrollController.dispose(); _valueController.dispose(); _carbsRatioController.dispose(); _portionSizeController.dispose(); _totalCarbsController.dispose(); _notesController.dispose(); _mealController.dispose(); _mealSourceController.dispose(); _mealCategoryController.dispose(); _mealPortionTypeController.dispose(); _portionSizeAccuracyController.dispose(); _carbsRatioAccuracyController.dispose(); _amountController.dispose(); super.dispose(); } void reload({String? message}) { if (widget.id != 0) { setState(() { _logMeal = LogMeal.get(widget.id); }); } _isNew = _logMeal == null; setState(() { if (message != null) { var snackBar = SnackBar( content: Text(message), duration: const Duration(seconds: 2), ); ScaffoldMessenger.of(context) ..removeCurrentSnackBar() ..showSnackBar(snackBar); } }); } void updateCarbsRatioAccuracy(Accuracy? value) { setState(() { _carbsRatioAccuracy = value; _carbsRatioAccuracyController.text = (_carbsRatioAccuracy ?? '').toString(); }); } void updatePortionSizeAccuracy(Accuracy? value) { setState(() { _portionSizeAccuracy = value; _portionSizeAccuracyController.text = (_portionSizeAccuracy ?? '').toString(); }); } void updateMealCategory(MealCategory? value) { setState(() { _mealCategory = value; _mealCategoryController.text = (_mealCategory ?? '').toString(); }); } void updateMealPortionType(MealPortionType? value) { setState(() { _mealPortionType = value; _mealPortionTypeController.text = (_mealPortionType ?? '').toString(); }); } void updateMealSource(MealSource? value) { setState(() { _mealSource = value; _mealSourceController.text = (_mealSource ?? '').toString(); }); } Future onSelectMeal(Meal? meal) async { setState(() { _meal = meal; _mealController.text = (_meal ?? '').toString(); _valueController.text = _mealController.text; _carbsRatioController.text = (meal?.carbsRatio ?? '').toString(); _amountController.text = '1'; _portionSizeController.text = (meal?.portionSize ?? '').toString(); _totalCarbsController.text = (meal?.carbsPerPortion ?? '').toString(); }); updateMealSource(meal?.mealSource.target); updateMealCategory(meal?.mealCategory.target); updateMealPortionType(meal?.mealPortionType.target); updatePortionSizeAccuracy(meal?.portionSizeAccuracy.target); updateCarbsRatioAccuracy(meal?.carbsRatioAccuracy.target); } void handleSaveAction() async { setState(() { _isSaving = true; }); if (_logMealForm.currentState!.validate()) { LogMeal logMeal = LogMeal( id: widget.id, value: _valueController.text, carbsRatio: double.tryParse(_carbsRatioController.text), portionSize: double.tryParse(_portionSizeController.text), totalCarbs: double.tryParse(_totalCarbsController.text), amount: double.parse(_amountController.text), ); logMeal.logEntry.targetId = widget.logEntryId; logMeal.meal.target = _meal; logMeal.mealSource.target = _mealSource; logMeal.mealCategory.target = _mealCategory; logMeal.mealPortionType.target = _mealPortionType; logMeal.portionSizeAccuracy.target = _portionSizeAccuracy; logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy; LogMeal.put(logMeal); Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Saved', logMeal]); } setState(() { _isSaving = false; }); } void handleCancelAction() { if (Settings.get().showConfirmationDialogOnCancel && ((_isNew && (_valueController.text != '' || _meal != null || _mealSource != null || _mealCategory != null || _mealPortionType != null || double.tryParse(_amountController.text) != 1 || double.tryParse(_carbsRatioController.text) != null || double.tryParse(_portionSizeController.text) != null || double.tryParse(_totalCarbsController.text) != null || _carbsRatioAccuracy != null || _portionSizeAccuracy != null || _notesController.text != '')) || (!_isNew && (_valueController.text != _logMeal!.value || _meal != _logMeal!.meal.target || _mealSource != _logMeal!.mealSource.target || _mealCategory != _logMeal!.mealCategory.target || _mealPortionType != _logMeal!.mealPortionType.target || double.tryParse(_amountController.text) != _logMeal!.amount || double.tryParse(_carbsRatioController.text) != _logMeal!.carbsRatio || double.tryParse(_portionSizeController.text) != _logMeal!.portionSize || double.tryParse(_totalCarbsController.text) != _logMeal!.totalCarbs || _carbsRatioAccuracy != _logMeal!.carbsRatioAccuracy.target || _portionSizeAccuracy != _logMeal!.portionSizeAccuracy.target || _notesController.text != (_logMeal!.notes ?? ''))))) { DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, ); } else { Navigator.pop(context); } } void updateAmount(double? newAmount) { if (newAmount != null) { setState(() { _amountController.text = Utils.getFractionDigitsLength(newAmount) == 0 ? newAmount.toInt().toString() : newAmount.toString(); }); double? portionSize; double? basePortionSize; if (_portionSizeController.text != '') { portionSize = double.tryParse(_portionSizeController.text); } if (portionSize != null && portionSize != 0) { basePortionSize = portionSize / _amount; } else if (_meal != null) { basePortionSize = _meal!.portionSize; } if (basePortionSize != null) { setState(() { portionSize = basePortionSize! * newAmount; _portionSizeController.text = Utils.toStringMatchingTemplateFractionPrecision( portionSize!, Settings.nutritionSteps); }); calculateThirdMeasurementOfPortionCarbsRelation( portionSizeUpdate: portionSize); } setState(() { _amount = newAmount; }); } } void calculateThirdMeasurementOfPortionCarbsRelation( {double? carbsRatioUpdate, double? portionSizeUpdate, double? totalCarbsUpdate}) { if (!_setManually) { double? carbsRatio = carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text); double? portionSize = portionSizeUpdate ?? double.tryParse(_portionSizeController.text); double? totalCarbs = totalCarbsUpdate ?? double.tryParse(_totalCarbsController.text); int toCalculate = 0; const calcCarbsRatio = 1; const calcTotalCarbs = 2; const calcPortionSize = 3; if (carbsRatioUpdate != null) { if (portionSize != null && portionSize != 0) { toCalculate = calcTotalCarbs; } else if (totalCarbs != null && totalCarbs != 0) { toCalculate = calcPortionSize; } } else if (portionSizeUpdate != null) { if (carbsRatio != null && carbsRatio != 0) { toCalculate = calcTotalCarbs; } else if (totalCarbs != null && totalCarbs != 0) { toCalculate = calcCarbsRatio; } } else if (totalCarbsUpdate != null) { if (carbsRatio != null && carbsRatio != 0) { toCalculate = calcPortionSize; } else if (portionSize != null && portionSize != 0) { toCalculate = calcCarbsRatio; } } else { if (carbsRatio != null && carbsRatio != 0) { if (portionSize != null && portionSize != 0) { toCalculate = calcTotalCarbs; } else if (totalCarbs != null && totalCarbs != 0) { toCalculate = calcPortionSize; } } else if (portionSize != null && portionSize != 0 && totalCarbs != null && totalCarbs != 0) { toCalculate = calcCarbsRatio; } } setState(() { if (toCalculate == calcCarbsRatio) { _carbsRatioController.text = Utils.calculateCarbsRatio(totalCarbs!, portionSize!).toString(); } else if (toCalculate == calcTotalCarbs) { _totalCarbsController.text = Utils.toStringMatchingTemplateFractionPrecision( Utils.calculateCarbs(carbsRatio!, portionSize!, step: Settings.nutritionSteps), Settings.nutritionSteps); } else if (toCalculate == calcPortionSize) { _portionSizeController.text = Utils.toStringMatchingTemplateFractionPrecision( Utils.calculatePortionSize(carbsRatio!, totalCarbs!, step: Settings.nutritionSteps), Settings.nutritionSteps); } }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_isNew ? 'New Meal for Log Entry' : _logMeal!.value), ), drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName), body: Scrollbar( controller: _scrollController, child: SingleChildScrollView( controller: _scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FormWrapper( formState: _logMealForm, fields: [ Row( children: [ Expanded( child: AutoCompleteDropdownButton( controller: _mealController, selectedItem: _meal, label: 'Meal', items: _meals, onChanged: onSelectMeal, ), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => _meal == null ? const MealDetailScreen() : MealDetailScreen(id: _meal!.id), ), ).then((result) { onSelectMeal(result?[1]); reload(message: result?[0]); }); }, icon: Icon(_meal == null ? Icons.add : Icons.edit), ), ], ), TextFormField( controller: _valueController, decoration: const InputDecoration( labelText: 'Name', ), validator: (value) { if (value!.trim().isEmpty) { return 'Empty name'; } return null; }, ), Row( children: [ Expanded( flex: 10, child: NumberFormField( controller: _amountController, label: 'Amount', suffix: _mealPortionType?.value, onChanged: updateAmount, ), ), Expanded( child: TextButton( onPressed: () => updateAmount(0.5), child: Column( children: const [ Text( '1', style: TextStyle( decoration: TextDecoration.underline, decorationThickness: 2), ), Text('2'), ], ), ), ), Expanded( child: TextButton( onPressed: () => updateAmount(0.33), child: Column( children: const [ Text( '1', style: TextStyle( decoration: TextDecoration.underline, decorationThickness: 2), ), Text('3'), ], ), ), ), Expanded( child: TextButton( onPressed: () => updateAmount(0.67), child: Column( children: const [ Text( '2', style: TextStyle( decoration: TextDecoration.underline, decorationThickness: 2), ), Text('3'), ], ), ), ), ], ), Row( children: [ Expanded( child: NumberFormField( label: 'Portion size', suffix: Settings.nutritionMeasurementSuffix, controller: _portionSizeController, showSteppers: false, autoRoundToMultipleOfStep: true, step: Settings.nutritionSteps, onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); calculateThirdMeasurementOfPortionCarbsRelation( portionSizeUpdate: value); }, ), ), const SizedBox(width: 10), Expanded( child: NumberFormField( label: 'Carbs ratio', suffix: '%', controller: _carbsRatioController, showSteppers: false, onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); calculateThirdMeasurementOfPortionCarbsRelation( carbsRatioUpdate: value); }, ), ), const SizedBox(width: 10), Expanded( child: NumberFormField( label: 'Total carbs', suffix: Settings.nutritionMeasurementSuffix, controller: _totalCarbsController, showSteppers: false, autoRoundToMultipleOfStep: true, step: Settings.nutritionSteps, onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); calculateThirdMeasurementOfPortionCarbsRelation( totalCarbsUpdate: value); }, ), ), ], ), Expanded( child: BooleanFormField( value: _setManually, label: 'set carbs ratio manually', onChanged: (value) { setState(() { _setManually = value; calculateThirdMeasurementOfPortionCarbsRelation(); }); }, ), ), TextFormField( controller: _notesController, decoration: const InputDecoration( labelText: 'Notes', ), keyboardType: TextInputType.multiline, minLines: 2, maxLines: 5, ), const Divider(), GestureDetector( onTap: () => setState(() { _isExpanded = !_isExpanded; }), child: Row( mainAxisSize: MainAxisSize.max, children: [ Text( 'ADDITIONAL FIELDS', style: Theme.of(context).textTheme.subtitle2, ), const Spacer(), Icon(_isExpanded ? Icons.expand_less : Icons.expand_more), ], ), ), Column( children: _isExpanded ? [ Padding( padding: const EdgeInsets.symmetric(vertical: 5.0), child: Row( children: [ Expanded( child: AutoCompleteDropdownButton( controller: _mealSourceController, selectedItem: _mealSource, label: 'Meal Source', items: _mealSources, onChanged: updateMealSource, ), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => _mealSource == null ? const MealSourceDetailScreen() : MealSourceDetailScreen( id: _mealSource!.id), ), ).then((result) { updateMealSource(result?[1]); reload(message: result?[0]); }); }, icon: Icon(_mealSource == null ? Icons.add : Icons.edit), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 5.0), child: Row( children: [ Expanded( child: AutoCompleteDropdownButton< MealCategory>( controller: _mealCategoryController, selectedItem: _mealCategory, label: 'Meal Category', items: _mealCategories, onChanged: updateMealCategory, ), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => _mealCategory == null ? const MealCategoryDetailScreen() : MealCategoryDetailScreen( id: _mealCategory!.id), ), ).then((result) { updateMealCategory(result?[1]); reload(message: result?[0]); }); }, icon: Icon(_mealCategory == null ? Icons.add : Icons.edit), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 5.0), child: Row( children: [ Expanded( child: AutoCompleteDropdownButton< MealPortionType>( controller: _mealPortionTypeController, selectedItem: _mealPortionType, label: 'Meal Portion Type', items: _mealPortionTypes, onChanged: updateMealPortionType, ), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => _mealPortionType == null ? const MealPortionTypeDetailScreen() : MealPortionTypeDetailScreen( id: _mealPortionType!.id), ), ).then((result) { updateMealPortionType(result?[1]); reload(message: result?[0]); }); }, icon: Icon(_mealPortionType == null ? Icons.add : Icons.edit), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 5.0), child: Row( children: [ Expanded( child: AutoCompleteDropdownButton( controller: _portionSizeAccuracyController, selectedItem: _portionSizeAccuracy, label: 'Portion Size Accuracy', items: _portionSizeAccuracies, onChanged: updatePortionSizeAccuracy, ), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => _portionSizeAccuracy == null ? const AccuracyDetailScreen() : AccuracyDetailScreen( id: _portionSizeAccuracy! .id), ), ).then((result) { updatePortionSizeAccuracy(result?[1]); reload(message: result?[0]); }); }, icon: Icon(_portionSizeAccuracy == null ? Icons.add : Icons.edit), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 5.0), child: Row( children: [ Expanded( child: AutoCompleteDropdownButton( controller: _carbsRatioAccuracyController, selectedItem: _carbsRatioAccuracy, label: 'Carbs Ratio Accuracy', items: _carbsRatioAccuracies, onChanged: updateCarbsRatioAccuracy, ), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => _carbsRatioAccuracy == null ? const AccuracyDetailScreen() : AccuracyDetailScreen( id: _carbsRatioAccuracy! .id), ), ).then((result) { updateCarbsRatioAccuracy(result?[1]); reload(message: result?[0]); }); }, icon: Icon(_carbsRatioAccuracy == null ? Icons.add : Icons.edit), ), ], ), ), ] : [], ), ], ), ], ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, onAction: _isSaving ? null : handleSaveAction, ), ); } }