Handle changing a food entry

This commit is contained in:
Marco 2024-09-24 17:23:01 +02:00
parent a7a7f44050
commit ace03d98d2
7 changed files with 264 additions and 63 deletions

View File

@ -126,7 +126,7 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
var entry = FoodEntryState( var entry = FoodEntryState(
name: nameController.text, name: nameController.text,
mass: massAsNumber, mass: massAsNumber,
kcalPerMass: kcalPerMassAsNumber, kcalPer100: kcalPerMassAsNumber,
waitingForNetwork: false, waitingForNetwork: false,
); );

View File

@ -15,6 +15,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
required this.storage}) required this.storage})
: super(initialState) { : super(initialState) {
on<FoodEntryEvent>(handleFoodEntryEvent); on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodChangedEvent>(handleFoodChangedEvent);
on<FoodDeletionEvent>(handleDeleteFoodEvent); on<FoodDeletionEvent>(handleDeleteFoodEvent);
on<BarcodeScanned>(handleBarcodeScannedEvent); on<BarcodeScanned>(handleBarcodeScannedEvent);
} }
@ -30,6 +31,22 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
emit(newState); emit(newState);
} }
void handleFoodChangedEvent(
FoodChangedEvent event, Emitter<PageState> emit) async {
var entries = state.foodEntries;
var index = entries.indexWhere((entry) {
return entry.id == event.newEntry.id;
});
entries.removeAt(index);
entries.insert(index, event.newEntry);
await storage.writeEntriesForDate(forDate, entries);
storage.addFoodEntryToLookupDatabase(event.newEntry);
emit(PageState(foodEntries: entries));
}
void handleDeleteFoodEvent( void handleDeleteFoodEvent(
FoodDeletionEvent event, Emitter<PageState> emit) async { FoodDeletionEvent event, Emitter<PageState> emit) async {
state.foodEntries.removeWhere((entry) => entry.id == event.entryID); state.foodEntries.removeWhere((entry) => entry.id == event.entryID);
@ -57,7 +74,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
List<FoodEntryState> newList = List.from(state.foodEntries); List<FoodEntryState> newList = List.from(state.foodEntries);
var newEntryWaiting = FoodEntryState( var newEntryWaiting = FoodEntryState(
kcalPerMass: 0, name: "", mass: 0, waitingForNetwork: true); kcalPer100: 0, name: "", mass: 0, waitingForNetwork: true);
newList.add(newEntryWaiting); newList.add(newEntryWaiting);
emit(PageState(foodEntries: newList)); emit(PageState(foodEntries: newList));
@ -81,7 +98,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
var newEntryFinishedWaiting = FoodEntryState( var newEntryFinishedWaiting = FoodEntryState(
name: response.food?.name ?? "", name: response.food?.name ?? "",
mass: response.food?.mass ?? 0, mass: response.food?.mass ?? 0,
kcalPerMass: response.food?.kcalPer100g ?? 0, kcalPer100: response.food?.kcalPer100g ?? 0,
waitingForNetwork: false, waitingForNetwork: false,
); );
newList.add(newEntryFinishedWaiting); newList.add(newEntryFinishedWaiting);
@ -98,6 +115,12 @@ class FoodEntryEvent extends FoodEvent {
FoodEntryEvent({required this.entry}); FoodEntryEvent({required this.entry});
} }
class FoodChangedEvent extends FoodEvent {
final FoodEntryState newEntry;
FoodChangedEvent({required this.newEntry});
}
class FoodDeletionEvent extends FoodEvent { class FoodDeletionEvent extends FoodEvent {
final String entryID; final String entryID;
@ -133,21 +156,37 @@ class PageState {
class FoodEntryState { class FoodEntryState {
final String name; final String name;
final int mass; final int mass;
final int kcalPerMass; final int kcalPer100;
final String id; final String id;
final bool waitingForNetwork; final bool waitingForNetwork;
FoodEntryState({ factory FoodEntryState({
required name,
required mass,
required kcalPer100,
required waitingForNetwork,
}) {
return FoodEntryState.withID(
id: const Uuid().v1(),
name: name,
mass: mass,
kcalPer100: kcalPer100,
waitingForNetwork: waitingForNetwork,
);
}
FoodEntryState.withID({
required this.id,
required this.name, required this.name,
required this.mass, required this.mass,
required this.kcalPerMass, required this.kcalPer100,
required this.waitingForNetwork, required this.waitingForNetwork,
}) : id = const Uuid().v1(); });
@override @override
String toString() { String toString() {
//we use quotation marks around the name because the name might contain //we use quotation marks around the name because the name might contain
//commas and we want to store it in a csv file //commas and we want to store it in a csv file
return '$id,"$name",$mass,$kcalPerMass'; return '$id,"$name",$mass,$kcalPer100';
} }
} }

View File

@ -5,9 +5,14 @@ import 'package:calorimeter/utils/row_with_spacers_widget.dart';
class FoodEntryWidget extends StatefulWidget { class FoodEntryWidget extends StatefulWidget {
final FoodEntryState entry; final FoodEntryState entry;
final Function(BuildContext context, String id) onDelete; final Function(BuildContext context, String id) onDelete;
final Function(BuildContext context, FoodEntryState entry) onChange;
const FoodEntryWidget( const FoodEntryWidget({
{super.key, required this.entry, required this.onDelete}); super.key,
required this.entry,
required this.onDelete,
required this.onChange,
});
@override @override
State<FoodEntryWidget> createState() => _FoodEntryWidgetState(); State<FoodEntryWidget> createState() => _FoodEntryWidgetState();
@ -45,17 +50,24 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
? Container() ? Container()
: Text(widget.entry.mass.ceil().toString(), : Text(widget.entry.mass.ceil().toString(),
textAlign: TextAlign.end), textAlign: TextAlign.end),
widget.entry.waitingForNetwork
? Container()
: Text(widget.entry.kcalPerMass.ceil().toString(),
textAlign: TextAlign.end),
Opacity( Opacity(
opacity: showCancelAndDelete ? 0.0 : 1.0, opacity: showCancelAndDelete ? 0.0 : 1.0,
child: Text( child: widget.entry.waitingForNetwork
(widget.entry.mass * widget.entry.kcalPerMass / 100) ? Container()
.ceil() : Text(widget.entry.kcalPer100.ceil().toString(),
.toString(), textAlign: TextAlign.end),
textAlign: TextAlign.end), ),
Opacity(
opacity: showCancelAndDelete ? 0.0 : 1.0,
child: widget.entry.waitingForNetwork
? Container()
: Text(
(widget.entry.mass *
widget.entry.kcalPer100 /
100)
.ceil()
.toString(),
textAlign: TextAlign.end),
), ),
), ),
), ),
@ -92,6 +104,28 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
? () => widget.onDelete(context, widget.entry.id) ? () => widget.onDelete(context, widget.entry.id)
: null), : null),
), ),
SizedBox(
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(Icons.edit),
onPressed: showCancelAndDelete
? () async {
await showDialog(
context: context,
builder: (dialogContext) {
return FoodEntryChangeDialog(
entry: widget.entry,
onChange: (context, entry) {
widget.onChange(context, entry);
});
},
);
setState(() {
showCancelAndDelete = false;
});
}
: null),
),
], ],
), ),
), ),
@ -100,3 +134,124 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
); );
} }
} }
class FoodEntryChangeDialog extends StatefulWidget {
final FoodEntryState entry;
final Function(BuildContext context, FoodEntryState entry) onChange;
const FoodEntryChangeDialog(
{required this.entry, super.key, required this.onChange});
@override
State<FoodEntryChangeDialog> createState() => _FoodEntryChangeDialogState();
}
class _FoodEntryChangeDialogState extends State<FoodEntryChangeDialog> {
late TextEditingController nameController;
late TextEditingController massController;
late TextEditingController kcalPer100Controller;
static const textFieldVerticalPadding = 16.0;
static const textFieldHorizontalPadding = 16.0;
@override
void initState() {
nameController = TextEditingController();
nameController.text = widget.entry.name;
massController = TextEditingController();
massController.text = widget.entry.mass.toString();
kcalPer100Controller = TextEditingController();
kcalPer100Controller.text = widget.entry.kcalPer100.toString();
super.initState();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: textFieldVerticalPadding,
horizontal: textFieldHorizontalPadding),
child: TextField(
onSubmitted: (val) => _onSubmitAction(),
controller: nameController,
decoration: const InputDecoration(
label: Text("Name"),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: textFieldVerticalPadding,
horizontal: textFieldHorizontalPadding),
child: TextField(
onSubmitted: (val) => _onSubmitAction(),
controller: massController,
decoration: const InputDecoration(
label: Text("Menge"),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: textFieldVerticalPadding,
horizontal: textFieldHorizontalPadding),
child: TextField(
onSubmitted: (val) => _onSubmitAction(),
controller: kcalPer100Controller,
decoration: const InputDecoration(
label: Text("kcal pro Menge"),
),
),
)
],
),
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.cancel)),
IconButton(
onPressed: () => _onSubmitAction(),
icon: const Icon(Icons.check),
)
],
);
}
void _onSubmitAction() {
int mass;
int kcalPer100;
try {
mass = int.parse(massController.text);
} catch (e) {
mass = 0;
}
try {
kcalPer100 = int.parse(kcalPer100Controller.text);
} catch (e) {
kcalPer100 = 0;
}
var newEntry = FoodEntryState.withID(
id: widget.entry.id,
name: nameController.text,
mass: mass,
kcalPer100: kcalPer100,
waitingForNetwork: widget.entry.waitingForNetwork,
);
widget.onChange(context, newEntry);
Navigator.of(context).pop();
}
}

