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.
This commit is contained in:
Marco 2024-10-03 23:21:13 +02:00
parent 970cea8ba7
commit cb18e1d1f0
9 changed files with 163 additions and 268 deletions

View File

@ -94,15 +94,25 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
} }
if (response.status == FoodFactResponseStatus.barcodeNotFound) { if (response.status == FoodFactResponseStatus.barcodeNotFound) {
List<FoodEntryState> listWithEntryRemoved =
List.from(state.foodEntries);
listWithEntryRemoved
.removeWhere((entry) => entry.id == newEntryWaiting.id);
emit(PageState( emit(PageState(
foodEntries: state.foodEntries, foodEntries: listWithEntryRemoved,
errorString: "Barcode konnte nicht gefunden werden.")); errorString: "Barcode konnte nicht gefunden werden."));
return; return;
} }
if (response.status == if (response.status ==
FoodFactResponseStatus.foodFactServerNotReachable) { FoodFactResponseStatus.foodFactServerNotReachable) {
List<FoodEntryState> listWithEntryRemoved =
List.from(state.foodEntries);
listWithEntryRemoved
.removeWhere((entry) => entry.id == newEntryWaiting.id);
emit(PageState( emit(PageState(
foodEntries: state.foodEntries, foodEntries: listWithEntryRemoved,
errorString: "OpenFoodFacts-Server konnte nicht erreicht werden.")); errorString: "OpenFoodFacts-Server konnte nicht erreicht werden."));
return; return;
} }

View File

@ -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/storage/storage.dart';
import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart';
import 'package:calorimeter/utils/theme_bloc.dart'; import 'package:calorimeter/utils/theme_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:go_router/go_router.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
var storage = await FoodStorage.create(); var storage = await FoodStorage.create();
await storage.buildFoodLookupDatabase(); await storage.buildFoodLookupDatabase();
var kcalLimit = await storage.readLimit(); var kcalLimit = await storage.readLimit();
@ -56,8 +60,25 @@ class MainApp extends StatelessWidget {
newBrightness = Brightness.dark; newBrightness = Brightness.dark;
} }
return MaterialApp( return MaterialApp.router(
home: PerDateWidget(date: DateTime.now()), 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 [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,

View File

@ -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<PerDatePageview> createState() => _PerDatePageviewState();
}
class _PerDatePageviewState extends State<PerDatePageview> {
late PageController pageController;
late DateTime displayedDate;
late List<int> 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);
});
}),
);
}
}

View File

