From 8990fed1482e5707cbd590c1adae5a9e6f64f62d Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Sat, 16 Dec 2023 01:22:27 +0100 Subject: [PATCH 1/2] Support for '-' in nutritional values --- .../pages/product/add_basic_details_page.dart | 12 +- .../pages/product/add_other_details_page.dart | 6 +- .../nutrition_add_nutrient_button.dart | 4 +- .../pages/product/nutrition_page_loaded.dart | 213 ++++++++++++------ .../lib/pages/text_field_helper.dart | 21 +- 5 files changed, 176 insertions(+), 80 deletions(-) diff --git a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart index 3e745dfa4d4..97ad6c5399e 100644 --- a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart +++ b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart @@ -34,8 +34,8 @@ class AddBasicDetailsPage extends StatefulWidget { class _AddBasicDetailsPageState extends State { final TextEditingController _productNameController = TextEditingController(); - late final TextEditingControllerWithInitialValue _brandNameController; - late final TextEditingControllerWithInitialValue _weightController; + late final TextEditingControllerWithHistory _brandNameController; + late final TextEditingControllerWithHistory _weightController; final double _heightSpace = LARGE_SPACE; final GlobalKey _formKey = GlobalKey(); @@ -47,10 +47,10 @@ class _AddBasicDetailsPageState extends State { void initState() { super.initState(); _product = widget.product; - _weightController = TextEditingControllerWithInitialValue( + _weightController = TextEditingControllerWithHistory( text: MultilingualHelper.getCleanText(_product.quantity ?? ''), ); - _brandNameController = TextEditingControllerWithInitialValue( + _brandNameController = TextEditingControllerWithHistory( text: _formatProductBrands(_product.brands), ); _multilingualHelper = MultilingualHelper( @@ -230,11 +230,11 @@ class _AddBasicDetailsPageState extends State { Product getBasicProduct() => Product(barcode: _product.barcode); - if (_weightController.valueHasChanged) { + if (_weightController.isDifferentFromInitialValue) { result ??= getBasicProduct(); result.quantity = _weightController.text; } - if (_brandNameController.valueHasChanged) { + if (_brandNameController.isDifferentFromInitialValue) { result ??= getBasicProduct(); result.brands = _formatProductBrands(_brandNameController.text); } diff --git a/packages/smooth_app/lib/pages/product/add_other_details_page.dart b/packages/smooth_app/lib/pages/product/add_other_details_page.dart index e82eb340665..30191a1c661 100644 --- a/packages/smooth_app/lib/pages/product/add_other_details_page.dart +++ b/packages/smooth_app/lib/pages/product/add_other_details_page.dart @@ -25,7 +25,7 @@ class AddOtherDetailsPage extends StatefulWidget { } class _AddOtherDetailsPageState extends State { - late final TextEditingControllerWithInitialValue _websiteController; + late final TextEditingControllerWithHistory _websiteController; final double _heightSpace = LARGE_SPACE; final GlobalKey _formKey = GlobalKey(); @@ -36,7 +36,7 @@ class _AddOtherDetailsPageState extends State { super.initState(); _product = widget.product; _websiteController = - TextEditingControllerWithInitialValue(text: _product.website ?? ''); + TextEditingControllerWithHistory(text: _product.website ?? ''); } @override @@ -111,7 +111,7 @@ class _AddOtherDetailsPageState extends State { } /// Returns `true` if any value differs with initial state. - bool _isEdited() => _websiteController.valueHasChanged; + bool _isEdited() => _websiteController.isDifferentFromInitialValue; /// Exits the page if the [flag] is `true`. void _exitPage(final bool flag) { diff --git a/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart b/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart index b513ed827bf..90f368331d9 100644 --- a/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart +++ b/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart @@ -35,8 +35,8 @@ class NutritionAddNutrientButton extends StatelessWidget { a.name!.compareTo(b.name!)); List filteredList = List.from(leftovers); - final TextEditingControllerWithInitialValue nutritionTextController = - TextEditingControllerWithInitialValue(); + final TextEditingControllerWithHistory nutritionTextController = + TextEditingControllerWithHistory(); final ScrollController controller = ScrollController(); final OrderedNutrient? selected = await showDialog( 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 781c2c6a969..419ee87c1a3 100644 --- a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart +++ b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart @@ -87,9 +87,9 @@ class _NutritionPageLoadedState extends State late final NumberFormat _decimalNumberFormat; late final NutritionContainer _nutritionContainer; - final Map _controllers = - {}; - TextEditingControllerWithInitialValue? _servingController; + final Map _controllers = + {}; + TextEditingControllerWithHistory? _servingController; final GlobalKey _formKey = GlobalKey(); final List _focusNodes = []; @@ -110,7 +110,7 @@ class _NutritionPageLoadedState extends State void dispose() { _focusNodes.clear(); - for (final TextEditingControllerWithInitialValue controller + for (final TextEditingControllerWithHistory controller in _controllers.values) { controller.dispose(); } @@ -165,18 +165,20 @@ class _NutritionPageLoadedState extends State final Nutrient nutrient = _getNutrient(orderedNutrient); if (_controllers[nutrient] == null) { final double? value = _nutritionContainer.getValue(nutrient); - _controllers[nutrient] = TextEditingControllerWithInitialValue( + _controllers[nutrient] = TextEditingControllerWithHistory( text: value == null ? '' : _decimalNumberFormat.format(value), ); } children.add( - _NutrientRow( - _nutritionContainer, - _decimalNumberFormat, - _controllers[nutrient]!, - orderedNutrient, - i, + ChangeNotifierProvider.value( + value: _controllers[nutrient]!, + child: _NutrientRow( + _nutritionContainer, + _decimalNumberFormat, + orderedNutrient, + i, + ), ), ); } @@ -230,13 +232,12 @@ class _NutritionPageLoadedState extends State final String value = _nutritionContainer.servingSize; if (_servingController == null) { - _servingController = TextEditingControllerWithInitialValue(text: value); + _servingController = TextEditingControllerWithHistory(text: value); _servingController!.selection = TextSelection.collapsed(offset: _servingController!.text.length - 1); } - final TextEditingControllerWithInitialValue controller = - _servingController!; + final TextEditingControllerWithHistory controller = _servingController!; return Padding( padding: const EdgeInsetsDirectional.only(bottom: VERY_LARGE_SPACE), @@ -318,15 +319,18 @@ class _NutritionPageLoadedState extends State mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Switch( - value: _nutritionContainer.noNutritionData, - onChanged: (final bool value) => - setState(() => _nutritionContainer.noNutritionData = value), - trackColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.onPrimary), + Expanded( + flex: 2, + child: Switch( + value: _nutritionContainer.noNutritionData, + onChanged: (final bool value) => + setState(() => _nutritionContainer.noNutritionData = value), + trackColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.onPrimary), + ), ), - SizedBox( - width: _getColumnSize(context, 0.6), + Expanded( + flex: 6, child: AutoSizeText( localizations.nutrition_page_unspecified, style: Theme.of(context).primaryTextTheme.bodyMedium?.copyWith( @@ -342,12 +346,13 @@ class _NutritionPageLoadedState extends State /// Returns `true` if any value differs with initial state. bool _isEdited() { - if (_servingController != null && _servingController!.valueHasChanged) { + if (_servingController != null && + _servingController!.isDifferentFromInitialValue) { return true; } - for (final TextEditingControllerWithInitialValue controller + for (final TextEditingControllerWithHistory controller in _controllers.values) { - if (controller.valueHasChanged) { + if (controller.isDifferentFromInitialValue) { return true; } } @@ -359,7 +364,7 @@ class _NutritionPageLoadedState extends State return null; } for (final Nutrient nutrient in _controllers.keys) { - final TextEditingControllerWithInitialValue controller = + final TextEditingControllerWithHistory controller = _controllers[nutrient]!; _nutritionContainer.setNutrientValueText( nutrient, @@ -437,51 +442,50 @@ class _NutrientRow extends StatelessWidget { const _NutrientRow( this.nutritionContainer, this.decimalNumberFormat, - this.controller, this.orderedNutrient, this.position, ); final NutritionContainer nutritionContainer; final NumberFormat decimalNumberFormat; - final TextEditingControllerWithInitialValue controller; final OrderedNutrient orderedNutrient; final int position; @override - Widget build(BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: _NutrientValueCell( - decimalNumberFormat, - controller, - orderedNutrient, - position, - ), + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 6, + child: _NutrientValueCell( + decimalNumberFormat, + orderedNutrient, + position, ), - SizedBox( - width: _getColumnSize(context, 0.3), - child: _NutrientUnitCell( - nutritionContainer, - orderedNutrient, - ), + ), + Expanded( + flex: 3, + child: _NutrientUnitCell( + nutritionContainer, + orderedNutrient, ), - ], - ); + ), + const _NutrientUnitVisibility() + ], + ); + } } class _NutrientValueCell extends StatelessWidget { const _NutrientValueCell( this.decimalNumberFormat, - this.controller, this.orderedNutrient, this.position, ); final NumberFormat decimalNumberFormat; - final TextEditingControllerWithInitialValue controller; final OrderedNutrient orderedNutrient; final int position; @@ -491,11 +495,13 @@ class _NutrientValueCell extends StatelessWidget { context, listen: false, ); - + final TextEditingControllerWithHistory controller = + context.watch(); final bool isLast = position == focusNodes.length - 1; return TextFormField( controller: controller, + enabled: controller.isSet, focusNode: focusNodes[position], decoration: InputDecoration( enabledBorder: const UnderlineInputBorder(), @@ -550,16 +556,29 @@ class _NutrientUnitCellState extends State<_NutrientUnitCell> { Widget build(BuildContext context) { final Unit unit = widget.nutritionContainer.getUnit(_getNutrient(widget.orderedNutrient)); - return ElevatedButton( - onPressed: widget.nutritionContainer.isEditableWeight(unit) - ? () => setState( - () => widget.nutritionContainer - .setNextWeightUnit(widget.orderedNutrient), - ) - : null, - child: Text( - _getUnitLabel(unit), - style: const TextStyle(fontWeight: FontWeight.bold), + return Padding( + padding: const EdgeInsetsDirectional.only( + start: VERY_SMALL_SPACE, + end: SMALL_SPACE, + ), + child: _NutritionCellTextWatcher( + builder: (_, TextEditingControllerWithHistory controller) { + return ElevatedButton( + onPressed: controller.isNotSet + ? null + : widget.nutritionContainer.isEditableWeight(unit) + ? () => setState( + () => widget.nutritionContainer + .setNextWeightUnit(widget.orderedNutrient), + ) + : null, + child: Text( + _getUnitLabel(unit), + textAlign: TextAlign.center, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ); + }, ), ); } @@ -577,11 +596,40 @@ class _NutrientUnitCellState extends State<_NutrientUnitCell> { _unitLabels[unit] ?? UnitHelper.unitToString(unit)!; } -double _getColumnSize( - final BuildContext context, - final double adjustmentFactor, -) => - MediaQuery.of(context).size.width * adjustmentFactor; +class _NutrientUnitVisibility extends StatelessWidget { + const _NutrientUnitVisibility(); + + @override + Widget build(BuildContext context) { + return _NutritionCellTextWatcher( + builder: ( + BuildContext context, + TextEditingControllerWithHistory controller, + ) { + final bool isValueSet = !controller.isNotSet; + + return ElevatedButton( + onPressed: () { + if (isValueSet) { + controller.text = '-'; + } else { + if (controller.previousValue != '-') { + controller.text = controller.previousValue ?? '-'; + } else { + controller.text = ''; + } + } + }, + child: Icon( + isValueSet + ? Icons.visibility_rounded + : Icons.visibility_off_rounded, + ), + ); + }, + ); + } +} // cf. https://github.com/openfoodfacts/smooth-app/issues/3387 Nutrient _getNutrient(final OrderedNutrient orderedNutrient) { @@ -593,3 +641,38 @@ Nutrient _getNutrient(final OrderedNutrient orderedNutrient) { } throw Exception('unknown nutrient for "${orderedNutrient.id}"'); } + +extension _NutritionTextEditionController on TextEditingController { + bool get isSet => text.trim() != '-'; + + bool get isNotSet => text.trim() == '-'; +} + +/// Use this Widget to be notified when the value is set or not +class _NutritionCellTextWatcher extends StatelessWidget { + const _NutritionCellTextWatcher({ + required this.builder, + }); + + final Widget Function( + BuildContext context, + TextEditingControllerWithHistory value, + ) builder; + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, TextEditingControllerWithHistory controller) { + return controller; + }, + shouldRebuild: (_, TextEditingControllerWithHistory controller) { + return controller.isDifferentFromPreviousValue; + }, + builder: (BuildContext context, + TextEditingControllerWithHistory controller, _) { + return builder(context, controller); + }, + ); + } +} diff --git a/packages/smooth_app/lib/pages/text_field_helper.dart b/packages/smooth_app/lib/pages/text_field_helper.dart index 6e8e5e51750..d297ed9662a 100644 --- a/packages/smooth_app/lib/pages/text_field_helper.dart +++ b/packages/smooth_app/lib/pages/text_field_helper.dart @@ -1,14 +1,27 @@ import 'package:flutter/material.dart'; -/// A [TextEditingController] that saves the value passed to the constructor. -class TextEditingControllerWithInitialValue extends TextEditingController { - TextEditingControllerWithInitialValue({String? text}) +/// A [TextEditingController] that saves the value passed to the constructor +/// and persists the previous value. +class TextEditingControllerWithHistory extends TextEditingController { + TextEditingControllerWithHistory({String? text}) : _initialValue = text, + _previousValue = text, super(text: text); final String? _initialValue; + String? _previousValue; String? get initialValue => _initialValue; - bool get valueHasChanged => _initialValue != text; + String? get previousValue => _previousValue; + + bool get isDifferentFromInitialValue => _initialValue != text; + + bool get isDifferentFromPreviousValue => _previousValue != text; + + @override + set text(String newText) { + _previousValue = text; + super.text = newText; + } } From 38cc56f56389350a42c1c85c637d68ea7bf93828 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Sat, 16 Dec 2023 01:28:55 +0100 Subject: [PATCH 2/2] Minor improvement --- .../smooth_app/lib/pages/product/nutrition_page_loaded.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 419ee87c1a3..4faa9bee6db 100644 --- a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart +++ b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart @@ -606,7 +606,7 @@ class _NutrientUnitVisibility extends StatelessWidget { BuildContext context, TextEditingControllerWithHistory controller, ) { - final bool isValueSet = !controller.isNotSet; + final bool isValueSet = controller.isSet; return ElevatedButton( onPressed: () {