From 3b8eb1db8b4ab04008bece9fbfed053693373b5d Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Mon, 28 Oct 2024 17:44:47 +0100 Subject: [PATCH] Improvements for the product page --- .../lib/helpers/product_cards_helper.dart | 117 ++++++++++--- .../knowledge_panel_group_card.dart | 14 +- .../knowledge_panel_product_cards.dart | 38 +---- .../lib/pages/prices/prices_card.dart | 154 ++++++++++-------- .../product_page/new_product_footer.dart | 37 +++-- .../product_page/new_product_page.dart | 2 - .../lib/pages/product/website_card.dart | 90 +++++----- 7 files changed, 270 insertions(+), 182 deletions(-) diff --git a/packages/smooth_app/lib/helpers/product_cards_helper.dart b/packages/smooth_app/lib/helpers/product_cards_helper.dart index 4f387736d44..64bf8acd5f3 100644 --- a/packages/smooth_app/lib/helpers/product_cards_helper.dart +++ b/packages/smooth_app/lib/helpers/product_cards_helper.dart @@ -10,6 +10,8 @@ import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/helpers/image_field_extension.dart'; import 'package:smooth_app/helpers/ui_helpers.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; SmoothAppBar buildEditProductAppBar({ @@ -88,26 +90,99 @@ const EdgeInsets SMOOTH_CARD_PADDING = EdgeInsets.symmetric( /// A SmoothCard on Product cards using default margin and padding. Widget buildProductSmoothCard({ Widget? header, + Widget? title, + EdgeInsetsGeometry? titlePadding, required Widget body, - EdgeInsets? padding = EdgeInsets.zero, - EdgeInsets? margin = const EdgeInsets.symmetric( + EdgeInsetsGeometry? padding = EdgeInsets.zero, + EdgeInsetsGeometry? margin = const EdgeInsets.symmetric( horizontal: SMALL_SPACE, ), -}) => - SmoothCard( - margin: margin, - padding: padding, - child: switch (header) { - Object _ => Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (header != null) header, - body, - ], +}) { + assert( + (header != null && title == null) || header == null, + "You can't pass a header and a title at the same time", + ); + + Widget child; + + if (title != null) { + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + _ProductSmoothCardTitle( + title: title, + padding: titlePadding, + ), + body, + ], + ); + } else if (header != null) { + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + header, + body, + ], + ); + } else { + child = body; + } + + return SmoothCard( + margin: margin, + padding: padding, + child: child, + ); +} + +class _ProductSmoothCardTitle extends StatelessWidget { + const _ProductSmoothCardTitle({ + required this.title, + this.padding, + }); + + final Widget title; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + final SmoothColorsThemeExtension colors = + Theme.of(context).extension()!; + final EdgeInsetsGeometry effectivePadding = padding ?? + const EdgeInsetsDirectional.symmetric( + vertical: MEDIUM_SPACE, + ); + final TextStyle titleStyle = + Theme.of(context).textTheme.displaySmall ?? const TextStyle(); + final double fontSize = titleStyle.fontSize ?? 15.0; + + return Container( + constraints: BoxConstraints( + minHeight: + MEDIUM_SPACE * 2 + MediaQuery.textScalerOf(context).scale(fontSize), + ), + decoration: BoxDecoration( + color: context.lightTheme() + ? colors.primaryMedium + : colors.primarySemiDark, + borderRadius: const BorderRadius.vertical( + top: ROUNDED_RADIUS, + ), + ), + padding: effectivePadding, + child: Center( + child: DefaultTextStyle( + style: titleStyle, + textAlign: TextAlign.center, + child: SizedBox( + width: double.infinity, + child: title, ), - _ => body - }, + ), + ), ); + } +} // used to be in now defunct `AttributeListExpandable` List getPopulatedAttributes( @@ -170,7 +245,7 @@ List getSortedAttributes( } final Map> mandatoryAttributesByGroup = >{}; - // collecting all the mandatory attributes, by group +// collecting all the mandatory attributes, by group for (final AttributeGroup attributeGroup in product.attributeGroups!) { mandatoryAttributesByGroup[attributeGroup.id!] = getFilteredAttributes( attributeGroup, @@ -181,7 +256,7 @@ List getSortedAttributes( ); } - // now ordering by attribute group order +// now ordering by attribute group order for (final String attributeGroupId in attributeGroupOrder) { final List? attributes = mandatoryAttributesByGroup[attributeGroupId]; @@ -266,7 +341,7 @@ ProductImageData getProductImageData( language, ); if (productImage != null) { - // we found a localized version for this image +// we found a localized version for this image return ProductImageData( imageId: productImage.imgid, imageField: imageField, @@ -344,7 +419,7 @@ List getRawProductImages( for (final ProductImage productImage in rawImages) { final int? imageId = int.tryParse(productImage.imgid!); if (imageId == null) { - // highly unlikely +// highly unlikely continue; } final ProductImage? previous = map[imageId]; @@ -354,12 +429,12 @@ List getRawProductImages( } final ImageSize? currentImageSize = productImage.size; if (currentImageSize == null) { - // highly unlikely +// highly unlikely continue; } final ImageSize? previousImageSize = previous.size; if (previousImageSize == imageSize) { - // we already have the best +// we already have the best continue; } map[imageId] = productImage; 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 6c9ef63db4f..4b6d177491c 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 @@ -4,6 +4,8 @@ import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_card.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; class KnowledgePanelGroupCard extends StatelessWidget { const KnowledgePanelGroupCard({ @@ -19,6 +21,9 @@ class KnowledgePanelGroupCard extends StatelessWidget { @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); + final SmoothColorsThemeExtension themeExtension = + themeData.extension()!; + return Provider( lazy: true, create: (_) => groupElement, @@ -32,8 +37,13 @@ class KnowledgePanelGroupCard extends StatelessWidget { explicitChildNodes: true, child: Text( groupElement.title!, - style: - themeData.textTheme.titleSmall!.apply(color: Colors.grey), + style: themeData.textTheme.titleSmall!.copyWith( + fontSize: 15.5, + fontWeight: FontWeight.w700, + color: context.lightTheme() + ? themeExtension.primaryUltraBlack + : themeExtension.primaryLight, + ), ), ), ), diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_product_cards.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_product_cards.dart index 9cd29efdf45..250634b25a5 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_product_cards.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_product_cards.dart @@ -4,7 +4,6 @@ import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; import 'package:smooth_app/themes/smooth_theme_colors.dart'; -import 'package:smooth_app/themes/theme_provider.dart'; class KnowledgePanelProductCards extends StatelessWidget { const KnowledgePanelProductCards(this.knowledgePanelWidgets); @@ -26,31 +25,12 @@ class KnowledgePanelProductCards extends StatelessWidget { if (hasTitle) { content = buildProductSmoothCard( - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - color: context.lightTheme() - ? colors.primaryMedium - : colors.primarySemiDark, - borderRadius: const BorderRadius.vertical( - top: ROUNDED_RADIUS, - ), - ), - width: double.infinity, - padding: const EdgeInsetsDirectional.symmetric( - vertical: SMALL_SPACE, - ), - child: Center(child: widget.children.first), - ), - Padding( - padding: SMOOTH_CARD_PADDING, - child: Column( - children: widget.children.sublist(1), - ), - ), - ], + title: Text((widget.children.first as KnowledgePanelTitle).title), + body: Padding( + padding: SMOOTH_CARD_PADDING, + child: Column( + children: widget.children.sublist(1), + ), ), padding: EdgeInsets.zero, margin: EdgeInsets.zero, @@ -71,10 +51,8 @@ class KnowledgePanelProductCards extends StatelessWidget { return Center( child: Padding( - padding: const EdgeInsetsDirectional.only( - bottom: SMALL_SPACE, - start: SMALL_SPACE, - end: SMALL_SPACE, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: SMALL_SPACE, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/packages/smooth_app/lib/pages/prices/prices_card.dart b/packages/smooth_app/lib/pages/prices/prices_card.dart index 1337706f7c5..cdbb73a386e 100644 --- a/packages/smooth_app/lib/pages/prices/prices_card.dart +++ b/packages/smooth_app/lib/pages/prices/prices_card.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -9,8 +11,9 @@ import 'package:smooth_app/pages/prices/get_prices_model.dart'; import 'package:smooth_app/pages/prices/price_meta_product.dart'; import 'package:smooth_app/pages/prices/prices_page.dart'; import 'package:smooth_app/pages/prices/product_price_add_page.dart'; -import 'package:smooth_app/resources/app_icons.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'; /// Card that displays buttons related to prices. class PricesCard extends StatelessWidget { @@ -21,88 +24,109 @@ class PricesCard extends StatelessWidget { @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); - final SmoothColorsThemeExtension? themeExtension = - Theme.of(context).extension(); + final SmoothColorsThemeExtension themeExtension = + Theme.of(context).extension()!; return buildProductSmoothCard( + title: Stack( + children: [ + Positioned.directional( + textDirection: Directionality.of(context), + start: LARGE_SPACE, + child: Container( + decoration: BoxDecoration( + color: themeExtension.orange, + shape: BoxShape.circle, + ), + padding: const EdgeInsetsDirectional.only( + top: 5.0, + start: 6.0, + end: 6.0, + bottom: 7.0, + ), + child: const icons.Lab( + size: 10.0, + color: Colors.white, + ), + ), + ), + const SizedBox(width: SMALL_SPACE), + Center(child: Text(appLocalizations.prices_generic_title)), + ], + ), body: Container( width: double.infinity, padding: const EdgeInsetsDirectional.all(LARGE_SPACE), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, + child: Stack( children: [ - Row( - mainAxisSize: MainAxisSize.min, + Positioned.directional( + textDirection: Directionality.of(context), + bottom: 0.0, + end: 0.0, + child: const _PricesCardTitleIcon(), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ - Text( - AppLocalizations.of(context).prices_generic_title, - style: Theme.of(context).textTheme.displaySmall, - ), - const SizedBox(width: SMALL_SPACE), - Container( - decoration: BoxDecoration( - color: themeExtension!.secondaryNormal, - borderRadius: CIRCULAR_BORDER_RADIUS, - ), - margin: const EdgeInsets.only(top: 0.5), - padding: const EdgeInsets.symmetric( - horizontal: MEDIUM_SPACE, - vertical: VERY_SMALL_SPACE, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - appLocalizations.preview_badge, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, + Padding( + padding: const EdgeInsets.symmetric(horizontal: SMALL_SPACE), + child: SmoothLargeButtonWithIcon( + text: appLocalizations.prices_view_prices, + icon: CupertinoIcons.tag_fill, + onPressed: () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => PricesPage( + GetPricesModel.product( + product: PriceMetaProduct.product(product), + context: context, + ), ), ), - const SizedBox(width: SMALL_SPACE), - const Lab( - color: Colors.white, - size: 13.0, - ), - ], + ), ), ), - ], - ), - const SizedBox(height: SMALL_SPACE), - Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: SmoothLargeButtonWithIcon( - text: appLocalizations.prices_view_prices, - icon: CupertinoIcons.tag_fill, - onPressed: () async => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => PricesPage( - GetPricesModel.product( - product: PriceMetaProduct.product(product), - context: context, - ), + Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: SmoothLargeButtonWithIcon( + text: appLocalizations.prices_add_a_price, + icon: Icons.add, + onPressed: () async => ProductPriceAddPage.showProductPage( + context: context, + product: PriceMetaProduct.product(product), + proofType: ProofType.priceTag, ), ), ), - ), - ), - Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: SmoothLargeButtonWithIcon( - text: appLocalizations.prices_add_a_price, - icon: Icons.add, - onPressed: () async => ProductPriceAddPage.showProductPage( - context: context, - product: PriceMetaProduct.product(product), - proofType: ProofType.priceTag, - ), - ), + ], ), ], ), ), + margin: const EdgeInsetsDirectional.only( + start: SMALL_SPACE, + end: SMALL_SPACE, + top: VERY_LARGE_SPACE, + ), + ); + } +} + +class _PricesCardTitleIcon extends StatelessWidget { + const _PricesCardTitleIcon(); + + @override + Widget build(BuildContext context) { + final SmoothColorsThemeExtension? themeExtension = + Theme.of(context).extension(); + + return Transform.rotate( + angle: -pi / 6, + child: icons.Lab( + size: 100.0, + color: themeExtension?.orange + .withOpacity(context.lightTheme() ? 0.15 : 0.4), + ), ); } } 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 520f8fbfafb..6fc9f7f4a58 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 @@ -25,33 +25,23 @@ import 'package:smooth_app/themes/theme_provider.dart'; class ProductFooter extends StatelessWidget { const ProductFooter({super.key}); - static const double kHeight = 46.0; + static const double kHeight = 48.0; @override Widget build(BuildContext context) { - double bottomPadding = MediaQuery.viewPaddingOf(context).bottom; - // Add an extra padding (for Android) - if (bottomPadding == 0.0) { - bottomPadding = 16.0; - } - return DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, boxShadow: [ BoxShadow( - color: Theme.of(context).shadowColor.withOpacity(0.1), + color: Theme.of(context) + .shadowColor + .withOpacity(context.lightTheme() ? 0.25 : 0.6), blurRadius: 10.0, ), ], ), - child: Padding( - padding: EdgeInsetsDirectional.only( - top: 16.0, - bottom: bottomPadding, - ), - child: const _ProductFooterButtonsBar(), - ), + child: const _ProductFooterButtonsBar(), ); } } @@ -64,8 +54,14 @@ class _ProductFooterButtonsBar extends StatelessWidget { final SmoothColorsThemeExtension themeExtension = Theme.of(context).extension()!; + double bottomPadding = MediaQuery.viewPaddingOf(context).bottom; + // Add an extra padding (for Android) + if (bottomPadding == 0.0) { + bottomPadding = LARGE_SPACE; + } + return SizedBox( - height: ProductFooter.kHeight, + height: ProductFooter.kHeight + LARGE_SPACE + bottomPadding, child: OutlinedButtonTheme( data: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( @@ -80,8 +76,12 @@ class _ProductFooterButtonsBar extends StatelessWidget { ), ), child: ListView( - padding: - const EdgeInsetsDirectional.symmetric(horizontal: SMALL_SPACE), + padding: EdgeInsetsDirectional.only( + start: SMALL_SPACE, + end: SMALL_SPACE, + top: LARGE_SPACE, + bottom: bottomPadding, + ), scrollDirection: Axis.horizontal, children: const [ SizedBox(width: 10.0), @@ -383,6 +383,7 @@ class _ProductFooterFilledButton extends StatelessWidget { side: BorderSide.none, ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconTheme( data: const IconThemeData( 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 837922549db..2747e07c2f3 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 @@ -258,8 +258,6 @@ class ProductPageState extends State if (questionsLayout == ProductQuestionsLayout.banner) // assuming it's tall enough in order to go above the banner const SizedBox(height: 4 * VERY_LARGE_SPACE), - // Space for the navigation bar - SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ), ); diff --git a/packages/smooth_app/lib/pages/product/website_card.dart b/packages/smooth_app/lib/pages/product/website_card.dart index 28f3d400db0..df800cc25db 100644 --- a/packages/smooth_app/lib/pages/product/website_card.dart +++ b/packages/smooth_app/lib/pages/product/website_card.dart @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; +import 'package:smooth_app/resources/app_icons.dart' as icons; /// Card that displays a website link. class WebsiteCard extends StatelessWidget { @@ -15,58 +16,59 @@ class WebsiteCard extends StatelessWidget { final AppLocalizations localizations = AppLocalizations.of(context); final String website = _getWebsite(); - return buildProductSmoothCard( - body: Semantics( - label: localizations.product_field_website_title, - value: Uri.parse(website).host, - link: true, - excludeSemantics: true, + return Semantics( + label: localizations.product_field_website_title, + value: Uri.parse(website).host, + link: true, + excludeSemantics: true, + child: Padding( + padding: const EdgeInsetsDirectional.only( + top: VERY_LARGE_SPACE, + start: SMALL_SPACE, + end: SMALL_SPACE, + ), child: InkWell( onTap: () async => LaunchUrlHelper.launchURL(website), borderRadius: ROUNDED_BORDER_RADIUS, - child: Container( - width: double.infinity, - padding: const EdgeInsetsDirectional.only( - start: LARGE_SPACE, - top: LARGE_SPACE, - bottom: LARGE_SPACE, - // To be perfectly aligned with arrows - end: 21.0, - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - localizations.product_field_website_title, - style: Theme.of(context).textTheme.displaySmall, - ), - const SizedBox(height: SMALL_SPACE), - Text( - website, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Colors.blue), - ), - ], + child: buildProductSmoothCard( + title: Text(localizations.product_field_website_title), + body: Container( + width: double.infinity, + padding: const EdgeInsetsDirectional.only( + start: LARGE_SPACE, + top: LARGE_SPACE, + bottom: LARGE_SPACE, + // To be perfectly aligned with arrows + end: 21.0, + ), + child: Row( + children: [ + Expanded( + child: Text( + website, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Colors.blue), + ), ), - ), - const Icon(Icons.open_in_new), - ], + const Padding( + padding: EdgeInsetsDirectional.only( + start: 5.0, + end: 3.0, + ), + child: icons.ExternalLink( + size: 20.0, + ), + ), + ], + ), ), + margin: EdgeInsets.zero, ), ), ), - margin: const EdgeInsets.only( - left: SMALL_SPACE, - right: SMALL_SPACE, - bottom: MEDIUM_SPACE, - ), ); }