Add switch to search for ingredients in English as well

This commit is contained in:
Roland Geider
2023-03-20 17:14:12 +01:00
parent fa8dd7e46f
commit 984df49c41
6 changed files with 184 additions and 143 deletions

View File

@@ -330,14 +330,24 @@ class NutritionPlansProvider with ChangeNotifier {
}
/// Searches for an ingredient
Future<List> searchIngredient(String name, [String languageCode = 'en']) async {
Future<List> 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]);

View File

@@ -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),

View File

@@ -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<String> 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<String> scanBarcode(BuildContext context) async {
}
class _IngredientTypeaheadState extends State<IngredientTypeahead> {
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<NutritionPlansProvider>(context, listen: false).searchIngredient(
pattern,
languageCode: Localizations.localeOf(context).languageCode,
searchEnglish: _searchEnglish,
);
},
itemBuilder: (context, dynamic suggestion) {
final url = context.read<NutritionPlansProvider>().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<NutritionPlansProvider>(context, listen: false).searchIngredient(
pattern,
Localizations.localeOf(context).languageCode,
);
},
itemBuilder: (context, dynamic suggestion) {
final url = context.read<NutritionPlansProvider>().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<NutritionPlansProvider>(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<NutritionPlansProvider>(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'),
);
}
}

View File

@@ -319,16 +319,18 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i7.NutritionPlansP
) as _i8.Future<void>);
@override
_i8.Future<List<dynamic>> 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<List<dynamic>>.value(<dynamic>[]),
) as _i8.Future<List<dynamic>>);

View File

@@ -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((_) =>

View File

@@ -319,16 +319,18 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i7.NutritionPlansP
) as _i8.Future<void>);
@override
_i8.Future<List<dynamic>> 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<List<dynamic>>.value(<dynamic>[]),
) as _i8.Future<List<dynamic>>);