Compare commits

..

13 Commits

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

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

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

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

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

2
.gitignore vendored
View File

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

View File

@ -15,6 +15,7 @@ android {
namespace = "de.swgross.calorimeter" namespace = "de.swgross.calorimeter"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
dependenciesInfo { dependenciesInfo {
includeInApk = false includeInApk = false
includeInBundle = false includeInBundle = false
@ -33,8 +34,8 @@ android {
applicationId = "de.swgross.calorimeter" applicationId = "de.swgross.calorimeter"
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = 2 versionCode = 5
versionName = "1.0.2" versionName = "1.0.5"
} }
signingConfigs { signingConfigs {
@ -47,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,28 +21,52 @@ 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: [
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)),
),
Offstage(
offstage: !open,
child: AnimatedOpacity(
duration: Duration(milliseconds: 250),
opacity: open ? 1.0 : 0.0,
child: RowWidget( child: RowWidget(
showDividers: true,
Autocomplete<String>( Autocomplete<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.down, optionsViewOpenDirection: OptionsViewOpenDirection.down,
fieldViewBuilder: (context, controller, focusNode, onSubmitted) { fieldViewBuilder:
(context, controller, focusNode, onSubmitted) {
nameController = controller; nameController = controller;
return TextFormField( return TextFormField(
scrollPadding: EdgeInsets.only(bottom: 100),
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
decoration: InputDecoration( decoration: InputDecoration(
@ -53,7 +79,7 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
return const Iterable<String>.empty(); return const Iterable<String>.empty();
} }
return suggestions.keys.where( return widget.foodEntryLookupDatabase.keys.where(
(name) { (name) {
return name return name
.toLowerCase() .toLowerCase()
@ -62,7 +88,8 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
); );
}, },
onSelected: (selectedFood) { onSelected: (selectedFood) {
int kcalPerMassForSelectedFood = suggestions[selectedFood]!; int kcalPerMassForSelectedFood =
widget.foodEntryLookupDatabase[selectedFood]!;
setState(() { setState(() {
nameController.text = selectedFood; nameController.text = selectedFood;
kcalPerMassController.text = kcalPerMassController.text =
@ -70,11 +97,12 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
}); });
}), }),
TextField( TextField(
scrollPadding: EdgeInsets.only(bottom: 100),
textAlign: TextAlign.end, textAlign: TextAlign.end,
decoration: InputDecoration( decoration: InputDecoration(
label: Align( label: Directionality(
alignment: Alignment.centerRight, textDirection: TextDirection.rtl,
child: Text(AppLocalizations.of(context)!.amountPer), child: Text(AppLocalizations.of(context)!.amount),
), ),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@ -82,26 +110,34 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
onSubmitted: (value) => onSubmitAction(), onSubmitted: (value) => onSubmitAction(),
), ),
TextField( TextField(
scrollPadding: EdgeInsets.only(bottom: 100),
textAlign: TextAlign.end, textAlign: TextAlign.end,
decoration: InputDecoration( decoration: InputDecoration(
label: Align( label: Directionality(
alignment: Alignment.centerRight, textDirection: TextDirection.rtl,
child: Text(AppLocalizations.of(context)!.kcalper), child: Text(AppLocalizations.of(context)!.kcal))),
)),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
controller: kcalPerMassController, controller: kcalPerMassController,
onSubmitted: (value) => onSubmitAction(), onSubmitted: (value) => onSubmitAction(),
), ),
Padding( ElevatedButton(
padding: const EdgeInsets.only(left: 16.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
onPressed: () => onSubmitAction(), onPressed: () => onSubmitAction(),
child: const Icon(Icons.add)), 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

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

View File

@ -1,8 +1,8 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* 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,56 +34,61 @@ 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: [
Positioned.fill( RowWidget(
child: Padding( showDividers: !widget.entry.isSelected,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: RowWidget(
widget.entry.waitingForNetwork widget.entry.waitingForNetwork
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Text(widget.entry.name), : Text(widget.entry.name),
widget.entry.waitingForNetwork AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.0 : 1.0,
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( ),
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.0 : 1.0, opacity: widget.entry.isSelected ? 0.0 : 1.0,
child: widget.entry.waitingForNetwork child: widget.entry.waitingForNetwork
? Container() ? Container()
: Text(widget.entry.kcalPer100.ceil().toString(), : Text(widget.entry.kcalPer100.ceil().toString(),
textAlign: TextAlign.end), textAlign: TextAlign.end),
), ),
Opacity( AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.0 : 1.0, opacity: widget.entry.isSelected ? 0.0 : 1.0,
child: widget.entry.waitingForNetwork child: widget.entry.waitingForNetwork
? Container() ? Container()
: Text( : Text(
(widget.entry.mass * (widget.entry.mass * widget.entry.kcalPer100 / 100)
widget.entry.kcalPer100 /
100)
.ceil() .ceil()
.toString(), .toString(),
textAlign: TextAlign.end), textAlign: TextAlign.end),
), ),
), ),
), Positioned.fill(
), child: Stack(children: [
Opacity( AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.66 : 0.0, opacity: widget.entry.isSelected ? 0.66 : 0.0,
child: Container( child: Container(
color: Theme.of(context).colorScheme.secondary)), color: Theme.of(context).colorScheme.secondary,
]),
), ),
Opacity( ),
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 1.0 : 0.0, opacity: widget.entry.isSelected ? 1.0 : 0.0,
child: Center(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
SizedBox( SizedBox(
width: 64,
child: IconButton( child: IconButton(
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
@ -94,29 +101,27 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
return FoodEntryChangeDialog( return FoodEntryChangeDialog(
entry: widget.entry, entry: widget.entry,
onChange: (context, entry) { onChange: (context, entry) {
widget.onChange(context, entry); widget.onChange(
context, entry);
});
}); });
},
);
} }
: null), : null)),
),
SizedBox( SizedBox(
width: 64,
child: IconButton( child: IconButton(
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
iconSize: 24, icon: const Icon(
icon: const Icon(Icons.delete), Icons.delete,
color: Colors.redAccent, color: Colors.redAccent,
),
onPressed: widget.entry.isSelected onPressed: widget.entry.isSelected
? () => widget.onDelete(context, widget.entry.id) ? () =>
: null), widget.onDelete(context, widget.entry.id)
), : null))
], ])))
), ]))
), ]));
],
),
);
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */ /* 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,65 +9,81 @@ 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>(
future: initFuture,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Center(child: CircularProgressIndicator());
}
return MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => FoodEntryBloc( create: (context) => FoodEntryBloc(
storage: storage, storage: widget.storage,
initialState: initialState: GlobalEntryState(
GlobalEntryState(foodEntries: {timeNow: entriesForToday}), foodEntries: {timeNow: entriesForToday}),
), ),
), ),
BlocProvider( BlocProvider(
create: (context) => SettingsDataBloc( create: (context) => SettingsDataBloc(
SettingsState(kcalLimit: kcalLimit), SettingsState(kcalLimit: kcalLimit),
storage: storage), storage: widget.storage),
), ),
BlocProvider( BlocProvider(
create: (context) => ThemeDataBloc( create: (context) => ThemeDataBloc(
ThemeState(brightness: brightness), ThemeState(brightness: brightness),
storage: storage), storage: widget.storage),
), ),
], ],
child: BlocBuilder<ThemeDataBloc, ThemeState>( child: BlocBuilder<ThemeDataBloc, ThemeState>(
@ -76,22 +93,15 @@ class MainApp extends StatelessWidget {
newBrightness = Brightness.dark; newBrightness = Brightness.dark;
} }
return MaterialApp.router( return MaterialApp(
//locale: Locale('de'), home: PerDatePageViewController(
routerConfig: GoRouter( initialDate: DateTimeHelper.now()),
routes: [ localizationsDelegates:
GoRoute( AppLocalizations.localizationsDelegates,
path: '/', supportedLocales: [
builder: (context, state) { Locale('en'),
return PerDatePageViewController( ...AppLocalizations.supportedLocales,
initialDate: DateTimeHelper.now(),
);
},
),
], ],
),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData( theme: ThemeData(
dividerTheme: const DividerThemeData(space: 2), dividerTheme: const DividerThemeData(space: 2),
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
@ -102,7 +112,8 @@ class MainApp extends StatelessWidget {
); );
}, },
), ),
), );
}),
); );
} }
} }

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,29 +22,55 @@ class FoodEntryList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.builder( 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, itemCount: entries.length + 1,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (BuildContext itemBuilderContext, int listIndex) { itemBuilder: (BuildContext itemBuilderContext, int listIndex) {
//last item in list is the widget to enter food //last item in list is the widget to enter food
if (listIndex == entries.length) { if (listIndex == entries.length) {
return Column( return EnterFoodWidget(
children: [ foodEntryLookupDatabase:
EnterFoodWidget( FoodStorage.getInstance().getFoodEntryLookupDatabase,
onAdd: (context, entry) { onAdd: (context, entry) {
context context
.read<FoodEntryBloc>() .read<FoodEntryBloc>()
.add(FoodEntryEvent(entry: entry, forDate: date)); .add(FoodEntryEvent(entry: entry, forDate: date));
}, },
),
const SizedBox(height: 75),
],
); );
} }
var entryIndex = listIndex; var entryIndex = listIndex;
return Column( return FoodEntryWidget(
children: [
FoodEntryWidget(
key: ValueKey(entries[entryIndex].id), key: ValueKey(entries[entryIndex].id),
entry: entries[entryIndex], entry: entries[entryIndex],
onDelete: (_, id) { onDelete: (_, id) {
@ -58,11 +88,11 @@ class FoodEntryList extends StatelessWidget {
FoodEntryTapped(entry: tappedEntry, forDate: date), 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

@ -2,7 +2,6 @@
/* 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:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/perdate/perdate_pageview.dart'; import 'package:calorimeter/perdate/perdate_pageview.dart';
import 'package:calorimeter/utils/app_drawer.dart'; import 'package:calorimeter/utils/app_drawer.dart';
@ -40,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(
@ -74,16 +60,28 @@ class PerDatePageViewController extends StatelessWidget {
}), }),
), ),
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [ floatingActionButton: _getFABs(context),
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked,
body: PerDatePageView(
pageController: pageController,
initialDate: initialDate,
),
);
},
),
);
}
OverflowBar _getFABs(BuildContext context) {
return OverflowBar(
children: [
ScanFoodFAB( ScanFoodFAB(
onPressed: () { onPressed: () {
var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add( context.read<FoodEntryBloc>().add(
BarcodeScanned( BarcodeAboutToBeScanned(
scanResultFuture: result, forDate:
forDate: context context.read<PageViewStateProvider>().displayedDate,
.read<PageViewStateProvider>()
.displayedDate,
), ),
); );
}, },
@ -94,22 +92,12 @@ class PerDatePageViewController extends StatelessWidget {
onDateSelected: (dateSelected) { onDateSelected: (dateSelected) {
if (dateSelected == null) return; if (dateSelected == null) return;
var dateDiff = dateSelected.difference(initialDate).inDays; Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
log("dateDiff = $dateDiff"); PerDatePageViewController(initialDate: dateSelected)));
pageController.jumpToPage(initialOffset - dateDiff);
}, },
), ),
]), ],
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked,
body: PerDatePageView(
pageController: pageController,
initialDate: initialDate,
),
),
);
}),
); );
} }
} }
@ -118,6 +106,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,
@ -134,6 +123,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

@ -29,10 +29,15 @@ class _PerDateWidgetState extends State<PerDateWidget>
return BlocConsumer<FoodEntryBloc, GlobalEntryState>( return BlocConsumer<FoodEntryBloc, GlobalEntryState>(
listener: (context, pageState) { listener: (context, pageState) {
if (pageState.errorString != null) { if (pageState.appError != null) {
showNewSnackbarWith(context, pageState.errorString!); showNewSnackbarWith(context, pageState.appError!);
} }
}, },
buildWhen: (previous, current) {
if (current.stateChangedForDate == null) return true;
if (current.stateChangedForDate == widget.date) return true;
return false;
},
builder: (context, pageState) { builder: (context, pageState) {
return FoodEntryList( return FoodEntryList(
entries: pageState.foodEntries[widget.date] ?? [], entries: pageState.foodEntries[widget.date] ?? [],
@ -41,9 +46,10 @@ class _PerDateWidgetState extends State<PerDateWidget>
); );
} }
void showNewSnackbarWith(BuildContext context, String text) { void showNewSnackbarWith(BuildContext context, GlobalAppError error) {
var snackbar = var snackbar = ErrorSnackbar(
ErrorSnackbar(colorScheme: Theme.of(context).colorScheme, text: text); colorScheme: Theme.of(context).colorScheme,
text: error.toErrorString(context));
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
..removeCurrentSnackBar() ..removeCurrentSnackBar()

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(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: 48),
child: Row(
children: [ children: [
Expanded(flex: 10, child: widget1 ?? Container()), Expanded(
Expanded(flex: 6, child: widget2 ?? Container()), flex: 10,
Expanded(flex: 6, child: widget3 ?? Container()), child: Padding(
Expanded(flex: 6, child: widget4 ?? Container()), 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 {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

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

View File

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