mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge pull request #592 from wger-project/ingredient-form-ux
IngredientForm UX fixes
This commit is contained in:
@@ -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)'),
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user