Implement food entry lookup on entering a food name. #1

Merged
marco merged 1 commits from food-suggestion into master 2024-09-05 11:08:51 +00:00
9 changed files with 179 additions and 104 deletions

View File

@ -1,3 +1,6 @@
import 'dart:developer';
import 'package:calodiary/storage/storage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:calodiary/food_entry_bloc.dart'; import 'package:calodiary/food_entry_bloc.dart';
import 'package:calodiary/row_with_spacers_widget.dart'; import 'package:calodiary/row_with_spacers_widget.dart';
@ -16,22 +19,53 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
TextEditingController nameController = TextEditingController(); TextEditingController nameController = TextEditingController();
TextEditingController massController = TextEditingController(); TextEditingController massController = TextEditingController();
TextEditingController kcalPerMassController = TextEditingController(); TextEditingController kcalPerMassController = TextEditingController();
Map<String, double> suggestions = {};
@override
void initState() {
suggestions = FoodStorage.getInstance().getFoodEntryLookupDatabase;
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var nameWidget = TextField( var nameWidget = Autocomplete<String>(
decoration: const InputDecoration(hintText: "Name"), optionsViewOpenDirection: OptionsViewOpenDirection.down,
controller: nameController, 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 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( var massWidget = TextField(
decoration: const InputDecoration(hintText: "Menge"), decoration: const InputDecoration(label: Text("Menge")),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
controller: massController, controller: massController,
); );
var kcalPerMassWidget = TextField( var kcalPerMassWidget = TextField(
decoration: const InputDecoration(hintText: "kcal pro 100g"), decoration: const InputDecoration(label: Text("kcal pro")),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
controller: kcalPerMassController); controller: kcalPerMassController);

View File

@ -4,7 +4,7 @@ import 'package:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> { class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> {
final FoodEntryState initialState; final FoodEntryState initialState;
final AppStorage storage; final FoodStorage storage;
final DateTime forDate; final DateTime forDate;
FoodEntryBloc( FoodEntryBloc(
@ -12,16 +12,18 @@ class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> {
required this.forDate, required this.forDate,
required this.storage}) required this.storage})
: super(initialState) { : super(initialState) {
on<FoodEntryEvent>(addFoodEntry); on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodDeletionEvent>(deleteFood); on<FoodDeletionEvent>(deleteFood);
on<PageChangedEvent>(updateEntries); on<PageChangedEvent>(updateEntries);
} }
void addFoodEntry(FoodEntryEvent event, Emitter<FoodEntryState> emit) async { void handleFoodEntryEvent(
FoodEntryEvent event, Emitter<FoodEntryState> emit) async {
FoodEntryState newState = FoodEntryState.from(state); FoodEntryState newState = FoodEntryState.from(state);
newState.addEntry(event.entry); newState.addEntry(event.entry);
await storage.writeEntriesForDate(forDate, newState.foodEntries); await storage.writeEntriesForDate(forDate, newState.foodEntries);
storage.addFoodEntryToLookupDatabase(event.entry);
emit(newState); emit(newState);
} }

View File

@ -4,27 +4,34 @@ import 'package:calodiary/row_with_spacers_widget.dart';
class FoodEntryWidget extends StatelessWidget { class FoodEntryWidget extends StatelessWidget {
final FoodEntry entry; final FoodEntry entry;
final Function(BuildContext context) onDelete; final Function(BuildContext context, String id) onDelete;
const FoodEntryWidget( const FoodEntryWidget(
{super.key, required this.entry, required this.onDelete}); {super.key, required this.entry, required this.onDelete});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Dismissible(
child: Padding( key: ValueKey(entry.id),
padding: const EdgeInsets.only(left: 4.0), onDismissed: (direction) {
child: RowWidget( onDelete(context, entry.id);
Text(entry.name), },
Text(entry.mass.ceil().toString()), child: Card(
Text(entry.kcalPerMass.ceil().toString()), elevation: 5.0,
Text((entry.mass * entry.kcalPerMass / 100).ceil().toString()), child: Padding(
IconButton( padding: const EdgeInsets.only(left: 4.0),
style: IconButton.styleFrom(padding: EdgeInsets.zero), child: RowWidget(
onPressed: () { Text(entry.name),
onDelete(context); Text(entry.mass.ceil().toString()),
}, Text(entry.kcalPerMass.ceil().toString()),
icon: const Icon(Icons.delete_forever_rounded)), 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)),
),
), ),
), ),
); );

View File

@ -10,7 +10,8 @@ import 'package:go_router/go_router.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
var storage = await AppStorage.create(); var storage = await FoodStorage.create();
await storage.buildFoodLookupDatabase();
var kcalLimit = await storage.readLimit(); var kcalLimit = await storage.readLimit();
var brightness = await storage.readBrightness(); var brightness = await storage.readBrightness();
@ -24,7 +25,7 @@ void main() async {
} }
class MainApp extends StatelessWidget { class MainApp extends StatelessWidget {
final AppStorage storage; final FoodStorage storage;
final double kcalLimit; final double kcalLimit;
final String brightness; final String brightness;
@ -36,38 +37,41 @@ class MainApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return SafeArea(
providers: [ child: MultiBlocProvider(
BlocProvider( providers: [
create: (context) => SettingsDataBloc( BlocProvider(
SettingsState(kcalLimit: kcalLimit), create: (context) => SettingsDataBloc(
storage: storage), SettingsState(kcalLimit: kcalLimit),
), storage: storage),
BlocProvider( ),
create: (context) => ThemeDataBloc(ThemeState(brightness: brightness), BlocProvider(
storage: storage), create: (context) => ThemeDataBloc(
), ThemeState(brightness: brightness),
], storage: storage),
child: BlocBuilder<ThemeDataBloc, ThemeState>( ),
builder: (context, state) { ],
var switchToTheme = ThemeData.light(); child: BlocBuilder<ThemeDataBloc, ThemeState>(
if (state.brightness == 'dark') { builder: (context, state) {
switchToTheme = ThemeData.dark(); var switchToTheme = ThemeData.light();
} if (state.brightness == 'dark') {
switchToTheme = ThemeData.dark();
}
return MaterialApp.router( return MaterialApp.router(
localizationsDelegates: const [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: const [ supportedLocales: const [
Locale('de'), Locale('de'),
], ],
theme: switchToTheme, theme: switchToTheme,
routerConfig: router, routerConfig: router,
); );
}, },
),
), ),
); );
} }

