From d36a2f9a057b5cfc6d62b9b0da4dca414a9aadee Mon Sep 17 00:00:00 2001 From: Tamara Slosarek Date: Fri, 5 Apr 2024 17:04:12 +0200 Subject: [PATCH] feat(#259): add tutorial components and initial setup instruction --- app/lib/common/theme.dart | 4 + app/lib/common/widgets/direction_button.dart | 62 ++++++++++ app/lib/common/widgets/module.dart | 3 + .../common/widgets/tutorial/container.dart | 106 ++++++++++++++++++ app/lib/common/widgets/tutorial/content.dart | 13 +++ app/lib/common/widgets/tutorial/show.dart | 30 +++++ .../drug_selection/pages/drug_selection.dart | 17 +++ app/lib/l10n/app_en.arb | 4 + app/lib/onboarding/pages/onboarding.dart | 61 +++------- pharme.code-workspace | 2 + 10 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 app/lib/common/widgets/direction_button.dart create mode 100644 app/lib/common/widgets/tutorial/container.dart create mode 100644 app/lib/common/widgets/tutorial/content.dart create mode 100644 app/lib/common/widgets/tutorial/show.dart diff --git a/app/lib/common/theme.dart b/app/lib/common/theme.dart index 27167b0c..b62377a6 100644 --- a/app/lib/common/theme.dart +++ b/app/lib/common/theme.dart @@ -21,6 +21,10 @@ class PharMeTheme { surfaceVariant: surfaceColor, ), textTheme: textTheme, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: surfaceColor, + dragHandleColor: onSurfaceColor, + ) ); } diff --git a/app/lib/common/widgets/direction_button.dart b/app/lib/common/widgets/direction_button.dart new file mode 100644 index 00000000..2ecda2d0 --- /dev/null +++ b/app/lib/common/widgets/direction_button.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import '../module.dart'; + +enum ButtonDirection { forward, backward } + +class DirectionButton extends StatelessWidget { + const DirectionButton({ + super.key, + required this.direction, + required this.text, + required this.onPressed, + this.emphasize = false, + this.onDarkBackground = false, + }); + + final ButtonDirection direction; + final String text; + final FutureOr Function() onPressed; + final bool emphasize; + final bool onDarkBackground; + + @override + Widget build(BuildContext context) { + const lightColor = Colors.white; + const darkColor = PharMeTheme.onSurfaceText; + final buttonStyle = emphasize + ? ElevatedButton.styleFrom( + backgroundColor: onDarkBackground ? lightColor : darkColor, + ) + : null; + final textColor = emphasize == onDarkBackground + ? PharMeTheme.onSurfaceText + : Colors.white; + final separator = SizedBox(width: 8); + final iconData = direction == ButtonDirection.forward + ? Icons.arrow_forward_rounded + : Icons.arrow_back_rounded; + final icon = Icon( + iconData, + color: textColor, + size: 32, + ); + final buttonText = Text( + text, + style: PharMeTheme.textTheme.titleLarge!.copyWith(color: textColor), + ); + final buttonContent = direction == ButtonDirection.forward + ? [ separator, buttonText, separator, icon ] + : [ icon, separator, buttonText, separator ]; + return TextButton( + style: buttonStyle, + onPressed: onPressed, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: buttonContent, + ), + ); + } + +} \ No newline at end of file diff --git a/app/lib/common/widgets/module.dart b/app/lib/common/widgets/module.dart index 17af1bd6..d129045d 100644 --- a/app/lib/common/widgets/module.dart +++ b/app/lib/common/widgets/module.dart @@ -4,6 +4,7 @@ export 'checkbox_wrapper.dart'; export 'dialog_action.dart'; export 'dialog_content_text.dart'; export 'dialog_wrapper.dart'; +export 'direction_button.dart'; export 'drug_list/builder.dart'; export 'drug_list/cubit.dart'; export 'drug_list/drug_items/drug_cards.dart'; @@ -25,3 +26,5 @@ export 'radiant_gradient_mask.dart'; export 'rounded_card.dart'; export 'scroll_list.dart'; export 'subheader_divider.dart'; +export 'tutorial/content.dart'; +export 'tutorial/show.dart'; diff --git a/app/lib/common/widgets/tutorial/container.dart b/app/lib/common/widgets/tutorial/container.dart new file mode 100644 index 00000000..2bb96193 --- /dev/null +++ b/app/lib/common/widgets/tutorial/container.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import '../../module.dart'; + +class TutorialContainer extends HookWidget { + const TutorialContainer({ + super.key, + required this.pages, + required this.finishTutorial, + this.lastNextButtonText, + }); + + final List pages; + final Future Function() finishTutorial; + final String? lastNextButtonText; + + @override + Widget build(BuildContext context) { + final currentPageIndex = useState(0); + return Padding( + padding: EdgeInsets.only( + left: PharMeTheme.largeSpace, + bottom: PharMeTheme.largeSpace, + right: PharMeTheme.largeSpace, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPageContent(context, currentPageIndex), + ), + ); + } + + List _buildPageContent( + BuildContext context, + ValueNotifier currentPageIndex, + ) { + final currentPage = pages[currentPageIndex.value]; + final title = currentPage.title != null + ? currentPage.title!(context) + : null; + final content = currentPage.content != null + ? currentPage.content!(context) + : null; + final asset = currentPage.assetPath != null + ? Image.asset(currentPage.assetPath!) + : null; + return [ + if (title != null) Text( + title, + style: PharMeTheme.textTheme.headlineMedium!.copyWith( + fontSize: PharMeTheme.textTheme.headlineSmall!.fontSize, + ), + ), + if (content != null || asset != null) Padding( + padding: EdgeInsetsDirectional.only(top: PharMeTheme.mediumSpace), + child: Expanded( + child: Center(child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + if (content != null) + Text.rich(content, style: PharMeTheme.textTheme.bodyLarge), + if (asset != null) asset, + ], + ), + )), + ), + ), + Padding( + padding: EdgeInsetsDirectional.only(top: PharMeTheme.mediumSpace), + child: _buildActionBar(context, currentPageIndex), + ), + ]; + } + + Widget _buildActionBar( + BuildContext context, + ValueNotifier currentPageIndex, + ) { + final isFirstPage = currentPageIndex.value == 0; + final isLastPage = currentPageIndex.value == pages.length - 1; + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!isFirstPage) DirectionButton( + direction: ButtonDirection.backward, + onPressed: () => currentPageIndex.value = currentPageIndex.value - 1, + text: context.l10n.onboarding_prev, + ), + DirectionButton( + direction: ButtonDirection.forward, + onPressed: isLastPage + ? finishTutorial + : () => currentPageIndex.value = currentPageIndex.value + 1, + + text: isLastPage && lastNextButtonText != null + ? lastNextButtonText! + : context.l10n.action_continue, + emphasize: isLastPage, + ), + ], + ); + } +} \ No newline at end of file diff --git a/app/lib/common/widgets/tutorial/content.dart b/app/lib/common/widgets/tutorial/content.dart new file mode 100644 index 00000000..aeb60597 --- /dev/null +++ b/app/lib/common/widgets/tutorial/content.dart @@ -0,0 +1,13 @@ +import '../../module.dart'; + +class TutorialContent { + TutorialContent({ + this.title, + this.content, + this.assetPath, + }); + + final String Function(BuildContext)? title; + final TextSpan Function(BuildContext)? content; + final String? assetPath; +} \ No newline at end of file diff --git a/app/lib/common/widgets/tutorial/show.dart b/app/lib/common/widgets/tutorial/show.dart new file mode 100644 index 00000000..5a66a48f --- /dev/null +++ b/app/lib/common/widgets/tutorial/show.dart @@ -0,0 +1,30 @@ +import '../../module.dart'; +import 'container.dart'; + +Future showTutorial({ + required BuildContext context, + required List pages, + required bool concludesWholeTutorial, +}) => showModalBottomSheet( + context: context, + enableDrag: true, + showDragHandle: true, + isDismissible: false, + isScrollControlled: true, + useSafeArea: true, + elevation: 0, + builder: (context) => TutorialContainer( + pages: pages, + lastNextButtonText: concludesWholeTutorial + ? context.l10n.tutorial_to_the_app + : null, + finishTutorial: () async { + final closeTutorial = Navigator.of(context).pop; + if (concludesWholeTutorial) { + MetaData.instance.tutorialDone = true; + await MetaData.save(); + } + closeTutorial(); + }, + ), +); \ No newline at end of file diff --git a/app/lib/drug_selection/pages/drug_selection.dart b/app/lib/drug_selection/pages/drug_selection.dart index 1c1627bf..6b74f22b 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -23,6 +23,23 @@ class DrugSelectionPage extends HookWidget { create: (context) => cubit ?? DrugSelectionCubit(activeDrugs), child: BlocBuilder( builder: (context, state) { + if (concludesOnboarding) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await showTutorial( + context: context, + pages: [ + TutorialContent( + title: (context) => + context.l10n.tutorial_initial_drug_selection_title, + content: (context) => TextSpan( + text: context.l10n.tutorial_initial_drug_selection_body, + ), + ), + ], + concludesWholeTutorial: false, + ); + }); + } return unscrollablePageScaffold( title: context.l10n.drug_selection_header, body: Column( diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 10301fb3..0fb41ebc 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -346,6 +346,10 @@ "nav_more": "More", "tab_more": "More", + "tutorial_to_the_app": "Proceed to the app", + "tutorial_initial_drug_selection_title": "Setup PharMe", + "tutorial_initial_drug_selection_body": "As a first step, please update the list of your current medications.", + "onboarding_get_started": "Get started", "onboarding_next": "Next", "onboarding_prev": "Back", diff --git a/app/lib/onboarding/pages/onboarding.dart b/app/lib/onboarding/pages/onboarding.dart index 5763ae8f..1315a397 100644 --- a/app/lib/onboarding/pages/onboarding.dart +++ b/app/lib/onboarding/pages/onboarding.dart @@ -156,15 +156,14 @@ class OnboardingPage extends HookWidget { PageController pageController, bool isLastPage, ) { - final buttonStyle = isLastPage - ? ElevatedButton.styleFrom(backgroundColor: Colors.white) - : null; - final textColor = isLastPage - ? PharMeTheme.onSurfaceText - : Colors.white; - return TextButton( + return DirectionButton( key: Key('nextButton'), - style: buttonStyle, + direction: ButtonDirection.forward, + text: isLastPage + ? isRevisiting + ? context.l10n.action_back_to_app + : context.l10n.onboarding_get_started + : context.l10n.onboarding_next, onPressed: () async { if (isLastPage) { if (isRevisiting) { @@ -184,28 +183,8 @@ class OnboardingPage extends HookWidget { ); } }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 8), - Text( - isLastPage - ? isRevisiting - ? context.l10n.action_back_to_app - : context.l10n.onboarding_get_started - : context.l10n.onboarding_next, - style: PharMeTheme.textTheme.headlineSmall! - .copyWith(color: textColor), - ), - SizedBox(width: 8), - Icon( - Icons.arrow_forward_rounded, - color: textColor, - size: 32, - ), - ], - ), + onDarkBackground: true, + emphasize: isLastPage, ); } @@ -215,31 +194,17 @@ class OnboardingPage extends HookWidget { bool isFirstPage, ) { if (!isFirstPage) { - return TextButton( + return DirectionButton( key: Key('prevButton'), + direction: ButtonDirection.backward, onPressed: () { pageController.previousPage( duration: Duration(milliseconds: 500), curve: Curves.ease, ); }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.arrow_back_rounded, - color: Colors.white, - size: 32, - ), - SizedBox(width: 8), - Text( - context.l10n.onboarding_prev, - style: PharMeTheme.textTheme.headlineSmall! - .copyWith(color: Colors.white), - ), - ], - ), + text: context.l10n.onboarding_prev, + onDarkBackground: true, ); } else { return SizedBox.shrink(); diff --git a/pharme.code-workspace b/pharme.code-workspace index 56e2a8a8..45e2c600 100644 --- a/pharme.code-workspace +++ b/pharme.code-workspace @@ -52,6 +52,7 @@ "irinotecan", "Keycloak", "lookupkey", + "LTRB", "MedlinePlus", "Metabolizer", "mocktail", @@ -71,6 +72,7 @@ "simvastatin", "tacrolimus", "tramadol", + "unscrollable", "voriconazole" ] }