Compare commits

..

7 Commits

Author SHA1 Message Date
a3eb907a8e Merge pull request 'Change behavior of global state' (#11) from fix-global-state into master
Reviewed-on: #11
2025-01-12 16:24:34 +00:00
7728ec3b66 Change behavior of global state
Until now, FoodEntryBloc (which is holding the global state  for every
day) would cause a change in every widget in the tree. For example, when
an entry for one day gets added, all other entries in opened days would
also be rebuilt.

Now, the GlobalState will be emitted with an additional date, which
signals, which date caused the state change.
With this information, I selectively only build the EntryLists that
needs to be rebuilt.

Additionally, the calendar FAB will push a new route instead of
navigating to a new day by utilizing the pageController.
2025-01-12 17:23:59 +01:00
7126b1b593 Remove go_router 2025-01-05 19:29:28 +01:00
e1fdefe979 Merge pull request 'Prepare v1.0.5' (#10) from prepare_v1.0.5 into master
Reviewed-on: #10
2025-01-05 16:33:27 +00:00
435ad4e618 Prepare v1.0.5 2025-01-05 17:31:17 +01:00
0aca111cb5 Merge pull request 'Overhaul ui and remove BackButtonListener' (#9) from home-button-in-drawer into master
Reviewed-on: #9
2025-01-05 16:27:57 +00:00
2509c1721c Overhaul ui and remove BackButtonListener
1. Make EnterFoodWidget animated
2. Fix exception when reading quantity for a food.

Introduce first integration test
2025-01-05 17:25:34 +01:00
27 changed files with 774 additions and 445 deletions

2
.gitignore vendored
View File

@ -42,3 +42,5 @@ app.*.map.json
/android/app/profile
/android/app/release
assets/icon_base.xcf
/metadata/**/*.xcf

View File

@ -34,8 +34,8 @@ android {
applicationId = "de.swgross.calorimeter"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = 4
versionName = "1.0.4"
versionCode = 5
versionName = "1.0.5"
}
signingConfigs {
@ -48,6 +48,10 @@ android {
}
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
release {
signingConfig = signingConfigs.release
}

View File

@ -0,0 +1,59 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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<String>, "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);
});
});
}

View File

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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<String, int> foodEntryLookupDatabase;
const EnterFoodWidget({super.key, required this.onAdd});
const EnterFoodWidget(
{super.key, required this.onAdd, required this.foodEntryLookupDatabase});
@override
State<EnterFoodWidget> createState() => _EnterFoodWidgetState();
@ -19,89 +21,123 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
late TextEditingController nameController;
late TextEditingController massController;
late TextEditingController kcalPerMassController;
late Map<String, int> 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<String>(
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<String>.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<String>(
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<String>.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<EnterFoodWidget> {
nameController.text = "";
massController.text = "";
kcalPerMassController.text = "";
open = false;
});
}
}

View File

