Compare commits

..

13 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
cfc712458f New flutter version 2024-12-24 13:32:06 +01:00
b87c288527 Version 1.0.4
Fix handling of kcal amount extraction from json
2024-12-23 19:39:51 +01:00
63e9b471b4 Merge pull request 'Fix json misalignment and error representation' (#8) from fix-json-misalignment-and-error-representation into master
Reviewed-on: #8
2024-12-22 17:29:20 +00:00
7b440e82aa Remove unused function 2024-12-22 18:23:25 +01:00
69bee8de7f fix everything 2024-12-22 18:13:29 +01:00
b5e0d19536 Fix locales
Reordering locales will make English default now.
Additionally, one translation bug was fixed.
2024-12-16 16:07:20 +01:00
28 changed files with 930 additions and 517 deletions

@ -1 +1 @@
Subproject commit dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
Subproject commit 17025dd88227cd9532c33fa78f5250d548d87e9a

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

@ -15,6 +15,7 @@ android {
namespace = "de.swgross.calorimeter"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
dependenciesInfo {
includeInApk = false
includeInBundle = false
@ -33,8 +34,8 @@ android {
applicationId = "de.swgross.calorimeter"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = 2
versionName = "1.0.2"
versionCode = 5
versionName = "1.0.5"
}
signingConfigs {
@ -47,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

@ -2,8 +2,11 @@
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_scan/food_fact_lookup.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:calorimeter/storage/storage.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
@ -16,7 +19,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodChangedEvent>(handleFoodChangedEvent);
on<FoodDeletionEvent>(handleDeleteFoodEvent);
on<BarcodeScanned>(handleBarcodeScannedEvent);
on<BarcodeAboutToBeScanned>(handleBarcodeScannedEvent);
on<FoodEntryTapped>(handleFoodEntryTapped);
}
void handlePageBeingInitialized(
@ -24,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(
@ -37,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(
@ -62,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(
@ -81,23 +79,32 @@ 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(
BarcodeScanned event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
BarcodeAboutToBeScanned event, Emitter<GlobalEntryState> emit) async {
ScanResult scanResult = ScanResult();
try {
scanResult = await BarcodeScanner.scan();
} on PlatformException catch (e) {
if (e.code == BarcodeScanner.cameraAccessDenied) {
emit(GlobalEntryState(
foodEntries: state.foodEntries,
stateChangedForDate: event.forDate,
appError:
GlobalAppError(GlobalAppErrorType.errCameraPermissionDenied)));
}
return;
}
var client = FoodFactLookupClient();
var scanResult = await event.scanResultFuture;
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
if (scanResult.type == ResultType.Cancelled) {
return;
@ -105,7 +112,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
if (scanResult.type == ResultType.Error) {
emit(GlobalEntryState(
foodEntries: state.foodEntries,
errorString: "Fehler beim Scannen des Barcodes"));
stateChangedForDate: event.forDate,
appError: GlobalAppError(GlobalAppErrorType.errGeneralError)));
return;
}
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
@ -117,10 +125,13 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
waitingForNetwork: true,
isSelected: false,
);
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
@ -138,7 +149,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
emit(GlobalEntryState(
foodEntries: newFoodEntries,
errorString: "Barcode konnte nicht gefunden werden."));
stateChangedForDate: event.forDate,
appError: GlobalAppError(GlobalAppErrorType.errbarcodeNotFound)));
return;
}
if (response.status ==
@ -149,7 +161,9 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
emit(GlobalEntryState(
foodEntries: newFoodEntries,
errorString: "OpenFoodFacts-Server konnte nicht erreicht werden."));
stateChangedForDate: event.forDate,
appError:
GlobalAppError(GlobalAppErrorType.errServerNotReachable)));
return;
}
@ -167,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));
});
}
@ -192,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));
}
}
@ -224,10 +239,8 @@ class FoodDeletionEvent extends FoodEvent {
FoodDeletionEvent({required this.entryID, required super.forDate});
}
class BarcodeScanned extends FoodEvent {
final Future<ScanResult> scanResultFuture;
BarcodeScanned({required this.scanResultFuture, required super.forDate});
class BarcodeAboutToBeScanned extends FoodEvent {
BarcodeAboutToBeScanned({required super.forDate});
}
class FoodEntryTapped extends FoodEvent {
@ -236,12 +249,21 @@ class FoodEntryTapped extends FoodEvent {
FoodEntryTapped({required this.entry, required super.forDate});
}
/// This is the state for one date/page
class PermissionException extends FoodEvent {
PermissionException({required super.forDate});
}
class PageEntryState {}
class GlobalEntryState {
final Map<DateTime, List<FoodEntryState>> foodEntries;
final String? errorString;
final GlobalAppError? appError;
GlobalEntryState({required this.foodEntries, this.errorString});
//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: {});
@ -305,3 +327,29 @@ class FoodEntryState {
return '$id,"$name",$mass,$kcalPer100';
}
}
enum GlobalAppErrorType {
errGeneralError,
errbarcodeNotFound,
errServerNotReachable,
errCameraPermissionDenied
}
class GlobalAppError {
final GlobalAppErrorType type;
GlobalAppError(this.type);
String toErrorString(BuildContext context) {
switch (type) {
case GlobalAppErrorType.errGeneralError:
return AppLocalizations.of(context)!.errGeneralBarcodeError;
case GlobalAppErrorType.errbarcodeNotFound:
return AppLocalizations.of(context)!.errBarcodeNotFound;
case GlobalAppErrorType.errServerNotReachable:
return AppLocalizations.of(context)!.errServerNotReachable;
case GlobalAppErrorType.errCameraPermissionDenied:
return AppLocalizations.of(context)!.errPermissionNotGranted;
}
}
}

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

@ -56,19 +56,45 @@ class FoodFactModel {
});
factory FoodFactModel.fromJson(Map<String, dynamic> json) {
String quantityString = json['product']['product_quantity'] ?? "0";
int quantity;
int kcalPer100gForModel = 0;
int kcalPer100g = 0;
int kcalPer100gPrepared = 0;
try {
quantity = int.parse(quantityString);
kcalPer100g = (json['product']['nutriments']['energy-kcal_100g'] as num)
.toDouble()
.ceil();
kcalPer100gForModel = kcalPer100g;
} catch (e) {
quantity = 0;
try {
kcalPer100gPrepared =
(json['product']['nutriments']['energy-kcal_prepared_100g'] as num)
.toDouble()
.ceil();
kcalPer100gForModel = kcalPer100gPrepared;
} catch (e) {
kcalPer100gForModel = 0;
}
}
int quantityForModel = 0;
try {
String quantityString = json['product']['product_quantity'] ?? "0";
quantityForModel = double.parse(quantityString).ceil();
} catch (e) {
try {
quantityForModel =
(json['product']['product_quantity'] as num).toDouble().ceil();
} catch (e) {
quantityForModel = 0;
}
}
return FoodFactModel(
name: json['product']['product_name'],
kcalPer100g: json['product']['nutriments']['energy-kcal_100g'],
mass: quantity);
name: json['product']['product_name'] ?? "",
kcalPer100g: kcalPer100gForModel,
mass: quantityForModel,
);
}
}

