From 2509c1721c1ff4b70a606943de862ba93e7b7ea0 Mon Sep 17 00:00:00 2001 From: Marco Date: Fri, 3 Jan 2025 22:34:01 +0100 Subject: [PATCH] Overhaul ui and remove BackButtonListener 1. Make EnterFoodWidget animated 2. Fix exception when reading quantity for a food. Introduce first integration test --- android/app/build.gradle | 4 + integration_test/app_test.dart | 59 ++++++ lib/food_entry/enter_food_widget.dart | 179 ++++++++++------- lib/food_entry/food_entry_bloc.dart | 29 +-- lib/food_entry/food_entry_widget.dart | 173 +++++++++-------- lib/food_scan/food_fact_lookup.dart | 16 +- lib/l10n/app_de.arb | 7 +- lib/l10n/app_en.arb | 7 +- lib/main.dart | 159 +++++++-------- lib/perdate/entry_list.dart | 114 +++++++---- lib/perdate/perdate_pageview.dart | 1 + lib/perdate/perdate_pageview_controller.dart | 84 ++++---- lib/storage/storage.dart | 2 + lib/utils/app_drawer.dart | 12 ++ lib/utils/row_with_spacers_widget.dart | 58 +++++- lib/utils/settings.dart | 4 +- lib/utils/settings_bloc.dart | 2 + pubspec.lock | 191 ++++++++++++++++++- pubspec.yaml | 9 +- test/split_with_ignore_test.dart | 59 ------ 20 files changed, 744 insertions(+), 425 deletions(-) create mode 100644 integration_test/app_test.dart delete mode 100644 test/split_with_ignore_test.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 3b70bed..378a988 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,6 +48,10 @@ android { } buildTypes { + debug { + applicationIdSuffix '.debug' + versionNameSuffix '-DEBUG' + } release { signingConfig = signingConfigs.release } diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart new file mode 100644 index 0000000..802d613 --- /dev/null +++ b/integration_test/app_test.dart @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright (C) 2024 Marco Groß */ + +import 'package:calorimeter/main.dart'; +import 'package:calorimeter/storage/storage.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUp(() {}); + + group('end-to-end test', () { + testWidgets('add food manually', (tester) async { + var foodStorage = await FoodStorage.create(); + + await tester.pumpWidget(MainApp(storage: foodStorage)); + await tester.pumpAndSettle(); + + final addButtonFinder = find.byIcon(Icons.add); + expect(addButtonFinder, findsOneWidget); + + await tester.tap(addButtonFinder); + await tester.pumpAndSettle(); + + final nameAutocompleteFinder = + find.widgetWithText(Autocomplete, "Name"); + final amountFinder = find.widgetWithText(TextField, "Amount"); + final kcalFinder = find.widgetWithText(TextField, "kcal"); + final addButton = find.widgetWithIcon(ElevatedButton, Icons.check); + + expect(nameAutocompleteFinder, findsOneWidget); + expect(amountFinder, findsOneWidget); + expect(kcalFinder, findsOneWidget); + expect(addButton, findsOneWidget); + + await tester.enterText(nameAutocompleteFinder, "Bread"); + await tester.enterText(amountFinder, "150"); + await tester.enterText(kcalFinder, "250"); + + await tester.tap(addButton); + await tester.pumpAndSettle(); + + // EnterFoodWidget collapses + expect(nameAutocompleteFinder, findsNothing); + + var enteredFood = find.text("Bread"); + var enteredAmount = find.text("150"); + var enteredKcal = find.text("250"); + + await tester.pumpAndSettle(); + + expect(enteredFood, findsOneWidget); + expect(enteredAmount, findsOneWidget); + expect(enteredKcal, findsOneWidget); + }); + }); +} diff --git a/lib/food_entry/enter_food_widget.dart b/lib/food_entry/enter_food_widget.dart index 8b9938a..152dc36 100644 --- a/lib/food_entry/enter_food_widget.dart +++ b/lib/food_entry/enter_food_widget.dart @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright (C) 2024 Marco Groß */ -import 'package:calorimeter/storage/storage.dart'; + import 'package:flutter/material.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/utils/row_with_spacers_widget.dart'; @@ -8,8 +8,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class EnterFoodWidget extends StatefulWidget { final Function(BuildContext context, FoodEntryState entry) onAdd; + final Map foodEntryLookupDatabase; - const EnterFoodWidget({super.key, required this.onAdd}); + const EnterFoodWidget( + {super.key, required this.onAdd, required this.foodEntryLookupDatabase}); @override State createState() => _EnterFoodWidgetState(); @@ -19,89 +21,123 @@ class _EnterFoodWidgetState extends State { late TextEditingController nameController; late TextEditingController massController; late TextEditingController kcalPerMassController; - late Map suggestions; + late bool open; @override void initState() { nameController = TextEditingController(); massController = TextEditingController(); kcalPerMassController = TextEditingController(); - suggestions = FoodStorage.getInstance().getFoodEntryLookupDatabase; + + open = false; super.initState(); } @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: RowWidget( - Autocomplete( - optionsViewOpenDirection: OptionsViewOpenDirection.down, - fieldViewBuilder: (context, controller, focusNode, onSubmitted) { - nameController = controller; - return TextFormField( - controller: controller, - focusNode: focusNode, - decoration: InputDecoration( - label: Text(AppLocalizations.of(context)!.name), - ), - ); - }, - optionsBuilder: (TextEditingValue textEditingValue) { - if (textEditingValue.text == '') { - return const Iterable.empty(); - } - - return suggestions.keys.where( - (name) { - return name - .toLowerCase() - .contains(textEditingValue.text.toLowerCase()); - }, - ); - }, - onSelected: (selectedFood) { - int kcalPerMassForSelectedFood = suggestions[selectedFood]!; - setState(() { - nameController.text = selectedFood; - kcalPerMassController.text = - kcalPerMassForSelectedFood.toString(); - }); - }), - TextField( - textAlign: TextAlign.end, - decoration: InputDecoration( - label: Align( - alignment: Alignment.centerRight, - child: Text(AppLocalizations.of(context)!.amountPer), - ), - ), - keyboardType: TextInputType.number, - controller: massController, - onSubmitted: (value) => onSubmitAction(), - ), - TextField( - textAlign: TextAlign.end, - decoration: InputDecoration( - label: Align( - alignment: Alignment.centerRight, - child: Text(AppLocalizations.of(context)!.kcalper), - )), - keyboardType: TextInputType.number, - controller: kcalPerMassController, - onSubmitted: (value) => onSubmitAction(), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - padding: EdgeInsets.zero, + return Column( + children: [ + Stack( + children: [ + if (!open) + RowWidget( + showDividers: false, + null, + null, + null, + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + ), + onPressed: () => setState(() => open = true), + child: Icon(Icons.add)), ), - onPressed: () => onSubmitAction(), - child: const Icon(Icons.add)), + Offstage( + offstage: !open, + child: AnimatedOpacity( + duration: Duration(milliseconds: 250), + opacity: open ? 1.0 : 0.0, + child: RowWidget( + showDividers: true, + Autocomplete( + optionsViewOpenDirection: OptionsViewOpenDirection.down, + fieldViewBuilder: + (context, controller, focusNode, onSubmitted) { + nameController = controller; + return TextFormField( + scrollPadding: EdgeInsets.only(bottom: 100), + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + label: Text(AppLocalizations.of(context)!.name), + ), + ); + }, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '') { + return const Iterable.empty(); + } + + return widget.foodEntryLookupDatabase.keys.where( + (name) { + return name + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }, + ); + }, + onSelected: (selectedFood) { + int kcalPerMassForSelectedFood = + widget.foodEntryLookupDatabase[selectedFood]!; + setState(() { + nameController.text = selectedFood; + kcalPerMassController.text = + kcalPerMassForSelectedFood.toString(); + }); + }), + TextField( + scrollPadding: EdgeInsets.only(bottom: 100), + textAlign: TextAlign.end, + decoration: InputDecoration( + label: Directionality( + textDirection: TextDirection.rtl, + child: Text(AppLocalizations.of(context)!.amount), + ), + ), + keyboardType: TextInputType.number, + controller: massController, + onSubmitted: (value) => onSubmitAction(), + ), + TextField( + scrollPadding: EdgeInsets.only(bottom: 100), + textAlign: TextAlign.end, + decoration: InputDecoration( + label: Directionality( + textDirection: TextDirection.rtl, + child: Text(AppLocalizations.of(context)!.kcal))), + keyboardType: TextInputType.number, + controller: kcalPerMassController, + onSubmitted: (value) => onSubmitAction(), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + ), + onPressed: () => onSubmitAction(), + child: const Icon(Icons.check)), + ), + ), + ), + ], ), - ), + SizedBox( + height: 200, + child: GestureDetector( + onTap: () => setState(() { + open = false; + }))), + ], ); } @@ -143,6 +179,7 @@ class _EnterFoodWidgetState extends State { nameController.text = ""; massController.text = ""; kcalPerMassController.text = ""; + open = false; }); } } diff --git a/lib/food_entry/food_entry_bloc.dart b/lib/food_entry/food_entry_bloc.dart index c702abf..f30b014 100644 --- a/lib/food_entry/food_entry_bloc.dart +++ b/lib/food_entry/food_entry_bloc.dart @@ -40,12 +40,8 @@ class FoodEntryBloc extends Bloc { await storage.writeEntriesForDate(event.forDate, entriesForDate); storage.addFoodEntryToLookupDatabase(event.entry); - // 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}); + newFoodEntries.addAll({event.forDate: entriesForDate}); emit(GlobalEntryState(foodEntries: newFoodEntries)); } @@ -65,12 +61,8 @@ class FoodEntryBloc extends Bloc { await storage.writeEntriesForDate(event.forDate, entriesForDate); storage.addFoodEntryToLookupDatabase(event.newEntry); - // 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}); + newFoodEntries.addAll({event.forDate: entriesForDate}); emit(GlobalEntryState(foodEntries: newFoodEntries)); } @@ -84,12 +76,8 @@ class FoodEntryBloc extends Bloc { 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}); + newFoodEntries.addAll({event.forDate: entriesForDate}); emit(GlobalEntryState(foodEntries: newFoodEntries)); } @@ -109,11 +97,10 @@ class FoodEntryBloc extends Bloc { return; } + var client = FoodFactLookupClient(); var entriesForDate = state.foodEntries[event.forDate]; if (entriesForDate == null) return; - var client = FoodFactLookupClient(); - if (scanResult.type == ResultType.Cancelled) { return; } @@ -134,9 +121,8 @@ class FoodEntryBloc extends Bloc { ); entriesForDate.add(newEntryWaiting); - var newFoodEntries = state.foodEntries; - newFoodEntries.addAll({event.forDate: entriesForDate}); - emit(GlobalEntryState(foodEntries: newFoodEntries)); + state.foodEntries.addAll({event.forDate: entriesForDate}); + emit(GlobalEntryState(foodEntries: state.foodEntries)); await responseFuture.then((response) async { var index = entriesForDate @@ -184,9 +170,8 @@ class FoodEntryBloc extends Bloc { await storage.writeEntriesForDate(event.forDate, entriesForDate); storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting); - var entriesFromStorage = await storage.getEntriesForDate(event.forDate); var newFoodEntries = state.foodEntries; - newFoodEntries.addAll({event.forDate: entriesFromStorage}); + newFoodEntries.addAll({event.forDate: entriesForDate}); emit(GlobalEntryState(foodEntries: newFoodEntries)); }); diff --git a/lib/food_entry/food_entry_widget.dart b/lib/food_entry/food_entry_widget.dart index 0c162ae..1e5d7c4 100644 --- a/lib/food_entry/food_entry_widget.dart +++ b/lib/food_entry/food_entry_widget.dart @@ -1,8 +1,8 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright (C) 2024 Marco Groß */ -import 'package:flutter/material.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/utils/row_with_spacers_widget.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class FoodEntryWidget extends StatefulWidget { @@ -24,6 +24,8 @@ class FoodEntryWidget extends StatefulWidget { } class _FoodEntryWidgetState extends State { + final animationDuration = const Duration(milliseconds: 150); + @override void initState() { super.initState(); @@ -32,91 +34,94 @@ class _FoodEntryWidgetState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => widget.onTap(context, widget.entry), - child: Stack( - children: [ - Positioned.fill( - child: Stack(children: [ - Positioned.fill( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: RowWidget( - widget.entry.waitingForNetwork - ? const Center(child: CircularProgressIndicator()) - : Text(widget.entry.name), - widget.entry.waitingForNetwork - ? Container() - : Text(widget.entry.mass.ceil().toString(), - textAlign: TextAlign.end), - Opacity( - opacity: widget.entry.isSelected ? 0.0 : 1.0, - child: widget.entry.waitingForNetwork - ? Container() - : Text(widget.entry.kcalPer100.ceil().toString(), - textAlign: TextAlign.end), - ), - Opacity( - opacity: widget.entry.isSelected ? 0.0 : 1.0, - child: widget.entry.waitingForNetwork - ? Container() - : Text( - (widget.entry.mass * - widget.entry.kcalPer100 / - 100) - .ceil() - .toString(), - textAlign: TextAlign.end), - ), - ), - ), - ), - Opacity( - opacity: widget.entry.isSelected ? 0.66 : 0.0, - child: Container( - color: Theme.of(context).colorScheme.secondary)), - ]), - ), - Opacity( - opacity: widget.entry.isSelected ? 1.0 : 0.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - child: IconButton( - padding: const EdgeInsets.all(0.0), - icon: const Icon(Icons.edit), - onPressed: widget.entry.isSelected - ? () async { - widget.onTap(context, widget.entry); - await showDialog( - context: context, - builder: (dialogContext) { - return FoodEntryChangeDialog( - entry: widget.entry, - onChange: (context, entry) { - widget.onChange(context, entry); - }); - }, - ); - } - : null), - ), - SizedBox( - child: IconButton( - padding: const EdgeInsets.all(0.0), - iconSize: 24, - icon: const Icon(Icons.delete), - color: Colors.redAccent, - onPressed: widget.entry.isSelected - ? () => widget.onDelete(context, widget.entry.id) - : null), - ), - ], + onTap: () { + widget.onTap(context, widget.entry); + }, + child: Stack(children: [ + RowWidget( + showDividers: !widget.entry.isSelected, + widget.entry.waitingForNetwork + ? const Center(child: CircularProgressIndicator()) + : Text(widget.entry.name), + AnimatedOpacity( + duration: animationDuration, + opacity: widget.entry.isSelected ? 0.0 : 1.0, + child: widget.entry.waitingForNetwork + ? Container() + : Text(widget.entry.mass.ceil().toString(), + textAlign: TextAlign.end), + ), + AnimatedOpacity( + duration: animationDuration, + opacity: widget.entry.isSelected ? 0.0 : 1.0, + child: widget.entry.waitingForNetwork + ? Container() + : Text(widget.entry.kcalPer100.ceil().toString(), + textAlign: TextAlign.end), + ), + AnimatedOpacity( + duration: animationDuration, + opacity: widget.entry.isSelected ? 0.0 : 1.0, + child: widget.entry.waitingForNetwork + ? Container() + : Text( + (widget.entry.mass * widget.entry.kcalPer100 / 100) + .ceil() + .toString(), + textAlign: TextAlign.end), ), ), - ], - ), - ); + Positioned.fill( + child: Stack(children: [ + AnimatedOpacity( + duration: animationDuration, + opacity: widget.entry.isSelected ? 0.66 : 0.0, + child: Container( + color: Theme.of(context).colorScheme.secondary, + ), + ), + AnimatedOpacity( + duration: animationDuration, + opacity: widget.entry.isSelected ? 1.0 : 0.0, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 64, + child: IconButton( + padding: const EdgeInsets.all(0.0), + icon: const Icon(Icons.edit), + onPressed: widget.entry.isSelected + ? () async { + widget.onTap(context, widget.entry); + await showDialog( + context: context, + builder: (dialogContext) { + return FoodEntryChangeDialog( + entry: widget.entry, + onChange: (context, entry) { + widget.onChange( + context, entry); + }); + }); + } + : null)), + SizedBox( + width: 64, + child: IconButton( + padding: const EdgeInsets.all(0.0), + icon: const Icon( + Icons.delete, + color: Colors.redAccent, + ), + onPressed: widget.entry.isSelected + ? () => + widget.onDelete(context, widget.entry.id) + : null)) + ]))) + ])) + ])); } } diff --git a/lib/food_scan/food_fact_lookup.dart b/lib/food_scan/food_fact_lookup.dart index 27f226c..de425bf 100644 --- a/lib/food_scan/food_fact_lookup.dart +++ b/lib/food_scan/food_fact_lookup.dart @@ -77,19 +77,23 @@ class FoodFactModel { } } - String quantityString = json['product']['product_quantity'] ?? "0"; - double quantity; - + int quantityForModel = 0; try { - quantity = double.parse(quantityString); + String quantityString = json['product']['product_quantity'] ?? "0"; + quantityForModel = double.parse(quantityString).ceil(); } catch (e) { - quantity = 0; + try { + quantityForModel = + (json['product']['product_quantity'] as num).toDouble().ceil(); + } catch (e) { + quantityForModel = 0; + } } return FoodFactModel( name: json['product']['product_name'] ?? "", kcalPer100g: kcalPer100gForModel, - mass: quantity.ceil(), + mass: quantityForModel, ); } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d6937d7..7a28583 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,9 +1,12 @@ { + "today": "Heute", "ok": "OK", "name": "Name", "amount": "Menge", - "amountPer": "Menge in 100 g/ml", - "kcalper": "kcal pro 100 g/ml", + "amountPer": "Menge in g oder ml", + "kcal": "kcal", + "kcalper": "kcal pro 100 g oder ml", + "kcalSum": "kcal gesamt", "kcalToday": "kcal heute", "menu": "Menü", "settings": "Einstellungen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 59fe373..eee3525 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,9 +1,12 @@ { + "today": "Today", "ok": "OK", "name": "Name", "amount": "Amount", - "amountPer": "Amount in 100 g/ml", - "kcalper": "kcal per 100 g/ml", + "amountPer": "Amount in g or ml", + "kcal": "kcal", + "kcalper": "kcal per 100 g or ml", + "kcalSum": "kcal total", "kcalToday": "kcal today", "menu": "Menu", "settings": "Settings", diff --git a/lib/main.dart b/lib/main.dart index 2b8e012..ea9f2ff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright (C) 2024 Marco Groß */ + import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/perdate/perdate_pageview_controller.dart'; import 'package:calorimeter/storage/storage.dart'; @@ -8,103 +9,111 @@ 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:go_router/go_router.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -List entriesForToday = []; -DateTime timeNow = DateTime.now(); - void main() async { WidgetsFlutterBinding.ensureInitialized(); - var storage = await FoodStorage.create(); - await storage.buildFoodLookupDatabase(); - - timeNow = DateTimeHelper.now(); - entriesForToday = await storage.getEntriesForDate(timeNow); - - var kcalLimit = await storage.readLimit(); - var brightness = await storage.readBrightness(); + var foodStorage = await FoodStorage.create(); + await foodStorage.buildFoodLookupDatabase(); runApp( - MainApp( - storage: storage, - kcalLimit: kcalLimit, - brightness: brightness, - ), + MainApp(storage: foodStorage), ); } -class MainApp extends StatelessWidget { +class MainApp extends StatefulWidget { final FoodStorage storage; - final double kcalLimit; - final String brightness; - const MainApp( - {required this.storage, - required this.kcalLimit, - required this.brightness, - super.key}); + const MainApp({super.key, required this.storage}); + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + late DateTime timeNow; + late List entriesForToday; + + late double kcalLimit; + late String brightness; + + late Future? initFuture; + + Future asyncInit() async { + timeNow = DateTimeHelper.now(); + entriesForToday = await widget.storage.getEntriesForDate(timeNow); + + kcalLimit = await widget.storage.readLimit(); + brightness = await widget.storage.readBrightness(); + + return true; + } + + @override + void initState() { + super.initState(); + initFuture = asyncInit(); + } @override Widget build(BuildContext context) { return SafeArea( - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => FoodEntryBloc( - storage: storage, - initialState: - GlobalEntryState(foodEntries: {timeNow: entriesForToday}), - ), - ), - BlocProvider( - create: (context) => SettingsDataBloc( - SettingsState(kcalLimit: kcalLimit), - storage: storage), - ), - BlocProvider( - create: (context) => ThemeDataBloc( - ThemeState(brightness: brightness), - storage: storage), - ), - ], - child: BlocBuilder( - builder: (context, state) { - var newBrightness = Brightness.light; - if (state.brightness == 'dark') { - newBrightness = Brightness.dark; + child: FutureBuilder( + future: initFuture, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return Center(child: CircularProgressIndicator()); } - return MaterialApp.router( - routerConfig: GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) { - return PerDatePageViewController( - initialDate: DateTimeHelper.now(), - ); - }, + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => FoodEntryBloc( + storage: widget.storage, + initialState: GlobalEntryState( + foodEntries: {timeNow: entriesForToday}), ), - ], - ), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: [ - Locale('en'), - Locale('de'), - ], - theme: ThemeData( - dividerTheme: const DividerThemeData(space: 2), - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.lightBlue, - brightness: newBrightness, ), + BlocProvider( + create: (context) => SettingsDataBloc( + SettingsState(kcalLimit: kcalLimit), + storage: widget.storage), + ), + BlocProvider( + create: (context) => ThemeDataBloc( + ThemeState(brightness: brightness), + storage: widget.storage), + ), + ], + child: BlocBuilder( + builder: (context, state) { + var newBrightness = Brightness.light; + if (state.brightness == 'dark') { + newBrightness = Brightness.dark; + } + + return MaterialApp( + home: PerDatePageViewController( + initialDate: DateTimeHelper.now()), + localizationsDelegates: + AppLocalizations.localizationsDelegates, + supportedLocales: [ + Locale('en'), + ...AppLocalizations.supportedLocales, + ], + theme: ThemeData( + dividerTheme: const DividerThemeData(space: 2), + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.lightBlue, + brightness: newBrightness, + ), + ), + ); + }, ), ); - }, - ), - ), + }), ); } } diff --git a/lib/perdate/entry_list.dart b/lib/perdate/entry_list.dart index ccb4c6c..a4c7e0e 100644 --- a/lib/perdate/entry_list.dart +++ b/lib/perdate/entry_list.dart @@ -1,10 +1,14 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright (C) 2024 Marco Groß */ + import 'package:calorimeter/food_entry/enter_food_widget.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_widget.dart'; +import 'package:calorimeter/storage/storage.dart'; +import 'package:calorimeter/utils/row_with_spacers_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class FoodEntryList extends StatelessWidget { final List entries; @@ -18,51 +22,77 @@ class FoodEntryList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( - itemCount: entries.length + 1, - itemBuilder: (BuildContext itemBuilderContext, int listIndex) { - //last item in list is the widget to enter food - if (listIndex == entries.length) { - return Column( - children: [ - EnterFoodWidget( - onAdd: (context, entry) { + var headerStyle = TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface); + + return Column( + children: [ + if (entries.isNotEmpty) + RowWidget( + showDividers: true, + Text(AppLocalizations.of(context)!.name, style: headerStyle), + Align( + alignment: Alignment.centerRight, + child: Text(AppLocalizations.of(context)!.amountPer, + style: headerStyle), + ), + Align( + alignment: Alignment.centerRight, + child: Text(AppLocalizations.of(context)!.kcalper, + style: headerStyle), + ), + Align( + alignment: Alignment.centerRight, + child: Text(AppLocalizations.of(context)!.kcalSum, + style: headerStyle), + ), + ), + if (entries.isNotEmpty) Divider(), + Expanded( + child: ListView.separated( + itemCount: entries.length + 1, + separatorBuilder: (context, index) { + return Divider(); + }, + itemBuilder: (BuildContext itemBuilderContext, int listIndex) { + //last item in list is the widget to enter food + if (listIndex == entries.length) { + return EnterFoodWidget( + foodEntryLookupDatabase: + FoodStorage.getInstance().getFoodEntryLookupDatabase, + onAdd: (context, entry) { + context + .read() + .add(FoodEntryEvent(entry: entry, forDate: date)); + }, + ); + } + + var entryIndex = listIndex; + return FoodEntryWidget( + key: ValueKey(entries[entryIndex].id), + entry: entries[entryIndex], + onDelete: (_, id) { context .read() - .add(FoodEntryEvent(entry: entry, forDate: date)); + .add(FoodDeletionEvent(entryID: id, forDate: date)); }, - ), - const SizedBox(height: 75), - ], - ); - } - - var entryIndex = listIndex; - return Column( - children: [ - FoodEntryWidget( - key: ValueKey(entries[entryIndex].id), - entry: entries[entryIndex], - onDelete: (_, id) { - context - .read() - .add(FoodDeletionEvent(entryID: id, forDate: date)); - }, - onChange: (_, changedEntry) { - context.read().add( - FoodChangedEvent(newEntry: changedEntry, forDate: date), - ); - }, - onTap: (_, tappedEntry) { - context.read().add( - FoodEntryTapped(entry: tappedEntry, forDate: date), - ); - }, - ), - const Divider(), - ], - ); - }, + onChange: (_, changedEntry) { + context.read().add( + FoodChangedEvent(newEntry: changedEntry, forDate: date), + ); + }, + onTap: (_, tappedEntry) { + context.read().add( + FoodEntryTapped(entry: tappedEntry, forDate: date), + ); + }, + ); + }, + ), + ), + ], ); } } diff --git a/lib/perdate/perdate_pageview.dart b/lib/perdate/perdate_pageview.dart index fac9ec2..49dd214 100644 --- a/lib/perdate/perdate_pageview.dart +++ b/lib/perdate/perdate_pageview.dart @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright (C) 2024 Marco Groß */ + import 'dart:developer'; import 'package:calorimeter/perdate/perdate_pageview_controller.dart'; diff --git a/lib/perdate/perdate_pageview_controller.dart b/lib/perdate/perdate_pageview_controller.dart index aa02261..4094110 100644 --- a/lib/perdate/perdate_pageview_controller.dart +++ b/lib/perdate/perdate_pageview_controller.dart @@ -39,22 +39,9 @@ class PerDatePageViewController extends StatelessWidget { 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( + child: Builder( + builder: (context) { + return Scaffold( appBar: AppBar( title: Builder(builder: (context) { return Text(DateFormat.yMMMMd( @@ -73,40 +60,45 @@ class PerDatePageViewController extends StatelessWidget { }), ), drawer: const AppDrawer(), - floatingActionButton: OverflowBar(children: [ - ScanFoodFAB( - onPressed: () { - context.read().add( - BarcodeAboutToBeScanned( - 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); - }, - ), - ]), + floatingActionButton: _getFABs(context), floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, body: PerDatePageView( pageController: pageController, initialDate: initialDate, ), - ), - ); - }), + ); + }, + ), + ); + } + + OverflowBar _getFABs(BuildContext context) { + return OverflowBar( + children: [ + ScanFoodFAB( + onPressed: () { + context.read().add( + BarcodeAboutToBeScanned( + 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); + }, + ), + ], ); } } @@ -115,6 +107,7 @@ class PageViewStateProvider with ChangeNotifier { DateTime _displayedDate; final List _visitedIndexes; bool _backButtonWasPressed = false; + bool _isVisible = false; PageViewStateProvider({required DateTime initialDate, int initialOffset = 0}) : _displayedDate = initialDate, @@ -131,6 +124,9 @@ class PageViewStateProvider with ChangeNotifier { notifyListeners(); } + void setVisible(vis) => _isVisible = true; + get isVisible => _isVisible; + get visitedIndexes => _visitedIndexes; void addVisitedIndex(int index) { diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index b2cc27f..55e910e 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright (C) 2024 Marco Groß */ + import 'dart:io'; import 'package:calorimeter/food_entry/food_entry_bloc.dart'; @@ -87,6 +88,7 @@ class FoodStorage { String fullString = ''; for (var entry in foodEntries) { + if (entry.waitingForNetwork) continue; fullString += '${entry.toString()}\n'; } diff --git a/lib/utils/app_drawer.dart b/lib/utils/app_drawer.dart index 7cf7300..2469edc 100644 --- a/lib/utils/app_drawer.dart +++ b/lib/utils/app_drawer.dart @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright (C) 2024 Marco Groß */ +import 'package:calorimeter/perdate/perdate_pageview_controller.dart'; +import 'package:calorimeter/utils/date_time_helper.dart'; import 'package:calorimeter/utils/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -29,6 +31,16 @@ class AppDrawer extends StatelessWidget { title: Text(AppLocalizations.of(context)!.menu), ), ), + ListTile( + title: Text(AppLocalizations.of(context)!.today), + trailing: const Icon(Icons.home), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => PerDatePageViewController( + initialDate: DateTimeHelper.now()))); + }, + ), ListTile( title: Text(AppLocalizations.of(context)!.settings), trailing: const Icon(Icons.settings), diff --git a/lib/utils/row_with_spacers_widget.dart b/lib/utils/row_with_spacers_widget.dart index 61b9df3..0c7e98e 100644 --- a/lib/utils/row_with_spacers_widget.dart +++ b/lib/utils/row_with_spacers_widget.dart @@ -7,19 +7,61 @@ class RowWidget extends StatelessWidget { final Widget? widget2; final Widget? widget3; final Widget? widget4; + final bool showDividers; const RowWidget(this.widget1, this.widget2, this.widget3, this.widget4, - {super.key}); + {super.key, required this.showDividers}); @override Widget build(BuildContext context) { - return Row( - children: [ - Expanded(flex: 10, child: widget1 ?? Container()), - Expanded(flex: 6, child: widget2 ?? Container()), - Expanded(flex: 6, child: widget3 ?? Container()), - Expanded(flex: 6, child: widget4 ?? Container()), - ], + return IntrinsicHeight( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: 48), + child: Row( + children: [ + Expanded( + flex: 10, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: widget1 ?? Container(), + ), + ), + Opacity( + opacity: showDividers ? 1.0 : 0.0, + child: VerticalDivider(), + ), + Expanded( + flex: 6, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: widget2 ?? Container(), + ), + ), + Opacity( + opacity: showDividers ? 1.0 : 0.0, + child: VerticalDivider(), + ), + Expanded( + flex: 6, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: widget3 ?? Container(), + ), + ), + Opacity( + opacity: showDividers ? 1.0 : 0.0, + child: VerticalDivider(), + ), + Expanded( + flex: 6, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: widget4 ?? Container(), + ), + ), + ], + ), + ), ); } } diff --git a/lib/utils/settings.dart b/lib/utils/settings.dart index 3c37a96..dc18662 100644 --- a/lib/utils/settings.dart +++ b/lib/utils/settings.dart @@ -36,6 +36,8 @@ class _SettingsWidgetState extends State { return AlertDialog( title: Text(AppLocalizations.of(context)!.dayLimit), content: TextField( + decoration: InputDecoration( + hintText: state.kcalLimit.toString()), controller: kcalPerDayCtrl, onSubmitted: (val) => submitDailyKcal()), actions: [ @@ -61,7 +63,7 @@ class _SettingsWidgetState extends State { try { setting = double.parse(kcalPerDayCtrl.text); } catch (e) { - setting = 2000.0; + setting = context.read().dailyKcal; } context.read().add(DailyKcalLimitUpdated(kcal: setting)); Navigator.of(context).pop(); diff --git a/lib/utils/settings_bloc.dart b/lib/utils/settings_bloc.dart index 5913e5c..f7d8235 100644 --- a/lib/utils/settings_bloc.dart +++ b/lib/utils/settings_bloc.dart @@ -15,6 +15,8 @@ class SettingsDataBloc extends Bloc { await storage.updateLimit(event.kcal); emit(SettingsState(kcalLimit: event.kcal)); } + + get dailyKcal => state.kcalLimit; } class SettingsEvent {} diff --git a/pubspec.lock b/pubspec.lock index 20e7c76..8d79fc6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,6 +70,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + url: "https://pub.dev" + source: hosted + version: "4.0.3" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + url: "https://pub.dev" + source: hosted + version: "2.4.14" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + url: "https://pub.dev" + source: hosted + version: "8.9.3" characters: dependency: transitive description: @@ -102,6 +166,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" collection: dependency: transitive description: @@ -134,6 +206,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + url: "https://pub.dev" + source: hosted + version: "3.0.1" fake_async: dependency: transitive description: @@ -154,10 +234,10 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.0" fixnum: dependency: transitive description: @@ -179,6 +259,11 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -218,6 +303,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -234,6 +324,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.6.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http: + dependency: "direct main" + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -246,10 +352,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" image: dependency: transitive description: @@ -258,6 +364,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -370,6 +481,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + url: "https://pub.dev" + source: hosted + version: "5.4.5" nested: dependency: transitive description: @@ -435,7 +554,7 @@ packages: source: hosted version: "2.2.1" path_provider_platform_interface: - dependency: transitive + dependency: "direct main" description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" @@ -462,12 +581,12 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" plugin_platform_interface: - dependency: transitive + dependency: "direct main" description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" @@ -490,6 +609,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" protobuf: dependency: transitive description: @@ -514,6 +641,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.5" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + url: "https://pub.dev" + source: hosted + version: "1.4.0" settings_ui: dependency: "direct main" description: @@ -559,6 +694,14 @@ packages: description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" source_map_stack_trace: dependency: transitive description: @@ -607,6 +750,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -615,6 +766,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -647,6 +806,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: @@ -719,6 +886,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + url: "https://pub.dev" + source: hosted + version: "3.0.4" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 33d683b..0ea808c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,13 +19,20 @@ dependencies: barcode_scan2: ^4.3.3 provider: ^6.1.2 test: ^1.25.7 - go_router: ^14.3.0 + go_router: ^14.6.2 + path_provider_platform_interface: ^2.1.2 + plugin_platform_interface: ^2.1.8 + http: ^1.2.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 flutter_launcher_icons: ^0.14.1 + integration_test: + sdk: flutter + mockito: ^5.4.5 + build_runner: ^2.4.14 flutter: uses-material-design: true diff --git a/test/split_with_ignore_test.dart b/test/split_with_ignore_test.dart deleted file mode 100644 index af7e403..0000000 --- a/test/split_with_ignore_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:calorimeter/storage/storage.dart'; -import 'package:test/test.dart'; - -void main() { - group( - 'Test custom split with ignore', - () { - test('string without ignoring', () { - var testString = 'This is a test string'; - var resultingList = testString.splitWithIgnore(' '); - - expect(resultingList[0], equals('This')); - expect(resultingList[1], equals('is')); - expect(resultingList[2], equals('a')); - expect(resultingList[3], equals('test')); - expect(resultingList[4], equals('string')); - }); - - test('string that does not contain the ignored character', () { - var testString = 'This is a test string'; - var resultingList = testString.splitWithIgnore(' ', ignoreIn: '"'); - - expect(resultingList[0], equals('This')); - expect(resultingList[1], equals('is')); - expect(resultingList[2], equals('a')); - expect(resultingList[3], equals('test')); - expect(resultingList[4], equals('string')); - }); - - test( - 'string that contains ignored character', - () { - var testString = 'This is "a test" string'; - var resultingList = testString.splitWithIgnore(' ', ignoreIn: '"'); - - expect(resultingList[0], equals('This')); - expect(resultingList[1], equals('is')); - expect(resultingList[2], equals('"a test"')); - expect(resultingList[3], equals('string')); - }, - ); - - test( - 'string that contains commas that should be ignored', - () { - var testString = - 'f9a96b80-71f9-11ef-8df4-f3628a737a16,"Erdnüsse, geröstet",120.0,100.0'; - var resultingList = testString.splitWithIgnore(',', ignoreIn: '"'); - - expect( - resultingList[0], equals('f9a96b80-71f9-11ef-8df4-f3628a737a16')); - expect(resultingList[1], equals('"Erdnüsse, geröstet"')); - expect(resultingList[2], equals('120.0')); - expect(resultingList[3], equals('100.0')); - }, - ); - }, - ); -}