Compare commits

..

12 Commits

Author SHA1 Message Date
ce373404ad Bloc is now global and responsible for all dates 2024-10-08 00:58:56 +02:00
cb18e1d1f0 Introduce PageView with manual switching of dates
1. Show PerDate widgets inside of an PageView
2. Introduce GoRouter so we can intercept back button taps with
   BackButtonListener
3. Implement rudimentary navigation
4. Fix bug that still showed a spinner event when the barcode was not
   found.
2024-10-06 02:20:08 +02:00
970cea8ba7 Oops, forgot to store the entries that were scanned. They just disappeared after restarting the app 2024-10-02 16:22:33 +02:00
37cceb3a9b Fix bug that caused infinite loop for selecting FoodEntries
And also, the Cancel button is removed when the FoodEntry is selected.
2024-09-25 18:20:50 +02:00
a70a7fd1e3 Remove debug statement 2024-09-25 17:43:42 +02:00
f28626c49e Move select logic for FoodEntry into BLoC 2024-09-25 17:33:40 +02:00
ace03d98d2 Handle changing a food entry 2024-09-24 17:23:01 +02:00
a7a7f44050 Make scanned widgets appear in the list instead of putting the info into
the EnterFoodWidget.

1. Make scanned foods appear in the list of foods
2. Remove Controller for entering food

This commit removes the EnterFoodController that was used to put a
scanned food into the EnterFoodWidget.
This is now unnecessary because scanning a product will be distributed
via the FoodBLoC.
2024-09-24 14:41:42 +02:00
e75a0765b4 Reverse the days for the lookup database so the newer entries are being suggested 2024-09-13 22:50:08 +02:00
9179cff38e Merge pull request 'Fix comma bug' (#6) from fix-comma-bug into master
Reviewed-on: #6
2024-09-13 20:39:00 +00:00
07a16c0642 Fix comma bug
This fixes a bug that was caused by a name containing a comma.
We store the food entries in a csv so logically a comma in the name,
causes the parser to crash.

We fix this, by wrapping the name in quotation marks and writing a new
parser for food entries.
2024-09-13 22:35:32 +02:00
0edb178963 Merge pull request 'Fix bug that left PerDate widget empty when popped to.' (#5) from fix-empty-page-bug into master
Reviewed-on: #5
2024-09-13 14:54:13 +00:00
15 changed files with 1156 additions and 353 deletions

View File

@ -1,12 +1,10 @@
import 'package:calorimeter/utils/enter_food_controller.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/utils/row_with_spacers_widget.dart'; import 'package:calorimeter/utils/row_with_spacers_widget.dart';
import 'package:provider/provider.dart';
class EnterFoodWidget extends StatefulWidget { class EnterFoodWidget extends StatefulWidget {
final Function(BuildContext context, FoodEntry entry) onAdd; final Function(BuildContext context, FoodEntryState entry) onAdd;
const EnterFoodWidget({super.key, required this.onAdd}); const EnterFoodWidget({super.key, required this.onAdd});
@ -18,7 +16,7 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
late TextEditingController nameController; late TextEditingController nameController;
late TextEditingController massController; late TextEditingController massController;
late TextEditingController kcalPerMassController; late TextEditingController kcalPerMassController;
late Map<String, double> suggestions; late Map<String, int> suggestions;
@override @override
void initState() { void initState() {
@ -32,18 +30,12 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<EnterFoodController>(
builder: (context, food, child) {
nameController.text = food.name;
kcalPerMassController.text = food.kcalPer100g;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: RowWidget( child: RowWidget(
Autocomplete<String>( Autocomplete<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.down, optionsViewOpenDirection: OptionsViewOpenDirection.down,
fieldViewBuilder: fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
(context, controller, focusNode, onSubmitted) {
nameController = controller; nameController = controller;
return TextFormField( return TextFormField(
controller: controller, controller: controller,
@ -67,17 +59,18 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
); );
}, },
onSelected: (selectedFood) { onSelected: (selectedFood) {
double kcalPerMassForSelectedFood = int kcalPerMassForSelectedFood = suggestions[selectedFood]!;
suggestions[selectedFood]!; setState(() {
context nameController.text = selectedFood;
.read<EnterFoodController>() kcalPerMassController.text =
.set(selectedFood, kcalPerMassForSelectedFood.toString()); kcalPerMassForSelectedFood.toString();
});
}), }),
TextField( TextField(
textAlign: TextAlign.end, textAlign: TextAlign.end,
decoration: const InputDecoration( decoration: const InputDecoration(
label: Align( label:
alignment: Alignment.centerRight, child: Text("Menge")), Align(alignment: Alignment.centerRight, child: Text("Menge")),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
controller: massController, controller: massController,
@ -87,8 +80,7 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
textAlign: TextAlign.end, textAlign: TextAlign.end,
decoration: const InputDecoration( decoration: const InputDecoration(
label: Align( label: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight, child: Text("kcal pro"))),
child: Text("kcal pro"))),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
controller: kcalPerMassController, controller: kcalPerMassController,
onSubmitted: (value) => onSubmitAction(), onSubmitted: (value) => onSubmitAction(),
@ -104,16 +96,14 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
), ),
), ),
); );
},
);
} }
void onSubmitAction() { void onSubmitAction() {
double massAsNumber = 0.0; int massAsNumber = 0;
double kcalPerMassAsNumber = 0.0; int kcalPerMassAsNumber = 0;
try { try {
massAsNumber = double.parse(massController.text.replaceAll(",", ".")); massAsNumber = int.parse(massController.text.replaceAll(",", "."));
} catch (e) { } catch (e) {
var snackbar = const SnackBar(content: Text("Menge muss eine Zahl sein")); var snackbar = const SnackBar(content: Text("Menge muss eine Zahl sein"));
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
@ -123,21 +113,28 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
try { try {
kcalPerMassAsNumber = kcalPerMassAsNumber =
double.parse(kcalPerMassController.text.replaceAll(",", ".")); int.parse(kcalPerMassController.text.replaceAll(",", "."));
} catch (e) { } catch (e) {
var snackbar = var snackbar =
const SnackBar(content: Text("'kcal pro 100g' muss eine Zahl sein")); const SnackBar(content: Text("'kcal pro 100g' muss eine Zahl sein"));
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).removeCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(snackbar); ScaffoldMessenger.of(context).showSnackBar(snackbar);
return; return;
} }
var entry = FoodEntry( var entry = FoodEntryState(
name: nameController.text, name: nameController.text,
mass: massAsNumber, mass: massAsNumber,
kcalPerMass: kcalPerMassAsNumber); kcalPer100: kcalPerMassAsNumber,
waitingForNetwork: false,
isSelected: false,
);
widget.onAdd(context, entry); widget.onAdd(context, entry);
context.read<EnterFoodController>().set("", ""); setState(() {
nameController.text = "";
massController.text = "";
kcalPerMassController.text = "";
});
} }
} }

