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"
This commit is contained in:
monsieurtanuki
2022-06-24 12:58:42 +02:00
committed by GitHub
parent a521305d02
commit 0f94837966
4 changed files with 135 additions and 117 deletions

View File

@ -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,

View File

@ -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"

View File

@ -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<OrderedNutrient> 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,
);

View File

@ -39,7 +39,7 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded> {
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<NutritionPageLoaded> {
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,52 +75,63 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded> {
@override
Widget build(BuildContext context) {
final AppLocalizations localizations = AppLocalizations.of(context);
final LocalDatabase localDatabase = context.read<LocalDatabase>();
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final List<Widget> children = <Widget>[];
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: <Widget>[
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: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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<NutritionPageLoaded> {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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<NutritionPageLoaded> {
label: Text(appLocalizations.nutrition_page_add_nutrient),
);
Future<bool> _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<bool>(
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<void> _validateAndSave(final AppLocalizations localizations,
final LocalDatabase localDatabase) async {
if (!_formKey.currentState!.validate()) {
return;
}
await _showSavePopup(localizations, localDatabase);
}
Future<void> _showSavePopup(
final AppLocalizations appLocalizations,
final LocalDatabase localDatabase,
) async {
final bool shouldSave = await showDialog<bool>(
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<void> _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)) !=
if (_nutritionContainer.getValue(key) == null) {
if (controller.value.text != '') {
return true;
}
} else 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
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<bool> _mayExitPage({required final bool saving}) async {
if (!_isEdited()) {
return true;
}
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final LocalDatabase localDatabase = context.read<LocalDatabase>();
if (!saving) {
final bool? pleaseSave = await showDialog<bool>(
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,
);
}
}