diff --git a/lib/main.dart b/lib/main.dart index 7166f6a..7dcad6a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ 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'; @@ -18,14 +19,8 @@ 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); + timeNow = DateTimeHelper.now(); entriesForToday = await storage.getEntriesForDate(timeNow); var kcalLimit = await storage.readLimit(); @@ -87,14 +82,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..ed07217 100644 --- a/lib/perdate/perdate_pageview.dart +++ b/lib/perdate/perdate_pageview.dart @@ -1,76 +1,50 @@ +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"); - backButtonWasPressed = true; - pageController.jumpToPage(visitedIndexes.last); + context + .read() + .addVisitedindexIfNotVisitedByBackButton(value); + context.read().setDisplayedDate(newDate); + }, + itemBuilder: (context, index) { + log("itemBuilder() called with index $index"); + var dateToBuildWidgetFor = initialDate + .subtract(Duration(days: index - pageController.initialPage)); - 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..c40bbb7 --- /dev/null +++ b/lib/perdate/perdate_pageview_controller.dart @@ -0,0 +1,147 @@ +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 ChangeNotifierProvider( + create: (context) => PageViewStateProvider( + initialDate: initialDate, + initialOffset: initialOffset, + ), + child: Builder(builder: (context) { + return BackButtonListener( + onBackButtonPressed: () async { + context.read().backButtonWasPressed = true; + var visitedIndexes = + context.read().visitedIndexes; + if (visitedIndexes.length == 1) { + return false; + } + + visitedIndexes.removeLast(); + pageController.jumpToPage(visitedIndexes.last); + + return true; + }, + child: 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: Builder(builder: (context) { + return SumWidget( + date: context.watch().displayedDate); + }), + ), + drawer: const AppDrawer(), + floatingActionButton: OverflowBar(children: [ + ScanFoodFAB( + onPressed: () { + var result = BarcodeScanner.scan(); + context.read().add( + BarcodeScanned( + scanResultFuture: result, + forDate: context + .read() + .displayedDate, + ), + ); + }, + ), + 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; + final List _visitedIndexes; + bool _backButtonWasPressed = false; + + PageViewStateProvider({required DateTime initialDate, int initialOffset = 0}) + : _displayedDate = initialDate, + _visitedIndexes = [] { + _visitedIndexes.add(initialOffset); + } + + set backButtonWasPressed(val) => _backButtonWasPressed = val; + get backButtonWasPressed => _backButtonWasPressed; + + get displayedDate => _displayedDate; + void setDisplayedDate(date) { + _displayedDate = date; + notifyListeners(); + } + + get visitedIndexes => _visitedIndexes; + + void addVisitedIndex(int index) { + _visitedIndexes.add(index); + } + + void addVisitedindexIfNotVisitedByBackButton(int index) { + if (_backButtonWasPressed) { + _backButtonWasPressed = false; + return; + } + + addVisitedIndex(index); + } +} diff --git a/lib/perdate/perdate_widget.dart b/lib/perdate/perdate_widget.dart index e5437c4..4ccee06 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -1,22 +1,11 @@ -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'; 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(); @@ -24,9 +13,6 @@ class PerDateWidget extends StatefulWidget { class _PerDateWidgetState extends State with AutomaticKeepAliveClientMixin { - late FoodStorage storage; - List entries = []; - @override void initState() { context @@ -35,11 +21,6 @@ class _PerDateWidgetState extends State super.initState(); } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { super.build(context); @@ -50,43 +31,10 @@ class _PerDateWidgetState extends State 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); + builder: (context, pageState) { + return FoodEntryList( + entries: pageState.foodEntries[widget.date] ?? [], + date: widget.date); }, ); } 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/sum_widget.dart b/lib/utils/sum_widget.dart index 8e08e0d..ca444e2 100644 --- a/lib/utils/sum_widget.dart +++ b/lib/utils/sum_widget.dart @@ -4,53 +4,57 @@ import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart'; class SumWidget extends StatelessWidget { - final List foodEntries; - const SumWidget({required this.foodEntries, super.key}); + final DateTime date; + + const SumWidget({super.key, required this.date}); @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - var sum = 0.0; - for (var entry in foodEntries) { - sum += entry.kcalPer100 / 100 * entry.mass; - } - var diff = state.kcalLimit - sum; - var diffLimit = state.kcalLimit ~/ 4; + return BlocBuilder( + builder: (context, entryState) { + return BlocBuilder( + builder: (context, settingsState) { + var sum = 0.0; + for (var entry in entryState.foodEntries[date] ?? []) { + sum += entry.kcalPer100 / 100 * entry.mass; + } + var diff = settingsState.kcalLimit - sum; + var diffLimit = settingsState.kcalLimit ~/ 4; - var textColor = Theme.of(context).colorScheme.onSecondary; - var newTextColor = textColor; - var brightness = Theme.of(context).brightness; + var textColor = Theme.of(context).colorScheme.onSecondary; + var newTextColor = textColor; + var brightness = Theme.of(context).brightness; - switch (brightness) { - case Brightness.dark: - if (diff < 0) { - newTextColor = Colors.red[900]!; - } else if (diff < diffLimit) { - newTextColor = Colors.orange[900]!; - } - break; + switch (brightness) { + case Brightness.dark: + if (diff < 0) { + newTextColor = Colors.red[900]!; + } else if (diff < diffLimit) { + newTextColor = Colors.orange[900]!; + } + break; - case Brightness.light: - if (diff < 0) { - newTextColor = Colors.redAccent; - } else if (diff < diffLimit) { - newTextColor = Colors.orangeAccent; - } - break; - } + case Brightness.light: + if (diff < 0) { + newTextColor = Colors.redAccent; + } else if (diff < diffLimit) { + newTextColor = Colors.orangeAccent; + } + break; + } - return Align( - alignment: Alignment.centerLeft, - child: Text( - 'kcal heute: ${sum.ceil().toString()}/${state.kcalLimit.ceil()}', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(color: newTextColor), - ), - ); - }, - ); + return Align( + alignment: Alignment.centerLeft, + child: Text( + 'kcal heute: ${sum.ceil().toString()}/${settingsState.kcalLimit.ceil()}', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: newTextColor), + ), + ); + }, + ); + }); } }