import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:calorimeter/food_scan/food_fact_lookup.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:calorimeter/storage/storage.dart'; import 'package:uuid/uuid.dart'; class FoodEntryBloc extends Bloc { final PageState initialState; final FoodStorage storage; final DateTime forDate; FoodEntryBloc( {required this.initialState, required this.forDate, required this.storage}) : super(initialState) { on(handleFoodEntryEvent); on(handleFoodChangedEvent); on(handleDeleteFoodEvent); on(handleBarcodeScannedEvent); on(handleFoodEntryTapped); } void handleFoodEntryEvent( FoodEntryEvent event, Emitter emit) async { PageState newState = PageState.from(state); newState.addEntry(event.entry); await storage.writeEntriesForDate(forDate, newState.foodEntries); storage.addFoodEntryToLookupDatabase(event.entry); 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); await storage.writeEntriesForDate(forDate, state.foodEntries); emit(PageState.from(state)); } void handleBarcodeScannedEvent( BarcodeScanned event, Emitter emit) async { var client = FoodFactLookupClient(); var scanResult = await event.scanResultFuture; if (scanResult.type == ResultType.Cancelled) { return; } if (scanResult.type == ResultType.Error) { emit(PageState( foodEntries: state.foodEntries, errorString: "Fehler beim Scannen des Barcodes")); return; } var responseFuture = client.retrieveFoodInfo(scanResult.rawContent); List newList = List.from(state.foodEntries); var newEntryWaiting = FoodEntryState( kcalPer100: 0, name: "", mass: 0, waitingForNetwork: true, isSelected: false, ); newList.add(newEntryWaiting); emit(PageState(foodEntries: newList)); await responseFuture.then((response) { var index = newList .indexWhere((entryState) => entryState.id == newEntryWaiting.id); // element not found (was deleted previously) if (index == -1) { return; } if (response.status == FoodFactResponseStatus.barcodeNotFound) { emit(PageState( foodEntries: state.foodEntries, errorString: "Barcode konnte nicht gefunden werden.")); return; } if (response.status == FoodFactResponseStatus.foodFactServerNotReachable) { emit(PageState( foodEntries: state.foodEntries, errorString: "OpenFoodFacts-Server konnte nicht erreicht werden.")); return; } var newEntryFinishedWaiting = FoodEntryState( name: response.food?.name ?? "", mass: response.food?.mass ?? 0, kcalPer100: response.food?.kcalPer100g ?? 0, waitingForNetwork: false, isSelected: false, ); newList.removeAt(index); newList.insert(index, newEntryFinishedWaiting); emit(PageState(foodEntries: newList)); }); } void handleFoodEntryTapped( FoodEntryTapped event, Emitter emit) async { for (var entry in state.foodEntries) { entry.isSelected = false; } var selectedEntry = state.foodEntries.firstWhere((entry) { return entry.id == event.entryID; }); selectedEntry.selected = !selectedEntry.selected; emit(PageState(foodEntries: state.foodEntries)); } } class FoodEvent {} class FoodEntryEvent extends FoodEvent { final FoodEntryState entry; FoodEntryEvent({required this.entry}); } class FoodChangedEvent extends FoodEvent { final FoodEntryState newEntry; FoodChangedEvent({required this.newEntry}); } class FoodDeletionEvent extends FoodEvent { final String entryID; FoodDeletionEvent({required this.entryID}); } class BarcodeScanned extends FoodEvent { final Future scanResultFuture; BarcodeScanned({required this.scanResultFuture}); } class FoodEntryTapped extends FoodEvent { final String entryID; FoodEntryTapped({required this.entryID}); } /// This is the state for one date/page class PageState { final List foodEntries; final String? errorString; PageState({required this.foodEntries, this.errorString}); factory PageState.init() { return PageState(foodEntries: []); } static from(PageState state) { return PageState(foodEntries: state.foodEntries); } void addEntry(FoodEntryState entry) { foodEntries.add(entry); } } class FoodEntryState { final String name; final int mass; final int kcalPer100; final String id; final bool waitingForNetwork; bool isSelected; factory FoodEntryState({ required name, required mass, required kcalPer100, required waitingForNetwork, required isSelected, }) { return FoodEntryState.withID( id: const Uuid().v1(), name: name, mass: mass, kcalPer100: kcalPer100, waitingForNetwork: waitingForNetwork, isSelected: isSelected, ); } FoodEntryState.withID({ required this.id, required this.name, required this.mass, required this.kcalPer100, required this.waitingForNetwork, required this.isSelected, }); set selected(bool selected) => isSelected = selected; bool get selected => isSelected; @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,$kcalPer100'; } }