mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-19 07:50:52 +01:00
Merge pull request #613 from wger-project/ingredient-details
popup ingredient details in typeahead suggestions and recently used
This commit is contained in:
@@ -24,9 +24,27 @@ part 'ingredient.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Ingredient {
|
||||
// fields returned by django api that we ignore here:
|
||||
// uuid, last_updated, last_imported, weight_units, language
|
||||
// most license fields
|
||||
|
||||
@JsonKey(required: true)
|
||||
final int id;
|
||||
|
||||
// some ingredients don't have these 3 fields set. E.g. USDA entries that
|
||||
// have been removed upstream, or manually added ingredients.
|
||||
@JsonKey(required: true, name: 'remote_id')
|
||||
final String? remoteId;
|
||||
|
||||
@JsonKey(required: true, name: 'source_name')
|
||||
final String? sourceName;
|
||||
|
||||
@JsonKey(required: true, name: 'source_url')
|
||||
final String? sourceUrl;
|
||||
|
||||
@JsonKey(required: true, name: 'license_object_url')
|
||||
final String? licenseObjectURl;
|
||||
|
||||
/// Barcode of the product
|
||||
@JsonKey(required: true)
|
||||
final String? code;
|
||||
@@ -73,6 +91,10 @@ class Ingredient {
|
||||
IngredientImage? image;
|
||||
|
||||
Ingredient({
|
||||
required this.remoteId,
|
||||
required this.sourceName,
|
||||
required this.sourceUrl,
|
||||
this.licenseObjectURl,
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
|
||||
@@ -11,6 +11,10 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
|
||||
json,
|
||||
requiredKeys: const [
|
||||
'id',
|
||||
'remote_id',
|
||||
'source_name',
|
||||
'source_url',
|
||||
'license_object_url',
|
||||
'code',
|
||||
'name',
|
||||
'created',
|
||||
@@ -25,6 +29,10 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
|
||||
],
|
||||
);
|
||||
return Ingredient(
|
||||
remoteId: json['remote_id'] as String?,
|
||||
sourceName: json['source_name'] as String?,
|
||||
sourceUrl: json['source_url'] as String?,
|
||||
licenseObjectURl: json['license_object_url'] as String?,
|
||||
id: (json['id'] as num).toInt(),
|
||||
code: json['code'] as String?,
|
||||
name: json['name'] as String,
|
||||
@@ -46,6 +54,10 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
|
||||
Map<String, dynamic> _$IngredientToJson(Ingredient instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'remote_id': instance.remoteId,
|
||||
'source_name': instance.sourceName,
|
||||
'source_url': instance.sourceUrl,
|
||||
'license_object_url': instance.licenseObjectURl,
|
||||
'code': instance.code,
|
||||
'name': instance.name,
|
||||
'created': instance.created.toIso8601String(),
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:wger/models/nutrition/nutritional_goals.dart';
|
||||
|
||||
class NutritionalValues {
|
||||
double energy = 0;
|
||||
double protein = 0;
|
||||
@@ -114,6 +116,19 @@ class NutritionalValues {
|
||||
return 'e: $energy, p: $protein, c: $carbohydrates, cS: $carbohydratesSugar, f: $fat, fS: $fatSaturated, fi: $fiber, s: $sodium';
|
||||
}
|
||||
|
||||
NutritionalGoals toGoals() {
|
||||
return NutritionalGoals(
|
||||
energy: energy,
|
||||
protein: protein,
|
||||
carbohydrates: carbohydrates,
|
||||
carbohydratesSugar: carbohydratesSugar,
|
||||
fat: fat,
|
||||
fatSaturated: fatSaturated,
|
||||
fiber: fiber,
|
||||
sodium: sodium,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
//ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hash(
|
||||
|
||||
@@ -38,10 +38,9 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
static const _nutritionalPlansInfoPath = 'nutritionplaninfo';
|
||||
static const _mealPath = 'meal';
|
||||
static const _mealItemPath = 'mealitem';
|
||||
static const _ingredientPath = 'ingredient';
|
||||
static const _ingredientInfoPath = 'ingredientinfo';
|
||||
static const _ingredientSearchPath = 'ingredient/search';
|
||||
static const _nutritionDiaryPath = 'nutritiondiary';
|
||||
static const _ingredientImagePath = 'ingredient-image';
|
||||
|
||||
final WgerBaseProvider baseProvider;
|
||||
List<NutritionalPlan> _plans = [];
|
||||
@@ -131,6 +130,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
try {
|
||||
plan = findById(planId);
|
||||
} on NoSuchEntryException {
|
||||
// TODO: remove this useless call, because we will fetch all details below
|
||||
plan = await fetchAndSetPlanSparse(planId);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
final List<MealItem> mealItems = [];
|
||||
final meal = Meal.fromJson(mealData);
|
||||
|
||||
// TODO: we should add these ingredients to the ingredient cache
|
||||
for (final mealItemData in mealData['meal_items']) {
|
||||
final mealItem = MealItem.fromJson(mealItemData);
|
||||
|
||||
@@ -298,7 +299,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
// Get ingredient from the server and save to cache
|
||||
} on StateError {
|
||||
final data = await baseProvider.fetch(
|
||||
baseProvider.makeUrl(_ingredientPath, id: ingredientId),
|
||||
baseProvider.makeUrl(_ingredientInfoPath, id: ingredientId),
|
||||
);
|
||||
ingredient = Ingredient.fromJson(data);
|
||||
_ingredients.add(ingredient);
|
||||
@@ -370,14 +371,15 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
|
||||
// Send the request
|
||||
final data = await baseProvider.fetch(
|
||||
baseProvider.makeUrl(_ingredientPath, query: {'code': code}),
|
||||
baseProvider.makeUrl(_ingredientInfoPath, query: {'code': code}),
|
||||
);
|
||||
|
||||
if (data['count'] == 0) {
|
||||
return null;
|
||||
} else {
|
||||
return Ingredient.fromJson(data['results'][0]);
|
||||
}
|
||||
// TODO we should probably add it to ingredient cache.
|
||||
// TODO: we could also use the ingredient cache for code searches
|
||||
return Ingredient.fromJson(data['results'][0]);
|
||||
}
|
||||
|
||||
/// Log meal to nutrition diary
|
||||
|
||||
@@ -94,7 +94,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
|
||||
exercisesProvider.fetchAndSetInitialData(),
|
||||
]);
|
||||
} catch (e) {
|
||||
log('fire! fire!');
|
||||
log('Exception loading base data');
|
||||
log(e.toString());
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
|
||||
measurementProvider.fetchAndSetAllCategoriesAndEntries(),
|
||||
]);
|
||||
} catch (e) {
|
||||
log('fire! fire!');
|
||||
log('Exception loading plans, weight, measurements and gallery');
|
||||
log(e.toString());
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
|
||||
await nutritionPlansProvider.fetchAndSetPlanFull(plan.id!);
|
||||
}
|
||||
} catch (e) {
|
||||
log('fire! fire!');
|
||||
log('Exception loading current nutritional plan');
|
||||
log(e.toString());
|
||||
}
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Macros preview',
|
||||
'Macros preview', // TODO fix l10n
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
FutureBuilder<Ingredient>(
|
||||
@@ -430,16 +430,18 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
itemCount: suggestions.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
void select() {
|
||||
final ingredient = suggestions[index].ingredient;
|
||||
selectIngredient(
|
||||
ingredient.id,
|
||||
ingredient.name,
|
||||
suggestions[index].amount,
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
final ingredient = suggestions[index].ingredient;
|
||||
selectIngredient(
|
||||
ingredient.id,
|
||||
ingredient.name,
|
||||
suggestions[index].amount,
|
||||
);
|
||||
},
|
||||
onTap: select,
|
||||
title: Text(
|
||||
'${suggestions[index].ingredient.name} (${suggestions[index].amount.toStringAsFixed(0)}$unit)',
|
||||
),
|
||||
@@ -447,7 +449,23 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
suggestions[index].ingredient.nutritionalValues,
|
||||
context,
|
||||
)),
|
||||
trailing: const Icon(Icons.copy),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () {
|
||||
showIngredientDetails(
|
||||
context,
|
||||
suggestions[index].ingredient.id,
|
||||
select: select,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
const Icon(Icons.copy),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -16,12 +16,15 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/models/nutrition/ingredient.dart';
|
||||
import 'package:wger/models/nutrition/meal.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/ingredient_dialogs.dart';
|
||||
|
||||
List<String> getNutritionColumnNames(BuildContext context) => [
|
||||
AppLocalizations.of(context).energy,
|
||||
@@ -95,3 +98,15 @@ String getKcalConsumedVsPlanned(Meal meal, BuildContext context) {
|
||||
|
||||
return '${consumed.toStringAsFixed(0)} / ${planned.toStringAsFixed(0)} ${loc.kcal}';
|
||||
}
|
||||
|
||||
void showIngredientDetails(BuildContext context, int id, {void Function()? select}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FutureBuilder<Ingredient>(
|
||||
future: Provider.of<NutritionPlansProvider>(context, listen: false).fetchIngredient(id),
|
||||
builder: (BuildContext context, AsyncSnapshot<Ingredient> snapshot) {
|
||||
return IngredientDetails(snapshot, select: select);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
199
lib/widgets/nutrition/ingredient_dialogs.dart
Normal file
199
lib/widgets/nutrition/ingredient_dialogs.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:wger/helpers/misc.dart';
|
||||
import 'package:wger/models/nutrition/ingredient.dart';
|
||||
import 'package:wger/models/nutrition/nutritional_goals.dart';
|
||||
import 'package:wger/widgets/nutrition/macro_nutrients_table.dart';
|
||||
|
||||
Widget ingredientImage(String url, BuildContext context) {
|
||||
var radius = 100.0;
|
||||
final height = MediaQuery.sizeOf(context).height;
|
||||
final width = MediaQuery.sizeOf(context).width;
|
||||
final smallest = height < width ? height : width;
|
||||
if (smallest < 400) {
|
||||
radius = smallest / 4;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: CircleAvatar(backgroundImage: NetworkImage(url), radius: radius),
|
||||
);
|
||||
}
|
||||
|
||||
class IngredientDetails extends StatelessWidget {
|
||||
final AsyncSnapshot<Ingredient> snapshot;
|
||||
final void Function()? select;
|
||||
const IngredientDetails(this.snapshot, {super.key, this.select});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Ingredient? ingredient;
|
||||
NutritionalGoals? goals;
|
||||
String? source;
|
||||
|
||||
if (snapshot.hasData) {
|
||||
ingredient = snapshot.data;
|
||||
goals = ingredient!.nutritionalValues.toGoals();
|
||||
source = ingredient.sourceName ?? 'unknown';
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: (snapshot.hasData) ? Text(ingredient!.name) : null,
|
||||
content: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (snapshot.hasError)
|
||||
Text(
|
||||
'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
if (ingredient?.image?.image != null)
|
||||
ingredientImage(ingredient!.image!.image, context),
|
||||
if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(),
|
||||
if (snapshot.hasData)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 400),
|
||||
child: MacronutrientsTable(
|
||||
nutritionalGoals: goals!,
|
||||
plannedValuesPercentage: goals.energyPercentage(),
|
||||
showGperKg: false,
|
||||
),
|
||||
),
|
||||
if (snapshot.hasData && ingredient!.licenseObjectURl == null)
|
||||
Text('Source: ${source!}'),
|
||||
if (snapshot.hasData && ingredient!.licenseObjectURl != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: InkWell(
|
||||
child: Text('Source: ${source!}'),
|
||||
onTap: () => launchURL(ingredient!.licenseObjectURl!, context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (snapshot.hasData && select != null)
|
||||
TextButton(
|
||||
key: const Key('ingredient-details-continue-button'),
|
||||
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
|
||||
onPressed: () {
|
||||
select!();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
key: const Key('ingredient-details-close-button'),
|
||||
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IngredientScanResultDialog extends StatelessWidget {
|
||||
final AsyncSnapshot<Ingredient?> snapshot;
|
||||
final String barcode;
|
||||
final Function(int id, String name, num? amount) selectIngredient;
|
||||
|
||||
const IngredientScanResultDialog(this.snapshot, this.barcode, this.selectIngredient, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Ingredient? ingredient;
|
||||
NutritionalGoals? goals;
|
||||
String? title;
|
||||
String? source;
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
ingredient = snapshot.data;
|
||||
title = ingredient != null
|
||||
? AppLocalizations.of(context).productFound
|
||||
: AppLocalizations.of(context).productNotFound;
|
||||
if (ingredient != null) {
|
||||
goals = ingredient.nutritionalValues.toGoals();
|
||||
source = ingredient.sourceName ?? 'unknown';
|
||||
}
|
||||
}
|
||||
return AlertDialog(
|
||||
key: const Key('ingredient-scan-result-dialog'),
|
||||
title: title != null ? Text(title) : null,
|
||||
content: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (snapshot.hasError)
|
||||
Text(
|
||||
'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
if (snapshot.connectionState == ConnectionState.done && ingredient == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).productNotFoundDescription(barcode),
|
||||
),
|
||||
),
|
||||
if (ingredient != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child:
|
||||
Text(AppLocalizations.of(context).productFoundDescription(ingredient.name)),
|
||||
),
|
||||
if (ingredient?.image?.image != null)
|
||||
ingredientImage(ingredient!.image!.image, context),
|
||||
if (snapshot.connectionState != ConnectionState.done && !snapshot.hasError)
|
||||
const CircularProgressIndicator(),
|
||||
if (goals != null)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 400),
|
||||
child: MacronutrientsTable(
|
||||
nutritionalGoals: goals,
|
||||
plannedValuesPercentage: goals.energyPercentage(),
|
||||
showGperKg: false,
|
||||
),
|
||||
),
|
||||
if (ingredient != null && ingredient.licenseObjectURl == null)
|
||||
Text('Source: ${source!}'),
|
||||
if (ingredient?.licenseObjectURl != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: InkWell(
|
||||
child: Text('Source: ${source!}'),
|
||||
onTap: () => launchURL(ingredient!.licenseObjectURl!, context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (ingredient != null) // if barcode matched
|
||||
TextButton(
|
||||
key: const Key('ingredient-scan-result-dialog-confirm-button'),
|
||||
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
|
||||
onPressed: () {
|
||||
selectIngredient(ingredient!.id, ingredient.name, null);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
// if didn't match, or we're still waiting
|
||||
TextButton(
|
||||
key: const Key('ingredient-scan-result-dialog-close-button'),
|
||||
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,15 @@ class MacronutrientsTable extends StatelessWidget {
|
||||
super.key,
|
||||
required this.nutritionalGoals,
|
||||
required this.plannedValuesPercentage,
|
||||
required this.nutritionalGoalsGperKg,
|
||||
this.nutritionalGoalsGperKg,
|
||||
this.showGperKg = true,
|
||||
});
|
||||
|
||||
static const double tablePadding = 7;
|
||||
final NutritionalGoals nutritionalGoals;
|
||||
final NutritionalGoals plannedValuesPercentage;
|
||||
final NutritionalGoals? nutritionalGoalsGperKg;
|
||||
final bool showGperKg;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -42,7 +44,8 @@ class MacronutrientsTable extends StatelessWidget {
|
||||
),
|
||||
Text(goal != null ? valFn(goal.toStringAsFixed(0)) : '', textAlign: TextAlign.right),
|
||||
Text(pct != null ? pct.toStringAsFixed(1) : '', textAlign: TextAlign.right),
|
||||
Text(perkg != null ? perkg.toStringAsFixed(1) : '', textAlign: TextAlign.right),
|
||||
if (showGperKg)
|
||||
Text(perkg != null ? perkg.toStringAsFixed(1) : '', textAlign: TextAlign.right),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -62,7 +65,7 @@ class MacronutrientsTable extends StatelessWidget {
|
||||
columnHeader(true, loc.macronutrients),
|
||||
columnHeader(false, loc.total),
|
||||
columnHeader(false, loc.percentEnergy),
|
||||
columnHeader(false, loc.gPerBodyKg),
|
||||
if (showGperKg) columnHeader(false, loc.gPerBodyKg),
|
||||
],
|
||||
),
|
||||
macroRow(0, false, loc.energy, (NutritionalGoals ng) => ng.energy),
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:wger/widgets/nutrition/nutrition_tile.dart';
|
||||
import 'package:wger/widgets/nutrition/widgets.dart';
|
||||
|
||||
/// a NutritionTitle showing an ingredient, with its
|
||||
/// avatar and nutritional values
|
||||
/// avatar, nutritional values and button to popup its details
|
||||
class MealItemValuesTile extends StatelessWidget {
|
||||
final Ingredient ingredient;
|
||||
final NutritionalValues nutritionalValues;
|
||||
@@ -29,6 +29,15 @@ class MealItemValuesTile extends StatelessWidget {
|
||||
return NutritionTile(
|
||||
leading: IngredientAvatar(ingredient: ingredient),
|
||||
title: Text(getShortNutritionValues(nutritionalValues, context)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () {
|
||||
showIngredientDetails(
|
||||
context,
|
||||
ingredient.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ import 'package:wger/models/exercises/ingredient_api.dart';
|
||||
import 'package:wger/models/nutrition/ingredient.dart';
|
||||
import 'package:wger/providers/nutrition.dart';
|
||||
import 'package:wger/widgets/core/core.dart';
|
||||
import 'package:wger/widgets/nutrition/nutrition_tiles.dart';
|
||||
import 'package:wger/widgets/nutrition/helpers.dart';
|
||||
import 'package:wger/widgets/nutrition/ingredient_dialogs.dart';
|
||||
|
||||
class ScanReader extends StatelessWidget {
|
||||
const ScanReader();
|
||||
@@ -159,6 +160,18 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
|
||||
),
|
||||
title: Text(suggestion.value),
|
||||
// subtitle: Text(suggestion.data.id.toString()),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () {
|
||||
showIngredientDetails(
|
||||
context,
|
||||
suggestion.data.id,
|
||||
select: () {
|
||||
widget.selectIngredient(suggestion.data.id, suggestion.value, null);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
transitionBuilder: (context, animation, child) => FadeTransition(
|
||||
@@ -193,87 +206,24 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
|
||||
if (!widget.test!) {
|
||||
barcode = await readerscan(context);
|
||||
}
|
||||
|
||||
if (barcode.isNotEmpty) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final result = await Provider.of<NutritionPlansProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).searchIngredientWithCode(barcode);
|
||||
// TODO: show spinner...
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
key: const Key('found-dialog'),
|
||||
title: Text(AppLocalizations.of(context).productFound),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context).productFoundDescription(result.name)),
|
||||
MealItemValuesTile(
|
||||
ingredient: result,
|
||||
nutritionalValues: result.nutritionalValues,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: const Key('found-dialog-confirm-button'),
|
||||
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
|
||||
onPressed: () {
|
||||
widget.selectIngredient(result.id, result.name, null);
|
||||
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(barcode),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: const Key('notFound-dialog-close-button'),
|
||||
child: Text(
|
||||
MaterialLocalizations.of(context).closeButtonLabel,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(e, context);
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FutureBuilder<Ingredient?>(
|
||||
future: Provider.of<NutritionPlansProvider>(context, listen: false)
|
||||
.searchIngredientWithCode(barcode),
|
||||
builder: (BuildContext context, AsyncSnapshot<Ingredient?> snapshot) {
|
||||
return IngredientScanResultDialog(snapshot, barcode, widget.selectIngredient);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"id": 10065,
|
||||
"code": "0043647440020",
|
||||
"name": "'Old Times' Orange Fine Cut Marmalade",
|
||||
"created": "2020-12-20T01:00:00+01:00",
|
||||
"last_update": "2022-08-09T10:23:11+02:00",
|
||||
"energy": 269,
|
||||
"protein": "0.000",
|
||||
"carbohydrates": "67.000",
|
||||
"carbohydrates_sugar": "66.000",
|
||||
"fat": "0.000",
|
||||
"fat_saturated": "0.000",
|
||||
"fiber": null,
|
||||
"sodium": "0.000",
|
||||
"license": 5,
|
||||
"license_author": "Open Food Facts",
|
||||
"language": 2
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"id": 58300,
|
||||
"code": "4071800000992",
|
||||
"name": "1688 Mehrkorn",
|
||||
"created": "2020-12-20T01:00:00+02:00",
|
||||
"last_update": "2022-08-09T18:55:00+02:00",
|
||||
"energy": 229,
|
||||
"protein": "7.680",
|
||||
"carbohydrates": "35.700",
|
||||
"carbohydrates_sugar": "2.320",
|
||||
"fat": "4.820",
|
||||
"fat_saturated": "0.714",
|
||||
"fiber": "6.960",
|
||||
"sodium": "0.000",
|
||||
"license": 5,
|
||||
"license_author": "Open Food Facts",
|
||||
"language": 2
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"id": 59887,
|
||||
"code": "4311501354155",
|
||||
"name": "Baked Beans",
|
||||
"created": "2020-12-20T23:10:54+01:00",
|
||||
"last_update": "2022-08-09T13:32:41+01:00",
|
||||
"energy": 86,
|
||||
"protein": "4.400",
|
||||
"carbohydrates": "11.000",
|
||||
"carbohydrates_sugar": "5.300",
|
||||
"fat": "0.100",
|
||||
"fat_saturated": "0.000",
|
||||
"fiber": "5.000",
|
||||
"sodium": "0.480",
|
||||
"license": 5,
|
||||
"license_author": "Open Food Facts",
|
||||
"language": 1
|
||||
}
|
||||
56
test/fixtures/nutrition/ingredientinfo_10065.json
vendored
Normal file
56
test/fixtures/nutrition/ingredientinfo_10065.json
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"id": 10065,
|
||||
"uuid": "cd134608-3fbf-4d54-bf9d-ecc56e4271f2",
|
||||
"remote_id": "0043647440020",
|
||||
"source_name": "Open Food Facts",
|
||||
"source_url": "https://world.openfoodfacts.org/api/v2/product/0043647440020.json",
|
||||
"code": "0043647440020",
|
||||
"name": "'Old Times' Orange Fine Cut Marmalade",
|
||||
"created": "2020-12-20T01:00:00+01:00",
|
||||
"last_update": "2024-06-07T06:04:05.211180+02:00",
|
||||
"last_imported": "2020-12-20T14:44:55.854000+01:00",
|
||||
"energy": 269,
|
||||
"protein": "0.000",
|
||||
"carbohydrates": "67.000",
|
||||
"carbohydrates_sugar": "66.000",
|
||||
"fat": "0.000",
|
||||
"fat_saturated": "0.000",
|
||||
"fiber": null,
|
||||
"sodium": "0.000",
|
||||
"weight_units": [],
|
||||
"language": {
|
||||
"id": 2,
|
||||
"short_name": "en",
|
||||
"full_name": "English",
|
||||
"full_name_en": "English"
|
||||
},
|
||||
"image": {
|
||||
"id": 7545,
|
||||
"uuid": "c7a43097-da62-4a35-95f4-fe466e68cbc6",
|
||||
"ingredient_id": 10065,
|
||||
"ingredient_uuid": "cd134608-3fbf-4d54-bf9d-ecc56e4271f2",
|
||||
"image": "https://wger.de/media/ingredients/10065/c7a43097-da62-4a35-95f4-fe466e68cbc6.jpg",
|
||||
"created": "2023-04-21T20:13:01.136749+02:00",
|
||||
"last_update": "2023-04-21T20:13:01.151137+02:00",
|
||||
"size": 43397,
|
||||
"width": 219,
|
||||
"height": 400,
|
||||
"license": 1,
|
||||
"license_title": "Photo",
|
||||
"license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=0043647440020&id=2",
|
||||
"license_author": "tacinte",
|
||||
"license_author_url": "https://world.openfoodfacts.org/photographer/tacinte",
|
||||
"license_derivative_source_url": ""
|
||||
},
|
||||
"license": {
|
||||
"id": 5,
|
||||
"full_name": "Open Data Commons Open Database License",
|
||||
"short_name": "ODbL",
|
||||
"url": "https://opendatacommons.org/licenses/odbl/"
|
||||
},
|
||||
"license_title": "'Old Times' Orange Fine Cut Marmalade",
|
||||
"license_object_url": "",
|
||||
"license_author": "",
|
||||
"license_author_url": "",
|
||||
"license_derivative_source_url": ""
|
||||
}
|
||||
56
test/fixtures/nutrition/ingredientinfo_58300.json
vendored
Normal file
56
test/fixtures/nutrition/ingredientinfo_58300.json
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"id": 58300,
|
||||
"uuid": "151a8d44-735e-4f9b-9e22-7dc4c604154a",
|
||||
"remote_id": "4071800000992",
|
||||
"source_name": "Open Food Facts",
|
||||
"source_url": "https://world.openfoodfacts.org/api/v2/product/4071800000992.json",
|
||||
"code": "4071800000992",
|
||||
"name": "1688 Mehrkorn",
|
||||
"created": "2020-12-20T01:00:00+01:00",
|
||||
"last_update": "2024-06-07T06:06:48.259950+02:00",
|
||||
"last_imported": "2020-12-20T14:46:58.971000+01:00",
|
||||
"energy": 229,
|
||||
"protein": "7.680",
|
||||
"carbohydrates": "35.700",
|
||||
"carbohydrates_sugar": "2.320",
|
||||
"fat": "4.820",
|
||||
"fat_saturated": "0.714",
|
||||
"fiber": "6.960",
|
||||
"sodium": "0.000",
|
||||
"weight_units": [],
|
||||
"language": {
|
||||
"id": 1,
|
||||
"short_name": "de",
|
||||
"full_name": "Deutsch",
|
||||
"full_name_en": "German"
|
||||
},
|
||||
"image": {
|
||||
"id": 10862,
|
||||
"uuid": "a5b6766f-b606-451c-b806-24eb3a8597ea",
|
||||
"ingredient_id": 58300,
|
||||
"ingredient_uuid": "151a8d44-735e-4f9b-9e22-7dc4c604154a",
|
||||
"image": "https://wger.de/media/ingredients/58300/a5b6766f-b606-451c-b806-24eb3a8597ea.jpg",
|
||||
"created": "2023-05-08T11:37:13.876341+02:00",
|
||||
"last_update": "2023-05-08T11:37:13.891457+02:00",
|
||||
"size": 33617,
|
||||
"width": 400,
|
||||
"height": 225,
|
||||
"license": 1,
|
||||
"license_title": "Photo",
|
||||
"license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=4071800000992&id=15",
|
||||
"license_author": "kiliweb",
|
||||
"license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb",
|
||||
"license_derivative_source_url": ""
|
||||
},
|
||||
"license": {
|
||||
"id": 5,
|
||||
"full_name": "Open Data Commons Open Database License",
|
||||
"short_name": "ODbL",
|
||||
"url": "https://opendatacommons.org/licenses/odbl/"
|
||||
},
|
||||
"license_title": "1688 Mehrkorn",
|
||||
"license_object_url": "",
|
||||
"license_author": "",
|
||||
"license_author_url": "",
|
||||
"license_derivative_source_url": ""
|
||||
}
|
||||
56
test/fixtures/nutrition/ingredientinfo_59887.json
vendored
Normal file
56
test/fixtures/nutrition/ingredientinfo_59887.json
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"id": 59887,
|
||||
"uuid": "2efa101b-d6f7-4755-838d-b3bb5c483b02",
|
||||
"remote_id": "4311501354155",
|
||||
"source_name": "Open Food Facts",
|
||||
"source_url": "https://world.openfoodfacts.org/api/v2/product/4311501354155.json",
|
||||
"code": "4311501354155",
|
||||
"name": "Baked Beans",
|
||||
"created": "2020-12-20T01:00:00+01:00",
|
||||
"last_update": "2024-06-07T06:40:23.364866+02:00",
|
||||
"last_imported": "2020-12-20T14:47:03.728000+01:00",
|
||||
"energy": 86,
|
||||
"protein": "4.400",
|
||||
"carbohydrates": "11.000",
|
||||
"carbohydrates_sugar": "5.300",
|
||||
"fat": "0.600",
|
||||
"fat_saturated": "0.100",
|
||||
"fiber": "9.100",
|
||||
"sodium": "0.260",
|
||||
"weight_units": [],
|
||||
"language": {
|
||||
"id": 1,
|
||||
"short_name": "de",
|
||||
"full_name": "Deutsch",
|
||||
"full_name_en": "German"
|
||||
},
|
||||
"image": {
|
||||
"id": 1718,
|
||||
"uuid": "493bc593-4813-4bae-b92a-a4e1fbee5e44",
|
||||
"ingredient_id": 59887,
|
||||
"ingredient_uuid": "2efa101b-d6f7-4755-838d-b3bb5c483b02",
|
||||
"image": "https://wger.de/media/ingredients/59887/493bc593-4813-4bae-b92a-a4e1fbee5e44.jpg",
|
||||
"created": "2023-04-09T01:45:50.874157+02:00",
|
||||
"last_update": "2023-04-09T01:45:50.911258+02:00",
|
||||
"size": 27752,
|
||||
"width": 286,
|
||||
"height": 400,
|
||||
"license": 1,
|
||||
"license_title": "Photo",
|
||||
"license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=4311501354155&id=6",
|
||||
"license_author": "kiliweb",
|
||||
"license_author_url": "https://world.openfoodfacts.org/photographer/kiliweb",
|
||||
"license_derivative_source_url": ""
|
||||
},
|
||||
"license": {
|
||||
"id": 5,
|
||||
"full_name": "Open Data Commons Open Database License",
|
||||
"short_name": "ODbL",
|
||||
"url": "https://opendatacommons.org/licenses/odbl/"
|
||||
},
|
||||
"license_title": "Baked Beans",
|
||||
"license_object_url": "https://world.openfoodfacts.org/product/4311501354155/",
|
||||
"license_author": "",
|
||||
"license_author_url": "",
|
||||
"license_derivative_source_url": ""
|
||||
}
|
||||
63
test/fixtures/nutrition/ingredientinfo_right_code.json
vendored
Normal file
63
test/fixtures/nutrition/ingredientinfo_right_code.json
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 19733,
|
||||
"uuid": "354c6050-5654-41bc-8823-4fd27b54fb59",
|
||||
"remote_id": "3068320105222",
|
||||
"source_name": "Open Food Facts",
|
||||
"source_url": "https://world.openfoodfacts.org/api/v2/product/3068320105222.json",
|
||||
"code": "3068320105222",
|
||||
"name": "Badoit zest citron vert sans sucres",
|
||||
"created": "2020-12-20T01:00:00+01:00",
|
||||
"last_update": "2024-06-07T06:39:29.032545+02:00",
|
||||
"last_imported": "2020-12-20T14:45:31.370000+01:00",
|
||||
"energy": 1,
|
||||
"protein": "0.000",
|
||||
"carbohydrates": "0.000",
|
||||
"carbohydrates_sugar": "0.000",
|
||||
"fat": "0.000",
|
||||
"fat_saturated": "0.000",
|
||||
"fiber": null,
|
||||
"sodium": "0.020",
|
||||
"weight_units": [],
|
||||
"language": {
|
||||
"id": 12,
|
||||
"short_name": "fr",
|
||||
"full_name": "Fran\u00e7ais",
|
||||
"full_name_en": "French"
|
||||
},
|
||||
"image": {
|
||||
"id": 26949,
|
||||
"uuid": "5177e2d1-b03a-42d9-bdf1-2dd3edad8210",
|
||||
"ingredient_id": 19733,
|
||||
"ingredient_uuid": "354c6050-5654-41bc-8823-4fd27b54fb59",
|
||||
"image": "https://wger.de/media/ingredients/19733/5177e2d1-b03a-42d9-bdf1-2dd3edad8210.jpg",
|
||||
"created": "2023-12-04T10:48:19.019477+01:00",
|
||||
"last_update": "2023-12-04T10:48:19.062343+01:00",
|
||||
"size": 13302,
|
||||
"width": 112,
|
||||
"height": 400,
|
||||
"license": 1,
|
||||
"license_title": "Photo",
|
||||
"license_object_url": "https://world.openfoodfacts.org/cgi/product_image.pl?code=3068320105222&id=11",
|
||||
"license_author": "hungergames",
|
||||
"license_author_url": "https://world.openfoodfacts.org/photographer/hungergames",
|
||||
"license_derivative_source_url": ""
|
||||
},
|
||||
"license": {
|
||||
"id": 5,
|
||||
"full_name": "Open Data Commons Open Database License",
|
||||
"short_name": "ODbL",
|
||||
"url": "https://opendatacommons.org/licenses/odbl/"
|
||||
},
|
||||
"license_title": "Badoit zest citron vert sans sucres",
|
||||
"license_object_url": "",
|
||||
"license_author": "",
|
||||
"license_author_url": "",
|
||||
"license_derivative_source_url": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 9436,
|
||||
"code": "0013087245950",
|
||||
"name": " Gâteau double chocolat ",
|
||||
"creation_date": "2020-12-20",
|
||||
"update_date": "2020-12-20",
|
||||
"energy": 360,
|
||||
"protein": "5.000",
|
||||
"carbohydrates": "45.000",
|
||||
"carbohydrates_sugar": "27.000",
|
||||
"fat": "18.000",
|
||||
"fat_saturated": "4.500",
|
||||
"fiber": "2.000",
|
||||
"sodium": "0.356",
|
||||
"license": 5,
|
||||
"license_author": "Open Food Facts",
|
||||
"language": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19,7 +19,6 @@ void main() {
|
||||
const String planInfoUrl = 'nutritionplaninfo';
|
||||
const String planUrl = 'nutritionplan';
|
||||
const String diaryUrl = 'nutritiondiary';
|
||||
const String ingredientUrl = 'ingredient';
|
||||
|
||||
final Map<String, dynamic> nutritionalPlanInfoResponse = jsonDecode(
|
||||
fixture('nutrition/nutritional_plan_info_detail_response.json'),
|
||||
@@ -31,13 +30,13 @@ void main() {
|
||||
fixture('nutrition/nutrition_diary_response.json'),
|
||||
)['results'];
|
||||
final Map<String, dynamic> ingredient59887Response = jsonDecode(
|
||||
fixture('nutrition/ingredient_59887_response.json'),
|
||||
fixture('nutrition/ingredientinfo_59887.json'),
|
||||
);
|
||||
final Map<String, dynamic> ingredient10065Response = jsonDecode(
|
||||
fixture('nutrition/ingredient_10065_response.json'),
|
||||
fixture('nutrition/ingredientinfo_10065.json'),
|
||||
);
|
||||
final Map<String, dynamic> ingredient58300Response = jsonDecode(
|
||||
fixture('nutrition/ingredient_58300_response.json'),
|
||||
fixture('nutrition/ingredientinfo_58300.json'),
|
||||
);
|
||||
|
||||
final ingredientList = [
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:network_image_mock/network_image_mock.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/models/exercises/ingredient_api.dart';
|
||||
@@ -26,6 +27,9 @@ import 'nutritional_plan_form_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
final ingredient = Ingredient(
|
||||
remoteId: '1',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/1',
|
||||
id: 1,
|
||||
code: '123456787',
|
||||
name: 'Water',
|
||||
@@ -52,20 +56,20 @@ void main() {
|
||||
var plan1 = NutritionalPlan.empty();
|
||||
var meal1 = Meal();
|
||||
|
||||
final Uri tUriRightCode = Uri.parse('https://localhost/api/v2/ingredient/?code=123');
|
||||
final Uri tUriEmptyCode = Uri.parse('https://localhost/api/v2/ingredient/?code="%20"');
|
||||
final Uri tUriBadCode = Uri.parse('https://localhost/api/v2/ingredient/?code=222');
|
||||
final Uri tUriRightCode = Uri.parse('https://localhost/api/v2/ingredientinfo/?code=123');
|
||||
final Uri tUriEmptyCode = Uri.parse('https://localhost/api/v2/ingredientinfo/?code="%20"');
|
||||
final Uri tUriBadCode = Uri.parse('https://localhost/api/v2/ingredientinfo/?code=222');
|
||||
|
||||
when(client.get(tUriRightCode, headers: anyNamed('headers'))).thenAnswer(
|
||||
(_) => Future.value(http.Response(fixture('nutrition/search_ingredient_right_code.json'), 200)),
|
||||
(_) => Future.value(http.Response(fixture('nutrition/ingredientinfo_right_code.json'), 200)),
|
||||
);
|
||||
|
||||
when(client.get(tUriEmptyCode, headers: anyNamed('headers'))).thenAnswer(
|
||||
(_) => Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200)),
|
||||
(_) => Future.value(http.Response(fixture('nutrition/ingredientinfo_wrong_code.json'), 200)),
|
||||
);
|
||||
|
||||
when(client.get(tUriBadCode, headers: anyNamed('headers'))).thenAnswer(
|
||||
(_) => Future.value(http.Response(fixture('nutrition/search_ingredient_wrong_code.json'), 200)),
|
||||
(_) => Future.value(http.Response(fixture('nutrition/ingredientinfo_wrong_code.json'), 200)),
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
@@ -124,13 +128,15 @@ void main() {
|
||||
});
|
||||
|
||||
group('Test the AlertDialogs for scanning result', () {
|
||||
// TODO: why do we need to support empty barcodes?
|
||||
testWidgets('with empty code', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createMealItemFormScreen(meal1, '', true));
|
||||
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('with correct code', (WidgetTester tester) async {
|
||||
@@ -139,7 +145,8 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('with incorrect code', (WidgetTester tester) async {
|
||||
@@ -148,7 +155,8 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('notFound-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -218,9 +226,9 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
|
||||
await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(formState.ingredientIdController.text, '1');
|
||||
@@ -232,12 +240,12 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('found-dialog-close-button')));
|
||||
await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-close-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsNothing);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -261,9 +269,9 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
|
||||
await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
|
||||
@@ -272,15 +280,16 @@ void main() {
|
||||
expect(find.text('Please enter a valid number'), findsOneWidget);
|
||||
});
|
||||
|
||||
//TODO: isn't this test just a duplicate of the above one? can be removed?
|
||||
testWidgets('save ingredient with incorrect weight input type', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
|
||||
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
|
||||
await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
|
||||
@@ -298,22 +307,22 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('scan-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
|
||||
await tester.tap(find.byKey(const Key('ingredient-scan-result-dialog-confirm-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
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();
|
||||
|
||||
expect(find.byKey(const Key('found-dialog')), findsNothing);
|
||||
// once ID and weight are set, it'll fetchIngredient and show macros preview and ingredient image
|
||||
when(mockNutrition.fetchIngredient(1)).thenAnswer((_) => Future.value(
|
||||
Ingredient.fromJson(jsonDecode(fixture('nutrition/ingredientinfo_59887.json'))),
|
||||
));
|
||||
await mockNetworkImagesFor(() => tester.pumpAndSettle());
|
||||
|
||||
expect(find.byKey(const Key('ingredient-scan-result-dialog')), findsNothing);
|
||||
|
||||
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -26,6 +26,9 @@ import 'package:wger/models/nutrition/meal_item.dart';
|
||||
import 'package:wger/models/nutrition/nutritional_plan.dart';
|
||||
|
||||
final ingredient1 = Ingredient(
|
||||
remoteId: '1',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/1',
|
||||
id: 1,
|
||||
code: '123456787',
|
||||
name: 'Water',
|
||||
@@ -40,6 +43,9 @@ final ingredient1 = Ingredient(
|
||||
sodium: 0.5,
|
||||
);
|
||||
final ingredient2 = Ingredient(
|
||||
remoteId: '2',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/2',
|
||||
id: 2,
|
||||
code: '123456788',
|
||||
name: 'Burger soup',
|
||||
@@ -54,6 +60,9 @@ final ingredient2 = Ingredient(
|
||||
sodium: 0,
|
||||
);
|
||||
final ingredient3 = Ingredient(
|
||||
remoteId: '3',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/3',
|
||||
id: 3,
|
||||
code: '123456789',
|
||||
name: 'Broccoli cake',
|
||||
@@ -68,6 +77,9 @@ final ingredient3 = Ingredient(
|
||||
sodium: 10,
|
||||
);
|
||||
final muesli = Ingredient(
|
||||
remoteId: '1',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/1',
|
||||
id: 1,
|
||||
code: '123456787',
|
||||
name: 'Müsli',
|
||||
@@ -82,6 +94,9 @@ final muesli = Ingredient(
|
||||
sodium: 0.5,
|
||||
);
|
||||
final milk = Ingredient(
|
||||
remoteId: '1',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/1',
|
||||
id: 1,
|
||||
code: '123456787',
|
||||
name: 'Milk',
|
||||
@@ -96,6 +111,9 @@ final milk = Ingredient(
|
||||
sodium: 0.5,
|
||||
);
|
||||
final apple = Ingredient(
|
||||
remoteId: '1',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/1',
|
||||
id: 1,
|
||||
code: '123456787',
|
||||
name: 'Apple',
|
||||
@@ -110,6 +128,9 @@ final apple = Ingredient(
|
||||
sodium: 0.5,
|
||||
);
|
||||
final cake = Ingredient(
|
||||
remoteId: '1',
|
||||
sourceName: 'Built-in testdata',
|
||||
sourceUrl: 'https://example.com/ingredient/1',
|
||||
id: 1,
|
||||
code: '111111111',
|
||||
name: 'Lemon CAke',
|
||||
|
||||
Reference in New Issue
Block a user