From ace03d98d20df85711a629c1c380dff7092eeffc Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 24 Sep 2024 17:23:01 +0200 Subject: [PATCH] Handle changing a food entry --- lib/food_entry/enter_food_widget.dart | 2 +- lib/food_entry/food_entry_bloc.dart | 53 +++++++- lib/food_entry/food_entry_widget.dart | 177 ++++++++++++++++++++++++-- lib/perdate/entry_list.dart | 9 +- lib/perdate/perdate_widget.dart | 74 +++++------ lib/storage/storage.dart | 10 +- lib/utils/sum_widget.dart | 2 +- 7 files changed, 264 insertions(+), 63 deletions(-) diff --git a/lib/food_entry/enter_food_widget.dart b/lib/food_entry/enter_food_widget.dart index 95511f6..7501520 100644 --- a/lib/food_entry/enter_food_widget.dart +++ b/lib/food_entry/enter_food_widget.dart @@ -126,7 +126,7 @@ class _EnterFoodWidgetState extends State { var entry = FoodEntryState( name: nameController.text, mass: massAsNumber, - kcalPerMass: kcalPerMassAsNumber, + kcalPer100: kcalPerMassAsNumber, waitingForNetwork: false, ); diff --git a/lib/food_entry/food_entry_bloc.dart b/lib/food_entry/food_entry_bloc.dart index 35baaf8..f0e847a 100644 --- a/lib/food_entry/food_entry_bloc.dart +++ b/lib/food_entry/food_entry_bloc.dart @@ -15,6 +15,7 @@ class FoodEntryBloc extends Bloc { required this.storage}) : super(initialState) { on(handleFoodEntryEvent); + on(handleFoodChangedEvent); on(handleDeleteFoodEvent); on(handleBarcodeScannedEvent); } @@ -30,6 +31,22 @@ class FoodEntryBloc extends Bloc { emit(newState); } + void handleFoodChangedEvent( + FoodChangedEvent event, Emitter emit) async { + var entries = state.foodEntries; + var index = entries.indexWhere((entry) { + return entry.id == event.newEntry.id; + }); + + entries.removeAt(index); + entries.insert(index, event.newEntry); + + await storage.writeEntriesForDate(forDate, entries); + storage.addFoodEntryToLookupDatabase(event.newEntry); + + emit(PageState(foodEntries: entries)); + } + void handleDeleteFoodEvent( FoodDeletionEvent event, Emitter emit) async { state.foodEntries.removeWhere((entry) => entry.id == event.entryID); @@ -57,7 +74,7 @@ class FoodEntryBloc extends Bloc { List newList = List.from(state.foodEntries); var newEntryWaiting = FoodEntryState( - kcalPerMass: 0, name: "", mass: 0, waitingForNetwork: true); + kcalPer100: 0, name: "", mass: 0, waitingForNetwork: true); newList.add(newEntryWaiting); emit(PageState(foodEntries: newList)); @@ -81,7 +98,7 @@ class FoodEntryBloc extends Bloc { var newEntryFinishedWaiting = FoodEntryState( name: response.food?.name ?? "", mass: response.food?.mass ?? 0, - kcalPerMass: response.food?.kcalPer100g ?? 0, + kcalPer100: response.food?.kcalPer100g ?? 0, waitingForNetwork: false, ); newList.add(newEntryFinishedWaiting); @@ -98,6 +115,12 @@ class FoodEntryEvent extends FoodEvent { FoodEntryEvent({required this.entry}); } +class FoodChangedEvent extends FoodEvent { + final FoodEntryState newEntry; + + FoodChangedEvent({required this.newEntry}); +} + class FoodDeletionEvent extends FoodEvent { final String entryID; @@ -133,21 +156,37 @@ class PageState { class FoodEntryState { final String name; final int mass; - final int kcalPerMass; + final int kcalPer100; final String id; final bool waitingForNetwork; - FoodEntryState({ + factory FoodEntryState({ + required name, + required mass, + required kcalPer100, + required waitingForNetwork, + }) { + return FoodEntryState.withID( + id: const Uuid().v1(), + name: name, + mass: mass, + kcalPer100: kcalPer100, + waitingForNetwork: waitingForNetwork, + ); + } + + FoodEntryState.withID({ + required this.id, required this.name, required this.mass, - required this.kcalPerMass, + required this.kcalPer100, required this.waitingForNetwork, - }) : id = const Uuid().v1(); + }); @override String toString() { //we use quotation marks around the name because the name might contain //commas and we want to store it in a csv file - return '$id,"$name",$mass,$kcalPerMass'; + return '$id,"$name",$mass,$kcalPer100'; } } diff --git a/lib/food_entry/food_entry_widget.dart b/lib/food_entry/food_entry_widget.dart index 6f52be6..02f9df4 100644 --- a/lib/food_entry/food_entry_widget.dart +++ b/lib/food_entry/food_entry_widget.dart @@ -5,9 +5,14 @@ import 'package:calorimeter/utils/row_with_spacers_widget.dart'; class FoodEntryWidget extends StatefulWidget { final FoodEntryState entry; final Function(BuildContext context, String id) onDelete; + final Function(BuildContext context, FoodEntryState entry) onChange; - const FoodEntryWidget( - {super.key, required this.entry, required this.onDelete}); + const FoodEntryWidget({ + super.key, + required this.entry, + required this.onDelete, + required this.onChange, + }); @override State createState() => _FoodEntryWidgetState(); @@ -45,17 +50,24 @@ class _FoodEntryWidgetState extends State { ? Container() : Text(widget.entry.mass.ceil().toString(), textAlign: TextAlign.end), - widget.entry.waitingForNetwork - ? Container() - : Text(widget.entry.kcalPerMass.ceil().toString(), - textAlign: TextAlign.end), Opacity( opacity: showCancelAndDelete ? 0.0 : 1.0, - child: Text( - (widget.entry.mass * widget.entry.kcalPerMass / 100) - .ceil() - .toString(), - textAlign: TextAlign.end), + child: widget.entry.waitingForNetwork + ? Container() + : Text(widget.entry.kcalPer100.ceil().toString(), + textAlign: TextAlign.end), + ), + Opacity( + opacity: showCancelAndDelete ? 0.0 : 1.0, + child: widget.entry.waitingForNetwork + ? Container() + : Text( + (widget.entry.mass * + widget.entry.kcalPer100 / + 100) + .ceil() + .toString(), + textAlign: TextAlign.end), ), ), ), @@ -92,6 +104,28 @@ class _FoodEntryWidgetState extends State { ? () => widget.onDelete(context, widget.entry.id) : null), ), + SizedBox( + child: IconButton( + padding: const EdgeInsets.all(0.0), + icon: const Icon(Icons.edit), + onPressed: showCancelAndDelete + ? () async { + await showDialog( + context: context, + builder: (dialogContext) { + return FoodEntryChangeDialog( + entry: widget.entry, + onChange: (context, entry) { + widget.onChange(context, entry); + }); + }, + ); + setState(() { + showCancelAndDelete = false; + }); + } + : null), + ), ], ), ), @@ -100,3 +134,124 @@ class _FoodEntryWidgetState extends State { ); } } + +class FoodEntryChangeDialog extends StatefulWidget { + final FoodEntryState entry; + final Function(BuildContext context, FoodEntryState entry) onChange; + + const FoodEntryChangeDialog( + {required this.entry, super.key, required this.onChange}); + + @override + State createState() => _FoodEntryChangeDialogState(); +} + +class _FoodEntryChangeDialogState extends State { + late TextEditingController nameController; + late TextEditingController massController; + late TextEditingController kcalPer100Controller; + + static const textFieldVerticalPadding = 16.0; + static const textFieldHorizontalPadding = 16.0; + + @override + void initState() { + nameController = TextEditingController(); + nameController.text = widget.entry.name; + + massController = TextEditingController(); + massController.text = widget.entry.mass.toString(); + + kcalPer100Controller = TextEditingController(); + kcalPer100Controller.text = widget.entry.kcalPer100.toString(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: textFieldVerticalPadding, + horizontal: textFieldHorizontalPadding), + child: TextField( + onSubmitted: (val) => _onSubmitAction(), + controller: nameController, + decoration: const InputDecoration( + label: Text("Name"), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: textFieldVerticalPadding, + horizontal: textFieldHorizontalPadding), + child: TextField( + onSubmitted: (val) => _onSubmitAction(), + controller: massController, + decoration: const InputDecoration( + label: Text("Menge"), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: textFieldVerticalPadding, + horizontal: textFieldHorizontalPadding), + child: TextField( + onSubmitted: (val) => _onSubmitAction(), + controller: kcalPer100Controller, + decoration: const InputDecoration( + label: Text("kcal pro Menge"), + ), + ), + ) + ], + ), + actions: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.cancel)), + IconButton( + onPressed: () => _onSubmitAction(), + icon: const Icon(Icons.check), + ) + ], + ); + } + + void _onSubmitAction() { + int mass; + int kcalPer100; + + try { + mass = int.parse(massController.text); + } catch (e) { + mass = 0; + } + + try { + kcalPer100 = int.parse(kcalPer100Controller.text); + } catch (e) { + kcalPer100 = 0; + } + + var newEntry = FoodEntryState.withID( + id: widget.entry.id, + name: nameController.text, + mass: mass, + kcalPer100: kcalPer100, + waitingForNetwork: widget.entry.waitingForNetwork, + ); + + widget.onChange(context, newEntry); + + Navigator.of(context).pop(); + } +} diff --git a/lib/perdate/entry_list.dart b/lib/perdate/entry_list.dart index 1017749..c139ebe 100644 --- a/lib/perdate/entry_list.dart +++ b/lib/perdate/entry_list.dart @@ -39,11 +39,16 @@ class FoodEntryList extends StatelessWidget { FoodEntryWidget( key: ValueKey(entries[entryIndex].id), entry: entries[entryIndex], - onDelete: (callbackContext, id) { - callbackContext.read().add(FoodDeletionEvent( + onDelete: (_, id) { + context.read().add(FoodDeletionEvent( entryID: id, )); }, + onChange: (_, changedEntry) { + context + .read() + .add(FoodChangedEvent(newEntry: changedEntry)); + }, ), const Divider(), ], diff --git a/lib/perdate/perdate_widget.dart b/lib/perdate/perdate_widget.dart index 3c725ce..9ae14aa 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -54,43 +54,45 @@ class _PerDateWidgetState extends State { ) ], child: BlocConsumer( - listener: (context, pageState) { - if (pageState.errorString != null) { - showNewSnackbarWith(context, pageState.errorString!); - } - }, builder: (context, pageState) { - return Scaffold( - appBar: AppBar( - title: - Text(DateFormat.yMMMMd('de').format(widget.date)), - actions: const [ThemeSwitcherButton()], - ), - body: FoodEntryList(entries: pageState.foodEntries), - bottomNavigationBar: BottomAppBar( - shape: const RectangularNotchShape(), - color: Theme.of(context).colorScheme.secondary, - child: - SumWidget(foodEntries: pageState.foodEntries)), - drawer: const AppDrawer(), - floatingActionButton: OverflowBar(children: [ - ScanFoodFloatingButton( - onPressed: () { - var result = BarcodeScanner.scan(); - context.read().add( - BarcodeScanned(scanResultFuture: result)); - }, + listener: (context, pageState) { + if (pageState.errorString != null) { + showNewSnackbarWith(context, pageState.errorString!); + } + }, + builder: (context, pageState) { + return Scaffold( + appBar: AppBar( + title: Text( + DateFormat.yMMMMd('de').format(widget.date)), + actions: const [ThemeSwitcherButton()], ), - const SizedBox(width: 8), - CalendarFloatingButton( - startFromDate: widget.date, - onDateSelected: (dateSelected) { - _onDateSelected(dateSelected); - }, - ), - ]), - floatingActionButtonLocation: - FloatingActionButtonLocation.endDocked); - }), + body: FoodEntryList(entries: pageState.foodEntries), + bottomNavigationBar: BottomAppBar( + shape: const RectangularNotchShape(), + color: Theme.of(context).colorScheme.secondary, + child: SumWidget( + foodEntries: pageState.foodEntries)), + drawer: const AppDrawer(), + floatingActionButton: OverflowBar(children: [ + ScanFoodFloatingButton( + onPressed: () { + var result = BarcodeScanner.scan(); + context.read().add( + BarcodeScanned(scanResultFuture: result)); + }, + ), + const SizedBox(width: 8), + CalendarFloatingButton( + startFromDate: widget.date, + onDateSelected: (dateSelected) { + _onDateSelected(dateSelected); + }, + ), + ]), + floatingActionButtonLocation: + FloatingActionButtonLocation.endDocked); + }, + ), ); }); } diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 06aa58c..b129588 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -62,7 +62,7 @@ class FoodStorage { var entry = FoodEntryState( name: fields[1].replaceAll('"', ""), mass: mass, - kcalPerMass: kcalPerMass, + kcalPer100: kcalPerMass, waitingForNetwork: false, ); entries.add(entry); @@ -169,14 +169,14 @@ class FoodStorage { var entriesForDate = await getEntriesForDate(date); for (var entry in entriesForDate) { - _foodLookupDatabase[entry.name] = entry.kcalPerMass; - log("Added entry: ${entry.name}/${entry.kcalPerMass}"); + _foodLookupDatabase[entry.name] = entry.kcalPer100; + log("Added entry: ${entry.name}/${entry.kcalPer100}"); } } void addFoodEntryToLookupDatabase(FoodEntryState entry) { - _foodLookupDatabase[entry.name] = entry.kcalPerMass; - log("Added entry: ${entry.name}/${entry.kcalPerMass}"); + _foodLookupDatabase[entry.name] = entry.kcalPer100; + log("Added entry: ${entry.name}/${entry.kcalPer100}"); } Map get getFoodEntryLookupDatabase => _foodLookupDatabase; diff --git a/lib/utils/sum_widget.dart b/lib/utils/sum_widget.dart index 5334030..8e08e0d 100644 --- a/lib/utils/sum_widget.dart +++ b/lib/utils/sum_widget.dart @@ -13,7 +13,7 @@ class SumWidget extends StatelessWidget { builder: (context, state) { var sum = 0.0; for (var entry in foodEntries) { - sum += entry.kcalPerMass / 100 * entry.mass; + sum += entry.kcalPer100 / 100 * entry.mass; } var diff = state.kcalLimit - sum; var diffLimit = state.kcalLimit ~/ 4;