Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Dieterbe committed Jul 16, 2024
1 parent 47a5f4a commit dc1f220
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 169 deletions.
20 changes: 11 additions & 9 deletions lib/widgets/nutrition/forms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
),
Expand All @@ -456,7 +458,7 @@ class IngredientFormState extends State<IngredientForm> {
showIngredientDetails(
context,
suggestions[index].ingredient.id,
image: suggestions[index].ingredient.image?.image,
select: select,
);
},
),
Expand Down
64 changes: 3 additions & 61 deletions lib/widgets/nutrition/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/misc.dart';
import 'package:wger/models/nutrition/ingredient.dart';
import 'package:wger/models/nutrition/meal.dart';
import 'package:wger/models/nutrition/nutritional_goals.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/macro_nutrients_table.dart';
import 'package:wger/widgets/nutrition/ingredient_dialogs.dart';

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

void showIngredientDetails(BuildContext context, int id, {String? image}) {
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) {
Ingredient? ingredient;
NutritionalGoals? goals;
String? source;

if (snapshot.hasData) {
ingredient = snapshot.data;
goals = ingredient!.nutritionalValues.toGoals();
source = ingredient.sourceName ?? 'unknown';
}
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 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 (image != null)
CircleAvatar(backgroundImage: NetworkImage(image), radius: radius),
if (image != null) const SizedBox(height: 12),
if (snapshot.hasError)
Text(
'Ingredient lookup error: ${snapshot.error ?? 'unknown error'}',
style: const TextStyle(color: Colors.red),
),
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),
),
),
],
),
),
),
);
return IngredientDetails(snapshot, select: select);
},
),
);
Expand Down
199 changes: 199 additions & 0 deletions lib/widgets/nutrition/ingredient_dialogs.dart
Original file line number Diff line number Diff line change
@@ -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();
},
),
],
);
}
}
1 change: 0 additions & 1 deletion lib/widgets/nutrition/nutrition_tiles.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class MealItemValuesTile extends StatelessWidget {
showIngredientDetails(
context,
ingredient.id,
image: ingredient.image?.image,
);
},
),
Expand Down
Loading

0 comments on commit dc1f220

Please sign in to comment.