@ -15,13 +15,16 @@ import 'package:provider/provider.dart';
class PerDateWidget extends StatefulWidget { class PerDateWidget extends StatefulWidget {
final DateTime date; 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 @override
State<PerDateWidget> createState() => _PerDateWidgetState(); State<PerDateWidget> createState() => _PerDateWidgetState();
} }
class _PerDateWidgetState extends State<PerDateWidget> { class _PerDateWidgetState extends State<PerDateWidget>
with AutomaticKeepAliveClientMixin<PerDateWidget> {
late FoodStorage storage; late FoodStorage storage;
late Future<List<FoodEntryState>> entriesFuture; late Future<List<FoodEntryState>> entriesFuture;
List<FoodEntryState> entries = []; List<FoodEntryState> entries = [];
@ -36,8 +39,15 @@ class _PerDateWidgetState extends State<PerDateWidget> {
super.initState(); super.initState();
} }
@override
void dispose() {
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return FutureBuilder( return FutureBuilder(
future: entriesFuture, future: entriesFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -85,7 +95,7 @@ class _PerDateWidgetState extends State<PerDateWidget> {
CalendarFloatingButton( CalendarFloatingButton(
startFromDate: widget.date, startFromDate: widget.date,
onDateSelected: (dateSelected) { onDateSelected: (dateSelected) {
_onDateSelected(dateSelected); widget.onDateSelected(dateSelected);
}, },
), ),
]), ]),
@ -106,26 +116,8 @@ class _PerDateWidgetState extends State<PerDateWidget> {
..showSnackBar(snackbar); ..showSnackBar(snackbar);
} }
void _onDateSelected(DateTime date) { @override
Navigator.of(context).push( bool get wantKeepAlive => true;
MaterialPageRoute(
builder: (context) {
return PerDateWidget(date: date);
},
),
).then((val) {
setState(
() {
entriesFuture = storage.getEntriesForDate(widget.date);
entriesFuture.then(
(val) {
entries = val;
},
);
},
);
});
}
} }
class ErrorSnackbar extends SnackBar { class ErrorSnackbar extends SnackBar {

View File

@ -1,4 +1,3 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
@ -171,13 +170,11 @@ class FoodStorage {
for (var entry in entriesForDate) { for (var entry in entriesForDate) {
_foodLookupDatabase[entry.name] = entry.kcalPer100; _foodLookupDatabase[entry.name] = entry.kcalPer100;
log("Added entry: ${entry.name}/${entry.kcalPer100}");
} }
} }
void addFoodEntryToLookupDatabase(FoodEntryState entry) { void addFoodEntryToLookupDatabase(FoodEntryState entry) {
_foodLookupDatabase[entry.name] = entry.kcalPer100; _foodLookupDatabase[entry.name] = entry.kcalPer100;
log("Added entry: ${entry.name}/${entry.kcalPer100}");
} }
Map<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase; Map<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase;

View File

@ -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<String, int> _foodLookupDatabase = {};
FoodStorage._create();
static Future<FoodStorage> 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<List<FoodEntryState>> getEntriesForDate(DateTime date) async {
List<FoodEntryState> 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<void> writeEntriesForDate(
DateTime date, List<FoodEntryState> 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<void> 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<double> 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<String> 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<void> 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<void> buildFoodLookupDatabase() async {
// get a list of dates of the last 365 days
var dates = List<DateTime>.generate(365, (idx) {
var durationToPast = Duration(days: idx);
return DateTime.now().subtract(durationToPast);
});
for (var date in dates.reversed) {
addFoodEntryToLookupDatabaseFor(date);
}
}
Future<void> 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<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase;
}
extension SplitWithIgnore on String {
List<String> splitWithIgnore(String delimiter, {String? ignoreIn}) {
List<String> 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;
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
class CalendarFloatingButton extends StatelessWidget { class CalendarFloatingButton extends StatelessWidget {
final DateTime startFromDate; final DateTime startFromDate;
final Function(DateTime) onDateSelected; final Function(DateTime?) onDateSelected;
const CalendarFloatingButton( const CalendarFloatingButton(
{super.key, required this.startFromDate, required this.onDateSelected}); {super.key, required this.startFromDate, required this.onDateSelected});
@ -17,12 +17,12 @@ class CalendarFloatingButton extends StatelessWidget {
initialDate: startFromDate, initialDate: startFromDate,
currentDate: DateTime.now(), currentDate: DateTime.now(),
firstDate: DateTime.now().subtract(const Duration(days: 365 * 10)), 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; if (!context.mounted) return;
onDateSelected(datePicked ?? DateTime.now()); onDateSelected(datePicked);
}, },
heroTag: "calendarFAB", heroTag: "calendarFAB",
child: const Icon(Icons.today), child: const Icon(Icons.today),

View File

@ -183,18 +183,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.1" version: "0.14.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "5.0.0"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -205,6 +205,11 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -221,6 +226,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" 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: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -305,10 +318,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "5.0.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -353,10 +366,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: mime name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "2.0.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -401,10 +414,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.10" version: "2.2.11"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@ -646,10 +659,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.0" version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -678,10 +691,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.1.0"
web_socket: web_socket:
dependency: transitive dependency: transitive
description: description:
@ -710,10 +723,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.1.0"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@ -732,4 +745,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.5.3 <4.0.0" dart: ">=3.5.3 <4.0.0"
flutter: ">=3.22.0" flutter: ">=3.24.0"

View File

@ -20,12 +20,13 @@ dependencies:
barcode_scan2: ^4.3.3 barcode_scan2: ^4.3.3
provider: ^6.1.2 provider: ^6.1.2
test: ^1.25.7 test: ^1.25.7
go_router: ^14.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^4.0.0 flutter_lints: ^5.0.0
flutter_launcher_icons: "^0.13.1" flutter_launcher_icons: ^0.14.1
flutter: flutter:
uses-material-design: true uses-material-design: true