View File

@ -1,102 +1,305 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_scan/food_fact_lookup.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> { class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
final FoodEntryState initialState; final GlobalEntryState initialState;
final FoodStorage storage; final FoodStorage storage;
final DateTime forDate;
FoodEntryBloc( FoodEntryBloc({required this.initialState, required this.storage})
{required this.initialState,
required this.forDate,
required this.storage})
: super(initialState) { : super(initialState) {
on<PageBeingInitialized>(handlePageBeingInitialized);
on<FoodEntryEvent>(handleFoodEntryEvent); on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodDeletionEvent>(deleteFood); on<FoodChangedEvent>(handleFoodChangedEvent);
on<PageChangedEvent>(updateEntries); on<FoodDeletionEvent>(handleDeleteFoodEvent);
on<BarcodeScanned>(handleBarcodeScannedEvent);
on<FoodEntryTapped>(handleFoodEntryTapped);
}
void handlePageBeingInitialized(
PageBeingInitialized event, Emitter<GlobalEntryState> emit) async {
var newList = await storage.getEntriesForDate(event.forDate);
state.foodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: state.foodEntries));
} }
void handleFoodEntryEvent( void handleFoodEntryEvent(
FoodEntryEvent event, Emitter<FoodEntryState> emit) async { FoodEntryEvent event, Emitter<GlobalEntryState> emit) async {
FoodEntryState newState = FoodEntryState.from(state); var entriesForDate = state.foodEntries[event.forDate];
newState.addEntry(event.entry); entriesForDate ??= [];
await storage.writeEntriesForDate(forDate, newState.foodEntries); entriesForDate.add(event.entry);
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.entry); storage.addFoodEntryToLookupDatabase(event.entry);
emit(newState); // 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});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void deleteFood(FoodDeletionEvent event, Emitter<FoodEntryState> emit) async { void handleFoodChangedEvent(
state.foodEntries.removeWhere((entry) => entry.id == event.entryID); FoodChangedEvent event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
await storage.writeEntriesForDate(forDate, state.foodEntries); var index = entriesForDate.indexWhere((entry) {
return entry.id == event.newEntry.id;
});
emit(FoodEntryState.from(state)); entriesForDate.removeAt(index);
entriesForDate.insert(index, event.newEntry);
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});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void updateEntries( void handleDeleteFoodEvent(
PageChangedEvent event, Emitter<FoodEntryState> emit) async { FoodDeletionEvent event, Emitter<GlobalEntryState> emit) async {
var entries = await storage.getEntriesForDate(event.changedToDate); var entriesForDate = state.foodEntries[event.forDate];
var newState = FoodEntryState(foodEntries: entries); if (entriesForDate == null) return;
emit(newState);
entriesForDate.removeWhere((entry) => entry.id == event.entryID);
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});
emit(GlobalEntryState(foodEntries: newFoodEntries));
}
void handleBarcodeScannedEvent(
BarcodeScanned event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var client = FoodFactLookupClient();
var scanResult = await event.scanResultFuture;
if (scanResult.type == ResultType.Cancelled) {
return;
}
if (scanResult.type == ResultType.Error) {
emit(GlobalEntryState(
foodEntries: state.foodEntries,
errorString: "Fehler beim Scannen des Barcodes"));
return;
}
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
var newEntryWaiting = FoodEntryState(
kcalPer100: 0,
name: "",
mass: 0,
waitingForNetwork: true,
isSelected: false,
);
entriesForDate.add(newEntryWaiting);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
await responseFuture.then((response) async {
var index = entriesForDate
.indexWhere((entryState) => entryState.id == newEntryWaiting.id);
// element not found (was deleted previously)
if (index == -1) {
return;
}
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(
foodEntries: newFoodEntries,
errorString: "Barcode konnte nicht gefunden werden."));
return;
}
if (response.status ==
FoodFactResponseStatus.foodFactServerNotReachable) {
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(
foodEntries: newFoodEntries,
errorString: "OpenFoodFacts-Server konnte nicht erreicht werden."));
return;
}
var newEntryFinishedWaiting = FoodEntryState(
name: response.food?.name ?? "",
mass: response.food?.mass ?? 0,
kcalPer100: response.food?.kcalPer100g ?? 0,
waitingForNetwork: false,
isSelected: false,
);
entriesForDate.removeAt(index);
entriesForDate.insert(index, newEntryFinishedWaiting);
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});
emit(GlobalEntryState(foodEntries: newFoodEntries));
});
}
void handleFoodEntryTapped(
FoodEntryTapped event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var oldStateOfTappedEntry = event.entry.isSelected;
for (var entry in entriesForDate) {
entry.isSelected = false;
}
var selectedEntry = entriesForDate.firstWhere((entry) {
return entry.id == event.entry.id;
});
selectedEntry.isSelected = !oldStateOfTappedEntry;
emit(GlobalEntryState(foodEntries: state.foodEntries));
} }
} }
class FoodEvent {} class FoodEvent {
final DateTime forDate;
FoodEvent({required this.forDate});
}
class PageBeingInitialized extends FoodEvent {
PageBeingInitialized({required super.forDate});
}
class FoodEntryEvent extends FoodEvent { class FoodEntryEvent extends FoodEvent {
final FoodEntry entry; final FoodEntryState entry;
FoodEntryEvent({required this.entry}); FoodEntryEvent({required this.entry, required super.forDate});
}
class FoodChangedEvent extends FoodEvent {
final FoodEntryState newEntry;
FoodChangedEvent({required this.newEntry, required super.forDate});
} }
class FoodDeletionEvent extends FoodEvent { class FoodDeletionEvent extends FoodEvent {
final String entryID; final String entryID;
FoodDeletionEvent({required this.entryID}); FoodDeletionEvent({required this.entryID, required super.forDate});
} }
class PageChangedEvent extends FoodEvent { class BarcodeScanned extends FoodEvent {
final DateTime changedToDate; final Future<ScanResult> scanResultFuture;
PageChangedEvent({required this.changedToDate}); BarcodeScanned({required this.scanResultFuture, required super.forDate});
}
class FoodEntryTapped extends FoodEvent {
final FoodEntryState entry;
FoodEntryTapped({required this.entry, required super.forDate});
}
/// This is the state for one date/page
class GlobalEntryState {
final Map<DateTime, List<FoodEntryState>> foodEntries;
final String? errorString;
GlobalEntryState({required this.foodEntries, this.errorString});
factory GlobalEntryState.init() {
return GlobalEntryState(foodEntries: {});
}
static from(GlobalEntryState state) {
return GlobalEntryState(foodEntries: state.foodEntries);
}
bool addEntry(FoodEntryState entry, DateTime date) {
var list = foodEntries[date];
if (list == null) {
return false;
}
list.add(entry);
return true;
}
} }
class FoodEntryState { class FoodEntryState {
final List<FoodEntry> foodEntries;
FoodEntryState({required this.foodEntries});
factory FoodEntryState.init() {
return FoodEntryState(foodEntries: []);
}
static from(FoodEntryState state) {
List<FoodEntry> newList = List.from(state.foodEntries);
return FoodEntryState(foodEntries: newList);
}
void addEntry(FoodEntry entry) {
foodEntries.add(entry);
}
}
class FoodEntry {
final String name;
final double mass;
final double kcalPerMass;
final String id; final String id;
final String name;
final int mass;
final int kcalPer100;
final bool waitingForNetwork;
bool isSelected;
FoodEntry({ factory FoodEntryState({
required name,
required mass,
required kcalPer100,
required waitingForNetwork,
required isSelected,
}) {
return FoodEntryState.withID(
id: const Uuid().v1(),
name: name,
mass: mass,
kcalPer100: kcalPer100,
waitingForNetwork: waitingForNetwork,
isSelected: isSelected,
);
}
FoodEntryState.withID({
required this.id,
required this.name, required this.name,
required this.mass, required this.mass,
required this.kcalPerMass, required this.kcalPer100,
}) : id = const Uuid().v1(); required this.waitingForNetwork,
required this.isSelected,
});
@override @override
String toString() { String toString() {
return '$id,$name,$mass,$kcalPerMass'; //we use quotation marks around the name because the name might contain
//commas and we want to store it in a csv file
return '$id,"$name",$mass,$kcalPer100';
} }
} }

