From 30aa2bf1290b5cd6b7d01b659c8c50b6197c3f9f Mon Sep 17 00:00:00 2001 From: dd-dreams <80887265+dd-dreams@users.noreply.github.com> Date: Tue, 8 Oct 2024 01:48:54 +0300 Subject: [PATCH 1/5] add importing meals via csv --- .../data/data_source/meal_data_source.dart | 47 ++++++++++ lib/core/data/dbo/meal_dbo.dart | 7 +- lib/core/data/dbo/meal_dbo.g.dart | 5 ++ lib/core/data/repository/meal_repository.dart | 31 +++++++ lib/core/utils/hive_db_provider.dart | 4 + lib/core/utils/locator.dart | 10 ++- .../data/data_sources/local_data_source.dart | 13 +++ .../data/repository/products_repository.dart | 9 +- .../add_meal/domain/entity/meal_entity.dart | 4 + .../usecase/search_products_usecase.dart | 7 ++ .../presentation/bloc/products_bloc.dart | 20 +++-- .../widgets/meal_info_button.dart | 6 ++ lib/features/settings/settings_screen.dart | 86 +++++++++++++++++++ lib/generated/intl/messages_en.dart | 1 + lib/generated/l10n.dart | 20 +++++ lib/l10n/intl_en.arb | 2 + pubspec.yaml | 2 + 17 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 lib/core/data/data_source/meal_data_source.dart create mode 100644 lib/core/data/repository/meal_repository.dart create mode 100644 lib/features/add_meal/data/data_sources/local_data_source.dart diff --git a/lib/core/data/data_source/meal_data_source.dart b/lib/core/data/data_source/meal_data_source.dart new file mode 100644 index 000000000..cd81e0b7a --- /dev/null +++ b/lib/core/data/data_source/meal_data_source.dart @@ -0,0 +1,47 @@ +import 'package:collection/collection.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:logging/logging.dart'; +import 'package:opennutritracker/core/data/dbo/meal_dbo.dart'; + +class MealDataSource { + final log = Logger('MealDataSource'); + final Box _mealBox; + + MealDataSource(this._mealBox); + + Future addMeal(MealDBO mealDBO) async { + if ((await getMealByName(mealDBO.name ?? "")) == null ){ + log.fine('Adding new meal item to db'); + _mealBox.add(mealDBO); + } + } + + Future deleteMealFromId(String mealId) async { + log.fine('Deleting meal item from db'); + _mealBox.values + .where((dbo) => dbo.code == mealId) + .toList() + .forEach((element) { + element.delete(); + }); + } + + Future getMealById(String mealId) async { + return _mealBox.values.firstWhereOrNull( + (meal) => meal.code == mealId + ); + } + + Future getMealByName(String name) async { + return _mealBox.values.firstWhereOrNull( + (meal) => meal.name == name + ); + } + + Future> getMealsByName(String name) async { + return _mealBox.values.where( + (meal) => meal.name!.contains(name) + ).toList(); + } + +} diff --git a/lib/core/data/dbo/meal_dbo.dart b/lib/core/data/dbo/meal_dbo.dart index d6b771639..967daa11f 100644 --- a/lib/core/data/dbo/meal_dbo.dart +++ b/lib/core/data/dbo/meal_dbo.dart @@ -80,7 +80,9 @@ enum MealSourceDBO { @HiveField(2) off, @HiveField(3) - fdc; + fdc, + @HiveField(4) + imported; factory MealSourceDBO.fromMealSourceEntity(MealSourceEntity entity) { MealSourceDBO mealSourceDBO; @@ -91,6 +93,9 @@ enum MealSourceDBO { case MealSourceEntity.custom: mealSourceDBO = MealSourceDBO.custom; break; + case MealSourceEntity.imported: + mealSourceDBO = MealSourceDBO.imported; + break; case MealSourceEntity.off: mealSourceDBO = MealSourceDBO.off; break; diff --git a/lib/core/data/dbo/meal_dbo.g.dart b/lib/core/data/dbo/meal_dbo.g.dart index 402180c5e..ed637ffaf 100644 --- a/lib/core/data/dbo/meal_dbo.g.dart +++ b/lib/core/data/dbo/meal_dbo.g.dart @@ -88,6 +88,8 @@ class MealSourceDBOAdapter extends TypeAdapter { return MealSourceDBO.off; case 3: return MealSourceDBO.fdc; + case 4: + return MealSourceDBO.imported; default: return MealSourceDBO.unknown; } @@ -108,6 +110,9 @@ class MealSourceDBOAdapter extends TypeAdapter { case MealSourceDBO.fdc: writer.writeByte(3); break; + case MealSourceDBO.imported: + writer.writeByte(4); + break; } } diff --git a/lib/core/data/repository/meal_repository.dart b/lib/core/data/repository/meal_repository.dart new file mode 100644 index 000000000..616768528 --- /dev/null +++ b/lib/core/data/repository/meal_repository.dart @@ -0,0 +1,31 @@ +import 'package:opennutritracker/core/data/data_source/meal_data_source.dart'; +import 'package:opennutritracker/core/data/dbo/meal_dbo.dart'; +import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; + +class MealRepository { + final MealDataSource _mealDataSource; + + MealRepository(this._mealDataSource); + + Future addMeal(MealEntity mealEntity) async { + final mealDBO = MealDBO.fromMealEntity(mealEntity); + + await _mealDataSource.addMeal(mealDBO); + } + + Future deleteMeal(MealEntity mealEntity) async { + if (mealEntity.code != null) { + await _mealDataSource.deleteMealFromId(mealEntity.code ?? ""); + } + } + + // Future updateMeal(String mealId, Map fields) async { + // var result = await _mealDataSource.updateMeal(mealId, fields); + // return result == null ? null : MealEntity.fromMealDBO(result); + // } + + Future getMealById(String mealId) async { + final result = await _mealDataSource.getMealById(mealId); + return result == null ? null : MealEntity.fromMealDBO(result); + } +} diff --git a/lib/core/utils/hive_db_provider.dart b/lib/core/utils/hive_db_provider.dart index 33181f6e7..9f670966a 100644 --- a/lib/core/utils/hive_db_provider.dart +++ b/lib/core/utils/hive_db_provider.dart @@ -22,12 +22,14 @@ class HiveDBProvider extends ChangeNotifier { static const userActivityBoxName = 'UserActivityBox'; static const userBoxName = 'UserBox'; static const trackedDayBoxName = 'TrackedDayBox'; + static const importedMealsBoxName = 'importedMealsBox'; late Box configBox; late Box intakeBox; late Box userActivityBox; late Box userBox; late Box trackedDayBox; + late Box importedMealsBox; Future initHiveDB(Uint8List encryptionKey) async { final encryptionCypher = HiveAesCipher(encryptionKey); @@ -58,6 +60,8 @@ class HiveDBProvider extends ChangeNotifier { await Hive.openBox(userBoxName, encryptionCipher: encryptionCypher); trackedDayBox = await Hive.openBox(trackedDayBoxName, encryptionCipher: encryptionCypher); + importedMealsBox = await Hive.openBox(importedMealsBoxName, + encryptionCipher: encryptionCypher); } static generateNewHiveEncryptionKey() => Hive.generateSecureKey(); diff --git a/lib/core/utils/locator.dart b/lib/core/utils/locator.dart index 4d93cce72..5afef334b 100644 --- a/lib/core/utils/locator.dart +++ b/lib/core/utils/locator.dart @@ -6,8 +6,10 @@ import 'package:opennutritracker/core/data/data_source/physical_activity_data_so import 'package:opennutritracker/core/data/data_source/tracked_day_data_source.dart'; import 'package:opennutritracker/core/data/data_source/user_activity_data_source.dart'; import 'package:opennutritracker/core/data/data_source/user_data_source.dart'; +import 'package:opennutritracker/core/data/data_source/meal_data_source.dart'; import 'package:opennutritracker/core/data/repository/config_repository.dart'; import 'package:opennutritracker/core/data/repository/intake_repository.dart'; +import 'package:opennutritracker/core/data/repository/meal_repository.dart'; import 'package:opennutritracker/core/data/repository/physical_activity_repository.dart'; import 'package:opennutritracker/core/data/repository/tracked_day_repository.dart'; import 'package:opennutritracker/core/data/repository/user_activity_repository.dart'; @@ -36,6 +38,7 @@ import 'package:opennutritracker/features/add_activity/presentation/bloc/recent_ import 'package:opennutritracker/features/add_meal/data/data_sources/fdc_data_source.dart'; import 'package:opennutritracker/features/add_meal/data/data_sources/off_data_source.dart'; import 'package:opennutritracker/features/add_meal/data/data_sources/sp_fdc_data_source.dart'; +import 'package:opennutritracker/features/add_meal/data/data_sources/local_data_source.dart'; import 'package:opennutritracker/features/add_meal/data/repository/products_repository.dart'; import 'package:opennutritracker/features/add_meal/domain/usecase/search_products_usecase.dart'; import 'package:opennutritracker/features/add_meal/presentation/bloc/food_bloc.dart'; @@ -137,13 +140,15 @@ Future initLocator() async { locator.registerLazySingleton( () => IntakeRepository(locator())); locator.registerLazySingleton( - () => ProductsRepository(locator(), locator(), locator())); + () => ProductsRepository(locator(), locator(), locator(), locator())); locator.registerLazySingleton( () => UserActivityRepository(locator())); locator.registerLazySingleton( () => PhysicalActivityRepository(locator())); locator.registerLazySingleton( () => TrackedDayRepository(locator())); + locator.registerLazySingleton( + () => MealRepository(locator())); // DataSources locator @@ -156,9 +161,12 @@ Future initLocator() async { () => UserActivityDataSource(hiveDBProvider.userActivityBox)); locator.registerLazySingleton( () => PhysicalActivityDataSource()); + locator.registerLazySingleton( + () => MealDataSource(hiveDBProvider.importedMealsBox)); locator.registerLazySingleton(() => OFFDataSource()); locator.registerLazySingleton(() => FDCDataSource()); locator.registerLazySingleton(() => SpFdcDataSource()); + locator.registerLazySingleton(() => LocalDataSource()); locator.registerLazySingleton( () => TrackedDayDataSource(hiveDBProvider.trackedDayBox)); diff --git a/lib/features/add_meal/data/data_sources/local_data_source.dart b/lib/features/add_meal/data/data_sources/local_data_source.dart new file mode 100644 index 000000000..4b2dd7f00 --- /dev/null +++ b/lib/features/add_meal/data/data_sources/local_data_source.dart @@ -0,0 +1,13 @@ +import 'package:logging/logging.dart'; +import 'package:opennutritracker/core/data/data_source/meal_data_source.dart'; +import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; +import 'package:opennutritracker/core/utils/locator.dart'; + +class LocalDataSource { + final log = Logger('LocalDataSource'); + + Future> fetchSearchWordResults(String searchString) async { + final mealSrc = locator(); + return (await mealSrc.getMealsByName(searchString)).map((meal) => MealEntity.fromMealDBO(meal)).toList(); + } +} diff --git a/lib/features/add_meal/data/repository/products_repository.dart b/lib/features/add_meal/data/repository/products_repository.dart index 63ef1c018..2b3486747 100644 --- a/lib/features/add_meal/data/repository/products_repository.dart +++ b/lib/features/add_meal/data/repository/products_repository.dart @@ -1,15 +1,18 @@ import 'package:opennutritracker/features/add_meal/data/data_sources/fdc_data_source.dart'; import 'package:opennutritracker/features/add_meal/data/data_sources/off_data_source.dart'; import 'package:opennutritracker/features/add_meal/data/data_sources/sp_fdc_data_source.dart'; +import 'package:opennutritracker/features/add_meal/data/data_sources/local_data_source.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; class ProductsRepository { final OFFDataSource _offDataSource; final FDCDataSource _fdcDataSource; final SpFdcDataSource _spBackendDataSource; + final LocalDataSource _localDataSource; ProductsRepository( - this._offDataSource, this._fdcDataSource, this._spBackendDataSource); + this._offDataSource, this._fdcDataSource, this._spBackendDataSource, + this._localDataSource); Future> getOFFProductsByString(String searchString) async { final offWordResponse = @@ -41,6 +44,10 @@ class ProductsRepository { return products; } + Future> getLocalMealsByString(String searchString) async { + return await _localDataSource.fetchSearchWordResults(searchString); + } + Future getOFFProductByBarcode(String barcode) async { final productResponse = await _offDataSource.fetchBarcodeResults(barcode); diff --git a/lib/features/add_meal/domain/entity/meal_entity.dart b/lib/features/add_meal/domain/entity/meal_entity.dart index 6a60603b8..62b97e5b5 100644 --- a/lib/features/add_meal/domain/entity/meal_entity.dart +++ b/lib/features/add_meal/domain/entity/meal_entity.dart @@ -164,6 +164,7 @@ enum MealSourceEntity { unknown, custom, off, + imported, fdc; factory MealSourceEntity.fromMealSourceDBO(MealSourceDBO mealSourceDBO) { @@ -175,6 +176,9 @@ enum MealSourceEntity { case MealSourceDBO.custom: mealSourceEntity = MealSourceEntity.custom; break; + case MealSourceDBO.imported: + mealSourceEntity = MealSourceEntity.imported; + break; case MealSourceDBO.off: mealSourceEntity = MealSourceEntity.off; break; diff --git a/lib/features/add_meal/domain/usecase/search_products_usecase.dart b/lib/features/add_meal/domain/usecase/search_products_usecase.dart index d3b6d72ad..239d9947c 100644 --- a/lib/features/add_meal/domain/usecase/search_products_usecase.dart +++ b/lib/features/add_meal/domain/usecase/search_products_usecase.dart @@ -18,4 +18,11 @@ class SearchProductsUseCase { await _productsRepository.getSupabaseFDCFoodsByString(searchString); return foods; } + + Future> searchLocalProductsByString( + String searchString) async { + final products = + await _productsRepository.getLocalMealsByString(searchString); + return products; + } } diff --git a/lib/features/add_meal/presentation/bloc/products_bloc.dart b/lib/features/add_meal/presentation/bloc/products_bloc.dart index 34050a0d9..c69aba598 100644 --- a/lib/features/add_meal/presentation/bloc/products_bloc.dart +++ b/lib/features/add_meal/presentation/bloc/products_bloc.dart @@ -20,13 +20,21 @@ class ProductsBloc extends Bloc { if (event.searchString != _searchString) { _searchString = event.searchString; emit(ProductsLoadingState()); - try { - final result = await _searchProductUseCase - .searchOFFProductsByString(_searchString); + + // First search locally + final result = await _searchProductUseCase + .searchLocalProductsByString(_searchString); + if (result.isNotEmpty) { emit(ProductsLoadedState(products: result)); - } catch (error) { - log.severe(error); - emit(ProductsFailedState()); + } else { + try { + final result = await _searchProductUseCase + .searchOFFProductsByString(_searchString); + emit(ProductsLoadedState(products: result)); + } catch (error) { + log.severe(error); + emit(ProductsFailedState()); + } } } }); diff --git a/lib/features/meal_detail/presentation/widgets/meal_info_button.dart b/lib/features/meal_detail/presentation/widgets/meal_info_button.dart index 0581ef513..004b8d5df 100644 --- a/lib/features/meal_detail/presentation/widgets/meal_info_button.dart +++ b/lib/features/meal_detail/presentation/widgets/meal_info_button.dart @@ -35,6 +35,9 @@ class MealInfoButton extends StatelessWidget { case MealSourceEntity.custom: siteUrl = ""; break; + case MealSourceEntity.imported: + siteUrl = ""; + break; case MealSourceEntity.off: siteUrl = url ?? OFFConst.offWebsiteUrl; break; @@ -54,6 +57,9 @@ class MealInfoButton extends StatelessWidget { case MealSourceEntity.custom: infoLabel = S.of(context).additionalInfoLabelCustom; break; + case MealSourceEntity.imported: + infoLabel = S.of(context).additionalInfoLabelImport; + break; case MealSourceEntity.off: infoLabel = S.of(context).additionalInfoLabelOFF; break; diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index d0ac5d688..fac38ac52 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -1,3 +1,6 @@ +import 'dart:io'; +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:opennutritracker/core/domain/entity/app_theme_entity.dart'; @@ -13,6 +16,14 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:csv/csv.dart'; +import 'package:opennutritracker/features/edit_meal/presentation/bloc/edit_meal_bloc.dart'; +import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; +import 'package:opennutritracker/features/add_meal/domain/entity/meal_nutriments_entity.dart'; +import 'package:opennutritracker/core/data/data_source/meal_data_source.dart'; +import 'package:opennutritracker/core/data/dbo/meal_dbo.dart'; +import 'package:opennutritracker/core/utils/id_generator.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -23,10 +34,12 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { late SettingsBloc _settingsBloc; + late EditMealBloc _editMealBloc; @override void initState() { _settingsBloc = locator(); + _editMealBloc = locator(); super.initState(); } @@ -78,6 +91,11 @@ class _SettingsScreenState extends State { onTap: () => _showPrivacyDialog(context, state.sendAnonymousData), ), + ListTile( + leading: const Icon(Icons.import_export), + title: Text(S.of(context).settingImportLabel), + onTap: () => _showImportFilePicker(context), + ), ListTile( leading: const Icon(Icons.error_outline_outlined), title: Text(S.of(context).settingAboutLabel), @@ -394,6 +412,74 @@ class _SettingsScreenState extends State { } } + void _showImportFilePicker(BuildContext context) async { + // FilePicker currently doesn't support picking exclusivley CSV files: + // https://github.com/miguelpruivo/flutter_file_picker/issues/976#issuecomment-1063914016 + FilePickerResult? result = await FilePicker.platform.pickFiles(); + + if (context.mounted) { + if (result != null) { + final String filePath = result.files.single.path!; + if (!filePath.endsWith('csv')) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Not a valid file type. Only CSV is supported.')), + ); + } + else { + final input = File(filePath).openRead(); + final fields = await input.transform(utf8.decoder).transform(const CsvToListConverter()).toList(); + final nameIndex = fields[0].indexOf('name'); + final proteinIndex = fields[0].indexOf('protein'); + final kcalIndex = fields[0].indexOf('food_energy'); + final carbsIndex = fields[0].indexOf('carbohydrates'); + final fatIndex = fields[0].indexOf('total_fat'); + final sugarIndex = fields[0].indexOf('total_sugars'); + final saturatedFatIndex = fields[0].indexOf('saturated_fat'); + final fiberIndex = fields[0].indexOf('fiber'); + // TODO: Currently unused, useful in the future. + final barcodeIndex = fields[0].indexOf('barcode'); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Loading meals, this could take a while.')), + ); + } + + final mealSrc = locator(); + + // Adding the new meals + for (var item in fields.sublist(1)) { + final nutriments = MealNutrimentsEntity( + energyKcal100: kcalIndex == -1 || item[kcalIndex] is String ? null : item[kcalIndex].toDouble(), + carbohydrates100: carbsIndex == -1 || item[carbsIndex] is String ? null : item[carbsIndex].toDouble(), + fat100: fatIndex == -1 || item[fatIndex] is String ? null : item[fatIndex].toDouble(), + proteins100: proteinIndex == -1 || item[proteinIndex] is String ? null : item[proteinIndex].toDouble(), + sugars100: sugarIndex == -1 || item[sugarIndex] is String ? null : item[sugarIndex].toDouble(), + saturatedFat100: saturatedFatIndex == -1 || item[saturatedFatIndex] is String ? null : item[saturatedFatIndex].toDouble(), + fiber100: fiberIndex == -1 || item[fiberIndex] is String ? null : item[fiberIndex].toDouble() + ); + final mealEntity = MealEntity( + code: IdGenerator.getUniqueID(), + name: item[nameIndex], + url: null, + mealQuantity: "100", + mealUnit: 'g', + servingQuantity: null, + servingUnit: 'g', + nutriments: nutriments, + source: MealSourceEntity.imported); + mealSrc.addMeal(MealDBO.fromMealEntity(mealEntity)); + } + } + } else { + // User canceled the picker + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No file selected')), + ); + } + } + } + void _launchSourceCodeUrl(BuildContext context) async { final sourceCodeUri = Uri.parse(AppConst.sourceCodeUrl); _launchUrl(context, sourceCodeUri); diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 6867887e2..964aee44c 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -585,6 +585,7 @@ class MessageLookup extends MessageLookupByLibrary { "servingSizeLabel": MessageLookupByLibrary.simpleMessage("Serving size (g/ml)"), "settingAboutLabel": MessageLookupByLibrary.simpleMessage("About"), + "settingImportLabel": MessageLookupByLibrary.simpleMessage("Import CSV"), "settingFeedbackLabel": MessageLookupByLibrary.simpleMessage("Feedback"), "settingsCalculationsLabel": diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 12a0372b6..cbb981793 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -640,6 +640,16 @@ class S { ); } + /// `Import CSV` + String get settingImportLabel { + return Intl.message( + 'Import CSV', + name: 'settingImportLabel', + desc: '', + args: [], + ); + } + /// `Mass` String get settingsMassLabel { return Intl.message( @@ -1111,6 +1121,16 @@ class S { ); } + /// `Imported Meal Item` + String get additionalInfoLabelImport { + return Intl.message( + 'Imported Meal Item', + name: 'additionalInfoLabelImport', + desc: '', + args: [], + ); + } + /// `Information provided\n by the \n'2011 Compendium\n of Physical Activities'` String get additionalInfoLabelCompendium2011 { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 477e40d01..bfc0f6399 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -62,6 +62,7 @@ "settingsSourceCodeLabel": "Source Code", "settingFeedbackLabel": "Feedback", "settingAboutLabel": "About", + "settingImportLabel": "Import CSV", "settingsMassLabel": "Mass", "settingsDistanceLabel": "Distance", "settingsVolumeLabel": "Volume", @@ -114,6 +115,7 @@ "additionalInfoLabelFDC": "More Information at\nFoodData Central", "additionalInfoLabelUnknown": "Unknown Meal Item", "additionalInfoLabelCustom": "Custom Meal Item", + "additionalInfoLabelImport": "Imported Meal Item", "additionalInfoLabelCompendium2011": "Information provided\n by the \n'2011 Compendium\n of Physical Activities'", "quantityLabel": "Quantity", "baseQuantityLabel": "Base quantity (g/ml)", diff --git a/pubspec.yaml b/pubspec.yaml index b6395133f..3a648f707 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,8 @@ dependencies: cached_network_image: ^3.2.3 flutter_cache_manager: ^3.3.0 envied: ^0.5.3 + file_picker: ^8.0.0 + csv: ^6.0.0 flutter_bloc: ^8.1.2 equatable: ^2.0.5 From bdd7e545dbdd1d78ddaf29359d80840cb6550a9a Mon Sep 17 00:00:00 2001 From: dd-dreams <80887265+dd-dreams@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:31:42 +0300 Subject: [PATCH 2/5] check for an empty name --- lib/features/settings/settings_screen.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index fac38ac52..bb1247b56 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -449,6 +449,10 @@ class _SettingsScreenState extends State { // Adding the new meals for (var item in fields.sublist(1)) { + if (item[nameIndex].isEmpty) { + continue; + } + final nutriments = MealNutrimentsEntity( energyKcal100: kcalIndex == -1 || item[kcalIndex] is String ? null : item[kcalIndex].toDouble(), carbohydrates100: carbsIndex == -1 || item[carbsIndex] is String ? null : item[carbsIndex].toDouble(), From b75702162c2f3525c3e6b4b25483e51027ffe8f5 Mon Sep 17 00:00:00 2001 From: dd-dreams <80887265+dd-dreams@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:31:42 +0300 Subject: [PATCH 3/5] add barcode field and refactor to MealNutrimentsDBO --- lib/core/data/data_source/meal_data_source.dart | 7 +++++++ lib/core/data/dbo/meal_dbo.dart | 9 +++++++-- lib/core/data/dbo/meal_dbo.g.dart | 7 +++++-- .../add_meal/domain/entity/meal_entity.dart | 11 ++++++++--- lib/features/settings/settings_screen.dart | 17 ++++++++++------- 5 files changed, 37 insertions(+), 14 deletions(-) diff --git a/lib/core/data/data_source/meal_data_source.dart b/lib/core/data/data_source/meal_data_source.dart index cd81e0b7a..2310bba9d 100644 --- a/lib/core/data/data_source/meal_data_source.dart +++ b/lib/core/data/data_source/meal_data_source.dart @@ -44,4 +44,11 @@ class MealDataSource { ).toList(); } + + Future getMealByBarCode(String barcode) async { + return _mealBox.values.firstWhereOrNull( + (meal) => meal.barcode == barcode + ); + } + } diff --git a/lib/core/data/dbo/meal_dbo.dart b/lib/core/data/dbo/meal_dbo.dart index 967daa11f..ec625fe12 100644 --- a/lib/core/data/dbo/meal_dbo.dart +++ b/lib/core/data/dbo/meal_dbo.dart @@ -37,6 +37,9 @@ class MealDBO extends HiveObject { @HiveField(11) final MealNutrimentsDBO nutriments; + @HiveField(12) + final String? barcode; + MealDBO( {required this.code, required this.name, @@ -49,7 +52,8 @@ class MealDBO extends HiveObject { required this.servingQuantity, required this.servingUnit, required this.nutriments, - required this.source}); + required this.source, + required this.barcode}); factory MealDBO.fromMealEntity( MealEntity mealEntity) => @@ -68,7 +72,8 @@ class MealDBO extends HiveObject { MealNutrimentsDBO.fromProductNutrimentsEntity( mealEntity.nutriments), source: - MealSourceDBO.fromMealSourceEntity(mealEntity.source)); + MealSourceDBO.fromMealSourceEntity(mealEntity.source), + barcode: mealEntity.barcode); } @HiveType(typeId: 14) diff --git a/lib/core/data/dbo/meal_dbo.g.dart b/lib/core/data/dbo/meal_dbo.g.dart index ed637ffaf..1858c1391 100644 --- a/lib/core/data/dbo/meal_dbo.g.dart +++ b/lib/core/data/dbo/meal_dbo.g.dart @@ -29,13 +29,14 @@ class MealDBOAdapter extends TypeAdapter { servingUnit: fields[9] as String?, nutriments: fields[11] as MealNutrimentsDBO, source: fields[10] as MealSourceDBO, + barcode: fields[12] as String?, ); } @override void write(BinaryWriter writer, MealDBO obj) { writer - ..writeByte(12) + ..writeByte(13) ..writeByte(0) ..write(obj.code) ..writeByte(1) @@ -59,7 +60,9 @@ class MealDBOAdapter extends TypeAdapter { ..writeByte(10) ..write(obj.source) ..writeByte(11) - ..write(obj.nutriments); + ..write(obj.nutriments) + ..writeByte(12) + ..write(obj.barcode); } @override diff --git a/lib/features/add_meal/domain/entity/meal_entity.dart b/lib/features/add_meal/domain/entity/meal_entity.dart index 62b97e5b5..034a2e226 100644 --- a/lib/features/add_meal/domain/entity/meal_entity.dart +++ b/lib/features/add_meal/domain/entity/meal_entity.dart @@ -26,6 +26,8 @@ class MealEntity extends Equatable { final double? servingQuantity; final String? servingUnit; + final String? barcode; + final MealSourceEntity source; final MealNutrimentsEntity nutriments; @@ -42,7 +44,8 @@ class MealEntity extends Equatable { required this.servingQuantity, required this.servingUnit, required this.nutriments, - required this.source}); + required this.source, + this.barcode}); factory MealEntity.empty() => MealEntity( code: IdGenerator.getUniqueID(), @@ -53,7 +56,8 @@ class MealEntity extends Equatable { servingQuantity: null, servingUnit: 'g', nutriments: MealNutrimentsEntity.empty(), - source: MealSourceEntity.custom); + source: MealSourceEntity.custom, + barcode: null); factory MealEntity.fromMealDBO(MealDBO mealDBO) => MealEntity( code: mealDBO.code, @@ -68,7 +72,8 @@ class MealEntity extends Equatable { servingUnit: mealDBO.servingUnit, nutriments: MealNutrimentsEntity.fromMealNutrimentsDBO(mealDBO.nutriments), - source: MealSourceEntity.fromMealSourceDBO(mealDBO.source)); + source: MealSourceEntity.fromMealSourceDBO(mealDBO.source), + barcode: mealDBO.barcode); factory MealEntity.fromOFFProduct(OFFProductDTO offProduct) { return MealEntity( diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index bb1247b56..32f878e30 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -20,9 +20,9 @@ import 'package:file_picker/file_picker.dart'; import 'package:csv/csv.dart'; import 'package:opennutritracker/features/edit_meal/presentation/bloc/edit_meal_bloc.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; -import 'package:opennutritracker/features/add_meal/domain/entity/meal_nutriments_entity.dart'; import 'package:opennutritracker/core/data/data_source/meal_data_source.dart'; import 'package:opennutritracker/core/data/dbo/meal_dbo.dart'; +import 'package:opennutritracker/core/data/dbo/meal_nutriments_dbo.dart'; import 'package:opennutritracker/core/utils/id_generator.dart'; class SettingsScreen extends StatefulWidget { @@ -436,7 +436,6 @@ class _SettingsScreenState extends State { final sugarIndex = fields[0].indexOf('total_sugars'); final saturatedFatIndex = fields[0].indexOf('saturated_fat'); final fiberIndex = fields[0].indexOf('fiber'); - // TODO: Currently unused, useful in the future. final barcodeIndex = fields[0].indexOf('barcode'); if (context.mounted) { @@ -453,7 +452,7 @@ class _SettingsScreenState extends State { continue; } - final nutriments = MealNutrimentsEntity( + final nutriments = MealNutrimentsDBO( energyKcal100: kcalIndex == -1 || item[kcalIndex] is String ? null : item[kcalIndex].toDouble(), carbohydrates100: carbsIndex == -1 || item[carbsIndex] is String ? null : item[carbsIndex].toDouble(), fat100: fatIndex == -1 || item[fatIndex] is String ? null : item[fatIndex].toDouble(), @@ -462,17 +461,21 @@ class _SettingsScreenState extends State { saturatedFat100: saturatedFatIndex == -1 || item[saturatedFatIndex] is String ? null : item[saturatedFatIndex].toDouble(), fiber100: fiberIndex == -1 || item[fiberIndex] is String ? null : item[fiberIndex].toDouble() ); - final mealEntity = MealEntity( + final mealDBO = MealDBO( code: IdGenerator.getUniqueID(), name: item[nameIndex], + brands: null, + thumbnailImageUrl: null, + mainImageUrl: null, url: null, - mealQuantity: "100", + mealQuantity: '100', mealUnit: 'g', servingQuantity: null, servingUnit: 'g', nutriments: nutriments, - source: MealSourceEntity.imported); - mealSrc.addMeal(MealDBO.fromMealEntity(mealEntity)); + source: MealSourceDBO.imported, + barcode: item[barcodeIndex]); + mealSrc.addMeal(mealDBO); } } } else { From 896ca60582e6f2a0968c74b11c6e6308fdbc97ee Mon Sep 17 00:00:00 2001 From: dd-dreams <80887265+dd-dreams@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:54:11 +0300 Subject: [PATCH 4/5] add local barcode search --- .../data/data_source/meal_data_source.dart | 2 +- .../data/data_sources/local_data_source.dart | 13 +++++- .../data/repository/products_repository.dart | 7 ++++ .../search_product_by_barcode_usecase.dart | 7 +++- lib/features/settings/settings_screen.dart | 42 ++++++++++--------- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/lib/core/data/data_source/meal_data_source.dart b/lib/core/data/data_source/meal_data_source.dart index 2310bba9d..36d69effe 100644 --- a/lib/core/data/data_source/meal_data_source.dart +++ b/lib/core/data/data_source/meal_data_source.dart @@ -45,7 +45,7 @@ class MealDataSource { } - Future getMealByBarCode(String barcode) async { + Future getMealByBarcode(String barcode) async { return _mealBox.values.firstWhereOrNull( (meal) => meal.barcode == barcode ); diff --git a/lib/features/add_meal/data/data_sources/local_data_source.dart b/lib/features/add_meal/data/data_sources/local_data_source.dart index 4b2dd7f00..881c1229a 100644 --- a/lib/features/add_meal/data/data_sources/local_data_source.dart +++ b/lib/features/add_meal/data/data_sources/local_data_source.dart @@ -2,12 +2,23 @@ import 'package:logging/logging.dart'; import 'package:opennutritracker/core/data/data_source/meal_data_source.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/core/utils/locator.dart'; +import 'package:opennutritracker/features/scanner/data/product_not_found_exception.dart'; class LocalDataSource { final log = Logger('LocalDataSource'); + final mealSrc = locator(); Future> fetchSearchWordResults(String searchString) async { - final mealSrc = locator(); return (await mealSrc.getMealsByName(searchString)).map((meal) => MealEntity.fromMealDBO(meal)).toList(); } + + Future fetchBarcodeResults(String barcode) async { + log.fine('Fetching Local result for $barcode'); + final product = await mealSrc.getMealByBarcode(barcode); + if (product == null) { + log.warning("Local product not found"); + return Future.error(ProductNotFoundException); + } + return MealEntity.fromMealDBO(product); + } } diff --git a/lib/features/add_meal/data/repository/products_repository.dart b/lib/features/add_meal/data/repository/products_repository.dart index 2b3486747..15ee06398 100644 --- a/lib/features/add_meal/data/repository/products_repository.dart +++ b/lib/features/add_meal/data/repository/products_repository.dart @@ -53,4 +53,11 @@ class ProductsRepository { return MealEntity.fromOFFProduct(productResponse.product); } + + Future getImportProductByBarcode(String barcode) async { + final productResponse = await _localDataSource.fetchBarcodeResults(barcode); + + return productResponse; + } + } diff --git a/lib/features/scanner/domain/usecase/search_product_by_barcode_usecase.dart b/lib/features/scanner/domain/usecase/search_product_by_barcode_usecase.dart index 039b39167..03e0d0097 100644 --- a/lib/features/scanner/domain/usecase/search_product_by_barcode_usecase.dart +++ b/lib/features/scanner/domain/usecase/search_product_by_barcode_usecase.dart @@ -7,6 +7,11 @@ class SearchProductByBarcodeUseCase { SearchProductByBarcodeUseCase(this._productsRepository); Future searchProductByBarcode(String barcode) async { - return await _productsRepository.getOFFProductByBarcode(barcode); + try { + final localProduct = await _productsRepository.getImportProductByBarcode(barcode); + return localProduct; + } catch (e) { + return await _productsRepository.getOFFProductByBarcode(barcode); + } } } diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 32f878e30..3819a29d9 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -426,17 +426,18 @@ class _SettingsScreenState extends State { ); } else { - final input = File(filePath).openRead(); - final fields = await input.transform(utf8.decoder).transform(const CsvToListConverter()).toList(); - final nameIndex = fields[0].indexOf('name'); - final proteinIndex = fields[0].indexOf('protein'); - final kcalIndex = fields[0].indexOf('food_energy'); - final carbsIndex = fields[0].indexOf('carbohydrates'); - final fatIndex = fields[0].indexOf('total_fat'); - final sugarIndex = fields[0].indexOf('total_sugars'); - final saturatedFatIndex = fields[0].indexOf('saturated_fat'); - final fiberIndex = fields[0].indexOf('fiber'); - final barcodeIndex = fields[0].indexOf('barcode'); + final file = File(filePath); + final lines = await file.readAsLines(); + final columns = lines[0].split(','); + final nameIndex = columns[0].indexOf('name'); + final proteinIndex = columns[0].indexOf('protein'); + final kcalIndex = columns[0].indexOf('food_energy'); + final carbsIndex = columns[0].indexOf('carbohydrates'); + final fatIndex = columns[0].indexOf('total_fat'); + final sugarIndex = columns[0].indexOf('total_sugars'); + final saturatedFatIndex = columns[0].indexOf('saturated_fat'); + final fiberIndex = columns[0].indexOf('fiber'); + final barcodeIndex = columns[0].indexOf('barcode'); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -447,19 +448,20 @@ class _SettingsScreenState extends State { final mealSrc = locator(); // Adding the new meals - for (var item in fields.sublist(1)) { + for (var line in lines.sublist(1)) { + final item = line.split(','); if (item[nameIndex].isEmpty) { continue; } final nutriments = MealNutrimentsDBO( - energyKcal100: kcalIndex == -1 || item[kcalIndex] is String ? null : item[kcalIndex].toDouble(), - carbohydrates100: carbsIndex == -1 || item[carbsIndex] is String ? null : item[carbsIndex].toDouble(), - fat100: fatIndex == -1 || item[fatIndex] is String ? null : item[fatIndex].toDouble(), - proteins100: proteinIndex == -1 || item[proteinIndex] is String ? null : item[proteinIndex].toDouble(), - sugars100: sugarIndex == -1 || item[sugarIndex] is String ? null : item[sugarIndex].toDouble(), - saturatedFat100: saturatedFatIndex == -1 || item[saturatedFatIndex] is String ? null : item[saturatedFatIndex].toDouble(), - fiber100: fiberIndex == -1 || item[fiberIndex] is String ? null : item[fiberIndex].toDouble() + energyKcal100: kcalIndex == -1 ? null : double.tryParse(item[kcalIndex]), + carbohydrates100: carbsIndex == -1 ? null : double.tryParse(item[carbsIndex]), + fat100: fatIndex == -1 ? null : double.tryParse(item[fatIndex]), + proteins100: proteinIndex == -1 ? null : double.tryParse(item[proteinIndex]), + sugars100: sugarIndex == -1 ? null : double.tryParse(item[sugarIndex]), + saturatedFat100: saturatedFatIndex == -1 ? null : double.tryParse(item[saturatedFatIndex]), + fiber100: fiberIndex == -1 ? null : double.tryParse(item[fiberIndex]) ); final mealDBO = MealDBO( code: IdGenerator.getUniqueID(), @@ -474,7 +476,7 @@ class _SettingsScreenState extends State { servingUnit: 'g', nutriments: nutriments, source: MealSourceDBO.imported, - barcode: item[barcodeIndex]); + barcode: barcodeIndex == -1 ? null : item[barcodeIndex]); mealSrc.addMeal(mealDBO); } } From fc861fc2a57f940c44417687772f6e156c0640c1 Mon Sep 17 00:00:00 2001 From: dd-dreams <80887265+dd-dreams@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:54:11 +0300 Subject: [PATCH 5/5] remove csv dependency and cleanup --- lib/features/settings/settings_screen.dart | 3 --- pubspec.yaml | 1 - 2 files changed, 4 deletions(-) diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 3819a29d9..11f7d85b9 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,9 +16,7 @@ import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:csv/csv.dart'; import 'package:opennutritracker/features/edit_meal/presentation/bloc/edit_meal_bloc.dart'; -import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/core/data/data_source/meal_data_source.dart'; import 'package:opennutritracker/core/data/dbo/meal_dbo.dart'; import 'package:opennutritracker/core/data/dbo/meal_nutriments_dbo.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 3a648f707..3e9011f5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,6 @@ dependencies: flutter_cache_manager: ^3.3.0 envied: ^0.5.3 file_picker: ^8.0.0 - csv: ^6.0.0 flutter_bloc: ^8.1.2 equatable: ^2.0.5