Skip to content

Commit

Permalink
feat: 5430 - "producer provided" icon for nutrients and 4 product fie…
Browse files Browse the repository at this point in the history
…lds (openfoodfacts#5777)

* feat: 5430 - "producer provided" icon for nutrients and 4 product fields

Impacted files:
* `add_basic_details_page.dart`: display "producer provided" icon for name, brands and quantity
* `nutrition_page_loaded.dart`: display "producer provided" icon for serving size and each individual nutrient
* `product_query.dart`: set the icon to display when a product field value is "producer provided"
* `smooth_autocomplete_text_field.dart`: added parameter `suffixIcon`
* `smooth_text_form_field.dart`: added parameter `suffixIcon`

* Minor code cleaning

* Minor code cleaning

* Standard info tile about "owner fields"

New file:
* `owner_field_info.dart`: Standard info tile about "owner fields".

Impacted files:
* `add_basic_details_page.dart`: now displaying `OwnerFieldInfo` if relevant; minor refactoring
* `nutrition_page_loaded.dart`: now displaying `OwnerFieldInfo` if relevant; minor refactoring
* `product_query.dart`: moved field to new file `owner_field_info.dart`

* translations and semantics

Impacted files:
* add_basic_details_page.dart: added semantics
* app_en.arb: 2 new labels for "owner field info"
* nutrition_page_loaded.dart: added semantics
* owner_field_info.dart: translated labels

* typo fix
  • Loading branch information
monsieurtanuki authored Nov 11, 2024
1 parent 0587050 commit 45b7e7b
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class SmoothTextFormField extends StatefulWidget {
required this.hintText,
this.hintTextFontSize,
this.prefixIcon,
this.suffixIcon,
this.textInputType,
this.onChanged,
this.onFieldSubmitted,
Expand All @@ -34,6 +35,7 @@ class SmoothTextFormField extends StatefulWidget {
final TextEditingController? controller;
final String hintText;
final Widget? prefixIcon;
final Widget? suffixIcon;
final bool? enabled;
final TextInputAction? textInputAction;
final String? Function(String?)? validator;
Expand Down Expand Up @@ -121,18 +123,19 @@ class _SmoothTextFormFieldState extends State<SmoothTextFormField> {
width: 5.0,
),
),
suffixIcon: widget.type == TextFieldTypes.PASSWORD
? IconButton(
tooltip: appLocalization.show_password,
splashRadius: 10.0,
onPressed: () => setState(() {
_obscureText = !_obscureText;
}),
icon: _obscureText
? const Icon(Icons.visibility_off)
: const Icon(Icons.visibility),
)
: null,
suffixIcon: widget.suffixIcon ??
(widget.type == TextFieldTypes.PASSWORD
? IconButton(
tooltip: appLocalization.show_password,
splashRadius: 10.0,
onPressed: () => setState(() {
_obscureText = !_obscureText;
}),
icon: _obscureText
? const Icon(Icons.visibility_off)
: const Icon(Icons.visibility),
)
: null),
errorMaxLines: 2,
),
);
Expand Down
8 changes: 8 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2587,6 +2587,14 @@
"app_rating_dialog_title_enjoying_positive_actions": "Yeah!",
"not_really": "Not really",
"app_rating_dialog_title_not_enjoying_app": "We are so sorry to hear that! Could you tell us what happened?",
"owner_field_info_title": "Producer provided values",
"@owner_field_info_title": {
"description": "Title of the 'producer provided' info list-tile"
},
"owner_field_info_message": "With that logo we highlight data provided by the producer, and that may not be editable.",
"@owner_field_info_message": {
"description": "Title of the 'producer provided' info list-tile"
},
"edit_packagings_title": "Packaging components",
"@edit_packagings_title": {
"description": "Title of the structured packagings page"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class SmoothAutocompleteTextField extends StatefulWidget {
required this.manager,
this.minLengthForSuggestions = 1,
this.allowEmojis = true,
this.suffixIcon,
});

