Overhaul ui and remove BackButtonListener #9

Merged
marco merged 1 commits from home-button-in-drawer into master 2025-01-05 16:27:58 +00:00
20 changed files with 744 additions and 425 deletions
Showing only changes of commit 2509c1721c - Show all commits

View File

@ -48,6 +48,10 @@ android {
} }
buildTypes { buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
release { release {
signingConfig = signingConfigs.release signingConfig = signingConfigs.release
} }

View File

@ -0,0 +1,59 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:calorimeter/main.dart';
import 'package:calorimeter/storage/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUp(() {});
group('end-to-end test', () {
testWidgets('add food manually', (tester) async {
var foodStorage = await FoodStorage.create();
await tester.pumpWidget(MainApp(storage: foodStorage));
await tester.pumpAndSettle();
final addButtonFinder = find.byIcon(Icons.add);
expect(addButtonFinder, findsOneWidget);
await tester.tap(addButtonFinder);
await tester.pumpAndSettle();
final nameAutocompleteFinder =
find.widgetWithText(Autocomplete<String>, "Name");
final amountFinder = find.widgetWithText(TextField, "Amount");
final kcalFinder = find.widgetWithText(TextField, "kcal");
final addButton = find.widgetWithIcon(ElevatedButton, Icons.check);
expect(nameAutocompleteFinder, findsOneWidget);
expect(amountFinder, findsOneWidget);
expect(kcalFinder, findsOneWidget);
expect(addButton, findsOneWidget);
await tester.enterText(nameAutocompleteFinder, "Bread");
await tester.enterText(amountFinder, "150");
await tester.enterText(kcalFinder, "250");
await tester.tap(addButton);
await tester.pumpAndSettle();
// EnterFoodWidget collapses
expect(nameAutocompleteFinder, findsNothing);
var enteredFood = find.text("Bread");
var enteredAmount = find.text("150");
var enteredKcal = find.text("250");
await tester.pumpAndSettle();
expect(enteredFood, findsOneWidget);
expect(enteredAmount, findsOneWidget);
expect(enteredKcal, findsOneWidget);
});
});
}

