diff --git a/lib/food_entry/enter_food_widget.dart b/lib/food_entry/enter_food_widget.dart index 031707c..bbfe0d3 100644 --- a/lib/food_entry/enter_food_widget.dart +++ b/lib/food_entry/enter_food_widget.dart @@ -1,4 +1,4 @@ -import 'package:calorimeter/perdate/perdate_widget.dart'; +import 'package:calorimeter/utils/enter_food_controller.dart'; import 'package:calorimeter/storage/storage.dart'; import 'package:flutter/material.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; diff --git a/lib/food_scan/food_fact_lookup.dart b/lib/food_scan/food_fact_lookup.dart index 5c60253..2d1667f 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.barcodeNotFound); + } + + 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, + barcodeNotFound, + 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..2dfd4e8 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -1,5 +1,6 @@ -import 'dart:developer'; - +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:calorimeter/food_scan/food_fact_lookup.dart'; +import 'package:calorimeter/utils/enter_food_controller.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'; @@ -29,7 +30,6 @@ class _PerDateWidgetState extends State { @override void initState() { - log("PerDateWidgetState's initState()"); storage = FoodStorage.getInstance(); entriesFuture = storage.getEntriesForDate(widget.date); entriesFuture.then((val) { @@ -40,67 +40,118 @@ class _PerDateWidgetState extends State { @override Widget build(BuildContext context) { - log("PerDateWidgetState's build()"); 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 (scanResult.type == ResultType.Cancelled) { + return; + } + + if (!context.mounted) return; + + if (scanResult.type == ResultType.Error) { + showNewSnackbarWith(context, + "Fehler beim Scannen des Barcodes."); + } + var response = await client + .retrieveFoodInfo(scanResult.rawContent); + + if (!context.mounted) return; + + if (response.status == + FoodFactResponseStatus.barcodeNotFound) { + showNewSnackbarWith(context, + "Barcode konnte nicht gefunden werden."); + return; + } + + if (response.status == + FoodFactResponseStatus + .foodFactServerNotReachable) { + showNewSnackbarWith(context, + "OpenFoodFacts-Server konnte nicht erreicht werden."); + return; + } + + 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); + }), + ); }); } -} -class EnterFoodController extends ChangeNotifier { - String name = ""; - String kcalPer100g = ""; + void showNewSnackbarWith(BuildContext context, String text) { + var snackbar = + ErrorSnackbar(colorScheme: Theme.of(context).colorScheme, text: text); - void set(String newName, String newKcal) { - name = newName; - kcalPer100g = newKcal; - notifyListeners(); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(snackbar); } } + +class ErrorSnackbar extends SnackBar { + final String text; + final ColorScheme colorScheme; + ErrorSnackbar({ + required this.text, + required this.colorScheme, + super.key, + }) : super( + content: Text(text, style: TextStyle(color: colorScheme.onError)), + backgroundColor: colorScheme.error); +} diff --git a/lib/utils/enter_food_controller.dart b/lib/utils/enter_food_controller.dart new file mode 100644 index 0000000..44a998f --- /dev/null +++ b/lib/utils/enter_food_controller.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class EnterFoodController extends ChangeNotifier { + String name = ""; + String kcalPer100g = ""; + + void set(String newName, String newKcal) { + name = newName; + kcalPer100g = newKcal; + notifyListeners(); + } +} 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, ); }, );