From cc874d23f6bf2a0f08fc4f336391d4398f3e7eff Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Thu, 7 Nov 2024 19:41:32 +0100 Subject: [PATCH] feat: Product page with the status bar from the POC (#5808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Product page with the status bar from the POC * Revert compatibility colors * Remove 2 warnings * Oops, the translation… * Use the correct back button on Android * Fix --- .../smooth_product_card_found.dart | 2 +- .../helpers/product_compatibility_helper.dart | 50 +-- packages/smooth_app/lib/l10n/app_en.arb | 4 + .../lib/pages/personalized_ranking_page.dart | 2 +- .../pages/product/compare_products3_page.dart | 4 +- .../product/product_compatibility_header.dart | 3 +- .../product_page/new_product_footer.dart | 10 +- .../product_page/new_product_header.dart | 298 ++++++++++++++++++ .../product_page/new_product_page.dart | 283 +++++++++-------- .../lib/pages/product/summary_card.dart | 16 +- .../lib/themes/smooth_theme_colors.dart | 16 + .../lib/widgets/smooth_scaffold.dart | 9 + 12 files changed, 503 insertions(+), 194 deletions(-) create mode 100644 packages/smooth_app/lib/pages/product/product_page/new_product_header.dart diff --git a/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart b/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart index cf201e5ab752..7a758ebac626 100644 --- a/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart +++ b/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart @@ -114,7 +114,7 @@ class SmoothProductCardFound extends StatelessWidget { Icon( Icons.circle, size: 15, - color: helper.getButtonColor(isDarkMode), + color: helper.getColor(context), ), const Padding( padding: EdgeInsetsDirectional.only( diff --git a/packages/smooth_app/lib/helpers/product_compatibility_helper.dart b/packages/smooth_app/lib/helpers/product_compatibility_helper.dart index 1c5f76d18611..1f14e7b4b2f4 100644 --- a/packages/smooth_app/lib/helpers/product_compatibility_helper.dart +++ b/packages/smooth_app/lib/helpers/product_compatibility_helper.dart @@ -1,7 +1,7 @@ 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/design_constants.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; class ProductCompatibilityHelper { ProductCompatibilityHelper.product(final MatchedProductV2 product) @@ -11,20 +11,18 @@ class ProductCompatibilityHelper { final MatchedProductStatusV2 status; - Color getHeaderBackgroundColor(bool darkMode) { - if (darkMode) { - return _getDarkColors(); - } else { - return _getLightColors(); - } - } + Color getColor(BuildContext context) { + final SmoothColorsThemeExtension theme = + Theme.of(context).extension()!; - Color getButtonColor(bool darkMode) { - if (darkMode) { - return _getLightColors(); - } else { - return _getDarkColors(); - } + return switch (status) { + MatchedProductStatusV2.VERY_GOOD_MATCH => theme.green, + MatchedProductStatusV2.GOOD_MATCH => theme.green, + MatchedProductStatusV2.POOR_MATCH => theme.orange, + MatchedProductStatusV2.MAY_NOT_MATCH => theme.orange, + MatchedProductStatusV2.DOES_NOT_MATCH => theme.red, + MatchedProductStatusV2.UNKNOWN_MATCH => theme.greyNormal, + }; } Color getHeaderForegroundColor(bool darkMode) => @@ -33,30 +31,6 @@ class ProductCompatibilityHelper { Color getButtonForegroundColor(bool darkMode) => getHeaderForegroundColor(darkMode); - // According to color contrast tool https://material.io/resources/color - // on all those background colors the best is to write in black. - Color _getDarkColors() { - switch (status) { - case MatchedProductStatusV2.VERY_GOOD_MATCH: - return DARK_GREEN_COLOR; - case MatchedProductStatusV2.GOOD_MATCH: - return LIGHT_GREEN_COLOR; - case MatchedProductStatusV2.POOR_MATCH: - return DARK_YELLOW_COLOR; - case MatchedProductStatusV2.MAY_NOT_MATCH: - return DARK_ORANGE_COLOR; - case MatchedProductStatusV2.DOES_NOT_MATCH: - return RED_COLOR; - case MatchedProductStatusV2.UNKNOWN_MATCH: - return FAIR_GREY_COLOR; - } - } - - Color _getLightColors() { - // TODO(monsieurtanuki): difference between dark and light - return _getDarkColors(); - } - String getHeaderText(final AppLocalizations appLocalizations) { switch (status) { case MatchedProductStatusV2.VERY_GOOD_MATCH: diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 0e662c907ab8..c473f6d0ec03 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -3073,5 +3073,9 @@ "photo_viewer_details_url_title": "URL", "@photo_viewer_details_url_title": { "description": "Label for the link of a photo" + }, + "product_page_compatibility_score": "Compatible", + "@product_page_compatibility_score": { + "description": "Compatibility score on top of the product page. The sentence is \"100%\" Compatible" } } \ No newline at end of file diff --git a/packages/smooth_app/lib/pages/personalized_ranking_page.dart b/packages/smooth_app/lib/pages/personalized_ranking_page.dart index 8441d94c5167..cc4a9d31eae8 100644 --- a/packages/smooth_app/lib/pages/personalized_ranking_page.dart +++ b/packages/smooth_app/lib/pages/personalized_ranking_page.dart @@ -241,7 +241,7 @@ class _PersonalizedRankingPageState extends State barcode: matchedProduct.barcode, backgroundColor: ProductCompatibilityHelper.status(matchedProduct.status) - .getHeaderBackgroundColor(darkMode) + .getColor(context) .withAlpha(_backgroundAlpha), ), ); diff --git a/packages/smooth_app/lib/pages/product/compare_products3_page.dart b/packages/smooth_app/lib/pages/product/compare_products3_page.dart index 56b967f430ae..c69c6f38d60a 100644 --- a/packages/smooth_app/lib/pages/product/compare_products3_page.dart +++ b/packages/smooth_app/lib/pages/product/compare_products3_page.dart @@ -69,8 +69,6 @@ class _CompareProducts3PageState extends State { final AppLocalizations appLocalizations = AppLocalizations.of(context); context.watch(); - final bool darkMode = Theme.of(context).brightness == Brightness.dark; - final ProductPreferences productPreferences = context.watch(); final List> scoreAttributesArray = >[]; @@ -85,7 +83,7 @@ class _CompareProducts3PageState extends State { scoreWidgets.add( Expanded( child: Container( - color: helper.getHeaderBackgroundColor(darkMode), + color: helper.getColor(context), child: Center( child: Text( matchedProduct.score.toInt().toString(), diff --git a/packages/smooth_app/lib/pages/product/product_compatibility_header.dart b/packages/smooth_app/lib/pages/product/product_compatibility_header.dart index ebd720c34d5e..e01324bc84ac 100644 --- a/packages/smooth_app/lib/pages/product/product_compatibility_header.dart +++ b/packages/smooth_app/lib/pages/product/product_compatibility_header.dart @@ -25,6 +25,7 @@ class ProductCompatibilityHeader extends StatelessWidget { product, productPreferences, ); + final ProductCompatibilityHelper helper = ProductCompatibilityHelper.product(matchedProduct); final AppLocalizations appLocalizations = AppLocalizations.of(context); @@ -33,7 +34,7 @@ class ProductCompatibilityHeader extends StatelessWidget { return Ink( decoration: BoxDecoration( - color: helper.getHeaderBackgroundColor(isDarkMode), + color: helper.getColor(context), // Ensure that the header has the same circular radius as the SmoothCard. borderRadius: const BorderRadiusDirectional.only( topStart: ROUNDED_RADIUS, diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_footer.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_footer.dart index 2474a9d47e25..0dc3e526ea1c 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_footer.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_footer.dart @@ -365,6 +365,8 @@ class _ProductFooterFilledButton extends StatelessWidget { Widget build(BuildContext context) { final SmoothColorsThemeExtension themeExtension = Theme.of(context).extension()!; + final ProductPageCompatibility compatibility = + context.watch(); return Semantics( excludeSemantics: true, @@ -374,9 +376,11 @@ class _ProductFooterFilledButton extends StatelessWidget { onPressed: onTap, style: OutlinedButton.styleFrom( foregroundColor: Colors.white, - backgroundColor: context.lightTheme() - ? themeExtension.primaryBlack - : themeExtension.primarySemiDark, + backgroundColor: compatibility.score > 0 + ? compatibility.color + : context.lightTheme() + ? themeExtension.primaryBlack + : themeExtension.primarySemiDark, side: BorderSide.none, ), child: Row( diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_header.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_header.dart new file mode 100644 index 000000000000..00e784fef71d --- /dev/null +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_header.dart @@ -0,0 +1,298 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart' hide Listener; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/helpers/num_utils.dart'; +import 'package:smooth_app/helpers/provider_helper.dart'; +import 'package:smooth_app/pages/navigator/app_navigator.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; +import 'package:smooth_app/pages/product/product_page/new_product_page.dart'; +import 'package:smooth_app/resources/app_icons.dart' as icons; +import 'package:smooth_app/themes/constant_icons.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +class ProductHeader extends StatefulWidget { + const ProductHeader({ + super.key, + }); + + @override + State createState() => _ProductHeaderState(); +} + +class _ProductHeaderState extends State { + double _titleOpacity = 0.0; + double _compatibilityScoreOpacity = 0.0; + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.viewPaddingOf(context).top; + + return VisibilityDetector( + key: const Key('product_header'), + onVisibilityChanged: _onVisibilityChanged, + child: Listener( + listener: ( + _, + __, + ScrollController scrollController, + ) => + _onScroll(scrollController), + child: Consumer( + builder: (BuildContext context, + ProductPageCompatibility productCompatibility, _) { + return Material( + color: productCompatibility.color, + child: DefaultTextStyle.merge( + style: const TextStyle(color: Colors.white), + child: IconTheme( + data: const IconThemeData(color: Colors.white), + child: SizedBox( + height: kToolbarHeight + statusBarHeight, + child: Padding( + padding: EdgeInsetsDirectional.only(top: statusBarHeight), + child: Row( + children: [ + const _ProductHeaderBackButton(), + Expanded( + child: Offstage( + offstage: _titleOpacity == 0.0, + child: Opacity( + opacity: _titleOpacity, + child: const _ProductHeaderName(), + ), + ), + ), + if (productCompatibility.score > 0) + _ProductCompatibilityScore( + progress: _compatibilityScoreOpacity, + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ), + ); + } + + void _onScroll(ScrollController scrollController) { + /// Get the title opacity depending on the scroll position + final double titleOpacity = + scrollController.offset.progressAndClamp(20.0, kToolbarHeight, 1.0); + final double compatibilityScoreOpacity = + scrollController.offset.progressAndClamp(20.0, kToolbarHeight * 2, 1.0); + + if (_titleOpacity != titleOpacity || + _compatibilityScoreOpacity != compatibilityScoreOpacity) { + _titleOpacity = titleOpacity; + _compatibilityScoreOpacity = compatibilityScoreOpacity; + + // Calling setState() may already be in a build() call + SchedulerBinding.instance.addPostFrameCallback((_) { + setState(() {}); + }); + } + } + + /// Change the status bar to a transparent one + void _onVisibilityChanged(VisibilityInfo info) { + if (info.visibleFraction == 1.0) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + ), + ); + } + } +} + +class _ProductHeaderBackButton extends StatelessWidget { + const _ProductHeaderBackButton(); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 56.0, + child: Tooltip( + message: MaterialLocalizations.of(context).backButtonTooltip, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + Navigator.of(context).maybePop(); + }, + child: SizedBox.expand( + child: Icon(ConstantIcons.instance.getBackIcon()), + ), + ), + ), + ); + } +} + +class _ProductHeaderName extends StatelessWidget { + const _ProductHeaderName(); + + @override + Widget build(BuildContext context) { + return ConsumerFilter( + buildWhen: (Product? previousValue, Product currentValue) { + return previousValue?.brands != currentValue.brands || + previousValue?.productName != currentValue.productName; + }, + builder: (BuildContext context, Product product, _) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.productName ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18.0, + height: 0.9, + ), + ), + Text( + product.brands ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16.5, + ), + ), + ], + ); + }, + ); + } +} + +class _ProductCompatibilityScore extends StatelessWidget { + const _ProductCompatibilityScore({ + required this.progress, + }); + + //ignore: constant_identifier_names + static const double MAX_WIDTH = 40.0; + static const EdgeInsetsGeometry PADDING = EdgeInsetsDirectional.only( + start: MEDIUM_SPACE, + end: BALANCED_SPACE, + ); + + final double progress; + + @override + Widget build(BuildContext context) { + final ProductPageCompatibility compatibility = + context.watch(); + + return Padding( + padding: PADDING, + child: SizedBox( + width: computeWidth(context) + (MAX_WIDTH * progress), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: ROUNDED_BORDER_RADIUS, + border: Border.all(color: Colors.white), + ), + child: InkWell( + onTap: () => AppNavigator.of(context).push( + AppRoutes.PREFERENCES(PreferencePageType.FOOD), + ), + borderRadius: ROUNDED_BORDER_RADIUS, + child: ClipRRect( + borderRadius: ROUNDED_BORDER_RADIUS, + child: _getScoreWidget(context, compatibility), + ), + ), + ), + ), + ); + } + + Widget _getScoreWidget( + BuildContext context, + ProductPageCompatibility compatibility, + ) { + return IntrinsicHeight( + child: Row( + children: [ + Opacity( + opacity: progress, + child: Container( + width: MAX_WIDTH * progress, + height: double.infinity, + alignment: Alignment.center, + padding: const EdgeInsetsDirectional.only(start: 2.5), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadiusDirectional.horizontal( + start: Radius.circular(18.0), + ), + ), + child: Transform.translate( + offset: Offset((1 - progress) * 10, 0.0), + child: SizedBox( + child: icons.Info( + color: compatibility.color, + ), + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 6.0, + bottom: 8.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${compatibility.score.toInt()}%', + style: const TextStyle( + fontSize: 12.0, + height: 0.9, + fontWeight: FontWeight.bold, + ), + ), + Text( + AppLocalizations.of(context) + .product_page_compatibility_score, + style: const TextStyle( + fontSize: 9.0, + height: 0.9, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + double computeWidth(BuildContext context) { + return math.min( + 80.0, + (MediaQuery.sizeOf(context).width - PADDING.horizontal) * (18 / 100), + ); + } +} diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart index 893a684006bc..0b8b82cd64c2 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; -import 'package:smooth_app/cards/product_cards/product_image_carousel.dart'; +import 'package:provider/single_child_widget.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/data_models/product_list.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; @@ -14,12 +13,12 @@ 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/helpers/product_compatibility_helper.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/pages/prices/prices_card.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/product_page/new_product_footer.dart'; +import 'package:smooth_app/pages/product/product_page/new_product_header.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'; @@ -52,13 +51,11 @@ class ProductPage extends StatefulWidget { class ProductPageState extends State with TraceableClientMixin, UpToDateMixin { - final ScrollController _carouselController = ScrollController(); - + final ScrollController _scrollController = ScrollController(); late ProductPreferences _productPreferences; late ProductQuestionsLayout questionsLayout; bool _keepRobotoffQuestionsAlive = true; - bool scrollingUp = true; double bottomPadding = 0.0; @override @@ -81,7 +78,6 @@ class ProductPageState extends State final ExternalScanCarouselManagerState carouselManager = ExternalScanCarouselManager.read(context); carouselManager.currentBarcode = barcode; - final ThemeData themeData = Theme.of(context); final SmoothColorsThemeExtension themeExtension = Theme.of(context).extension()!; @@ -89,79 +85,63 @@ class ProductPageState extends State context.watch(); refreshUpToDate(); + final MatchedProductV2 matchedProductV2 = MatchedProductV2( + upToDateProduct, + _productPreferences, + ); + return MultiProvider( - providers: >[ + providers: [ Provider.value(value: upToDateProduct), Provider.value(value: this), + Provider.value( + value: ProductPageCompatibility( + color: ProductCompatibilityHelper.product( + matchedProductV2, + ).getColor(context), + score: matchedProductV2.score, + ), + ), + ChangeNotifierProvider.value( + value: _scrollController, + ), ], - child: Provider.value( - value: upToDateProduct, - child: SmoothScaffold( - contentBehindStatusBar: true, - spaceBehindStatusBar: false, - statusBarBackgroundColor: SmoothScaffold.semiTranslucentStatusBar, - backgroundColor: - !context.darkTheme() ? themeExtension.primaryLight : null, - body: Stack( - children: [ - NotificationListener( - onNotification: (UserScrollNotification notification) { - if (notification.direction == ScrollDirection.forward) { - if (!scrollingUp) { - setState(() => scrollingUp = true); - } - } else if (notification.direction == - ScrollDirection.reverse) { - if (scrollingUp) { - setState(() => scrollingUp = false); + child: SmoothScaffold( + contentBehindStatusBar: true, + spaceBehindStatusBar: false, + changeStatusBarBrightness: false, + statusBarBackgroundColor: Colors.transparent, + backgroundColor: + !context.darkTheme() ? themeExtension.primaryLight : null, + body: Stack( + children: [ + _buildProductBody(context), + const Positioned( + left: 0.0, + right: 0.0, + top: 0.0, + child: ProductHeader(), + ), + if (questionsLayout == ProductQuestionsLayout.banner) + Positioned( + left: 0.0, + right: 0.0, + bottom: 0.0, + child: MeasureSize( + onChange: (Size size) { + if (size.height != bottomPadding) { + setState(() => bottomPadding = size.height); } - } - return true; - }, - child: _buildProductBody(context), - ), - Padding( - padding: const EdgeInsetsDirectional.only(start: SMALL_SPACE), - child: SafeArea( - child: AnimatedContainer( - duration: SmoothAnimationsDuration.short, - width: kToolbarHeight, - height: kToolbarHeight, - decoration: BoxDecoration( - color: scrollingUp - ? themeData.primaryColor - : Colors.transparent, - shape: BoxShape.circle, - ), - child: Offstage( - offstage: !scrollingUp, - child: const SmoothBackButton(iconColor: Colors.white), - ), + }, + child: ProductQuestionsWidget( + upToDateProduct, + layout: ProductQuestionsLayout.banner, ), ), ), - if (questionsLayout == ProductQuestionsLayout.banner) - Positioned.directional( - start: 0.0, - end: 0.0, - bottom: 0.0, - textDirection: Directionality.of(context), - child: MeasureSize( - onChange: (Size size) { - if (size.height != bottomPadding) { - setState(() => bottomPadding = size.height); - } - }, - child: ProductQuestionsWidget( - upToDateProduct, - layout: ProductQuestionsLayout.banner, - ), - ), - ), - ], - ), - bottomNavigationBar: const ProductFooter(), + ], ), + bottomNavigationBar: const ProductFooter(), ), ); } @@ -181,85 +161,81 @@ class ProductPageState extends State final AppLocalizations appLocalizations = AppLocalizations.of(context); final UserPreferences userPreferences = context.watch(); - return RefreshIndicator( - onRefresh: () async => ProductRefresher().fetchAndRefresh( - barcode: barcode, - context: context, - ), - child: ListView( - // /!\ Smart Dart - // `physics: const AlwaysScrollableScrollPhysics()` - // means that we will always scroll, even if it's pointless. - // Why do we need to? For the RefreshIndicator, that wouldn't be - // triggered on a ListView smaller than the screen - // (as there will be no scroll). - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.only( - bottom: SMALL_SPACE, + return SafeArea( + child: RefreshIndicator( + onRefresh: () async => ProductRefresher().fetchAndRefresh( + barcode: barcode, + context: context, ), - children: [ - Align( - heightFactor: 0.7, - alignment: AlignmentDirectional.topStart, - child: ProductImageCarousel( - upToDateProduct, - height: 200, - controller: _carouselController, - ), + child: ListView( + // /!\ Smart Dart + // `physics: const AlwaysScrollableScrollPhysics()` + // means that we will always scroll, even if it's pointless. + // Why do we need to? For the RefreshIndicator, that wouldn't be + // triggered on a ListView smaller than the screen + // (as there will be no scroll). + physics: const AlwaysScrollableScrollPhysics(), + controller: _scrollController, + padding: const EdgeInsets.only( + top: kToolbarHeight + SMALL_SPACE, + bottom: SMALL_SPACE, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: SMALL_SPACE, - ), - child: HeroMode( - enabled: widget.withHeroAnimation && - widget.heroTag?.isNotEmpty == true, - child: Hero( - tag: widget.heroTag ?? '', - child: KeepQuestionWidgetAlive( - keepWidgetAlive: _keepRobotoffQuestionsAlive, - child: SummaryCard( - upToDateProduct, - _productPreferences, - isFullVersion: true, - showQuestionsBanner: true, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: SMALL_SPACE, + ), + child: HeroMode( + enabled: widget.withHeroAnimation && + widget.heroTag?.isNotEmpty == true, + child: Hero( + tag: widget.heroTag ?? '', + child: KeepQuestionWidgetAlive( + keepWidgetAlive: _keepRobotoffQuestionsAlive, + child: SummaryCard( + upToDateProduct, + _productPreferences, + isFullVersion: true, + showQuestionsBanner: true, + showCompatibilityHeader: false, + ), ), ), ), ), - ), - 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!), - PricesCard(upToDateProduct), - 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), + 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!), + PricesCard(upToDateProduct), + 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), + ), ), ), ), - ), - if (questionsLayout == ProductQuestionsLayout.banner) - // assuming it's tall enough in order to go above the banner - const SizedBox(height: 4 * VERY_LARGE_SPACE), - ], + if (questionsLayout == ProductQuestionsLayout.banner) + // assuming it's tall enough in order to go above the banner + const SizedBox(height: 4 * VERY_LARGE_SPACE), + ], + ), ), ); } @@ -279,3 +255,26 @@ class ProductPageState extends State return result!; } } + +class ProductPageCompatibility { + ProductPageCompatibility({ + required this.color, + required this.score, + }); + + final Color color; + final double score; + + @override + //ignore: avoid_equals_and_hash_code_on_mutable_classes (false positive) + bool operator ==(Object other) => + identical(this, other) || + other is ProductPageCompatibility && + runtimeType == other.runtimeType && + color == other.color && + score == other.score; + + @override + //ignore: avoid_equals_and_hash_code_on_mutable_classes (false positive) + int get hashCode => color.hashCode ^ score.hashCode; +} diff --git a/packages/smooth_app/lib/pages/product/summary_card.dart b/packages/smooth_app/lib/pages/product/summary_card.dart index 43fa4841c773..5cf9c944118f 100644 --- a/packages/smooth_app/lib/pages/product/summary_card.dart +++ b/packages/smooth_app/lib/pages/product/summary_card.dart @@ -42,6 +42,7 @@ class SummaryCard extends StatefulWidget { this._productPreferences, { this.isFullVersion = false, this.showQuestionsBanner = false, + this.showCompatibilityHeader = true, this.isRemovable = true, this.isSettingVisible = true, this.isProductEditable = true, @@ -74,6 +75,9 @@ class SummaryCard extends StatefulWidget { /// If true, all chips / groups are clickable final bool attributeGroupsClickable; + /// If true, the compatibility header will be shown + final bool showCompatibilityHeader; + final EdgeInsetsGeometry? padding; /// An optional shadow to apply to the card @@ -107,11 +111,13 @@ class _SummaryCardState extends State with UpToDateMixin { refreshUpToDate(); if (widget.isFullVersion) { return buildProductSmoothCard( - header: ProductCompatibilityHeader( - product: upToDateProduct, - productPreferences: widget._productPreferences, - isSettingVisible: widget.isSettingVisible, - ), + header: widget.showCompatibilityHeader + ? ProductCompatibilityHeader( + product: upToDateProduct, + productPreferences: widget._productPreferences, + isSettingVisible: widget.isSettingVisible, + ) + : null, body: Padding( padding: widget.padding ?? SMOOTH_CARD_PADDING, child: _buildSummaryCardContent(context), diff --git a/packages/smooth_app/lib/themes/smooth_theme_colors.dart b/packages/smooth_app/lib/themes/smooth_theme_colors.dart index 76637bd70e0a..d65d73ec0d79 100644 --- a/packages/smooth_app/lib/themes/smooth_theme_colors.dart +++ b/packages/smooth_app/lib/themes/smooth_theme_colors.dart @@ -16,6 +16,7 @@ class SmoothColorsThemeExtension required this.orange, required this.red, required this.greyDark, + required this.greyNormal, required this.greyLight, }); @@ -33,20 +34,27 @@ class SmoothColorsThemeExtension orange = const Color(0xFFFB8229), red = const Color(0xFFEB5757), greyDark = const Color(0xFF666666), + greyNormal = const Color(0xFF6C6C6C), greyLight = const Color(0xFF8F8F8F); // Ristreto final Color primaryUltraBlack; + // Chocolate final Color primaryBlack; + // Cortado final Color primaryDark; + // Mocha final Color primarySemiDark; + // Macchiato final Color primaryNormal; + // Cappuccino final Color primaryMedium; + // Latte final Color primaryLight; final Color secondaryNormal; @@ -55,6 +63,7 @@ class SmoothColorsThemeExtension final Color orange; final Color red; final Color greyDark; + final Color greyNormal; final Color greyLight; @override @@ -72,6 +81,7 @@ class SmoothColorsThemeExtension Color? orange, Color? red, Color? greyDark, + Color? greyNormal, Color? greyLight, }) { return SmoothColorsThemeExtension( @@ -88,6 +98,7 @@ class SmoothColorsThemeExtension orange: orange ?? this.orange, red: red ?? this.red, greyDark: greyDark ?? this.greyDark, + greyNormal: greyDark ?? this.greyDark, greyLight: greyLight ?? this.greyLight, ); } @@ -167,6 +178,11 @@ class SmoothColorsThemeExtension other.greyDark, t, )!, + greyNormal: Color.lerp( + greyNormal, + other.greyNormal, + t, + )!, greyLight: Color.lerp( greyLight, other.greyLight, diff --git a/packages/smooth_app/lib/widgets/smooth_scaffold.dart b/packages/smooth_app/lib/widgets/smooth_scaffold.dart index 08ef268854fc..2c75e2d2e08b 100644 --- a/packages/smooth_app/lib/widgets/smooth_scaffold.dart +++ b/packages/smooth_app/lib/widgets/smooth_scaffold.dart @@ -11,6 +11,7 @@ class SmoothScaffold extends Scaffold { this.contentBehindStatusBar = false, this.spaceBehindStatusBar = false, this.fixKeyboard = false, + this.changeStatusBarBrightness = true, bool? resizeToAvoidBottomInset, super.key, super.appBar, @@ -52,6 +53,7 @@ class SmoothScaffold extends Scaffold { final Color? statusBarBackgroundColor; final bool contentBehindStatusBar; final bool spaceBehindStatusBar; + final bool changeStatusBarBrightness; /// On some screens an extra padding maybe wrongly added when the keyboard is /// visible @@ -120,6 +122,10 @@ class SmoothScaffoldState extends ScaffoldState { } } + if (!_changeStatusBarBrightness) { + return child; + } + return AnnotatedRegion( value: _overlayStyle, child: Theme( @@ -139,6 +145,9 @@ class SmoothScaffoldState extends ScaffoldState { bool get _spaceBehindStatusBar => (widget as SmoothScaffold).spaceBehindStatusBar == true; + bool get _changeStatusBarBrightness => + (widget as SmoothScaffold).changeStatusBarBrightness == true; + Brightness? get _brightness => (widget as SmoothScaffold).brightness ?? SmoothBrightnessOverride.of(context)?.brightness;