View File

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */ /* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
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';
@ -8,8 +8,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class EnterFoodWidget extends StatefulWidget { class EnterFoodWidget extends StatefulWidget {
final Function(BuildContext context, FoodEntryState entry) onAdd; 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 @override
State<EnterFoodWidget> createState() => _EnterFoodWidgetState(); State<EnterFoodWidget> createState() => _EnterFoodWidgetState();
@ -19,89 +21,123 @@ 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, int> suggestions; late bool open;
@override @override
void initState() { void initState() {
nameController = TextEditingController(); nameController = TextEditingController();
massController = TextEditingController(); massController = TextEditingController();
kcalPerMassController = TextEditingController(); kcalPerMassController = TextEditingController();
suggestions = FoodStorage.getInstance().getFoodEntryLookupDatabase;
open = false;
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Column(
padding: const EdgeInsets.symmetric(horizontal: 8.0), children: [
child: RowWidget( Stack(
Autocomplete<String>( children: [
optionsViewOpenDirection: OptionsViewOpenDirection.down, if (!open)
fieldViewBuilder: (context, controller, focusNode, onSubmitted) { RowWidget(
nameController = controller; showDividers: false,
return TextFormField( null,
controller: controller, null,
focusNode: focusNode, null,
decoration: InputDecoration( ElevatedButton(
label: Text(AppLocalizations.of(context)!.name), style: ElevatedButton.styleFrom(
), padding: EdgeInsets.zero,
); ),
}, onPressed: () => setState(() => open = true),
optionsBuilder: (TextEditingValue textEditingValue) { child: Icon(Icons.add)),
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,
), ),
onPressed: () => onSubmitAction(), Offstage(
child: const Icon(Icons.add)), 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 = ""; nameController.text = "";
massController.text = ""; massController.text = "";
kcalPerMassController.text = ""; kcalPerMassController.text = "";
open = false;
}); });
} }
} }

View File

@ -40,12 +40,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate); await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.entry); 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; var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList}); newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries)); emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
@ -65,12 +61,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate); await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.newEntry); 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; var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList}); newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries)); emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
@ -84,12 +76,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate); 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; var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList}); newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries)); emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
@ -109,11 +97,10 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
return; return;
} }
var client = FoodFactLookupClient();
var entriesForDate = state.foodEntries[event.forDate]; var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return; if (entriesForDate == null) return;
var client = FoodFactLookupClient();
if (scanResult.type == ResultType.Cancelled) { if (scanResult.type == ResultType.Cancelled) {
return; return;
} }
@ -134,9 +121,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
); );
entriesForDate.add(newEntryWaiting); entriesForDate.add(newEntryWaiting);
var newFoodEntries = state.foodEntries; state.foodEntries.addAll({event.forDate: entriesForDate});
newFoodEntries.addAll({event.forDate: entriesForDate}); emit(GlobalEntryState(foodEntries: state.foodEntries));
emit(GlobalEntryState(foodEntries: newFoodEntries));
await responseFuture.then((response) async { await responseFuture.then((response) async {
var index = entriesForDate var index = entriesForDate
@ -184,9 +170,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate); await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting); storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting);
var entriesFromStorage = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries; var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesFromStorage}); newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries)); emit(GlobalEntryState(foodEntries: newFoodEntries));
}); });

View File

@ -1,8 +1,8 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */ /* 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/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:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class FoodEntryWidget extends StatefulWidget { class FoodEntryWidget extends StatefulWidget {
@ -24,6 +24,8 @@ class FoodEntryWidget extends StatefulWidget {
} }
class _FoodEntryWidgetState extends State<FoodEntryWidget> { class _FoodEntryWidgetState extends State<FoodEntryWidget> {
final animationDuration = const Duration(milliseconds: 150);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -32,91 +34,94 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () => widget.onTap(context, widget.entry), onTap: () {
child: Stack( widget.onTap(context, widget.entry);
children: [ },
Positioned.fill( child: Stack(children: [
child: Stack(children: [ RowWidget(
Positioned.fill( showDividers: !widget.entry.isSelected,
child: Padding( widget.entry.waitingForNetwork
padding: const EdgeInsets.symmetric(horizontal: 8.0), ? const Center(child: CircularProgressIndicator())
child: RowWidget( : Text(widget.entry.name),
widget.entry.waitingForNetwork AnimatedOpacity(
? const Center(child: CircularProgressIndicator()) duration: animationDuration,
: Text(widget.entry.name), opacity: widget.entry.isSelected ? 0.0 : 1.0,
widget.entry.waitingForNetwork child: widget.entry.waitingForNetwork
? Container() ? Container()
: Text(widget.entry.mass.ceil().toString(), : Text(widget.entry.mass.ceil().toString(),
textAlign: TextAlign.end), textAlign: TextAlign.end),
Opacity( ),
opacity: widget.entry.isSelected ? 0.0 : 1.0, AnimatedOpacity(
child: widget.entry.waitingForNetwork duration: animationDuration,
? Container() opacity: widget.entry.isSelected ? 0.0 : 1.0,
: Text(widget.entry.kcalPer100.ceil().toString(), child: widget.entry.waitingForNetwork
textAlign: TextAlign.end), ? Container()
), : Text(widget.entry.kcalPer100.ceil().toString(),
Opacity( textAlign: TextAlign.end),
opacity: widget.entry.isSelected ? 0.0 : 1.0, ),
child: widget.entry.waitingForNetwork AnimatedOpacity(
? Container() duration: animationDuration,
: Text( opacity: widget.entry.isSelected ? 0.0 : 1.0,
(widget.entry.mass * child: widget.entry.waitingForNetwork
widget.entry.kcalPer100 / ? Container()
100) : Text(
.ceil() (widget.entry.mass * widget.entry.kcalPer100 / 100)
.toString(), .ceil()
textAlign: TextAlign.end), .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),
),
],
), ),
), ),
], Positioned.fill(
), child: Stack(children: [
); AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.66 : 0.0,
child: Container(
color: Theme.of(context).colorScheme.secondary,
),
),
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 1.0 : 0.0,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
width: 64,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(Icons.edit),
onPressed: widget.entry.isSelected
? () async {
widget.onTap(context, widget.entry);
await showDialog(
context: context,
builder: (dialogContext) {
return FoodEntryChangeDialog(
entry: widget.entry,
onChange: (context, entry) {
widget.onChange(
context, entry);
});
});
}
: null)),
SizedBox(
width: 64,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(
Icons.delete,
color: Colors.redAccent,
),
onPressed: widget.entry.isSelected
? () =>
widget.onDelete(context, widget.entry.id)
: null))
])))
]))
]));
} }
} }

View File

@ -77,19 +77,23 @@ class FoodFactModel {
} }
} }
String quantityString = json['product']['product_quantity'] ?? "0"; int quantityForModel = 0;
double quantity;
try { try {
quantity = double.parse(quantityString); String quantityString = json['product']['product_quantity'] ?? "0";
quantityForModel = double.parse(quantityString).ceil();
} catch (e) { } catch (e) {
quantity = 0; try {
quantityForModel =
(json['product']['product_quantity'] as num).toDouble().ceil();
} catch (e) {
quantityForModel = 0;
}
} }
return FoodFactModel( return FoodFactModel(
name: json['product']['product_name'] ?? "", name: json['product']['product_name'] ?? "",
kcalPer100g: kcalPer100gForModel, kcalPer100g: kcalPer100gForModel,
mass: quantity.ceil(), mass: quantityForModel,
); );
} }
} }

View File

@ -1,9 +1,12 @@
{ {
"today": "Heute",
"ok": "OK", "ok": "OK",
"name": "Name", "name": "Name",
"amount": "Menge", "amount": "Menge",
"amountPer": "Menge in 100 g/ml", "amountPer": "Menge in g oder ml",
"kcalper": "kcal pro 100 g/ml", "kcal": "kcal",
"kcalper": "kcal pro 100 g oder ml",
"kcalSum": "kcal gesamt",
"kcalToday": "kcal heute", "kcalToday": "kcal heute",
"menu": "Menü", "menu": "Menü",
"settings": "Einstellungen", "settings": "Einstellungen",

View File

@ -1,9 +1,12 @@
{ {
"today": "Today",
"ok": "OK", "ok": "OK",
"name": "Name", "name": "Name",
"amount": "Amount", "amount": "Amount",
"amountPer": "Amount in 100 g/ml", "amountPer": "Amount in g or ml",
"kcalper": "kcal per 100 g/ml", "kcal": "kcal",
"kcalper": "kcal per 100 g or ml",
"kcalSum": "kcal total",
"kcalToday": "kcal today", "kcalToday": "kcal today",
"menu": "Menu", "menu": "Menu",
"settings": "Settings", "settings": "Settings",

View File

@ -1,5 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */ /* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/perdate/perdate_pageview_controller.dart'; import 'package:calorimeter/perdate/perdate_pageview_controller.dart';
import 'package:calorimeter/storage/storage.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: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:go_router/go_router.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
List<FoodEntryState> entriesForToday = [];
DateTime timeNow = DateTime.now();
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
var storage = await FoodStorage.create(); var foodStorage = await FoodStorage.create();
await storage.buildFoodLookupDatabase(); await foodStorage.buildFoodLookupDatabase();
timeNow = DateTimeHelper.now();
entriesForToday = await storage.getEntriesForDate(timeNow);
var kcalLimit = await storage.readLimit();
var brightness = await storage.readBrightness();
runApp( runApp(
MainApp( MainApp(storage: foodStorage),
storage: storage,
kcalLimit: kcalLimit,
brightness: brightness,
),
); );
} }
class MainApp extends StatelessWidget { class MainApp extends StatefulWidget {
final FoodStorage storage; final FoodStorage storage;
final double kcalLimit;
final String brightness;
const MainApp( const MainApp({super.key, required this.storage});
{required this.storage,
required this.kcalLimit, @override
required this.brightness, State<MainApp> createState() => _MainAppState();
super.key}); }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
child: MultiBlocProvider( child: FutureBuilder<Object>(
providers: [ future: initFuture,
BlocProvider( builder: (context, snapshot) {
create: (context) => FoodEntryBloc( if (snapshot.connectionState != ConnectionState.done) {
storage: storage, return Center(child: CircularProgressIndicator());
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;
} }
return MaterialApp.router( return MultiBlocProvider(
routerConfig: GoRouter( providers: [
routes: [ BlocProvider(
GoRoute( create: (context) => FoodEntryBloc(
path: '/', storage: widget.storage,
builder: (context, state) { initialState: GlobalEntryState(
return PerDatePageViewController( foodEntries: {timeNow: entriesForToday}),
initialDate: DateTimeHelper.now(),
);
},
), ),
],
),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: [
Locale('en'),
Locale('de'),
],
theme: ThemeData(
dividerTheme: const DividerThemeData(space: 2),
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.lightBlue,
brightness: newBrightness,
), ),
BlocProvider(
create: (context) => SettingsDataBloc(
SettingsState(kcalLimit: kcalLimit),
storage: widget.storage),
),
BlocProvider(
create: (context) => ThemeDataBloc(
ThemeState(brightness: brightness),
storage: widget.storage),
),
],
child: BlocBuilder<ThemeDataBloc, ThemeState>(
builder: (context, state) {
var newBrightness = Brightness.light;
if (state.brightness == 'dark') {
newBrightness = Brightness.dark;
}
return MaterialApp(
home: PerDatePageViewController(
initialDate: DateTimeHelper.now()),
localizationsDelegates:
AppLocalizations.localizationsDelegates,
supportedLocales: [
Locale('en'),
...AppLocalizations.supportedLocales,
],
theme: ThemeData(
dividerTheme: const DividerThemeData(space: 2),
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.lightBlue,
brightness: newBrightness,
),
),
);
},
), ),
); );
}, }),
),
),
); );
} }
} }

View File

@ -1,10 +1,14 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */ /* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:calorimeter/food_entry/enter_food_widget.dart'; 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_bloc.dart';
import 'package:calorimeter/food_entry/food_entry_widget.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class FoodEntryList extends StatelessWidget { class FoodEntryList extends StatelessWidget {
final List<FoodEntryState> entries; final List<FoodEntryState> entries;
@ -18,51 +22,77 @@ class FoodEntryList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.builder( var headerStyle = TextStyle(
itemCount: entries.length + 1, fontWeight: FontWeight.bold,
itemBuilder: (BuildContext itemBuilderContext, int listIndex) { color: Theme.of(context).colorScheme.onSurface);
//last item in list is the widget to enter food
if (listIndex == entries.length) { return Column(
return Column( children: [
children: [ if (entries.isNotEmpty)
EnterFoodWidget( RowWidget(
onAdd: (context, entry) { 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 context
.read<FoodEntryBloc>() .read<FoodEntryBloc>()
.add(FoodEntryEvent(entry: entry, forDate: date)); .add(FoodDeletionEvent(entryID: id, forDate: date));
}, },
), onChange: (_, changedEntry) {
const SizedBox(height: 75), context.read<FoodEntryBloc>().add(
], FoodChangedEvent(newEntry: changedEntry, forDate: date),
); );
} },
onTap: (_, tappedEntry) {
var entryIndex = listIndex; context.read<FoodEntryBloc>().add(
return Column( FoodEntryTapped(entry: tappedEntry, forDate: date),
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(),
],
);
},
); );
} }
} }

View File

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

View File

@ -39,22 +39,9 @@ class PerDatePageViewController extends StatelessWidget {
initialDate: initialDate, initialDate: initialDate,
initialOffset: initialOffset, initialOffset: initialOffset,
), ),
child: Builder(builder: (context) { child: Builder(
return BackButtonListener( builder: (context) {
onBackButtonPressed: () async { return Scaffold(
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(
appBar: AppBar( appBar: AppBar(
title: Builder(builder: (context) { title: Builder(builder: (context) {
return Text(DateFormat.yMMMMd( return Text(DateFormat.yMMMMd(
@ -73,40 +60,45 @@ class PerDatePageViewController extends StatelessWidget {
}), }),
), ),
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [ floatingActionButton: _getFABs(context),
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);
},
),
]),
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked, FloatingActionButtonLocation.endDocked,
body: PerDatePageView( body: PerDatePageView(
pageController: pageController, pageController: pageController,
initialDate: initialDate, 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; DateTime _displayedDate;
final List<int> _visitedIndexes; final List<int> _visitedIndexes;
bool _backButtonWasPressed = false; bool _backButtonWasPressed = false;
bool _isVisible = false;
PageViewStateProvider({required DateTime initialDate, int initialOffset = 0}) PageViewStateProvider({required DateTime initialDate, int initialOffset = 0})
: _displayedDate = initialDate, : _displayedDate = initialDate,
@ -131,6 +124,9 @@ class PageViewStateProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setVisible(vis) => _isVisible = true;
get isVisible => _isVisible;
get visitedIndexes => _visitedIndexes; get visitedIndexes => _visitedIndexes;
void addVisitedIndex(int index) { void addVisitedIndex(int index) {

View File

@ -1,5 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */ /* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'dart:io'; import 'dart:io';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
@ -87,6 +88,7 @@ class FoodStorage {
String fullString = ''; String fullString = '';
for (var entry in foodEntries) { for (var entry in foodEntries) {
if (entry.waitingForNetwork) continue;
fullString += '${entry.toString()}\n'; fullString += '${entry.toString()}\n';
} }

View File

@ -1,5 +1,7 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */ /* 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:calorimeter/utils/settings.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -29,6 +31,16 @@ class AppDrawer extends StatelessWidget {
title: Text(AppLocalizations.of(context)!.menu), 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( ListTile(
title: Text(AppLocalizations.of(context)!.settings), title: Text(AppLocalizations.of(context)!.settings),
trailing: const Icon(Icons.settings), trailing: const Icon(Icons.settings),

View File

@ -7,19 +7,61 @@ class RowWidget extends StatelessWidget {
final Widget? widget2; final Widget? widget2;
final Widget? widget3; final Widget? widget3;
final Widget? widget4; final Widget? widget4;
final bool showDividers;
const RowWidget(this.widget1, this.widget2, this.widget3, this.widget4, const RowWidget(this.widget1, this.widget2, this.widget3, this.widget4,
{super.key}); {super.key, required this.showDividers});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return IntrinsicHeight(
children: [ child: ConstrainedBox(
Expanded(flex: 10, child: widget1 ?? Container()), constraints: BoxConstraints(minHeight: 48),
Expanded(flex: 6, child: widget2 ?? Container()), child: Row(
Expanded(flex: 6, child: widget3 ?? Container()), children: [
Expanded(flex: 6, child: widget4 ?? Container()), Expanded(
], flex: 10,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: widget1 ?? Container(),
),
),
Opacity(
opacity: showDividers ? 1.0 : 0.0,
child: VerticalDivider(),
),
Expanded(
flex: 6,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: widget2 ?? Container(),
),
),
Opacity(
opacity: showDividers ? 1.0 : 0.0,
child: VerticalDivider(),
),
Expanded(
flex: 6,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: widget3 ?? Container(),
),
),
Opacity(
opacity: showDividers ? 1.0 : 0.0,
child: VerticalDivider(),
),
Expanded(
flex: 6,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: widget4 ?? Container(),
),
),
],
),
),
); );
} }
} }

View File

@ -36,6 +36,8 @@ class _SettingsWidgetState extends State<SettingsWidget> {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context)!.dayLimit), title: Text(AppLocalizations.of(context)!.dayLimit),
content: TextField( content: TextField(
decoration: InputDecoration(
hintText: state.kcalLimit.toString()),
controller: kcalPerDayCtrl, controller: kcalPerDayCtrl,
onSubmitted: (val) => submitDailyKcal()), onSubmitted: (val) => submitDailyKcal()),
actions: [ actions: [
@ -61,7 +63,7 @@ class _SettingsWidgetState extends State<SettingsWidget> {
try { try {
setting = double.parse(kcalPerDayCtrl.text); setting = double.parse(kcalPerDayCtrl.text);
} catch (e) { } catch (e) {
setting = 2000.0; setting = context.read<SettingsDataBloc>().dailyKcal;
} }
context.read<SettingsDataBloc>().add(DailyKcalLimitUpdated(kcal: setting)); context.read<SettingsDataBloc>().add(DailyKcalLimitUpdated(kcal: setting));
Navigator.of(context).pop(); Navigator.of(context).pop();

View File

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

View File

@ -70,6 +70,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" 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: characters:
dependency: transitive dependency: transitive
description: description:
@ -102,6 +166,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" 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: collection:
dependency: transitive dependency: transitive
description: description:
@ -134,6 +206,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -154,10 +234,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -179,6 +259,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.6" version: "8.1.6"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -218,6 +303,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -234,6 +324,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.6.2" 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: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -246,10 +352,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "4.1.2"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -258,6 +364,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.2" version: "4.5.2"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -370,6 +481,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
mockito:
dependency: "direct dev"
description:
name: mockito
sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6
url: "https://pub.dev"
source: hosted
version: "5.4.5"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -435,7 +554,7 @@ packages:
source: hosted source: hosted
version: "2.2.1" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
@ -462,12 +581,12 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.6" version: "3.1.5"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: "direct main"
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
@ -490,6 +609,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.1" version: "6.0.1"
process:
dependency: transitive
description:
name: process
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
protobuf: protobuf:
dependency: transitive dependency: transitive
description: description:
@ -514,6 +641,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" 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: settings_ui:
dependency: "direct main" dependency: "direct main"
description: description:
@ -559,6 +694,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -607,6 +750,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" 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: string_scanner:
dependency: transitive dependency: transitive
description: description:
@ -615,6 +766,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -647,6 +806,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.5" version: "0.6.5"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -719,6 +886,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" 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: webkit_inspection_protocol:
dependency: transitive dependency: transitive
description: description:

View File

@ -19,13 +19,20 @@ dependencies:
barcode_scan2: ^4.3.3 barcode_scan2: ^4.3.3
provider: ^6.1.2 provider: ^6.1.2
test: ^1.25.7 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
integration_test:
sdk: flutter
mockito: ^5.4.5
build_runner: ^2.4.14
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@ -1,59 +0,0 @@
import 'package:calorimeter/storage/storage.dart';
import 'package:test/test.dart';
void main() {
group(
'Test custom split with ignore',
() {
test('string without ignoring', () {
var testString = 'This is a test string';
var resultingList = testString.splitWithIgnore(' ');
expect(resultingList[0], equals('This'));
expect(resultingList[1], equals('is'));
expect(resultingList[2], equals('a'));
expect(resultingList[3], equals('test'));
expect(resultingList[4], equals('string'));
});
test('string that does not contain the ignored character', () {
var testString = 'This is a test string';
var resultingList = testString.splitWithIgnore(' ', ignoreIn: '"');
expect(resultingList[0], equals('This'));
expect(resultingList[1], equals('is'));
expect(resultingList[2], equals('a'));
expect(resultingList[3], equals('test'));
expect(resultingList[4], equals('string'));
});
test(
'string that contains ignored character',
() {
var testString = 'This is "a test" string';
var resultingList = testString.splitWithIgnore(' ', ignoreIn: '"');
expect(resultingList[0], equals('This'));
expect(resultingList[1], equals('is'));
expect(resultingList[2], equals('"a test"'));
expect(resultingList[3], equals('string'));
},
);
test(
'string that contains commas that should be ignored',
() {
var testString =
'f9a96b80-71f9-11ef-8df4-f3628a737a16,"Erdnüsse, geröstet",120.0,100.0';
var resultingList = testString.splitWithIgnore(',', ignoreIn: '"');
expect(
resultingList[0], equals('f9a96b80-71f9-11ef-8df4-f3628a737a16'));
expect(resultingList[1], equals('"Erdnüsse, geröstet"'));
expect(resultingList[2], equals('120.0'));
expect(resultingList[3], equals('100.0'));
},
);
},
);
}