View File

@ -1,14 +1,21 @@
{
"today": "Heute",
"ok": "OK",
"name": "Name",
"amount": "Menge",
"amountPer": "Amount 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",
"yourSettings": "Deine persönlichen Einstellungen",
"dayLimit": "Kalorienlimit pro Tag",
"errAmountNotANumber": "Menge muss eine Zahl sein",
"errKcalNotANumber": "kcal muss eine Zahl sein"
"errKcalNotANumber": "kcal muss eine Zahl sein",
"errGeneralBarcodeError": "Fehler beim Scannen des Barcodes",
"errBarcodeNotFound": "Barcode konnte nicht gefunden werden.",
"errServerNotReachable": "OpenFoodFacts-Server konnte nicht erreicht werden.",
"errPermissionNotGranted": "Kamera-Berechtigung muss aktiviert werden."
}

View File

@ -1,14 +1,21 @@
{
"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",
"yourSettings": "Your personal settings",
"dayLimit": "Calorie limit per day",
"errAmountNotANumber": "Amount must be a number",
"errKcalNotANumber": "kcal must be a number"
"errKcalNotANumber": "kcal must be a number",
"errGeneralBarcodeError": "Error while scanning the barcode.",
"errBarcodeNotFound": "Barcode could not be found.",
"errServerNotReachable": "OpenFoodFacts server could not be reached.",
"errPermissionNotGranted": "Permission to use camera must be given."
}

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,101 +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(
//locale: Locale('de'),
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: AppLocalizations.supportedLocales,
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

@ -2,7 +2,6 @@
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'dart:developer';
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/perdate/perdate_pageview.dart';
import 'package:calorimeter/utils/app_drawer.dart';
@ -40,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(
@ -74,42 +60,44 @@ class PerDatePageViewController extends StatelessWidget {
}),
),
drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [
ScanFoodFAB(
onPressed: () {
var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add(
BarcodeScanned(
scanResultFuture: result,
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)));
},
),
],
);
}
}
@ -118,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,
@ -134,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

