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:
Marco 2024-09-09 22:41:48 +02:00
parent e75a0765b4
commit a7a7f44050
10 changed files with 477 additions and 223 deletions

View File

@ -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 = "";
});
} }
} }

View File

@ -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

View File

@ -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(

View File

@ -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);
} }
} }

View File

@ -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: [

View File

@ -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 {

View File

@ -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 {

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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