mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Add switch to search for ingredients in English as well
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>);
|
||||
|
||||
@@ -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((_) =>
|
||||
|
||||
@@ -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>>);
|
||||
|
||||
Reference in New Issue
Block a user