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(
name: nameController.text,
mass: massAsNumber,
kcalPerMass: kcalPerMassAsNumber,
kcalPer100: kcalPerMassAsNumber,
waitingForNetwork: false,
);

View File

@ -15,6 +15,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
required this.storage})
: super(initialState) {
on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodChangedEvent>(handleFoodChangedEvent);
on<FoodDeletionEvent>(handleDeleteFoodEvent);
on<BarcodeScanned>(handleBarcodeScannedEvent);
}
@ -30,6 +31,22 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
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(
FoodDeletionEvent event, Emitter<PageState> emit) async {
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);
var newEntryWaiting = FoodEntryState(
kcalPerMass: 0, name: "", mass: 0, waitingForNetwork: true);
kcalPer100: 0, name: "", mass: 0, waitingForNetwork: true);
newList.add(newEntryWaiting);
emit(PageState(foodEntries: newList));
@ -81,7 +98,7 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
var newEntryFinishedWaiting = FoodEntryState(
name: response.food?.name ?? "",
mass: response.food?.mass ?? 0,
kcalPerMass: response.food?.kcalPer100g ?? 0,
kcalPer100: response.food?.kcalPer100g ?? 0,
waitingForNetwork: false,
);
newList.add(newEntryFinishedWaiting);
@ -98,6 +115,12 @@ class FoodEntryEvent extends FoodEvent {
FoodEntryEvent({required this.entry});
}
class FoodChangedEvent extends FoodEvent {
final FoodEntryState newEntry;
FoodChangedEvent({required this.newEntry});
}
class FoodDeletionEvent extends FoodEvent {
final String entryID;
@ -133,21 +156,37 @@ class PageState {
class FoodEntryState {
final String name;
final int mass;
final int kcalPerMass;
final int kcalPer100;
final String id;
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.mass,
required this.kcalPerMass,
required this.kcalPer100,
required this.waitingForNetwork,
}) : id = const Uuid().v1();
});
@override
String toString() {
//we use quotation marks around the name because the name might contain
//commas and we want to store it in a csv file
return '$id,"$name",$mass,$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 {
final FoodEntryState entry;
final Function(BuildContext context, String id) onDelete;
final Function(BuildContext context, FoodEntryState entry) onChange;
const FoodEntryWidget(
{super.key, required this.entry, required this.onDelete});
const FoodEntryWidget({
super.key,
required this.entry,
required this.onDelete,
required this.onChange,
});
@override
State<FoodEntryWidget> createState() => _FoodEntryWidgetState();
@ -45,17 +50,24 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
? Container()
: Text(widget.entry.mass.ceil().toString(),
textAlign: TextAlign.end),
widget.entry.waitingForNetwork
? Container()
: Text(widget.entry.kcalPerMass.ceil().toString(),
textAlign: TextAlign.end),
Opacity(
opacity: showCancelAndDelete ? 0.0 : 1.0,
child: Text(
(widget.entry.mass * widget.entry.kcalPerMass / 100)
.ceil()
.toString(),
textAlign: TextAlign.end),
child: widget.entry.waitingForNetwork
? Container()
: Text(widget.entry.kcalPer100.ceil().toString(),
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)
: 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(
key: ValueKey(entries[entryIndex].id),
entry: entries[entryIndex],
onDelete: (callbackContext, id) {
callbackContext.read<FoodEntryBloc>().add(FoodDeletionEvent(
onDelete: (_, id) {
context.read<FoodEntryBloc>().add(FoodDeletionEvent(
entryID: id,
));
},
onChange: (_, changedEntry) {
context
.read<FoodEntryBloc>()
.add(FoodChangedEvent(newEntry: changedEntry));
},
),
const Divider(),
],

View File

@ -54,43 +54,45 @@ class _PerDateWidgetState extends State<PerDateWidget> {
)
],
child: BlocConsumer<FoodEntryBloc, PageState>(
listener: (context, pageState) {
if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!);
}
}, builder: (context, pageState) {
return Scaffold(
appBar: AppBar(
title:
Text(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));
},
listener: (context, pageState) {
if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!);
}
},
builder: (context, pageState) {
return Scaffold(
appBar: AppBar(
title: Text(
DateFormat.yMMMMd('de').format(widget.date)),
actions: const [ThemeSwitcherButton()],
),
const SizedBox(width: 8),
CalendarFloatingButton(
startFromDate: widget.date,
onDateSelected: (dateSelected) {
_onDateSelected(dateSelected);
},
),
]),
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked);
}),
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),
CalendarFloatingButton(
startFromDate: widget.date,
onDateSelected: (dateSelected) {
_onDateSelected(dateSelected);
},
),
]),
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked);
},
),
);
});
}

View File

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

View File

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