From cb18e1d1f06af616eadbf56172a3769edd3cc4c4 Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 3 Oct 2024 23:21:13 +0200 Subject: [PATCH] Introduce PageView with manual switching of dates 1. Show PerDate widgets inside of an PageView 2. Introduce GoRouter so we can intercept back button taps with BackButtonListener 3. Implement rudimentary navigation 4. Fix bug that still showed a spinner event when the barcode was not found. --- lib/food_entry/food_entry_bloc.dart | 14 +- lib/main.dart | 27 ++- lib/perdate/perdate_pageview.dart | 76 +++++++++ lib/perdate/perdate_widget.dart | 38 ++--- lib/storage/storage.dart | 3 - lib/storage/storage.dart.orig | 215 ------------------------ lib/utils/calendar_floating_button.dart | 6 +- pubspec.lock | 47 ++++-- pubspec.yaml | 5 +- 9 files changed, 163 insertions(+), 268 deletions(-) create mode 100644 lib/perdate/perdate_pageview.dart delete mode 100644 lib/storage/storage.dart.orig diff --git a/lib/food_entry/food_entry_bloc.dart b/lib/food_entry/food_entry_bloc.dart index 44bd89f..7e4718a 100644 --- a/lib/food_entry/food_entry_bloc.dart +++ b/lib/food_entry/food_entry_bloc.dart @@ -94,15 +94,25 @@ class FoodEntryBloc extends Bloc { } if (response.status == FoodFactResponseStatus.barcodeNotFound) { + List listWithEntryRemoved = + List.from(state.foodEntries); + listWithEntryRemoved + .removeWhere((entry) => entry.id == newEntryWaiting.id); + emit(PageState( - foodEntries: state.foodEntries, + foodEntries: listWithEntryRemoved, 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); + emit(PageState( - foodEntries: state.foodEntries, + foodEntries: listWithEntryRemoved, errorString: "OpenFoodFacts-Server konnte nicht erreicht werden.")); return; } diff --git a/lib/main.dart b/lib/main.dart index fe25176..4d24433 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,17 @@ -import 'package:calorimeter/perdate/perdate_widget.dart'; +import 'package:calorimeter/perdate/perdate_pageview.dart'; import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/theme_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:go_router/go_router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + + WidgetsFlutterBinding.ensureInitialized(); + var storage = await FoodStorage.create(); await storage.buildFoodLookupDatabase(); var kcalLimit = await storage.readLimit(); @@ -56,8 +60,25 @@ class MainApp extends StatelessWidget { newBrightness = Brightness.dark; } - return MaterialApp( - home: PerDateWidget(date: DateTime.now()), + return MaterialApp.router( + routerConfig: GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) { + return PerDatePageview( + initalDate: DateTime.now().copyWith( + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0, + ), + ); + }, + ), + ], + ), localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, diff --git a/lib/perdate/perdate_pageview.dart b/lib/perdate/perdate_pageview.dart new file mode 100644 index 0000000..c444391 --- /dev/null +++ b/lib/perdate/perdate_pageview.dart @@ -0,0 +1,76 @@ +import 'package:calorimeter/perdate/perdate_widget.dart'; +import 'package:flutter/material.dart'; + +class PerDatePageview extends StatefulWidget { + // 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}); + + @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); + } + + @override + Widget build(BuildContext context) { + return BackButtonListener( + onBackButtonPressed: () async { + if (visitedIndexes.length == 1) { + return false; + } + + visitedIndexes.removeLast(); + + 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, + onDateSelected: (dateSelected) { + if (dateSelected == null) return; + + var diff = dateSelected.difference(dateToBuildWidgetFor); + var newIndex = index - diff.inDays; + + pageController.jumpToPage(newIndex); + }); + }), + ); + } +} diff --git a/lib/perdate/perdate_widget.dart b/lib/perdate/perdate_widget.dart index 9ae14aa..f34eafa 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -15,13 +15,16 @@ import 'package:provider/provider.dart'; class PerDateWidget extends StatefulWidget { final DateTime date; - const PerDateWidget({super.key, required this.date}); + final Function(DateTime?) onDateSelected; + const PerDateWidget( + {super.key, required this.date, required this.onDateSelected}); @override State createState() => _PerDateWidgetState(); } -class _PerDateWidgetState extends State { +class _PerDateWidgetState extends State + with AutomaticKeepAliveClientMixin { late FoodStorage storage; late Future> entriesFuture; List entries = []; @@ -36,8 +39,15 @@ class _PerDateWidgetState extends State { super.initState(); } + @override + void dispose() { + super.dispose(); + } + @override Widget build(BuildContext context) { + super.build(context); + return FutureBuilder( future: entriesFuture, builder: (context, snapshot) { @@ -85,7 +95,7 @@ class _PerDateWidgetState extends State { CalendarFloatingButton( startFromDate: widget.date, onDateSelected: (dateSelected) { - _onDateSelected(dateSelected); + widget.onDateSelected(dateSelected); }, ), ]), @@ -106,26 +116,8 @@ class _PerDateWidgetState extends State { ..showSnackBar(snackbar); } - void _onDateSelected(DateTime date) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return PerDateWidget(date: date); - }, - ), - ).then((val) { - setState( - () { - entriesFuture = storage.getEntriesForDate(widget.date); - entriesFuture.then( - (val) { - entries = val; - }, - ); - }, - ); - }); - } + @override + bool get wantKeepAlive => true; } class ErrorSnackbar extends SnackBar { diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 8098ed6..4b72eeb 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; @@ -171,13 +170,11 @@ class FoodStorage { for (var entry in entriesForDate) { _foodLookupDatabase[entry.name] = entry.kcalPer100; - log("Added entry: ${entry.name}/${entry.kcalPer100}"); } } void addFoodEntryToLookupDatabase(FoodEntryState entry) { _foodLookupDatabase[entry.name] = entry.kcalPer100; - log("Added entry: ${entry.name}/${entry.kcalPer100}"); } Map get getFoodEntryLookupDatabase => _foodLookupDatabase; diff --git a/lib/storage/storage.dart.orig b/lib/storage/storage.dart.orig deleted file mode 100644 index 55ea163..0000000 --- a/lib/storage/storage.dart.orig +++ /dev/null @@ -1,215 +0,0 @@ -import 'dart:developer'; -import 'dart:io'; - -import 'package:calorimeter/food_entry/food_entry_bloc.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class FoodStorage { - static late FoodStorage _instance; - late String path; - final Map _foodLookupDatabase = {}; - - FoodStorage._create(); - - static Future create() async { - var storage = FoodStorage._create(); - - Directory dir = Directory(''); - - if (UniversalPlatform.isDesktop) { - dir = await getApplicationCacheDirectory(); - } else if (UniversalPlatform.isAndroid) { - dir = await getApplicationDocumentsDirectory(); - } - - storage.path = dir.path; - _instance = storage; - - return _instance; - } - - static FoodStorage getInstance() => _instance; - - Future> getEntriesForDate(DateTime date) async { - List entries = []; - var filePath = '$path/${date.year}/${date.month}/${date.day}'; - - var file = File(filePath); - var exists = await file.exists(); - - if (!exists) return []; - - var lines = await file.readAsLines(); - - for (var line in lines) { -<<<<<<< HEAD - var fields = line.splitWithIgnore(',', ignoreIn: '"'); - var entry = FoodEntry( - name: fields[1].replaceAll('"', ""), - mass: double.parse(fields[2]), - kcalPerMass: double.parse(fields[3])); -======= - var fields = line.split(','); - var entry = FoodEntryState( - name: fields[1], - mass: int.parse(fields[2]), - kcalPerMass: int.parse(fields[3]), - waitingForNetwork: false, - ); ->>>>>>> 7921f09 (wip) - entries.add(entry); - } - - return entries; - } - - Future writeEntriesForDate( - DateTime date, List foodEntries) async { - var filePath = '$path/${date.year}/${date.month}/${date.day}'; - var file = File(filePath); - - var exists = await file.exists(); - - if (!exists) { - await file.create(recursive: true); - } - - String fullString = ''; - for (var entry in foodEntries) { - fullString += '${entry.toString()}\n'; - } - - await file.writeAsString(fullString); - } - - Future updateLimit(double limit) async { - var filePath = '$path/limit'; - var file = File(filePath); - - var exists = await file.exists(); - if (!exists) { - await file.create(); - } - - await file.writeAsString(limit.toString()); - } - - Future readLimit() async { - var filePath = '$path/limit'; - var file = File(filePath); - var exists = await file.exists(); - - if (!exists) { - return 2000; - } - - var line = await file.readAsLines(); - - double limit; - try { - limit = double.parse(line[0]); - } catch (e) { - limit = 2000; - } - - return limit; - } - - Future readBrightness() async { - var filePath = '$path/brightness'; - var file = File(filePath); - var exists = await file.exists(); - - if (!exists) { - return 'dark'; - } - - var line = await file.readAsLines(); - - if (line.isEmpty || (line[0] != 'dark' && line[0] != 'light')) { - return 'dark'; - } - - return line[0]; - } - - Future writeBrightness(String brightness) async { - var filePath = '$path/brightness'; - var file = File(filePath); - var exists = await file.exists(); - - if (!exists) { - file.create(); - } - - await file.writeAsString(brightness); - } - - Future buildFoodLookupDatabase() async { - // 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); - }); - - for (var date in dates.reversed) { - addFoodEntryToLookupDatabaseFor(date); - } - } - - Future addFoodEntryToLookupDatabaseFor(DateTime date) async { - var entriesForDate = await getEntriesForDate(date); - - for (var entry in entriesForDate) { - _foodLookupDatabase[entry.name] = entry.kcalPerMass; - log("Added entry: ${entry.name}/${entry.kcalPerMass}"); - } - } - - void addFoodEntryToLookupDatabase(FoodEntryState entry) { - _foodLookupDatabase[entry.name] = entry.kcalPerMass; - log("Added entry: ${entry.name}/${entry.kcalPerMass}"); - } - - Map get getFoodEntryLookupDatabase => _foodLookupDatabase; -} - -extension SplitWithIgnore on String { - List splitWithIgnore(String delimiter, {String? ignoreIn}) { - List parts = []; - - if (ignoreIn == null) { - return split(delimiter); - } - - int index = -1; - int indexCharAfterDelimiter = 0; - bool inIgnore = false; - for (var rune in runes) { - var char = String.fromCharCode(rune); - - index += 1; - - if (char == ignoreIn) { - inIgnore = !inIgnore; - continue; - } - - if (inIgnore) { - continue; - } - - if (char == delimiter) { - parts.add(substring(indexCharAfterDelimiter, index)); - indexCharAfterDelimiter = index + 1; - } - - if (index + 1 == length) { - parts.add(substring(indexCharAfterDelimiter, length)); - } - } - - return parts; - } -} diff --git a/lib/utils/calendar_floating_button.dart b/lib/utils/calendar_floating_button.dart index 161b0bc..032cb5d 100644 --- a/lib/utils/calendar_floating_button.dart +++ b/lib/utils/calendar_floating_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; class CalendarFloatingButton extends StatelessWidget { final DateTime startFromDate; - final Function(DateTime) onDateSelected; + final Function(DateTime?) onDateSelected; const CalendarFloatingButton( {super.key, required this.startFromDate, required this.onDateSelected}); @@ -17,12 +17,12 @@ class CalendarFloatingButton extends StatelessWidget { initialDate: startFromDate, currentDate: DateTime.now(), firstDate: DateTime.now().subtract(const Duration(days: 365 * 10)), - lastDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365 * 10)), ); if (!context.mounted) return; - onDateSelected(datePicked ?? DateTime.now()); + onDateSelected(datePicked); }, heroTag: "calendarFAB", child: const Icon(Icons.today), diff --git a/pubspec.lock b/pubspec.lock index ec2f85e..82e7ca7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -183,18 +183,18 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.1" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -205,6 +205,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -221,6 +226,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" + url: "https://pub.dev" + source: hosted + version: "14.3.0" http_multi_server: dependency: transitive description: @@ -305,10 +318,10 @@ packages: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" logging: dependency: transitive description: @@ -353,10 +366,10 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" nested: dependency: transitive description: @@ -401,10 +414,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.11" path_provider_foundation: dependency: transitive description: @@ -646,10 +659,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.1" vector_math: dependency: transitive description: @@ -678,10 +691,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -710,10 +723,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -732,4 +745,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.3 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index b84d1dd..4c05272 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,12 +20,13 @@ dependencies: barcode_scan2: ^4.3.3 provider: ^6.1.2 test: ^1.25.7 + go_router: ^14.3.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 - flutter_launcher_icons: "^0.13.1" + flutter_lints: ^5.0.0 + flutter_launcher_icons: ^0.14.1 flutter: uses-material-design: true