diff --git a/packages/smooth_app/assets/onboarding/birthday-cake.svg b/packages/smooth_app/assets/onboarding/birthday-cake.svg deleted file mode 100644 index a26a2c391ec..00000000000 --- a/packages/smooth_app/assets/onboarding/birthday-cake.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/smooth_app/assets/onboarding/cloud.svg b/packages/smooth_app/assets/onboarding/cloud.svg new file mode 100644 index 00000000000..8127531dc4e --- /dev/null +++ b/packages/smooth_app/assets/onboarding/cloud.svg @@ -0,0 +1,11 @@ + + + Vector + + + + + + + + \ No newline at end of file diff --git a/packages/smooth_app/assets/onboarding/reinvention.svg b/packages/smooth_app/assets/onboarding/reinvention.svg deleted file mode 100644 index 641f75117ca..00000000000 --- a/packages/smooth_app/assets/onboarding/reinvention.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/smooth_app/lib/data_models/onboarding_loader.dart b/packages/smooth_app/lib/data_models/onboarding_loader.dart index 9ba991956e8..2b8602e0850 100644 --- a/packages/smooth_app/lib/data_models/onboarding_loader.dart +++ b/packages/smooth_app/lib/data_models/onboarding_loader.dart @@ -44,7 +44,7 @@ class OnboardingLoader { } return; case OnboardingPage.NOT_STARTED: - case OnboardingPage.REINVENTION: + case OnboardingPage.HOME_PAGE: case OnboardingPage.SCAN_EXAMPLE: case OnboardingPage.HEALTH_CARD_EXAMPLE: case OnboardingPage.ECO_CARD_EXAMPLE: diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 7c55331791d..7cc4f69934a 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1092,14 +1092,12 @@ } } }, - "onboarding_reinventing_text1": "We invented\nthe collaborative\nscanning app in 2012", - "@onboarding_reinventing_text1": { - "description": "Onboarding / Reinventing page: text 1/2. If possible, balanced on 3 lines." - }, - "onboarding_reinventing_text2": "As we turn 10,\nwe're reinventing it\nfrom the ground up!", - "@onboarding_reinventing_text2": { - "description": "Onboarding / Reinventing page: text 2/2. If possible, balanced on 3 lines." + "onboarding_home_welcome_text1": "Welcome !", + "onboarding_home_welcome_text2": "The app that helps you choose food that is good for **you** and the **planet**!", + "@onboarding_home_welcome_text2": { + "description": "Onboarding home screen welcome text, text surrounded by * will be bold" }, + "onboarding_continue_button": "Continue", "onboarding_welcome_loading_dialog_title": "Loading your first example product", "@onboarding_welcome_loading_dialog_title": { "description": "Title for the onboarding loading dialog" diff --git a/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart b/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart index 9cd670c5ee9..5cccb33335e 100644 --- a/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart +++ b/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart @@ -18,7 +18,7 @@ import 'package:smooth_app/widgets/will_pop_scope.dart'; enum OnboardingPage { NOT_STARTED, - REINVENTION, + HOME_PAGE, WELCOME, SCAN_EXAMPLE, HEALTH_CARD_EXAMPLE, @@ -52,7 +52,7 @@ enum OnboardingPage { Color getBackgroundColor() { switch (this) { case OnboardingPage.NOT_STARTED: - case OnboardingPage.REINVENTION: + case OnboardingPage.HOME_PAGE: return const Color(0xFFDFF4FF); case OnboardingPage.WELCOME: return const Color(0xFFFCFCFC); @@ -79,8 +79,8 @@ enum OnboardingPage { final Color backgroundColor = getBackgroundColor(); switch (this) { case OnboardingPage.NOT_STARTED: - case OnboardingPage.REINVENTION: - return ReinventionPage(backgroundColor); + case OnboardingPage.HOME_PAGE: + return const OnboardingHomePage(); case OnboardingPage.WELCOME: return WelcomePage(backgroundColor); case OnboardingPage.SCAN_EXAMPLE: diff --git a/packages/smooth_app/lib/pages/onboarding/reinvention_page.dart b/packages/smooth_app/lib/pages/onboarding/reinvention_page.dart index a60ab501412..690c8833d61 100644 --- a/packages/smooth_app/lib/pages/onboarding/reinvention_page.dart +++ b/packages/smooth_app/lib/pages/onboarding/reinvention_page.dart @@ -1,171 +1,277 @@ -import 'dart:io'; - -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:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; import 'package:rive/rive.dart'; -import 'package:smooth_app/generic_lib/design_constants.dart'; -import 'package:smooth_app/generic_lib/duration_constants.dart'; -import 'package:smooth_app/helpers/app_helper.dart'; -import 'package:smooth_app/pages/onboarding/next_button.dart'; +import 'package:smooth_app/data_models/onboarding_loader.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; +import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart'; +import 'package:smooth_app/pages/onboarding/v2/onboarding_bottom_hills.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; +import 'package:smooth_app/widgets/smooth_text.dart'; /// Onboarding page: "reinvention" -class ReinventionPage extends StatelessWidget { - const ReinventionPage(this.backgroundColor); - - final Color backgroundColor; +class OnboardingHomePage extends StatelessWidget { + const OnboardingHomePage({super.key}); @override Widget build(BuildContext context) { - const double muchTooBigFontSize = 150; - final AppLocalizations appLocalizations = AppLocalizations.of(context); - final TextStyle headlineStyle = Theme.of(context) - .textTheme - .displayMedium! - .copyWith(fontSize: muchTooBigFontSize); - final Size screenSize = MediaQuery.sizeOf(context); - final double animHeight = 352.0 * screenSize.width / 375.0; - - return ColoredBox( - color: backgroundColor, - child: SafeArea( - bottom: false, + return Scaffold( + backgroundColor: const Color(0xFFE3F3FE), + body: Provider.value( + value: OnboardingConfig._(MediaQuery.of(context)), child: Stack( children: [ - Positioned( - left: 0.0, - right: 0.0, - bottom: 0.0, - top: screenSize.height * 0.75, - child: _Background( - screenWidth: screenSize.width, - ), - ), - Positioned( - left: 0.0, - right: 0.0, - bottom: 0.0, - child: RepaintBoundary( - child: SizedBox( - width: screenSize.width, - height: animHeight, - child: const RiveAnimation.asset( - 'assets/onboarding/onboarding.riv', - artboard: 'Reinvention', - animations: ['Loop'], - alignment: Alignment.bottomCenter, - ), - ), - ), + const _OnboardingWelcomePageContent(), + OnboardingBottomHills( + onTap: () async { + final UserPreferences userPreferences = + context.read(); + final LocalDatabase localDatabase = + context.read(); + + await OnboardingLoader(localDatabase) + .runAtNextTime(OnboardingPage.HOME_PAGE, context); + if (context.mounted) { + await OnboardingFlowNavigator(userPreferences).navigateToPage( + context, + OnboardingPage.HOME_PAGE.getNextPage(), + ); + } + }, ), - Positioned( - top: 0.0, - left: 0.0, - right: 0.0, - bottom: animHeight - 20.0, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - flex: 30, - child: Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: Center( - child: AutoSizeText( - appLocalizations.onboarding_reinventing_text1, - style: headlineStyle, - maxLines: 3, - textAlign: TextAlign.center, - ), - ), - ), - ), - Flexible( - flex: 15, - child: SvgPicture.asset( - 'assets/onboarding/birthday-cake.svg', - package: AppHelper.APP_PACKAGE, - ), - ), - Flexible( - flex: 30, - child: Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: Center( - child: AutoSizeText( - appLocalizations.onboarding_reinventing_text2, - style: headlineStyle, - maxLines: 3, - textAlign: TextAlign.center, - ), - ), - ), - ), - Flexible( - flex: 25, - child: SvgPicture.asset( - 'assets/onboarding/title.svg', - package: AppHelper.APP_PACKAGE, - ), - ), - ], + ], + ), + ), + ); + } +} + +class _OnboardingWelcomePageContent extends StatelessWidget { + const _OnboardingWelcomePageContent(); + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final double fontMultiplier = OnboardingConfig.of(context).fontMultiplier; + final double hillsHeight = OnboardingBottomHills.height(context); + + return Padding( + padding: EdgeInsetsDirectional.only( + top: hillsHeight * 0.5 + MediaQuery.viewPaddingOf(context).top, + bottom: hillsHeight, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 15, + child: Text( + appLocalizations.onboarding_home_welcome_text1, + style: TextStyle( + fontSize: 45 * fontMultiplier, + fontWeight: FontWeight.bold, ), + textAlign: TextAlign.center, ), - Positioned( - bottom: 0, - child: SafeArea( - bottom: !Platform.isIOS, - child: const NextButton( - OnboardingPage.REINVENTION, - backgroundColor: null, - nextKey: Key('nextAfterReinvention'), + ), + const Expanded( + flex: 37, + child: _SunAndCloud(), + ), + Expanded( + flex: 45, + child: FractionallySizedBox( + widthFactor: 0.65, + child: Align( + alignment: const Alignment(0, -0.2), + child: OnboardingText( + text: appLocalizations.onboarding_home_welcome_text2, ), ), ), - ], - ), + ), + ], ), ); } } -class _Background extends StatelessWidget { - const _Background({required this.screenWidth}); +class _SunAndCloud extends StatefulWidget { + const _SunAndCloud(); + + @override + State<_SunAndCloud> createState() => _SunAndCloudState(); +} + +class _SunAndCloudState extends State<_SunAndCloud> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; - final double screenWidth; + @override + void initState() { + super.initState(); + _controller = + AnimationController(vsync: this, duration: const Duration(seconds: 2)) + ..addListener(() => setState(() {})); + _animation = Tween( + begin: -1.0, + end: 1.0, + ).animate(_controller); + _controller.repeat(reverse: true); + } @override Widget build(BuildContext context) { - return SizedBox.expand( - child: RepaintBoundary( - child: Stack( + final TextDirection textDirection = Directionality.of(context); + + return RepaintBoundary( + child: LayoutBuilder(builder: ( + BuildContext context, + BoxConstraints constraints, + ) { + return Stack( children: [ - AnimatedPositioned( - bottom: 0.0, - right: 0.0, - width: screenWidth * 0.808, - duration: SmoothAnimationsDuration.short, - child: SvgPicture.asset( - 'assets/onboarding/hill_end.svg', - fit: BoxFit.fill, - ), + Positioned.directional( + top: constraints.maxHeight * 0.3, + bottom: constraints.maxHeight * 0.2, + start: (_animation.value * 161.0) * 0.3, + textDirection: textDirection, + child: SvgPicture.asset('assets/onboarding/cloud.svg'), ), - AnimatedPositioned( - bottom: 0.0, - left: 0.0, - width: screenWidth * 0.855, - duration: SmoothAnimationsDuration.short, - child: SvgPicture.asset( - 'assets/onboarding/hill_start.svg', - fit: BoxFit.fill, + const Align( + alignment: Alignment.center, + child: RiveAnimation.asset( + 'assets/animations/off.riv', + artboard: 'Success', + animations: ['Timeline 1'], ), - ) + ), + Positioned.directional( + top: constraints.maxHeight * 0.22, + bottom: constraints.maxHeight * 0.35, + end: (_animation.value * 40.0) - 31, + textDirection: textDirection, + child: SvgPicture.asset('assets/onboarding/cloud.svg'), + ), ], - ), + ); + }), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +class OnboardingText extends StatelessWidget { + const OnboardingText({ + required this.text, + this.margin, + super.key, + }); + + final String text; + final EdgeInsetsGeometry? margin; + + @override + Widget build(BuildContext context) { + double fontMultiplier; + try { + fontMultiplier = OnboardingConfig.of(context).fontMultiplier; + } catch (_) { + fontMultiplier = + OnboardingConfig.computeFontMultiplier(MediaQuery.of(context)); + } + + final Color backgroundColor = + Theme.of(context).extension()!.orange; + + return RichText( + text: TextSpan( + children: _extractChunks().map(((String text, bool highlighted) el) { + if (el.$2) { + return _createSpan( + el.$1, + 30 * fontMultiplier, + backgroundColor, + ); + } else { + return TextSpan(text: el.$1); + } + }).toList(growable: false), + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 30 * fontMultiplier, + height: 1.53, + fontWeight: FontWeight.w600, + ), ), + textAlign: TextAlign.center, ); } + + Iterable<(String, bool)> _extractChunks() { + final Iterable matches = + RegExp(r'\*\*(.*?)\*\*').allMatches(text); + + if (matches.length <= 1) { + return <(String, bool)>[(text, false)]; + } + + final List<(String, bool)> chunks = <(String, bool)>[]; + + int lastMatch = 0; + + for (final RegExpMatch match in matches) { + if (matches.first.start > 0) { + chunks.add((text.substring(lastMatch, match.start), false)); + } + + chunks.add((text.substring(match.start + 2, match.end - 2), true)); + lastMatch = match.end; + } + + if (lastMatch < text.length) { + chunks.add((text.substring(lastMatch), false)); + } + + return chunks; + } + + WidgetSpan _createSpan(String text, double fontSize, Color backgroundColor) => + HighlightedTextSpan( + text: text, + textStyle: TextStyle( + color: Colors.white, + fontSize: fontSize, + fontWeight: FontWeight.w700, + ), + padding: const EdgeInsetsDirectional.only( + top: 1.0, + bottom: 5.0, + start: 15.0, + end: 15.0, + ), + margin: margin ?? const EdgeInsetsDirectional.symmetric(vertical: 2.5), + backgroundColor: backgroundColor, + radius: 30.0, + ); +} + +// TODO(g123k): Move elsewhere when the onboarding will be redesigned +class OnboardingConfig { + OnboardingConfig._(MediaQueryData mediaQuery) + : fontMultiplier = computeFontMultiplier(mediaQuery); + final double fontMultiplier; + + static double computeFontMultiplier(MediaQueryData mediaQuery) => + ((mediaQuery.size.width * 45) / 428) / 45; + + static OnboardingConfig of(BuildContext context) => + context.watch(); } diff --git a/packages/smooth_app/lib/pages/onboarding/v2/onboarding_bottom_hills.dart b/packages/smooth_app/lib/pages/onboarding/v2/onboarding_bottom_hills.dart new file mode 100644 index 00000000000..49b3b784299 --- /dev/null +++ b/packages/smooth_app/lib/pages/onboarding/v2/onboarding_bottom_hills.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/resources/app_icons.dart' as icons; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; + +class OnboardingBottomHills extends StatelessWidget { + const OnboardingBottomHills({ + required this.onTap, + super.key, + }); + + final VoidCallback onTap; + + static double height(BuildContext context) { + final double screenHeight = MediaQuery.sizeOf(context).height; + final double bottomPadding = MediaQuery.viewPaddingOf(context).bottom; + return screenHeight * (0.12 + (bottomPadding / screenHeight)); + } + + @override + Widget build(BuildContext context) { + final TextDirection textDirection = Directionality.of(context); + final double bottomPadding = MediaQuery.viewPaddingOf(context).bottom; + final double maxHeight = OnboardingBottomHills.height(context); + final SmoothColorsThemeExtension colors = + Theme.of(context).extension()!; + + return Positioned( + top: null, + bottom: 0.0, + left: 0.0, + right: 0.0, + height: maxHeight, + child: SizedBox( + child: Stack( + children: [ + Positioned.directional( + start: 0.0, + bottom: 0.0, + textDirection: textDirection, + child: SvgPicture.asset( + 'assets/onboarding/hill_start.svg', + height: maxHeight, + ), + ), + Positioned.directional( + end: 0.0, + bottom: 0.0, + textDirection: textDirection, + child: SvgPicture.asset( + 'assets/onboarding/hill_end.svg', + height: maxHeight * 0.965, + ), + ), + Positioned.directional( + textDirection: textDirection, + bottom: bottomPadding + (Platform.isIOS ? 0.0 : 15.0), + end: 15.0, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Colors.white, + ), + padding: MaterialStateProperty.all( + const EdgeInsetsDirectional.only( + start: LARGE_SPACE + 1.0, + end: LARGE_SPACE, + top: SMALL_SPACE, + bottom: SMALL_SPACE, + ), + ), + elevation: MaterialStateProperty.all(4.0), + iconColor: MaterialStateProperty.all( + colors.orange, + ), + foregroundColor: MaterialStateProperty.all( + colors.orange, + ), + iconSize: MaterialStateProperty.all(21.0), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + ), + shadowColor: MaterialStateProperty.all( + Colors.black.withOpacity(0.50), + ), + ), + onPressed: onTap, + child: Row( + children: [ + Text( + AppLocalizations.of(context).onboarding_continue_button, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22.0, + ), + ), + const SizedBox(width: LARGE_SPACE), + const icons.Arrow.right(), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/smooth_app/lib/widgets/smooth_text.dart b/packages/smooth_app/lib/widgets/smooth_text.dart index c8a3b9c5ef6..bfa2f17066c 100644 --- a/packages/smooth_app/lib/widgets/smooth_text.dart +++ b/packages/smooth_app/lib/widgets/smooth_text.dart @@ -190,3 +190,31 @@ class TextHighlighter extends StatelessWidget { return endPosition + diff; } } + +class HighlightedTextSpan extends WidgetSpan { + HighlightedTextSpan({ + required String text, + required TextStyle textStyle, + required EdgeInsetsGeometry padding, + required Color backgroundColor, + required double radius, + EdgeInsetsGeometry? margin, + }) : assert(radius > 0.0), + super( + alignment: PlaceholderAlignment.middle, + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.all( + Radius.circular(radius), + ), + ), + margin: margin, + padding: padding, + child: Text( + text, + style: textStyle, + ), + ), + ); +}