View File

@ -3,33 +3,33 @@ import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/utils/row_with_spacers_widget.dart'; import 'package:calorimeter/utils/row_with_spacers_widget.dart';
class FoodEntryWidget extends StatefulWidget { class FoodEntryWidget extends StatefulWidget {
final FoodEntry entry; final FoodEntryState entry;
final Function(BuildContext context, String id) onDelete; final Function(BuildContext context, String id) onDelete;
final Function(BuildContext context, FoodEntryState entry) onChange;
final Function(BuildContext context, FoodEntryState entry) onTap;
const FoodEntryWidget( const FoodEntryWidget({
{super.key, required this.entry, required this.onDelete}); super.key,
required this.entry,
required this.onDelete,
required this.onChange,
required this.onTap,
});
@override @override
State<FoodEntryWidget> createState() => _FoodEntryWidgetState(); State<FoodEntryWidget> createState() => _FoodEntryWidgetState();
} }
class _FoodEntryWidgetState extends State<FoodEntryWidget> { class _FoodEntryWidgetState extends State<FoodEntryWidget> {
late bool showCancelAndDelete;
@override @override
void initState() { void initState() {
showCancelAndDelete = false;
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () => widget.onTap(context, widget.entry),
setState(() {
showCancelAndDelete = !showCancelAndDelete;
});
},
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
@ -38,15 +38,28 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: RowWidget( child: RowWidget(
Text(widget.entry.name), widget.entry.waitingForNetwork
Text(widget.entry.mass.ceil().toString(), ? const Center(child: CircularProgressIndicator())
textAlign: TextAlign.end), : Text(widget.entry.name),
Text(widget.entry.kcalPerMass.ceil().toString(), widget.entry.waitingForNetwork
? Container()
: Text(widget.entry.mass.ceil().toString(),
textAlign: TextAlign.end), textAlign: TextAlign.end),
Opacity( Opacity(
opacity: showCancelAndDelete ? 0.0 : 1.0, opacity: widget.entry.isSelected ? 0.0 : 1.0,
child: Text( child: widget.entry.waitingForNetwork
(widget.entry.mass * widget.entry.kcalPerMass / 100) ? 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() .ceil()
.toString(), .toString(),
textAlign: TextAlign.end), textAlign: TextAlign.end),
@ -55,26 +68,35 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
), ),
), ),
Opacity( Opacity(
opacity: showCancelAndDelete ? 0.66 : 0.0, opacity: widget.entry.isSelected ? 0.66 : 0.0,
child: Container( child: Container(
color: Theme.of(context).colorScheme.secondary)), color: Theme.of(context).colorScheme.secondary)),
]), ]),
), ),
Opacity( Opacity(
opacity: showCancelAndDelete ? 1.0 : 0.0, opacity: widget.entry.isSelected ? 1.0 : 0.0,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
SizedBox( SizedBox(
child: IconButton( child: IconButton(
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
icon: const Icon(Icons.cancel), icon: const Icon(Icons.edit),
onPressed: showCancelAndDelete onPressed: widget.entry.isSelected
? () => setState(() { ? () async {
showCancelAndDelete = false; widget.onTap(context, widget.entry);
}) await showDialog(
: null, context: context,
), builder: (dialogContext) {
return FoodEntryChangeDialog(
entry: widget.entry,
onChange: (context, entry) {
widget.onChange(context, entry);
});
},
);
}
: null),
), ),
SizedBox( SizedBox(
child: IconButton( child: IconButton(
@ -82,7 +104,7 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
iconSize: 24, iconSize: 24,
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
color: Colors.redAccent, color: Colors.redAccent,
onPressed: showCancelAndDelete onPressed: widget.entry.isSelected
? () => widget.onDelete(context, widget.entry.id) ? () => widget.onDelete(context, widget.entry.id)
: null), : null),
), ),
@ -94,3 +116,125 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
); );
} }
} }
class FoodEntryChangeDialog extends StatefulWidget {
final FoodEntryState entry;
final Function(BuildContext context, FoodEntryState entry) onChange;
const FoodEntryChangeDialog(
{required this.entry, super.key, required this.onChange});
@override
State<FoodEntryChangeDialog> createState() => _FoodEntryChangeDialogState();
}
class _FoodEntryChangeDialogState extends State<FoodEntryChangeDialog> {
late TextEditingController nameController;
late TextEditingController massController;
late TextEditingController kcalPer100Controller;
static const textFieldVerticalPadding = 16.0;
static const textFieldHorizontalPadding = 16.0;
@override
void initState() {
nameController = TextEditingController();
nameController.text = widget.entry.name;
massController = TextEditingController();
massController.text = widget.entry.mass.toString();
kcalPer100Controller = TextEditingController();
kcalPer100Controller.text = widget.entry.kcalPer100.toString();
super.initState();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: textFieldVerticalPadding,
horizontal: textFieldHorizontalPadding),
child: TextField(
onSubmitted: (val) => _onSubmitAction(),
controller: nameController,
decoration: const InputDecoration(
label: Text("Name"),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: textFieldVerticalPadding,
horizontal: textFieldHorizontalPadding),
child: TextField(
onSubmitted: (val) => _onSubmitAction(),
controller: massController,
decoration: const InputDecoration(
label: Text("Menge"),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: textFieldVerticalPadding,
horizontal: textFieldHorizontalPadding),
child: TextField(
onSubmitted: (val) => _onSubmitAction(),
controller: kcalPer100Controller,
decoration: const InputDecoration(
label: Text("kcal pro Menge"),
),
),
)
],
),
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.cancel)),
IconButton(
onPressed: () => _onSubmitAction(),
icon: const Icon(Icons.check),
)
],
);
}
void _onSubmitAction() {
int mass;
int kcalPer100;
try {
mass = int.parse(massController.text);
} catch (e) {
mass = 0;
}
try {
kcalPer100 = int.parse(kcalPer100Controller.text);
} catch (e) {
kcalPer100 = 0;
}
var newEntry = FoodEntryState.withID(
id: widget.entry.id,
name: nameController.text,
mass: mass,
kcalPer100: kcalPer100,
waitingForNetwork: widget.entry.waitingForNetwork,
isSelected: false,
);
widget.onChange(context, newEntry);
Navigator.of(context).pop();
}
}

