diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 0af0ee58..32f9eed7 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -330,14 +330,24 @@ class NutritionPlansProvider with ChangeNotifier { } /// Searches for an ingredient - Future searchIngredient(String name, [String languageCode = 'en']) async { + Future searchIngredient( + String name, { + String languageCode = 'en', + bool searchEnglish = false, + }) async { if (name.length <= 1) { return []; } + final languages = [languageCode]; + if (searchEnglish && languageCode != LANGUAGE_SHORT_ENGLISH) { + languages.add(LANGUAGE_SHORT_ENGLISH); + } + // Send the request final response = await baseProvider.fetch( - baseProvider.makeUrl(_ingredientSearchPath, query: {'term': name, 'language': languageCode}), + baseProvider + .makeUrl(_ingredientSearchPath, query: {'term': name, 'language': languages.join(',')}), ); // Process the response @@ -355,7 +365,7 @@ class NutritionPlansProvider with ChangeNotifier { baseProvider.makeUrl(_ingredientPath, query: {'code': code}), ); - if (data["count"] == 0) { + if (data['count'] == 0) { return null; } else { return Ingredient.fromJson(data['results'][0]); diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 46a43678..10aee009 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -17,7 +17,6 @@ */ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:wger/exceptions/http_exception.dart'; @@ -148,9 +147,9 @@ class MealItemForm extends StatelessWidget { IngredientTypeahead( _ingredientIdController, _ingredientController, - true, - _barcode, - _test, + showScanner: true, + barcode: _barcode, + test: _test, ), TextFormField( key: const Key('field-weight'), @@ -255,9 +254,6 @@ class IngredientLogForm extends StatelessWidget { IngredientTypeahead( _ingredientIdController, _ingredientController, - true, - '', - false, ), TextFormField( decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), @@ -277,7 +273,8 @@ class IngredientLogForm extends StatelessWidget { }, ), TextFormField( - readOnly: true, // Stop keyboard from appearing + readOnly: true, + // Stop keyboard from appearing decoration: InputDecoration( labelText: AppLocalizations.of(context).date, suffixIcon: const Icon(Icons.calendar_today_outlined), diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index 3aed571b..6cbd4c9f 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -18,11 +18,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_barcode_scanner/flutter_barcode_scanner.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/helpers/consts.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/widgets/core/core.dart'; @@ -30,12 +30,18 @@ import 'package:wger/widgets/core/core.dart'; class IngredientTypeahead extends StatefulWidget { final TextEditingController _ingredientController; final TextEditingController _ingredientIdController; - late String? _barcode; - late final bool? _test; - final bool _showScanner; - IngredientTypeahead(this._ingredientIdController, this._ingredientController, this._showScanner, - [this._barcode, this._test]); + String? barcode = ''; + late final bool? test; + final bool showScanner; + + IngredientTypeahead( + this._ingredientIdController, + this._ingredientController, { + this.showScanner = true, + this.test = false, + this.barcode = '', + }); @override _IngredientTypeaheadState createState() => _IngredientTypeaheadState(); @@ -45,7 +51,12 @@ Future scanBarcode(BuildContext context) async { String barcode; try { barcode = await FlutterBarcodeScanner.scanBarcode( - '#ff6666', AppLocalizations.of(context).close, true, ScanMode.BARCODE); + '#ff6666', + AppLocalizations.of(context).close, + true, + ScanMode.BARCODE, + ); + if (barcode.compareTo('-1') == 0) { return ''; } @@ -57,116 +68,134 @@ Future scanBarcode(BuildContext context) async { } class _IngredientTypeaheadState extends State { + var _searchEnglish = true; + @override Widget build(BuildContext context) { - return TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: widget._ingredientController, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - labelText: AppLocalizations.of(context).searchIngredient, - suffixIcon: widget._showScanner ? scanButton() : null, + return Column( + children: [ + TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: widget._ingredientController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + labelText: AppLocalizations.of(context).searchIngredient, + suffixIcon: widget.showScanner ? scanButton() : null, + ), + ), + suggestionsCallback: (pattern) async { + return Provider.of(context, listen: false).searchIngredient( + pattern, + languageCode: Localizations.localeOf(context).languageCode, + searchEnglish: _searchEnglish, + ); + }, + itemBuilder: (context, dynamic suggestion) { + final url = context.read().baseProvider.auth.serverUrl; + return ListTile( + leading: suggestion['data']['image'] != null + ? CircleAvatar(backgroundImage: NetworkImage(url! + suggestion['data']['image'])) + : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)), + title: Text(suggestion['value']), + ); + }, + 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; + }, ), - ), - suggestionsCallback: (pattern) async { - return Provider.of(context, listen: false).searchIngredient( - pattern, - Localizations.localeOf(context).languageCode, - ); - }, - itemBuilder: (context, dynamic suggestion) { - final url = context.read().baseProvider.auth.serverUrl; - return ListTile( - leading: suggestion['data']['image'] != null - ? CircleAvatar(backgroundImage: NetworkImage(url! + suggestion['data']['image'])) - : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)), - title: Text(suggestion['value']), - ); - }, - 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; - }, + if (Localizations.localeOf(context).languageCode != LANGUAGE_SHORT_ENGLISH) + SwitchListTile( + title: Text(AppLocalizations.of(context).searchNamesInEnglish), + value: _searchEnglish, + onChanged: (_) { + setState(() { + _searchEnglish = !_searchEnglish; + }); + }, + dense: true, + ), + ], ); } Widget scanButton() { return IconButton( - key: const Key('scan-button'), - onPressed: () async { - try { - if (!widget._test!) { - widget._barcode = await scanBarcode(context); - } - - if (widget._barcode!.isNotEmpty) { - final result = await Provider.of(context, listen: false) - .searchIngredientWithCode(widget._barcode!); - - if (result != null) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - key: const Key('found-dialog'), - title: Text(AppLocalizations.of(context).productFound), - content: - Text(AppLocalizations.of(context).productFoundDescription(result.name)), - 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(); - Navigator.of(ctx).pop(); - }, - ), - TextButton( - key: const Key('found-dialog-close-button'), - child: Text(MaterialLocalizations.of(context).closeButtonLabel), - onPressed: () { - Navigator.of(ctx).pop(); - }, - ) - ], - ), - ); - } else { - //nothing is matching barcode - showDialog( - context: context, - builder: (ctx) => AlertDialog( - key: const Key('notFound-dialog'), - title: Text(AppLocalizations.of(context).productNotFound), - content: Text( - AppLocalizations.of(context).productNotFoundDescription(widget._barcode!), - ), - actions: [ - TextButton( - key: const Key('notFound-dialog-close-button'), - child: Text(MaterialLocalizations.of(context).closeButtonLabel), - onPressed: () { - Navigator.of(ctx).pop(); - }, - ) - ], - ), - ); - } - } - } catch (e) { - showErrorDialog(e, context); + key: const Key('scan-button'), + onPressed: () async { + try { + if (!widget.test!) { + widget.barcode = await scanBarcode(context); } - }, - icon: Image.asset('assets/images/barcode_scanner_icon.png')); + + if (widget.barcode!.isNotEmpty) { + final result = await Provider.of(context, listen: false) + .searchIngredientWithCode(widget.barcode!); + + if (result != null) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + key: const Key('found-dialog'), + title: Text(AppLocalizations.of(context).productFound), + content: Text(AppLocalizations.of(context).productFoundDescription(result.name)), + 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(); + Navigator.of(ctx).pop(); + }, + ), + TextButton( + key: const Key('found-dialog-close-button'), + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + onPressed: () { + Navigator.of(ctx).pop(); + }, + ) + ], + ), + ); + } else { + //nothing is matching barcode + showDialog( + context: context, + builder: (ctx) => AlertDialog( + key: const Key('notFound-dialog'), + title: Text(AppLocalizations.of(context).productNotFound), + content: Text( + AppLocalizations.of(context).productNotFoundDescription(widget.barcode!), + ), + actions: [ + TextButton( + key: const Key('notFound-dialog-close-button'), + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + onPressed: () { + Navigator.of(ctx).pop(); + }, + ) + ], + ), + ); + } + } + } catch (e) { + showErrorDialog(e, context); + } + }, + icon: Image.asset('assets/images/barcode_scanner_icon.png'), + ); } } diff --git a/test/nutrition/nutritional_meal_form_test.mocks.dart b/test/nutrition/nutritional_meal_form_test.mocks.dart index dfdd9086..cf57b4bc 100644 --- a/test/nutrition/nutritional_meal_form_test.mocks.dart +++ b/test/nutrition/nutritional_meal_form_test.mocks.dart @@ -319,16 +319,18 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i7.NutritionPlansP ) as _i8.Future); @override _i8.Future> searchIngredient( - String? name, [ + String? name, { String? languageCode = r'en', - ]) => + bool? searchEnglish = false, + }) => (super.noSuchMethod( Invocation.method( #searchIngredient, - [ - name, - languageCode, - ], + [name], + { + #languageCode: languageCode, + #searchEnglish: searchEnglish, + }, ), returnValue: _i8.Future>.value([]), ) as _i8.Future>); diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index d454abd3..4e265976 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -24,7 +24,7 @@ import '../other/base_provider_test.mocks.dart'; import 'nutritional_plan_form_test.mocks.dart'; void main() { - Ingredient ingr = Ingredient( + final ingredient = Ingredient( id: 1, code: '123456787', name: 'Water', @@ -40,14 +40,12 @@ void main() { ); late MockWgerBaseProvider mockWgerBaseProvider; - late NutritionPlansProvider mockNutritionWithClient; var mockNutrition = MockNutritionPlansProvider(); final client = MockClient(); setUp(() { mockWgerBaseProvider = MockWgerBaseProvider(); - mockNutritionWithClient = NutritionPlansProvider(mockWgerBaseProvider, []); }); var plan1 = NutritionalPlan.empty(); @@ -57,22 +55,25 @@ void main() { final Uri tUriEmptyCode = Uri.parse('https://localhost/api/v2/ingredient/?code=\"%20\"'); final Uri tUriBadCode = Uri.parse('https://localhost/api/v2/ingredient/?code=222'); - when(client.get(tUriRightCode, headers: anyNamed('headers'))).thenAnswer((_) => - Future.value(http.Response(fixture('nutrition/search_ingredient_right_code.json'), 200))); + when(client.get(tUriRightCode, headers: anyNamed('headers'))).thenAnswer( + (_) => Future.value(http.Response(fixture('nutrition/search_ingredient_right_code.json'), 200)), + ); - when(client.get(tUriEmptyCode, headers: anyNamed('headers'))).thenAnswer((_) => - Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200))); + when(client.get(tUriEmptyCode, headers: anyNamed('headers'))).thenAnswer( + (_) => Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200)), + ); - when(client.get(tUriBadCode, headers: anyNamed('headers'))).thenAnswer((_) => - Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200))); + when(client.get(tUriBadCode, headers: anyNamed('headers'))).thenAnswer( + (_) => Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200)), + ); setUp(() { plan1 = getNutritionalPlan(); meal1 = plan1.meals.first; - final MealItem mealItem = MealItem(ingredientId: ingr.id, amount: 2); + final MealItem mealItem = MealItem(ingredientId: ingredient.id, amount: 2); mockNutrition = MockNutritionPlansProvider(); - when(mockNutrition.searchIngredientWithCode('123')).thenAnswer((_) => Future.value(ingr)); + when(mockNutrition.searchIngredientWithCode('123')).thenAnswer((_) => Future.value(ingredient)); when(mockNutrition.searchIngredientWithCode('')).thenAnswer((_) => Future.value(null)); when(mockNutrition.searchIngredientWithCode('222')).thenAnswer((_) => Future.value(null)); when(mockNutrition.searchIngredient(any)).thenAnswer((_) => diff --git a/test/nutrition/nutritional_plan_form_test.mocks.dart b/test/nutrition/nutritional_plan_form_test.mocks.dart index fde0989a..802102d9 100644 --- a/test/nutrition/nutritional_plan_form_test.mocks.dart +++ b/test/nutrition/nutritional_plan_form_test.mocks.dart @@ -319,16 +319,18 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i7.NutritionPlansP ) as _i8.Future); @override _i8.Future> searchIngredient( - String? name, [ + String? name, { String? languageCode = r'en', - ]) => + bool? searchEnglish = false, + }) => (super.noSuchMethod( Invocation.method( #searchIngredient, - [ - name, - languageCode, - ], + [name], + { + #languageCode: languageCode, + #searchEnglish: searchEnglish, + }, ), returnValue: _i8.Future>.value([]), ) as _i8.Future>);