View File

@ -39,11 +39,16 @@ class FoodEntryList extends StatelessWidget {
FoodEntryWidget( FoodEntryWidget(
key: ValueKey(entries[entryIndex].id), key: ValueKey(entries[entryIndex].id),
entry: entries[entryIndex], entry: entries[entryIndex],
onDelete: (callbackContext, id) { onDelete: (_, id) {
callbackContext.read<FoodEntryBloc>().add(FoodDeletionEvent( context.read<FoodEntryBloc>().add(FoodDeletionEvent(
entryID: id, entryID: id,
)); ));
}, },
onChange: (_, changedEntry) {
context
.read<FoodEntryBloc>()
.add(FoodChangedEvent(newEntry: changedEntry));
},
), ),
const Divider(), const Divider(),
], ],

View File

@ -54,43 +54,45 @@ class _PerDateWidgetState extends State<PerDateWidget> {
) )
], ],
child: BlocConsumer<FoodEntryBloc, PageState>( child: BlocConsumer<FoodEntryBloc, PageState>(
listener: (context, pageState) { listener: (context, pageState) {
if (pageState.errorString != null) { if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!); showNewSnackbarWith(context, pageState.errorString!);
} }
}, builder: (context, pageState) { },
return Scaffold( builder: (context, pageState) {
appBar: AppBar( return Scaffold(
title: appBar: AppBar(
Text(DateFormat.yMMMMd('de').format(widget.date)), title: Text(
actions: const [ThemeSwitcherButton()], DateFormat.yMMMMd('de').format(widget.date)),
), actions: const [ThemeSwitcherButton()],
body: FoodEntryList(entries: pageState.foodEntries),
bottomNavigationBar: BottomAppBar(
shape: const RectangularNotchShape(),
color: Theme.of(context).colorScheme.secondary,
child:
SumWidget(foodEntries: pageState.foodEntries)),
drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [
ScanFoodFloatingButton(
onPressed: () {
var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add(
BarcodeScanned(scanResultFuture: result));
},
), ),
const SizedBox(width: 8), body: FoodEntryList(entries: pageState.foodEntries),
CalendarFloatingButton( bottomNavigationBar: BottomAppBar(
startFromDate: widget.date, shape: const RectangularNotchShape(),
onDateSelected: (dateSelected) { color: Theme.of(context).colorScheme.secondary,
_onDateSelected(dateSelected); child: SumWidget(
}, foodEntries: pageState.foodEntries)),
), drawer: const AppDrawer(),
]), floatingActionButton: OverflowBar(children: [
floatingActionButtonLocation: ScanFoodFloatingButton(
FloatingActionButtonLocation.endDocked); onPressed: () {
}), var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add(
BarcodeScanned(scanResultFuture: result));
},
),
const SizedBox(width: 8),
CalendarFloatingButton(
startFromDate: widget.date,
onDateSelected: (dateSelected) {
_onDateSelected(dateSelected);
},
),
]),
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked);
},
),
); );
}); });
} }

