diff --git a/lib/main.dart b/lib/main.dart index 7166f6a..55a36b4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/perdate/perdate_pageview.dart'; +import 'package:calorimeter/perdate/perdate_pageview_controller.dart'; import 'package:calorimeter/storage/storage.dart'; +import 'package:calorimeter/utils/date_time_helper.dart'; import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/theme_bloc.dart'; import 'package:flutter/material.dart'; @@ -87,14 +89,8 @@ class MainApp extends StatelessWidget { GoRoute( path: '/', builder: (context, state) { - return PerDatePageview( - initalDate: DateTime.now().copyWith( - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - microsecond: 0, - ), + return PerDatePageViewController( + initialDate: DateTimeHelper.now(), ); }, ), diff --git a/lib/perdate/perdate_pageview.dart b/lib/perdate/perdate_pageview.dart index f076012..41fe57f 100644 --- a/lib/perdate/perdate_pageview.dart +++ b/lib/perdate/perdate_pageview.dart @@ -1,76 +1,46 @@ +import 'dart:developer'; + +import 'package:calorimeter/perdate/perdate_pageview_controller.dart'; import 'package:calorimeter/perdate/perdate_widget.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -class PerDatePageview extends StatefulWidget { +class PerDatePageView extends StatelessWidget { // this is the date for which the PerDate widget will be shown on screen // left of it will be yesterday's PerDate widget // right of it will be tomorrow's PerDate widget - final DateTime initalDate; - const PerDatePageview({required this.initalDate, super.key}); + final DateTime initialDate; + final PageController pageController; - @override - State createState() => _PerDatePageviewState(); -} - -class _PerDatePageviewState extends State { - late PageController pageController; - late DateTime displayedDate; - late List visitedIndexes = []; - final int initialOffset = 36500000; - - //TODO: that is just ugly - bool backButtonWasPressed = false; - - @override - void initState() { - super.initState(); - pageController = PageController(initialPage: initialOffset); - displayedDate = widget.initalDate; - visitedIndexes.add(initialOffset); - } + const PerDatePageView({ + required this.initialDate, + required this.pageController, + super.key, + }); @override Widget build(BuildContext context) { - return BackButtonListener( - onBackButtonPressed: () async { - if (visitedIndexes.length == 1) { - return false; - } + log("PerDatePageView's build()"); + return PageView.builder( + reverse: true, + controller: pageController, + onPageChanged: (value) { + log("onPageChanged() with value $value"); - visitedIndexes.removeLast(); + var diff = value - pageController.initialPage; + var newDate = initialDate.subtract(Duration(days: diff)); + log("newDate = $newDate"); + context.read().setDisplayedDate(newDate); + }, + itemBuilder: (context, index) { + log("itemBuilder() called with index $index"); + var dateToBuildWidgetFor = initialDate + .subtract(Duration(days: index - pageController.initialPage)); - backButtonWasPressed = true; - pageController.jumpToPage(visitedIndexes.last); - - return true; - }, - child: PageView.builder( - reverse: true, - controller: pageController, - onPageChanged: (value) { - if (backButtonWasPressed) { - backButtonWasPressed = false; - return; - } - - visitedIndexes.add(value); - }, - itemBuilder: (context, index) { - var dateToBuildWidgetFor = - displayedDate.subtract(Duration(days: index - initialOffset)); - - return PerDateWidget( - key: ValueKey(dateToBuildWidgetFor.toString()), - date: dateToBuildWidgetFor.copyWith(isUtc: true), - onDateSelected: (dateSelected) { - if (dateSelected == null) return; - - var diff = dateSelected.difference(dateToBuildWidgetFor); - var newIndex = index - diff.inDays; - - pageController.jumpToPage(newIndex); - }); - }), - ); + return PerDateWidget( + key: ValueKey(dateToBuildWidgetFor.toString()), + date: dateToBuildWidgetFor, + ); + }); } } diff --git a/lib/perdate/perdate_pageview_controller.dart b/lib/perdate/perdate_pageview_controller.dart new file mode 100644 index 0000000..770ce44 --- /dev/null +++ b/lib/perdate/perdate_pageview_controller.dart @@ -0,0 +1,126 @@ +import 'dart:developer'; + +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:calorimeter/food_entry/food_entry_bloc.dart'; +import 'package:calorimeter/perdate/perdate_pageview.dart'; +import 'package:calorimeter/utils/app_drawer.dart'; +import 'package:calorimeter/utils/calendar_floating_button.dart'; +import 'package:calorimeter/utils/date_time_helper.dart'; +import 'package:calorimeter/utils/rectangular_notch_shape.dart'; +import 'package:calorimeter/utils/scan_food_floating_button.dart'; +import 'package:calorimeter/utils/sum_widget.dart'; +import 'package:calorimeter/utils/theme_switcher_button.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class PerDatePageViewController extends StatelessWidget { + // this is the date for which the PerDate widget will be shown on screen + // left of it will be yesterday's PerDate widget + // right of it will be tomorrow's PerDate widget + final DateTime initialDate; + final PageController pageController; + static final int initialOffset = 36500000; + + const PerDatePageViewController._( + {required this.initialDate, required this.pageController}); + + factory PerDatePageViewController({required initialDate}) { + return PerDatePageViewController._( + initialDate: initialDate, + pageController: PageController(initialPage: initialOffset)); + } + + @override + Widget build(BuildContext context) { + return BackButtonListener( + onBackButtonPressed: () async { + var visitedIndexes = + context.read().visitedIndexes; + if (visitedIndexes.length == 1) { + return false; + } + + visitedIndexes.removeLast(); + + pageController.jumpToPage(visitedIndexes.last); + + return true; + }, + child: ChangeNotifierProvider( + create: (context) => PageViewStateProvider( + initialDate: initialDate, + initialOffset: initialOffset, + ), + child: Builder(builder: (context) { + return Scaffold( + appBar: AppBar( + title: Builder(builder: (context) { + return Text(DateFormat.yMMMMd('de').format( + context.watch().displayedDate)); + }), + actions: const [ThemeSwitcherButton()], + ), + bottomNavigationBar: BottomAppBar( + shape: const RectangularNotchShape(), + color: Theme.of(context).colorScheme.secondary, + child: SumWidget(foodEntries: [])), + drawer: const AppDrawer(), + floatingActionButton: OverflowBar(children: [ + ScanFoodFAB( + onPressed: () { + var result = BarcodeScanner.scan(); + context + .read() + .add(BarcodeScanned(scanResultFuture: result)); + }, + ), + const SizedBox(width: 8), + CalendarFAB( + startFromDate: DateTimeHelper.now(), + onDateSelected: (dateSelected) { + if (dateSelected == null) return; + + var dateDiff = dateSelected.difference(initialDate).inDays; + + log("dateDiff = $dateDiff"); + pageController.jumpToPage(initialOffset - dateDiff); + }, + ), + ]), + floatingActionButtonLocation: + FloatingActionButtonLocation.endDocked, + body: PerDatePageView( + pageController: pageController, + initialDate: initialDate, + ), + ); + }), + ), + ); + } +} + +class PageViewStateProvider with ChangeNotifier { + DateTime _displayedDate; + List visitedIndexes = []; + bool _backButtonWasPressed = false; + + PageViewStateProvider({required DateTime initialDate, int initialOffset = 0}) + : _displayedDate = initialDate { + visitedIndexes.add(initialOffset); + } + + set backButtonWasPressed(val) => _backButtonWasPressed = val; + get backButtonWasPressed => _backButtonWasPressed; + + get displayedDate => _displayedDate; + void setDisplayedDate(date) { + _displayedDate = date; + notifyListeners(); + } + + void addVisitedindex(int index) { + visitedIndexes.add(index); + } +} diff --git a/lib/perdate/perdate_widget.dart b/lib/perdate/perdate_widget.dart index e5437c4..12c6156 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -1,22 +1,13 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:calorimeter/utils/scan_food_floating_button.dart'; -import 'package:calorimeter/utils/app_drawer.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/perdate/entry_list.dart'; import 'package:calorimeter/storage/storage.dart'; -import 'package:calorimeter/utils/calendar_floating_button.dart'; -import 'package:calorimeter/utils/rectangular_notch_shape.dart'; -import 'package:calorimeter/utils/sum_widget.dart'; -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; - final Function(DateTime?) onDateSelected; - const PerDateWidget( - {super.key, required this.date, required this.onDateSelected}); + const PerDateWidget({super.key, required this.date}); @override State createState() => _PerDateWidgetState(); @@ -25,13 +16,16 @@ class PerDateWidget extends StatefulWidget { class _PerDateWidgetState extends State with AutomaticKeepAliveClientMixin { late FoodStorage storage; + late Future> entriesFuture; List entries = []; @override void initState() { - context - .read() - .add(PageBeingInitialized(forDate: widget.date)); + storage = FoodStorage.getInstance(); + entriesFuture = storage.getEntriesForDate(widget.date); + entriesFuture.then((val) { + entries = val; + }); super.initState(); } @@ -44,51 +38,29 @@ class _PerDateWidgetState extends State Widget build(BuildContext context) { super.build(context); - 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); - }, - ); + return FutureBuilder( + future: entriesFuture, + builder: (context, snapshot) { + return snapshot.connectionState != ConnectionState.done + ? const Center(child: CircularProgressIndicator()) + : 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 FoodEntryList(entries: pageState.foodEntries); + }, + ), + ); + }); } void showNewSnackbarWith(BuildContext context, String text) { diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 4b72eeb..b536135 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; +import 'package:calorimeter/utils/date_time_helper.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -157,7 +158,7 @@ class FoodStorage { // get a list of dates of the last 365 days var dates = List.generate(365, (idx) { var durationToPast = Duration(days: idx); - return DateTime.now().subtract(durationToPast); + return DateTimeHelper.now().subtract(durationToPast); }); for (var date in dates.reversed) { diff --git a/lib/utils/calendar_floating_button.dart b/lib/utils/calendar_floating_button.dart index 032cb5d..15de917 100644 --- a/lib/utils/calendar_floating_button.dart +++ b/lib/utils/calendar_floating_button.dart @@ -1,10 +1,11 @@ +import 'package:calorimeter/utils/date_time_helper.dart'; import 'package:flutter/material.dart'; -class CalendarFloatingButton extends StatelessWidget { +class CalendarFAB extends StatelessWidget { final DateTime startFromDate; final Function(DateTime?) onDateSelected; - const CalendarFloatingButton( + const CalendarFAB( {super.key, required this.startFromDate, required this.onDateSelected}); @override @@ -15,17 +16,18 @@ class CalendarFloatingButton extends StatelessWidget { locale: const Locale('de'), context: context, initialDate: startFromDate, - currentDate: DateTime.now(), - firstDate: DateTime.now().subtract(const Duration(days: 365 * 10)), - lastDate: DateTime.now().add(const Duration(days: 365 * 10)), + currentDate: DateTimeHelper.now(), + firstDate: + DateTimeHelper.now().subtract(const Duration(days: 365 * 10)), + lastDate: DateTimeHelper.now().add(const Duration(days: 365 * 10)), ); if (!context.mounted) return; - onDateSelected(datePicked); + onDateSelected(datePicked?.copyWith(isUtc: true)); }, heroTag: "calendarFAB", - child: const Icon(Icons.today), + child: const Icon(Icons.date_range), ); } } diff --git a/lib/utils/date_time_helper.dart b/lib/utils/date_time_helper.dart new file mode 100644 index 0000000..d87bec1 --- /dev/null +++ b/lib/utils/date_time_helper.dart @@ -0,0 +1,12 @@ +class DateTimeHelper { + static DateTime now() { + return DateTime.now().copyWith( + isUtc: true, + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0, + ); + } +} diff --git a/lib/utils/scan_food_floating_button.dart b/lib/utils/scan_food_floating_button.dart index 95b7695..cdb1667 100644 --- a/lib/utils/scan_food_floating_button.dart +++ b/lib/utils/scan_food_floating_button.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -class ScanFoodFloatingButton extends StatelessWidget { +class ScanFoodFAB extends StatelessWidget { final Function() onPressed; - const ScanFoodFloatingButton({super.key, required this.onPressed}); + const ScanFoodFAB({super.key, required this.onPressed}); @override Widget build(BuildContext context) { diff --git a/lib/utils/scan_food_notifier.dart b/lib/utils/scan_food_notifier.dart new file mode 100644 index 0000000..b1b6237 --- /dev/null +++ b/lib/utils/scan_food_notifier.dart @@ -0,0 +1,48 @@ +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:calorimeter/food_entry/food_entry_bloc.dart'; +import 'package:calorimeter/food_scan/food_fact_lookup.dart'; +import 'package:flutter/material.dart'; + +class ScanFoodNotifier with ChangeNotifier { + void handleBarcodeScannedEvent(BarcodeScanned barcode) async { + var client = FoodFactLookupClient(); + var scanResult = await barcode.scanResultFuture; + + if (scanResult.type == ResultType.Cancelled) { + return; + } + if (scanResult.type == ResultType.Error) { + return; + } + var responseFuture = client.retrieveFoodInfo(scanResult.rawContent); + + var newEntryWaiting = FoodEntryState( + kcalPer100: 0, + name: "", + mass: 0, + waitingForNetwork: true, + isSelected: false, + ); + + await responseFuture.then((response) async { + if (response.status == + FoodFactResponseStatus.foodFactServerNotReachable) { + return; + } + + var newEntryFinishedWaiting = FoodEntryState( + name: response.food?.name ?? "", + mass: response.food?.mass ?? 0, + kcalPer100: response.food?.kcalPer100g ?? 0, + waitingForNetwork: false, + isSelected: false, + ); + }); + } +} + +class BarcodeScanned { + final Future scanResultFuture; + + BarcodeScanned({required this.scanResultFuture}); +}