Make scanned widgets appear in the list instead of putting the info into
the EnterFoodWidget. 1. Make scanned foods appear in the list of foods 2. Remove Controller for entering food This commit removes the EnterFoodController that was used to put a scanned food into the EnterFoodWidget. This is now unnecessary because scanning a product will be distributed via the FoodBLoC.
This commit is contained in:
parent
e75a0765b4
commit
a7a7f44050
@ -1,4 +1,3 @@
|
|||||||
import 'package:calorimeter/utils/enter_food_controller.dart';
|
|
||||||
import 'package:calorimeter/storage/storage.dart';
|
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';
|
||||||
@ -6,7 +5,7 @@ import 'package:calorimeter/utils/row_with_spacers_widget.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class EnterFoodWidget extends StatefulWidget {
|
class EnterFoodWidget extends StatefulWidget {
|
||||||
final Function(BuildContext context, FoodEntry entry) onAdd;
|
final Function(BuildContext context, FoodEntryState entry) onAdd;
|
||||||
|
|
||||||
const EnterFoodWidget({super.key, required this.onAdd});
|
const EnterFoodWidget({super.key, required this.onAdd});
|
||||||
|
|
||||||
@ -18,7 +17,7 @@ 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, double> suggestions;
|
late Map<String, int> suggestions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -32,88 +31,80 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<EnterFoodController>(
|
return Padding(
|
||||||
builder: (context, food, child) {
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
nameController.text = food.name;
|
child: RowWidget(
|
||||||
kcalPerMassController.text = food.kcalPer100g;
|
Autocomplete<String>(
|
||||||
|
optionsViewOpenDirection: OptionsViewOpenDirection.down,
|
||||||
|
fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
|
||||||
|
nameController = controller;
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text("Name"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
|
if (textEditingValue.text == '') {
|
||||||
|
return const Iterable<String>.empty();
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return suggestions.keys.where(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
(name) {
|
||||||
child: RowWidget(
|
return name
|
||||||
Autocomplete<String>(
|
.toLowerCase()
|
||||||
optionsViewOpenDirection: OptionsViewOpenDirection.down,
|
.contains(textEditingValue.text.toLowerCase());
|
||||||
fieldViewBuilder:
|
|
||||||
(context, controller, focusNode, onSubmitted) {
|
|
||||||
nameController = controller;
|
|
||||||
return TextFormField(
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
label: Text("Name"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
);
|
||||||
if (textEditingValue.text == '') {
|
},
|
||||||
return const Iterable<String>.empty();
|
onSelected: (selectedFood) {
|
||||||
}
|
int kcalPerMassForSelectedFood = suggestions[selectedFood]!;
|
||||||
|
setState(() {
|
||||||
return suggestions.keys.where(
|
nameController.text = selectedFood;
|
||||||
(name) {
|
kcalPerMassController.text =
|
||||||
return name
|
kcalPerMassForSelectedFood.toString();
|
||||||
.toLowerCase()
|
});
|
||||||
.contains(textEditingValue.text.toLowerCase());
|
}),
|
||||||
},
|
TextField(
|
||||||
);
|
textAlign: TextAlign.end,
|
||||||
},
|
decoration: const InputDecoration(
|
||||||
onSelected: (selectedFood) {
|
label:
|
||||||
double kcalPerMassForSelectedFood =
|
Align(alignment: Alignment.centerRight, child: Text("Menge")),
|
||||||
suggestions[selectedFood]!;
|
|
||||||
context
|
|
||||||
.read<EnterFoodController>()
|
|
||||||
.set(selectedFood, kcalPerMassForSelectedFood.toString());
|
|
||||||
}),
|
|
||||||
TextField(
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
label: Align(
|
|
||||||
alignment: Alignment.centerRight, child: Text("Menge")),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
controller: massController,
|
|
||||||
onSubmitted: (value) => onSubmitAction(),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
label: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Text("kcal pro"))),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
controller: kcalPerMassController,
|
|
||||||
onSubmitted: (value) => onSubmitAction(),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
onPressed: () => onSubmitAction(),
|
|
||||||
child: const Icon(Icons.add)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
keyboardType: TextInputType.number,
|
||||||
},
|
controller: massController,
|
||||||
|
onSubmitted: (value) => onSubmitAction(),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Align(
|
||||||
|
alignment: Alignment.centerRight, child: Text("kcal pro"))),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
controller: kcalPerMassController,
|
||||||
|
onSubmitted: (value) => onSubmitAction(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
onPressed: () => onSubmitAction(),
|
||||||
|
child: const Icon(Icons.add)),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSubmitAction() {
|
void onSubmitAction() {
|
||||||
double massAsNumber = 0.0;
|
int massAsNumber = 0;
|
||||||
double kcalPerMassAsNumber = 0.0;
|
int kcalPerMassAsNumber = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
massAsNumber = double.parse(massController.text.replaceAll(",", "."));
|
massAsNumber = int.parse(massController.text.replaceAll(",", "."));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
var snackbar = const SnackBar(content: Text("Menge muss eine Zahl sein"));
|
var snackbar = const SnackBar(content: Text("Menge muss eine Zahl sein"));
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
@ -123,21 +114,27 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
kcalPerMassAsNumber =
|
kcalPerMassAsNumber =
|
||||||
double.parse(kcalPerMassController.text.replaceAll(",", "."));
|
int.parse(kcalPerMassController.text.replaceAll(",", "."));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
var snackbar =
|
var snackbar =
|
||||||
const SnackBar(content: Text("'kcal pro 100g' muss eine Zahl sein"));
|
const SnackBar(content: Text("'kcal pro 100g' muss eine Zahl sein"));
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).removeCurrentSnackBar();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackbar);
|
ScaffoldMessenger.of(context).showSnackBar(snackbar);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = FoodEntry(
|
var entry = FoodEntryState(
|
||||||
name: nameController.text,
|
name: nameController.text,
|
||||||
mass: massAsNumber,
|
mass: massAsNumber,
|
||||||
kcalPerMass: kcalPerMassAsNumber);
|
kcalPerMass: kcalPerMassAsNumber,
|
||||||
|
waitingForNetwork: false,
|
||||||
|
);
|
||||||
|
|
||||||
widget.onAdd(context, entry);
|
widget.onAdd(context, entry);
|
||||||
context.read<EnterFoodController>().set("", "");
|
setState(() {
|
||||||
|
nameController.text = "";
|
||||||
|
massController.text = "";
|
||||||
|
kcalPerMassController.text = "";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:calorimeter/food_scan/food_fact_lookup.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:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> {
|
class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
|
||||||
final FoodEntryState initialState;
|
final PageState initialState;
|
||||||
final FoodStorage storage;
|
final FoodStorage storage;
|
||||||
final DateTime forDate;
|
final DateTime forDate;
|
||||||
|
|
||||||
@ -13,13 +15,13 @@ class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> {
|
|||||||
required this.storage})
|
required this.storage})
|
||||||
: super(initialState) {
|
: super(initialState) {
|
||||||
on<FoodEntryEvent>(handleFoodEntryEvent);
|
on<FoodEntryEvent>(handleFoodEntryEvent);
|
||||||
on<FoodDeletionEvent>(deleteFood);
|
on<FoodDeletionEvent>(handleDeleteFoodEvent);
|
||||||
on<PageChangedEvent>(updateEntries);
|
on<BarcodeScanned>(handleBarcodeScannedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleFoodEntryEvent(
|
void handleFoodEntryEvent(
|
||||||
FoodEntryEvent event, Emitter<FoodEntryState> emit) async {
|
FoodEntryEvent event, Emitter<PageState> emit) async {
|
||||||
FoodEntryState newState = FoodEntryState.from(state);
|
PageState newState = PageState.from(state);
|
||||||
newState.addEntry(event.entry);
|
newState.addEntry(event.entry);
|
||||||
|
|
||||||
await storage.writeEntriesForDate(forDate, newState.foodEntries);
|
await storage.writeEntriesForDate(forDate, newState.foodEntries);
|
||||||
@ -28,26 +30,70 @@ class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> {
|
|||||||
emit(newState);
|
emit(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteFood(FoodDeletionEvent event, Emitter<FoodEntryState> emit) async {
|
void handleDeleteFoodEvent(
|
||||||
|
FoodDeletionEvent event, Emitter<PageState> emit) async {
|
||||||
state.foodEntries.removeWhere((entry) => entry.id == event.entryID);
|
state.foodEntries.removeWhere((entry) => entry.id == event.entryID);
|
||||||
|
|
||||||
await storage.writeEntriesForDate(forDate, state.foodEntries);
|
await storage.writeEntriesForDate(forDate, state.foodEntries);
|
||||||
|
|
||||||
emit(FoodEntryState.from(state));
|
emit(PageState.from(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateEntries(
|
void handleBarcodeScannedEvent(
|
||||||
PageChangedEvent event, Emitter<FoodEntryState> emit) async {
|
BarcodeScanned event, Emitter<PageState> emit) async {
|
||||||
var entries = await storage.getEntriesForDate(event.changedToDate);
|
var client = FoodFactLookupClient();
|
||||||
var newState = FoodEntryState(foodEntries: entries);
|
var scanResult = await event.scanResultFuture;
|
||||||
emit(newState);
|
|
||||||
|
if (scanResult.type == ResultType.Cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scanResult.type == ResultType.Error) {
|
||||||
|
emit(PageState(
|
||||||
|
foodEntries: state.foodEntries,
|
||||||
|
errorString: "Fehler beim Scannen des Barcodes"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
|
||||||
|
|
||||||
|
List<FoodEntryState> newList = List.from(state.foodEntries);
|
||||||
|
var newEntryWaiting = FoodEntryState(
|
||||||
|
kcalPerMass: 0, name: "", mass: 0, waitingForNetwork: true);
|
||||||
|
newList.add(newEntryWaiting);
|
||||||
|
emit(PageState(foodEntries: newList));
|
||||||
|
|
||||||
|
await responseFuture.then((response) {
|
||||||
|
newList.removeWhere((entryState) => entryState.id == newEntryWaiting.id);
|
||||||
|
|
||||||
|
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
|
||||||
|
emit(PageState(
|
||||||
|
foodEntries: state.foodEntries,
|
||||||
|
errorString: "Barcode konnte nicht gefunden werden."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status ==
|
||||||
|
FoodFactResponseStatus.foodFactServerNotReachable) {
|
||||||
|
emit(PageState(
|
||||||
|
foodEntries: state.foodEntries,
|
||||||
|
errorString: "OpenFoodFacts-Server konnte nicht erreicht werden."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newEntryFinishedWaiting = FoodEntryState(
|
||||||
|
name: response.food?.name ?? "",
|
||||||
|
mass: response.food?.mass ?? 0,
|
||||||
|
kcalPerMass: response.food?.kcalPer100g ?? 0,
|
||||||
|
waitingForNetwork: false,
|
||||||
|
);
|
||||||
|
newList.add(newEntryFinishedWaiting);
|
||||||
|
emit(PageState(foodEntries: newList));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FoodEvent {}
|
class FoodEvent {}
|
||||||
|
|
||||||
class FoodEntryEvent extends FoodEvent {
|
class FoodEntryEvent extends FoodEvent {
|
||||||
final FoodEntry entry;
|
final FoodEntryState entry;
|
||||||
|
|
||||||
FoodEntryEvent({required this.entry});
|
FoodEntryEvent({required this.entry});
|
||||||
}
|
}
|
||||||
@ -58,41 +104,44 @@ class FoodDeletionEvent extends FoodEvent {
|
|||||||
FoodDeletionEvent({required this.entryID});
|
FoodDeletionEvent({required this.entryID});
|
||||||
}
|
}
|
||||||
|
|
||||||
class PageChangedEvent extends FoodEvent {
|
class BarcodeScanned extends FoodEvent {
|
||||||
final DateTime changedToDate;
|
final Future<ScanResult> scanResultFuture;
|
||||||
|
|
||||||
PageChangedEvent({required this.changedToDate});
|
BarcodeScanned({required this.scanResultFuture});
|
||||||
}
|
}
|
||||||
|
|
||||||
class FoodEntryState {
|
/// This is the state for one date/page
|
||||||
final List<FoodEntry> foodEntries;
|
class PageState {
|
||||||
|
final List<FoodEntryState> foodEntries;
|
||||||
|
final String? errorString;
|
||||||
|
|
||||||
FoodEntryState({required this.foodEntries});
|
PageState({required this.foodEntries, this.errorString});
|
||||||
|
|
||||||
factory FoodEntryState.init() {
|
factory PageState.init() {
|
||||||
return FoodEntryState(foodEntries: []);
|
return PageState(foodEntries: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
static from(FoodEntryState state) {
|
static from(PageState state) {
|
||||||
List<FoodEntry> newList = List.from(state.foodEntries);
|
return PageState(foodEntries: state.foodEntries);
|
||||||
return FoodEntryState(foodEntries: newList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addEntry(FoodEntry entry) {
|
void addEntry(FoodEntryState entry) {
|
||||||
foodEntries.add(entry);
|
foodEntries.add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FoodEntry {
|
class FoodEntryState {
|
||||||
final String name;
|
final String name;
|
||||||
final double mass;
|
final int mass;
|
||||||
final double kcalPerMass;
|
final int kcalPerMass;
|
||||||
final String id;
|
final String id;
|
||||||
|
final bool waitingForNetwork;
|
||||||
|
|
||||||
FoodEntry({
|
FoodEntryState({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.mass,
|
required this.mass,
|
||||||
required this.kcalPerMass,
|
required this.kcalPerMass,
|
||||||
|
required this.waitingForNetwork,
|
||||||
}) : id = const Uuid().v1();
|
}) : id = const Uuid().v1();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -3,7 +3,7 @@ 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';
|
||||||
|
|
||||||
class FoodEntryWidget extends StatefulWidget {
|
class FoodEntryWidget extends StatefulWidget {
|
||||||
final FoodEntry entry;
|
final FoodEntryState entry;
|
||||||
final Function(BuildContext context, String id) onDelete;
|
final Function(BuildContext context, String id) onDelete;
|
||||||
|
|
||||||
const FoodEntryWidget(
|
const FoodEntryWidget(
|
||||||
@ -38,11 +38,17 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: RowWidget(
|
child: RowWidget(
|
||||||
Text(widget.entry.name),
|
widget.entry.waitingForNetwork
|
||||||
Text(widget.entry.mass.ceil().toString(),
|
? const Center(child: CircularProgressIndicator())
|
||||||
textAlign: TextAlign.end),
|
: Text(widget.entry.name),
|
||||||
Text(widget.entry.kcalPerMass.ceil().toString(),
|
widget.entry.waitingForNetwork
|
||||||
textAlign: TextAlign.end),
|
? 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(
|
||||||
opacity: showCancelAndDelete ? 0.0 : 1.0,
|
opacity: showCancelAndDelete ? 0.0 : 1.0,
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -45,13 +45,28 @@ class FoodFactLookupClient {
|
|||||||
class FoodFactModel {
|
class FoodFactModel {
|
||||||
final String name;
|
final String name;
|
||||||
final int kcalPer100g;
|
final int kcalPer100g;
|
||||||
|
final int mass;
|
||||||
|
|
||||||
FoodFactModel({required this.name, required this.kcalPer100g});
|
FoodFactModel({
|
||||||
|
required this.name,
|
||||||
|
required this.mass,
|
||||||
|
required this.kcalPer100g,
|
||||||
|
});
|
||||||
|
|
||||||
factory FoodFactModel.fromJson(Map<String, dynamic> json) {
|
factory FoodFactModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
String quantityString = json['product']['product_quantity'] ?? "0";
|
||||||
|
int quantity;
|
||||||
|
|
||||||
|
try {
|
||||||
|
quantity = int.parse(quantityString);
|
||||||
|
} catch (e) {
|
||||||
|
quantity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return FoodFactModel(
|
return FoodFactModel(
|
||||||
name: json['product']['product_name'],
|
name: json['product']['product_name'],
|
||||||
kcalPer100g: json['product']['nutriments']['energy-kcal_100g']);
|
kcalPer100g: json['product']['nutriments']['energy-kcal_100g'],
|
||||||
|
mass: quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class FoodEntryList extends StatelessWidget {
|
class FoodEntryList extends StatelessWidget {
|
||||||
final List<FoodEntry> entries;
|
final List<FoodEntryState> entries;
|
||||||
|
|
||||||
const FoodEntryList({
|
const FoodEntryList({
|
||||||
required this.entries,
|
required this.entries,
|
||||||
@ -17,6 +17,7 @@ class FoodEntryList extends StatelessWidget {
|
|||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: entries.length + 1,
|
itemCount: entries.length + 1,
|
||||||
itemBuilder: (BuildContext itemBuilderContext, int listIndex) {
|
itemBuilder: (BuildContext itemBuilderContext, int listIndex) {
|
||||||
|
//last item in list is the widget to enter food
|
||||||
if (listIndex == entries.length) {
|
if (listIndex == entries.length) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
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/utils/enter_food_controller.dart';
|
|
||||||
import 'package:calorimeter/utils/scan_food_floating_button.dart';
|
import 'package:calorimeter/utils/scan_food_floating_button.dart';
|
||||||
import 'package:calorimeter/utils/app_drawer.dart';
|
import 'package:calorimeter/utils/app_drawer.dart';
|
||||||
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
|
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
|
||||||
@ -25,8 +23,8 @@ class PerDateWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _PerDateWidgetState extends State<PerDateWidget> {
|
class _PerDateWidgetState extends State<PerDateWidget> {
|
||||||
late FoodStorage storage;
|
late FoodStorage storage;
|
||||||
late Future<List<FoodEntry>> entriesFuture;
|
late Future<List<FoodEntryState>> entriesFuture;
|
||||||
List<FoodEntry> entries = [];
|
List<FoodEntryState> entries = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -41,57 +39,60 @@ class _PerDateWidgetState extends State<PerDateWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: entriesFuture,
|
future: entriesFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return snapshot.connectionState != ConnectionState.done
|
return snapshot.connectionState != ConnectionState.done
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: MultiProvider(
|
: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(
|
BlocProvider(
|
||||||
create: (context) => EnterFoodController()),
|
create: (context) => FoodEntryBloc(
|
||||||
BlocProvider(
|
initialState: PageState(foodEntries: entries),
|
||||||
create: (context) => FoodEntryBloc(
|
storage: storage,
|
||||||
initialState: FoodEntryState(foodEntries: entries),
|
forDate: widget.date,
|
||||||
storage: storage,
|
),
|
||||||
forDate: widget.date,
|
)
|
||||||
),
|
],
|
||||||
)
|
child: BlocConsumer<FoodEntryBloc, PageState>(
|
||||||
],
|
listener: (context, pageState) {
|
||||||
child: BlocBuilder<FoodEntryBloc, FoodEntryState>(
|
if (pageState.errorString != null) {
|
||||||
builder: (context, state) {
|
showNewSnackbarWith(context, pageState.errorString!);
|
||||||
|
}
|
||||||
|
}, builder: (context, pageState) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title:
|
title:
|
||||||
Text(DateFormat.yMMMMd('de').format(widget.date)),
|
Text(DateFormat.yMMMMd('de').format(widget.date)),
|
||||||
actions: const [ThemeSwitcherButton()],
|
actions: const [ThemeSwitcherButton()],
|
||||||
),
|
),
|
||||||
body: FoodEntryList(entries: state.foodEntries),
|
body: FoodEntryList(entries: pageState.foodEntries),
|
||||||
bottomNavigationBar: BottomAppBar(
|
bottomNavigationBar: BottomAppBar(
|
||||||
shape: const RectangularNotchShape(),
|
shape: const RectangularNotchShape(),
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
child: SumWidget(foodEntries: state.foodEntries)),
|
child:
|
||||||
|
SumWidget(foodEntries: pageState.foodEntries)),
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: OverflowBar(
|
floatingActionButton: OverflowBar(children: [
|
||||||
children: [
|
ScanFoodFloatingButton(
|
||||||
ScanFoodFloatingButton(onPressed: () {
|
onPressed: () {
|
||||||
_onScanButtonPressed(context);
|
var result = BarcodeScanner.scan();
|
||||||
}),
|
context.read<FoodEntryBloc>().add(
|
||||||
const SizedBox(width: 8),
|
BarcodeScanned(scanResultFuture: result));
|
||||||
CalendarFloatingButton(
|
},
|
||||||
startFromDate: widget.date,
|
),
|
||||||
onDateSelected: (dateSelected) {
|
const SizedBox(width: 8),
|
||||||
_onDateSelected(dateSelected);
|
CalendarFloatingButton(
|
||||||
},
|
startFromDate: widget.date,
|
||||||
),
|
onDateSelected: (dateSelected) {
|
||||||
],
|
_onDateSelected(dateSelected);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
floatingActionButtonLocation:
|
floatingActionButtonLocation:
|
||||||
FloatingActionButtonLocation.endDocked);
|
FloatingActionButtonLocation.endDocked);
|
||||||
},
|
}),
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void showNewSnackbarWith(BuildContext context, String text) {
|
void showNewSnackbarWith(BuildContext context, String text) {
|
||||||
@ -123,41 +124,6 @@ class _PerDateWidgetState extends State<PerDateWidget> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScanButtonPressed(BuildContext context) async {
|
|
||||||
var client = FoodFactLookupClient();
|
|
||||||
|
|
||||||
var scanResult = await BarcodeScanner.scan();
|
|
||||||
|
|
||||||
if (scanResult.type == ResultType.Cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
if (scanResult.type == ResultType.Error) {
|
|
||||||
showNewSnackbarWith(context, "Fehler beim Scannen des Barcodes.");
|
|
||||||
}
|
|
||||||
var response = await client.retrieveFoodInfo(scanResult.rawContent);
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
|
|
||||||
showNewSnackbarWith(context, "Barcode konnte nicht gefunden werden.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status == FoodFactResponseStatus.foodFactServerNotReachable) {
|
|
||||||
showNewSnackbarWith(
|
|
||||||
context, "OpenFoodFacts-Server konnte nicht erreicht werden.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.read<EnterFoodController>().set(
|
|
||||||
response.food!.name,
|
|
||||||
response.food!.kcalPer100g.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorSnackbar extends SnackBar {
|
class ErrorSnackbar extends SnackBar {
|
||||||
|
@ -8,7 +8,7 @@ import 'package:universal_platform/universal_platform.dart';
|
|||||||
class FoodStorage {
|
class FoodStorage {
|
||||||
static late FoodStorage _instance;
|
static late FoodStorage _instance;
|
||||||
late String path;
|
late String path;
|
||||||
final Map<String, double> _foodLookupDatabase = {};
|
final Map<String, int> _foodLookupDatabase = {};
|
||||||
|
|
||||||
FoodStorage._create();
|
FoodStorage._create();
|
||||||
|
|
||||||
@ -31,8 +31,8 @@ class FoodStorage {
|
|||||||
|
|
||||||
static FoodStorage getInstance() => _instance;
|
static FoodStorage getInstance() => _instance;
|
||||||
|
|
||||||
Future<List<FoodEntry>> getEntriesForDate(DateTime date) async {
|
Future<List<FoodEntryState>> getEntriesForDate(DateTime date) async {
|
||||||
List<FoodEntry> entries = [];
|
List<FoodEntryState> entries = [];
|
||||||
var filePath = '$path/${date.year}/${date.month}/${date.day}';
|
var filePath = '$path/${date.year}/${date.month}/${date.day}';
|
||||||
|
|
||||||
var file = File(filePath);
|
var file = File(filePath);
|
||||||
@ -44,10 +44,27 @@ class FoodStorage {
|
|||||||
|
|
||||||
for (var line in lines) {
|
for (var line in lines) {
|
||||||
var fields = line.splitWithIgnore(',', ignoreIn: '"');
|
var fields = line.splitWithIgnore(',', ignoreIn: '"');
|
||||||
var entry = FoodEntry(
|
int mass = 0;
|
||||||
name: fields[1].replaceAll('"', ""),
|
int kcalPerMass = 0;
|
||||||
mass: double.parse(fields[2]),
|
|
||||||
kcalPerMass: double.parse(fields[3]));
|
try {
|
||||||
|
mass = int.parse(fields[2]);
|
||||||
|
} catch (e) {
|
||||||
|
mass = double.parse(fields[2]).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
kcalPerMass = int.parse(fields[3]);
|
||||||
|
} catch (e) {
|
||||||
|
kcalPerMass = double.parse(fields[3]).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = FoodEntryState(
|
||||||
|
name: fields[1].replaceAll('"', ""),
|
||||||
|
mass: mass,
|
||||||
|
kcalPerMass: kcalPerMass,
|
||||||
|
waitingForNetwork: false,
|
||||||
|
);
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +72,7 @@ class FoodStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> writeEntriesForDate(
|
Future<void> writeEntriesForDate(
|
||||||
DateTime date, List<FoodEntry> foodEntries) async {
|
DateTime date, List<FoodEntryState> foodEntries) async {
|
||||||
var filePath = '$path/${date.year}/${date.month}/${date.day}';
|
var filePath = '$path/${date.year}/${date.month}/${date.day}';
|
||||||
var file = File(filePath);
|
var file = File(filePath);
|
||||||
|
|
||||||
@ -157,12 +174,12 @@ class FoodStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addFoodEntryToLookupDatabase(FoodEntry entry) {
|
void addFoodEntryToLookupDatabase(FoodEntryState entry) {
|
||||||
_foodLookupDatabase[entry.name] = entry.kcalPerMass;
|
_foodLookupDatabase[entry.name] = entry.kcalPerMass;
|
||||||
log("Added entry: ${entry.name}/${entry.kcalPerMass}");
|
log("Added entry: ${entry.name}/${entry.kcalPerMass}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, double> get getFoodEntryLookupDatabase => _foodLookupDatabase;
|
Map<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SplitWithIgnore on String {
|
extension SplitWithIgnore on String {
|
||||||
|
215
lib/storage/storage.dart.orig
Normal file
215
lib/storage/storage.dart.orig
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
|
|
||||||
|
class FoodStorage {
|
||||||
|
static late FoodStorage _instance;
|
||||||
|
late String path;
|
||||||
|
final Map<String, int> _foodLookupDatabase = {};
|
||||||
|
|
||||||
|
FoodStorage._create();
|
||||||
|
|
||||||
|
static Future<FoodStorage> create() async {
|
||||||
|
var storage = FoodStorage._create();
|
||||||
|
|
||||||
|
Directory dir = Directory('');
|
||||||
|
|
||||||
|
if (UniversalPlatform.isDesktop) {
|
||||||
|
dir = await getApplicationCacheDirectory();
|
||||||
|
} else if (UniversalPlatform.isAndroid) {
|
||||||
|
dir = await getApplicationDocumentsDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.path = dir.path;
|
||||||
|
_instance = storage;
|
||||||
|
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FoodStorage getInstance() => _instance;
|
||||||
|
|
||||||
|
Future<List<FoodEntryState>> getEntriesForDate(DateTime date) async {
|
||||||
|
List<FoodEntryState> entries = [];
|
||||||
|
var filePath = '$path/${date.year}/${date.month}/${date.day}';
|
||||||
|
|
||||||
|
var file = File(filePath);
|
||||||
|
var exists = await file.exists();
|
||||||
|
|
||||||
|
if (!exists) return [];
|
||||||
|
|
||||||
|
var lines = await file.readAsLines();
|
||||||
|
|
||||||
|
for (var line in lines) {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
var fields = line.splitWithIgnore(',', ignoreIn: '"');
|
||||||
|
var entry = FoodEntry(
|
||||||
|
name: fields[1].replaceAll('"', ""),
|
||||||
|
mass: double.parse(fields[2]),
|
||||||
|
kcalPerMass: double.parse(fields[3]));
|
||||||
|
=======
|
||||||
|
var fields = line.split(',');
|
||||||
|
var entry = FoodEntryState(
|
||||||
|
name: fields[1],
|
||||||
|
mass: int.parse(fields[2]),
|
||||||
|
kcalPerMass: int.parse(fields[3]),
|
||||||
|
waitingForNetwork: false,
|
||||||
|
);
|
||||||
|
>>>>>>> 7921f09 (wip)
|
||||||
|
entries.add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeEntriesForDate(
|
||||||
|
DateTime date, List<FoodEntryState> foodEntries) async {
|
||||||
|
var filePath = '$path/${date.year}/${date.month}/${date.day}';
|
||||||
|
var file = File(filePath);
|
||||||
|
|
||||||
|
var exists = await file.exists();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
await file.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String fullString = '';
|
||||||
|
for (var entry in foodEntries) {
|
||||||
|
fullString += '${entry.toString()}\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.writeAsString(fullString);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateLimit(double limit) async {
|
||||||
|
var filePath = '$path/limit';
|
||||||
|
var file = File(filePath);
|
||||||
|
|
||||||
|
var exists = await file.exists();
|
||||||
|
if (!exists) {
|
||||||
|
await file.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.writeAsString(limit.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<double> readLimit() async {
|
||||||
|
var filePath = '$path/limit';
|
||||||
|
var file = File(filePath);
|
||||||
|
var exists = await file.exists();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
var line = await file.readAsLines();
|
||||||
|
|
||||||
|
double limit;
|
||||||
|
try {
|
||||||
|
limit = double.parse(line[0]);
|
||||||
|
} catch (e) {
|
||||||
|
limit = 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> readBrightness() async {
|
||||||
|
var filePath = '$path/brightness';
|
||||||
|
var file = File(filePath);
|
||||||
|
var exists = await file.exists();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
var line = await file.readAsLines();
|
||||||
|
|
||||||
|
if (line.isEmpty || (line[0] != 'dark' && line[0] != 'light')) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
return line[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeBrightness(String brightness) async {
|
||||||
|
var filePath = '$path/brightness';
|
||||||
|
var file = File(filePath);
|
||||||
|
var exists = await file.exists();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
file.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.writeAsString(brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> buildFoodLookupDatabase() async {
|
||||||
|
// get a list of dates of the last 365 days
|
||||||
|
var dates = List<DateTime>.generate(365, (idx) {
|
||||||
|
var durationToPast = Duration(days: idx);
|
||||||
|
return DateTime.now().subtract(durationToPast);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var date in dates.reversed) {
|
||||||
|
addFoodEntryToLookupDatabaseFor(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addFoodEntryToLookupDatabaseFor(DateTime date) async {
|
||||||
|
var entriesForDate = await getEntriesForDate(date);
|
||||||
|
|
||||||
|
for (var entry in entriesForDate) {
|
||||||
|
_foodLookupDatabase[entry.name] = entry.kcalPerMass;
|
||||||
|
log("Added entry: ${entry.name}/${entry.kcalPerMass}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addFoodEntryToLookupDatabase(FoodEntryState entry) {
|
||||||
|
_foodLookupDatabase[entry.name] = entry.kcalPerMass;
|
||||||
|
log("Added entry: ${entry.name}/${entry.kcalPerMass}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, int> get getFoodEntryLookupDatabase => _foodLookupDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SplitWithIgnore on String {
|
||||||
|
List<String> splitWithIgnore(String delimiter, {String? ignoreIn}) {
|
||||||
|
List<String> parts = [];
|
||||||
|
|
||||||
|
if (ignoreIn == null) {
|
||||||
|
return split(delimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = -1;
|
||||||
|
int indexCharAfterDelimiter = 0;
|
||||||
|
bool inIgnore = false;
|
||||||
|
for (var rune in runes) {
|
||||||
|
var char = String.fromCharCode(rune);
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
|
||||||
|
if (char == ignoreIn) {
|
||||||
|
inIgnore = !inIgnore;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inIgnore) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char == delimiter) {
|
||||||
|
parts.add(substring(indexCharAfterDelimiter, index));
|
||||||
|
indexCharAfterDelimiter = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index + 1 == length) {
|
||||||
|
parts.add(substring(indexCharAfterDelimiter, length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class EnterFoodController extends ChangeNotifier {
|
|
||||||
String name = "";
|
|
||||||
String kcalPer100g = "";
|
|
||||||
|
|
||||||
void set(String newName, String newKcal) {
|
|
||||||
name = newName;
|
|
||||||
kcalPer100g = newKcal;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ import 'package:calorimeter/food_entry/food_entry_bloc.dart';
|
|||||||
import 'package:calorimeter/utils/settings_bloc.dart';
|
import 'package:calorimeter/utils/settings_bloc.dart';
|
||||||
|
|
||||||
class SumWidget extends StatelessWidget {
|
class SumWidget extends StatelessWidget {
|
||||||
final List<FoodEntry> foodEntries;
|
final List<FoodEntryState> foodEntries;
|
||||||
const SumWidget({required this.foodEntries, super.key});
|
const SumWidget({required this.foodEntries, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
Reference in New Issue
Block a user