calorimeter/lib/food_entry/food_entry_bloc.dart

353 lines
10 KiB
Dart
Raw Permalink Normal View History

2024-12-07 12:29:34 +00:00
/* SPDX-License-Identifier: GPL-3.0-or-later */
2024-12-07 12:39:11 +00:00
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_scan/food_fact_lookup.dart';
2024-12-22 17:13:29 +00:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
2024-09-06 17:00:25 +00:00
import 'package:calorimeter/storage/storage.dart';
2024-12-22 17:13:29 +00:00
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2024-06-09 12:42:17 +00:00
import 'package:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
final GlobalEntryState initialState;
final FoodStorage storage;
2024-06-09 17:06:10 +00:00
FoodEntryBloc({required this.initialState, required this.storage})
: super(initialState) {
on<PageBeingInitialized>(handlePageBeingInitialized);
on<FoodEntryEvent>(handleFoodEntryEvent);
2024-09-24 15:23:01 +00:00
on<FoodChangedEvent>(handleFoodChangedEvent);
on<FoodDeletionEvent>(handleDeleteFoodEvent);
2024-12-22 17:13:29 +00:00
on<BarcodeAboutToBeScanned>(handleBarcodeScannedEvent);
on<FoodEntryTapped>(handleFoodEntryTapped);
}
void handlePageBeingInitialized(
PageBeingInitialized event, Emitter<GlobalEntryState> emit) async {
var newList = await storage.getEntriesForDate(event.forDate);
state.foodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: state.foodEntries));
}
void handleFoodEntryEvent(
FoodEntryEvent event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
entriesForDate ??= [];
entriesForDate.add(event.entry);
2024-06-09 17:06:10 +00:00
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.entry);
2024-06-09 17:06:10 +00:00
// this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
}
2024-06-09 12:42:17 +00:00
2024-09-24 15:23:01 +00:00
void handleFoodChangedEvent(
FoodChangedEvent event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var index = entriesForDate.indexWhere((entry) {
2024-09-24 15:23:01 +00:00
return entry.id == event.newEntry.id;
});
entriesForDate.removeAt(index);
entriesForDate.insert(index, event.newEntry);
2024-09-24 15:23:01 +00:00
await storage.writeEntriesForDate(event.forDate, entriesForDate);
2024-09-24 15:23:01 +00:00
storage.addFoodEntryToLookupDatabase(event.newEntry);
// this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
2024-09-24 15:23:01 +00:00
}
void handleDeleteFoodEvent(
FoodDeletionEvent event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
entriesForDate.removeWhere((entry) => entry.id == event.entryID);
2024-06-09 17:06:10 +00:00
await storage.writeEntriesForDate(event.forDate, entriesForDate);
2024-06-09 17:06:10 +00:00
// this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
2024-06-09 12:42:17 +00:00
}
2024-06-09 17:06:10 +00:00
void handleBarcodeScannedEvent(
2024-12-22 17:13:29 +00:00
BarcodeAboutToBeScanned event, Emitter<GlobalEntryState> emit) async {
ScanResult scanResult = ScanResult();
try {
scanResult = await BarcodeScanner.scan();
} on PlatformException catch (e) {
if (e.code == BarcodeScanner.cameraAccessDenied) {
emit(GlobalEntryState(
foodEntries: state.foodEntries,
appError:
GlobalAppError(GlobalAppErrorType.errCameraPermissionDenied)));
}
return;
}
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var client = FoodFactLookupClient();
if (scanResult.type == ResultType.Cancelled) {
return;
}
if (scanResult.type == ResultType.Error) {
emit(GlobalEntryState(
foodEntries: state.foodEntries,
2024-12-22 17:13:29 +00:00
appError: GlobalAppError(GlobalAppErrorType.errGeneralError)));
return;
}
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
var newEntryWaiting = FoodEntryState(
kcalPer100: 0,
name: "",
mass: 0,
waitingForNetwork: true,
isSelected: false,
);
2024-12-22 17:13:29 +00:00
entriesForDate.add(newEntryWaiting);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
await responseFuture.then((response) async {
var index = entriesForDate
.indexWhere((entryState) => entryState.id == newEntryWaiting.id);
// element not found (was deleted previously)
if (index == -1) {
return;
}
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(
foodEntries: newFoodEntries,
2024-12-22 17:13:29 +00:00
appError: GlobalAppError(GlobalAppErrorType.errbarcodeNotFound)));
return;
}
if (response.status ==
FoodFactResponseStatus.foodFactServerNotReachable) {
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(
foodEntries: newFoodEntries,
2024-12-22 17:13:29 +00:00
appError:
GlobalAppError(GlobalAppErrorType.errServerNotReachable)));
return;
}
var newEntryFinishedWaiting = FoodEntryState(
name: response.food?.name ?? "",
mass: response.food?.mass ?? 0,
kcalPer100: response.food?.kcalPer100g ?? 0,
waitingForNetwork: false,
isSelected: false,
);
entriesForDate.removeAt(index);
entriesForDate.insert(index, newEntryFinishedWaiting);
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting);
var entriesFromStorage = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesFromStorage});
emit(GlobalEntryState(foodEntries: newFoodEntries));
});
2024-06-09 17:06:10 +00:00
}
void handleFoodEntryTapped(
FoodEntryTapped event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var oldStateOfTappedEntry = event.entry.isSelected;
for (var entry in entriesForDate) {
entry.isSelected = false;
}
var selectedEntry = entriesForDate.firstWhere((entry) {
return entry.id == event.entry.id;
});
selectedEntry.isSelected = !oldStateOfTappedEntry;
emit(GlobalEntryState(foodEntries: state.foodEntries));
}
}
class FoodEvent {
final DateTime forDate;
FoodEvent({required this.forDate});
}
class PageBeingInitialized extends FoodEvent {
PageBeingInitialized({required super.forDate});
}
2024-06-09 12:42:17 +00:00
class FoodEntryEvent extends FoodEvent {
final FoodEntryState entry;
2024-06-09 17:06:10 +00:00
FoodEntryEvent({required this.entry, required super.forDate});
}
2024-09-24 15:23:01 +00:00
class FoodChangedEvent extends FoodEvent {
final FoodEntryState newEntry;
FoodChangedEvent({required this.newEntry, required super.forDate});
2024-09-24 15:23:01 +00:00
}
2024-06-09 12:42:17 +00:00
class FoodDeletionEvent extends FoodEvent {
final String entryID;
2024-06-09 17:06:10 +00:00
FoodDeletionEvent({required this.entryID, required super.forDate});
2024-06-09 17:06:10 +00:00
}
2024-12-22 17:13:29 +00:00
class BarcodeAboutToBeScanned extends FoodEvent {
BarcodeAboutToBeScanned({required super.forDate});
2024-06-09 12:42:17 +00:00
}
class FoodEntryTapped extends FoodEvent {
final FoodEntryState entry;
FoodEntryTapped({required this.entry, required super.forDate});
}
2024-12-22 17:13:29 +00:00
class PermissionException extends FoodEvent {
PermissionException({required super.forDate});
}
/// This is the state for one date/page
class GlobalEntryState {
final Map<DateTime, List<FoodEntryState>> foodEntries;
2024-12-22 17:13:29 +00:00
final GlobalAppError? appError;
2024-12-22 17:13:29 +00:00
GlobalEntryState({required this.foodEntries, this.appError});
factory GlobalEntryState.init() {
return GlobalEntryState(foodEntries: {});
}
static from(GlobalEntryState state) {
return GlobalEntryState(foodEntries: state.foodEntries);
}
bool addEntry(FoodEntryState entry, DateTime date) {
var list = foodEntries[date];
if (list == null) {
return false;
}
list.add(entry);
return true;
}
}
class FoodEntryState {
final String id;
final String name;
final int mass;
2024-09-24 15:23:01 +00:00
final int kcalPer100;
final bool waitingForNetwork;
bool isSelected;
2024-09-24 15:23:01 +00:00
factory FoodEntryState({
required name,
required mass,
required kcalPer100,
required waitingForNetwork,
required isSelected,
2024-09-24 15:23:01 +00:00
}) {
return FoodEntryState.withID(
id: const Uuid().v1(),
name: name,
mass: mass,
kcalPer100: kcalPer100,
waitingForNetwork: waitingForNetwork,
isSelected: isSelected,
2024-09-24 15:23:01 +00:00
);
}
FoodEntryState.withID({
required this.id,
required this.name,
required this.mass,
2024-09-24 15:23:01 +00:00
required this.kcalPer100,
required this.waitingForNetwork,
required this.isSelected,
2024-09-24 15:23:01 +00:00
});
2024-06-09 17:06:10 +00:00
@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
2024-09-24 15:23:01 +00:00
return '$id,"$name",$mass,$kcalPer100';
2024-06-09 17:06:10 +00:00
}
}
2024-12-22 17:13:29 +00:00
enum GlobalAppErrorType {
errGeneralError,
errbarcodeNotFound,
errServerNotReachable,
errCameraPermissionDenied
}
class GlobalAppError {
final GlobalAppErrorType type;
GlobalAppError(this.type);
String toErrorString(BuildContext context) {
switch (type) {
case GlobalAppErrorType.errGeneralError:
return AppLocalizations.of(context)!.errGeneralBarcodeError;
case GlobalAppErrorType.errbarcodeNotFound:
return AppLocalizations.of(context)!.errBarcodeNotFound;
case GlobalAppErrorType.errServerNotReachable:
return AppLocalizations.of(context)!.errServerNotReachable;
case GlobalAppErrorType.errCameraPermissionDenied:
return AppLocalizations.of(context)!.errPermissionNotGranted;
}
}
}