diff --git a/packages/smooth_app/lib/cards/data_cards/score_card.dart b/packages/smooth_app/lib/cards/data_cards/score_card.dart index 5426202ab8ab..17ca032141b4 100644 --- a/packages/smooth_app/lib/cards/data_cards/score_card.dart +++ b/packages/smooth_app/lib/cards/data_cards/score_card.dart @@ -9,46 +9,38 @@ import 'package:smooth_app/themes/constant_icons.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; enum CardEvaluation { - UNKNOWN, - VERY_BAD, - BAD, - NEUTRAL, - GOOD, - VERY_GOOD; + UNKNOWN( + backgroundColor: GREY_COLOR, + textColor: PRIMARY_GREY_COLOR, + ), + VERY_BAD( + backgroundColor: RED_BACKGROUND_COLOR, + textColor: RED_COLOR, + ), + BAD( + backgroundColor: ORANGE_BACKGROUND_COLOR, + textColor: LIGHT_ORANGE_COLOR, + ), + NEUTRAL( + backgroundColor: YELLOW_BACKGROUND_COLOR, + textColor: DARK_YELLOW_COLOR, + ), + GOOD( + backgroundColor: LIGHT_GREEN_BACKGROUND_COLOR, + textColor: LIGHT_GREEN_COLOR, + ), + VERY_GOOD( + backgroundColor: DARK_GREEN_BACKGROUND_COLOR, + textColor: DARK_GREEN_COLOR, + ); - Color getBackgroundColor() { - switch (this) { - case CardEvaluation.UNKNOWN: - return GREY_COLOR; - case CardEvaluation.VERY_BAD: - return RED_BACKGROUND_COLOR; - case CardEvaluation.BAD: - return ORANGE_BACKGROUND_COLOR; - case CardEvaluation.NEUTRAL: - return YELLOW_BACKGROUND_COLOR; - case CardEvaluation.GOOD: - return LIGHT_GREEN_BACKGROUND_COLOR; - case CardEvaluation.VERY_GOOD: - return DARK_GREEN_BACKGROUND_COLOR; - } - } + const CardEvaluation({ + required this.backgroundColor, + required this.textColor, + }); - Color getTextColor() { - switch (this) { - case CardEvaluation.UNKNOWN: - return PRIMARY_GREY_COLOR; - case CardEvaluation.VERY_BAD: - return RED_COLOR; - case CardEvaluation.BAD: - return LIGHT_ORANGE_COLOR; - case CardEvaluation.NEUTRAL: - return DARK_YELLOW_COLOR; - case CardEvaluation.GOOD: - return LIGHT_GREEN_COLOR; - case CardEvaluation.VERY_GOOD: - return DARK_GREEN_COLOR; - } - } + final Color backgroundColor; + final Color textColor; } class ScoreCard extends StatelessWidget { @@ -83,10 +75,10 @@ class ScoreCard extends StatelessWidget { ? 1 : SmoothTheme.ADDITIONAL_OPACITY_FOR_DARK; final Color backgroundColor = - cardEvaluation.getBackgroundColor().withOpacity(opacity); + cardEvaluation.backgroundColor.withOpacity(opacity); final Color textColor = themeData.brightness == Brightness.dark ? Colors.white - : cardEvaluation.getTextColor().withOpacity(opacity); + : cardEvaluation.textColor.withOpacity(opacity); final SvgIconChip? iconChip = iconUrl == null ? null : SvgIconChip(iconUrl!, height: iconHeight); diff --git a/packages/smooth_app/lib/data_models/preferences/user_preferences.dart b/packages/smooth_app/lib/data_models/preferences/user_preferences.dart index d95bec1d60d8..1b6cf7449e05 100644 --- a/packages/smooth_app/lib/data_models/preferences/user_preferences.dart +++ b/packages/smooth_app/lib/data_models/preferences/user_preferences.dart @@ -99,6 +99,10 @@ class UserPreferences extends ChangeNotifier { static const String _TAG_NUMBER_OF_SCANS = 'numberOfScans'; + /// User knowledge panel order + static const String _TAG_USER_KNOWLEDGE_PANEL_ORDER = + 'userKnowledgePanelOrder'; + Future init(final ProductPreferences productPreferences) async { await _onMigrate(); @@ -332,4 +336,14 @@ class UserPreferences extends ChangeNotifier { await _sharedPreferences.setString(_TAG_USER_PICTURE_SOURCE, source.tag); notifyListeners(); } + + List get userKnowledgePanelOrder => + _sharedPreferences.getStringList(_TAG_USER_KNOWLEDGE_PANEL_ORDER) ?? + []; + + Future setUserKnowledgePanelOrder(final List source) async { + await _sharedPreferences.setStringList( + _TAG_USER_KNOWLEDGE_PANEL_ORDER, source); + notifyListeners(); + } } diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart index f61ec5d336d0..ebce76fb64ae 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart @@ -12,10 +12,12 @@ class KnowledgePanelCard extends StatelessWidget { const KnowledgePanelCard({ required this.panelId, required this.product, + required this.isClickable, }); final String panelId; final Product product; + final bool isClickable; static const String PANEL_NUTRITION_TABLE_ID = 'nutrition_facts_table'; static const String PANEL_INGREDIENTS_ID = 'ingredients'; @@ -36,6 +38,7 @@ class KnowledgePanelCard extends StatelessWidget { panelId: panelId, product: product, isInitiallyExpanded: false, + isClickable: isClickable, ); } @@ -43,23 +46,26 @@ class KnowledgePanelCard extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE), child: InkWell( borderRadius: ANGULAR_BORDER_RADIUS, + onTap: !isClickable + ? null + : () async => Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => SmoothBrightnessOverride( + brightness: + SmoothBrightnessOverride.of(context)?.brightness, + child: KnowledgePanelPage( + panelId: panelId, + product: product, + ), + ), + ), + ), child: KnowledgePanelsBuilder.getPanelSummaryWidget( panel, - isClickable: true, + isClickable: isClickable, margin: EdgeInsets.zero, ), - onTap: () async => Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => SmoothBrightnessOverride( - brightness: SmoothBrightnessOverride.of(context)?.brightness, - child: KnowledgePanelPage( - panelId: panelId, - product: product, - ), - ), - ), - ), ), ); } diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_expanded_card.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_expanded_card.dart index 046e54ebba17..8b01476e83d8 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_expanded_card.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_expanded_card.dart @@ -8,11 +8,13 @@ class KnowledgePanelExpandedCard extends StatelessWidget { required this.panelId, required this.product, required this.isInitiallyExpanded, + required this.isClickable, }); final Product product; final String panelId; final bool isInitiallyExpanded; + final bool isClickable; @override Widget build(BuildContext context) { @@ -32,6 +34,7 @@ class KnowledgePanelExpandedCard extends StatelessWidget { knowledgePanelElement: element, product: product, isInitiallyExpanded: isInitiallyExpanded, + isClickable: isClickable, ); if (elementWidget != null) { elementWidgets.add( diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_group_card.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_group_card.dart index a08cd74844c8..89125ea50933 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_group_card.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_group_card.dart @@ -8,10 +8,12 @@ class KnowledgePanelGroupCard extends StatelessWidget { const KnowledgePanelGroupCard({ required this.groupElement, required this.product, + required this.isClickable, }); final KnowledgePanelPanelGroupElement groupElement; final Product product; + final bool isClickable; @override Widget build(BuildContext context) { @@ -35,6 +37,7 @@ class KnowledgePanelGroupCard extends StatelessWidget { KnowledgePanelCard( panelId: panelId, product: product, + isClickable: isClickable, ) ], ), diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart index e2f3ea22b247..3925c35b96b1 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart @@ -79,6 +79,7 @@ class _KnowledgePanelPageState extends State panelId: widget.panelId, product: upToDateProduct, isInitiallyExpanded: true, + isClickable: true, ), ), ), diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart index d7ebc1890115..ed463987c3ae 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart @@ -51,6 +51,7 @@ class KnowledgePanelsBuilder { knowledgePanelElement: element, product: product, isInitiallyExpanded: false, + isClickable: true, ); if (widget != null) { children.add(widget); @@ -153,11 +154,13 @@ class KnowledgePanelsBuilder { required final KnowledgePanelElement knowledgePanelElement, required final Product product, required final bool isInitiallyExpanded, + required final bool isClickable, }) { final Widget? result = _getElementWidget( element: knowledgePanelElement, product: product, isInitiallyExpanded: isInitiallyExpanded, + isClickable: isClickable, ); if (result == null) { return null; @@ -179,6 +182,7 @@ class KnowledgePanelsBuilder { required final KnowledgePanelElement element, required final Product product, required final bool isInitiallyExpanded, + required final bool isClickable, }) { switch (element.elementType) { case KnowledgePanelElementType.TEXT: @@ -205,12 +209,14 @@ class KnowledgePanelsBuilder { return KnowledgePanelCard( panelId: panelId, product: product, + isClickable: isClickable, ); case KnowledgePanelElementType.PANEL_GROUP: return KnowledgePanelGroupCard( groupElement: element.panelGroupElement!, product: product, + isClickable: isClickable, ); case KnowledgePanelElementType.TABLE: diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 39f4b29327a9..6cbc494bb313 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -2523,6 +2523,10 @@ "hunger_games_loading_line2": "We're downloading the questions!", "hunger_games_error_label": "Argh! Something went wrong… and we couldn't load the questions.", "hunger_games_error_retry_button": "Let's retry!", + "reorder_attribute_action": "Reorder the attributes", + "@reorder_attribute_action": { + "description": "An action button or a page title about reordering the attributes (e.g. 'is vegan?', 'nutrition facts', ...)" + }, "link_cant_be_opened": "This link can't be opened on your device. Please check that you have a browser installed.", "@link_cant_be_opened": { "description": "An error may happen if the device doesn't have a browser installed." diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart index c85851175384..861dd6adb435 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart @@ -49,6 +49,7 @@ class UserPreferencesDevMode extends AbstractUserPreferences { '__accessibilityNoColor'; static const String userPreferencesFlagAccessibilityEmoji = '__accessibilityEmoji'; + static const String userPreferencesFlagUserOrderedKP = '__userOrderedKP'; final TextEditingController _textFieldController = TextEditingController(); @@ -305,6 +306,16 @@ class UserPreferencesDevMode extends AbstractUserPreferences { builder: (BuildContext context) => const UserPreferencesDebugInfo())), ), + UserPreferencesItemSwitch( + title: 'User ordered knowledge panels', + value: userPreferences.getFlag(userPreferencesFlagUserOrderedKP) ?? + false, + onChanged: (bool value) async { + await userPreferences.setFlag( + userPreferencesFlagUserOrderedKP, value); + _showSuccessMessage(); + }, + ), UserPreferencesItemTile( title: 'Preference Search...', onTap: () async => Navigator.of(context).push( diff --git a/packages/smooth_app/lib/pages/product/new_product_page.dart b/packages/smooth_app/lib/pages/product/new_product_page.dart index 77f744a4f4ca..1bd7a1ffd497 100644 --- a/packages/smooth_app/lib/pages/product/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/new_product_page.dart @@ -14,18 +14,21 @@ import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/up_to_date_mixin.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/duration_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; -import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_product_cards.dart'; -import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; import 'package:smooth_app/pages/carousel_manager.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/pages/product/common/product_list_page.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/edit_product_page.dart'; import 'package:smooth_app/pages/product/product_questions_widget.dart'; +import 'package:smooth_app/pages/product/reorderable_knowledge_panel_page.dart'; +import 'package:smooth_app/pages/product/reordered_knowledge_panel_cards.dart'; +import 'package:smooth_app/pages/product/standard_knowledge_panel_cards.dart'; import 'package:smooth_app/pages/product/summary_card.dart'; import 'package:smooth_app/pages/product/website_card.dart'; import 'package:smooth_app/pages/product_list_user_dialog_helper.dart'; @@ -167,6 +170,7 @@ class _ProductPageState extends State Widget _buildProductBody(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); final LocalDatabase localDatabase = context.read(); + final UserPreferences userPreferences = context.watch(); final DaoProductList daoProductList = DaoProductList(localDatabase); return RefreshIndicator( onRefresh: () async => ProductRefresher().fetchAndRefresh( @@ -220,40 +224,38 @@ class _ProductPageState extends State appLocalizations, daoProductList, ), - _buildKnowledgePanelCards(), + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + ReorderedKnowledgePanelCards(upToDateProduct) + else + StandardKnowledgePanelCards(upToDateProduct), + // TODO(monsieurtanuki): include website in reordered knowledge panels if (upToDateProduct.website != null && upToDateProduct.website!.trim().isNotEmpty) WebsiteCard(upToDateProduct.website!), + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: SmoothLargeButtonWithIcon( + text: appLocalizations.reorder_attribute_action, + icon: Icons.sort, + onPressed: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + ReorderableKnowledgePanelPage(upToDateProduct), + ), + ), + ), + ), ], ), ); } - Widget _buildKnowledgePanelCards() { - final List knowledgePanelWidgets = []; - if (upToDateProduct.knowledgePanels != null) { - final List elements = - KnowledgePanelsBuilder.getRootPanelElements(upToDateProduct); - for (final KnowledgePanelElement panelElement in elements) { - final List children = KnowledgePanelsBuilder.getChildren( - context, - panelElement: panelElement, - product: upToDateProduct, - onboardingMode: false, - ); - if (children.isNotEmpty) { - knowledgePanelWidgets.add( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: children, - ), - ); - } - } - } - return KnowledgePanelProductCards(knowledgePanelWidgets); - } - Future _editList() async { final LocalDatabase localDatabase = context.read(); final DaoProductList daoProductList = DaoProductList(localDatabase); diff --git a/packages/smooth_app/lib/pages/product/reorderable_knowledge_panel_page.dart b/packages/smooth_app/lib/pages/product/reorderable_knowledge_panel_page.dart new file mode 100644 index 000000000000..4277134328e6 --- /dev/null +++ b/packages/smooth_app/lib/pages/product/reorderable_knowledge_panel_page.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; +import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_card.dart'; +import 'package:smooth_app/widgets/smooth_scaffold.dart'; + +/// Page where the user can reorder the Knowledge Panel Cards. +class ReorderableKnowledgePanelPage extends StatefulWidget { + const ReorderableKnowledgePanelPage(this.product); + + final Product product; + + static List getOrderedKnowledgePanels( + final UserPreferences userPreferences, + ) { + final List order = userPreferences.userKnowledgePanelOrder; + if (order.isNotEmpty) { + return order; + } + return List.from(ReorderableKnowledgePanelPage._initialOrder, + growable: true); + } + + // cf. product.knowledgePanels!.panelIdToPanelMap.keys + // TODO(monsieurtanuki): check how safe it is. What about new entries from the server? What about missing entries for a product? + static const List _initialOrder = [ + 'nutriscore', + 'nutrient_level_fat', + 'nutrient_level_saturated-fat', + 'nutrient_level_sugars', + 'nutrient_level_salt', + 'nutrition_facts_table', + 'serving_size', + 'ingredients', + 'nova', +// 'environment_card', +// 'health_card', + 'ingredients_analysis_en:palm-oil-free', + 'ingredients_analysis_en:vegan', + 'ingredients_analysis_en:vegetarian', +// 'ingredients_analysis', + 'ingredients_analysis_details', + 'ecoscore', +// 'packaging_components', +// 'packaging_materials', + 'packaging_recycling', + 'origins_of_ingredients', +// 'root', + ]; + + @override + State createState() => + _ReorderableKnowledgePanelPageState(); +} + +class _ReorderableKnowledgePanelPageState + extends State { + late List _order; + + void _initOrder(final BuildContext context) { + final UserPreferences userPreferences = context.read(); + _order = ReorderableKnowledgePanelPage.getOrderedKnowledgePanels( + userPreferences, + ); + } + + Future _setOrder() async { + final UserPreferences userPreferences = context.read(); + await userPreferences.setUserKnowledgePanelOrder(_order); + } + + @override + void initState() { + super.initState(); + _initOrder(context); + } + + @override + Widget build(BuildContext context) { + final List children = []; + int index = 0; + for (final String panelId in _order) { + children.add( + ListTile( + key: Key(panelId), + title: KnowledgePanelCard( + panelId: panelId, + product: widget.product, + isClickable: false, + ), + trailing: ReorderableDragStartListener( + key: ValueKey(index), + index: index++, + child: const Icon(Icons.drag_handle), + ), + ), + ); + } + return SmoothScaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).reorder_attribute_action), + actions: [ + IconButton( + icon: const Icon(Icons.clear_all), + onPressed: () async { + _order = []; + await _setOrder(); + if (!context.mounted) { + return; + } + setState(() => _initOrder(context)); + }, + ) + ], + ), + body: ReorderableListView( + buildDefaultDragHandles: false, + children: children, + onReorder: (int oldIndex, int newIndex) => setState( + () { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final String item = _order.removeAt(oldIndex); + _order.insert(newIndex, item); + _setOrder(); + }, + ), + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/product/reordered_knowledge_panel_cards.dart b/packages/smooth_app/lib/pages/product/reordered_knowledge_panel_cards.dart new file mode 100644 index 000000000000..58c66e1b3c94 --- /dev/null +++ b/packages/smooth_app/lib/pages/product/reordered_knowledge_panel_cards.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; +import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_card.dart'; +import 'package:smooth_app/pages/product/reorderable_knowledge_panel_page.dart'; + +/// Knowledge Panel Cards as reordered by the user. +class ReorderedKnowledgePanelCards extends StatelessWidget { + const ReorderedKnowledgePanelCards(this.product); + + final Product product; + + @override + Widget build(BuildContext context) { + final UserPreferences userPreferences = context.watch(); + final List order = + ReorderableKnowledgePanelPage.getOrderedKnowledgePanels( + userPreferences); + final List children = []; + for (final String panelId in order) { + children.add( + ListTile( + title: KnowledgePanelCard( + panelId: panelId, + product: product, + isClickable: true, + ), + ), + ); + } + return Column(children: children); + } +} diff --git a/packages/smooth_app/lib/pages/product/standard_knowledge_panel_cards.dart b/packages/smooth_app/lib/pages/product/standard_knowledge_panel_cards.dart new file mode 100644 index 000000000000..aa9df770839b --- /dev/null +++ b/packages/smooth_app/lib/pages/product/standard_knowledge_panel_cards.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_product_cards.dart'; +import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; + +/// Knowledge Panel Cards as provided by the server. +class StandardKnowledgePanelCards extends StatelessWidget { + const StandardKnowledgePanelCards(this.product); + + final Product product; + + @override + Widget build(BuildContext context) { + final List knowledgePanelWidgets = []; + if (product.knowledgePanels != null) { + final List elements = + KnowledgePanelsBuilder.getRootPanelElements(product); + for (final KnowledgePanelElement panelElement in elements) { + final List children = KnowledgePanelsBuilder.getChildren( + context, + panelElement: panelElement, + product: product, + onboardingMode: false, + ); + if (children.isNotEmpty) { + knowledgePanelWidgets.add( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ); + } + } + } + return KnowledgePanelProductCards(knowledgePanelWidgets); + } +}