From 0f94837966e4b34d9380c9f432bcd37b686f247a Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 24 Jun 2022 12:58:42 +0200 Subject: [PATCH] fix: #1700 - nutrients - refactored "save" action as in SimpleInputPage (#2338) Impacted files: * `app_en.arb`: removed 2 labels not used anymore * `nutrition_container.dart`: added "no nutrition data" * `nutrition_page_loaded.dart`: refactored the "save" action as in `SimpleInputPage`; added "no nutrition data"; just trying - remove the status bar recoloring; added a save/cancel bottom bar; fixed a bug in `_isEdited` * `product_query.dart`: added field "no nutrition data" --- .../lib/database/product_query.dart | 1 + packages/smooth_app/lib/l10n/app_en.arb | 2 - .../pages/product/nutrition_container.dart | 4 + .../pages/product/nutrition_page_loaded.dart | 245 ++++++++++-------- 4 files changed, 135 insertions(+), 117 deletions(-) diff --git a/packages/smooth_app/lib/database/product_query.dart b/packages/smooth_app/lib/database/product_query.dart index 02b7944ff9..d8e7a3b6d4 100644 --- a/packages/smooth_app/lib/database/product_query.dart +++ b/packages/smooth_app/lib/database/product_query.dart @@ -99,6 +99,7 @@ abstract class ProductQuery { ProductField.SERVING_SIZE, ProductField.STORES, ProductField.PACKAGING_QUANTITY, + ProductField.NO_NUTRITION_DATA, ProductField.NUTRIMENTS, ProductField.NUTRIENT_LEVELS, ProductField.NUTRIMENT_ENERGY_UNIT, diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index fcd52efbad..b1cf243a2e 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -44,7 +44,6 @@ "@label_web": {}, "learnMore": "Learn more", "@learnMore": {}, - "general_confirmation": "Are you sure?", "unknown": "Unknown", "@unknown": { "description": "Short label for product list view: the compatibility of that product with your preferences is unknown" @@ -423,7 +422,6 @@ }, "nutrition": "Nutrition", "@nutrition": {}, - "nutrition_page_close_confirmation": "Are you sure you want to close without saving?", "nutrition_facts_photo": "Nutrition facts photo", "@nutrition_facts_photo": { "description": "Button label: For adding a picture of the nutrition facts of a product" diff --git a/packages/smooth_app/lib/pages/product/nutrition_container.dart b/packages/smooth_app/lib/pages/product/nutrition_container.dart index 730702caa6..0ef5c78705 100644 --- a/packages/smooth_app/lib/pages/product/nutrition_container.dart +++ b/packages/smooth_app/lib/pages/product/nutrition_container.dart @@ -19,6 +19,7 @@ class NutritionContainer { } _servingSize = product.servingSize; _barcode = product.barcode!; + noNutritionData = product.noNutritionData ?? false; } static const String _energyId = 'energy'; @@ -66,6 +67,8 @@ class NutritionContainer { late final String _barcode; + late bool noNutritionData; + /// Returns the not interesting nutrients, for a "Please add me!" list. Iterable getLeftoverNutrients() => _nutrients.where( (final OrderedNutrient element) => _isNotRelevant(element), @@ -90,6 +93,7 @@ class NutritionContainer { /// Returns a [Product] with only nutrients data. Product getProduct() => Product( barcode: _barcode, + noNutritionData: noNutritionData, nutriments: _getNutriments(), servingSize: _servingSize, ); diff --git a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart index b345dfe1c4..49bf1ce970 100644 --- a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart +++ b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart @@ -39,7 +39,7 @@ class _NutritionPageLoadedState extends State { late final NumberFormat _numberFormat; late final NutritionContainer _nutritionContainer; - bool _unspecified = false; // TODO(monsieurtanuki): fetch that data from API? + late bool _noNutritionData; // If true then serving, if false then 100g. bool _servingOr100g = false; @@ -60,11 +60,9 @@ class _NutritionPageLoadedState extends State { void initState() { super.initState(); _product = widget.product; - _nutritionContainer = NutritionContainer( - orderedNutrients: widget.orderedNutrients, - product: _product, - ); + _nutritionContainer = _getFreshContainer(); _numberFormat = NumberFormat('####0.#####', ProductQuery.getLocaleString()); + _noNutritionData = _product.noNutritionData ?? false; } @override @@ -77,50 +75,61 @@ class _NutritionPageLoadedState extends State { @override Widget build(BuildContext context) { - final AppLocalizations localizations = AppLocalizations.of(context); - final LocalDatabase localDatabase = context.read(); + final AppLocalizations appLocalizations = AppLocalizations.of(context); final List children = []; - children.add(_switchNoNutrition(localizations)); - if (!_unspecified) { - children.add(_getServingField(localizations)); - children.add(_getServingSwitch(localizations)); + children.add(_switchNoNutrition(appLocalizations)); + if (!_noNutritionData) { + children.add(_getServingField(appLocalizations)); + children.add(_getServingSwitch(appLocalizations)); for (final OrderedNutrient orderedNutrient in _nutritionContainer.getDisplayableNutrients()) { children.add( - _getNutrientRow(localizations, orderedNutrient), + _getNutrientRow(appLocalizations, orderedNutrient), ); } - children.add(_addNutrientButton(localizations)); + children.add(_addNutrientButton(appLocalizations)); } return WillPopScope( - //return a boolean to decide whether to return to previous page or not - onWillPop: () => _showCancelPopup(localizations), + onWillPop: () async => _mayExitPage(saving: false), child: Scaffold( appBar: AppBar( title: AutoSizeText( - localizations.nutrition_page_title, + appLocalizations.nutrition_page_title, maxLines: 2, ), - actions: [ - IconButton( - onPressed: () => _validateAndSave(localizations, localDatabase), - icon: const Icon(Icons.check), - ) - ], - systemOverlayStyle: const SystemUiOverlayStyle( - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - ), ), body: Padding( padding: const EdgeInsets.symmetric( horizontal: LARGE_SPACE, vertical: SMALL_SPACE, ), - child: Form( - key: _formKey, - child: ListView(children: children), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 1, + child: Form( + key: _formKey, + child: ListView(children: children), + ), + ), + SmoothActionButtonsBar( + positiveAction: SmoothActionButton( + text: appLocalizations.save, + onPressed: () async => _exitPage( + await _mayExitPage(saving: true), + ), + ), + negativeAction: SmoothActionButton( + text: appLocalizations.cancel, + onPressed: () async => _exitPage( + await _mayExitPage(saving: false), + ), + ), + ), + ], ), ), ), @@ -268,9 +277,9 @@ class _NutritionPageLoadedState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Switch( - value: _unspecified, + value: _noNutritionData, onChanged: (final bool value) => - setState(() => _unspecified = !_unspecified), + setState(() => _noNutritionData = !_noNutritionData), trackColor: MaterialStateProperty.all( Theme.of(context).colorScheme.onPrimary), ), @@ -367,98 +376,104 @@ class _NutritionPageLoadedState extends State { label: Text(appLocalizations.nutrition_page_add_nutrient), ); - Future _showCancelPopup(final AppLocalizations appLocalizations) async { - //if no changes made then returns true to the onWillPop - // allowing it to let the user return back to previous screen - if (!_isEdited()) { - return true; - } - return await showDialog( - context: context, - builder: (BuildContext context) => SmoothAlertDialog( - title: appLocalizations.general_confirmation, - body: Text(appLocalizations.nutrition_page_close_confirmation), - negativeAction: SmoothActionButton( - text: appLocalizations.cancel, - // returns false to onWillPop after the alert dialog is closed with cancel button - //blocking return to the previous screen - onPressed: () => Navigator.pop(context), - ), - positiveAction: SmoothActionButton( - text: appLocalizations.okay, - // returns true to onWillPop after the alert dialog is closed with close button - //letting return to the previous screen - onPressed: () => Navigator.pop(context, true), - ), - ), - ) ?? - // in case alert dialog is closed, a false is return - // blocking the return to the previous screen - false; - } - - Future _validateAndSave(final AppLocalizations localizations, - final LocalDatabase localDatabase) async { - if (!_formKey.currentState!.validate()) { - return; - } - - await _showSavePopup(localizations, localDatabase); - } - - Future _showSavePopup( - final AppLocalizations appLocalizations, - final LocalDatabase localDatabase, - ) async { - final bool shouldSave = await showDialog( - context: context, - builder: (BuildContext context) => SmoothAlertDialog( - title: appLocalizations.general_confirmation, - body: Text(appLocalizations.save_confirmation), - negativeAction: SmoothActionButton( - text: appLocalizations.cancel, - onPressed: () => Navigator.pop(context, false), - ), - positiveAction: SmoothActionButton( - text: appLocalizations.save.toUpperCase(), - onPressed: () => Navigator.pop(context, true), - ), - ), - ) ?? - false; - - if (shouldSave) { - await _save(localDatabase); - } - } - - Future _save(final LocalDatabase localDatabase) async { - for (final String key in _controllers.keys) { - final TextEditingController controller = _controllers[key]!; - _nutritionContainer.setControllerText(key, controller.text); - } - // minimal product: we only want to save the nutrients - final Product inputProduct = _nutritionContainer.getProduct(); - await ProductRefresher().saveAndRefresh( - context: context, - localDatabase: localDatabase, - product: inputProduct, - ); - } - + /// Returns `true` if any value differs between form and container. bool _isEdited() { for (final String key in _controllers.keys) { final TextEditingController controller = _controllers[key]!; - if (_nutritionContainer.getValue(key) != null) { - if (_numberFormat.format(_nutritionContainer.getValue(key)) != - controller.value.text) { - //if any controller is not equal to the value in the container - // then the form is edited, return true + if (_nutritionContainer.getValue(key) == null) { + if (controller.value.text != '') { return true; } + } else if (_numberFormat.format(_nutritionContainer.getValue(key)) != + controller.value.text) { + return true; } } + if (_nutritionContainer.noNutritionData != _noNutritionData) { + return true; + } //else form is not edited just return false return false; } + + Product? _getChangedProduct() { + if (!_formKey.currentState!.validate()) { + return null; + } + // We use a separate fresh container here. + // If something breaks while saving, we won't get a half written object. + final NutritionContainer output = _getFreshContainer(); + for (final String key in _controllers.keys) { + final TextEditingController controller = _controllers[key]!; + output.setControllerText(key, controller.text); + } + output.noNutritionData = _noNutritionData; + return output.getProduct(); + } + + NutritionContainer _getFreshContainer() => NutritionContainer( + orderedNutrients: widget.orderedNutrients, + product: _product, + ); + + /// Exits the page if the [flag] is `true`. + void _exitPage(final bool flag) { + if (flag) { + Navigator.of(context).pop(); + } + } + + /// Returns `true` if we should really exit the page. + /// + /// Parameter [saving] tells about the context: are we leaving the page, + /// or have we clicked on the "save" button? + Future _mayExitPage({required final bool saving}) async { + if (!_isEdited()) { + return true; + } + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final LocalDatabase localDatabase = context.read(); + if (!saving) { + final bool? pleaseSave = await showDialog( + context: context, + builder: (final BuildContext context) => SmoothAlertDialog( + close: true, + body: Text(appLocalizations.edit_product_form_item_exit_confirmation), + title: appLocalizations.nutrition_page_title, + negativeAction: SmoothActionButton( + text: appLocalizations.ignore, + onPressed: () => Navigator.pop(context, false), + ), + positiveAction: SmoothActionButton( + text: appLocalizations.save, + onPressed: () => Navigator.pop(context, true), + ), + ), + ); + if (pleaseSave == null) { + return false; + } + if (pleaseSave == false) { + return true; + } + } + final Product? changedProduct = _getChangedProduct(); + if (changedProduct == null) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + // here I cheat and I reuse the only invalid case. + content: Text(appLocalizations.nutrition_page_invalid_number), + ), + ); + } + return false; + } + // if it fails, we stay on the same page + return ProductRefresher().saveAndRefresh( + context: context, + localDatabase: localDatabase, + product: changedProduct, + ); + } }