From b83f547f6b608d2fded81c606f7888d9b6ac73a7 Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 4 Sep 2024 22:47:32 +0200 Subject: [PATCH] Implement food entry lookup on entering a food name. Now, an on-the-fly food lookup is created from existing entries on startup. Those entries are used to make suggestions when the user is typing to enter new food entries. --- lib/enter_food_widget.dart | 46 +++++++++++++++++++++---- lib/food_entry_bloc.dart | 8 +++-- lib/food_entry_widget.dart | 37 ++++++++++++-------- lib/main.dart | 70 ++++++++++++++++++++------------------ lib/perdate_widget.dart | 27 +++++++++------ lib/settings_bloc.dart | 2 +- lib/storage/storage.dart | 43 +++++++++++++++++++---- lib/theme_bloc.dart | 2 +- pubspec.lock | 48 +++++++++++--------------- 9 files changed, 179 insertions(+), 104 deletions(-) diff --git a/lib/enter_food_widget.dart b/lib/enter_food_widget.dart index 56f91a2..0be078b 100644 --- a/lib/enter_food_widget.dart +++ b/lib/enter_food_widget.dart @@ -1,3 +1,6 @@ +import 'dart:developer'; + +import 'package:calodiary/storage/storage.dart'; import 'package:flutter/material.dart'; import 'package:calodiary/food_entry_bloc.dart'; import 'package:calodiary/row_with_spacers_widget.dart'; @@ -16,22 +19,53 @@ class _EnterFoodWidgetState extends State { TextEditingController nameController = TextEditingController(); TextEditingController massController = TextEditingController(); TextEditingController kcalPerMassController = TextEditingController(); + Map suggestions = {}; + + @override + void initState() { + suggestions = FoodStorage.getInstance().getFoodEntryLookupDatabase; + super.initState(); + } @override Widget build(BuildContext context) { - var nameWidget = TextField( - decoration: const InputDecoration(hintText: "Name"), - controller: nameController, - ); + var nameWidget = Autocomplete( + 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.empty(); + } + + return suggestions.keys.where( + (name) { + return name + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }, + ); + }, + onSelected: (selectedFood) { + double kcalPerMassForSelectedFood = suggestions[selectedFood]!; + setState(() { + kcalPerMassController.text = kcalPerMassForSelectedFood.toString(); + }); + }); var massWidget = TextField( - decoration: const InputDecoration(hintText: "Menge"), + decoration: const InputDecoration(label: Text("Menge")), keyboardType: TextInputType.number, controller: massController, ); var kcalPerMassWidget = TextField( - decoration: const InputDecoration(hintText: "kcal pro 100g"), + decoration: const InputDecoration(label: Text("kcal pro")), keyboardType: TextInputType.number, controller: kcalPerMassController); diff --git a/lib/food_entry_bloc.dart b/lib/food_entry_bloc.dart index ffc4741..d714a7e 100644 --- a/lib/food_entry_bloc.dart +++ b/lib/food_entry_bloc.dart @@ -4,7 +4,7 @@ import 'package:uuid/uuid.dart'; class FoodEntryBloc extends Bloc { final FoodEntryState initialState; - final AppStorage storage; + final FoodStorage storage; final DateTime forDate; FoodEntryBloc( @@ -12,16 +12,18 @@ class FoodEntryBloc extends Bloc { required this.forDate, required this.storage}) : super(initialState) { - on(addFoodEntry); + on(handleFoodEntryEvent); on(deleteFood); on(updateEntries); } - void addFoodEntry(FoodEntryEvent event, Emitter emit) async { + void handleFoodEntryEvent( + FoodEntryEvent event, Emitter emit) async { FoodEntryState newState = FoodEntryState.from(state); newState.addEntry(event.entry); await storage.writeEntriesForDate(forDate, newState.foodEntries); + storage.addFoodEntryToLookupDatabase(event.entry); emit(newState); } diff --git a/lib/food_entry_widget.dart b/lib/food_entry_widget.dart index cb06921..cd6d1b0 100644 --- a/lib/food_entry_widget.dart +++ b/lib/food_entry_widget.dart @@ -4,27 +4,34 @@ import 'package:calodiary/row_with_spacers_widget.dart'; class FoodEntryWidget extends StatelessWidget { final FoodEntry entry; - final Function(BuildContext context) onDelete; + final Function(BuildContext context, String id) onDelete; const FoodEntryWidget( {super.key, required this.entry, required this.onDelete}); @override Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: RowWidget( - Text(entry.name), - Text(entry.mass.ceil().toString()), - Text(entry.kcalPerMass.ceil().toString()), - Text((entry.mass * entry.kcalPerMass / 100).ceil().toString()), - IconButton( - style: IconButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - onDelete(context); - }, - icon: const Icon(Icons.delete_forever_rounded)), + return Dismissible( + key: ValueKey(entry.id), + onDismissed: (direction) { + onDelete(context, entry.id); + }, + child: Card( + elevation: 5.0, + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: RowWidget( + Text(entry.name), + Text(entry.mass.ceil().toString()), + Text(entry.kcalPerMass.ceil().toString()), + Text((entry.mass * entry.kcalPerMass / 100).ceil().toString()), + IconButton( + style: IconButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + onDelete(context, entry.id); + }, + icon: const Icon(Icons.delete_forever_rounded)), + ), ), ), ); diff --git a/lib/main.dart b/lib/main.dart index bcf5b74..39b3d05 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,7 +10,8 @@ import 'package:go_router/go_router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - var storage = await AppStorage.create(); + var storage = await FoodStorage.create(); + await storage.buildFoodLookupDatabase(); var kcalLimit = await storage.readLimit(); var brightness = await storage.readBrightness(); @@ -24,7 +25,7 @@ void main() async { } class MainApp extends StatelessWidget { - final AppStorage storage; + final FoodStorage storage; final double kcalLimit; final String brightness; @@ -36,38 +37,41 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => SettingsDataBloc( - SettingsState(kcalLimit: kcalLimit), - storage: storage), - ), - BlocProvider( - create: (context) => ThemeDataBloc(ThemeState(brightness: brightness), - storage: storage), - ), - ], - child: BlocBuilder( - builder: (context, state) { - var switchToTheme = ThemeData.light(); - if (state.brightness == 'dark') { - switchToTheme = ThemeData.dark(); - } + return SafeArea( + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SettingsDataBloc( + SettingsState(kcalLimit: kcalLimit), + storage: storage), + ), + BlocProvider( + create: (context) => ThemeDataBloc( + ThemeState(brightness: brightness), + storage: storage), + ), + ], + child: BlocBuilder( + builder: (context, state) { + var switchToTheme = ThemeData.light(); + if (state.brightness == 'dark') { + switchToTheme = ThemeData.dark(); + } - return MaterialApp.router( - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('de'), - ], - theme: switchToTheme, - routerConfig: router, - ); - }, + return MaterialApp.router( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('de'), + ], + theme: switchToTheme, + routerConfig: router, + ); + }, + ), ), ); } diff --git a/lib/perdate_widget.dart b/lib/perdate_widget.dart index 46f33b0..ef7fa3c 100644 --- a/lib/perdate_widget.dart +++ b/lib/perdate_widget.dart @@ -19,7 +19,7 @@ class PerDateWidget extends StatefulWidget { } class _PerDateWidgetState extends State { - late AppStorage storage; + late FoodStorage storage; late Future> entriesFuture; late List entries; @@ -27,7 +27,7 @@ class _PerDateWidgetState extends State { void initState() { super.initState(); - storage = AppStorage.getInstance(); + storage = FoodStorage.getInstance(); entriesFuture = storage.getEntriesForDate(widget.date); entriesFuture.then((val) { entries = val; @@ -93,15 +93,20 @@ class _PerDateWidgetState extends State { ); } - return FoodEntryWidget( - entry: state.foodEntries[index], - onDelete: (callbackContext) { - callbackContext - .read() - .add(FoodDeletionEvent( - entryID: state.foodEntries[index].id, - )); - }, + return Column( + children: [ + FoodEntryWidget( + entry: state.foodEntries[index], + onDelete: (callbackContext, id) { + callbackContext + .read() + .add(FoodDeletionEvent( + entryID: id, + )); + }, + ), + const Divider(), + ], ); }, ); diff --git a/lib/settings_bloc.dart b/lib/settings_bloc.dart index 6c76f04..ac88d5c 100644 --- a/lib/settings_bloc.dart +++ b/lib/settings_bloc.dart @@ -2,7 +2,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:calodiary/storage/storage.dart'; class SettingsDataBloc extends Bloc { - final AppStorage storage; + final FoodStorage storage; SettingsDataBloc(super.initialState, {required this.storage}) { on(persistDailyLimit); diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 688897f..cd9e47f 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -1,16 +1,19 @@ +import 'dart:developer'; import 'dart:io'; import 'package:calodiary/food_entry_bloc.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_platform/universal_platform.dart'; -class AppStorage { - static late AppStorage _instance; +class FoodStorage { + static late FoodStorage _instance; late String path; - AppStorage._create(); + late Map _foodLookupDatabase = {}; - static Future create() async { - var storage = AppStorage._create(); + FoodStorage._create(); + + static Future create() async { + var storage = FoodStorage._create(); Directory dir = Directory(''); @@ -26,7 +29,7 @@ class AppStorage { return _instance; } - static AppStorage getInstance() => _instance; + static FoodStorage getInstance() => _instance; Future> getEntriesForDate(DateTime date) async { List entries = []; @@ -132,4 +135,32 @@ class AppStorage { await file.writeAsString(brightness); } + + Future buildFoodLookupDatabase() async { + // get a list of dates of the last 365 days + var dates = List.generate(365, (idx) { + var pastDay = Duration(days: idx); + return DateTime.now().subtract(pastDay); + }); + + for (var date in dates) { + 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(FoodEntry entry) { + _foodLookupDatabase[entry.name] = entry.kcalPerMass; + log("Added entry: ${entry.name}/${entry.kcalPerMass}"); + } + + Map get getFoodEntryLookupDatabase => _foodLookupDatabase; } diff --git a/lib/theme_bloc.dart b/lib/theme_bloc.dart index 6981490..8dc83fe 100644 --- a/lib/theme_bloc.dart +++ b/lib/theme_bloc.dart @@ -2,7 +2,7 @@ import 'package:calodiary/storage/storage.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ThemeDataBloc extends Bloc { - final AppStorage storage; + final FoodStorage storage; ThemeDataBloc(super.initialState, {required this.storage}) { on(switchTheme); diff --git a/pubspec.lock b/pubspec.lock index 2b68069..5c9312d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" fake_async: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" fixnum: dependency: transitive description: @@ -121,10 +121,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554 + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" url: "https://pub.dev" source: hosted - version: "14.2.0" + version: "14.2.7" intl: dependency: "direct main" description: @@ -193,10 +193,10 @@ packages: dependency: transitive description: name: meta - sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.14.0" + version: "1.15.0" nested: dependency: transitive description: @@ -217,18 +217,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -257,10 +257,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: @@ -350,10 +350,10 @@ packages: dependency: transitive description: name: test_api - sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" typed_data: dependency: transitive description: @@ -374,10 +374,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" vector_math: dependency: transitive description: @@ -390,18 +390,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.2" - win32: - dependency: transitive - description: - name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 - url: "https://pub.dev" - source: hosted - version: "5.5.1" + version: "14.2.5" xdg_directories: dependency: transitive description: