Skip to content

Commit 984df49

Browse files
committed
Add switch to search for ingredients in English as well
1 parent fa8dd7e commit 984df49

File tree

6 files changed

+179
-138
lines changed

6 files changed

+179
-138
lines changed

lib/providers/nutrition.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,24 @@ class NutritionPlansProvider with ChangeNotifier {
330330
}
331331

332332
/// Searches for an ingredient
333-
Future<List> searchIngredient(String name, [String languageCode = 'en']) async {
333+
Future<List> searchIngredient(
334+
String name, {
335+
String languageCode = 'en',
336+
bool searchEnglish = false,
337+
}) async {
334338
if (name.length <= 1) {
335339
return [];
336340
}
337341

342+
final languages = [languageCode];
343+
if (searchEnglish && languageCode != LANGUAGE_SHORT_ENGLISH) {
344+
languages.add(LANGUAGE_SHORT_ENGLISH);
345+
}
346+
338347
// Send the request
339348
final response = await baseProvider.fetch(
340-
baseProvider.makeUrl(_ingredientSearchPath, query: {'term': name, 'language': languageCode}),
349+
baseProvider
350+
.makeUrl(_ingredientSearchPath, query: {'term': name, 'language': languages.join(',')}),
341351
);
342352

343353
// Process the response
@@ -355,7 +365,7 @@ class NutritionPlansProvider with ChangeNotifier {
355365
baseProvider.makeUrl(_ingredientPath, query: {'code': code}),
356366
);
357367

358-
if (data["count"] == 0) {
368+
if (data['count'] == 0) {
359369
return null;
360370
} else {
361371
return Ingredient.fromJson(data['results'][0]);

lib/widgets/nutrition/forms.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818

1919
import 'package:flutter/material.dart';
20-
import 'package:flutter/services.dart';
2120
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2221
import 'package:provider/provider.dart';
2322
import 'package:wger/exceptions/http_exception.dart';
@@ -148,9 +147,9 @@ class MealItemForm extends StatelessWidget {
148147
IngredientTypeahead(
149148
_ingredientIdController,
150149
_ingredientController,
151-
true,
152-
_barcode,
153-
_test,
150+
showScanner: true,
151+
barcode: _barcode,
152+
test: _test,
154153
),
155154
TextFormField(
156155
key: const Key('field-weight'),
@@ -255,9 +254,6 @@ class IngredientLogForm extends StatelessWidget {
255254
IngredientTypeahead(
256255
_ingredientIdController,
257256
_ingredientController,
258-
true,
259-
'',
260-
false,
261257
),
262258
TextFormField(
263259
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
@@ -277,7 +273,8 @@ class IngredientLogForm extends StatelessWidget {
277273
},
278274
),
279275
TextFormField(
280-
readOnly: true, // Stop keyboard from appearing
276+
readOnly: true,
277+
// Stop keyboard from appearing
281278
decoration: InputDecoration(
282279
labelText: AppLocalizations.of(context).date,
283280
suffixIcon: const Icon(Icons.calendar_today_outlined),

lib/widgets/nutrition/widgets.dart

Lines changed: 133 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,30 @@
1818

1919
import 'package:flutter/material.dart';
2020
import 'package:flutter/services.dart';
21-
import 'package:flutter/widgets.dart';
2221
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
2322
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2423
import 'package:flutter_typeahead/flutter_typeahead.dart';
2524
import 'package:provider/provider.dart';
25+
import 'package:wger/helpers/consts.dart';
2626
import 'package:wger/helpers/ui.dart';
2727
import 'package:wger/providers/nutrition.dart';
2828
import 'package:wger/widgets/core/core.dart';
2929

3030
class IngredientTypeahead extends StatefulWidget {
3131
final TextEditingController _ingredientController;
3232
final TextEditingController _ingredientIdController;
33-
late String? _barcode;
34-
late final bool? _test;
35-
final bool _showScanner;
3633

37-
IngredientTypeahead(this._ingredientIdController, this._ingredientController, this._showScanner,
38-
[this._barcode, this._test]);
34+
String? barcode = '';
35+
late final bool? test;
36+
final bool showScanner;
37+
38+
IngredientTypeahead(
39+
this._ingredientIdController,
40+
this._ingredientController, {
41+
this.showScanner = true,
42+
this.test = false,
43+
this.barcode = '',
44+
});
3945

4046
@override
4147
_IngredientTypeaheadState createState() => _IngredientTypeaheadState();
@@ -45,7 +51,12 @@ Future<String> scanBarcode(BuildContext context) async {
4551
String barcode;
4652
try {
4753
barcode = await FlutterBarcodeScanner.scanBarcode(
48-
'#ff6666', AppLocalizations.of(context).close, true, ScanMode.BARCODE);
54+
'#ff6666',
55+
AppLocalizations.of(context).close,
56+
true,
57+
ScanMode.BARCODE,
58+
);
59+
4960
if (barcode.compareTo('-1') == 0) {
5061
return '';
5162
}
@@ -57,116 +68,134 @@ Future<String> scanBarcode(BuildContext context) async {
5768
}
5869

5970
class _IngredientTypeaheadState extends State<IngredientTypeahead> {
71+
var _searchEnglish = true;
72+
6073
@override
6174
Widget build(BuildContext context) {
62-
return TypeAheadFormField(
63-
textFieldConfiguration: TextFieldConfiguration(
64-
controller: widget._ingredientController,
65-
decoration: InputDecoration(
66-
prefixIcon: const Icon(Icons.search),
67-
labelText: AppLocalizations.of(context).searchIngredient,
68-
suffixIcon: widget._showScanner ? scanButton() : null,
75+
return Column(
76+
children: [
77+
TypeAheadFormField(
78+
textFieldConfiguration: TextFieldConfiguration(
79+
controller: widget._ingredientController,
80+
decoration: InputDecoration(
81+
prefixIcon: const Icon(Icons.search),
82+
labelText: AppLocalizations.of(context).searchIngredient,
83+
suffixIcon: widget.showScanner ? scanButton() : null,
84+
),
85+
),
86+
suggestionsCallback: (pattern) async {
87+
return Provider.of<NutritionPlansProvider>(context, listen: false).searchIngredient(
88+
pattern,
89+
languageCode: Localizations.localeOf(context).languageCode,
90+
searchEnglish: _searchEnglish,
91+
);
92+
},
93+
itemBuilder: (context, dynamic suggestion) {
94+
final url = context.read<NutritionPlansProvider>().baseProvider.auth.serverUrl;
95+
return ListTile(
96+
leading: suggestion['data']['image'] != null
97+
? CircleAvatar(backgroundImage: NetworkImage(url! + suggestion['data']['image']))
98+
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)),
99+
title: Text(suggestion['value']),
100+
);
101+
},
102+
transitionBuilder: (context, suggestionsBox, controller) {
103+
return suggestionsBox;
104+
},
105+
onSuggestionSelected: (dynamic suggestion) {
106+
widget._ingredientIdController.text = suggestion['data']['id'].toString();
107+
widget._ingredientController.text = suggestion['value'];
108+
},
109+
validator: (value) {
110+
if (value!.isEmpty) {
111+
return AppLocalizations.of(context).selectIngredient;
112+
}
113+
return null;
114+
},
69115
),
70-
),
71-
suggestionsCallback: (pattern) async {
72-
return Provider.of<NutritionPlansProvider>(context, listen: false).searchIngredient(
73-
pattern,
74-
Localizations.localeOf(context).languageCode,
75-
);
76-
},
77-
itemBuilder: (context, dynamic suggestion) {
78-
final url = context.read<NutritionPlansProvider>().baseProvider.auth.serverUrl;
79-
return ListTile(
80-
leading: suggestion['data']['image'] != null
81-
? CircleAvatar(backgroundImage: NetworkImage(url! + suggestion['data']['image']))
82-
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)),
83-
title: Text(suggestion['value']),
84-
);
85-
},
86-
transitionBuilder: (context, suggestionsBox, controller) {
87-
return suggestionsBox;
88-
},
89-
onSuggestionSelected: (dynamic suggestion) {
90-
widget._ingredientIdController.text = suggestion['data']['id'].toString();
91-
widget._ingredientController.text = suggestion['value'];
92-
},
93-
validator: (value) {
94-
if (value!.isEmpty) {
95-
return AppLocalizations.of(context).selectIngredient;
96-
}
97-
return null;
98-
},
116+
if (Localizations.localeOf(context).languageCode != LANGUAGE_SHORT_ENGLISH)
117+
SwitchListTile(
118+
title: Text(AppLocalizations.of(context).searchNamesInEnglish),
119+
value: _searchEnglish,
120+
onChanged: (_) {
121+
setState(() {
122+
_searchEnglish = !_searchEnglish;
123+
});
124+
},
125+
dense: true,
126+
),
127+
],
99128
);
100129
}
101130

102131
Widget scanButton() {
103132
return IconButton(
104-
key: const Key('scan-button'),
105-
onPressed: () async {
106-
try {
107-
if (!widget._test!) {
108-
widget._barcode = await scanBarcode(context);
109-
}
133+
key: const Key('scan-button'),
134+
onPressed: () async {
135+
try {
136+
if (!widget.test!) {
137+
widget.barcode = await scanBarcode(context);
138+
}
110139

111-
if (widget._barcode!.isNotEmpty) {
112-
final result = await Provider.of<NutritionPlansProvider>(context, listen: false)
113-
.searchIngredientWithCode(widget._barcode!);
140+
if (widget.barcode!.isNotEmpty) {
141+
final result = await Provider.of<NutritionPlansProvider>(context, listen: false)
142+
.searchIngredientWithCode(widget.barcode!);
114143

115-
if (result != null) {
116-
showDialog(
117-
context: context,
118-
builder: (ctx) => AlertDialog(
119-
key: const Key('found-dialog'),
120-
title: Text(AppLocalizations.of(context).productFound),
121-
content:
122-
Text(AppLocalizations.of(context).productFoundDescription(result.name)),
123-
actions: [
124-
TextButton(
125-
key: const Key('found-dialog-confirm-button'),
126-
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
127-
onPressed: () {
128-
widget._ingredientController.text = result.name;
129-
widget._ingredientIdController.text = result.id.toString();
130-
Navigator.of(ctx).pop();
131-
},
132-
),
133-
TextButton(
134-
key: const Key('found-dialog-close-button'),
135-
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
136-
onPressed: () {
137-
Navigator.of(ctx).pop();
138-
},
139-
)
140-
],
141-
),
142-
);
143-
} else {
144-
//nothing is matching barcode
145-
showDialog(
146-
context: context,
147-
builder: (ctx) => AlertDialog(
148-
key: const Key('notFound-dialog'),
149-
title: Text(AppLocalizations.of(context).productNotFound),
150-
content: Text(
151-
AppLocalizations.of(context).productNotFoundDescription(widget._barcode!),
144+
if (result != null) {
145+
showDialog(
146+
context: context,
147+
builder: (ctx) => AlertDialog(
148+
key: const Key('found-dialog'),
149+
title: Text(AppLocalizations.of(context).productFound),
150+
content: Text(AppLocalizations.of(context).productFoundDescription(result.name)),
151+
actions: [
152+
TextButton(
153+
key: const Key('found-dialog-confirm-button'),
154+
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
155+
onPressed: () {
156+
widget._ingredientController.text = result.name;
157+
widget._ingredientIdController.text = result.id.toString();
158+
Navigator.of(ctx).pop();
159+
},
152160
),
153-
actions: [
154-
TextButton(
155-
key: const Key('notFound-dialog-close-button'),
156-
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
157-
onPressed: () {
158-
Navigator.of(ctx).pop();
159-
},
160-
)
161-
],
161+
TextButton(
162+
key: const Key('found-dialog-close-button'),
163+
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
164+
onPressed: () {
165+
Navigator.of(ctx).pop();
166+
},
167+
)
168+
],
169+
),
170+
);
171+
} else {
172+
//nothing is matching barcode
173+
showDialog(
174+
context: context,
175+
builder: (ctx) => AlertDialog(
176+
key: const Key('notFound-dialog'),
177+
title: Text(AppLocalizations.of(context).productNotFound),
178+
content: Text(
179+
AppLocalizations.of(context).productNotFoundDescription(widget.barcode!),
162180
),
163-
);
164-
}
181+
actions: [
182+
TextButton(
183+
key: const Key('notFound-dialog-close-button'),
184+
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
185+
onPressed: () {
186+
Navigator.of(ctx).pop();
187+
},
188+
)
189+
],
190+
),
191+
);
165192
}
166-
} catch (e) {
167-
showErrorDialog(e, context);
168193
}
169-
},
170-
icon: Image.asset('assets/images/barcode_scanner_icon.png'));
194+
} catch (e) {
195+
showErrorDialog(e, context);
196+
}
197+
},
198+
icon: Image.asset('assets/images/barcode_scanner_icon.png'),
199+
);
171200
}
172201
}

test/nutrition/nutritional_meal_form_test.mocks.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -319,16 +319,18 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i7.NutritionPlansP
319319
) as _i8.Future<void>);
320320
@override
321321
_i8.Future<List<dynamic>> searchIngredient(
322-
String? name, [
322+
String? name, {
323323
String? languageCode = r'en',
324-
]) =>
324+
bool? searchEnglish = false,
325+
}) =>
325326
(super.noSuchMethod(
326327
Invocation.method(
327328
#searchIngredient,
328-
[
329-
name,
330-
languageCode,
331-
],
329+
[name],
330+
{
331+
#languageCode: languageCode,
332+
#searchEnglish: searchEnglish,
333+
},
332334
),
333335
returnValue: _i8.Future<List<dynamic>>.value(<dynamic>[]),
334336
) as _i8.Future<List<dynamic>>);

0 commit comments

Comments
 (0)