diff --git a/packages/smooth_app/lib/helpers/extension_on_text_helper.dart b/packages/smooth_app/lib/helpers/extension_on_text_helper.dart index ea07e963e476..1b57a2fa2e8c 100644 --- a/packages/smooth_app/lib/helpers/extension_on_text_helper.dart +++ b/packages/smooth_app/lib/helpers/extension_on_text_helper.dart @@ -44,3 +44,8 @@ extension StringExtensions on String { return count; } } + +extension TextScalerExtension on BuildContext { + /// Returns the text font multiplier + double textScaler() => MediaQuery.textScalerOf(this).scale(1.0); +} diff --git a/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart index d8007f0384f0..f8c4e29d3c9f 100644 --- a/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart +++ b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -5,6 +7,7 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/news_feed/newsfeed_provider.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; +import 'package:smooth_app/helpers/extension_on_text_helper.dart'; import 'package:smooth_app/helpers/provider_helper.dart'; import 'package:smooth_app/pages/navigator/app_navigator.dart'; import 'package:smooth_app/pages/scan/carousel/main_card/scan_tagline.dart'; @@ -19,23 +22,45 @@ class ScanMainCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: ConsumerFilter( - buildWhen: - (AppNewsProvider? previousValue, AppNewsProvider currentValue) { - return previousValue?.hasContent != currentValue.hasContent; - }, - builder: (BuildContext context, AppNewsProvider newsFeed, _) { - if (!newsFeed.hasContent) { - return const _SearchCard( - expandedMode: true, - ); - } else { - return Semantics( - explicitChildNodes: true, - child: const Column( + return ConsumerFilter( + buildWhen: + (AppNewsProvider? previousValue, AppNewsProvider currentValue) { + return previousValue?.hasContent != currentValue.hasContent; + }, + builder: (BuildContext context, AppNewsProvider newsFeed, _) { + if (!newsFeed.hasContent) { + return const _SearchCard( + expandedMode: true, + ); + } else { + return Semantics( + explicitChildNodes: true, + child: LayoutBuilder( + builder: (_, BoxConstraints constraints) { + final bool dense = constraints.maxHeight * 0.4 <= + _maxHeight(context.textScaler()); + + if (dense) { + return ListView( + padding: EdgeInsets.zero, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: math.max( + _SearchCard.computeMinSize(context), + constraints.maxHeight * 0.5, + ), + ), + child: const _SearchCard( + expandedMode: false, + ), + ), + const SizedBox(height: SMALL_SPACE), + const ScanTagLine(dense: true), + ], + ); + } else { + return const Column( children: [ Expanded( flex: 6, @@ -46,18 +71,28 @@ class ScanMainCard extends StatelessWidget { SizedBox(height: SMALL_SPACE), Expanded( flex: 4, - child: ScanTagLine(), + child: ScanTagLine(dense: false), ), ], - ), - ); - } - }, - ), - ), - ], + ); + } + }, + ), + ); + } + }, ); } + + double _maxHeight(double textScaler) { + if (textScaler < 1.1) { + return 160.0; + } else if (textScaler < 1.3) { + return 173.0; + } else { + return 186.0; + } + } } class _SearchCard extends StatelessWidget { @@ -75,34 +110,43 @@ class _SearchCard extends StatelessWidget { final Widget widget = SmoothCard( color: lightTheme ? Colors.grey.withValues(alpha: 0.1) : Colors.black, - padding: const EdgeInsets.symmetric( - vertical: MEDIUM_SPACE, - horizontal: LARGE_SPACE, - ), + padding: EdgeInsets.zero, margin: const EdgeInsets.symmetric( horizontal: 0.0, vertical: VERY_SMALL_SPACE, ), ignoreDefaultSemantics: true, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SvgPicture.asset( - lightTheme - ? 'assets/app/logo_text_black.svg' - : 'assets/app/logo_text_white.svg', - semanticsLabel: localizations.homepage_main_card_logo_description, - ), - TextWithBoldParts( - text: localizations.homepage_main_card_subheading, - textAlign: TextAlign.center, - textStyle: const TextStyle(height: 1.3), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: SMALL_SPACE), - child: _SearchBar(), - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: MEDIUM_SPACE, + horizontal: LARGE_SPACE, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + LayoutBuilder(builder: (_, BoxConstraints constraints) { + return SvgPicture.asset( + lightTheme + ? 'assets/app/logo_text_black.svg' + : 'assets/app/logo_text_white.svg', + width: math.min(311.0, constraints.maxWidth * 0.85), + semanticsLabel: + localizations.homepage_main_card_logo_description, + ); + }), + const SizedBox(height: VERY_SMALL_SPACE), + TextWithBoldParts( + text: localizations.homepage_main_card_subheading, + textAlign: TextAlign.center, + textStyle: const TextStyle(height: 1.3, fontSize: 15.0), + ), + const SizedBox(height: MEDIUM_SPACE), + const Padding( + padding: EdgeInsets.symmetric(horizontal: SMALL_SPACE), + child: _SearchBar(), + ), + ], + ), ), ); @@ -117,6 +161,24 @@ class _SearchCard extends StatelessWidget { return widget; } } + + static double computeMinSize(BuildContext context, {bool expanded = false}) { + if (expanded) { + return MediaQuery.sizeOf(context).height * 0.4; + } else { + return (MEDIUM_SPACE * 3) + + (VERY_SMALL_SPACE * 3) + + // Logo + 54.0 + + // Text + ((2 * DefaultTextStyle.of(context).style.fontSize!) * 1.3) * + context.textScaler() + + // Search bar + SearchFieldUIHelper.SEARCH_BAR_HEIGHT + + // Extra space + VERY_LARGE_SPACE; + } + } } class _SearchBar extends StatelessWidget { @@ -158,6 +220,7 @@ class _SearchBar extends StatelessWidget { child: Text( localizations.homepage_main_card_search_field_hint, maxLines: 1, + textScaler: TextScaler.noScaling, overflow: TextOverflow.ellipsis, style: SearchFieldUIHelper.textStyle(context), ), diff --git a/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_tagline.dart b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_tagline.dart index e7a745487dc6..1d95c48412b6 100644 --- a/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_tagline.dart +++ b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_tagline.dart @@ -4,6 +4,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; import 'package:shimmer/shimmer.dart'; import 'package:smooth_app/cards/category_cards/svg_cache.dart'; import 'package:smooth_app/data_models/news_feed/newsfeed_model.dart'; @@ -12,6 +13,7 @@ import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_app_logo.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; +import 'package:smooth_app/helpers/extension_on_text_helper.dart'; import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/helpers/provider_helper.dart'; import 'package:smooth_app/resources/app_icons.dart'; @@ -20,12 +22,22 @@ import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_app/widgets/smooth_text.dart'; class ScanTagLine extends StatelessWidget { - const ScanTagLine({super.key}); + const ScanTagLine({required this.dense, super.key}); + + final bool dense; @override Widget build(BuildContext context) { - return ChangeNotifierProvider<_ScanNewsFeedProvider>( - create: (BuildContext context) => _ScanNewsFeedProvider(context), + return MultiProvider( + providers: [ + ChangeNotifierProvider<_ScanNewsFeedProvider>( + create: (BuildContext context) => _ScanNewsFeedProvider(context), + ), + Provider<_ScanTagLineDensity>( + create: (_) => + dense ? _ScanTagLineDensity.dense : _ScanTagLineDensity.normal, + ), + ], child: Consumer<_ScanNewsFeedProvider>( builder: ( BuildContext context, @@ -47,20 +59,29 @@ class ScanTagLine extends StatelessWidget { } } +enum _ScanTagLineDensity { + dense, + normal, +} + class _ScanTagLineLoading extends StatelessWidget { const _ScanTagLineLoading(); @override Widget build(BuildContext context) { + final _ScanTagLineDensity density = context.read<_ScanTagLineDensity>(); + return Shimmer.fromColors( baseColor: Theme.of(context) .extension()! .primaryMedium, highlightColor: Colors.white, - child: const SmoothCard( + child: SmoothCard( + margin: EdgeInsets.zero, child: SizedBox( width: double.infinity, - height: 200.0, + height: + density == _ScanTagLineDensity.dense ? 200.0 : double.infinity, ), ), ); @@ -79,6 +100,9 @@ class _ScanTagLineContent extends StatefulWidget { } class _ScanTagLineContentState extends State<_ScanTagLineContent> { + // Default values seem weird + static const Radius radius = Radius.circular(16.0); + Timer? _timer; int _index = -1; @@ -106,92 +130,103 @@ class _ScanTagLineContentState extends State<_ScanTagLineContent> { Theme.of(context).extension()!; final AppNewsItem currentNews = widget.news.elementAt(_index); - // Default values seem weird - const Radius radius = Radius.circular(16.0); + final bool dense = + context.read<_ScanTagLineDensity>() == _ScanTagLineDensity.dense; - return LayoutBuilder(builder: ( - BuildContext context, - BoxConstraints constraints, - ) { - final bool dense = constraints.maxHeight <= 155.0; - - return Column( - children: [ - DecoratedBox( - decoration: BoxDecoration( - color: currentNews.style?.titleBackground ?? - (!themeProvider.isDarkMode(context) - ? theme.primarySemiDark - : theme.primaryBlack), - borderRadius: const BorderRadiusDirectional.only( - topStart: radius, - topEnd: radius, - ), + return Column( + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: currentNews.style?.titleBackground ?? + (!themeProvider.isDarkMode(context) + ? theme.primarySemiDark + : theme.primaryBlack), + borderRadius: const BorderRadiusDirectional.only( + topStart: radius, + topEnd: radius, ), - child: Padding( - padding: EdgeInsetsDirectional.only( - start: dense ? LARGE_SPACE : MEDIUM_SPACE, - end: dense ? MEDIUM_SPACE : BALANCED_SPACE, - top: VERY_SMALL_SPACE, - bottom: VERY_SMALL_SPACE, - ), - child: _TagLineContentTitle( - title: currentNews.title, - backgroundColor: currentNews.style?.titleBackground, - indicatorColor: currentNews.style?.titleIndicatorColor, - titleColor: currentNews.style?.titleTextColor, - dense: dense, - ), + ), + child: Padding( + padding: EdgeInsetsDirectional.only( + start: dense ? LARGE_SPACE : MEDIUM_SPACE, + end: dense ? MEDIUM_SPACE : BALANCED_SPACE, + top: VERY_SMALL_SPACE, + bottom: VERY_SMALL_SPACE, + ), + child: _TagLineContentTitle( + title: currentNews.title, + backgroundColor: currentNews.style?.titleBackground, + indicatorColor: currentNews.style?.titleIndicatorColor, + titleColor: currentNews.style?.titleTextColor, + dense: dense, ), ), - Expanded( - child: Material( - type: MaterialType.card, - color: currentNews.style?.contentBackgroundColor ?? - (!themeProvider.isDarkMode(context) - ? theme.primaryMedium - : theme.primaryDark), - borderRadius: const BorderRadius.vertical(bottom: radius), - child: InkWell( - borderRadius: const BorderRadius.vertical(bottom: radius), - onTap: () => LaunchUrlHelper.launchURLAndFollowDeepLinks( - context, - currentNews.url, - ), - child: Padding( - padding: EdgeInsetsDirectional.symmetric( - vertical: dense ? 6.0 : SMALL_SPACE, - horizontal: MEDIUM_SPACE, - ), - child: Column( - children: [ - Expanded( - child: _TagLineContentBody( - message: currentNews.message, - textColor: currentNews.style?.messageTextColor, - image: currentNews.image, - dense: dense, - ), - ), - SizedBox(height: dense ? VERY_SMALL_SPACE : SMALL_SPACE), - Align( - alignment: AlignmentDirectional.bottomEnd, - child: _TagLineContentButton( - label: currentNews.buttonLabel, - backgroundColor: currentNews.style?.buttonBackground, - foregroundColor: currentNews.style?.buttonTextColor, - dense: dense, - ), - ), - ], - ), + ), + _buildCard(currentNews, themeProvider, context, theme, dense), + ], + ); + } + + Widget _buildCard( + AppNewsItem currentNews, + ThemeProvider themeProvider, + BuildContext context, + SmoothColorsThemeExtension theme, + bool dense, + ) { + Widget body = _TagLineContentBody( + message: currentNews.message, + textColor: currentNews.style?.messageTextColor, + image: currentNews.image, + dense: dense, + ); + + if (!dense) { + body = Expanded(child: body); + } + + final Widget card = Material( + type: MaterialType.card, + color: currentNews.style?.contentBackgroundColor ?? + (!themeProvider.isDarkMode(context) + ? theme.primaryMedium + : theme.primaryDark), + borderRadius: const BorderRadius.vertical(bottom: radius), + child: InkWell( + borderRadius: const BorderRadius.vertical(bottom: radius), + onTap: () => LaunchUrlHelper.launchURLAndFollowDeepLinks( + context, + currentNews.url, + ), + child: Padding( + padding: EdgeInsetsDirectional.symmetric( + vertical: dense ? 6.0 : SMALL_SPACE, + horizontal: MEDIUM_SPACE, + ), + child: Column( + children: [ + body, + SizedBox(height: dense ? VERY_SMALL_SPACE : SMALL_SPACE), + Align( + alignment: AlignmentDirectional.bottomEnd, + child: _TagLineContentButton( + label: currentNews.buttonLabel, + backgroundColor: currentNews.style?.buttonBackground, + foregroundColor: currentNews.style?.buttonTextColor, + dense: dense, ), ), - ), + ], ), - ], - ); - }); + ), + ), + ); + + if (dense) { + return card; + } else { + return Expanded(child: card); + } } @override @@ -285,13 +320,16 @@ class _TagLineContentBodyState extends State<_TagLineContentBody> { final Widget text = TextWithBoldParts( text: widget.message, - maxLines: widget.dense ? 3 : null, + + /// We have to force a maxLines + maxLines: widget.dense ? 500 : null, overflow: widget.dense ? TextOverflow.ellipsis : null, textStyle: TextStyle( color: widget.textColor ?? (!themeProvider.isDarkMode(context) ? theme.primarySemiDark : theme.primaryLight), + fontSize: 15.0, ), ); @@ -300,28 +338,34 @@ class _TagLineContentBodyState extends State<_TagLineContentBody> { } final int imageFlex = ((widget.image!.width ?? 0.2) * 10).toInt(); - return Row( - children: [ - if (!_imageError) ...[ - Expanded( - flex: imageFlex, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.sizeOf(context).height * 0.06, - ), - child: AspectRatio( - aspectRatio: 1.0, - child: _image(), + return Padding( + padding: const EdgeInsetsDirectional.only( + top: SMALL_SPACE, + bottom: VERY_SMALL_SPACE, + ), + child: Row( + children: [ + if (!_imageError) ...[ + Expanded( + flex: imageFlex, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.06, + ), + child: AspectRatio( + aspectRatio: 1.0, + child: _image(), + ), ), ), + SizedBox(width: widget.dense ? SMALL_SPACE : MEDIUM_SPACE), + ], + Expanded( + flex: 10 - imageFlex, + child: text, ), - SizedBox(width: widget.dense ? SMALL_SPACE : MEDIUM_SPACE), ], - Expanded( - flex: 10 - imageFlex, - child: text, - ), - ], + ), ); } @@ -386,41 +430,45 @@ class _TagLineContentButton extends StatelessWidget { final SmoothColorsThemeExtension theme = Theme.of(context).extension()!; - return Semantics( - button: true, - label: label ?? localizations.tagline_feed_news_button, - excludeSemantics: true, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(ROUNDED_RADIUS), - color: backgroundColor ?? theme.primaryBlack, - ), - child: Padding( - padding: const EdgeInsetsDirectional.symmetric( - vertical: VERY_SMALL_SPACE, - horizontal: VERY_LARGE_SPACE, + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: SMALL_SPACE), + child: Semantics( + button: true, + label: label ?? localizations.tagline_feed_news_button, + excludeSemantics: true, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(ROUNDED_RADIUS), + color: backgroundColor ?? theme.primaryBlack, ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsetsDirectional.only(bottom: 1.0), - child: Text( - label ?? localizations.tagline_feed_news_button, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + vertical: VERY_SMALL_SPACE, + horizontal: VERY_LARGE_SPACE, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(bottom: 1.0), + child: Text( + label ?? localizations.tagline_feed_news_button, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), ), - ), - const SizedBox(width: MEDIUM_SPACE), - CircledArrow.right( - type: CircledArrowType.normal, - size: 18.0, - color: backgroundColor ?? theme.primaryBlack, - circleColor: Colors.white, - ), - ], + const SizedBox(width: MEDIUM_SPACE), + CircledArrow.right( + type: CircledArrowType.normal, + size: 18.0 * context.textScaler(), + color: backgroundColor ?? theme.primaryBlack, + padding: EdgeInsets.all(4.0 * context.textScaler()), + circleColor: Colors.white, + ), + ], + ), ), ), ),