@ -27,7 +27,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
var newList = await storage.getEntriesForDate(event.forDate);
state.foodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: state.foodEntries));
emit(GlobalEntryState(
foodEntries: state.foodEntries, stateChangedForDate: event.forDate));
}
void handleFoodEntryEvent(
@ -40,14 +41,11 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
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));
emit(GlobalEntryState(
foodEntries: newFoodEntries, stateChangedForDate: event.forDate));
}
void handleFoodChangedEvent(
@ -65,14 +63,11 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
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));
emit(GlobalEntryState(
foodEntries: newFoodEntries, stateChangedForDate: event.forDate));
}
void handleDeleteFoodEvent(
@ -84,14 +79,11 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
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));
emit(GlobalEntryState(
foodEntries: newFoodEntries, stateChangedForDate: event.forDate));
}
void handleBarcodeScannedEvent(
@ -103,23 +95,24 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
if (e.code == BarcodeScanner.cameraAccessDenied) {
emit(GlobalEntryState(
foodEntries: state.foodEntries,
stateChangedForDate: event.forDate,
appError:
GlobalAppError(GlobalAppErrorType.errCameraPermissionDenied)));
}
return;
}
var client = FoodFactLookupClient();
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var client = FoodFactLookupClient();
if (scanResult.type == ResultType.Cancelled) {
return;
}
if (scanResult.type == ResultType.Error) {
emit(GlobalEntryState(
foodEntries: state.foodEntries,
stateChangedForDate: event.forDate,
appError: GlobalAppError(GlobalAppErrorType.errGeneralError)));
return;
}
@ -134,9 +127,11 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
);
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,
stateChangedForDate: event.forDate,
));
await responseFuture.then((response) async {
var index = entriesForDate
@ -154,6 +149,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
emit(GlobalEntryState(
foodEntries: newFoodEntries,
stateChangedForDate: event.forDate,
appError: GlobalAppError(GlobalAppErrorType.errbarcodeNotFound)));
return;
}
@ -165,6 +161,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
emit(GlobalEntryState(
foodEntries: newFoodEntries,
stateChangedForDate: event.forDate,
appError:
GlobalAppError(GlobalAppErrorType.errServerNotReachable)));
return;
@ -184,11 +181,11 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
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));
emit(GlobalEntryState(
foodEntries: newFoodEntries, stateChangedForDate: event.forDate));
});
}
@ -209,7 +206,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
selectedEntry.isSelected = !oldStateOfTappedEntry;
emit(GlobalEntryState(foodEntries: state.foodEntries));
emit(GlobalEntryState(
foodEntries: state.foodEntries, stateChangedForDate: event.forDate));
}
}
@ -255,12 +253,17 @@ class PermissionException extends FoodEvent {
PermissionException({required super.forDate});
}
/// This is the state for one date/page
class PageEntryState {}
class GlobalEntryState {
final Map<DateTime, List<FoodEntryState>> foodEntries;
final GlobalAppError? appError;
GlobalEntryState({required this.foodEntries, this.appError});
//we use this to only redraw pages whose entries changed
final DateTime? stateChangedForDate;
GlobalEntryState(
{required this.foodEntries, this.stateChangedForDate, this.appError});
factory GlobalEntryState.init() {
return GlobalEntryState(foodEntries: {});

View File

@ -1,8 +1,8 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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<FoodEntryWidget> {
final animationDuration = const Duration(milliseconds: 150);
@override
void initState() {
super.initState();
@ -32,91 +34,94 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
@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))
])))
]))
]));
}
}

View File

@ -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,
);
}
}

View File

@ -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",

View File

@ -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",

View File

@ -1,5 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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<FoodEntryState> 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<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
late DateTime timeNow;
late List<FoodEntryState> entriesForToday;
late double kcalLimit;
late String brightness;
late Future<bool>? initFuture;
Future<bool> 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<ThemeDataBloc, ThemeState>(
builder: (context, state) {
var newBrightness = Brightness.light;
if (state.brightness == 'dark') {
newBrightness = Brightness.dark;
child: FutureBuilder<Object>(
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<ThemeDataBloc, ThemeState>(
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,
),
),
);
},
),
);
},
),
),
}),
);
}
}

View File

@ -1,10 +1,14 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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<FoodEntryState> 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<FoodEntryBloc>()
.add(FoodEntryEvent(entry: entry, forDate: date));
},
);
}
var entryIndex = listIndex;
return FoodEntryWidget(
key: ValueKey(entries[entryIndex].id),
entry: entries[entryIndex],
onDelete: (_, id) {
context
.read<FoodEntryBloc>()
.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<FoodEntryBloc>()
.add(FoodDeletionEvent(entryID: id, forDate: date));
},
onChange: (_, changedEntry) {
context.read<FoodEntryBloc>().add(
FoodChangedEvent(newEntry: changedEntry, forDate: date),
);
},
onTap: (_, tappedEntry) {
context.read<FoodEntryBloc>().add(
FoodEntryTapped(entry: tappedEntry, forDate: date),
);
},
),
const Divider(),
],
);
},
onChange: (_, changedEntry) {
context.read<FoodEntryBloc>().add(
FoodChangedEvent(newEntry: changedEntry, forDate: date),
);
},
onTap: (_, tappedEntry) {
context.read<FoodEntryBloc>().add(
FoodEntryTapped(entry: tappedEntry, forDate: date),
);
},
);
},
),
),
],
);
}
}

