diff --git a/packages/smooth_app/assets/misc/logo_obf_half.svg b/packages/smooth_app/assets/misc/logo_obf_half.svg new file mode 100644 index 00000000000..cf489b462d0 --- /dev/null +++ b/packages/smooth_app/assets/misc/logo_obf_half.svg @@ -0,0 +1,14 @@ + + + Group 3 + + + + + + + + + + + \ No newline at end of file diff --git a/packages/smooth_app/assets/misc/logo_off_half.svg b/packages/smooth_app/assets/misc/logo_off_half.svg new file mode 100644 index 00000000000..949a582801f --- /dev/null +++ b/packages/smooth_app/assets/misc/logo_off_half.svg @@ -0,0 +1,18 @@ + + + Group 4 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/smooth_app/assets/misc/logo_opf_half.svg b/packages/smooth_app/assets/misc/logo_opf_half.svg new file mode 100644 index 00000000000..4b748a28d9b --- /dev/null +++ b/packages/smooth_app/assets/misc/logo_opf_half.svg @@ -0,0 +1,20 @@ + + + Group 2 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/smooth_app/assets/misc/logo_opff_half.svg b/packages/smooth_app/assets/misc/logo_opff_half.svg new file mode 100644 index 00000000000..8eb7df1bcee --- /dev/null +++ b/packages/smooth_app/assets/misc/logo_opff_half.svg @@ -0,0 +1,23 @@ + + + Group 5 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 80ba6a19c9c..6df16d3708c 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -3271,5 +3271,21 @@ "carousel_loading_text": "We are searching for it in our database of more than **3 million products!**", "@carousel_loading_text": { "description": "Please keep the ** syntax to make the text bold" + }, + "product_type_subtitle_food": "Vegetables, fruits, frozen food…", + "@product_type_subtitle_food" : { + "description": "Example of products for food category" + }, + "product_type_subtitle_beauty": "Makeup, soaps, toothpastes…", + "@product_type_subtitle_beauty" : { + "description": "Example of products for beauty category" + }, + "product_type_subtitle_pet_food": "Food for dogs, cats…", + "@product_type_subtitle_pet_food" : { + "description": "Example of products for pet food category" + }, + "product_type_subtitle_product": "Smartphones, furniture…", + "@product_type_subtitle_product" : { + "description": "Example of products for other categories" } } diff --git a/packages/smooth_app/lib/pages/navigator/app_navigator.dart b/packages/smooth_app/lib/pages/navigator/app_navigator.dart index 47d606a2bcd..4c13003a6cf 100644 --- a/packages/smooth_app/lib/pages/navigator/app_navigator.dart +++ b/packages/smooth_app/lib/pages/navigator/app_navigator.dart @@ -13,7 +13,7 @@ import 'package:smooth_app/pages/navigator/error_page.dart'; import 'package:smooth_app/pages/navigator/external_page.dart'; import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; -import 'package:smooth_app/pages/product/add_new_product_page.dart'; +import 'package:smooth_app/pages/product/add_new_product/add_new_product_page.dart'; import 'package:smooth_app/pages/product/edit_product_page.dart'; import 'package:smooth_app/pages/product/product_loader_page.dart'; import 'package:smooth_app/pages/product/product_page/new_product_header.dart'; diff --git a/packages/smooth_app/lib/pages/offline_data_page.dart b/packages/smooth_app/lib/pages/offline_data_page.dart index 382e502fbd5..18c648aefed 100644 --- a/packages/smooth_app/lib/pages/offline_data_page.dart +++ b/packages/smooth_app/lib/pages/offline_data_page.dart @@ -11,7 +11,7 @@ import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/duration_constants.dart'; import 'package:smooth_app/helpers/app_helper.dart'; -import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/pages/product/product_type_extensions.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; diff --git a/packages/smooth_app/lib/pages/product/add_new_product_page.dart b/packages/smooth_app/lib/pages/product/add_new_product/add_new_product_page.dart similarity index 98% rename from packages/smooth_app/lib/pages/product/add_new_product_page.dart rename to packages/smooth_app/lib/pages/product/add_new_product/add_new_product_page.dart index 3a6491a32c4..25ccaa47ccb 100644 --- a/packages/smooth_app/lib/pages/product/add_new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/add_new_product/add_new_product_page.dart @@ -19,11 +19,13 @@ import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/pages/crop_parameters.dart'; import 'package:smooth_app/pages/image_crop_page.dart'; import 'package:smooth_app/pages/preferences/user_preferences_widgets.dart'; +import 'package:smooth_app/pages/product/add_new_product/product_type_radio_list_tile.dart'; import 'package:smooth_app/pages/product/add_new_product_helper.dart'; import 'package:smooth_app/pages/product/common/product_dialog_helper.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/product_image_swipeable_view.dart'; +import 'package:smooth_app/pages/product/product_type_extensions.dart'; import 'package:smooth_app/pages/product/simple_input_page_helpers.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -96,7 +98,6 @@ class _AddNewProductPageState extends State (widget.displayPictures ? 1 : 0); double get _progress => (_pageNumber + 1) / _totalPages; - bool get _isLastPage => (_pageNumber + 1) == _totalPages; ProductType? _inputProductType; late ColorScheme _colorScheme; @@ -576,15 +577,12 @@ class _AddNewProductPageState extends State for (final ProductType productType in ProductType.values) { rows.add( - RadioListTile( - title: Text(productType.getLabel(appLocalizations)), - onChanged: (ProductType? value) { - if (value != null) { - setState(() => _inputProductType = value); - } + ProductTypeRadioListTile( + productType: productType, + checked: productType == _inputProductType, + onChanged: (ProductType value) { + setState(() => _inputProductType = value); }, - value: productType, - groupValue: _inputProductType, ), ); } diff --git a/packages/smooth_app/lib/pages/product/add_new_product/product_type_radio_list_tile.dart b/packages/smooth_app/lib/pages/product/add_new_product/product_type_radio_list_tile.dart new file mode 100644 index 00000000000..aa373190cea --- /dev/null +++ b/packages/smooth_app/lib/pages/product/add_new_product/product_type_radio_list_tile.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/pages/product/product_type_extensions.dart'; +import 'package:smooth_app/resources/app_icons.dart' as icons; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; + +class ProductTypeRadioListTile extends StatefulWidget { + const ProductTypeRadioListTile({ + required this.productType, + required this.checked, + required this.onChanged, + super.key, + }); + + final ProductType productType; + final bool checked; + final void Function(ProductType) onChanged; + + @override + State createState() => _ProductTypeRadioListTile(); +} + +class _ProductTypeRadioListTile extends State + with TickerProviderStateMixin { + AnimationController? _controller; + late Animation _colorAnimation; + late Animation _opacityAnimation; + bool? _currentTheme; + + @override + void didUpdateWidget(ProductTypeRadioListTile oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.checked != widget.checked) { + if (widget.checked) { + _controller?.forward(); + } else { + _controller?.reverse(); + } + } + } + + // To reset animations when theme changes + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + if (_currentTheme != context.lightTheme()) { + _controller = null; + } + } + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final bool lightTheme = context.lightTheme(); + _initAnimationsIfNecessary(lightTheme); + + final SmoothColorsThemeExtension theme = + Theme.of(context).extension()!; + + return Semantics( + label: + '${widget.productType.getTitle(appLocalizations)} ${widget.productType.getSubtitle(appLocalizations)}', + checked: widget.checked, + excludeSemantics: true, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: _colorAnimation.value, + borderRadius: ANGULAR_BORDER_RADIUS, + border: Border.all( + color: lightTheme ? theme.primarySemiDark : theme.primaryNormal, + width: 2.0, + ), + ), + margin: const EdgeInsetsDirectional.only( + start: VERY_LARGE_SPACE, + end: VERY_LARGE_SPACE, + top: VERY_LARGE_SPACE, + ), + child: InkWell( + onTap: () => widget.onChanged(widget.productType), + child: Stack( + children: [ + PositionedDirectional( + bottom: 0.0, + end: 0.0, + child: ClipRRect( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(ANGULAR_RADIUS.x - 2.0), + ), + child: SvgPicture.asset( + widget.productType.getIllustration(), + width: 50.0, + ), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only( + top: LARGE_SPACE, + start: MEDIUM_SPACE, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + border: Border.all( + color: lightTheme + ? theme.primarySemiDark + : theme.primaryMedium, + ), + shape: BoxShape.circle, + ), + child: Opacity( + opacity: _opacityAnimation.value, + child: const icons.Check( + size: 9.0, + ), + ), + ), + const SizedBox(width: MEDIUM_SPACE), + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only( + bottom: LARGE_SPACE, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.productType.getTitle(appLocalizations), + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + fontWeight: FontWeight.bold, + color: lightTheme ? Colors.black : Colors.white, + ), + ), + const SizedBox(height: SMALL_SPACE), + Text( + widget.productType.getSubtitle(appLocalizations), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: lightTheme + ? Colors.black54 + : Colors.white54, + ), + ), + ], + ), + ), + ) + ], + ), + ), + ], + ), + ), + ), + ); + } + + void _initAnimationsIfNecessary(bool lightTheme) { + if (_controller != null) { + return; + } + + _currentTheme = lightTheme; + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + )..addListener(() => setState(() {})); + + final ThemeData themeData = Theme.of(context); + + _colorAnimation = ColorTween( + begin: themeData.scaffoldBackgroundColor.withOpacity(0.0), + end: lightTheme + ? themeData.extension()!.primaryMedium + : themeData.extension()!.primarySemiDark, + ).animate( + CurvedAnimation(parent: _controller!, curve: Curves.fastOutSlowIn), + ); + + _opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _controller!, curve: Curves.fastOutSlowIn), + ); + } +} diff --git a/packages/smooth_app/lib/pages/product/product_incomplete_card.dart b/packages/smooth_app/lib/pages/product/product_incomplete_card.dart index af583b87195..0dad587f222 100644 --- a/packages/smooth_app/lib/pages/product/product_incomplete_card.dart +++ b/packages/smooth_app/lib/pages/product/product_incomplete_card.dart @@ -3,10 +3,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; -import 'package:smooth_app/pages/product/add_new_product_page.dart'; +import 'package:smooth_app/pages/product/add_new_product/add_new_product_page.dart'; import 'package:smooth_app/pages/product/product_field_editor.dart'; +import 'package:smooth_app/pages/product/product_type_extensions.dart'; import 'package:smooth_app/pages/product/simple_input_page_helpers.dart'; -import 'package:smooth_app/query/product_query.dart'; /// "Incomplete product!" card to be displayed in product summary, if relevant. /// diff --git a/packages/smooth_app/lib/pages/product/product_type_extensions.dart b/packages/smooth_app/lib/pages/product/product_type_extensions.dart new file mode 100644 index 00000000000..43cf39defcc --- /dev/null +++ b/packages/smooth_app/lib/pages/product/product_type_extensions.dart @@ -0,0 +1,75 @@ +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; + +extension ProductTypeExtension on ProductType { + String getTitle(AppLocalizations appLocalizations) { + return switch (this) { + ProductType.food => appLocalizations.product_type_label_food, + ProductType.beauty => appLocalizations.product_type_label_beauty, + ProductType.petFood => appLocalizations.product_type_label_pet_food, + ProductType.product => appLocalizations.product_type_label_product, + }; + } + + String getSubtitle(AppLocalizations appLocalizations) { + return switch (this) { + ProductType.food => appLocalizations.product_type_subtitle_food, + ProductType.beauty => appLocalizations.product_type_subtitle_beauty, + ProductType.petFood => appLocalizations.product_type_subtitle_pet_food, + ProductType.product => appLocalizations.product_type_subtitle_product, + }; + } + + String getIllustration() { + return switch (this) { + ProductType.food => 'assets/misc/logo_off_half.svg', + ProductType.beauty => 'assets/misc/logo_obf_half.svg', + ProductType.petFood => 'assets/misc/logo_opff_half.svg', + ProductType.product => 'assets/misc/logo_opf_half.svg', + }; + } + + String getDomain() => switch (this) { + ProductType.food => 'openfoodfacts', + ProductType.beauty => 'openbeautyfacts', + ProductType.petFood => 'openpetfoodfacts', + ProductType.product => 'openproductsfacts', + }; + + String getLabel(final AppLocalizations appLocalizations) => switch (this) { + ProductType.food => appLocalizations.product_type_label_food, + ProductType.beauty => appLocalizations.product_type_label_beauty, + ProductType.petFood => appLocalizations.product_type_label_pet_food, + ProductType.product => appLocalizations.product_type_label_product, + }; + + String getRoadToScoreLabel(final AppLocalizations appLocalizations) => + switch (this) { + ProductType.food => appLocalizations.hey_incomplete_product_message, + ProductType.beauty => + appLocalizations.hey_incomplete_product_message_beauty, + ProductType.petFood => + appLocalizations.hey_incomplete_product_message_pet_food, + ProductType.product => + appLocalizations.hey_incomplete_product_message_product, + }; + + String getShareProductLabel( + final AppLocalizations appLocalizations, + final String url, + ) => + switch (this) { + ProductType.food => appLocalizations.share_product_text( + url, + ), + ProductType.beauty => appLocalizations.share_product_text_beauty( + url, + ), + ProductType.petFood => appLocalizations.share_product_text_pet_food( + url, + ), + ProductType.product => appLocalizations.share_product_text_product( + url, + ), + }; +} diff --git a/packages/smooth_app/lib/pages/search/search_product_helper.dart b/packages/smooth_app/lib/pages/search/search_product_helper.dart index d2cca1d1fcb..cf22f9139b4 100644 --- a/packages/smooth_app/lib/pages/search/search_product_helper.dart +++ b/packages/smooth_app/lib/pages/search/search_product_helper.dart @@ -13,8 +13,8 @@ import 'package:smooth_app/pages/navigator/app_navigator.dart'; import 'package:smooth_app/pages/product/common/product_dialog_helper.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/product/common/search_helper.dart'; +import 'package:smooth_app/pages/product/product_type_extensions.dart'; import 'package:smooth_app/query/keywords_product_query.dart'; -import 'package:smooth_app/query/product_query.dart'; /// Search helper dedicated to product search. class SearchProductHelper extends SearchHelper { diff --git a/packages/smooth_app/lib/query/product_query.dart b/packages/smooth_app/lib/query/product_query.dart index 437549dcf01..57c776c4f90 100644 --- a/packages/smooth_app/lib/query/product_query.dart +++ b/packages/smooth_app/lib/query/product_query.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; @@ -9,6 +8,7 @@ import 'package:smooth_app/database/dao_string.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; +import 'package:smooth_app/pages/product/product_type_extensions.dart'; import 'package:uuid/uuid.dart'; // ignore: avoid_classes_with_only_static_members @@ -295,49 +295,3 @@ abstract class ProductQuery { ProductField.OWNER_FIELDS, ]; } - -extension ProductTypeExtension on ProductType { - String getDomain() => switch (this) { - ProductType.food => 'openfoodfacts', - ProductType.beauty => 'openbeautyfacts', - ProductType.petFood => 'openpetfoodfacts', - ProductType.product => 'openproductsfacts', - }; - - String getLabel(final AppLocalizations appLocalizations) => switch (this) { - ProductType.food => appLocalizations.product_type_label_food, - ProductType.beauty => appLocalizations.product_type_label_beauty, - ProductType.petFood => appLocalizations.product_type_label_pet_food, - ProductType.product => appLocalizations.product_type_label_product, - }; - - String getRoadToScoreLabel(final AppLocalizations appLocalizations) => - switch (this) { - ProductType.food => appLocalizations.hey_incomplete_product_message, - ProductType.beauty => - appLocalizations.hey_incomplete_product_message_beauty, - ProductType.petFood => - appLocalizations.hey_incomplete_product_message_pet_food, - ProductType.product => - appLocalizations.hey_incomplete_product_message_product, - }; - - String getShareProductLabel( - final AppLocalizations appLocalizations, - final String url, - ) => - switch (this) { - ProductType.food => appLocalizations.share_product_text( - url, - ), - ProductType.beauty => appLocalizations.share_product_text_beauty( - url, - ), - ProductType.petFood => appLocalizations.share_product_text_pet_food( - url, - ), - ProductType.product => appLocalizations.share_product_text_product( - url, - ), - }; -}