@ -29,10 +29,15 @@ class _PerDateWidgetState extends State<PerDateWidget>
return BlocConsumer<FoodEntryBloc, GlobalEntryState>(
listener: (context, pageState) {
if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!);
if (pageState.appError != null) {
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] ?? [],
@ -41,9 +46,10 @@ class _PerDateWidgetState extends State<PerDateWidget>
);
}
void showNewSnackbarWith(BuildContext context, String text) {
var snackbar =
ErrorSnackbar(colorScheme: Theme.of(context).colorScheme, text: text);
void showNewSnackbarWith(BuildContext context, GlobalAppError error) {
var snackbar = ErrorSnackbar(
colorScheme: Theme.of(context).colorScheme,
text: error.toErrorString(context));
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()

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

@ -5,31 +5,31 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.3.3"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.11.0"
archive:
dependency: transitive
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a"
url: "https://pub.dev"
source: hosted
version: "3.6.1"
version: "4.0.2"
args:
dependency: transitive
description:
@ -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,14 +166,22 @@ 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:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
convert:
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,38 +311,51 @@ 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:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
version: "4.5.2"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: "direct main"
description:
@ -270,10 +368,10 @@ packages:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.0.5"
js:
dependency: transitive
description:
@ -294,18 +392,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
@ -318,10 +416,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "5.1.1"
logging:
dependency: transitive
description:
@ -334,10 +432,10 @@ packages:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.3-main.0"
matcher:
dependency: transitive
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:
@ -390,10 +496,10 @@ packages:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
path:
dependency: transitive
description:
@ -414,10 +520,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "8c4967f8b7cb46dc914e178daa29813d83ae502e0529d7b0478330616a691ef7"
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev"
source: hosted
version: "2.2.14"
version: "2.2.15"
path_provider_foundation:
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"
@ -482,6 +588,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
posix:
dependency: transitive
description:
name: posix
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
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:
@ -502,10 +624,18 @@ packages:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
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:
@ -518,10 +648,10 @@ packages:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
@ -550,7 +680,15 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
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:
@ -563,10 +701,10 @@ packages:
dependency: transitive
description:
name: source_maps
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.12"
version: "0.10.13"
source_span:
dependency: transitive
description:
@ -587,10 +725,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.0"
stream_channel:
dependency: transitive
description:
@ -599,14 +737,30 @@ 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:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
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:
@ -619,26 +773,34 @@ packages:
dependency: "direct main"
description:
name: test
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
url: "https://pub.dev"
source: hosted
version: "1.25.7"
version: "1.25.8"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.3"
test_core:
dependency: transitive
description:
name: test_core
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
url: "https://pub.dev"
source: hosted
version: "0.6.4"
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:
@ -675,18 +837,18 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "14.3.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
web:
dependency: transitive
description:
@ -711,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:
@ -739,10 +909,10 @@ packages:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
sdks:
dart: ">=3.5.3 <4.0.0"
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.0"

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