View File

@ -1,5 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'dart:developer';
import 'package:calorimeter/perdate/perdate_pageview_controller.dart';

View File

@ -39,22 +39,9 @@ class PerDatePageViewController extends StatelessWidget {
initialDate: initialDate,
initialOffset: initialOffset,
),
child: Builder(builder: (context) {
return BackButtonListener(
onBackButtonPressed: () async {
context.read<PageViewStateProvider>().backButtonWasPressed = true;
var visitedIndexes =
context.read<PageViewStateProvider>().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,44 @@ class PerDatePageViewController extends StatelessWidget {
}),
),
drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [
ScanFoodFAB(
onPressed: () {
context.read<FoodEntryBloc>().add(
BarcodeAboutToBeScanned(
forDate: context
.read<PageViewStateProvider>()
.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<FoodEntryBloc>().add(
BarcodeAboutToBeScanned(
forDate:
context.read<PageViewStateProvider>().displayedDate,
),
);
},
),
const SizedBox(width: 8),
CalendarFAB(
startFromDate: DateTimeHelper.now(),
onDateSelected: (dateSelected) {
if (dateSelected == null) return;
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
PerDatePageViewController(initialDate: dateSelected)));
},
),
],
);
}
}
@ -115,6 +106,7 @@ class PageViewStateProvider with ChangeNotifier {
DateTime _displayedDate;
final List<int> _visitedIndexes;
bool _backButtonWasPressed = false;
bool _isVisible = false;
PageViewStateProvider({required DateTime initialDate, int initialOffset = 0})
: _displayedDate = initialDate,
@ -131,6 +123,9 @@ class PageViewStateProvider with ChangeNotifier {
notifyListeners();
}
void setVisible(vis) => _isVisible = true;
get isVisible => _isVisible;
get visitedIndexes => _visitedIndexes;
void addVisitedIndex(int index) {

View File

@ -33,6 +33,11 @@ class _PerDateWidgetState extends State<PerDateWidget>
showNewSnackbarWith(context, pageState.appError!);
}
},
buildWhen: (previous, current) {
if (current.stateChangedForDate == null) return true;
if (current.stateChangedForDate == widget.date) return true;
return false;
},
builder: (context, pageState) {
return FoodEntryList(
entries: pageState.foodEntries[widget.date] ?? [],

View File

@ -1,5 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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';
}

View File

@ -1,5 +1,7 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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),

View File

@ -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(),
),
),
],
),
),
);
}
}

View File

@ -36,6 +36,8 @@ class _SettingsWidgetState extends State<SettingsWidget> {
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<SettingsWidget> {
try {
setting = double.parse(kcalPerDayCtrl.text);
} catch (e) {
setting = 2000.0;
setting = context.read<SettingsDataBloc>().dailyKcal;
}
context.read<SettingsDataBloc>().add(DailyKcalLimitUpdated(kcal: setting));
Navigator.of(context).pop();

View File

@ -15,6 +15,8 @@ class SettingsDataBloc extends Bloc<SettingsEvent, SettingsState> {
await storage.updateLimit(event.kcal);
emit(SettingsState(kcalLimit: event.kcal));
}
get dailyKcal => state.kcalLimit;
}
class SettingsEvent {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@ -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:
@ -205,11 +290,6 @@ 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:
@ -218,6 +298,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:
@ -226,14 +311,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
go_router:
dependency: "direct main"
graphs:
dependency: transitive
description:
name: go_router
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539"
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "14.6.2"
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 +339,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 +351,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 +468,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 +541,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 +568,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 +596,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 +628,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 +681,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 +737,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 +753,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 +793,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 +873,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:

View File

@ -19,13 +19,19 @@ dependencies:
barcode_scan2: ^4.3.3
provider: ^6.1.2
test: ^1.25.7
go_router: ^14.3.0
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

View File

@ -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'));
},
);
},
);
}