Merge pull request 'Implement food entry lookup on entering a food name.' (#1) from food-suggestion into master

Reviewed-on: marco/calodiary#1
This commit is contained in:
marco 2024-09-05 11:08:50 +00:00
commit 131f39c1c8
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:calodiary/food_entry_bloc.dart';
import 'package:calodiary/row_with_spacers_widget.dart';
@ -16,22 +19,53 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
TextEditingController nameController = TextEditingController();
TextEditingController massController = TextEditingController();
TextEditingController kcalPerMassController = TextEditingController();
Map<String, double> 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<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 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);

View File

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

View File

@ -4,14 +4,20 @@ 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(
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(
@ -22,11 +28,12 @@ class FoodEntryWidget extends StatelessWidget {
IconButton(
style: IconButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
onDelete(context);
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 {
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,7 +37,8 @@ class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
return SafeArea(
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SettingsDataBloc(
@ -44,7 +46,8 @@ class MainApp extends StatelessWidget {
storage: storage),
),
BlocProvider(
create: (context) => ThemeDataBloc(ThemeState(brightness: brightness),
create: (context) => ThemeDataBloc(
ThemeState(brightness: brightness),
storage: storage),
),
],
@ -69,6 +72,7 @@ class MainApp extends StatelessWidget {
);
},
),
),
);
}
}

View File

@ -19,7 +19,7 @@ class PerDateWidget extends StatefulWidget {
}
class _PerDateWidgetState extends State<PerDateWidget> {
late AppStorage storage;
late FoodStorage storage;
late Future<List<FoodEntry>> entriesFuture;
late List<FoodEntry> entries;
@ -27,7 +27,7 @@ class _PerDateWidgetState extends State<PerDateWidget> {
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<PerDateWidget> {
);
}
return FoodEntryWidget(
return Column(
children: [
FoodEntryWidget(
entry: state.foodEntries[index],
onDelete: (callbackContext) {
onDelete: (callbackContext, id) {
callbackContext
.read<FoodEntryBloc>()
.add(FoodDeletionEvent(
entryID: state.foodEntries[index].id,
entryID: id,
));
},
),
const Divider(),
],
);
},
);

View File

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

View File

@ -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<String, double> _foodLookupDatabase = {};
static Future<AppStorage> create() async {
var storage = AppStorage._create();
FoodStorage._create();
static Future<FoodStorage> 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<List<FoodEntry>> getEntriesForDate(DateTime date) async {
List<FoodEntry> entries = [];
@ -132,4 +135,32 @@ class AppStorage {
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';
class ThemeDataBloc extends Bloc<ThemeToggleEvent, ThemeState> {
final AppStorage storage;
final FoodStorage storage;
ThemeDataBloc(super.initialState, {required this.storage}) {
on<ThemeToggleEvent>(switchTheme);

View File

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