final FocusNode focusNode;
Expand All @@ -29,6 +30,7 @@ class SmoothAutocompleteTextField extends StatefulWidget {
final int minLengthForSuggestions;
final AutocompleteManager? manager;
final bool allowEmojis;
final Widget? suffixIcon;

@override
State<SmoothAutocompleteTextField> createState() =>
Expand Down Expand Up @@ -82,6 +84,7 @@ class _SmoothAutocompleteTextFieldState
FilteringTextInputFormatter.deny(TextHelper.emojiRegex),
],
decoration: InputDecoration(
suffixIcon: widget.suffixIcon,
filled: true,
border: const OutlineInputBorder(
borderRadius: ANGULAR_BORDER_RADIUS,
Expand Down
64 changes: 62 additions & 2 deletions packages/smooth_app/lib/pages/product/add_basic_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:smooth_app/pages/product/common/product_buttons.dart';
import 'package:smooth_app/pages/product/common/product_refresher.dart';
import 'package:smooth_app/pages/product/may_exit_page_helper.dart';
import 'package:smooth_app/pages/product/multilingual_helper.dart';
import 'package:smooth_app/pages/product/owner_field_info.dart';
import 'package:smooth_app/pages/text_field_helper.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';
Expand Down Expand Up @@ -99,7 +100,7 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
appBar: buildEditProductAppBar(
context: context,
title: appLocalizations.basic_details,
product: widget.product,
product: _product,
),
body: Form(
key: _formKey,
Expand Down Expand Up @@ -142,6 +143,9 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
Widget? child) {
if (_multilingualHelper.isMonolingual()) {
return SmoothTextFormField(
suffixIcon: _getOwnerFieldIcon(
ProductField.NAME,
),
controller: _productNameController,
type: TextFieldTypes.PLAIN_TEXT,
hintText: appLocalizations.product_name,
Expand All @@ -164,6 +168,11 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
Padding(
padding: const EdgeInsets.all(8.0),
child: SmoothTextFormField(
suffixIcon: _getOwnerFieldIcon(
ProductField.NAME_IN_LANGUAGES,
language: _multilingualHelper
.getCurrentLanguage(),
),
controller: _productNameController,
type: TextFieldTypes.PLAIN_TEXT,
hintText: appLocalizations.product_name,
Expand Down Expand Up @@ -197,6 +206,9 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
allowEmojis: false,
hintText: appLocalizations.brand_name,
constraints: constraints,
suffixIcon: _getOwnerFieldIcon(
ProductField.BRANDS,
),
manager: AutocompleteManager(
TaxonomyNameAutocompleter(
taxonomyNames: <TaxonomyName>[
Expand All @@ -208,18 +220,26 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
limit: 25,
fuzziness: Fuzziness.none,
uriHelper: ProductQuery.getUriProductHelper(
productType: widget.product.productType,
productType: _product.productType,
),
),
),
),
),
SizedBox(height: _heightSpace),
SmoothTextFormField(
suffixIcon: _getOwnerFieldIcon(
ProductField.QUANTITY,
),
controller: _weightController,
type: TextFieldTypes.PLAIN_TEXT,
hintText: appLocalizations.quantity,
),
if (_hasOwnerField())
const Padding(
padding: EdgeInsets.only(top: LARGE_SPACE),
child: Card(child: OwnerFieldInfo()),
),
// in order to be able to scroll suggestions
SizedBox(height: MediaQuery.sizeOf(context).height),
],
Expand Down Expand Up @@ -332,4 +352,44 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
}
return result;
}

Widget? _getOwnerFieldIcon(
final ProductField productField, {
final OpenFoodFactsLanguage? language,
}) =>
_isOwnerField(productField, language: language)
? Semantics(
label: AppLocalizations.of(context).owner_field_info_title,
child: const Icon(OwnerFieldInfo.ownerFieldIconData),
)
: null;

bool _hasOwnerField() {
if (_multilingualHelper.isMonolingual()) {
if (_isOwnerField(ProductField.NAME)) {
return true;
}
} else {
if (_isOwnerField(
ProductField.NAME_IN_LANGUAGES,
language: _multilingualHelper.getCurrentLanguage(),
)) {
return true;
}
}
return _isOwnerField(ProductField.BRANDS) ||
_isOwnerField(ProductField.QUANTITY);
}

bool _isOwnerField(
final ProductField productField, {
final OpenFoodFactsLanguage? language,
}) =>
_product.getOwnerFieldTimestamp(
OwnerField.productField(
productField,
language ?? ProductQuery.getLanguage(),
),
) !=
null;
}
66 changes: 62 additions & 4 deletions packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import 'package:smooth_app/pages/product/may_exit_page_helper.dart';
import 'package:smooth_app/pages/product/nutrition_add_nutrient_button.dart';
import 'package:smooth_app/pages/product/nutrition_container.dart';
import 'package:smooth_app/pages/product/ordered_nutrients_cache.dart';
import 'package:smooth_app/pages/product/owner_field_info.dart';
import 'package:smooth_app/pages/product/simple_input_number_field.dart';
import 'package:smooth_app/pages/text_field_helper.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';
import 'package:smooth_app/widgets/will_pop_scope.dart';

Expand Down Expand Up @@ -130,6 +132,9 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded>
children.add(_switchNoNutrition(appLocalizations));

if (!_nutritionContainer.noNutritionData) {
final Iterable<OrderedNutrient> displayableNutrients =
_nutritionContainer.getDisplayableNutrients();

children.add(
Padding(
padding: const EdgeInsets.symmetric(vertical: MEDIUM_SPACE),
Expand All @@ -140,12 +145,13 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded>
),
),
);
if (_hasOwnerField(displayableNutrients)) {
children.add(const OwnerFieldInfo());
}

children.add(_getServingField(appLocalizations));
children.add(_getServingSwitch(appLocalizations));

final Iterable<OrderedNutrient> displayableNutrients =
_nutritionContainer.getDisplayableNutrients();

if (_focusNodes.length != displayableNutrients.length) {
_focusNodes.clear();
_focusNodes.addAll(
Expand Down Expand Up @@ -177,6 +183,7 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded>
_decimalNumberFormat,
orderedNutrient,
i,
upToDateProduct,
),
),
);
Expand Down Expand Up @@ -225,6 +232,30 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded>
);
}

