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> */
|
2024-09-09 20:41:48 +00:00
|
|
|
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';
|
2024-05-29 22:58:26 +00:00
|
|
|
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';
|
2024-05-29 22:58:26 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
|
|
|
|
final GlobalEntryState initialState;
|
2024-09-04 20:47:32 +00:00
|
|
|
final FoodStorage storage;
|
2024-06-09 17:06:10 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
FoodEntryBloc({required this.initialState, required this.storage})
|
2024-06-11 17:05:42 +00:00
|
|
|
: super(initialState) {
|
2024-10-07 22:58:56 +00:00
|
|
|
on<PageBeingInitialized>(handlePageBeingInitialized);
|
2024-09-04 20:47:32 +00:00
|
|
|
on<FoodEntryEvent>(handleFoodEntryEvent);
|
2024-09-24 15:23:01 +00:00
|
|
|
on<FoodChangedEvent>(handleFoodChangedEvent);
|
2024-09-09 20:41:48 +00:00
|
|
|
on<FoodDeletionEvent>(handleDeleteFoodEvent);
|
2024-12-22 17:13:29 +00:00
|
|
|
on<BarcodeAboutToBeScanned>(handleBarcodeScannedEvent);
|
2024-09-25 15:33:40 +00:00
|
|
|
on<FoodEntryTapped>(handleFoodEntryTapped);
|
2024-05-29 22:58:26 +00:00
|
|
|
}
|
2024-10-07 22:58:56 +00:00
|
|
|
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));
|
|
|
|
}
|
2024-05-29 22:58:26 +00:00
|
|
|
|
2024-09-04 20:47:32 +00:00
|
|
|
void handleFoodEntryEvent(
|
2024-10-07 22:58:56 +00:00
|
|
|
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
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
await storage.writeEntriesForDate(event.forDate, entriesForDate);
|
2024-09-04 20:47:32 +00:00
|
|
|
storage.addFoodEntryToLookupDatabase(event.entry);
|
2024-06-09 17:06:10 +00:00
|
|
|
|
2024-10-07 22:58:56 +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-05-29 22:58:26 +00:00
|
|
|
}
|
2024-06-09 12:42:17 +00:00
|
|
|
|
2024-09-24 15:23:01 +00:00
|
|
|
void handleFoodChangedEvent(
|
2024-10-07 22:58:56 +00:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
entriesForDate.removeAt(index);
|
|
|
|
entriesForDate.insert(index, event.newEntry);
|
2024-09-24 15:23:01 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
await storage.writeEntriesForDate(event.forDate, entriesForDate);
|
2024-09-24 15:23:01 +00:00
|
|
|
storage.addFoodEntryToLookupDatabase(event.newEntry);
|
|
|
|
|
2024-10-07 22:58:56 +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-09-24 15:23:01 +00:00
|
|
|
}
|
|
|
|
|
2024-09-09 20:41:48 +00:00
|
|
|
void handleDeleteFoodEvent(
|
2024-10-07 22:58:56 +00:00
|
|
|
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
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
await storage.writeEntriesForDate(event.forDate, entriesForDate);
|
2024-06-09 17:06:10 +00:00
|
|
|
|
2024-10-07 22:58:56 +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
|
|
|
|
2024-09-09 20:41:48 +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;
|
|
|
|
}
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
var entriesForDate = state.foodEntries[event.forDate];
|
|
|
|
if (entriesForDate == null) return;
|
|
|
|
|
2024-09-09 20:41:48 +00:00
|
|
|
var client = FoodFactLookupClient();
|
|
|
|
|
|
|
|
if (scanResult.type == ResultType.Cancelled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (scanResult.type == ResultType.Error) {
|
2024-10-07 22:58:56 +00:00
|
|
|
emit(GlobalEntryState(
|
2024-09-09 20:41:48 +00:00
|
|
|
foodEntries: state.foodEntries,
|
2024-12-22 17:13:29 +00:00
|
|
|
appError: GlobalAppError(GlobalAppErrorType.errGeneralError)));
|
2024-09-09 20:41:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
|
|
|
|
|
|
|
|
var newEntryWaiting = FoodEntryState(
|
2024-09-25 15:33:40 +00:00
|
|
|
kcalPer100: 0,
|
|
|
|
name: "",
|
|
|
|
mass: 0,
|
|
|
|
waitingForNetwork: true,
|
|
|
|
isSelected: false,
|
|
|
|
);
|
2024-12-22 17:13:29 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
entriesForDate.add(newEntryWaiting);
|
|
|
|
var newFoodEntries = state.foodEntries;
|
|
|
|
newFoodEntries.addAll({event.forDate: entriesForDate});
|
|
|
|
emit(GlobalEntryState(foodEntries: newFoodEntries));
|
2024-09-09 20:41:48 +00:00
|
|
|
|
2024-10-02 14:22:33 +00:00
|
|
|
await responseFuture.then((response) async {
|
2024-10-07 22:58:56 +00:00
|
|
|
var index = entriesForDate
|
2024-09-25 15:33:40 +00:00
|
|
|
.indexWhere((entryState) => entryState.id == newEntryWaiting.id);
|
|
|
|
|
|
|
|
// element not found (was deleted previously)
|
|
|
|
if (index == -1) {
|
|
|
|
return;
|
|
|
|
}
|
2024-09-09 20:41:48 +00:00
|
|
|
|
|
|
|
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
|
2024-10-07 22:58:56 +00:00
|
|
|
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
|
|
|
|
var newFoodEntries = state.foodEntries;
|
|
|
|
newFoodEntries.addAll({event.forDate: entriesForDate});
|
2024-10-03 21:21:13 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
emit(GlobalEntryState(
|
|
|
|
foodEntries: newFoodEntries,
|
2024-12-22 17:13:29 +00:00
|
|
|
appError: GlobalAppError(GlobalAppErrorType.errbarcodeNotFound)));
|
2024-09-09 20:41:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (response.status ==
|
|
|
|
FoodFactResponseStatus.foodFactServerNotReachable) {
|
2024-10-07 22:58:56 +00:00
|
|
|
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
|
|
|
|
var newFoodEntries = state.foodEntries;
|
|
|
|
newFoodEntries.addAll({event.forDate: entriesForDate});
|
2024-10-03 21:21:13 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
emit(GlobalEntryState(
|
|
|
|
foodEntries: newFoodEntries,
|
2024-12-22 17:13:29 +00:00
|
|
|
appError:
|
|
|
|
GlobalAppError(GlobalAppErrorType.errServerNotReachable)));
|
2024-09-09 20:41:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var newEntryFinishedWaiting = FoodEntryState(
|
|
|
|
name: response.food?.name ?? "",
|
|
|
|
mass: response.food?.mass ?? 0,
|
2024-12-23 18:38:45 +00:00
|
|
|
kcalPer100: response.food?.kcalPer100g ?? 0,
|
2024-09-09 20:41:48 +00:00
|
|
|
waitingForNetwork: false,
|
2024-09-25 15:33:40 +00:00
|
|
|
isSelected: false,
|
2024-09-09 20:41:48 +00:00
|
|
|
);
|
2024-09-25 15:33:40 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
entriesForDate.removeAt(index);
|
|
|
|
entriesForDate.insert(index, newEntryFinishedWaiting);
|
2024-10-02 14:22:33 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
await storage.writeEntriesForDate(event.forDate, entriesForDate);
|
2024-10-02 14:22:33 +00:00
|
|
|
storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting);
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
var entriesFromStorage = await storage.getEntriesForDate(event.forDate);
|
|
|
|
var newFoodEntries = state.foodEntries;
|
|
|
|
newFoodEntries.addAll({event.forDate: entriesFromStorage});
|
|
|
|
|
|
|
|
emit(GlobalEntryState(foodEntries: newFoodEntries));
|
2024-09-09 20:41:48 +00:00
|
|
|
});
|
2024-06-09 17:06:10 +00:00
|
|
|
}
|
2024-09-25 15:33:40 +00:00
|
|
|
|
|
|
|
void handleFoodEntryTapped(
|
2024-10-07 22:58:56 +00:00
|
|
|
FoodEntryTapped event, Emitter<GlobalEntryState> emit) async {
|
|
|
|
var entriesForDate = state.foodEntries[event.forDate];
|
|
|
|
if (entriesForDate == null) return;
|
|
|
|
|
2024-09-25 16:20:50 +00:00
|
|
|
var oldStateOfTappedEntry = event.entry.isSelected;
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
for (var entry in entriesForDate) {
|
2024-09-25 15:33:40 +00:00
|
|
|
entry.isSelected = false;
|
|
|
|
}
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
var selectedEntry = entriesForDate.firstWhere((entry) {
|
2024-09-25 16:20:50 +00:00
|
|
|
return entry.id == event.entry.id;
|
2024-09-25 15:33:40 +00:00
|
|
|
});
|
|
|
|
|
2024-09-25 16:20:50 +00:00
|
|
|
selectedEntry.isSelected = !oldStateOfTappedEntry;
|
2024-09-25 15:33:40 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
emit(GlobalEntryState(foodEntries: state.foodEntries));
|
2024-09-25 15:33:40 +00:00
|
|
|
}
|
2024-05-29 22:58:26 +00:00
|
|
|
}
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
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 {
|
2024-09-09 20:41:48 +00:00
|
|
|
final FoodEntryState entry;
|
2024-06-09 17:06:10 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
FoodEntryEvent({required this.entry, required super.forDate});
|
2024-05-29 22:58:26 +00:00
|
|
|
}
|
|
|
|
|
2024-09-24 15:23:01 +00:00
|
|
|
class FoodChangedEvent extends FoodEvent {
|
|
|
|
final FoodEntryState newEntry;
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
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
|
|
|
|
2024-10-07 22:58:56 +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
|
|
|
}
|
|
|
|
|
2024-09-25 15:33:40 +00:00
|
|
|
class FoodEntryTapped extends FoodEvent {
|
2024-09-25 16:20:50 +00:00
|
|
|
final FoodEntryState entry;
|
2024-09-25 15:33:40 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
FoodEntryTapped({required this.entry, required super.forDate});
|
2024-09-25 15:33:40 +00:00
|
|
|
}
|
|
|
|
|
2024-12-22 17:13:29 +00:00
|
|
|
class PermissionException extends FoodEvent {
|
|
|
|
PermissionException({required super.forDate});
|
|
|
|
}
|
|
|
|
|
2024-09-09 20:41:48 +00:00
|
|
|
/// This is the state for one date/page
|
2024-10-07 22:58:56 +00:00
|
|
|
class GlobalEntryState {
|
|
|
|
final Map<DateTime, List<FoodEntryState>> foodEntries;
|
2024-12-22 17:13:29 +00:00
|
|
|
final GlobalAppError? appError;
|
2024-05-29 22:58:26 +00:00
|
|
|
|
2024-12-22 17:13:29 +00:00
|
|
|
GlobalEntryState({required this.foodEntries, this.appError});
|
2024-05-29 22:58:26 +00:00
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
factory GlobalEntryState.init() {
|
|
|
|
return GlobalEntryState(foodEntries: {});
|
2024-05-29 22:58:26 +00:00
|
|
|
}
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
static from(GlobalEntryState state) {
|
|
|
|
return GlobalEntryState(foodEntries: state.foodEntries);
|
2024-05-29 22:58:26 +00:00
|
|
|
}
|
|
|
|
|
2024-10-07 22:58:56 +00:00
|
|
|
bool addEntry(FoodEntryState entry, DateTime date) {
|
|
|
|
var list = foodEntries[date];
|
|
|
|
|
|
|
|
if (list == null) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
list.add(entry);
|
|
|
|
|
|
|
|
return true;
|
2024-05-29 22:58:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-09 20:41:48 +00:00
|
|
|
class FoodEntryState {
|
2024-10-07 22:58:56 +00:00
|
|
|
final String id;
|
2024-05-29 22:58:26 +00:00
|
|
|
final String name;
|
2024-09-09 20:41:48 +00:00
|
|
|
final int mass;
|
2024-09-24 15:23:01 +00:00
|
|
|
final int kcalPer100;
|
2024-09-09 20:41:48 +00:00
|
|
|
final bool waitingForNetwork;
|
2024-09-25 15:33:40 +00:00
|
|
|
bool isSelected;
|
2024-05-29 22:58:26 +00:00
|
|
|
|
2024-09-24 15:23:01 +00:00
|
|
|
factory FoodEntryState({
|
|
|
|
required name,
|
|
|
|
required mass,
|
|
|
|
required kcalPer100,
|
|
|
|
required waitingForNetwork,
|
2024-09-25 15:33:40 +00:00
|
|
|
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,
|
2024-09-25 15:33:40 +00:00
|
|
|
isSelected: isSelected,
|
2024-09-24 15:23:01 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
FoodEntryState.withID({
|
|
|
|
required this.id,
|
2024-05-29 22:58:26 +00:00
|
|
|
required this.name,
|
|
|
|
required this.mass,
|
2024-09-24 15:23:01 +00:00
|
|
|
required this.kcalPer100,
|
2024-09-09 20:41:48 +00:00
|
|
|
required this.waitingForNetwork,
|
2024-09-25 15:33:40 +00:00
|
|
|
required this.isSelected,
|
2024-09-24 15:23:01 +00:00
|
|
|
});
|
2024-06-09 17:06:10 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() {
|
2024-09-13 20:35:32 +00:00
|
|
|
//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-05-29 22:58:26 +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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|