Merge pull request #592 from wger-project/ingredient-form-ux

IngredientForm UX fixes
This commit is contained in:
Roland Geider
2024-05-29 10:48:33 +02:00
committed by GitHub
3 changed files with 80 additions and 26 deletions

View File

@@ -169,7 +169,6 @@ class IngredientFormState extends State<IngredientForm> {
final _timeController = TextEditingController(); // optional
final _mealItem = MealItem.empty();
bool validIngredientId = false;
@override
void initState() {
super.initState();
@@ -182,6 +181,26 @@ class IngredientFormState extends State<IngredientForm> {
MealItem get mealItem => _mealItem;
void selectIngredient(int id, String name, num? amount) {
setState(() {
_mealItem.ingredientId = id;
_ingredientController.text = name;
_ingredientIdController.text = id.toString();
if (amount != null) {
_amountController.text = amount.toStringAsFixed(0);
_mealItem.amount = amount;
}
});
}
// note: does not reset text search and amount inputs
void unSelectIngredient() {
setState(() {
_mealItem.ingredientId = 0;
_ingredientIdController.text = '';
});
}
@override
Widget build(BuildContext context) {
final String unit = AppLocalizations.of(context).g;
@@ -197,6 +216,8 @@ class IngredientFormState extends State<IngredientForm> {
_ingredientController,
barcode: widget.barcode,
test: widget.test,
selectIngredient: selectIngredient,
unSelectIngredient: unSelectIngredient,
),
Row(
children: [
@@ -287,7 +308,7 @@ class IngredientFormState extends State<IngredientForm> {
),
],
),
if (validIngredientId)
if (ingredientIdController.text.isNotEmpty && _amountController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
@@ -302,10 +323,9 @@ class IngredientFormState extends State<IngredientForm> {
builder: (BuildContext context, AsyncSnapshot<Ingredient> snapshot) {
if (snapshot.hasData) {
_mealItem.ingredient = snapshot.data!;
return ListTile(
leading: IngredientAvatar(ingredient: _mealItem.ingredient),
title:
Text(getShortNutritionValues(_mealItem.nutritionalValues, context)),
return MealItemTile(
ingredient: _mealItem.ingredient,
nutritionalValues: _mealItem.nutritionalValues,
);
} else if (snapshot.hasError) {
return Padding(
@@ -328,9 +348,7 @@ class IngredientFormState extends State<IngredientForm> {
),
),
ElevatedButton(
key: const Key(
SUBMIT_BUTTON_KEY_NAME), // needed? mealItemForm had it, but not ingredientlogform
key: const Key(SUBMIT_BUTTON_KEY_NAME),
child: Text(AppLocalizations.of(context).save),
onPressed: () async {
if (!_form.currentState!.validate()) {
@@ -365,15 +383,9 @@ class IngredientFormState extends State<IngredientForm> {
return Card(
child: ListTile(
onTap: () {
_ingredientController.text = widget.recent[index].ingredient.name;
_ingredientIdController.text =
widget.recent[index].ingredient.id.toString();
_amountController.text = widget.recent[index].amount.toStringAsFixed(0);
setState(() {
_mealItem.ingredientId = widget.recent[index].ingredientId;
_mealItem.amount = widget.recent[index].amount;
validIngredientId = true;
});
final ingredient = widget.recent[index].ingredient;
selectIngredient(
ingredient.id, ingredient.name, widget.recent[index].amount);
},
title: Text(
'${widget.recent[index].ingredient.name} (${widget.recent[index].amount.toStringAsFixed(0)}$unit)'),

View File

@@ -32,6 +32,7 @@ import 'package:wger/models/exercises/ingredient_api.dart';
import 'package:wger/models/nutrition/ingredient.dart';
import 'package:wger/models/nutrition/log.dart';
import 'package:wger/models/nutrition/nutritional_plan.dart';
import 'package:wger/models/nutrition/nutritional_values.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/widgets/core/core.dart';
import 'package:wger/widgets/nutrition/helpers.dart';
@@ -62,12 +63,17 @@ class IngredientTypeahead extends StatefulWidget {
final bool? test;
final bool showScanner;
final Function(int id, String name, num? amount) selectIngredient;
final Function() unSelectIngredient;
const IngredientTypeahead(
this._ingredientIdController,
this._ingredientController, {
this.showScanner = true,
this.test = false,
this.barcode = '',
required this.selectIngredient,
required this.unSelectIngredient,
});
@override
@@ -118,6 +124,10 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
}
return null;
},
onChanged: (value) {
// unselect to start a new search
widget.unSelectIngredient();
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
labelText: AppLocalizations.of(context).searchIngredient,
@@ -126,7 +136,8 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
);
},
suggestionsCallback: (pattern) {
if (pattern == '') {
// don't do search if user has already loaded a specific item
if (pattern == '' || widget._ingredientIdController.text.isNotEmpty) {
return null;
}
@@ -151,10 +162,7 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
child: child,
),
onSelected: (suggestion) {
//SuggestionsController.of(context).;
widget._ingredientIdController.text = suggestion.data.id.toString();
widget._ingredientController.text = suggestion.value;
widget.selectIngredient(suggestion.data.id, suggestion.value, null);
},
),
if (Localizations.localeOf(context).languageCode != LANGUAGE_SHORT_ENGLISH)
@@ -188,6 +196,7 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
}
final result = await Provider.of<NutritionPlansProvider>(context, listen: false)
.searchIngredientWithCode(barcode);
// TODO: show spinner...
if (!mounted) {
return;
}
@@ -198,14 +207,22 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
builder: (ctx) => AlertDialog(
key: const Key('found-dialog'),
title: Text(AppLocalizations.of(context).productFound),
content: Text(AppLocalizations.of(context).productFoundDescription(result.name)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(context).productFoundDescription(result.name)),
MealItemTile(
ingredient: result,
nutritionalValues: result.nutritionalValues,
),
],
),
actions: [
TextButton(
key: const Key('found-dialog-confirm-button'),
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
onPressed: () {
widget._ingredientController.text = result.name;
widget._ingredientIdController.text = result.id.toString();
widget.selectIngredient(result.id, result.name, null);
Navigator.of(ctx).pop();
},
),
@@ -357,3 +374,23 @@ class IngredientAvatar extends StatelessWidget {
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey));
}
}
class MealItemTile extends StatelessWidget {
final Ingredient ingredient;
final NutritionalValues nutritionalValues;
const MealItemTile({
super.key,
required this.ingredient,
required this.nutritionalValues,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: IngredientAvatar(ingredient: ingredient),
title: Text(getShortNutritionValues(nutritionalValues, context)),
// subtitle: Text(ingredient.id.toString()),
);
}
}

View File

@@ -305,6 +305,11 @@ void main() {
expect(formState.ingredientIdController.text, '1');
// once ID and weight are set, it'll fetchIngredient and show macros preview
when(mockNutrition.fetchIngredient(1)).thenAnswer((_) => Future.value(
Ingredient.fromJson(jsonDecode(fixture('nutrition/ingredient_59887_response.json'))),
));
await tester.enterText(find.byKey(const Key('field-weight')), '2');
await tester.pumpAndSettle();