From df6e5dd8cb404d826c62ecb3997ef70fa5ff7b3b Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 8 Sep 2024 01:57:40 +0200 Subject: [PATCH] Add error handling for food scanning and make sum widget have different colors depending on difference to limit setting. --- lib/food_scan/food_fact_lookup.dart | 47 +++++++- lib/perdate/perdate_widget.dart | 135 +++++++++++++++-------- lib/utils/scan_food_floating_button.dart | 22 +--- lib/utils/sum_widget.dart | 46 +++++--- 4 files changed, 167 insertions(+), 83 deletions(-) diff --git a/lib/food_scan/food_fact_lookup.dart b/lib/food_scan/food_fact_lookup.dart index 5c60253..1a0e00b 100644 --- a/lib/food_scan/food_fact_lookup.dart +++ b/lib/food_scan/food_fact_lookup.dart @@ -1,22 +1,44 @@ import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; class FoodFactLookupClient { FoodFactLookupClient(); - static const String host = "world.openfoodfacts.org"; static const String url = "https://world.openfoodfacts.org/api/v3/product/"; String getProductUrl(String ean) { return "$url$ean.json"; } - Future retrieveFoodInfo(String ean) async { + Future retrieveFoodInfo(String ean) async { HttpClient client = HttpClient(); - var request = await client.getUrl(Uri.parse(getProductUrl(ean))); - var response = await request.close(); - var asString = await response.transform(utf8.decoder).join(); - return FoodFactModel.fromJson(jsonDecode(asString)); + String asString = ""; + + try { + var request = await client.getUrl(Uri.parse(getProductUrl(ean))); + + var response = await request.close(); + + if (response.statusCode != HttpStatus.ok) { + return FoodFactResponse( + food: null, status: FoodFactResponseStatus.foodNotFound); + } + + asString = await response.transform(utf8.decoder).join(); + } on SocketException { + return FoodFactResponse( + food: null, + status: FoodFactResponseStatus.foodFactServerNotReachable); + } catch (e) { + log(e.toString()); + } finally { + client.close(); + } + + return FoodFactResponse( + food: FoodFactModel.fromJson(jsonDecode(asString)), + status: FoodFactResponseStatus.ok); } } @@ -32,3 +54,16 @@ class FoodFactModel { kcalPer100g: json['product']['nutriments']['energy-kcal_100g']); } } + +enum FoodFactResponseStatus { + foodFactServerNotReachable, + foodNotFound, + ok, +} + +class FoodFactResponse { + final FoodFactModel? food; + final FoodFactResponseStatus status; + + FoodFactResponse({required this.food, required this.status}); +} diff --git a/lib/perdate/perdate_widget.dart b/lib/perdate/perdate_widget.dart index 7028578..c3cbb12 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -1,5 +1,7 @@ import 'dart:developer'; +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:calorimeter/food_scan/food_fact_lookup.dart'; import 'package:calorimeter/utils/scan_food_floating_button.dart'; import 'package:calorimeter/utils/app_drawer.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; @@ -44,52 +46,97 @@ class _PerDateWidgetState extends State { return FutureBuilder( future: entriesFuture, builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const Center(child: CircularProgressIndicator()); - } else { - return ChangeNotifierProvider( - create: (context) => EnterFoodController(), - child: BlocProvider( - create: (context) => FoodEntryBloc( - initialState: FoodEntryState(foodEntries: entries), - storage: storage, - forDate: widget.date), - child: BlocBuilder( - builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: - Text(DateFormat.yMMMMd('de').format(widget.date)), - actions: const [ThemeSwitcherButton()], + return snapshot.connectionState != ConnectionState.done + ? const Center(child: CircularProgressIndicator()) + : MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => EnterFoodController()), + BlocProvider( + create: (context) => FoodEntryBloc( + initialState: FoodEntryState(foodEntries: entries), + storage: storage, + forDate: widget.date, ), - body: FoodEntryList(entries: state.foodEntries), - bottomNavigationBar: BottomAppBar( - shape: const RectangularNotchShape(), - color: Theme.of(context).colorScheme.secondary, - child: SumWidget(foodEntries: state.foodEntries)), - drawer: const AppDrawer(), - floatingActionButton: OverflowBar(children: [ - const ScanFoodFloatingButton(), - const SizedBox(width: 8), - CalendarFloatingButton( - startFromDate: widget.date, - onDateSelected: (dateSelected) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return PerDateWidget(date: dateSelected); - }, - ), - ); - }, + ) + ], + child: BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: + Text(DateFormat.yMMMMd('de').format(widget.date)), + actions: const [ThemeSwitcherButton()], ), - ]), - floatingActionButtonLocation: - FloatingActionButtonLocation.endDocked); - }), - ), - ); - } + body: FoodEntryList(entries: state.foodEntries), + bottomNavigationBar: BottomAppBar( + shape: const RectangularNotchShape(), + color: Theme.of(context).colorScheme.secondary, + child: SumWidget(foodEntries: state.foodEntries)), + drawer: const AppDrawer(), + floatingActionButton: OverflowBar(children: [ + ScanFoodFloatingButton( + onPressed: () async { + var client = FoodFactLookupClient(); + + var scanResult = await BarcodeScanner.scan(); + if (!context.mounted) return; + if (scanResult.type == ResultType.Cancelled) { + return; + } + if (scanResult.type == ResultType.Error) { + var snackbar = const SnackBar( + content: Text("Error scanning barcode")); + ScaffoldMessenger.of(context) + .showSnackBar(snackbar); + } + var response = await client + .retrieveFoodInfo(scanResult.rawContent); + + if (!context.mounted) return; + + if (response.status == + FoodFactResponseStatus.foodNotFound) { + var snackbar = const SnackBar( + content: Text("Food not found")); + ScaffoldMessenger.of(context) + .showSnackBar(snackbar); + } + + if (response.status == + FoodFactResponseStatus + .foodFactServerNotReachable) { + var snackbar = const SnackBar( + content: + Text("FoodFact server not reachable")); + ScaffoldMessenger.of(context) + .showSnackBar(snackbar); + } + + context.read().set( + response.food!.name, + response.food!.kcalPer100g.toString(), + ); + }, + ), + const SizedBox(width: 8), + CalendarFloatingButton( + startFromDate: widget.date, + onDateSelected: (dateSelected) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return PerDateWidget(date: dateSelected); + }, + ), + ); + }, + ), + ]), + floatingActionButtonLocation: + FloatingActionButtonLocation.endDocked); + }), + ); }); } } diff --git a/lib/utils/scan_food_floating_button.dart b/lib/utils/scan_food_floating_button.dart index b0443fa..95b7695 100644 --- a/lib/utils/scan_food_floating_button.dart +++ b/lib/utils/scan_food_floating_button.dart @@ -1,30 +1,16 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:calorimeter/food_scan/food_fact_lookup.dart'; -import 'package:calorimeter/perdate/perdate_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class ScanFoodFloatingButton extends StatelessWidget { - const ScanFoodFloatingButton({ - super.key, - }); + final Function() onPressed; + const ScanFoodFloatingButton({super.key, required this.onPressed}); @override Widget build(BuildContext context) { return FloatingActionButton( heroTag: "scanFoodFAB", child: const Icon(Icons.barcode_reader), - onPressed: () async { - var client = FoodFactLookupClient(); - - var scanResult = await BarcodeScanner.scan(); - var food = await client.retrieveFoodInfo(scanResult.rawContent); - - if (!context.mounted) return; - - context - .read() - .set(food.name, food.kcalPer100g.toString()); + onPressed: () { + onPressed(); }, ); } diff --git a/lib/utils/sum_widget.dart b/lib/utils/sum_widget.dart index 668c03a..8184f21 100644 --- a/lib/utils/sum_widget.dart +++ b/lib/utils/sum_widget.dart @@ -2,37 +2,53 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart'; -import 'package:calorimeter/utils/row_with_spacers_widget.dart'; -class SumWidget extends StatefulWidget { +class SumWidget extends StatelessWidget { final List foodEntries; const SumWidget({required this.foodEntries, super.key}); - @override - State createState() => _SumWidgetState(); -} - -class _SumWidgetState extends State { @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { var sum = 0.0; - for (var entry in widget.foodEntries) { + for (var entry in foodEntries) { sum += entry.kcalPerMass / 100 * entry.mass; } + var diff = state.kcalLimit - sum; + var diffLimit = state.kcalLimit ~/ 4; - return RowWidget( - Text( + var textColor = Theme.of(context).colorScheme.onSecondary; + var newTextColor = textColor; + var brightness = Theme.of(context).brightness; + + switch (brightness) { + case Brightness.dark: + if (diff < 0) { + newTextColor = Colors.red[900]!; + } else if (diff < diffLimit) { + newTextColor = Colors.orange[900]!; + } + break; + + case Brightness.light: + if (diff < 0) { + newTextColor = Colors.redAccent; + } else if (diff < diffLimit) { + newTextColor = Colors.orangeAccent; + } + break; + } + + return Align( + alignment: Alignment.centerLeft, + child: Text( 'kcal heute: ${sum.ceil().toString()}/${state.kcalLimit.ceil()}', style: Theme.of(context) .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.onPrimary), + .bodyLarge! + .copyWith(color: newTextColor), ), - null, - null, - null, ); }, );