diff --git a/.metadata b/.metadata index a64a16055..b02a7e4bf 100644 --- a/.metadata +++ b/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - channel: stable + revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" + channel: "stable" project_type: app @@ -13,14 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: android - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: ios - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + - platform: linux + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + - platform: macos + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + - platform: web + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + - platform: windows + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section diff --git a/intake_test.hive b/intake_test.hive index 65c48028c..2f735d694 100644 Binary files a/intake_test.hive and b/intake_test.hive differ diff --git a/lib/features/edit_meal/presentation/edit_meal_screen.dart b/lib/features/edit_meal/presentation/edit_meal_screen.dart index 5cb301eca..302a37442 100644 --- a/lib/features/edit_meal/presentation/edit_meal_screen.dart +++ b/lib/features/edit_meal/presentation/edit_meal_screen.dart @@ -10,6 +10,7 @@ import 'package:opennutritracker/core/utils/extensions.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/core/utils/navigation_options.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; +import 'package:opennutritracker/validation/food_name_validator.dart'; import 'package:opennutritracker/features/edit_meal/presentation/bloc/edit_meal_bloc.dart'; import 'package:opennutritracker/features/edit_meal/presentation/widgets/default_meal_image.dart'; import 'package:opennutritracker/features/meal_detail/meal_detail_screen.dart'; @@ -17,14 +18,16 @@ import 'package:opennutritracker/generated/l10n.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class EditMealScreen extends StatefulWidget { - const EditMealScreen({super.key}); + const EditMealScreen({Key? key}) : super(key: key); @override State createState() => _EditMealScreenState(); } class _EditMealScreenState extends State { + final _formKey = GlobalKey(); final log = Logger('EditMealScreen'); + late MealEntity _mealEntity; late DateTime _day; late IntakeTypeEntity _intakeTypeEntity; @@ -43,77 +46,52 @@ class _EditMealScreenState extends State { final _proteinTextController = TextEditingController(); final _units = ['g', 'ml', 'g/ml']; - late String? selectedUnit; - - // late List _mealUnitDropdownItems; + late String selectedUnit; late List> _mealUnitButtonSegment; - // TODO: Add base quantity and unit - String baseQuantity = "100"; - String baseQuantityUnit = " g/ml"; + String baseQuantity = '100'; + String baseQuantityUnit = ' g/ml'; @override void initState() { - _editMealBloc = locator(); super.initState(); - + _editMealBloc = locator(); _baseQuantityTextController.addListener(() { - setState(() { - baseQuantity = _baseQuantityTextController.text; - }); + setState(() { baseQuantity = _baseQuantityTextController.text; }); }); } @override void didChangeDependencies() { - final args = - ModalRoute.of(context)?.settings.arguments as EditMealScreenArguments; + super.didChangeDependencies(); + final args = ModalRoute.of(context)!.settings.arguments as EditMealScreenArguments; _mealEntity = args.mealEntity; _day = args.day; _intakeTypeEntity = args.intakeTypeEntity; _usesImperialUnits = args.usesImperialUnits; - _nameTextController.text = _mealEntity.name ?? ""; - _brandsTextController.text = _mealEntity.brands ?? ""; - _mealQuantityTextController.text = _mealEntity.mealQuantity ?? ""; - _servingQuantityTextController.text = - _mealEntity.servingQuantity.toStringOrEmpty(); - _kcalTextController.text = - _mealEntity.nutriments.energyKcal100.toStringOrEmpty(); - _carbsTextController.text = - _mealEntity.nutriments.carbohydrates100.toStringOrEmpty(); + _nameTextController.text = _mealEntity.name ?? ''; + _brandsTextController.text = _mealEntity.brands ?? ''; + _mealQuantityTextController.text = _mealEntity.mealQuantity ?? ''; + _servingQuantityTextController.text = _mealEntity.servingQuantity.toStringOrEmpty(); + _kcalTextController.text = _mealEntity.nutriments.energyKcal100.toStringOrEmpty(); + _carbsTextController.text = _mealEntity.nutriments.carbohydrates100.toStringOrEmpty(); _fatTextController.text = _mealEntity.nutriments.fat100.toStringOrEmpty(); - _proteinTextController.text = - _mealEntity.nutriments.proteins100.toStringOrEmpty(); + _proteinTextController.text = _mealEntity.nutriments.proteins100.toStringOrEmpty(); selectedUnit = _switchButtonUnit(_mealEntity.mealUnit); - // Convert meal size to imperial units if necessary if (_usesImperialUnits) { - _mealQuantityTextController.text = _convertToImperial( - _mealQuantityTextController.text, _mealEntity.mealUnit ?? "0"); - _servingQuantityTextController.text = _convertToImperial( - _servingQuantityTextController.text, _mealEntity.mealUnit ?? "0"); + _mealQuantityTextController.text = + _convertToImperial(_mealQuantityTextController.text, _mealEntity.mealUnit ?? 'g'); + _servingQuantityTextController.text = + _convertToImperial(_servingQuantityTextController.text, _mealEntity.mealUnit ?? 'g'); } _mealUnitButtonSegment = [ - ButtonSegment( - value: _units[0], - label: Text( - _usesImperialUnits ? S.of(context).ozUnit : S.of(context).gramUnit), - ), - ButtonSegment( - value: _units[1], - label: Text(_usesImperialUnits - ? S.of(context).flOzUnit - : S.of(context).milliliterUnit), - ), - ButtonSegment( - value: _units[2], - label: Text(S.of(context).gramMilliliterUnit), - ), + ButtonSegment(value: _units[0], label: Text(_usesImperialUnits ? S.of(context).ozUnit : S.of(context).gramUnit)), + ButtonSegment(value: _units[1], label: Text(_usesImperialUnits ? S.of(context).flOzUnit : S.of(context).milliliterUnit)), + ButtonSegment(value: _units[2], label: Text(S.of(context).gramMilliliterUnit)), ]; - - super.didChangeDependencies(); } @override @@ -124,21 +102,19 @@ class _EditMealScreenState extends State { title: Text(S.of(context).editMealLabel), actions: [ Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), + padding: const EdgeInsets.fromLTRB(16,0,16,0), child: FilledButton( - onPressed: () => _onSavePressed(_usesImperialUnits), - child: Text(S.of(context).buttonSaveLabel)), - ) + onPressed: () => _onSavePressed(), + child: Text(S.of(context).buttonSaveLabel), + ), + ), ], ), body: BlocBuilder( - bloc: locator()..add(InitializeEditMealEvent()), - builder: (BuildContext context, EditMealState state) { - if (state is EditMealLoadingState) { - return _getLoadingContent(); - } else if (state is EditMealLoadedState) { - return _getLoadedContent(state.usesImperialUnits); - } + bloc: _editMealBloc..add(InitializeEditMealEvent()), + builder: (context, state) { + if (state is EditMealLoadingState) return _getLoadingContent(); + if (state is EditMealLoadedState) return _getLoadedContent(state.usesImperialUnits); return const SizedBox.shrink(); }, ), @@ -146,198 +122,78 @@ class _EditMealScreenState extends State { ); } - Widget _getLoadingContent() { - return const Center( - child: CircularProgressIndicator(), - ); - } + Widget _getLoadingContent() => const Center(child: CircularProgressIndicator()); Widget _getLoadedContent(bool usesImperialUnits) { - return ListView( - padding: const EdgeInsets.all(16), - children: [ - Center( + return Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + Center( child: ClipOval( - child: CachedNetworkImage( - cacheManager: locator(), - width: 120, - height: 120, - placeholder: (context, string) => const DefaultMealImage(), - errorWidget: (context, exception, stacktrace) => - const DefaultMealImage(), - fit: BoxFit.cover, - imageUrl: _mealEntity.mainImageUrl ?? "", + child: CachedNetworkImage( + cacheManager: locator(), + width: 120, height: 120, + placeholder: (ctx,url)=>const DefaultMealImage(), + errorWidget: (ctx,_,__)=>const DefaultMealImage(), + fit: BoxFit.cover, + imageUrl: _mealEntity.mainImageUrl ?? '', + ), + ), ), - )), - const SizedBox(height: 32), - TextFormField( - controller: _nameTextController, - decoration: InputDecoration( - labelText: S.of(context).mealNameLabel, - border: const OutlineInputBorder()), - keyboardType: TextInputType.text, - ), - const SizedBox(height: 16), - TextFormField( - controller: _brandsTextController, - decoration: InputDecoration( - labelText: S.of(context).mealBrandsLabel, - border: const OutlineInputBorder()), - keyboardType: TextInputType.text, - ), - const SizedBox(height: 32), - TextFormField( - controller: _mealQuantityTextController, - decoration: InputDecoration( - labelText: _usesImperialUnits - ? S.of(context).mealSizeLabelImperial - : S.of(context).mealSizeLabel, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _servingQuantityTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: _usesImperialUnits - ? S.of(context).servingSizeLabelImperial - : S.of(context).servingSizeLabelMetric, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - SegmentedButton( - segments: _mealUnitButtonSegment, - selected: {selectedUnit ?? _units[2]}, - onSelectionChanged: (Set newSelection) { - setState(() { - selectedUnit = newSelection.first; - }); - }, - ), - const SizedBox(height: 48), - TextFormField( - controller: _baseQuantityTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).baseQuantityLabel, - border: const OutlineInputBorder()), - keyboardType: TextInputType.number, - ), - const SizedBox(height: 48), - TextFormField( - controller: _kcalTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: - S.of(context).mealKcalLabel + baseQuantity + baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _carbsTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).mealCarbsLabel + - baseQuantity + - baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _fatTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: - S.of(context).mealFatLabel + baseQuantity + baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _proteinTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).mealProteinLabel + - baseQuantity + - baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - ], + const SizedBox(height:32), + TextFormField( + controller: _nameTextController, + decoration: InputDecoration(labelText: S.of(context).mealNameLabel, border: OutlineInputBorder()), + validator: (v){ if(!FoodNameValidator.isValid(v??'')) return 'O nome deve conter pelo menos uma letra'; return null; }, + ), + const SizedBox(height:16), + TextFormField(controller: _brandsTextController, decoration: InputDecoration(labelText: S.of(context).mealBrandsLabel, border: OutlineInputBorder())), + const SizedBox(height:32), + TextFormField(controller: _mealQuantityTextController, decoration: InputDecoration(labelText: usesImperialUnits?S.of(context).mealSizeLabelImperial:S.of(context).mealSizeLabel, border:OutlineInputBorder()), keyboardType: TextInputType.numberWithOptions(decimal:true)), + const SizedBox(height:16), + TextFormField(controller: _servingQuantityTextController,inputFormatters:CustomTextInputFormatter.doubleOnly(),decoration:InputDecoration(labelText:usesImperialUnits?S.of(context).servingSizeLabelImperial:S.of(context).servingSizeLabelMetric,border:OutlineInputBorder()),keyboardType:TextInputType.numberWithOptions(decimal:true)), + const SizedBox(height:16), + SegmentedButton(segments:_mealUnitButtonSegment,selected:{selectedUnit},onSelectionChanged:(s)=>setState(()=>selectedUnit=s.first)), + const SizedBox(height:48), + TextFormField(controller:_baseQuantityTextController,inputFormatters:CustomTextInputFormatter.doubleOnly(),decoration:InputDecoration(labelText:S.of(context).baseQuantityLabel,border:OutlineInputBorder()),keyboardType:TextInputType.number), + const SizedBox(height:48), + TextFormField(controller:_kcalTextController,inputFormatters:CustomTextInputFormatter.doubleOnly(),decoration:InputDecoration(labelText:S.of(context).mealKcalLabel+baseQuantity+baseQuantityUnit,border:OutlineInputBorder()),keyboardType:TextInputType.numberWithOptions(decimal:true)), + const SizedBox(height:16), + TextFormField(controller:_carbsTextController,inputFormatters:CustomTextInputFormatter.doubleOnly(),decoration:InputDecoration(labelText:S.of(context).mealCarbsLabel+baseQuantity+baseQuantityUnit,border:OutlineInputBorder()),keyboardType:TextInputType.numberWithOptions(decimal:true)), + const SizedBox(height:16), + TextFormField(controller:_fatTextController,inputFormatters:CustomTextInputFormatter.doubleOnly(),decoration:InputDecoration(labelText:S.of(context).mealFatLabel+baseQuantity+baseQuantityUnit,border:OutlineInputBorder()),keyboardType:TextInputType.numberWithOptions(decimal:true)), + const SizedBox(height:16), + TextFormField(controller:_proteinTextController,inputFormatters:CustomTextInputFormatter.doubleOnly(),decoration:InputDecoration(labelText:S.of(context).mealProteinLabel+baseQuantity+baseQuantityUnit,border:OutlineInputBorder()),keyboardType:TextInputType.numberWithOptions(decimal:true)), + ], + ), ); } - void _onSavePressed(bool usesImperialUnits) { + void _onSavePressed() { + if (!_formKey.currentState!.validate()) return; try { - // Convert meal size back to metric units if necessary - final mealQuantity = usesImperialUnits - ? _convertToMetric( - _mealQuantityTextController.text, _mealEntity.mealUnit ?? "0") - : _mealQuantityTextController.text; - - final newMealEntity = _editMealBloc.createNewMealEntity( - _mealEntity, - _nameTextController.text, - _brandsTextController.text, - mealQuantity, - _servingQuantityTextController.text, - _baseQuantityTextController.text, - selectedUnit, - _kcalTextController.text, - _carbsTextController.text, - _fatTextController.text, - _proteinTextController.text); - - Navigator.of(context).pushNamedAndRemoveUntil( - NavigationOptions.mealDetailRoute, - ModalRoute.withName(NavigationOptions.addMealRoute), - arguments: MealDetailScreenArguments( - newMealEntity, _intakeTypeEntity, _day, usesImperialUnits)); - } catch (exception, stacktrace) { - log.warning("Error while creating new meal entity"); - Sentry.captureException(exception, stackTrace: stacktrace); - - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(S.of(context).errorMealSave))); + final mealQty = _usesImperialUnits?_convertToMetric(_mealQuantityTextController.text,_mealEntity.mealUnit??'g'):_mealQuantityTextController.text; + final newMeal = _editMealBloc.createNewMealEntity(_mealEntity,_nameTextController.text,_brandsTextController.text,mealQty,_servingQuantityTextController.text,_baseQuantityTextController.text,selectedUnit,_kcalTextController.text,_carbsTextController.text,_fatTextController.text,_proteinTextController.text); + Navigator.of(context).pushNamedAndRemoveUntil(NavigationOptions.mealDetailRoute,ModalRoute.withName(NavigationOptions.addMealRoute),arguments:MealDetailScreenArguments(newMeal,_intakeTypeEntity,_day,_usesImperialUnits)); + } catch(e,st){ + log.warning('Error while creating new meal entity'); + Sentry.captureException(e,stackTrace:st); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text(S.of(context).errorMealSave))); } } - String? _switchButtonUnit(String? unit) { - String? selectedUnit; - if (!_units.contains(unit)) { - selectedUnit = _units[2]; // Default to g/ml - } else { - selectedUnit = unit; - } - return selectedUnit; - } + String _switchButtonUnit(String? unit)=>_units.contains(unit)?unit!:_units[2]; - String _convertToImperial(String value, String unit) { - final double quantityValue = double.tryParse(value) ?? 0.0; - switch (unit) { - case 'g': - return (UnitCalc.gToOz(quantityValue)).toStringAsFixed(2); - case 'ml': - return (UnitCalc.mlToFlOz(quantityValue)).toStringAsFixed(2); - default: - return value; - } + String _convertToImperial(String value,String unit){ + final q=double.tryParse(value)??0.0; + return unit=='g'?UnitCalc.gToOz(q).toStringAsFixed(2):unit=='ml'?UnitCalc.mlToFlOz(q).toStringAsFixed(2):value; } - String _convertToMetric(String value, String unit) { - final double quantityValue = double.tryParse(value) ?? 0.0; - switch (unit) { - case 'g': - return (UnitCalc.ozToG(quantityValue)).toStringAsFixed(2); - case 'ml': - return (UnitCalc.flOzToMl(quantityValue)).toStringAsFixed(2); - default: - return value; - } + String _convertToMetric(String value,String unit){ + final q=double.tryParse(value)??0.0; + return unit=='g'?UnitCalc.ozToG(q).toStringAsFixed(2):unit=='ml'?UnitCalc.flOzToMl(q).toStringAsFixed(2):value; } } @@ -346,7 +202,5 @@ class EditMealScreenArguments { final MealEntity mealEntity; final IntakeTypeEntity intakeTypeEntity; final bool usesImperialUnits; - - EditMealScreenArguments( - this.day, this.mealEntity, this.intakeTypeEntity, this.usesImperialUnits); + EditMealScreenArguments(this.day,this.mealEntity,this.intakeTypeEntity,this.usesImperialUnits); } diff --git a/lib/validation/food_name_validator.dart b/lib/validation/food_name_validator.dart new file mode 100644 index 000000000..b3d7cba37 --- /dev/null +++ b/lib/validation/food_name_validator.dart @@ -0,0 +1,7 @@ +class FoodNameValidator { + static bool isValid(String name) { + if (name.isEmpty) return false; + + return RegExp(r'[A-Za-z]').hasMatch(name); + } +} diff --git a/test/unit_test/food_name_validation_test.dart b/test/unit_test/food_name_validation_test.dart new file mode 100644 index 000000000..64c8acf53 --- /dev/null +++ b/test/unit_test/food_name_validation_test.dart @@ -0,0 +1,20 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:opennutritracker/validation/food_name_validator.dart'; + + +void main() { + group('FoodNameValidator', () { + test('falha quando o nome é só números', () { + expect(FoodNameValidator.isValid('123456'), isFalse); + }); + test('falha quando o nome é só símbolos', () { + expect(FoodNameValidator.isValid('@#\$%^'), isFalse); + }); + test('sucesso quando há pelo menos uma letra', () { + expect(FoodNameValidator.isValid('Maçã123'), isTrue); + }); + test('falha quando string vazia', () { + expect(FoodNameValidator.isValid(''), isFalse); + }); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 000000000..b712bff7f --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:opennutritracker/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}