From 6748553105bf54186784f1b72a90e4d5ef187a85 Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 14 Nov 2024 16:08:01 +0100 Subject: [PATCH 01/17] build(app): hacky fix for building APKs --- app/.gitignore | 3 +++ app/CONTRIBUTING.md | 18 +++++++++++++ app/android/build.gradle | 56 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/app/.gitignore b/app/.gitignore index 4ff0d833..de8b9cf0 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,3 +1,6 @@ +# Files generated due to fix +android/build/app/ + # Miscellaneous *.class *.log diff --git a/app/CONTRIBUTING.md b/app/CONTRIBUTING.md index 34974fd9..77bc3ac5 100644 --- a/app/CONTRIBUTING.md +++ b/app/CONTRIBUTING.md @@ -28,6 +28,24 @@ alias flutter-generate='dart run build_runner build --delete-conflicting-outputs alias flutter-clean='find . -maxdepth 20 -type f \( -name "*.inject.summary" -o -name "*.inject.dart" -o -name "*.g.dart" \) -delete' ``` +## Updating Flutter and Android + +... can be super painful, because Java, Gradle, and Kotlin versions may be +wrong. + +Often problems in packages arise that rely on older Gradle versions. + +The places to check are: + +- Your Flutter version +- Your `JAVA_HOME` version +- The Gradle version in `gradle-wrapper.properties`; the recommended versions + are "between 7.3 and 7.6.1." (see + [Android Java Gradle migration guide](https://docs.flutter.dev/release/breaking-changes/android-java-gradle-migration-guide)) +- The Java version in `android/app/build/gradle` +- The Java version used for Gradle (check in Android Studio) +- The Kotlin version in `settings.gradle` + ## Architecture The app consists of multiple so-called modules. Our main modules (usually app diff --git a/app/android/build.gradle b/app/android/build.gradle index aca8b4a2..329f8738 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -18,6 +18,55 @@ allprojects { } } +subprojects { + afterEvaluate { project -> + // From https://medium.com/@vortj/solving-namespace-errors-in-flutters-android-gradle-configuration-c2baa6262f8b + if (project.hasProperty('android')) { + project.android { + if (namespace == null) { + namespace = project.group.toString() // Set namespace as fallback + } + project.tasks.whenTaskAdded { task -> + if (task.name.contains('processDebugManifest') || task.name.contains('processReleaseManifest')) { + task.doFirst { + File manifestFile = file("${projectDir}/src/main/AndroidManifest.xml") + if (manifestFile.exists()) { + String manifestContent = manifestFile.text + if (manifestContent.contains('package=')) { + manifestContent = manifestContent.replaceAll(/package="[^"]*"/, "") + manifestFile.write(manifestContent) + println "Removed 'package' attribute from ${manifestFile}" + } + } + } + } + } + } + } + // From https://github.com/flutter/flutter/issues/153281#issuecomment-2292201697 + if (project.extensions.findByName("android") != null) { + Integer pluginCompileSdk = project.android.compileSdk + if (pluginCompileSdk != null && pluginCompileSdk < 31) { + project.logger.error( + "Warning: Overriding compileSdk version in Flutter plugin: " + + project.name + + " from " + + pluginCompileSdk + + " to 31 (to work around https://issuetracker.google.com/issues/199180389)." + + "\nIf there is not a new version of " + project.name + ", consider filing an issue against " + + project.name + + " to increase their compileSdk to the latest (otherwise try updating to the latest version)." + ) + project.android { + compileSdk 31 + } + } + } + } + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(":app") +} + rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" @@ -27,3 +76,10 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + +// From https://stackoverflow.com/a/69050529 +configurations.all { + resolutionStrategy { + force 'androidx.core:core-ktx:1.6.0' + } +} From 2d2955a1a793d1c3c7192d73ab42eff6dbfea318 Mon Sep 17 00:00:00 2001 From: tamslo Date: Mon, 18 Nov 2024 17:39:14 +0100 Subject: [PATCH 02/17] build(app): adapt Android build setup --- app/android/app/build.gradle | 42 ++++++++++--------- app/android/build.gradle | 17 +------- .../gradle/wrapper/gradle-wrapper.properties | 4 +- app/android/settings.gradle | 30 +++++++++---- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index b605f436..88d3d85e 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,6 +12,12 @@ if (localPropertiesFile.exists()) { } } +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") @@ -21,35 +33,31 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { namespace 'de.hpi.pharme' - - compileSdk flutter.compileSdkVersion + compileSdk = flutter.compileSdkVersion + ndkVersion = "27.0.12077973" compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '17' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + jvmTarget = JavaVersion.VERSION_17 } defaultConfig { applicationId 'de.hpi.pharme' minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName multiDexEnabled true - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + } + + dependencies { + implementation 'com.android.support:multidex:1.0.3' } buildTypes { @@ -64,7 +72,3 @@ android { flutter { source '../..' } - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/app/android/build.gradle b/app/android/build.gradle index 329f8738..e0e4fa6f 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.0' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -63,13 +50,13 @@ subprojects { } } } - project.buildDir = "${rootProject.buildDir}/${project.name}" - project.evaluationDependsOn(":app") } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { project.evaluationDependsOn(':app') } diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index 5c6f89db..348c409e 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip diff --git a/app/android/settings.gradle b/app/android/settings.gradle index 44e62bcf..f83f10ea 100644 --- a/app/android/settings.gradle +++ b/app/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.7.1" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" From 82775b0a90ce24f33744b5e77ed7b0d7df05ffd6 Mon Sep 17 00:00:00 2001 From: tamslo Date: Wed, 20 Nov 2024 12:07:52 +0100 Subject: [PATCH 03/17] build(app): update iOS meta information --- app/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- app/ios/Runner/Info.plist | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) 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 - From 36155a5aa6b8c368609a75c117af97344f5c53ac Mon Sep 17 00:00:00 2001 From: tamslo Date: Wed, 20 Nov 2024 12:34:48 +0100 Subject: [PATCH 04/17] chore(app): add missing l10n metadata --- app/lib/l10n/app_en.arb | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 77aaf04f..302a5b3f 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -106,9 +106,57 @@ "inhibitor_third_person_salutation": "the user is", "inhibitor_third_person_salutation_genitive": "the user's", "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.", "drugs_page_guidelines_empty": "No guidelines are present for {drugName}", @@ -351,7 +399,23 @@ "faq_question_phenoconversion": "Why can my results change when I take certain medications?", "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_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_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_question_share": "Who can I share my results with?", From fbd3099fe226dfdb7bb50870f3542ef49756804e Mon Sep 17 00:00:00 2001 From: tamslo Date: Wed, 20 Nov 2024 13:04:54 +0100 Subject: [PATCH 05/17] feat(app): show dialog instead of different explanation text when continuing from first medication selection --- app/lib/common/widgets/dialog_wrapper.dart | 8 ++--- .../drug_selection/pages/drug_selection.dart | 30 ++++++++++++++----- app/lib/l10n/app_en.arb | 2 +- 3 files changed, 28 insertions(+), 12 deletions(-) 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/drug_selection/pages/drug_selection.dart b/app/lib/drug_selection/pages/drug_selection.dart index 1e38b865..ac0a997e 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -38,9 +38,7 @@ class DrugSelectionPage extends HookWidget { 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 +65,28 @@ 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( + 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 302a5b3f..c03d069f 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -32,7 +32,7 @@ "drug_item_brand_names": "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_continue_warning": "If you continue, you will complete the initial setup and will not be able to come back to this page. You can always change the status for a medication later in the app.", "drug_selection_settings_description": "Review the medications you are currently taking below.", "drug_selection_no_drugs_loaded": "No medications loaded", From 5d9e9c924568fe2efd72ab3271d489d3a298ed47 Mon Sep 17 00:00:00 2001 From: tamslo Date: Wed, 20 Nov 2024 14:05:08 +0100 Subject: [PATCH 06/17] chore(app): add empty metadata fields to l10n where missing --- app/lib/l10n/app_en.arb | 172 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index c03d069f..445c25ee 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,73 @@ }, "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_header": {}, "drug_selection_continue_warning": "If you continue, you will complete the initial setup and will not be able to come back to this page. 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 +115,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 +126,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,9 +148,13 @@ }, "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": { @@ -158,6 +208,7 @@ } }, "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": { @@ -169,12 +220,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": { @@ -194,11 +252,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": { @@ -213,6 +276,7 @@ } }, "report_no_result_genes": "Genes with no result", + "@report_no_result_genes": {}, "gene_page_headline": "{gene} report", "@gene_page_headline": { @@ -242,10 +306,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": { @@ -256,20 +325,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": { @@ -321,83 +403,156 @@ }, "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": { @@ -417,21 +572,36 @@ } }, "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": {} } From 9d964e4d7a5ce3ed19169e568555831881215d37 Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 14:08:30 +0100 Subject: [PATCH 07/17] fix(app): do not show inhibitor warning if indicators are not shown --- app/lib/common/widgets/drug_activity_selection.dart | 3 ++- .../widgets/drug_list/drug_items/drug_selection_list.dart | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/lib/common/widgets/drug_activity_selection.dart b/app/lib/common/widgets/drug_activity_selection.dart index f6b4ab2c..a74c35e0 100644 --- a/app/lib/common/widgets/drug_activity_selection.dart +++ b/app/lib/common/widgets/drug_activity_selection.dart @@ -15,6 +15,7 @@ SwitchListTile buildDrugActivitySelection({ required bool isActive, required bool disabled, EdgeInsetsGeometry? contentPadding, + bool warnIfInhibitor = true, }) => SwitchListTile.adaptive( key: key, value: isActive, @@ -23,7 +24,7 @@ SwitchListTile buildDrugActivitySelection({ 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(); } From e73bd73f698f6296df56eb9a40475f1c6c31dd7c Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 14:14:30 +0100 Subject: [PATCH 08/17] feat(app): improve wording --- app/lib/l10n/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 445c25ee..44a7d724 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -51,7 +51,7 @@ "drug_selection_header": "Current medications", "@drug_selection_header": {}, - "drug_selection_continue_warning": "If you continue, you will complete the initial setup and will not be able to come back to this page. You can always change the status for a medication later in the app.", + "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": {}, From 3051680816239d08bfcf21800a3224e7a716d93f Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 14:15:33 +0100 Subject: [PATCH 09/17] feat(#708): hide keyboard on screen interaction --- app/lib/common/widgets/page_scaffold.dart | 22 ++++++++++++------- app/lib/common/widgets/scroll_list.dart | 1 + .../drug_selection/pages/drug_selection.dart | 1 + app/lib/search/pages/search.dart | 1 + 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/lib/common/widgets/page_scaffold.dart b/app/lib/common/widgets/page_scaffold.dart index ff30215f..a4aa1443 100644 --- a/app/lib/common/widgets/page_scaffold.dart +++ b/app/lib/common/widgets/page_scaffold.dart @@ -62,12 +62,13 @@ Scaffold pageScaffold({ ); } -Scaffold unscrollablePageScaffold({ +Widget unscrollablePageScaffold({ required Widget body, String? title, String? titleTooltip, List? actions, bool canNavigateBack = true, + BuildContext? contextToDismissFocusOnTap, Key? key, }) { final appBar = title == null @@ -84,13 +85,18 @@ 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: () => contextToDismissFocusOnTap != null + ? FocusScope.of(contextToDismissFocusOnTap).unfocus() + : null, + child: Scaffold( + key: key, + appBar: appBar, + 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 ac0a997e..be91d793 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -32,6 +32,7 @@ class DrugSelectionPage extends HookWidget { return unscrollablePageScaffold( title: context.l10n.drug_selection_header, canNavigateBack: !concludesOnboarding, + contextToDismissFocusOnTap: context, body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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, From a069345fb48715914ea07660d85cb6798bfd8b0c Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 14:31:53 +0100 Subject: [PATCH 10/17] feat(#708): remove focus on route change --- app/lib/app.dart | 15 ++++++++++++++- app/lib/common/widgets/page_scaffold.dart | 9 ++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/lib/app.dart b/app/lib/app.dart index 6c59547e..ed745287 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -22,7 +22,10 @@ class PharMeApp extends StatelessWidget { child: MaterialApp.router( debugShowCheckedModeBanner: false, routeInformationParser: _appRouter.defaultRouteParser(), - routerDelegate: _appRouter.delegate(deepLinkBuilder: getInitialRoute), + routerDelegate: _appRouter.delegate( + deepLinkBuilder: getInitialRoute, + navigatorObservers: () => [RemoveFocusOnNavigate()], + ), theme: PharMeTheme.light, localizationsDelegates: [ AppLocalizations.delegate, @@ -36,3 +39,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/page_scaffold.dart b/app/lib/common/widgets/page_scaffold.dart index a4aa1443..9c4554af 100644 --- a/app/lib/common/widgets/page_scaffold.dart +++ b/app/lib/common/widgets/page_scaffold.dart @@ -62,6 +62,11 @@ Scaffold pageScaffold({ ); } +void _maybeRemoveFocus(BuildContext? contextToDismissFocusOnTap) => + contextToDismissFocusOnTap != null + ? FocusScope.of(contextToDismissFocusOnTap).unfocus() + : null; + Widget unscrollablePageScaffold({ required Widget body, String? title, @@ -86,9 +91,7 @@ Widget unscrollablePageScaffold({ titleSpacing: _getTitleSpacing(backButtonPresent: canNavigateBack), ); return GestureDetector( - onTap: () => contextToDismissFocusOnTap != null - ? FocusScope.of(contextToDismissFocusOnTap).unfocus() - : null, + onTap: () => _maybeRemoveFocus(contextToDismissFocusOnTap), child: Scaffold( key: key, appBar: appBar, From f6dd0b1167720a60ccd8275555deec05ebcf94f1 Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 14:48:54 +0100 Subject: [PATCH 11/17] feat(#708): do not resize screen when keyboard opens --- app/lib/common/widgets/page_scaffold.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/lib/common/widgets/page_scaffold.dart b/app/lib/common/widgets/page_scaffold.dart index 9c4554af..92451a5f 100644 --- a/app/lib/common/widgets/page_scaffold.dart +++ b/app/lib/common/widgets/page_scaffold.dart @@ -38,6 +38,7 @@ Scaffold pageScaffold({ }) { return Scaffold( key: key, + resizeToAvoidBottomInset: false, body: CustomScrollView(slivers: [ SliverAppBar( scrolledUnderElevation: 0, @@ -95,6 +96,7 @@ Widget unscrollablePageScaffold({ child: Scaffold( key: key, appBar: appBar, + resizeToAvoidBottomInset: false, body: SafeArea( child: Padding( padding: pagePadding(), From c59862574589906f17e2b50e8076c431bd6b0243 Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 16:21:41 +0100 Subject: [PATCH 12/17] feat(app): add title to confirm first medication selection dialog --- app/lib/drug_selection/pages/drug_selection.dart | 1 + app/lib/l10n/app_en.arb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/lib/drug_selection/pages/drug_selection.dart b/app/lib/drug_selection/pages/drug_selection.dart index be91d793..a566ea4f 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -69,6 +69,7 @@ class DrugSelectionPage extends HookWidget { 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( diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 44a7d724..9f4155df 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -51,6 +51,8 @@ "drug_selection_header": "Current medications", "@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.", From 6633964ca1ea0ade32fa45cd5a9b3a03aa970973 Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 16:22:12 +0100 Subject: [PATCH 13/17] refactor(app): pass whether to remove focus and to resize content to both scaffold types --- app/lib/common/widgets/page_scaffold.dart | 58 +++++++++++++---------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/app/lib/common/widgets/page_scaffold.dart b/app/lib/common/widgets/page_scaffold.dart index 92451a5f..629ebef9 100644 --- a/app/lib/common/widgets/page_scaffold.dart +++ b/app/lib/common/widgets/page_scaffold.dart @@ -29,37 +29,42 @@ 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, - resizeToAvoidBottomInset: false, - 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)), + ), + ]), + ), ); } @@ -75,6 +80,7 @@ Widget unscrollablePageScaffold({ List? actions, bool canNavigateBack = true, BuildContext? contextToDismissFocusOnTap, + bool resizeToAvoidBottomInset = false, Key? key, }) { final appBar = title == null @@ -96,7 +102,7 @@ Widget unscrollablePageScaffold({ child: Scaffold( key: key, appBar: appBar, - resizeToAvoidBottomInset: false, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, body: SafeArea( child: Padding( padding: pagePadding(), From 6f3bcae5ba50211e99bc13a0daa5736db29909a9 Mon Sep 17 00:00:00 2001 From: tamslo Date: Fri, 15 Nov 2024 17:07:26 +0100 Subject: [PATCH 14/17] feat: add dummy demo lab --- app/lib/login/models/dummy_demo_lab.dart | 16 ++++++++++++++++ app/lib/login/models/lab.dart | 2 +- .../oauth_authorization_code_flow_lab.dart | 2 +- app/lib/login/pages/login.dart | 10 +++++----- 4 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 app/lib/login/models/dummy_demo_lab.dart diff --git a/app/lib/login/models/dummy_demo_lab.dart b/app/lib/login/models/dummy_demo_lab.dart new file mode 100644 index 00000000..3eddfc26 --- /dev/null +++ b/app/lib/login/models/dummy_demo_lab.dart @@ -0,0 +1,16 @@ +import '../../common/module.dart'; +import 'lab.dart'; + +class DummyDemoLab extends Lab { + DummyDemoLab({ + required super.name, + required this.dataUrl, + }); + + Uri dataUrl; + + @override + Future<(List, List)> loadData() async { + return Lab.fetchData(dataUrl); + } +} \ No newline at end of file diff --git a/app/lib/login/models/lab.dart b/app/lib/login/models/lab.dart index 26dd20ab..1d20b120 100644 --- a/app/lib/login/models/lab.dart +++ b/app/lib/login/models/lab.dart @@ -24,7 +24,7 @@ class Lab { throw UnimplementedError(); } - Future<(List, List)> fetchData( + static Future<(List, List)> fetchData( Uri dataUrl, { Map? headers, diff --git a/app/lib/login/models/oauth_authorization_code_flow_lab.dart b/app/lib/login/models/oauth_authorization_code_flow_lab.dart index dc97f373..20673a2f 100644 --- a/app/lib/login/models/oauth_authorization_code_flow_lab.dart +++ b/app/lib/login/models/oauth_authorization_code_flow_lab.dart @@ -56,6 +56,6 @@ class OAuthAuthorizationCodeFlowLab extends Lab { @override Future<(List, List)> loadData() async { - return fetchData(dataUrl, headers: {'Authorization': 'Bearer $token'}); + return Lab.fetchData(dataUrl, headers: {'Authorization': 'Bearer $token'}); } } \ No newline at end of file diff --git a/app/lib/login/pages/login.dart b/app/lib/login/pages/login.dart index 748a5a92..9f606692 100644 --- a/app/lib/login/pages/login.dart +++ b/app/lib/login/pages/login.dart @@ -3,14 +3,14 @@ import 'package:provider/provider.dart'; import '../../../common/module.dart'; import '../cubit.dart'; -import '../models/oauth_authorization_code_flow_lab.dart'; +import '../models/dummy_demo_lab.dart'; final labs = [ - OAuthAuthorizationCodeFlowLab( + DummyDemoLab( name: 'Mount Sinai Health System', - authUrl: Uri.http('vm-slosarek01.dhclab.i.hpi.de:28080', 'realms/pharme/protocol/openid-connect/auth'), - tokenUrl: Uri.http('vm-slosarek01.dhclab.i.hpi.de:28080', 'realms/pharme/protocol/openid-connect/token'), - dataUrl: Uri.http('vm-slosarek01.dhclab.i.hpi.de:8081', 'api/v1/star-alleles'), + dataUrl: Uri.parse( + 'https://hpi-datastore.duckdns.org/userdata?id=66608824-2ab4-4f03-aef0-03aa007337d3', + ), ) ]; From 9f5dcda0ba0fc6bc874ac61d811b3a160ee9bc50 Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 23:22:51 +0100 Subject: [PATCH 15/17] feat(app): add Anni URL for testing --- app/lib/common/constants.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/common/constants.dart b/app/lib/common/constants.dart index be7a46a2..6a61d57a 100644 --- a/app/lib/common/constants.dart +++ b/app/lib/common/constants.dart @@ -1,7 +1,7 @@ import 'package:url_launcher/url_launcher.dart'; Uri anniUrl([String slug = '']) => - Uri.http('vm-slosarek01.dhclab.i.hpi.de:8000', 'api/v1/$slug'); + Uri.http('hpi-annotation-service.duckdns.org', 'api/v1/$slug'); final geneticInformationUrl = Uri.https( 'medlineplus.gov', From 12fa77faa6972561d67535583b46217cf02121b0 Mon Sep 17 00:00:00 2001 From: tamslo Date: Thu, 21 Nov 2024 23:38:18 +0100 Subject: [PATCH 16/17] feat(app): prettier switches on Android --- app/lib/common/widgets/drug_activity_selection.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/lib/common/widgets/drug_activity_selection.dart b/app/lib/common/widgets/drug_activity_selection.dart index a74c35e0..215736c1 100644 --- a/app/lib/common/widgets/drug_activity_selection.dart +++ b/app/lib/common/widgets/drug_activity_selection.dart @@ -20,6 +20,9 @@ SwitchListTile buildDrugActivitySelection({ 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, From f072f24af247f2da046157e7e110192ff7ee041c Mon Sep 17 00:00:00 2001 From: tamslo Date: Fri, 22 Nov 2024 14:40:57 +0100 Subject: [PATCH 17/17] feat(app): improve onboarding scrolling --- app/lib/onboarding/pages/onboarding.dart | 435 ++++++++++++++--------- pharme.code-workspace | 1 + 2 files changed, 266 insertions(+), 170 deletions(-) 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/pharme.code-workspace b/pharme.code-workspace index 6683765e..dea9a466 100644 --- a/pharme.code-workspace +++ b/pharme.code-workspace @@ -60,6 +60,7 @@ "drugclass", "drugid", "drugrecommendation", + "duckdns", "duloxetine", "endoxifen", "Ezallor",