Refactor ingredient search widget

This commit is contained in:
Roland Geider
2021-11-03 19:41:05 +01:00
parent 76ce31287f
commit 504e4ffaba
8 changed files with 148 additions and 104 deletions

View File

@@ -242,6 +242,12 @@
"@logMeal": {},
"addIngredient": "Add ingredient",
"@addIngredient": {},
"logIngredient": "Save to nutrition diary",
"@logIngredient": {},
"searchIngredient": "Search ingredient",
"@searchIngredient": {
"description": "Label on ingredient search form"
},
"nutritionalPlan": "Nutritional plan",
"@nutritionalPlan": {},
"nutritionalDiary": "Nutritional diary",

View File

@@ -339,11 +339,11 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier {
notifyListeners();
}
// Log custom ingredient to nutrition diary
Future<void> logIngredentToDiary(MealItem mealItem, int planId) async {
/// Log custom ingredient to nutrition diary
Future<void> logIngredentToDiary(MealItem mealItem, int planId, [DateTime? dateTime]) async {
final plan = findById(planId);
mealItem.ingredientObj = await fetchIngredient(mealItem.ingredientId);
final Log log = Log.fromMealItem(mealItem, plan.id!, null);
final Log log = Log.fromMealItem(mealItem, plan.id!, null, dateTime);
final data = await post(log.toJson(), makeUrl(_nutritionDiaryPath));
log.id = data['id'];

View File

@@ -44,7 +44,19 @@ class NutritionalPlanScreen extends StatelessWidget {
return Scaffold(
//appBar: getAppBar(nutritionalPlan),
//drawer: AppDrawer(),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.history_edu),
onPressed: () {
Navigator.pushNamed(
context,
FormScreen.routeName,
arguments: FormScreenArguments(
AppLocalizations.of(context).logIngredient,
IngredientLogForm(_nutritionalPlan),
),
);
},
),
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(

View File

@@ -18,7 +18,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:provider/provider.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/helpers/consts.dart';
@@ -29,6 +28,7 @@ import 'package:wger/models/nutrition/meal_item.dart';
import 'package:wger/models/nutrition/nutritional_plan.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/screens/nutritional_plan_screen.dart';
import 'package:wger/widgets/nutrition/widgets.dart';
class MealForm extends StatelessWidget {
late final Meal _meal;
@@ -115,18 +115,18 @@ class MealForm extends StatelessWidget {
class MealItemForm extends StatelessWidget {
final Meal _meal;
late final MealItem _mealItem;
final List<MealItem> _listMealItems;
final _form = GlobalKey<FormState>();
final _ingredientIdController = TextEditingController();
final _ingredientController = TextEditingController();
final _amountController = TextEditingController();
MealItemForm(this._meal, this._listMealItems, [mealItem]) {
_mealItem = mealItem ?? MealItem.empty();
_mealItem.mealId = _meal.id!;
}
final _form = GlobalKey<FormState>();
final _ingredientController = TextEditingController();
final _amountController = TextEditingController();
@override
Widget build(BuildContext context) {
final String unit = AppLocalizations.of(context).g;
@@ -136,37 +136,7 @@ class MealItemForm extends StatelessWidget {
key: _form,
child: Column(
children: [
TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: _ingredientController,
decoration: InputDecoration(labelText: AppLocalizations.of(context).ingredient),
),
suggestionsCallback: (pattern) async {
return Provider.of<NutritionPlansProvider>(context, listen: false).searchIngredient(
pattern,
Localizations.localeOf(context).languageCode,
);
},
itemBuilder: (context, dynamic suggestion) {
return ListTile(
title: Text(suggestion['value']),
subtitle: Text(suggestion['data']['id'].toString()),
);
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (dynamic suggestion) {
_mealItem.ingredientId = suggestion['data']['id'];
_ingredientController.text = suggestion['value'];
},
validator: (value) {
if (value!.isEmpty) {
return AppLocalizations.of(context).selectIngredient;
}
return null;
},
),
IngredientTypeahead(_ingredientIdController, _ingredientController),
TextFormField(
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
controller: _amountController,
@@ -191,6 +161,7 @@ class MealItemForm extends StatelessWidget {
return;
}
_form.currentState!.save();
_mealItem.ingredientId = int.parse(_ingredientIdController.text);
try {
Provider.of<NutritionPlansProvider>(context, listen: false)
@@ -217,6 +188,8 @@ class MealItemForm extends StatelessWidget {
child: ListTile(
onTap: () {
_ingredientController.text = _listMealItems[index].ingredientObj.name;
_ingredientIdController.text =
_listMealItems[index].ingredientObj.id.toString();
_amountController.text = _listMealItems[index].amount.toStringAsFixed(0);
_mealItem.ingredientId = _listMealItems[index].ingredientId;
_mealItem.amount = _listMealItems[index].amount;
@@ -236,63 +209,32 @@ class MealItemForm extends StatelessWidget {
}
}
class IngredientForm extends StatelessWidget {
class IngredientLogForm extends StatelessWidget {
late MealItem _mealItem;
final int _planId;
IngredientForm(this._planId) {
_mealItem = MealItem.empty();
}
final NutritionalPlan _plan;
final _form = GlobalKey<FormState>();
final _ingredientController = TextEditingController();
final _ingredientIdController = TextEditingController();
final _amountController = TextEditingController();
final _dateController = TextEditingController();
IngredientLogForm(this._plan) {
_mealItem = MealItem.empty();
_dateController.text = toDate(DateTime.now())!;
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(20),
margin: const EdgeInsets.all(20),
child: Form(
key: _form,
child: Column(
children: [
TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: _ingredientController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).ingredient),
),
suggestionsCallback: (pattern) async {
return Provider.of<NutritionPlansProvider>(context,
listen: false)
.searchIngredient(
pattern,
Localizations.localeOf(context).languageCode,
);
},
itemBuilder: (context, dynamic suggestion) {
return ListTile(
title: Text(suggestion['value']),
subtitle: Text(suggestion['data']['id'].toString()),
);
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (dynamic suggestion) {
_mealItem.ingredientId = suggestion['data']['id'];
_ingredientController.text = suggestion['value'];
},
validator: (value) {
if (value!.isEmpty) {
return AppLocalizations.of(context).selectIngredient;
}
return null;
},
),
IngredientTypeahead(_ingredientIdController, _ingredientController),
TextFormField(
decoration: InputDecoration(
labelText: AppLocalizations.of(context).weight),
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
controller: _amountController,
keyboardType: TextInputType.number,
onFieldSubmitted: (_) {},
@@ -308,6 +250,31 @@ class IngredientForm extends StatelessWidget {
return null;
},
),
TextFormField(
readOnly: true, // Stop keyboard from appearing
decoration: InputDecoration(
labelText: AppLocalizations.of(context).date,
suffixIcon: const Icon(Icons.calendar_today_outlined),
),
enableInteractiveSelection: false,
controller: _dateController,
onTap: () async {
// Show Date Picker Here
final pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(DateTime.now().year - 10),
lastDate: DateTime.now(),
);
if (pickedDate != null) {
_dateController.text = toDate(pickedDate)!;
}
},
onSaved: (newValue) {
_dateController.text = newValue!;
},
),
ElevatedButton(
child: Text(AppLocalizations.of(context).save),
onPressed: () async {
@@ -315,10 +282,11 @@ class IngredientForm extends StatelessWidget {
return;
}
_form.currentState!.save();
_mealItem.ingredientId = int.parse(_ingredientIdController.text);
try {
Provider.of<NutritionPlansProvider>(context, listen: false)
.logIngredentToDiary(_mealItem, _planId);
Provider.of<NutritionPlansProvider>(context, listen: false).logIngredentToDiary(
_mealItem, _plan.id!, DateTime.parse(_dateController.text));
} on WgerHttpException catch (error) {
showHttpExceptionErrorDialog(error, context);
} catch (error) {

View File

@@ -67,22 +67,6 @@ class NutritionalPlanDetailWidget extends StatelessWidget {
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
child: Text(AppLocalizations.of(context).addIngredient),
onPressed: () {
Navigator.pushNamed(
context,
FormScreen.routeName,
arguments: FormScreenArguments(
AppLocalizations.of(context).addIngredient,
IngredientForm(_nutritionalPlan.id!),
),
);
},
),
),
Container(
padding: const EdgeInsets.all(15),
height: 220,

View File

@@ -0,0 +1,74 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (C) 2020, 2021 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* wger Workout Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:provider/provider.dart';
import 'package:wger/providers/nutrition.dart';
class IngredientTypeahead extends StatefulWidget {
final TextEditingController _ingredientController;
final TextEditingController _ingredientIdController;
IngredientTypeahead(this._ingredientIdController, this._ingredientController);
@override
_IngredientTypeaheadState createState() => _IngredientTypeaheadState();
}
class _IngredientTypeaheadState extends State<IngredientTypeahead> {
@override
Widget build(BuildContext context) {
return TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: widget._ingredientController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).searchIngredient,
suffixIcon: const Icon(Icons.search),
),
),
suggestionsCallback: (pattern) async {
return Provider.of<NutritionPlansProvider>(context, listen: false).searchIngredient(
pattern,
Localizations.localeOf(context).languageCode,
);
},
itemBuilder: (context, dynamic suggestion) {
return ListTile(
title: Text(suggestion['value']),
subtitle: Text(suggestion['data']['id'].toString()),
);
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (dynamic suggestion) {
widget._ingredientIdController.text = suggestion['data']['id'].toString();
widget._ingredientController.text = suggestion['value'];
},
validator: (value) {
if (value!.isEmpty) {
return AppLocalizations.of(context).selectIngredient;
}
return null;
},
);
}
}

View File

@@ -692,7 +692,7 @@ packages:
name: rive
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.28"
version: "0.7.33"
shared_preferences:
dependency: "direct main"
description:

View File

@@ -47,7 +47,7 @@ dependencies:
version: ^2.0.0
package_info: ^2.0.2
provider: ^5.0.0
rive: 0.7.28
rive: ^0.7.33
shared_preferences: ^2.0.7
table_calendar: ^3.0.2
url_launcher: ^6.0.10