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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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,
- ),
- };
-}