bool _hasOwnerField(
final Iterable<OrderedNutrient> displayableNutrients,
) {
if (upToDateProduct.getOwnerFieldTimestamp(
OwnerField.productField(
ProductField.SERVING_SIZE,
ProductQuery.getLanguage(),
),
) !=
null) {
return true;
}
for (final OrderedNutrient orderedNutrient in displayableNutrients) {
final Nutrient nutrient = _getNutrient(orderedNutrient);
if (upToDateProduct.getOwnerFieldTimestamp(
OwnerField.nutrient(nutrient),
) !=
null) {
return true;
}
}
return false;
}

Widget _getServingField(final AppLocalizations appLocalizations) {
final String value = _nutritionContainer.servingSize;

Expand All @@ -245,6 +276,18 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded>
decoration: InputDecoration(
enabledBorder: const UnderlineInputBorder(),
labelText: appLocalizations.nutrition_page_serving_size,
suffixIcon: widget.product.getOwnerFieldTimestamp(
OwnerField.productField(
ProductField.SERVING_SIZE,
ProductQuery.getLanguage(),
),
) ==
null
? null
: Semantics(
label: appLocalizations.owner_field_info_title,
child: const Icon(OwnerFieldInfo.ownerFieldIconData),
),
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
Expand Down Expand Up @@ -442,12 +485,14 @@ class _NutrientRow extends StatelessWidget {
this.decimalNumberFormat,
this.orderedNutrient,
this.position,
this.product,
);

final NutritionContainer nutritionContainer;
final NumberFormat decimalNumberFormat;
final OrderedNutrient orderedNutrient;
final int position;
final Product product;

@override
Widget build(BuildContext context) {
Expand All @@ -465,6 +510,7 @@ class _NutrientRow extends StatelessWidget {
decimalNumberFormat,
orderedNutrient,
position,
product,
),
),
),
Expand Down Expand Up @@ -492,21 +538,25 @@ class _NutrientValueCell extends StatelessWidget {
this.decimalNumberFormat,
this.orderedNutrient,
this.position,
this.product,
);

final NumberFormat decimalNumberFormat;
final OrderedNutrient orderedNutrient;
final int position;
final Product product;

@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final List<FocusNode> focusNodes = Provider.of<List<FocusNode>>(
context,
listen: false,
);
final TextEditingControllerWithHistory controller =
context.watch<TextEditingControllerWithHistory>();
final bool isLast = position == focusNodes.length - 1;
final Nutrient? nutrient = orderedNutrient.nutrient;

return TextFormField(
controller: controller,
Expand All @@ -515,6 +565,14 @@ class _NutrientValueCell extends StatelessWidget {
decoration: InputDecoration(
enabledBorder: const UnderlineInputBorder(),
labelText: orderedNutrient.name,
suffixIcon: nutrient == null ||
product.getOwnerFieldTimestamp(OwnerField.nutrient(nutrient)) ==
null
? null
: Semantics(
label: appLocalizations.owner_field_info_title,
child: const Icon(OwnerFieldInfo.ownerFieldIconData),
),
),
keyboardType: const TextInputType.numberWithOptions(
signed: false,
Expand All @@ -540,7 +598,7 @@ class _NutrientValueCell extends StatelessWidget {
decimalNumberFormat.parse(value);
return null;
} catch (e) {
return AppLocalizations.of(context).nutrition_page_invalid_number;
return appLocalizations.nutrition_page_invalid_number;
}
},
);
Expand Down
Loading

0 comments on commit 45b7e7b

Please sign in to comment.