diff --git a/packages/smooth_app/lib/generic_lib/widgets/svg_icon.dart b/packages/smooth_app/lib/generic_lib/widgets/svg_icon.dart new file mode 100644 index 00000000000..240b827af40 --- /dev/null +++ b/packages/smooth_app/lib/generic_lib/widgets/svg_icon.dart @@ -0,0 +1,41 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/helpers/app_helper.dart'; + +/// SVG that looks like a ListTile icon. +class SvgIcon extends StatelessWidget { + const SvgIcon(this.assetName, {this.dontAddColor = false}); + + final String assetName; + final bool dontAddColor; + + @override + Widget build(BuildContext context) => SvgPicture.asset( + assetName, + height: DEFAULT_ICON_SIZE, + width: DEFAULT_ICON_SIZE, + colorFilter: dontAddColor + ? null + : ui.ColorFilter.mode( + _iconColor(Theme.of(context)), + ui.BlendMode.srcIn, + ), + package: AppHelper.APP_PACKAGE, + ); + + /// Returns the standard icon color in a [ListTile]. + /// + /// Simplified version from [ListTile], which was anyway not kind enough + /// to make it public. + Color _iconColor(ThemeData theme) { + switch (theme.brightness) { + case Brightness.light: + return Colors.black45; + case Brightness.dark: + return Colors.white; + } + } +} diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 00650ce193f..70321e25fdd 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1357,6 +1357,10 @@ "@edit_product_form_save": { "description": "Product edition - Nutrition facts - Save button" }, + "no_data_available": "No data available", + "@no_data_available": { + "description": "When there are no data to display" + }, "product_field_website_title": "Website", "@product_field_website_title": { "description": "Title of a product field: website" diff --git a/packages/smooth_app/lib/pages/product/attribute_first_row_helper.dart b/packages/smooth_app/lib/pages/product/attribute_first_row_helper.dart new file mode 100644 index 00000000000..d4ea4d7b6c0 --- /dev/null +++ b/packages/smooth_app/lib/pages/product/attribute_first_row_helper.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/generic_lib/widgets/svg_icon.dart'; +import 'package:smooth_app/helpers/analytics_helper.dart'; +import 'package:smooth_app/pages/product/common/product_refresher.dart'; +import 'package:smooth_app/pages/product/nutrition_page_loaded.dart'; +import 'package:smooth_app/pages/product/product_field_editor.dart'; +import 'package:smooth_app/pages/product/simple_input_page_helpers.dart'; + +class StringPair { + const StringPair({ + required this.first, + this.second, + }); + + final String first; + final String? second; +} + +abstract class AttributeFirstRowHelper { + List getAllTerms(); + + Widget? getLeadingIcon(); + + String getTitle(BuildContext context); + + Future onTap({ + required BuildContext context, + }); +} + +class AttributeFirstRowSimpleHelper extends AttributeFirstRowHelper { + AttributeFirstRowSimpleHelper({ + required this.helper, + }); + + final AbstractSimpleInputPageHelper helper; + + @override + List getAllTerms() { + final List allTerms = []; + + for (final String element in helper.terms) { + allTerms.add( + StringPair( + first: element, + ), + ); + } + + return allTerms; + } + + @override + Widget? getLeadingIcon() { + return helper.getIcon(); + } + + @override + String getTitle(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return helper.getTitle( + appLocalizations, + ); + } + + @override + Future onTap({ + required BuildContext context, + }) { + return ProductFieldSimpleEditor(helper).edit( + context: context, + product: helper.product, + ); + } +} + +class AttributeFirstRowNutritionHelper extends AttributeFirstRowHelper { + AttributeFirstRowNutritionHelper({ + required this.product, + }); + + final Product product; + + @override + List getAllTerms() { + final List allNutrients = []; + product.nutriments?.toData().forEach( + (String nutrientName, String quantity) { + allNutrients.add( + StringPair( + first: nutrientName.split('_100g')[0], + second: quantity, + ), + ); + }, + ); + + return allNutrients; + } + + @override + Widget? getLeadingIcon() { + return const SvgIcon( + 'assets/cacheTintable/scale-balance.svg', + dontAddColor: true, + ); + } + + @override + String getTitle(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return appLocalizations.nutrition_page_title; + } + + @override + Future onTap({ + required BuildContext context, + }) async { + if (!await ProductRefresher().checkIfLoggedIn( + context, + isLoggedInMandatory: true, + )) { + return; + } + + AnalyticsHelper.trackProductEdit( + AnalyticsEditEvents.nutrition_Facts, + product.barcode!, + ); + + if (!context.mounted) { + return; + } + + await NutritionPageLoaded.showNutritionPage( + product: product, + isLoggedInMandatory: true, + context: context, + ); + } +} + +class AttributeFirstRowIngredientsHelper extends AttributeFirstRowHelper { + AttributeFirstRowIngredientsHelper({ + required this.product, + }); + + final Product product; + + @override + List getAllTerms() { + final List allIngredients = []; + product.ingredients?.forEach( + (Ingredient element) { + if (element.text != null) { + allIngredients.add( + StringPair( + first: element.text!, + ), + ); + } + }, + ); + + return allIngredients; + } + + @override + Widget? getLeadingIcon() { + return const SvgIcon( + 'assets/cacheTintable/ingredients.svg', + dontAddColor: true, + ); + } + + @override + String getTitle(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return appLocalizations.ingredients; + } + + @override + Future onTap({ + required BuildContext context, + }) { + return ProductFieldOcrIngredientEditor().edit( + context: context, + product: product, + ); + } +} diff --git a/packages/smooth_app/lib/pages/product/attribute_first_row_widget.dart b/packages/smooth_app/lib/pages/product/attribute_first_row_widget.dart new file mode 100644 index 00000000000..ffd2c04eee0 --- /dev/null +++ b/packages/smooth_app/lib/pages/product/attribute_first_row_widget.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:smooth_app/pages/product/attribute_first_row_helper.dart'; + +class AttributeFirstRowWidget extends StatefulWidget { + const AttributeFirstRowWidget({ + required this.helper, + }); + + final AttributeFirstRowHelper helper; + + @override + State createState() => + _AttributeFirstRowWidgetState(); +} + +class _AttributeFirstRowWidgetState extends State { + bool _showAllTerms = false; + late final List allTerms; + + @override + void initState() { + super.initState(); + allTerms = widget.helper.getAllTerms(); + } + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final ThemeData theme = Theme.of(context); + const int numberThreshold = 4; + final bool hasManyTerms = allTerms.length > numberThreshold; + final List firstTerms = allTerms + .take( + numberThreshold, + ) + .toList(); + + if (firstTerms.isEmpty) { + firstTerms.add( + StringPair(first: appLocalizations.no_data_available), + ); + } + return Column( + children: [ + ListTile( + leading: widget.helper.getLeadingIcon(), + title: Text( + widget.helper.getTitle(context), + ), + trailing: const Icon( + Icons.edit, + ), + titleTextStyle: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 20.0, + color: theme.primaryColor, + ), + iconColor: theme.primaryColor, + tileColor: theme.colorScheme.secondary, + onTap: () async => widget.helper.onTap(context: context), + ), + _termsList( + _showAllTerms ? allTerms : firstTerms, + borderFlag: !hasManyTerms, + ), + if (hasManyTerms) ...[ + Padding( + padding: const EdgeInsets.only(left: 100.0), + child: ExpansionTile( + onExpansionChanged: (bool value) => setState(() { + _showAllTerms = value; + }), + title: const Text( + 'Expand', + style: TextStyle( + decoration: TextDecoration.underline, + ), + ), + ), + ) + ] + ], + ); + } + + Widget _termsList( + List terms, { + bool borderFlag = false, + }) { + return ListView.builder( + padding: const EdgeInsets.only(left: 100.0), + itemCount: terms.length, + shrinkWrap: true, + itemBuilder: (_, int index) { + return ListTile( + title: Text( + terms[index].first, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + shape: (index == terms.length - 1 && borderFlag) + ? null + : const Border( + bottom: BorderSide(), + ), + trailing: + terms[index].second != null ? Text(terms[index].second!) : null, + ); + }, + ); + } +} diff --git a/packages/smooth_app/lib/pages/product/edit_product_page.dart b/packages/smooth_app/lib/pages/product/edit_product_page.dart index a71034a0579..c28ef8b804e 100644 --- a/packages/smooth_app/lib/pages/product/edit_product_page.dart +++ b/packages/smooth_app/lib/pages/product/edit_product_page.dart @@ -1,13 +1,10 @@ // ignore_for_file: use_build_context_synchronously -import 'dart:ui' as ui; - import 'package:auto_size_text/auto_size_text.dart'; import 'package:barcode_widget/barcode_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/up_to_date_mixin.dart'; @@ -15,8 +12,8 @@ import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_list_tile_card.dart'; +import 'package:smooth_app/generic_lib/widgets/svg_icon.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; -import 'package:smooth_app/helpers/app_helper.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/pages/product/add_other_details_page.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; @@ -175,7 +172,7 @@ class _EditProductPageState extends State with UpToDateMixin { ], ), _ListTitleItem( - leading: const _SvgIcon('assets/cacheTintable/ingredients.svg'), + leading: const SvgIcon('assets/cacheTintable/ingredients.svg'), title: appLocalizations.edit_product_form_item_ingredients_title, onTap: () async => ProductFieldOcrIngredientEditor().edit( @@ -186,7 +183,7 @@ class _EditProductPageState extends State with UpToDateMixin { _getSimpleListTileItem(SimpleInputPageCategoryHelper()), _ListTitleItem( leading: - const _SvgIcon('assets/cacheTintable/scale-balance.svg'), + const SvgIcon('assets/cacheTintable/scale-balance.svg'), title: appLocalizations .edit_product_form_item_nutrition_facts_title, subtitle: appLocalizations @@ -210,7 +207,7 @@ class _EditProductPageState extends State with UpToDateMixin { }), _getSimpleListTileItem(SimpleInputPageLabelHelper()), _ListTitleItem( - leading: const _SvgIcon('assets/cacheTintable/packaging.svg'), + leading: const SvgIcon('assets/cacheTintable/packaging.svg'), title: appLocalizations.edit_packagings_title, onTap: () async => ProductFieldPackagingEditor().edit( context: context, @@ -351,38 +348,6 @@ class _ListTitleItem extends SmoothListTileCard { ); } -/// SVG that looks like a ListTile icon. -class _SvgIcon extends StatelessWidget { - const _SvgIcon(this.assetName); - - final String assetName; - - @override - Widget build(BuildContext context) => SvgPicture.asset( - assetName, - height: DEFAULT_ICON_SIZE, - width: DEFAULT_ICON_SIZE, - colorFilter: ui.ColorFilter.mode( - _iconColor(Theme.of(context)), - ui.BlendMode.srcIn, - ), - package: AppHelper.APP_PACKAGE, - ); - - /// Returns the standard icon color in a [ListTile]. - /// - /// Simplified version from [ListTile], which was anyway not kind enough - /// to make it public. - Color _iconColor(ThemeData theme) { - switch (theme.brightness) { - case Brightness.light: - return Colors.black45; - case Brightness.dark: - return Colors.white; - } - } -} - /// Barcodes only allowed have a length of 7, 8, 12 or 13 characters class _ProductBarcode extends StatefulWidget { _ProductBarcode({required this.product, Key? key})