Marco
cb18e1d1f0
1. Show PerDate widgets inside of an PageView 2. Introduce GoRouter so we can intercept back button taps with BackButtonListener 3. Implement rudimentary navigation 4. Fix bug that still showed a spinner event when the barcode was not found.
249 lines
6.7 KiB
Dart
249 lines
6.7 KiB
Dart
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<FoodEvent, PageState> {
|
|
final PageState initialState;
|
|
final FoodStorage storage;
|
|
final DateTime forDate;
|
|
|
|
FoodEntryBloc(
|
|
{required this.initialState,
|
|
required this.forDate,
|
|
required this.storage})
|
|
: super(initialState) {
|
|
on<FoodEntryEvent>(handleFoodEntryEvent);
|
|
on<FoodChangedEvent>(handleFoodChangedEvent);
|
|
on<FoodDeletionEvent>(handleDeleteFoodEvent);
|
|
on<BarcodeScanned>(handleBarcodeScannedEvent);
|
|
on<FoodEntryTapped>(handleFoodEntryTapped);
|
|
}
|
|
|
|
void handleFoodEntryEvent(
|
|
FoodEntryEvent event, Emitter<PageState> 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<PageState> 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<PageState> 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<PageState> 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<FoodEntryState> 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) async {
|
|
var index = newList
|
|
.indexWhere((entryState) => entryState.id == newEntryWaiting.id);
|
|
|
|
// element not found (was deleted previously)
|
|
if (index == -1) {
|
|
return;
|
|
}
|
|
|
|
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
|
|
List<FoodEntryState> listWithEntryRemoved =
|
|
List.from(state.foodEntries);
|
|
listWithEntryRemoved
|
|
.removeWhere((entry) => entry.id == newEntryWaiting.id);
|
|
|
|
emit(PageState(
|
|
foodEntries: listWithEntryRemoved,
|
|
errorString: "Barcode konnte nicht gefunden werden."));
|
|
return;
|
|
}
|
|
if (response.status ==
|
|
FoodFactResponseStatus.foodFactServerNotReachable) {
|
|
List<FoodEntryState> listWithEntryRemoved =
|
|
List.from(state.foodEntries);
|
|
listWithEntryRemoved
|
|
.removeWhere((entry) => entry.id == newEntryWaiting.id);
|
|
|
|
emit(PageState(
|
|
foodEntries: listWithEntryRemoved,
|
|
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);
|
|
|
|
await storage.writeEntriesForDate(forDate, newList);
|
|
storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting);
|
|
|
|
emit(PageState(foodEntries: newList));
|
|
});
|
|
}
|
|
|
|
void handleFoodEntryTapped(
|
|
FoodEntryTapped event, Emitter<PageState> emit) async {
|
|
var oldStateOfTappedEntry = event.entry.isSelected;
|
|
|
|
for (var entry in state.foodEntries) {
|
|
entry.isSelected = false;
|
|
}
|
|
|
|
var selectedEntry = state.foodEntries.firstWhere((entry) {
|
|
return entry.id == event.entry.id;
|
|
});
|
|
|
|
selectedEntry.isSelected = !oldStateOfTappedEntry;
|
|
|
|
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<ScanResult> scanResultFuture;
|
|
|
|
BarcodeScanned({required this.scanResultFuture});
|
|
}
|
|
|
|
class FoodEntryTapped extends FoodEvent {
|
|
final FoodEntryState entry;
|
|
|
|
FoodEntryTapped({required this.entry});
|
|
}
|
|
|
|
/// This is the state for one date/page
|
|
class PageState {
|
|
final List<FoodEntryState> 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,
|
|
});
|
|
|
|
@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';
|
|
}
|
|
}
|