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..36d69effe --- /dev/null +++ b/lib/core/data/data_source/meal_data_source.dart @@ -0,0 +1,54 @@ +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(); + } + + + 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 d6b771639..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) @@ -80,7 +85,9 @@ enum MealSourceDBO { @HiveField(2) off, @HiveField(3) - fdc; + fdc, + @HiveField(4) + imported; factory MealSourceDBO.fromMealSourceEntity(MealSourceEntity entity) { MealSourceDBO mealSourceDBO; @@ -91,6 +98,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..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 @@ -88,6 +91,8 @@ class MealSourceDBOAdapter extends TypeAdapter { return MealSourceDBO.off; case 3: return MealSourceDBO.fdc; + case 4: + return MealSourceDBO.imported; default: return MealSourceDBO.unknown; } @@ -108,6 +113,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..881c1229a --- /dev/null +++ b/lib/features/add_meal/data/data_sources/local_data_source.dart @@ -0,0 +1,24 @@ +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 { + 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 63ef1c018..15ee06398 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,9 +44,20 @@ 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); return MealEntity.fromOFFProduct(productResponse.product); } + + Future getImportProductByBarcode(String barcode) async { + final productResponse = await _localDataSource.fetchBarcodeResults(barcode); + + return productResponse; + } + } diff --git a/lib/features/add_meal/domain/entity/meal_entity.dart b/lib/features/add_meal/domain/entity/meal_entity.dart index 6a60603b8..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( @@ -164,6 +169,7 @@ enum MealSourceEntity { unknown, custom, off, + imported, fdc; factory MealSourceEntity.fromMealSourceDBO(MealSourceDBO mealSourceDBO) { @@ -175,6 +181,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/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 d0ac5d688..11f7d85b9 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:opennutritracker/core/domain/entity/app_theme_entity.dart'; @@ -13,6 +15,12 @@ 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:opennutritracker/features/edit_meal/presentation/bloc/edit_meal_bloc.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 { const SettingsScreen({super.key}); @@ -23,10 +31,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 +88,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 +409,83 @@ 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 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( + const SnackBar(content: Text('Loading meals, this could take a while.')), + ); + } + + final mealSrc = locator(); + + // Adding the new meals + for (var line in lines.sublist(1)) { + final item = line.split(','); + if (item[nameIndex].isEmpty) { + continue; + } + + final nutriments = MealNutrimentsDBO( + 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(), + name: item[nameIndex], + brands: null, + thumbnailImageUrl: null, + mainImageUrl: null, + url: null, + mealQuantity: '100', + mealUnit: 'g', + servingQuantity: null, + servingUnit: 'g', + nutriments: nutriments, + source: MealSourceDBO.imported, + barcode: barcodeIndex == -1 ? null : item[barcodeIndex]); + mealSrc.addMeal(mealDBO); + } + } + } 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..3e9011f5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: cached_network_image: ^3.2.3 flutter_cache_manager: ^3.3.0 envied: ^0.5.3 + file_picker: ^8.0.0 flutter_bloc: ^8.1.2 equatable: ^2.0.5