Merge pull request 'Overhaul ui and remove BackButtonListener' (#9) from home-button-in-drawer into master
Reviewed-on: #9
This commit is contained in:
commit
0aca111cb5
@ -48,6 +48,10 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix '.debug'
|
||||
versionNameSuffix '-DEBUG'
|
||||
}
|
||||
release {
|
||||
signingConfig = signingConfigs.release
|
||||
}
|
||||
|
59
integration_test/app_test.dart
Normal file
59
integration_test/app_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -40,12 +40,8 @@ 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));
|
||||
}
|
||||
@ -65,12 +61,8 @@ 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));
|
||||
}
|
||||
@ -84,12 +76,8 @@ 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));
|
||||
}
|
||||
@ -109,11 +97,10 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
|
||||
return;
|
||||
}
|
||||
|
||||
var client = FoodFactLookupClient();
|
||||
var entriesForDate = state.foodEntries[event.forDate];
|
||||
if (entriesForDate == null) return;
|
||||
|
||||
var client = FoodFactLookupClient();
|
||||
|
||||
if (scanResult.type == ResultType.Cancelled) {
|
||||
return;
|
||||
}
|
||||
@ -134,9 +121,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
|
||||
);
|
||||
|
||||
entriesForDate.add(newEntryWaiting);
|
||||
var newFoodEntries = state.foodEntries;
|
||||
newFoodEntries.addAll({event.forDate: entriesForDate});
|
||||
emit(GlobalEntryState(foodEntries: newFoodEntries));
|
||||
state.foodEntries.addAll({event.forDate: entriesForDate});
|
||||
emit(GlobalEntryState(foodEntries: state.foodEntries));
|
||||
|
||||
await responseFuture.then((response) async {
|
||||
var index = entriesForDate
|
||||
@ -184,9 +170,8 @@ 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));
|
||||
});
|
||||
|
@ -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))
|
||||
])))
|
||||
]))
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,19 +77,23 @@ class FoodFactModel {
|
||||
}
|
||||
}
|
||||
|
||||
String quantityString = json['product']['product_quantity'] ?? "0";
|
||||
double quantity;
|
||||
|
||||
int quantityForModel = 0;
|
||||
try {
|
||||
quantity = double.parse(quantityString);
|
||||
String quantityString = json['product']['product_quantity'] ?? "0";
|
||||
quantityForModel = double.parse(quantityString).ceil();
|
||||
} catch (e) {
|
||||
quantity = 0;
|
||||
try {
|
||||
quantityForModel =
|
||||
(json['product']['product_quantity'] as num).toDouble().ceil();
|
||||
} catch (e) {
|
||||
quantityForModel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return FoodFactModel(
|
||||
name: json['product']['product_name'] ?? "",
|
||||
kcalPer100g: kcalPer100gForModel,
|
||||
mass: quantity.ceil(),
|
||||
mass: quantityForModel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
{
|
||||
"today": "Heute",
|
||||
"ok": "OK",
|
||||
"name": "Name",
|
||||
"amount": "Menge",
|
||||
"amountPer": "Menge in 100 g/ml",
|
||||
"kcalper": "kcal pro 100 g/ml",
|
||||
"amountPer": "Menge in g oder ml",
|
||||
"kcal": "kcal",
|
||||
"kcalper": "kcal pro 100 g oder ml",
|
||||
"kcalSum": "kcal gesamt",
|
||||
"kcalToday": "kcal heute",
|
||||
"menu": "Menü",
|
||||
"settings": "Einstellungen",
|
||||
|
@ -1,9 +1,12 @@
|
||||
{
|
||||
"today": "Today",
|
||||
"ok": "OK",
|
||||
"name": "Name",
|
||||
"amount": "Amount",
|
||||
"amountPer": "Amount in 100 g/ml",
|
||||
"kcalper": "kcal per 100 g/ml",
|
||||
"amountPer": "Amount in g or ml",
|
||||
"kcal": "kcal",
|
||||
"kcalper": "kcal per 100 g or ml",
|
||||
"kcalSum": "kcal total",
|
||||
"kcalToday": "kcal today",
|
||||
"menu": "Menu",
|
||||
"settings": "Settings",
|
||||
|
159
lib/main.dart
159
lib/main.dart
@ -1,5 +1,6 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
|
||||
|
||||
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
|
||||
import 'package:calorimeter/perdate/perdate_pageview_controller.dart';
|
||||
import 'package:calorimeter/storage/storage.dart';
|
||||
@ -8,103 +9,111 @@ import 'package:calorimeter/utils/settings_bloc.dart';
|
||||
import 'package:calorimeter/utils/theme_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
List<FoodEntryState> entriesForToday = [];
|
||||
DateTime timeNow = DateTime.now();
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
var storage = await FoodStorage.create();
|
||||
await storage.buildFoodLookupDatabase();
|
||||
|
||||
timeNow = DateTimeHelper.now();
|
||||
entriesForToday = await storage.getEntriesForDate(timeNow);
|
||||
|
||||
var kcalLimit = await storage.readLimit();
|
||||
var brightness = await storage.readBrightness();
|
||||
var foodStorage = await FoodStorage.create();
|
||||
await foodStorage.buildFoodLookupDatabase();
|
||||
|
||||
runApp(
|
||||
MainApp(
|
||||
storage: storage,
|
||||
kcalLimit: kcalLimit,
|
||||
brightness: brightness,
|
||||
),
|
||||
MainApp(storage: foodStorage),
|
||||
);
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
class MainApp extends StatefulWidget {
|
||||
final FoodStorage storage;
|
||||
final double kcalLimit;
|
||||
final String brightness;
|
||||
|
||||
const MainApp(
|
||||
{required this.storage,
|
||||
required this.kcalLimit,
|
||||
required this.brightness,
|
||||
super.key});
|
||||
const MainApp({super.key, required this.storage});
|
||||
|
||||
@override
|
||||
State<MainApp> createState() => _MainAppState();
|
||||
}
|
||||
|
||||
class _MainAppState extends State<MainApp> {
|
||||
late DateTime timeNow;
|
||||
late List<FoodEntryState> entriesForToday;
|
||||
|
||||
late double kcalLimit;
|
||||
late String brightness;
|
||||
|
||||
late Future<bool>? initFuture;
|
||||
|
||||
Future<bool> asyncInit() async {
|
||||
timeNow = DateTimeHelper.now();
|
||||
entriesForToday = await widget.storage.getEntriesForDate(timeNow);
|
||||
|
||||
kcalLimit = await widget.storage.readLimit();
|
||||
brightness = await widget.storage.readBrightness();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initFuture = asyncInit();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => FoodEntryBloc(
|
||||
storage: storage,
|
||||
initialState:
|
||||
GlobalEntryState(foodEntries: {timeNow: entriesForToday}),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SettingsDataBloc(
|
||||
SettingsState(kcalLimit: kcalLimit),
|
||||
storage: storage),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ThemeDataBloc(
|
||||
ThemeState(brightness: brightness),
|
||||
storage: storage),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ThemeDataBloc, ThemeState>(
|
||||
builder: (context, state) {
|
||||
var newBrightness = Brightness.light;
|
||||
if (state.brightness == 'dark') {
|
||||
newBrightness = Brightness.dark;
|
||||
child: FutureBuilder<Object>(
|
||||
future: initFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return MaterialApp.router(
|
||||
routerConfig: GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) {
|
||||
return PerDatePageViewController(
|
||||
initialDate: DateTimeHelper.now(),
|
||||
);
|
||||
},
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => FoodEntryBloc(
|
||||
storage: widget.storage,
|
||||
initialState: GlobalEntryState(
|
||||
foodEntries: {timeNow: entriesForToday}),
|
||||
),
|
||||
],
|
||||
),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: [
|
||||
Locale('en'),
|
||||
Locale('de'),
|
||||
],
|
||||
theme: ThemeData(
|
||||
dividerTheme: const DividerThemeData(space: 2),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.lightBlue,
|
||||
brightness: newBrightness,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SettingsDataBloc(
|
||||
SettingsState(kcalLimit: kcalLimit),
|
||||
storage: widget.storage),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ThemeDataBloc(
|
||||
ThemeState(brightness: brightness),
|
||||
storage: widget.storage),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ThemeDataBloc, ThemeState>(
|
||||
builder: (context, state) {
|
||||
var newBrightness = Brightness.light;
|
||||
if (state.brightness == 'dark') {
|
||||
newBrightness = Brightness.dark;
|
||||
}
|
||||
|
||||
return MaterialApp(
|
||||
home: PerDatePageViewController(
|
||||
initialDate: DateTimeHelper.now()),
|
||||
localizationsDelegates:
|
||||
AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: [
|
||||
Locale('en'),
|
||||
...AppLocalizations.supportedLocales,
|
||||
],
|
||||
theme: ThemeData(
|
||||
dividerTheme: const DividerThemeData(space: 2),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.lightBlue,
|
||||
brightness: newBrightness,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -39,22 +39,9 @@ class PerDatePageViewController extends StatelessWidget {
|
||||
initialDate: initialDate,
|
||||
initialOffset: initialOffset,
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
return BackButtonListener(
|
||||
onBackButtonPressed: () async {
|
||||
context.read<PageViewStateProvider>().backButtonWasPressed = true;
|
||||
var visitedIndexes =
|
||||
context.read<PageViewStateProvider>().visitedIndexes;
|
||||
if (visitedIndexes.length == 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
visitedIndexes.removeLast();
|
||||
pageController.jumpToPage(visitedIndexes.last);
|
||||
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Builder(builder: (context) {
|
||||
return Text(DateFormat.yMMMMd(
|
||||
@ -73,40 +60,45 @@ class PerDatePageViewController extends StatelessWidget {
|
||||
}),
|
||||
),
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: OverflowBar(children: [
|
||||
ScanFoodFAB(
|
||||
onPressed: () {
|
||||
context.read<FoodEntryBloc>().add(
|
||||
BarcodeAboutToBeScanned(
|
||||
forDate: context
|
||||
.read<PageViewStateProvider>()
|
||||
.displayedDate,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CalendarFAB(
|
||||
startFromDate: DateTimeHelper.now(),
|
||||
onDateSelected: (dateSelected) {
|
||||
if (dateSelected == null) return;
|
||||
|
||||
var dateDiff = dateSelected.difference(initialDate).inDays;
|
||||
|
||||
log("dateDiff = $dateDiff");
|
||||
pageController.jumpToPage(initialOffset - dateDiff);
|
||||
},
|
||||
),
|
||||
]),
|
||||
floatingActionButton: _getFABs(context),
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.endDocked,
|
||||
body: PerDatePageView(
|
||||
pageController: pageController,
|
||||
initialDate: initialDate,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OverflowBar _getFABs(BuildContext context) {
|
||||
return OverflowBar(
|
||||
children: [
|
||||
ScanFoodFAB(
|
||||
onPressed: () {
|
||||
context.read<FoodEntryBloc>().add(
|
||||
BarcodeAboutToBeScanned(
|
||||
forDate:
|
||||
context.read<PageViewStateProvider>().displayedDate,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CalendarFAB(
|
||||
startFromDate: DateTimeHelper.now(),
|
||||
onDateSelected: (dateSelected) {
|
||||
if (dateSelected == null) return;
|
||||
|
||||
var dateDiff = dateSelected.difference(initialDate).inDays;
|
||||
|
||||
log("dateDiff = $dateDiff");
|
||||
pageController.jumpToPage(initialOffset - dateDiff);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -115,6 +107,7 @@ class PageViewStateProvider with ChangeNotifier {
|
||||
DateTime _displayedDate;
|
||||
final List<int> _visitedIndexes;
|
||||
bool _backButtonWasPressed = false;
|
||||
bool _isVisible = false;
|
||||
|
||||
PageViewStateProvider({required DateTime initialDate, int initialOffset = 0})
|
||||
: _displayedDate = initialDate,
|
||||
@ -131,6 +124,9 @@ class PageViewStateProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisible(vis) => _isVisible = true;
|
||||
get isVisible => _isVisible;
|
||||
|
||||
get visitedIndexes => _visitedIndexes;
|
||||
|
||||
void addVisitedIndex(int index) {
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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 {}
|
||||
|
191
pubspec.lock
191
pubspec.lock
@ -70,6 +70,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.14"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.3"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -102,6 +166,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -134,6 +206,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -154,10 +234,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "7.0.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -179,6 +259,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.6"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -218,6 +303,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -234,6 +324,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.6.2"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -246,10 +352,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -258,6 +364,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -370,6 +481,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
mockito:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mockito
|
||||
sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.5"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -435,7 +554,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
@ -462,12 +581,12 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
@ -490,6 +609,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -514,6 +641,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
settings_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -559,6 +694,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -607,6 +750,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -615,6 +766,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -647,6 +806,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.5"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -719,6 +886,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -19,13 +19,20 @@ dependencies:
|
||||
barcode_scan2: ^4.3.3
|
||||
provider: ^6.1.2
|
||||
test: ^1.25.7
|
||||
go_router: ^14.3.0
|
||||
go_router: ^14.6.2
|
||||
path_provider_platform_interface: ^2.1.2
|
||||
plugin_platform_interface: ^2.1.8
|
||||
http: ^1.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_launcher_icons: ^0.14.1
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
mockito: ^5.4.5
|
||||
build_runner: ^2.4.14
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
@ -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'));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user