View File

@ -19,7 +19,7 @@ class PerDateWidget extends StatefulWidget {
} }
class _PerDateWidgetState extends State<PerDateWidget> { class _PerDateWidgetState extends State<PerDateWidget> {
late AppStorage storage; late FoodStorage storage;
late Future<List<FoodEntry>> entriesFuture; late Future<List<FoodEntry>> entriesFuture;
late List<FoodEntry> entries; late List<FoodEntry> entries;
@ -27,7 +27,7 @@ class _PerDateWidgetState extends State<PerDateWidget> {
void initState() { void initState() {
super.initState(); super.initState();
storage = AppStorage.getInstance(); storage = FoodStorage.getInstance();
entriesFuture = storage.getEntriesForDate(widget.date); entriesFuture = storage.getEntriesForDate(widget.date);
entriesFuture.then((val) { entriesFuture.then((val) {
entries = val; entries = val;
@ -93,15 +93,20 @@ class _PerDateWidgetState extends State<PerDateWidget> {
); );
} }
return FoodEntryWidget( return Column(
entry: state.foodEntries[index], children: [
onDelete: (callbackContext) { FoodEntryWidget(
callbackContext entry: state.foodEntries[index],
.read<FoodEntryBloc>() onDelete: (callbackContext, id) {
.add(FoodDeletionEvent( callbackContext
entryID: state.foodEntries[index].id, .read<FoodEntryBloc>()
)); .add(FoodDeletionEvent(
}, entryID: id,
));
},
),
const Divider(),
],
); );
}, },
); );

View File

@ -2,7 +2,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:calodiary/storage/storage.dart'; import 'package:calodiary/storage/storage.dart';
class SettingsDataBloc extends Bloc<SettingsEvent, SettingsState> { class SettingsDataBloc extends Bloc<SettingsEvent, SettingsState> {
final AppStorage storage; final FoodStorage storage;
SettingsDataBloc(super.initialState, {required this.storage}) { SettingsDataBloc(super.initialState, {required this.storage}) {
on<DailyKcalLimitUpdated>(persistDailyLimit); on<DailyKcalLimitUpdated>(persistDailyLimit);

View File

@ -1,16 +1,19 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:calodiary/food_entry_bloc.dart'; import 'package:calodiary/food_entry_bloc.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:universal_platform/universal_platform.dart'; import 'package:universal_platform/universal_platform.dart';
class AppStorage { class FoodStorage {
static late AppStorage _instance; static late FoodStorage _instance;
late String path; late String path;
AppStorage._create(); late Map<String, double> _foodLookupDatabase = {};
static Future<AppStorage> create() async { FoodStorage._create();
var storage = AppStorage._create();
static Future<FoodStorage> create() async {
var storage = FoodStorage._create();
Directory dir = Directory(''); Directory dir = Directory('');
@ -26,7 +29,7 @@ class AppStorage {
return _instance; return _instance;
} }
static AppStorage getInstance() => _instance; static FoodStorage getInstance() => _instance;
Future<List<FoodEntry>> getEntriesForDate(DateTime date) async { Future<List<FoodEntry>> getEntriesForDate(DateTime date) async {
List<FoodEntry> entries = []; List<FoodEntry> entries = [];
@ -132,4 +135,32 @@ class AppStorage {
await file.writeAsString(brightness); 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 pastDay = Duration(days: idx);
return DateTime.now().subtract(pastDay);
});
for (var date in dates) {
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(FoodEntry entry) {
_foodLookupDatabase[entry.name] = entry.kcalPerMass;
log("Added entry: ${entry.name}/${entry.kcalPerMass}");
}
Map<String, double> get getFoodEntryLookupDatabase => _foodLookupDatabase;
} }

View File

@ -2,7 +2,7 @@ import 'package:calodiary/storage/storage.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class ThemeDataBloc extends Bloc<ThemeToggleEvent, ThemeState> { class ThemeDataBloc extends Bloc<ThemeToggleEvent, ThemeState> {
final AppStorage storage; final FoodStorage storage;
ThemeDataBloc(super.initialState, {required this.storage}) { ThemeDataBloc(super.initialState, {required this.storage}) {
on<ThemeToggleEvent>(switchTheme); on<ThemeToggleEvent>(switchTheme);

View File

@ -53,10 +53,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.5"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -69,10 +69,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -121,10 +121,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554 sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.0" version: "14.2.7"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -193,10 +193,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.14.0" version: "1.15.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -217,18 +217,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.4"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.5" version: "2.2.10"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@ -257,10 +257,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -350,10 +350,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.1" version: "0.7.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -374,10 +374,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.4.0" version: "4.5.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -390,18 +390,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.2" version: "14.2.5"
win32:
dependency: transitive
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.5.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description: