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.
This commit is contained in:
parent
fb0dbef158
commit
b83f547f6b
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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<ThemeDataBloc, ThemeState>(
|
||||
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<ThemeDataBloc, ThemeState>(
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
entry: state.foodEntries[index],
|
||||
onDelete: (callbackContext) {
|
||||
callbackContext
|
||||
.read<FoodEntryBloc>()
|
||||
.add(FoodDeletionEvent(
|
||||
entryID: state.foodEntries[index].id,
|
||||
));
|
||||
},
|
||||
return Column(
|
||||
children: [
|
||||
FoodEntryWidget(
|
||||
entry: state.foodEntries[index],
|
||||
onDelete: (callbackContext, id) {
|
||||
callbackContext
|
||||
.read<FoodEntryBloc>()
|
||||
.add(FoodDeletionEvent(
|
||||
entryID: id,
|
||||
));
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
48
pubspec.lock
48
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:
|
||||
|
Loading…
Reference in New Issue
Block a user