View File

@ -62,7 +62,7 @@ class FoodStorage {
var entry = FoodEntryState( var entry = FoodEntryState(
name: fields[1].replaceAll('"', ""), name: fields[1].replaceAll('"', ""),
mass: mass, mass: mass,
kcalPerMass: kcalPerMass, kcalPer100: kcalPerMass,
waitingForNetwork: false, waitingForNetwork: false,
); );
entries.add(entry); entries.add(entry);
@ -169,14 +169,14 @@ class FoodStorage {
var entriesForDate = await getEntriesForDate(date); var entriesForDate = await getEntriesForDate(date);
for (var entry in entriesForDate) { for (var entry in entriesForDate) {
_foodLookupDatabase[entry.name] = entry.kcalPerMass; _foodLookupDatabase[entry.name] = entry.kcalPer100;
log("Added entry: ${entry.name}/${entry.kcalPerMass}"); log("Added entry: ${entry.name}/${entry.kcalPer100}");
} }
} }
void addFoodEntryToLookupDatabase(FoodEntryState entry) { void addFoodEntryToLookupDatabase(FoodEntryState entry) {
_foodLookupDatabase[entry.name] = entry.kcalPerMass; _foodLookupDatabase[entry.name] = entry.kcalPer100;
log("Added entry: ${entry.name}/${entry.kcalPerMass}"); log("Added entry: ${entry.name}/${entry.kcalPer100}");
} }
Map<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase; Map<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase;

View File

@ -13,7 +13,7 @@ class SumWidget extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
var sum = 0.0; var sum = 0.0;
for (var entry in foodEntries) { for (var entry in foodEntries) {
sum += entry.kcalPerMass / 100 * entry.mass; sum += entry.kcalPer100 / 100 * entry.mass;
} }
var diff = state.kcalLimit - sum; var diff = state.kcalLimit - sum;
var diffLimit = state.kcalLimit ~/ 4; var diffLimit = state.kcalLimit ~/ 4;