diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 96a33dca..4bc8cd95 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = H4NQ8HX57R; + DEVELOPMENT_TEAM = ML9QV973KL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -495,7 +495,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = H4NQ8HX57R; + DEVELOPMENT_TEAM = ML9QV973KL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -525,7 +525,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = H4NQ8HX57R; + DEVELOPMENT_TEAM = ML9QV973KL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist index 7b584b9d..bce3399a 100644 --- a/app/ios/Runner/Info.plist +++ b/app/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -29,6 +31,8 @@ LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -46,9 +50,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/app/lib/app.dart b/app/lib/app.dart index ca52e43c..8667744a 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -38,6 +38,7 @@ class PharMeApp extends StatelessWidget { } return getInitialRoute(); }, + navigatorObservers: () => [RemoveFocusOnNavigate()], ), theme: PharMeTheme.light, localizationsDelegates: [ @@ -52,3 +53,13 @@ class PharMeApp extends StatelessWidget { ); } } + +// Based on https://github.com/flutter/flutter/issues/48464#issuecomment-586635827 +class RemoveFocusOnNavigate extends NavigatorObserver { + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + final focus = FocusManager.instance.primaryFocus; + focus?.unfocus(); + } +} diff --git a/app/lib/common/widgets/dialog_wrapper.dart b/app/lib/common/widgets/dialog_wrapper.dart index b710f4d6..d1eeef02 100644 --- a/app/lib/common/widgets/dialog_wrapper.dart +++ b/app/lib/common/widgets/dialog_wrapper.dart @@ -3,12 +3,12 @@ import '../module.dart'; class DialogWrapper extends StatelessWidget { const DialogWrapper({ super.key, - required this.title, - required this.content, required this.actions, + this.title, + this.content, }); - final String title; + final String? title; final Widget? content; final List actions; @@ -22,7 +22,7 @@ class DialogWrapper extends StatelessWidget { ) : content; return AlertDialog.adaptive( - title: Text(title), + title: title != null ? Text(title!) : null, content: materialContent, actions: actions, elevation: 0, diff --git a/app/lib/common/widgets/drug_activity_selection.dart b/app/lib/common/widgets/drug_activity_selection.dart index f6b4ab2c..215736c1 100644 --- a/app/lib/common/widgets/drug_activity_selection.dart +++ b/app/lib/common/widgets/drug_activity_selection.dart @@ -15,15 +15,19 @@ SwitchListTile buildDrugActivitySelection({ required bool isActive, required bool disabled, EdgeInsetsGeometry? contentPadding, + bool warnIfInhibitor = true, }) => SwitchListTile.adaptive( key: key, value: isActive, activeColor: PharMeTheme.primaryColor, + inactiveThumbColor: PharMeTheme.surfaceColor, + inactiveTrackColor: PharMeTheme.borderColor, + trackOutlineColor: WidgetStatePropertyAll(Colors.transparent), title: Text(title), subtitle: subtitle.isNotNullOrBlank ? Text(subtitle!, style: PharMeTheme.textTheme.bodyMedium): null, contentPadding: contentPadding, onChanged: disabled ? null : (newValue) { - if (isInhibitor(drug.name)) { + if (warnIfInhibitor && isInhibitor(drug.name)) { showAdaptiveDialog( context: context, builder: (context) => DialogWrapper( diff --git a/app/lib/common/widgets/drug_list/drug_items/drug_selection_list.dart b/app/lib/common/widgets/drug_list/drug_items/drug_selection_list.dart index 131164bf..bd0ae710 100644 --- a/app/lib/common/widgets/drug_list/drug_items/drug_selection_list.dart +++ b/app/lib/common/widgets/drug_list/drug_items/drug_selection_list.dart @@ -46,6 +46,7 @@ List _buildSelectionList( subtitle: (drug.annotations.brandNames.isNotEmpty) ? formatBrandNames(context, drug) : null, + warnIfInhibitor: showDrugInteractionIndicator, ) ).toList(); } diff --git a/app/lib/common/widgets/page_scaffold.dart b/app/lib/common/widgets/page_scaffold.dart index ff30215f..629ebef9 100644 --- a/app/lib/common/widgets/page_scaffold.dart +++ b/app/lib/common/widgets/page_scaffold.dart @@ -29,45 +29,58 @@ Widget buildTitle(String text, { String? tooltipText }) { ); } -Scaffold pageScaffold({ +Widget pageScaffold({ required List body, required String title, List? actions, bool canNavigateBack = true, + BuildContext? contextToDismissFocusOnTap, + bool resizeToAvoidBottomInset = false, Key? key, }) { - return Scaffold( - key: key, - body: CustomScrollView(slivers: [ - SliverAppBar( - scrolledUnderElevation: 0, - backgroundColor: PharMeTheme.appBarTheme.backgroundColor, - foregroundColor: PharMeTheme.appBarTheme.foregroundColor, - elevation: PharMeTheme.appBarTheme.elevation, - leadingWidth: PharMeTheme.appBarTheme.leadingWidth, - automaticallyImplyLeading: canNavigateBack, - floating: true, - pinned: true, - snap: false, - centerTitle: PharMeTheme.appBarTheme.centerTitle, - title: buildTitle(title), - actions: actions, - titleSpacing: _getTitleSpacing(backButtonPresent: canNavigateBack), - ), - SliverPadding( - padding: pagePadding(), - sliver: SliverList(delegate: SliverChildListDelegate(body)), - ), - ]), + return GestureDetector( + onTap: () => _maybeRemoveFocus(contextToDismissFocusOnTap), + child: Scaffold( + key: key, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + body: CustomScrollView(slivers: [ + SliverAppBar( + scrolledUnderElevation: 0, + backgroundColor: PharMeTheme.appBarTheme.backgroundColor, + foregroundColor: PharMeTheme.appBarTheme.foregroundColor, + elevation: PharMeTheme.appBarTheme.elevation, + leadingWidth: PharMeTheme.appBarTheme.leadingWidth, + automaticallyImplyLeading: canNavigateBack, + floating: true, + pinned: true, + snap: false, + centerTitle: PharMeTheme.appBarTheme.centerTitle, + title: buildTitle(title), + actions: actions, + titleSpacing: _getTitleSpacing(backButtonPresent: canNavigateBack), + ), + SliverPadding( + padding: pagePadding(), + sliver: SliverList(delegate: SliverChildListDelegate(body)), + ), + ]), + ), ); } -Scaffold unscrollablePageScaffold({ +void _maybeRemoveFocus(BuildContext? contextToDismissFocusOnTap) => + contextToDismissFocusOnTap != null + ? FocusScope.of(contextToDismissFocusOnTap).unfocus() + : null; + +Widget unscrollablePageScaffold({ required Widget body, String? title, String? titleTooltip, List? actions, bool canNavigateBack = true, + BuildContext? contextToDismissFocusOnTap, + bool resizeToAvoidBottomInset = false, Key? key, }) { final appBar = title == null @@ -84,13 +97,17 @@ Scaffold unscrollablePageScaffold({ scrolledUnderElevation: 0, titleSpacing: _getTitleSpacing(backButtonPresent: canNavigateBack), ); - return Scaffold( - key: key, - appBar: appBar, - body: SafeArea( - child: Padding( - padding: pagePadding(), - child: body, + return GestureDetector( + onTap: () => _maybeRemoveFocus(contextToDismissFocusOnTap), + child: Scaffold( + key: key, + appBar: appBar, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + body: SafeArea( + child: Padding( + padding: pagePadding(), + child: body, + ), ), ), ); diff --git a/app/lib/common/widgets/scroll_list.dart b/app/lib/common/widgets/scroll_list.dart index f9ba25bf..82d63608 100644 --- a/app/lib/common/widgets/scroll_list.dart +++ b/app/lib/common/widgets/scroll_list.dart @@ -14,6 +14,7 @@ Widget scrollList(List body, { bool keepPosition = false }) { child: Padding( padding: EdgeInsets.only(right: PharMeTheme.mediumSpace), child: FlutterListView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, delegate: FlutterListViewDelegate( (context, index) => body[index], childCount: body.length, diff --git a/app/lib/drug_selection/pages/drug_selection.dart b/app/lib/drug_selection/pages/drug_selection.dart index 1e38b865..a566ea4f 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -32,15 +32,14 @@ class DrugSelectionPage extends HookWidget { return unscrollablePageScaffold( title: context.l10n.drug_selection_header, canNavigateBack: !concludesOnboarding, + contextToDismissFocusOnTap: context, body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.symmetric(vertical: PharMeTheme.smallSpace), child: PageDescription.fromText( - concludesOnboarding - ? context.l10n.drug_selection_onboarding_description - : context.l10n.drug_selection_settings_description, + context.l10n.drug_selection_settings_description, ), ), Expanded(child: _buildDrugList(context, state)), @@ -67,10 +66,29 @@ class DrugSelectionPage extends HookWidget { child: FullWidthButton( context.l10n.action_continue, () async { - MetaData.instance.initialDrugSelectionDone = true; - await MetaData.save(); - // ignore: use_build_context_synchronously - await overwriteRoutes(context, nextPage: MainRoute()); + await showAdaptiveDialog( + context: context, + builder: (context) => DialogWrapper( + title: context.l10n.drug_selection_continue_warning_title, + content: Text(context.l10n.drug_selection_continue_warning), + actions: [ + DialogAction( + onPressed: context.router.root.maybePop, + text: context.l10n.action_cancel, + ), + DialogAction( + onPressed: () async { + MetaData.instance.initialDrugSelectionDone = true; + await MetaData.save(); + // ignore: use_build_context_synchronously + await overwriteRoutes(context, nextPage: MainRoute()); + }, + text: context.l10n.action_understood, + isDefault: true, + ), + ], + ), + ); }, enabled: _isEditable(state), ) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 77aaf04f..9f4155df 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1,17 +1,28 @@ { "@@locale": "en", "action_cancel": "Cancel", + "@action_cancel": {}, "action_continue": "Continue", + "@action_continue": {}, "action_understood": "Understood", + "@action_understood": {}, "action_back_to_app": "Back to app", + "@action_back_to_app": {}, "action_finish": "Finish", + "@action_finish": {}, "error_title": "Something went wrong", + "@error_title": {}, "error_uncaught_message_first_part": "PharMe has encountered an unknown error. ", + "@error_uncaught_message_first_part": {}, "error_uncaught_message_bold_part": "Please close the app and open it again.", + "@error_uncaught_message_bold_part": {}, "error_uncaught_message_contact": "The error has been logged for our technical staff; however, if this problem persists, please contact us:", + "@error_uncaught_message_contact": {}, "error_close_app": "Close app", + "@error_close_app": {}, "error_mail_subject": "[ACTION REQUIRED] Unknown Error Report", + "@error_mail_subject": {}, "error_mail_body": "Please describe what happened right before the error occurred: \n\n--- Please keep the following information, it will help us to pin down the error ---\n\n{error}", "@error_mail_body": { "placeholders": { @@ -23,43 +34,75 @@ }, "auth_welcome": "Welcome to PharMe", + "@auth_welcome": {}, "auth_choose_lab": "Please select your data provider", + "@auth_choose_lab": {}, "auth_sign_in": "Get data", + "@auth_sign_in": {}, "auth_success": "Successfully imported data", + "@auth_success": {}, "auth_loading_data": "Loading your data, please do not close the app...", + "@auth_loading_data": {}, "auth_updating_data": "Checking for updates, please do not close the app...", + "@auth_updating_data": {}, "drug_item_brand_names": "Brand names", + "@drug_item_brand_names": {}, "drug_selection_header": "Current medications", - "drug_selection_onboarding_description": "Please review the medications you are currently taking below and update them if needed. You can always change the status for a medication later on a medication page or in the settings.", + "@drug_selection_header": {}, + "drug_selection_continue_warning_title": "Confirm proceeding", + "@drug_selection_continue_warning_title": {}, + "drug_selection_continue_warning": "Proceeding will close the initial medication selection. You can always change the status for a medication later in the app.", + "@drug_selection_continue_warning": {}, "drug_selection_settings_description": "Review the medications you are currently taking below.", + "@drug_selection_settings_description": {}, "drug_selection_no_drugs_loaded": "No medications loaded", + "@drug_selection_no_drugs_loaded": {}, "drug_list_subheader_active_drugs": "Current medications", + "@drug_list_subheader_active_drugs": {}, "drug_list_subheader_all_drugs": "All medications", + "@drug_list_subheader_all_drugs": {}, "drug_list_subheader_other_drugs": "Other medications", + "@drug_list_subheader_other_drugs": {}, "err_could_not_retrieve_access_token": "An unexpected error occurred while logging in", + "@err_could_not_retrieve_access_token": {}, "err_fetch_user_data_failed": "An error occurred while getting data, please try again later", + "@err_fetch_user_data_failed": {}, "err_generic": "Error", + "@err_generic": {}, "update_warning_title": "Updated guidelines", + "@update_warning_title": {}, "update_warning_body": "The guidelines for interactions between genes and medications were updated. Please review your results, especially for the medications you are currently taking.", + "@update_warning_body": {}, "general_continue": "Continue", + "@general_continue": {}, "general_retry": "Retry", + "@general_retry": {}, "general_and": "and", + "@general_and": {}, "general_not_tested": "Not tested", + "@general_not_tested": {}, "warning_level_green": "Standard precautions", + "@warning_level_green": {}, "warning_level_missing": "Standard precautions (incomplete data)", + "@warning_level_missing": {}, "warning_level_yellow": "Use with caution", + "@warning_level_yellow": {}, "warning_level_red": "Consider alternatives", + "@warning_level_red": {}, "search_page_tooltip_search": "Search for medications by their name, brand name or class.", + "@search_page_tooltip_search": {}, "search_page_tooltip_search_no_class": "Search for medications by their name or brand name.", + "@search_page_tooltip_search_no_class": {}, "search_page_filter_label": "Filter by guideline result", + "@search_page_filter_label": {}, "search_page_indicator_explanation": "Taking medications with an {indicatorName} ({indicator}) can interact with your results for other medications", "@search_page_indicator_explanation": { "placeholders": { @@ -74,6 +117,7 @@ } }, "search_no_drugs_with_filter_amendment": " or filters", + "@search_no_drugs_with_filter_amendment": {}, "search_no_drugs": "No medications found. Try adjusting the search term{amendment}.\n\nIf the medication you are looking for is not included in PharMe, it might not have relevant DNA-based guidelines. Clinical dosing may apply, consult your pharmacist or doctor for more information.", "@search_no_drugs": { "placeholders": { @@ -84,9 +128,13 @@ } }, "drugs_page_disclaimer_description": "Please note: ", + "@drugs_page_disclaimer_description": {}, "drugs_page_disclaimer_text_part_0": "Never stop taking or change the dose of your medications without consulting your pharmacist or doctor.", + "@drugs_page_disclaimer_text_part_0": {}, "drugs_page_disclaimer_text_part_1": "Also, the information shown on this page is ONLY based on your DNA and certain medications you are currently taking.", + "@drugs_page_disclaimer_text_part_1": {}, "drugs_page_disclaimer_text_part_2": "Other important factors like weight, age, pre-existing conditions, and further medication interactions are not considered.", + "@drugs_page_disclaimer_text_part_2": {}, "drugs_page_is_inhibitor": "Taking {drugName} can interact with your results for the following gene(s): {genes}", "@drugs_page_is_inhibitor": { "placeholders": { @@ -102,14 +150,67 @@ }, "inhibitor_direct_salutation": "you are", + "@inhibitor_direct_salutation": {}, "inhibitor_direct_salutation_genitive": "your", + "@inhibitor_direct_salutation_genitive": {}, "inhibitor_third_person_salutation": "the user is", + "@inhibitor_third_person_salutation": {}, "inhibitor_third_person_salutation_genitive": "the user's", + "@inhibitor_third_person_salutation_genitive": {}, "inhibitor_message": "One or more of the medications {salutation} currently taking may interact with {salutationGenitive} genetic result", + "@inhibitor_message": { + "placeholders": { + "salutation": { + "type": "String", + "example": "you" + }, + "salutationGenitive": { + "type": "String", + "example": "your" + } + } + }, "inhibitors_consequence_adapted": "{salutationGenitive} {geneName} phenotype was adapted from {originalPhenotype}.", + "@inhibitors_consequence_adapted": { + "placeholders": { + "salutationGenitive": { + "type": "String", + "example": "your" + }, + "geneName": { + "type": "String", + "example": "CYP2D6" + }, + "originalPhenotype": { + "type": "String", + "example": "Normal Metabolizer" + } + } + }, "inhibitors_consequence_not_adapted": "{salutationGenitive} {geneName} phenotype was not adapted but may need to be.", + "@inhibitors_consequence_not_adapted": { + "placeholders": { + "salutationGenitive": { + "type": "String", + "example": "your" + }, + "geneName": { + "type": "String", + "example": "CYP2D6" + } + } + }, "inhibitors_tooltip": "Current interacting medications: {inhibitors}.", + "@inhibitors_tooltip": { + "placeholders": { + "inhibitors": { + "type": "String", + "example": "bupropion" + } + } + }, "consult_text": "Consult your pharmacist or doctor for more information.", + "@consult_text": {}, "drugs_page_guidelines_empty": "No guidelines are present for {drugName}", "@drugs_page_guidelines_empty": { @@ -121,12 +222,19 @@ } }, "drugs_page_header_drugclass": "Medication class", + "@drugs_page_header_drugclass": {}, "drugs_page_header_drug": "Medication information", + "@drugs_page_header_drug": {}, "drugs_page_text_active": "Current medication", + "@drugs_page_text_active": {}, "drugs_page_active_warn_header": "Are you sure you want to change the medication usage status?", + "@drugs_page_active_warn_header": {}, "drugs_page_active_warn": "This can interact with your results for other medications.", + "@drugs_page_active_warn": {}, "drugs_page_header_guideline": "DNA-based clinical guideline", + "@drugs_page_header_guideline": {}, "drugs_page_no_guidelines_text": "No pharmacogenomic recommendation can be made at this time. Consult your pharmacist or doctor for more information.", + "@drugs_page_no_guidelines_text": {}, "drugs_page_sources_description": "Tap here to review the corresponding guideline published by {source}", "@drugs_page_sources_description": { "placeholders": { @@ -146,11 +254,16 @@ } }, "drugs_page_tooltip_guideline_missing": "Guidelines provide recommendations on which medications to use based on your DNA. However, no guideline is present in this case (yet).", + "@drugs_page_tooltip_guideline_missing": {}, "drugs_page_recommendation_description": "What to do: ", + "@drugs_page_recommendation_description": {}, "report_content_explanation": "This is your PGx test report. Tap on a gene name for more details on your results and a list of implicated medications.", + "@report_content_explanation": {}, "report_legend_text": "Next to your gene result the number of implicated medications per guideline result is shown:", + "@report_legend_text": {}, "report_page_faq_tooltip": "To learn more about genetics in general, please refer to the FAQ", + "@report_page_faq_tooltip": {}, "report_page_indicator_explanation": "Phenotypes followed by an {indicatorName} ({indicator}) might be adjusted based on interactions with medications you are currently taking", "@report_page_indicator_explanation": { "placeholders": { @@ -165,6 +278,7 @@ } }, "report_no_result_genes": "Genes with no result", + "@report_no_result_genes": {}, "gene_page_headline": "{gene} report", "@gene_page_headline": { @@ -194,10 +308,15 @@ } }, "gene_page_genotype": "Genotype", + "@gene_page_genotype": {}, "gene_page_genotype_tooltip": "The genotype is the variant you carry for this gene.", + "@gene_page_genotype_tooltip": {}, "gene_page_phenotype": "Phenotype", + "@gene_page_phenotype": {}, "gene_page_phenotype_tooltip": "The phenotype often describes the gene's activity level or whether a specific variant is present.", + "@gene_page_phenotype_tooltip": {}, "gene_page_relevant_drugs": "Implicated medications", + "@gene_page_relevant_drugs": {}, "gene_page_relevant_drugs_tooltip": "The medications listed here are influenced by your {geneDisplayString} result.", "@gene_page_relevant_drugs_tooltip": { "placeholders": { @@ -208,20 +327,33 @@ } }, "gene_page_no_relevant_drugs": "This gene has no known effect on any medications.", + "@gene_page_no_relevant_drugs": {}, "gene_page_activity_score": "Activity score", + "@gene_page_activity_score": {}, "pdf_disclaimer": "Disclaimer: The information contained in this PDF document is intended solely for use by trained health care professionals. It is provided for informational purposes only and should not be considered medical advice. The content may include technical terminology and clinical data that are intended for professional interpretation and application. Recipients are advised to exercise professional judgment and discretion when utilizing the information contained herein. If you are not a trained health care professional, please consult with a qualified medical practitioner or specialist before interpreting or applying the information provided in this document.", + "@pdf_disclaimer": {}, "pdf_pgx_report": "PGx Report", + "@pdf_pgx_report": {}, "pdf_heading_user_data": "User data", + "@pdf_heading_user_data": {}, "pdf_heading_clinical_guidelines": "Clinical guideline(s)", + "@pdf_heading_clinical_guidelines": {}, "pdf_info_clinical_guidelines": "For more fine-grained information please refer to the original guideline(s) by following the URL(s) below.", + "@pdf_info_clinical_guidelines": {}, "pdf_info_clinical_guidelines_no_phenotype_guidelines": "No guidelines were found for the user's phenotype. For further guideline information please refer to the URL(s) below.\n\nPlease note that it is possible that the guideline includes relevant information for the user's phenotype, although it could not be identified by PharMe.", + "@pdf_info_clinical_guidelines_no_phenotype_guidelines": {}, "pdf_no_value": "n/a", + "@pdf_no_value": {}, "pdf_indication": "Indication", + "@pdf_indication": {}, "pdf_brand_names": "Brand names", + "@pdf_brand_names": {}, "pdf_tested_alleles": "Tested alleles", + "@pdf_tested_alleles": {}, "pdf_user_guideline": "User guideline", + "@pdf_user_guideline": {}, "pdf_guideline_link": "{guidelineSource} guideline link", "@pdf_guideline_link": { "placeholders": { @@ -273,101 +405,205 @@ }, "nav_report": "Genes", + "@nav_report": {}, "tab_report": "Gene report", + "@tab_report": {}, "nav_drugs": "Medications", + "@nav_drugs": {}, "tab_drugs": "Medication overview", + "@tab_drugs": {}, "nav_faq": "FAQ", + "@nav_faq": {}, "tab_faq": "Common questions", + "@tab_faq": {}, "nav_more": "More", + "@nav_more": {}, "tab_more": "More", + "@tab_more": {}, "tutorial_initial_drug_selection_title": "Setup PharMe", + "@tutorial_initial_drug_selection_title": {}, "tutorial_initial_drug_selection_body": "As a first step, please update the list of your current medications.", + "@tutorial_initial_drug_selection_body": {}, "tutorial_app_tour_1_title": "App Tour (1/5) · Navigation", + "@tutorial_app_tour_1_title": {}, "tutorial_app_tour_1_body": "We would first like to guide you through the app's main functions.\nYou can switch between PharMe's main screens using the bottom navigation bar – if you want to re-watch this app tour later, you can always do so on the last screen under More.", + "@tutorial_app_tour_1_body": {}, "tutorial_app_tour_2_title": "App Tour (2/5) · Medication List", + "@tutorial_app_tour_2_title": {}, "tutorial_app_tour_2_body": "Under Medications, you will find the list of all available medications in PharMe.\nYou can search for specific generic names, brand names, or medication classes, and filter the list by guideline result.\nAll medications in PharMe are labeled with a color and an icon: ", + "@tutorial_app_tour_2_body": {}, "tutorial_app_tour_3_title": "App Tour (3/5) · Medication Details", + "@tutorial_app_tour_3_title": {}, "tutorial_app_tour_3_body": "The medication details provide further information about how well this medication works for you, according to scientific guidelines.\nHere you can also change whether you are currently taking a medication and export a report for healthcare professionals.", + "@tutorial_app_tour_3_body": {}, "tutorial_app_tour_4_title": "App Tour (4/5) · Gene Report", + "@tutorial_app_tour_4_title": {}, "tutorial_app_tour_4_body": "Under Genes, you will find the results of your genetic test for genes with known medication interactions.\nSelect a gene to learn more about your results and how this gene might interact with specific medications.", + "@tutorial_app_tour_4_body": {}, "tutorial_app_tour_5_title": "App Tour (5/5) · FAQ & Additional Features", + "@tutorial_app_tour_5_title": {}, "tutorial_app_tour_5_body": "Under FAQ, you will find a list of frequently asked questions and further resources.\nUnder More, you will find additional information about the app as well as a contact form and other useful features. Please reach out if you have any questions while using the app!", + "@tutorial_app_tour_5_body": {}, "onboarding_get_started": "Get started", + "@onboarding_get_started": {}, "onboarding_next": "Next", + "@onboarding_next": {}, "onboarding_prev": "Back", + "@onboarding_prev": {}, "onboarding_1_header": "Welcome to PharMe", + "@onboarding_1_header": {}, "onboarding_1_text": "Your genome influences your health more than you might think, including how you react to medications.\n\nMore than 90 percent of people are vulnerable to unintended medication reactions.\n\nUse PharMe to find out about yours.", + "@onboarding_1_text": {}, "onboarding_2_header": "One size does not fit all", + "@onboarding_2_header": {}, "onboarding_1_disclaimer_part_1": "The information provided in PharMe is ONLY based on your DNA and certain medications that may interact with your genetic result.", + "@onboarding_1_disclaimer_part_1": {}, "onboarding_2_text": "Each person’s body reacts to medications differently.\n\nMedications that are effective for a majority of people can have adverse side effects for you.", + "@onboarding_2_text": {}, "onboarding_3_header": "Genome power unlocked to improve human health", + "@onboarding_3_header": {}, "onboarding_3_text": "PharMe informs you if your genome makes you more likely to experience an unintended medication response.\n\nThis enables you to avoid medications that are ineffective or have side effects.", + "@onboarding_3_text": {}, "onboarding_3_disclaimer": "Please note that this app does not provide recommendations for medications or dosages. Always consult your pharmacist or doctor for personalized advice.", + "@onboarding_3_disclaimer": {}, "onboarding_4_header": "Tailored to your genome", + "@onboarding_4_header": {}, "onboarding_4_text": "For PharMe to work, you need to get your genetics (DNA) tested at a lab. You don't need an account to use PharMe: You can just sign in to the lab's website through our app.", + "@onboarding_4_text": {}, "onboarding_4_button": "Find out more about gene tests here.", + "@onboarding_4_button": {}, "onboarding_4_already_tested_text": "PharMe works by matching your genetic data with known information about interactions between certain genes and medications.\n\nThe information presented in PharMe is based on scientifically proven guidelines published by the Clinical Pharmacogenetics Implementation Consortium (CPIC®) and the U.S. Food and Drug Administration (FDA).", + "@onboarding_4_already_tested_text": {}, "onboarding_5_header": "We care about your data protection", + "@onboarding_5_header": {}, "onboarding_5_text": "After downloading your genetic data from your data provider, it is solely kept on your phone in encrypted form.\n\nOur servers know nothing about you, neither your identity nor your DNA.", + "@onboarding_5_text": {}, + "more_page_account_settings": "Settings", + "@more_page_account_settings": {}, "more_page_delete_data": "Delete app data", + "@more_page_delete_data": {}, "more_page_delete_data_text": "Are you sure that you want to delete all app data? This also includes your genetic data and will reset the app.", + "@more_page_delete_data_text": {}, "more_page_delete_data_additional_text": "Your genetic data will be deleted and it might not be possible to import it again.", + "@more_page_delete_data_additional_text": {}, "more_page_delete_data_confirmation": "I understand the consequences and want to delete all app data", + "@more_page_delete_data_confirmation": {}, "more_page_app_information": "App information", + "@more_page_app_information": {}, "more_page_onboarding": "Repeat onboarding", + "@more_page_onboarding": {}, "more_page_app_tour": "Repeat app tour", + "@more_page_app_tour": {}, "more_page_about_us": "About us", + "@more_page_about_us": {}, "more_page_about_us_text": "PharMe was created as a bachelor's project at Hasso Plattner Institute (HPI) in Potsdam, Germany, in collaboration with health professionals from the Mount Sinai Health System in New York City, NY, USA.", + "@more_page_about_us_text": {}, "more_page_privacy_policy": "Privacy policy", + "@more_page_privacy_policy": {}, "more_page_privacy_policy_text": "These aren't the Droids you're looking for.", + "@more_page_privacy_policy_text": {}, "more_page_terms_and_conditions": "Terms of use", + "@more_page_terms_and_conditions": {}, "more_page_terms_and_conditions_text": "These aren't the Droids you're looking for.", + "@more_page_terms_and_conditions_text": {}, "more_page_help_and_feedback": "Help & Feedback", + "@more_page_help_and_feedback": {}, "more_page_genetic_information": "Learn about genetics (MedlinePlus)", + "@more_page_genetic_information": {}, "more_page_contact_us": "Contact us", + "@more_page_contact_us": {}, "contact_text": "You can contact us using the following email address:", + "@contact_text": {}, "contact_context_text": "To help us to understand the context of your message, please include the following information:", + "@contact_context_text": {}, "contact_subject": "Subject:", + "@contact_subject": {}, "contact_body": "Text:", + "@contact_body": {}, "contact_open_mail": "Send mail", + "@contact_open_mail": {}, "comprehension_intro_text": "Would you like to participate in a survey aiming to measure user comprehension of content in the app? This would help us make PharMe more understandable for everyone!", + "@comprehension_intro_text": {}, "comprehension_survey_button_text": "Continue to survey", + "@comprehension_survey_button_text": {}, "faq_section_title_pgx": "Pharmacogenomics (PGx)", + "@faq_section_title_pgx": {}, "faq_question_pgx_what": "What is pharmacogenomics?", + "@faq_question_pgx_what": {}, "faq_answer_pgx_what": "Pharmacogenomics (PGx) is the study of how your genes (DNA) affect your response to medications.", + "@faq_answer_pgx_what": {}, "faq_question_pgx_why": "Why is pharmacogenomics important?", + "@faq_question_pgx_why": {}, "faq_answer_pgx_why": "Pharmacogenomics is important because it helps to predict those who will respond well to medications and those who may have side effects. With this information we can better select the right medication and dose to avoid side effects.", + "@faq_answer_pgx_why": {}, "faq_question_genetics_info": "Where can I find out more about genetics in general?", + "@faq_question_genetics_info": {}, "faq_answer_genetics_info": "To learn more about genetics, we recommend MedlinePlus, a service of the National Library of Medicine:", + "@faq_answer_genetics_info": {}, "faq_question_which_medications": "Which medications have known gene interactions?", + "@faq_question_which_medications": {}, "faq_answer_which_medications": "Examples of medication classes with known gene interactions include anti-clotting medications (like clopidogrel and warfarin), antidepressants (like sertraline, citalopram, and paroxetine), anti-cholesterol medications (like simvastatin and atorvastatin), acid reducers (like pantoprazole and omeprazole), pain killers (like codeine, tramadol, and ibuprofen), antifungals (like voriconazole), medications that suppress the immune system (like tacrolimus), and anti-cancer medications (like fluorouracil and irinotecan).\n\nSearch a medication in the Medications tab to find out whether it has known gene interactions according to CPIC® and FDA guidelines.", + "@faq_answer_which_medications": {}, "faq_question_phenoconversion": "Why can my results change when I take certain medications?", + "@faq_question_phenoconversion": {}, "faq_answer_phenoconversion": "Certain medications can change your phenotype that descries how your body responds to medications. Typically, medications either inhibit or induce the activity of a gene. In PharMe, the following interactions are included:", + "@faq_answer_phenoconversion": {}, "faq_strong_inhibitors": "Strong {geneName} inhibitors:", + "@faq_strong_inhibitors": { + "placeholders": { + "geneName": { + "type": "String", + "example": "CYP2D6" + } + } + }, "faq_moderate_inhibitors": "Moderate {geneName} inhibitors:", + "@faq_moderate_inhibitors": { + "placeholders": { + "geneName": { + "type": "String", + "example": "CYP2D6" + } + } + }, "faq_question_family": "Will my results affect my family members?", + "@faq_question_family": {}, "faq_answer_family": "Yes, since this is a genetic test, it is possible that your results were passed down to you and your siblings from your parents and you will also pass them down to your children.", + "@faq_answer_family": {}, "faq_question_share": "Who can I share my results with?", + "@faq_question_share": {}, "faq_answer_share": "We recommend that you share your results with your pharmacists, doctors, and close family members such as parents, siblings, and children.", + "@faq_answer_share": {}, "faq_section_title_pharme": "PharMe App", + "@faq_section_title_pharme": {}, "faq_question_pharme_function": "What does PharMe do?", + "@faq_question_pharme_function": {}, "faq_answer_pharme_function": "PharMe provides user-friendly information on how your body reacts to medications based on your genes. This enables you to better understand which medications may be ineffective for you or could have potential side effects. We recommend that you share consult your health care team before making any changes to your treatments.", + "@faq_answer_pharme_function": {}, "faq_question_pharme_hcp": "Can I use PharMe's results without consulting a medical professional?", + "@faq_question_pharme_hcp": {}, "faq_answer_pharme_hcp": "No. Whether a medication is a good choice for you depends on a lot of other factors such as age, weight, or pre-existing conditions. We highly recommend that you talk to your health care team (e.g., pharmacist and doctors) before taking, stopping or adjusting the dose of any medication.", + "@faq_answer_pharme_hcp": {}, "faq_question_pharme_data_source": "Where does PharMe get its data from?", + "@faq_question_pharme_data_source": {}, "faq_answer_pharme_data_source": "PharMe is showing pharmacogenomic guidelines from the Clinical Pharmacogenetics Implementation Consortium (CPIC®) and the U.S. Food and Drug Administration (FDA). Our PGx experts adapted the language from the guidelines to make them more user-friendly and easier to understand; please note that this does only affect the guidelines' presentation, not affect the guidelines' statements.", + "@faq_answer_pharme_data_source": {}, "faq_section_title_security": "Data security", + "@faq_section_title_security": {}, "faq_question_data_security": "How is the security of my genetic data ensured?", + "@faq_question_data_security": {}, "faq_answer_data_security": "Once securely imported from the lab, your genetic data is re-encrypted, saved and never sent anywhere else. All computation is done on your phone. When fetching data from external resources, PharMe always uses generalized requests and only personalizes information locally on your phone. No personal data is sent to third parties. This provides the highest level of security for your personal information.", + "@faq_answer_data_security": {}, - "faq_contact_us": "Do you have unanswered questions or feedback? Contact us" + "faq_contact_us": "Do you have unanswered questions or feedback? Contact us", + "@faq_contact_us": {} } diff --git a/app/lib/onboarding/pages/onboarding.dart b/app/lib/onboarding/pages/onboarding.dart index 0c1e7037..720ed591 100644 --- a/app/lib/onboarding/pages/onboarding.dart +++ b/app/lib/onboarding/pages/onboarding.dart @@ -3,76 +3,62 @@ import '../../common/models/metadata.dart'; @RoutePage() class OnboardingPage extends HookWidget { - OnboardingPage({ this.isRevisiting = false }); + const OnboardingPage({ this.isRevisiting = false }); final bool isRevisiting; - final iconSize = 32.0; - final sidePadding = PharMeTheme.mediumSpace; - final indicatorSize = PharMeTheme.smallSpace; - final indicatorPadding = PharMeTheme.largeSpace; - - double getTopPadding(BuildContext context) { - return MediaQuery.of(context).padding.top + sidePadding; - } - - double _getBottomPadding(BuildContext context) { - return MediaQuery.of(context).padding.bottom + PharMeTheme.mediumSpace; - } - - double _getBottomSpace(context) { - // Icon button height and indicators - final bottomWidgetsSize = iconSize + indicatorSize + indicatorPadding; - const spaceBetweenBottomWidgets = PharMeTheme.largeSpace; - return _getBottomPadding(context) - + bottomWidgetsSize - + spaceBetweenBottomWidgets; - } - - final _pages = [ - OnboardingSubPage( - illustrationPath: 'assets/images/onboarding/OutlinedLogo.png', - getHeader: (context) => context.l10n.onboarding_1_header, - getText: (context) => context.l10n.onboarding_1_text, - color: PharMeTheme.sinaiCyan, - child: disclaimerCard( - getText: (context) => context.l10n.onboarding_1_disclaimer_part_1, - getSecondLineText: (context) => - context.l10n.drugs_page_disclaimer_text_part_2, - ), - ), - OnboardingSubPage( - illustrationPath: 'assets/images/onboarding/DrugReaction.png', - getHeader: (context) => context.l10n.onboarding_2_header, - getText: (context) => context.l10n.onboarding_2_text, - color: PharMeTheme.sinaiMagenta, - ), - OnboardingSubPage( - illustrationPath: 'assets/images/onboarding/GenomePower.png', - getHeader: (context) => context.l10n.onboarding_3_header, - getText: (context) => context.l10n.onboarding_3_text, - color: PharMeTheme.sinaiPurple, - child: disclaimerCard( - getText: (context) => context.l10n.onboarding_3_disclaimer, - ), - ), - OnboardingSubPage( - illustrationPath: 'assets/images/onboarding/Tailored.png', - getHeader: (context) => context.l10n.onboarding_4_header, - getText: (context) => context.l10n.onboarding_4_already_tested_text, - color: Colors.grey.shade600, - ), - OnboardingSubPage( - illustrationPath: 'assets/images/onboarding/DataProtection.png', - getHeader: (context) => context.l10n.onboarding_5_header, - getText: (context) => context.l10n.onboarding_5_text, - color: PharMeTheme.sinaiCyan, - ), - ]; - @override Widget build(BuildContext context) { - final colors = _pages.map((page) => page.color); + final pages = [ + OnboardingSubPage( + availableHeight: + OnboardingDimensions.contentHeight(context, isRevisiting), + illustrationPath: 'assets/images/onboarding/OutlinedLogo.png', + header: context.l10n.onboarding_1_header, + text: context.l10n.onboarding_1_text, + color: PharMeTheme.sinaiCyan, + child: DisclaimerCard( + text: context.l10n.onboarding_1_disclaimer_part_1, + secondLineText: context.l10n.drugs_page_disclaimer_text_part_2, + ), + ), + OnboardingSubPage( + availableHeight: + OnboardingDimensions.contentHeight(context, isRevisiting), + illustrationPath: 'assets/images/onboarding/DrugReaction.png', + header: context.l10n.onboarding_2_header, + text: context.l10n.onboarding_2_text, + color: PharMeTheme.sinaiMagenta, + ), + OnboardingSubPage( + availableHeight: + OnboardingDimensions.contentHeight(context, isRevisiting), + illustrationPath: 'assets/images/onboarding/GenomePower.png', + header: context.l10n.onboarding_3_header, + text: context.l10n.onboarding_3_text, + color: PharMeTheme.sinaiPurple, + child: DisclaimerCard( + text: context.l10n.onboarding_3_disclaimer, + ), + ), + OnboardingSubPage( + availableHeight: + OnboardingDimensions.contentHeight(context, isRevisiting), + illustrationPath: 'assets/images/onboarding/Tailored.png', + header: context.l10n.onboarding_4_header, + text: context.l10n.onboarding_4_already_tested_text, + color: Colors.grey.shade600, + ), + OnboardingSubPage( + availableHeight: + OnboardingDimensions.contentHeight(context, isRevisiting), + illustrationPath: 'assets/images/onboarding/DataProtection.png', + header: context.l10n.onboarding_5_header, + text: context.l10n.onboarding_5_text, + color: PharMeTheme.sinaiCyan, + ), + ]; + final colors = pages.map((page) => page.color); final tweenSequenceItems = []; for (var tweenIndex = 0; tweenIndex < colors.length - 1; tweenIndex++) { tweenSequenceItems.add( @@ -95,7 +81,7 @@ class OnboardingPage extends HookWidget { animation: pageController, builder: (context, child) { final color = pageController.hasClients - ? pageController.page! / (_pages.length - 1) + ? pageController.page! / (pages.length - 1) : .0; return DecoratedBox( @@ -109,50 +95,53 @@ class OnboardingPage extends HookWidget { alignment: Alignment.topCenter, children: [ if (isRevisiting) Positioned( - top: getTopPadding(context), - right: sidePadding, + top: OnboardingDimensions.getTopPadding(context), + right: OnboardingDimensions.sidePadding, child: IconButton( icon: Icon( Icons.close, - size: iconSize, + size: OnboardingDimensions.iconSize, color: Colors.white, ), onPressed: () => context.router.back(), ) ), Positioned.fill( - top: isRevisiting - ? getTopPadding(context) + iconSize - : getTopPadding(context), - bottom: _getBottomSpace(context), + top: OnboardingDimensions.getTopSpace(context, isRevisiting), + bottom: OnboardingDimensions.getBottomSpace(context), child: Padding( - padding: EdgeInsets.symmetric(horizontal: sidePadding), + padding: EdgeInsets.symmetric( + horizontal: OnboardingDimensions.sidePadding, + ), child: PageView( controller: pageController, onPageChanged: (newPage) => currentPage.value = newPage, - children: _pages, + children: pages, ), ), ), Positioned( - bottom: _getBottomSpace(context) - indicatorSize - indicatorPadding, + bottom: OnboardingDimensions.getBottomSpace(context) - + OnboardingDimensions.indicatorSize - + OnboardingDimensions.indicatorPadding, child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: _buildPageIndicator(context, currentPage.value), + children: + _buildPageIndicator(context, pages, currentPage.value), ), ), Positioned( - bottom: _getBottomPadding(context), - right: sidePadding, + bottom: OnboardingDimensions.getBottomPadding(context), + right: OnboardingDimensions.sidePadding, child: _buildNextButton( context, pageController, - currentPage.value == _pages.length - 1, + currentPage.value == pages.length - 1, ), ), Positioned( - bottom: _getBottomPadding(context), - left: sidePadding, + bottom: OnboardingDimensions.getBottomPadding(context), + left: OnboardingDimensions.sidePadding, child: _buildPrevButton( context, pageController, @@ -165,9 +154,13 @@ class OnboardingPage extends HookWidget { ); } - List _buildPageIndicator(BuildContext context, int currentPage) { + List _buildPageIndicator( + BuildContext context, + List pages, + int currentPage, + ) { final list = []; - for (var i = 0; i < _pages.length; ++i) { + for (var i = 0; i < pages.length; ++i) { list.add(_indicator(context, i == currentPage)); } return list; @@ -176,8 +169,8 @@ class OnboardingPage extends HookWidget { Widget _indicator(BuildContext context, bool isActive) { return AnimatedContainer( duration: Duration(milliseconds: 150), - margin: EdgeInsets.symmetric(horizontal: indicatorSize), - height: indicatorSize, + margin: EdgeInsets.symmetric(horizontal: OnboardingDimensions.indicatorSize), + height: OnboardingDimensions.indicatorSize, width: isActive ? PharMeTheme.mediumToLargeSpace : PharMeTheme.mediumSpace, @@ -220,7 +213,7 @@ class OnboardingPage extends HookWidget { ); } }, - iconSize: iconSize, + iconSize: OnboardingDimensions.iconSize, onDarkBackground: true, emphasize: isLastPage, ); @@ -242,7 +235,7 @@ class OnboardingPage extends HookWidget { ); }, text: context.l10n.onboarding_prev, - iconSize: iconSize, + iconSize: OnboardingDimensions.iconSize, onDarkBackground: true, ); } else { @@ -251,111 +244,215 @@ class OnboardingPage extends HookWidget { } } -class OnboardingSubPage extends StatelessWidget { +class OnboardingDimensions { + static const iconSize = 32.0; + static const sidePadding = PharMeTheme.mediumSpace; + static const indicatorSize = PharMeTheme.smallSpace; + static const indicatorPadding = PharMeTheme.largeSpace; + + static double getTopPadding(BuildContext context) { + return MediaQuery.of(context).padding.top + sidePadding; + } + + // ignore: avoid_positional_boolean_parameters + static double getTopSpace(BuildContext context, bool isRevisiting) { + return isRevisiting + ? OnboardingDimensions.getTopPadding(context) + + OnboardingDimensions.iconSize + : OnboardingDimensions.getTopPadding(context); + } + + static double getBottomPadding(BuildContext context) { + return MediaQuery.of(context).padding.bottom + PharMeTheme.mediumSpace; + } + + static double getBottomSpace(BuildContext context) { + // Icon button height and indicators + const bottomWidgetsSize = iconSize + indicatorSize + indicatorPadding; + const spaceBetweenBottomWidgets = PharMeTheme.largeSpace; + return getBottomPadding(context) + + bottomWidgetsSize + + spaceBetweenBottomWidgets; + } + + // ignore: avoid_positional_boolean_parameters + static double contentHeight(BuildContext context, bool isRevisiting) { + return MediaQuery.of(context).size.height + - getTopSpace(context, isRevisiting) + - getBottomSpace(context); + } + + static double contentWidth(BuildContext context) { + return MediaQuery.of(context).size.width - 2 * sidePadding; + } +} + +class OnboardingSubPage extends HookWidget { const OnboardingSubPage({ required this.illustrationPath, this.secondImagePath, - required this.getHeader, - required this.getText, + required this.header, + required this.text, required this.color, + required this.availableHeight, this.child, }); final String illustrationPath; final String? secondImagePath; - final String Function(BuildContext) getHeader; - final String Function(BuildContext) getText; + final String header; + final String text; + final double availableHeight; final Color color; final Widget? child; + double? _getContentHeight(GlobalKey contentKey) { + return contentKey.currentContext?.size?.height; + } + + double? _getMaxScrollOffset(GlobalKey contentKey) { + final contentHeight = _getContentHeight(contentKey); + if (contentHeight == null) return null; + return contentHeight - availableHeight; + } + + bool? _contentScrollable(GlobalKey contentKey) { + final contentHeight = _getContentHeight(contentKey); + if (contentHeight == null) return null; + return availableHeight < contentHeight; + } + + bool? _scrolledToEnd( + GlobalKey contentKey, + ScrollController scrollController, + ) { + final maxScrollOffset = _getMaxScrollOffset(contentKey); + if (maxScrollOffset == null) return null; + return scrollController.offset >= maxScrollOffset; + } + @override Widget build(BuildContext context) { - const scrollbarThickness = 4.0; + const scrollbarThickness = 6.5; const iconButtonPadding = 16.0; // to align the scrollbar - + const horizontalPadding = iconButtonPadding + 3 * scrollbarThickness; + const imageHeight = 175.0; + final contentKey = GlobalKey(); + final showScrollIndicatorButton = useState(false); final scrollController = ScrollController(); - return RawScrollbar( - controller: scrollController, // needed to always show scrollbar - thumbVisibility: true, - shape: StadiumBorder(), - padding: EdgeInsets.only( - top: PharMeTheme.mediumToLargeSpace, - right: iconButtonPadding, - ), - thumbColor: Colors.white54, - thickness: scrollbarThickness, - child: SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: iconButtonPadding + 3 * scrollbarThickness, + + WidgetsBinding.instance.addPostFrameCallback((_) { + final contentScrollable = _contentScrollable(contentKey) ?? false; + final scrolledToEnd = _scrolledToEnd(contentKey, scrollController) ?? false; + showScrollIndicatorButton.value = contentScrollable && !scrolledToEnd; + }); + + scrollController.addListener(() { + final hideButton = _scrolledToEnd(contentKey, scrollController) ?? false; + showScrollIndicatorButton.value = !hideButton; + }); + + return Stack( + alignment: Alignment.center, + children: [ + RawScrollbar( + controller: scrollController, // needed to always show scrollbar + thumbVisibility: true, + shape: StadiumBorder(), + padding: EdgeInsets.only( + top: PharMeTheme.mediumToLargeSpace, + right: iconButtonPadding, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: PharMeTheme.mediumSpace), - Center( - child: FractionallySizedBox( - alignment: Alignment.topCenter, - widthFactor: 0.75, - child: Image.asset( - illustrationPath, - height: 175, - ), - ), + thumbColor: Colors.white, + thickness: scrollbarThickness, + child: SingleChildScrollView( + controller: scrollController, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, ), - SizedBox(height: PharMeTheme.mediumToLargeSpace), - Column(children: [ - AutoSizeText( - getHeader(context), - style: PharMeTheme.textTheme.headlineLarge!.copyWith( - color: Colors.white, - ), - maxLines: 2, - ), - SizedBox(height: PharMeTheme.mediumToLargeSpace), - Text( - getText(context), - style: PharMeTheme.textTheme.bodyLarge!.copyWith( - color: Colors.white, - ), - ), - if (child != null) ...[ + child: Column( + key: contentKey, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ SizedBox(height: PharMeTheme.mediumSpace), - child!, + Center( + child: FractionallySizedBox( + alignment: Alignment.topCenter, + widthFactor: 0.75, + child: Image.asset( + illustrationPath, + height: imageHeight, + ), + ), + ), + SizedBox(height: PharMeTheme.mediumToLargeSpace), + Column(children: [ + AutoSizeText( + header, + style: PharMeTheme.textTheme.headlineLarge!.copyWith( + color: Colors.white, + ), + maxLines: 2, + ), + SizedBox(height: PharMeTheme.mediumToLargeSpace), + Text( + text, + style: PharMeTheme.textTheme.bodyLarge!.copyWith( + color: Colors.white, + ), + ), + if (child != null) ...[ + SizedBox(height: PharMeTheme.mediumSpace), + child!, + ], + ]), + // Empty widget for spaceBetween in this column to work properly + Container(), ], - ]), - // Empty widget for spaceBetween in this column to work properly - Container(), - ], + ), + ), ), ), - ), + if (showScrollIndicatorButton.value) Positioned( + bottom: 0, + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.white, + side: BorderSide(color: color, width: 3), + ), + icon: Icon( + Icons.arrow_downward, + size: OnboardingDimensions.iconSize * 0.85, + color: color, + ), + onPressed: () async { + await scrollController.animateTo( + _getMaxScrollOffset(contentKey)!, + duration: Duration(milliseconds: 500), + curve: Curves.linearToEaseOut, + ); + showScrollIndicatorButton.value = false; + }, + ) + ), + ], ); } } -BottomCard disclaimerCard({ - required String Function(BuildContext) getText, - String Function(BuildContext)? getSecondLineText, -}) => BottomCard( - getText: getText, - icon: Icon(Icons.warning_rounded, size: 32), - getSecondLineText: getSecondLineText, -); - -class BottomCard extends StatelessWidget { - const BottomCard({ +class DisclaimerCard extends StatelessWidget { + const DisclaimerCard({ this.icon, - required this.getText, - this.getSecondLineText, + required this.text, + this.secondLineText, this.onClick, }); final Icon? icon; - final String Function(BuildContext) getText; - final String Function(BuildContext)? getSecondLineText; + final String text; + final String? secondLineText; final GestureTapCallback? onClick; @override @@ -370,17 +467,15 @@ class BottomCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (icon != null) ...[ - icon!, - SizedBox(width: PharMeTheme.smallSpace), - ], + icon ?? Icon(Icons.warning_rounded, size: 32), + SizedBox(width: PharMeTheme.smallSpace), Expanded( child: Column( children: [ - getTextWidget(getText(context)), - if (getSecondLineText != null) ...[ + getTextWidget(text), + if (secondLineText != null) ...[ SizedBox(height: PharMeTheme.smallSpace), - getTextWidget(getSecondLineText!(context)), + getTextWidget(secondLineText!), ] ], ), @@ -398,6 +493,6 @@ class BottomCard extends StatelessWidget { Widget getTextWidget(String text) => Text( text, style: PharMeTheme.textTheme.bodyMedium, - textAlign: (icon != null) ? TextAlign.start : TextAlign.center, + textAlign: TextAlign.start, ); } diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 3fedca59..3d7cc1b2 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -24,6 +24,7 @@ class SearchPage extends HookWidget { child: unscrollablePageScaffold( title: context.l10n.tab_drugs, canNavigateBack: false, + contextToDismissFocusOnTap: context, body: DrugSearch( key: Key('drug-search'), showFilter: true, diff --git a/pharme.code-workspace b/pharme.code-workspace index c16fb2cb..d02f8770 100644 --- a/pharme.code-workspace +++ b/pharme.code-workspace @@ -63,6 +63,7 @@ "drugrecommendation", "drugselection", "duckdns", + "duckdns", "duloxetine", "endoxifen", "Ezallor",