Skip to content

Commit dc1f220

Browse files
committed
refactor
ingredient details dialog: - give it close/continue buttons to load into selection - always use image property from loaded ingredient this is a bit slower, but: * more consistent (no need to support absolute vs relative URL's separately) * cleaner (no need to pass it thru explicitly) * more future proof: we will get rid of the dedicated /ingredient/search endpoint which gives us images before we load the full ingredient. in the future we will simply load the ingredients, completely, all at once. * allows for easier code reuse with barcode scan result dialog barcode scan result dialog: - show image and detailed nutrition table - support a loading spinner - simplify error handling - deduplicate code between found & not found - share code with ingredient details dialog
1 parent 47a5f4a commit dc1f220

File tree

6 files changed

+255
-169
lines changed

6 files changed

+255
-169
lines changed

lib/widgets/nutrition/forms.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -430,16 +430,18 @@ class IngredientFormState extends State<IngredientForm> {
430430
itemCount: suggestions.length,
431431
shrinkWrap: true,
432432
itemBuilder: (context, index) {
433+
void select() {
434+
final ingredient = suggestions[index].ingredient;
435+
selectIngredient(
436+
ingredient.id,
437+
ingredient.name,
438+
suggestions[index].amount,
439+
);
440+
}
441+
433442
return Card(
434443
child: ListTile(
435-
onTap: () {
436-
final ingredient = suggestions[index].ingredient;
437-
selectIngredient(
438-
ingredient.id,
439-
ingredient.name,
440-
suggestions[index].amount,
441-
);
442-
},
444+
onTap: select,
443445
title: Text(
444446
'${suggestions[index].ingredient.name} (${suggestions[index].amount.toStringAsFixed(0)}$unit)',
445447
),
@@ -456,7 +458,7 @@ class IngredientFormState extends State<IngredientForm> {
456458
showIngredientDetails(
457459
context,
458460
suggestions[index].ingredient.id,
459-
image: suggestions[index].ingredient.image?.image,
461+
select: select,
460462
);
461463
},
462464
),

lib/widgets/nutrition/helpers.dart

Lines changed: 3 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,12 @@
1919
import 'package:flutter/material.dart';
2020
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2121
import 'package:provider/provider.dart';
22-
import 'package:wger/helpers/misc.dart';
2322
import 'package:wger/models/nutrition/ingredient.dart';
2423
import 'package:wger/models/nutrition/meal.dart';
25-
import 'package:wger/models/nutrition/nutritional_goals.dart';
2624
import 'package:wger/models/nutrition/nutritional_values.dart';
2725
import 'package:wger/providers/nutrition.dart';
2826
import 'package:wger/widgets/core/core.dart';
29-
import 'package:wger/widgets/nutrition/macro_nutrients_table.dart';
27+
import 'package:wger/widgets/nutrition/ingredient_dialogs.dart';
3028

3129
List<String> getNutritionColumnNames(BuildContext context) => [
3230
AppLocalizations.of(context).energy,
@@ -101,69 +99,13 @@ String getKcalConsumedVsPlanned(Meal meal, BuildContext context) {
10199
return '${consumed.toStringAsFixed(0)} / ${planned.toStringAsFixed(0)} ${loc.kcal}';
102100
}
103101

104-
void showIngredientDetails(BuildContext context, int id, {String? image}) {
102+
void showIngredientDetails(BuildContext context, int id, {void Function()? select}) {
105103
showDialog(
106104
context: context,
107105
builder: (context) => FutureBuilder<Ingredient>(
108106
future: Provider.of<NutritionPlansProvider>(context, listen: false).fetchIngredient(id),
109107
builder: (BuildContext context, AsyncSnapshot<Ingredient> snapshot) {
110-
Ingredient? ingredient;
111-
NutritionalGoals? goals;
112-
String? source;
113-
114-
if (snapshot.hasData) {
115-
ingredient = snapshot.data;
116-
goals = ingredient!.nutritionalValues.toGoals();
117-
source = ingredient.sourceName ?? 'unknown';
118-
}
119-
var radius = 100.0;
120-
final height = MediaQuery.sizeOf(context).height;
121-
final width = MediaQuery.sizeOf(context).width;
122-
final smallest = height < width ? height : width;
123-
if (smallest < 400) {
124-
radius = smallest / 4;
125-
}
126-
return AlertDialog(
127-
title: (snapshot.hasData) ? Text(ingredient!.name) : null,
128-
content: SingleChildScrollView(
129-
child: Padding(
130-
padding: const EdgeInsets.all(8.0),
131-
child: Column(
132-
mainAxisSize: MainAxisSize.min,
133-
children: [
134-
if (image != null)
135-
CircleAvatar(backgroundImage: NetworkImage(image), radius: radius),
136-
if (image != null) const SizedBox(height: 12),
137-
if (snapshot.hasError)
138-
Text(
139-
'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}',
140-
style: const TextStyle(color: Colors.red),
141-
),
142-
if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(),
143-
if (snapshot.hasData)
144-
ConstrainedBox(
145-
constraints: const BoxConstraints(minWidth: 400),
146-
child: MacronutrientsTable(
147-
nutritionalGoals: goals!,
148-
plannedValuesPercentage: goals.energyPercentage(),
149-
showGperKg: false,
150-
),
151-
),
152-
if (snapshot.hasData && ingredient!.licenseObjectURl == null)
153-
Text('Source: ${source!}'),
154-
if (snapshot.hasData && ingredient!.licenseObjectURl != null)
155-
Padding(
156-
padding: const EdgeInsets.only(top: 12),
157-
child: InkWell(
158-
child: Text('Source: ${source!}'),
159-
onTap: () => launchURL(ingredient!.licenseObjectURl!, context),
160-
),
161-
),
162-
],
163-
),
164-
),
165-
),
166-
);
108+
return IngredientDetails(snapshot, select: select);
167109
},
168110
),
169111
);
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3+
import 'package:wger/helpers/misc.dart';
4+
import 'package:wger/models/nutrition/ingredient.dart';
5+
import 'package:wger/models/nutrition/nutritional_goals.dart';
6+
import 'package:wger/widgets/nutrition/macro_nutrients_table.dart';
7+
8+
Widget ingredientImage(String url, BuildContext context) {
9+
var radius = 100.0;
10+
final height = MediaQuery.sizeOf(context).height;
11+
final width = MediaQuery.sizeOf(context).width;
12+
final smallest = height < width ? height : width;
13+
if (smallest < 400) {
14+
radius = smallest / 4;
15+
}
16+
return Padding(
17+
padding: const EdgeInsets.only(bottom: 12),
18+
child: CircleAvatar(backgroundImage: NetworkImage(url), radius: radius),
19+
);
20+
}
21+
22+
class IngredientDetails extends StatelessWidget {
23+
final AsyncSnapshot<Ingredient> snapshot;
24+
final void Function()? select;
25+
const IngredientDetails(this.snapshot, {super.key, this.select});
26+
27+
@override
28+
Widget build(BuildContext context) {
29+
Ingredient? ingredient;
30+
NutritionalGoals? goals;
31+
String? source;
32+
33+
if (snapshot.hasData) {
34+
ingredient = snapshot.data;
35+
goals = ingredient!.nutritionalValues.toGoals();
36+
source = ingredient.sourceName ?? 'unknown';
37+
}
38+
39+
return AlertDialog(
40+
title: (snapshot.hasData) ? Text(ingredient!.name) : null,
41+
content: SingleChildScrollView(
42+
child: Padding(
43+
padding: const EdgeInsets.all(8.0),
44+
child: Column(
45+
mainAxisSize: MainAxisSize.min,
46+
children: [
47+
if (snapshot.hasError)
48+
Text(
49+
'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}',
50+
style: const TextStyle(color: Colors.red),
51+
),
52+
if (ingredient?.image?.image != null)
53+
ingredientImage(ingredient!.image!.image, context),
54+
if (!snapshot.hasData && !snapshot.hasError) const CircularProgressIndicator(),
55+
if (snapshot.hasData)
56+
ConstrainedBox(
57+
constraints: const BoxConstraints(minWidth: 400),
58+
child: MacronutrientsTable(
59+
nutritionalGoals: goals!,
60+
plannedValuesPercentage: goals.energyPercentage(),
61+
showGperKg: false,
62+
),
63+
),
64+
if (snapshot.hasData && ingredient!.licenseObjectURl == null)
65+
Text('Source: ${source!}'),
66+
if (snapshot.hasData && ingredient!.licenseObjectURl != null)
67+
Padding(
68+
padding: const EdgeInsets.only(top: 12),
69+
child: InkWell(
70+
child: Text('Source: ${source!}'),
71+
onTap: () => launchURL(ingredient!.licenseObjectURl!, context),
72+
),
73+
),
74+
],
75+
),
76+
),
77+
),
78+
actions: [
79+
if (snapshot.hasData && select != null)
80+
TextButton(
81+
key: const Key('ingredient-details-continue-button'),
82+
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
83+
onPressed: () {
84+
select!();
85+
Navigator.of(context).pop();
86+
},
87+
),
88+
TextButton(
89+
key: const Key('ingredient-details-close-button'),
90+
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
91+
onPressed: () {
92+
Navigator.of(context).pop();
93+
},
94+
),
95+
],
96+
);
97+
}
98+
}
99+
100+
class IngredientScanResultDialog extends StatelessWidget {
101+
final AsyncSnapshot<Ingredient?> snapshot;
102+
final String barcode;
103+
final Function(int id, String name, num? amount) selectIngredient;
104+
105+
const IngredientScanResultDialog(this.snapshot, this.barcode, this.selectIngredient, {super.key});
106+
107+
@override
108+
Widget build(BuildContext context) {
109+
Ingredient? ingredient;
110+
NutritionalGoals? goals;
111+
String? title;
112+
String? source;
113+
114+
if (snapshot.connectionState == ConnectionState.done) {
115+
ingredient = snapshot.data;
116+
title = ingredient != null
117+
? AppLocalizations.of(context).productFound
118+
: AppLocalizations.of(context).productNotFound;
119+
if (ingredient != null) {
120+
goals = ingredient.nutritionalValues.toGoals();
121+
source = ingredient.sourceName ?? 'unknown';
122+
}
123+
}
124+
return AlertDialog(
125+
key: const Key('ingredient-scan-result-dialog'),
126+
title: title != null ? Text(title) : null,
127+
content: SingleChildScrollView(
128+
child: Padding(
129+
padding: const EdgeInsets.all(8.0),
130+
child: Column(
131+
mainAxisSize: MainAxisSize.min,
132+
children: [
133+
if (snapshot.hasError)
134+
Text(
135+
'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}',
136+
style: const TextStyle(color: Colors.red),
137+
),
138+
if (snapshot.connectionState == ConnectionState.done && ingredient == null)
139+
Padding(
140+
padding: const EdgeInsets.only(bottom: 8.0),
141+
child: Text(
142+
AppLocalizations.of(context).productNotFoundDescription(barcode),
143+
),
144+
),
145+
if (ingredient != null)
146+
Padding(
147+
padding: const EdgeInsets.only(bottom: 8.0),
148+
child:
149+
Text(AppLocalizations.of(context).productFoundDescription(ingredient.name)),
150+
),
151+
if (ingredient?.image?.image != null)
152+
ingredientImage(ingredient!.image!.image, context),
153+
if (snapshot.connectionState != ConnectionState.done && !snapshot.hasError)
154+
const CircularProgressIndicator(),
155+
if (goals != null)
156+
ConstrainedBox(
157+
constraints: const BoxConstraints(minWidth: 400),
158+
child: MacronutrientsTable(
159+
nutritionalGoals: goals,
160+
plannedValuesPercentage: goals.energyPercentage(),
161+
showGperKg: false,
162+
),
163+
),
164+
if (ingredient != null && ingredient.licenseObjectURl == null)
165+
Text('Source: ${source!}'),
166+
if (ingredient?.licenseObjectURl != null)
167+
Padding(
168+
padding: const EdgeInsets.only(top: 12),
169+
child: InkWell(
170+
child: Text('Source: ${source!}'),
171+
onTap: () => launchURL(ingredient!.licenseObjectURl!, context),
172+
),
173+
),
174+
],
175+
),
176+
),
177+
),
178+
actions: [
179+
if (ingredient != null) // if barcode matched
180+
TextButton(
181+
key: const Key('ingredient-scan-result-dialog-confirm-button'),
182+
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
183+
onPressed: () {
184+
selectIngredient(ingredient!.id, ingredient.name, null);
185+
Navigator.of(context).pop();
186+
},
187+
),
188+
// if didn't match, or we're still waiting
189+
TextButton(
190+
key: const Key('ingredient-scan-result-dialog-close-button'),
191+
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
192+
onPressed: () {
193+
Navigator.of(context).pop();
194+
},
195+
),
196+
],
197+
);
198+
}
199+
}

lib/widgets/nutrition/nutrition_tiles.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ class MealItemValuesTile extends StatelessWidget {
3535
showIngredientDetails(
3636
context,
3737
ingredient.id,
38-
image: ingredient.image?.image,
3938
);
4039
},
4140
),

0 commit comments

Comments
 (0)