From ce373404ad9fb89b1b0e17a09a8593c83d37d9cc Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 8 Oct 2024 00:58:56 +0200 Subject: [PATCH] Bloc is now global and responsible for all dates --- lib/food_entry/food_entry_bloc.dart | 187 ++++++++++++++++++---------- lib/main.dart | 21 ++++ lib/perdate/entry_list.dart | 22 ++-- lib/perdate/perdate_pageview.dart | 2 +- lib/perdate/perdate_widget.dart | 112 +++++++---------- 5 files changed, 204 insertions(+), 140 deletions(-) diff --git a/lib/food_entry/food_entry_bloc.dart b/lib/food_entry/food_entry_bloc.dart index 7e4718a..8e84075 100644 --- a/lib/food_entry/food_entry_bloc.dart +++ b/lib/food_entry/food_entry_bloc.dart @@ -4,61 +4,96 @@ 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; +class FoodEntryBloc extends Bloc { + final GlobalEntryState initialState; final FoodStorage storage; - final DateTime forDate; - FoodEntryBloc( - {required this.initialState, - required this.forDate, - required this.storage}) + FoodEntryBloc({required this.initialState, required this.storage}) : super(initialState) { + on(handlePageBeingInitialized); on(handleFoodEntryEvent); on(handleFoodChangedEvent); on(handleDeleteFoodEvent); on(handleBarcodeScannedEvent); on(handleFoodEntryTapped); } + void handlePageBeingInitialized( + PageBeingInitialized event, Emitter 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 emit) async { - PageState newState = PageState.from(state); - newState.addEntry(event.entry); + FoodEntryEvent event, Emitter emit) async { + var entriesForDate = state.foodEntries[event.forDate]; + entriesForDate ??= []; - await storage.writeEntriesForDate(forDate, newState.foodEntries); + entriesForDate.add(event.entry); + + await storage.writeEntriesForDate(event.forDate, entriesForDate); storage.addFoodEntryToLookupDatabase(event.entry); - emit(newState); + // 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)); } void handleFoodChangedEvent( - FoodChangedEvent event, Emitter emit) async { - var entries = state.foodEntries; - var index = entries.indexWhere((entry) { + FoodChangedEvent event, Emitter emit) async { + var entriesForDate = state.foodEntries[event.forDate]; + if (entriesForDate == null) return; + + var index = entriesForDate.indexWhere((entry) { return entry.id == event.newEntry.id; }); - entries.removeAt(index); - entries.insert(index, event.newEntry); + entriesForDate.removeAt(index); + entriesForDate.insert(index, event.newEntry); - await storage.writeEntriesForDate(forDate, entries); + await storage.writeEntriesForDate(event.forDate, entriesForDate); storage.addFoodEntryToLookupDatabase(event.newEntry); - emit(PageState(foodEntries: entries)); + // 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)); } void handleDeleteFoodEvent( - FoodDeletionEvent event, Emitter emit) async { - state.foodEntries.removeWhere((entry) => entry.id == event.entryID); + FoodDeletionEvent event, Emitter emit) async { + var entriesForDate = state.foodEntries[event.forDate]; + if (entriesForDate == null) return; - await storage.writeEntriesForDate(forDate, state.foodEntries); + entriesForDate.removeWhere((entry) => entry.id == event.entryID); - emit(PageState.from(state)); + await storage.writeEntriesForDate(event.forDate, entriesForDate); + + // 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)); } void handleBarcodeScannedEvent( - BarcodeScanned event, Emitter emit) async { + BarcodeScanned event, Emitter emit) async { + var entriesForDate = state.foodEntries[event.forDate]; + if (entriesForDate == null) return; + var client = FoodFactLookupClient(); var scanResult = await event.scanResultFuture; @@ -66,14 +101,13 @@ class FoodEntryBloc extends Bloc { return; } if (scanResult.type == ResultType.Error) { - emit(PageState( + emit(GlobalEntryState( 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: "", @@ -81,11 +115,13 @@ class FoodEntryBloc extends Bloc { waitingForNetwork: true, isSelected: false, ); - newList.add(newEntryWaiting); - emit(PageState(foodEntries: newList)); + entriesForDate.add(newEntryWaiting); + var newFoodEntries = state.foodEntries; + newFoodEntries.addAll({event.forDate: entriesForDate}); + emit(GlobalEntryState(foodEntries: newFoodEntries)); await responseFuture.then((response) async { - var index = newList + var index = entriesForDate .indexWhere((entryState) => entryState.id == newEntryWaiting.id); // element not found (was deleted previously) @@ -94,25 +130,23 @@ class FoodEntryBloc extends Bloc { } if (response.status == FoodFactResponseStatus.barcodeNotFound) { - List listWithEntryRemoved = - List.from(state.foodEntries); - listWithEntryRemoved - .removeWhere((entry) => entry.id == newEntryWaiting.id); + entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id); + var newFoodEntries = state.foodEntries; + newFoodEntries.addAll({event.forDate: entriesForDate}); - emit(PageState( - foodEntries: listWithEntryRemoved, + emit(GlobalEntryState( + foodEntries: newFoodEntries, errorString: "Barcode konnte nicht gefunden werden.")); return; } if (response.status == FoodFactResponseStatus.foodFactServerNotReachable) { - List listWithEntryRemoved = - List.from(state.foodEntries); - listWithEntryRemoved - .removeWhere((entry) => entry.id == newEntryWaiting.id); + entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id); + var newFoodEntries = state.foodEntries; + newFoodEntries.addAll({event.forDate: entriesForDate}); - emit(PageState( - foodEntries: listWithEntryRemoved, + emit(GlobalEntryState( + foodEntries: newFoodEntries, errorString: "OpenFoodFacts-Server konnte nicht erreicht werden.")); return; } @@ -125,91 +159,114 @@ class FoodEntryBloc extends Bloc { isSelected: false, ); - newList.removeAt(index); - newList.insert(index, newEntryFinishedWaiting); + entriesForDate.removeAt(index); + entriesForDate.insert(index, newEntryFinishedWaiting); - await storage.writeEntriesForDate(forDate, newList); + await storage.writeEntriesForDate(event.forDate, entriesForDate); storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting); - emit(PageState(foodEntries: newList)); + var entriesFromStorage = await storage.getEntriesForDate(event.forDate); + var newFoodEntries = state.foodEntries; + newFoodEntries.addAll({event.forDate: entriesFromStorage}); + + emit(GlobalEntryState(foodEntries: newFoodEntries)); }); } void handleFoodEntryTapped( - FoodEntryTapped event, Emitter emit) async { + FoodEntryTapped event, Emitter emit) async { + var entriesForDate = state.foodEntries[event.forDate]; + if (entriesForDate == null) return; + var oldStateOfTappedEntry = event.entry.isSelected; - for (var entry in state.foodEntries) { + for (var entry in entriesForDate) { entry.isSelected = false; } - var selectedEntry = state.foodEntries.firstWhere((entry) { + var selectedEntry = entriesForDate.firstWhere((entry) { return entry.id == event.entry.id; }); selectedEntry.isSelected = !oldStateOfTappedEntry; - emit(PageState(foodEntries: state.foodEntries)); + emit(GlobalEntryState(foodEntries: state.foodEntries)); } } -class FoodEvent {} +class FoodEvent { + final DateTime forDate; + + FoodEvent({required this.forDate}); +} + +class PageBeingInitialized extends FoodEvent { + PageBeingInitialized({required super.forDate}); +} class FoodEntryEvent extends FoodEvent { final FoodEntryState entry; - FoodEntryEvent({required this.entry}); + FoodEntryEvent({required this.entry, required super.forDate}); } class FoodChangedEvent extends FoodEvent { final FoodEntryState newEntry; - FoodChangedEvent({required this.newEntry}); + FoodChangedEvent({required this.newEntry, required super.forDate}); } class FoodDeletionEvent extends FoodEvent { final String entryID; - FoodDeletionEvent({required this.entryID}); + FoodDeletionEvent({required this.entryID, required super.forDate}); } class BarcodeScanned extends FoodEvent { final Future scanResultFuture; - BarcodeScanned({required this.scanResultFuture}); + BarcodeScanned({required this.scanResultFuture, required super.forDate}); } class FoodEntryTapped extends FoodEvent { final FoodEntryState entry; - FoodEntryTapped({required this.entry}); + FoodEntryTapped({required this.entry, required super.forDate}); } /// This is the state for one date/page -class PageState { - final List foodEntries; +class GlobalEntryState { + final Map> foodEntries; final String? errorString; - PageState({required this.foodEntries, this.errorString}); + GlobalEntryState({required this.foodEntries, this.errorString}); - factory PageState.init() { - return PageState(foodEntries: []); + factory GlobalEntryState.init() { + return GlobalEntryState(foodEntries: {}); } - static from(PageState state) { - return PageState(foodEntries: state.foodEntries); + static from(GlobalEntryState state) { + return GlobalEntryState(foodEntries: state.foodEntries); } - void addEntry(FoodEntryState entry) { - foodEntries.add(entry); + 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; final int kcalPer100; - final String id; final bool waitingForNetwork; bool isSelected; diff --git a/lib/main.dart b/lib/main.dart index 4d24433..7166f6a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/perdate/perdate_pageview.dart'; import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/utils/settings_bloc.dart'; @@ -7,6 +8,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:go_router/go_router.dart'; +List entriesForToday = []; +DateTime timeNow = DateTime.now(); + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -14,6 +18,16 @@ void main() async { var storage = await FoodStorage.create(); await storage.buildFoodLookupDatabase(); + timeNow = DateTime.now().copyWith( + hour: 0, + isUtc: true, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0); + + entriesForToday = await storage.getEntriesForDate(timeNow); + var kcalLimit = await storage.readLimit(); var brightness = await storage.readBrightness(); @@ -42,6 +56,13 @@ class MainApp extends StatelessWidget { return SafeArea( child: MultiBlocProvider( providers: [ + BlocProvider( + create: (context) => FoodEntryBloc( + storage: storage, + initialState: + GlobalEntryState(foodEntries: {timeNow: entriesForToday}), + ), + ), BlocProvider( create: (context) => SettingsDataBloc( SettingsState(kcalLimit: kcalLimit), diff --git a/lib/perdate/entry_list.dart b/lib/perdate/entry_list.dart index 5208ea8..c592178 100644 --- a/lib/perdate/entry_list.dart +++ b/lib/perdate/entry_list.dart @@ -6,9 +6,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class FoodEntryList extends StatelessWidget { final List entries; + final DateTime date; const FoodEntryList({ required this.entries, + required this.date, super.key, }); @@ -25,7 +27,7 @@ class FoodEntryList extends StatelessWidget { onAdd: (context, entry) { context .read() - .add(FoodEntryEvent(entry: entry)); + .add(FoodEntryEvent(entry: entry, forDate: date)); }, ), const SizedBox(height: 75), @@ -40,19 +42,19 @@ class FoodEntryList extends StatelessWidget { key: ValueKey(entries[entryIndex].id), entry: entries[entryIndex], onDelete: (_, id) { - context.read().add(FoodDeletionEvent( - entryID: id, - )); + context + .read() + .add(FoodDeletionEvent(entryID: id, forDate: date)); }, onChange: (_, changedEntry) { - context - .read() - .add(FoodChangedEvent(newEntry: changedEntry)); + context.read().add( + FoodChangedEvent(newEntry: changedEntry, forDate: date), + ); }, onTap: (_, tappedEntry) { - context - .read() - .add(FoodEntryTapped(entry: tappedEntry)); + context.read().add( + FoodEntryTapped(entry: tappedEntry, forDate: date), + ); }, ), const Divider(), diff --git a/lib/perdate/perdate_pageview.dart b/lib/perdate/perdate_pageview.dart index c444391..f076012 100644 --- a/lib/perdate/perdate_pageview.dart +++ b/lib/perdate/perdate_pageview.dart @@ -61,7 +61,7 @@ class _PerDatePageviewState extends State { return PerDateWidget( key: ValueKey(dateToBuildWidgetFor.toString()), - date: dateToBuildWidgetFor, + date: dateToBuildWidgetFor.copyWith(isUtc: true), onDateSelected: (dateSelected) { if (dateSelected == null) return; diff --git a/lib/perdate/perdate_widget.dart b/lib/perdate/perdate_widget.dart index f34eafa..e5437c4 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -11,7 +11,6 @@ import 'package:calorimeter/utils/theme_switcher_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; class PerDateWidget extends StatefulWidget { final DateTime date; @@ -26,16 +25,13 @@ class PerDateWidget extends StatefulWidget { class _PerDateWidgetState extends State with AutomaticKeepAliveClientMixin { late FoodStorage storage; - late Future> entriesFuture; List entries = []; @override void initState() { - storage = FoodStorage.getInstance(); - entriesFuture = storage.getEntriesForDate(widget.date); - entriesFuture.then((val) { - entries = val; - }); + context + .read() + .add(PageBeingInitialized(forDate: widget.date)); super.initState(); } @@ -48,63 +44,51 @@ class _PerDateWidgetState extends State Widget build(BuildContext context) { super.build(context); - return FutureBuilder( - future: entriesFuture, - builder: (context, snapshot) { - return snapshot.connectionState != ConnectionState.done - ? const Center(child: CircularProgressIndicator()) - : MultiProvider( - providers: [ - BlocProvider( - create: (context) => FoodEntryBloc( - initialState: PageState(foodEntries: entries), - storage: storage, - forDate: widget.date, - ), - ) - ], - 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)); - }, - ), - const SizedBox(width: 8), - CalendarFloatingButton( - startFromDate: widget.date, - onDateSelected: (dateSelected) { - widget.onDateSelected(dateSelected); - }, - ), - ]), - floatingActionButtonLocation: - FloatingActionButtonLocation.endDocked); - }, - ), - ); - }); + return BlocConsumer( + listener: (context, pageState) { + if (pageState.errorString != null) { + showNewSnackbarWith(context, pageState.errorString!); + } + }, + builder: (context, globalState) { + return Scaffold( + appBar: AppBar( + title: Text(DateFormat.yMMMMd('de').format(widget.date)), + actions: const [ThemeSwitcherButton()], + ), + body: FoodEntryList( + entries: globalState.foodEntries[widget.date] ?? [], + date: widget.date), + bottomNavigationBar: BottomAppBar( + shape: const RectangularNotchShape(), + color: Theme.of(context).colorScheme.secondary, + child: SumWidget( + foodEntries: globalState.foodEntries[widget.date] ?? [])), + drawer: const AppDrawer(), + floatingActionButton: OverflowBar(children: [ + ScanFoodFloatingButton( + onPressed: () { + var result = BarcodeScanner.scan(); + context.read().add( + BarcodeScanned( + scanResultFuture: result, + forDate: widget.date, + ), + ); + }, + ), + const SizedBox(width: 8), + CalendarFloatingButton( + startFromDate: widget.date, + onDateSelected: (dateSelected) { + widget.onDateSelected(dateSelected); + }, + ), + ]), + floatingActionButtonLocation: + FloatingActionButtonLocation.endDocked); + }, + ); } void showNewSnackbarWith(BuildContext context, String text) {