View File

@ -45,13 +45,28 @@ class FoodFactLookupClient {
class FoodFactModel { class FoodFactModel {
final String name; final String name;
final int kcalPer100g; final int kcalPer100g;
final int mass;
FoodFactModel({required this.name, required this.kcalPer100g}); FoodFactModel({
required this.name,
required this.mass,
required this.kcalPer100g,
});
factory FoodFactModel.fromJson(Map<String, dynamic> json) { factory FoodFactModel.fromJson(Map<String, dynamic> json) {
String quantityString = json['product']['product_quantity'] ?? "0";
int quantity;
try {
quantity = int.parse(quantityString);
} catch (e) {
quantity = 0;
}
return FoodFactModel( return FoodFactModel(
name: json['product']['product_name'], name: json['product']['product_name'],
kcalPer100g: json['product']['nutriments']['energy-kcal_100g']); kcalPer100g: json['product']['nutriments']['energy-kcal_100g'],
mass: quantity);
} }
} }

View File

@ -1,15 +1,33 @@
import 'package:calorimeter/perdate/perdate_widget.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/perdate/perdate_pageview.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart';
import 'package:calorimeter/utils/theme_bloc.dart'; import 'package:calorimeter/utils/theme_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:go_router/go_router.dart';
List<FoodEntryState> entriesForToday = [];
DateTime timeNow = DateTime.now();
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
var storage = await FoodStorage.create(); var storage = await FoodStorage.create();
await storage.buildFoodLookupDatabase(); await storage.buildFoodLookupDatabase();
timeNow = DateTime.now().copyWith(
hour: 0,
isUtc: true,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0);
entriesForToday = await storage.getEntriesForDate(timeNow);
var kcalLimit = await storage.readLimit(); var kcalLimit = await storage.readLimit();
var brightness = await storage.readBrightness(); var brightness = await storage.readBrightness();
@ -38,6 +56,13 @@ class MainApp extends StatelessWidget {
return SafeArea( return SafeArea(
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider(
create: (context) => FoodEntryBloc(
storage: storage,
initialState:
GlobalEntryState(foodEntries: {timeNow: entriesForToday}),
),
),
BlocProvider( BlocProvider(
create: (context) => SettingsDataBloc( create: (context) => SettingsDataBloc(
SettingsState(kcalLimit: kcalLimit), SettingsState(kcalLimit: kcalLimit),
@ -56,8 +81,25 @@ class MainApp extends StatelessWidget {
newBrightness = Brightness.dark; newBrightness = Brightness.dark;
} }
return MaterialApp( return MaterialApp.router(
home: PerDateWidget(date: DateTime.now()), routerConfig: GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) {
return PerDatePageview(
initalDate: DateTime.now().copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
),
);
},
),
],
),
localizationsDelegates: const [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,

View File

@ -5,10 +5,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class FoodEntryList extends StatelessWidget { class FoodEntryList extends StatelessWidget {
final List<FoodEntry> entries; final List<FoodEntryState> entries;
final DateTime date;
const FoodEntryList({ const FoodEntryList({
required this.entries, required this.entries,
required this.date,
super.key, super.key,
}); });
@ -17,6 +19,7 @@ class FoodEntryList extends StatelessWidget {
return ListView.builder( return ListView.builder(
itemCount: entries.length + 1, itemCount: entries.length + 1,
itemBuilder: (BuildContext itemBuilderContext, int listIndex) { itemBuilder: (BuildContext itemBuilderContext, int listIndex) {
//last item in list is the widget to enter food
if (listIndex == entries.length) { if (listIndex == entries.length) {
return Column( return Column(
children: [ children: [
@ -24,7 +27,7 @@ class FoodEntryList extends StatelessWidget {
onAdd: (context, entry) { onAdd: (context, entry) {
context context
.read<FoodEntryBloc>() .read<FoodEntryBloc>()
.add(FoodEntryEvent(entry: entry)); .add(FoodEntryEvent(entry: entry, forDate: date));
}, },
), ),
const SizedBox(height: 75), const SizedBox(height: 75),
@ -38,10 +41,20 @@ class FoodEntryList extends StatelessWidget {
FoodEntryWidget( FoodEntryWidget(
key: ValueKey(entries[entryIndex].id), key: ValueKey(entries[entryIndex].id),
entry: entries[entryIndex], entry: entries[entryIndex],
onDelete: (callbackContext, id) { onDelete: (_, id) {
callbackContext.read<FoodEntryBloc>().add(FoodDeletionEvent( context
entryID: id, .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(), const Divider(),

View File

@ -0,0 +1,76 @@
import 'package:calorimeter/perdate/perdate_widget.dart';
import 'package:flutter/material.dart';
class PerDatePageview extends StatefulWidget {
// this is the date for which the PerDate widget will be shown on screen
// left of it will be yesterday's PerDate widget
// right of it will be tomorrow's PerDate widget
final DateTime initalDate;
const PerDatePageview({required this.initalDate, super.key});
@override
State<PerDatePageview> createState() => _PerDatePageviewState();
}
class _PerDatePageviewState extends State<PerDatePageview> {
late PageController pageController;
late DateTime displayedDate;
late List<int> visitedIndexes = [];
final int initialOffset = 36500000;
//TODO: that is just ugly
bool backButtonWasPressed = false;
@override
void initState() {
super.initState();
pageController = PageController(initialPage: initialOffset);
displayedDate = widget.initalDate;
visitedIndexes.add(initialOffset);
}
@override
Widget build(BuildContext context) {
return BackButtonListener(
onBackButtonPressed: () async {
if (visitedIndexes.length == 1) {
return false;
}
visitedIndexes.removeLast();
backButtonWasPressed = true;
pageController.jumpToPage(visitedIndexes.last);
return true;
},
child: PageView.builder(
reverse: true,
controller: pageController,
onPageChanged: (value) {
if (backButtonWasPressed) {
backButtonWasPressed = false;
return;
}
visitedIndexes.add(value);
},
itemBuilder: (context, index) {
var dateToBuildWidgetFor =
displayedDate.subtract(Duration(days: index - initialOffset));
return PerDateWidget(
key: ValueKey(dateToBuildWidgetFor.toString()),
date: dateToBuildWidgetFor.copyWith(isUtc: true),
onDateSelected: (dateSelected) {
if (dateSelected == null) return;
var diff = dateSelected.difference(dateToBuildWidgetFor);
var newIndex = index - diff.inDays;
pageController.jumpToPage(newIndex);
});
}),
);
}
}

View File

@ -1,6 +1,4 @@
import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_scan/food_fact_lookup.dart';
import 'package:calorimeter/utils/enter_food_controller.dart';
import 'package:calorimeter/utils/scan_food_floating_button.dart'; import 'package:calorimeter/utils/scan_food_floating_button.dart';
import 'package:calorimeter/utils/app_drawer.dart'; import 'package:calorimeter/utils/app_drawer.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
@ -13,84 +11,83 @@ import 'package:calorimeter/utils/theme_switcher_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class PerDateWidget extends StatefulWidget { class PerDateWidget extends StatefulWidget {
final DateTime date; final DateTime date;
const PerDateWidget({super.key, required this.date}); final Function(DateTime?) onDateSelected;
const PerDateWidget(
{super.key, required this.date, required this.onDateSelected});
@override @override
State<PerDateWidget> createState() => _PerDateWidgetState(); State<PerDateWidget> createState() => _PerDateWidgetState();
} }
class _PerDateWidgetState extends State<PerDateWidget> { class _PerDateWidgetState extends State<PerDateWidget>
with AutomaticKeepAliveClientMixin<PerDateWidget> {
late FoodStorage storage; late FoodStorage storage;
late Future<List<FoodEntry>> entriesFuture; List<FoodEntryState> entries = [];
List<FoodEntry> entries = [];
@override @override
void initState() { void initState() {
storage = FoodStorage.getInstance(); context
entriesFuture = storage.getEntriesForDate(widget.date); .read<FoodEntryBloc>()
entriesFuture.then((val) { .add(PageBeingInitialized(forDate: widget.date));
entries = val;
});
super.initState(); super.initState();
} }
@override
void dispose() {
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( super.build(context);
future: entriesFuture,
builder: (context, snapshot) { return BlocConsumer<FoodEntryBloc, GlobalEntryState>(
return snapshot.connectionState != ConnectionState.done listener: (context, pageState) {
? const Center(child: CircularProgressIndicator()) if (pageState.errorString != null) {
: MultiProvider( showNewSnackbarWith(context, pageState.errorString!);
providers: [ }
ChangeNotifierProvider( },
create: (context) => EnterFoodController()), builder: (context, globalState) {
BlocProvider(
create: (context) => FoodEntryBloc(
initialState: FoodEntryState(foodEntries: entries),
storage: storage,
forDate: widget.date,
),
)
],
child: BlocBuilder<FoodEntryBloc, FoodEntryState>(
builder: (context, state) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: title: Text(DateFormat.yMMMMd('de').format(widget.date)),
Text(DateFormat.yMMMMd('de').format(widget.date)),
actions: const [ThemeSwitcherButton()], actions: const [ThemeSwitcherButton()],
), ),
body: FoodEntryList(entries: state.foodEntries), body: FoodEntryList(
entries: globalState.foodEntries[widget.date] ?? [],
date: widget.date),
bottomNavigationBar: BottomAppBar( bottomNavigationBar: BottomAppBar(
shape: const RectangularNotchShape(), shape: const RectangularNotchShape(),
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
child: SumWidget(foodEntries: state.foodEntries)), child: SumWidget(
foodEntries: globalState.foodEntries[widget.date] ?? [])),
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: OverflowBar( floatingActionButton: OverflowBar(children: [
children: [ ScanFoodFloatingButton(
ScanFoodFloatingButton(onPressed: () { onPressed: () {
_onScanButtonPressed(context); var result = BarcodeScanner.scan();
}), context.read<FoodEntryBloc>().add(
BarcodeScanned(
scanResultFuture: result,
forDate: widget.date,
),
);
},
),
const SizedBox(width: 8), const SizedBox(width: 8),
CalendarFloatingButton( CalendarFloatingButton(
startFromDate: widget.date, startFromDate: widget.date,
onDateSelected: (dateSelected) { onDateSelected: (dateSelected) {
_onDateSelected(dateSelected); widget.onDateSelected(dateSelected);
}, },
), ),
], ]),
),
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked); FloatingActionButtonLocation.endDocked);
}, },
),
);
},
); );
} }
@ -103,61 +100,8 @@ class _PerDateWidgetState extends State<PerDateWidget> {
..showSnackBar(snackbar); ..showSnackBar(snackbar);
} }
void _onDateSelected(DateTime date) { @override
Navigator.of(context).push( bool get wantKeepAlive => true;
MaterialPageRoute(
builder: (context) {
return PerDateWidget(date: date);
},
),
).then((val) {
setState(
() {
entriesFuture = storage.getEntriesForDate(widget.date);
entriesFuture.then(
(val) {
entries = val;
},
);
},
);
});
}
void _onScanButtonPressed(BuildContext context) async {
var client = FoodFactLookupClient();
var scanResult = await BarcodeScanner.scan();
if (scanResult.type == ResultType.Cancelled) {
return;
}
if (!context.mounted) return;
if (scanResult.type == ResultType.Error) {
showNewSnackbarWith(context, "Fehler beim Scannen des Barcodes.");
}
var response = await client.retrieveFoodInfo(scanResult.rawContent);
if (!context.mounted) return;
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
showNewSnackbarWith(context, "Barcode konnte nicht gefunden werden.");
return;
}
if (response.status == FoodFactResponseStatus.foodFactServerNotReachable) {
showNewSnackbarWith(
context, "OpenFoodFacts-Server konnte nicht erreicht werden.");
return;
}
context.read<EnterFoodController>().set(
response.food!.name,
response.food!.kcalPer100g.toString(),
);
}
} }
class ErrorSnackbar extends SnackBar { class ErrorSnackbar extends SnackBar {

View File

@ -1,4 +1,3 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
@ -8,7 +7,7 @@ import 'package:universal_platform/universal_platform.dart';
class FoodStorage { class FoodStorage {
static late FoodStorage _instance; static late FoodStorage _instance;
late String path; late String path;
final Map<String, double> _foodLookupDatabase = {}; final Map<String, int> _foodLookupDatabase = {};
FoodStorage._create(); FoodStorage._create();
@ -31,8 +30,8 @@ class FoodStorage {
static FoodStorage getInstance() => _instance; static FoodStorage getInstance() => _instance;
Future<List<FoodEntry>> getEntriesForDate(DateTime date) async { Future<List<FoodEntryState>> getEntriesForDate(DateTime date) async {
List<FoodEntry> entries = []; List<FoodEntryState> entries = [];
var filePath = '$path/${date.year}/${date.month}/${date.day}'; var filePath = '$path/${date.year}/${date.month}/${date.day}';
var file = File(filePath); var file = File(filePath);
@ -43,11 +42,29 @@ class FoodStorage {
var lines = await file.readAsLines(); var lines = await file.readAsLines();
for (var line in lines) { for (var line in lines) {
var fields = line.split(','); var fields = line.splitWithIgnore(',', ignoreIn: '"');
var entry = FoodEntry( int mass = 0;
name: fields[1], int kcalPerMass = 0;
mass: double.parse(fields[2]),
kcalPerMass: double.parse(fields[3])); try {
mass = int.parse(fields[2]);
} catch (e) {
mass = double.parse(fields[2]).toInt();
}
try {
kcalPerMass = int.parse(fields[3]);
} catch (e) {
kcalPerMass = double.parse(fields[3]).toInt();
}
var entry = FoodEntryState(
name: fields[1].replaceAll('"', ""),
mass: mass,
kcalPer100: kcalPerMass,
waitingForNetwork: false,
isSelected: false,
);
entries.add(entry); entries.add(entry);
} }
@ -55,7 +72,7 @@ class FoodStorage {
} }
Future<void> writeEntriesForDate( Future<void> writeEntriesForDate(
DateTime date, List<FoodEntry> foodEntries) async { DateTime date, List<FoodEntryState> foodEntries) async {
var filePath = '$path/${date.year}/${date.month}/${date.day}'; var filePath = '$path/${date.year}/${date.month}/${date.day}';
var file = File(filePath); var file = File(filePath);
@ -139,11 +156,11 @@ class FoodStorage {
Future<void> buildFoodLookupDatabase() async { Future<void> buildFoodLookupDatabase() async {
// get a list of dates of the last 365 days // get a list of dates of the last 365 days
var dates = List<DateTime>.generate(365, (idx) { var dates = List<DateTime>.generate(365, (idx) {
var pastDay = Duration(days: idx); var durationToPast = Duration(days: idx);
return DateTime.now().subtract(pastDay); return DateTime.now().subtract(durationToPast);
}); });
for (var date in dates) { for (var date in dates.reversed) {
addFoodEntryToLookupDatabaseFor(date); addFoodEntryToLookupDatabaseFor(date);
} }
} }
@ -152,15 +169,52 @@ class FoodStorage {
var entriesForDate = await getEntriesForDate(date); var entriesForDate = await getEntriesForDate(date);
for (var entry in entriesForDate) { for (var entry in entriesForDate) {
_foodLookupDatabase[entry.name] = entry.kcalPerMass; _foodLookupDatabase[entry.name] = entry.kcalPer100;
log("Added entry: ${entry.name}/${entry.kcalPerMass}");
} }
} }
void addFoodEntryToLookupDatabase(FoodEntry entry) { void addFoodEntryToLookupDatabase(FoodEntryState entry) {
_foodLookupDatabase[entry.name] = entry.kcalPerMass; _foodLookupDatabase[entry.name] = entry.kcalPer100;
log("Added entry: ${entry.name}/${entry.kcalPerMass}");
} }
Map<String, double> get getFoodEntryLookupDatabase => _foodLookupDatabase; Map<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase;
}
extension SplitWithIgnore on String {
List<String> splitWithIgnore(String delimiter, {String? ignoreIn}) {
List<String> parts = [];
if (ignoreIn == null) {
return split(delimiter);
}
int index = -1;
int indexCharAfterDelimiter = 0;
bool inIgnore = false;
for (var rune in runes) {
var char = String.fromCharCode(rune);
index += 1;
if (char == ignoreIn) {
inIgnore = !inIgnore;
continue;
}
if (inIgnore) {
continue;
}
if (char == delimiter) {
parts.add(substring(indexCharAfterDelimiter, index));
indexCharAfterDelimiter = index + 1;
}
if (index + 1 == length) {
parts.add(substring(indexCharAfterDelimiter, length));
}
}
return parts;
}
} }

View File

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

View File

@ -1,12 +0,0 @@
import 'package:flutter/material.dart';
class EnterFoodController extends ChangeNotifier {
String name = "";
String kcalPer100g = "";
void set(String newName, String newKcal) {
name = newName;
kcalPer100g = newKcal;
notifyListeners();
}
}

View File

@ -4,7 +4,7 @@ import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart';
class SumWidget extends StatelessWidget { class SumWidget extends StatelessWidget {
final List<FoodEntry> foodEntries; final List<FoodEntryState> foodEntries;
const SumWidget({required this.foodEntries, super.key}); const SumWidget({required this.foodEntries, super.key});
@override @override
@ -13,7 +13,7 @@ class SumWidget extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
var sum = 0.0; var sum = 0.0;
for (var entry in foodEntries) { for (var entry in foodEntries) {
sum += entry.kcalPerMass / 100 * entry.mass; sum += entry.kcalPer100 / 100 * entry.mass;
} }
var diff = state.kcalLimit - sum; var diff = state.kcalLimit - sum;
var diffLimit = state.kcalLimit ~/ 4; var diffLimit = state.kcalLimit ~/ 4;

View File

@ -1,6 +1,27 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
url: "https://pub.dev"
source: hosted
version: "72.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
url: "https://pub.dev"
source: hosted
version: "6.7.0"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -89,6 +110,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
coverage:
dependency: transitive
description:
name: coverage
sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5
url: "https://pub.dev"
source: hosted
version: "1.9.2"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -113,6 +150,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -138,18 +183,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.1" version: "0.14.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "5.0.0"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -160,6 +205,51 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc"
url: "https://pub.dev"
source: hosted
version: "14.3.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -176,6 +266,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.19.0"
io:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@ -212,10 +318,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "5.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -240,6 +362,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -248,6 +378,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -268,10 +414,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.10" version: "2.2.11"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@ -328,6 +474,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
protobuf: protobuf:
dependency: transitive dependency: transitive
description: description:
@ -344,6 +498,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.2" version: "6.1.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
settings_ui: settings_ui:
dependency: "direct main" dependency: "direct main"
description: description:
@ -352,11 +514,59 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
shelf:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.dev"
source: hosted
version: "1.4.1"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
url: "https://pub.dev"
source: hosted
version: "0.10.12"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -405,6 +615,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
test:
dependency: "direct main"
description:
name: test
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
url: "https://pub.dev"
source: hosted
version: "1.25.7"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
@ -413,6 +631,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.7.2"
test_core:
dependency: transitive
description:
name: test_core
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
url: "https://pub.dev"
source: hosted
version: "0.6.4"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -433,10 +659,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.0" version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -453,14 +679,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.5" version: "14.2.5"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.1.0"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@ -478,5 +744,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.5.2 <4.0.0" dart: ">=3.5.3 <4.0.0"
flutter: ">=3.22.0" flutter: ">=3.24.0"

View File

@ -4,7 +4,7 @@ publish_to: 'none'
version: 1.1.0 version: 1.1.0
environment: environment:
sdk: ^3.5.2 sdk: ^3.5.3
dependencies: dependencies:
flutter: flutter:
@ -19,12 +19,14 @@ dependencies:
uuid: ^4.5.0 uuid: ^4.5.0
barcode_scan2: ^4.3.3 barcode_scan2: ^4.3.3
provider: ^6.1.2 provider: ^6.1.2
test: ^1.25.7
go_router: ^14.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^4.0.0 flutter_lints: ^5.0.0
flutter_launcher_icons: "^0.13.1" flutter_launcher_icons: ^0.14.1
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@ -0,0 +1,59 @@
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'));
},
);
},
);
}