From 4f65425e660e626d6b041d6e761e3308d361878c Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 24 Sep 2024 01:47:55 +0200 Subject: [PATCH] wip --- lib/food_entry/enter_food_widget.dart | 27 ++-- lib/food_entry/food_entry_bloc.dart | 72 ++++++--- lib/food_entry/food_entry_widget.dart | 18 ++- lib/food_scan/food_fact_lookup.dart | 19 ++- lib/perdate/entry_list.dart | 3 +- lib/perdate/perdate_widget.dart | 8 +- lib/storage/storage.dart | 18 ++- lib/storage/storage.dart.orig | 215 ++++++++++++++++++++++++++ lib/utils/sum_widget.dart | 2 +- 9 files changed, 325 insertions(+), 57 deletions(-) create mode 100644 lib/storage/storage.dart.orig diff --git a/lib/food_entry/enter_food_widget.dart b/lib/food_entry/enter_food_widget.dart index bbfe0d3..9b5e820 100644 --- a/lib/food_entry/enter_food_widget.dart +++ b/lib/food_entry/enter_food_widget.dart @@ -6,7 +6,7 @@ import 'package:calorimeter/utils/row_with_spacers_widget.dart'; import 'package:provider/provider.dart'; 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}); @@ -18,7 +18,7 @@ class _EnterFoodWidgetState extends State { late TextEditingController nameController; late TextEditingController massController; late TextEditingController kcalPerMassController; - late Map suggestions; + late Map suggestions; @override void initState() { @@ -67,8 +67,7 @@ class _EnterFoodWidgetState extends State { ); }, onSelected: (selectedFood) { - double kcalPerMassForSelectedFood = - suggestions[selectedFood]!; + int kcalPerMassForSelectedFood = suggestions[selectedFood]!; context .read() .set(selectedFood, kcalPerMassForSelectedFood.toString()); @@ -109,11 +108,11 @@ class _EnterFoodWidgetState extends State { } void onSubmitAction() { - double massAsNumber = 0.0; - double kcalPerMassAsNumber = 0.0; + int massAsNumber = 0; + int kcalPerMassAsNumber = 0; try { - massAsNumber = double.parse(massController.text.replaceAll(",", ".")); + massAsNumber = int.parse(massController.text.replaceAll(",", ".")); } catch (e) { var snackbar = const SnackBar(content: Text("Menge muss eine Zahl sein")); ScaffoldMessenger.of(context).clearSnackBars(); @@ -123,19 +122,21 @@ class _EnterFoodWidgetState extends State { try { kcalPerMassAsNumber = - double.parse(kcalPerMassController.text.replaceAll(",", ".")); + int.parse(kcalPerMassController.text.replaceAll(",", ".")); } catch (e) { var snackbar = const SnackBar(content: Text("'kcal pro 100g' muss eine Zahl sein")); - ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).removeCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar(snackbar); return; } - var entry = FoodEntry( - name: nameController.text, - mass: massAsNumber, - kcalPerMass: kcalPerMassAsNumber); + var entry = FoodEntryState( + name: nameController.text, + mass: massAsNumber, + kcalPerMass: kcalPerMassAsNumber, + waitingForNetwork: false, + ); widget.onAdd(context, entry); context.read().set("", ""); diff --git a/lib/food_entry/food_entry_bloc.dart b/lib/food_entry/food_entry_bloc.dart index df2808d..207db42 100644 --- a/lib/food_entry/food_entry_bloc.dart +++ b/lib/food_entry/food_entry_bloc.dart @@ -1,10 +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:calorimeter/storage/storage.dart'; import 'package:uuid/uuid.dart'; -class FoodEntryBloc extends Bloc { - final FoodEntryState initialState; +class FoodEntryBloc extends Bloc { + final PageState initialState; final FoodStorage storage; final DateTime forDate; @@ -19,8 +20,8 @@ class FoodEntryBloc extends Bloc { } void handleFoodEntryEvent( - FoodEntryEvent event, Emitter emit) async { - FoodEntryState newState = FoodEntryState.from(state); + FoodEntryEvent event, Emitter emit) async { + PageState newState = PageState.from(state); newState.addEntry(event.entry); await storage.writeEntriesForDate(forDate, newState.foodEntries); @@ -30,22 +31,49 @@ class FoodEntryBloc extends Bloc { } void handleDeleteFoodEvent( - FoodDeletionEvent event, Emitter emit) async { + FoodDeletionEvent event, Emitter emit) async { state.foodEntries.removeWhere((entry) => entry.id == event.entryID); await storage.writeEntriesForDate(forDate, state.foodEntries); - emit(FoodEntryState.from(state)); + emit(PageState.from(state)); } void handleBarcodeScannedEvent( - BarcodeScanned event, Emitter emit) async {} + BarcodeScanned event, Emitter emit) async { + var client = FoodFactLookupClient(); + var scanResult = await event.scanResultFuture; + + if (scanResult.type == ResultType.Cancelled) { + return; + } + var responseFuture = client.retrieveFoodInfo(scanResult.rawContent); + + List 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); + + 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 FoodEntryEvent extends FoodEvent { - final FoodEntry entry; + final FoodEntryState entry; FoodEntryEvent({required this.entry}); } @@ -63,36 +91,36 @@ class BarcodeScanned extends FoodEvent { } /// This is the state for one date/page -class FoodEntryState { - final List foodEntries; +class PageState { + final List foodEntries; - FoodEntryState({required this.foodEntries}); + PageState({required this.foodEntries}); - factory FoodEntryState.init() { - return FoodEntryState(foodEntries: []); + factory PageState.init() { + return PageState(foodEntries: []); } - static from(FoodEntryState state) { - List newList = List.from(state.foodEntries); - - return FoodEntryState(foodEntries: newList); + static from(PageState state) { + return PageState(foodEntries: state.foodEntries); } - void addEntry(FoodEntry entry) { + void addEntry(FoodEntryState entry) { foodEntries.add(entry); } } -class FoodEntry { +class FoodEntryState { final String name; - final double mass; - final double kcalPerMass; + final int mass; + final int kcalPerMass; final String id; + final bool waitingForNetwork; - FoodEntry({ + FoodEntryState({ required this.name, required this.mass, required this.kcalPerMass, + required this.waitingForNetwork, }) : id = const Uuid().v1(); @override diff --git a/lib/food_entry/food_entry_widget.dart b/lib/food_entry/food_entry_widget.dart index 49760ee..6f52be6 100644 --- a/lib/food_entry/food_entry_widget.dart +++ b/lib/food_entry/food_entry_widget.dart @@ -3,7 +3,7 @@ import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/utils/row_with_spacers_widget.dart'; class FoodEntryWidget extends StatefulWidget { - final FoodEntry entry; + final FoodEntryState entry; final Function(BuildContext context, String id) onDelete; const FoodEntryWidget( @@ -38,11 +38,17 @@ class _FoodEntryWidgetState extends State { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: RowWidget( - Text(widget.entry.name), - Text(widget.entry.mass.ceil().toString(), - textAlign: TextAlign.end), - Text(widget.entry.kcalPerMass.ceil().toString(), - textAlign: TextAlign.end), + widget.entry.waitingForNetwork + ? const Center(child: CircularProgressIndicator()) + : Text(widget.entry.name), + widget.entry.waitingForNetwork + ? 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( diff --git a/lib/food_scan/food_fact_lookup.dart b/lib/food_scan/food_fact_lookup.dart index 2d1667f..4ca736a 100644 --- a/lib/food_scan/food_fact_lookup.dart +++ b/lib/food_scan/food_fact_lookup.dart @@ -45,13 +45,28 @@ class FoodFactLookupClient { class FoodFactModel { final String name; 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 json) { + String quantityString = json['product']['product_quantity'] ?? "0"; + int quantity; + + try { + quantity = int.parse(quantityString); + } catch (e) { + quantity = 0; + } + return FoodFactModel( name: json['product']['product_name'], - kcalPer100g: json['product']['nutriments']['energy-kcal_100g']); + kcalPer100g: json['product']['nutriments']['energy-kcal_100g'], + mass: quantity); } } diff --git a/lib/perdate/entry_list.dart b/lib/perdate/entry_list.dart index 756f9fd..1017749 100644 --- a/lib/perdate/entry_list.dart +++ b/lib/perdate/entry_list.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class FoodEntryList extends StatelessWidget { - final List entries; + final List entries; const FoodEntryList({ required this.entries, @@ -17,6 +17,7 @@ class FoodEntryList extends StatelessWidget { return ListView.builder( itemCount: entries.length + 1, itemBuilder: (BuildContext itemBuilderContext, int listIndex) { + //last item in list is the widget to enter food if (listIndex == entries.length) { return Column( children: [ diff --git a/lib/perdate/perdate_widget.dart b/lib/perdate/perdate_widget.dart index 0a9d1af..7bdc396 100644 --- a/lib/perdate/perdate_widget.dart +++ b/lib/perdate/perdate_widget.dart @@ -24,8 +24,8 @@ class PerDateWidget extends StatefulWidget { class _PerDateWidgetState extends State { late FoodStorage storage; - late Future> entriesFuture; - List entries = []; + late Future> entriesFuture; + List entries = []; @override void initState() { @@ -50,13 +50,13 @@ class _PerDateWidgetState extends State { create: (context) => EnterFoodController()), BlocProvider( create: (context) => FoodEntryBloc( - initialState: FoodEntryState(foodEntries: entries), + initialState: PageState(foodEntries: entries), storage: storage, forDate: widget.date, ), ) ], - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index ac4598b..29321e7 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -8,7 +8,7 @@ import 'package:universal_platform/universal_platform.dart'; class FoodStorage { static late FoodStorage _instance; late String path; - final Map _foodLookupDatabase = {}; + final Map _foodLookupDatabase = {}; FoodStorage._create(); @@ -31,8 +31,8 @@ class FoodStorage { static FoodStorage getInstance() => _instance; - Future> getEntriesForDate(DateTime date) async { - List entries = []; + Future> getEntriesForDate(DateTime date) async { + List entries = []; var filePath = '$path/${date.year}/${date.month}/${date.day}'; var file = File(filePath); @@ -44,10 +44,12 @@ class FoodStorage { for (var line in lines) { var fields = line.splitWithIgnore(',', ignoreIn: '"'); - var entry = FoodEntry( - name: fields[1].replaceAll('"', ""), - mass: double.parse(fields[2]), - kcalPerMass: double.parse(fields[3])); + var entry = FoodEntryState( + name: fields[1].replaceAll('"', ""), + mass: int.parse(fields[2]), + kcalPerMass: int.parse(fields[3]), + waitingForNetwork: false, + ); entries.add(entry); } @@ -55,7 +57,7 @@ class FoodStorage { } Future writeEntriesForDate( - DateTime date, List foodEntries) async { + DateTime date, List foodEntries) async { var filePath = '$path/${date.year}/${date.month}/${date.day}'; var file = File(filePath); diff --git a/lib/storage/storage.dart.orig b/lib/storage/storage.dart.orig new file mode 100644 index 0000000..55ea163 --- /dev/null +++ b/lib/storage/storage.dart.orig @@ -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 _foodLookupDatabase = {}; + + FoodStorage._create(); + + static Future 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> getEntriesForDate(DateTime date) async { + List 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 writeEntriesForDate( + DateTime date, List 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 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 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 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 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 buildFoodLookupDatabase() async { + // get a list of dates of the last 365 days + var dates = List.generate(365, (idx) { + var durationToPast = Duration(days: idx); + return DateTime.now().subtract(durationToPast); + }); + + for (var date in dates.reversed) { + addFoodEntryToLookupDatabaseFor(date); + } + } + + Future 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 get getFoodEntryLookupDatabase => _foodLookupDatabase; +} + +extension SplitWithIgnore on String { + List splitWithIgnore(String delimiter, {String? ignoreIn}) { + List 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; + } +} diff --git a/lib/utils/sum_widget.dart b/lib/utils/sum_widget.dart index 8184f21..5334030 100644 --- a/lib/utils/sum_widget.dart +++ b/lib/utils/sum_widget.dart @@ -4,7 +4,7 @@ import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart'; class SumWidget extends StatelessWidget { - final List foodEntries; + final List foodEntries; const SumWidget({required this.foodEntries, super.key}); @override