diff --git a/app/CONTRIBUTING.md b/app/CONTRIBUTING.md index 77bc3ac53..ec7413438 100644 --- a/app/CONTRIBUTING.md +++ b/app/CONTRIBUTING.md @@ -118,78 +118,29 @@ flutter pub run flutter_launcher_icons:main This will generate icons for both iOS as well as Android. +Another option is to use, e.g., . + ## Updating screencast and screenshots 🙅 _Not working yet due to login redirect, but keeping script for Sinai_ _version (login without redirect) – can adopt once different login types are_ _supported._ -Scripts were created to click through the app using tests and record the -screen. - -A simulator with the app in its initial state (or not installed) needs to be -running. - -### Screencasts - -The `generate_screendocs/generate_screencast.sh` script will create screencast. -It uses Xcode to record the screencast and [`ffmpeg`](https://ffmpeg.org/) -to cut the `full.mov` to relevant subsets (needs to be installed). - -To generate GIFs used in the Tutorial, -[ImageMagick](https://imagemagick.org/index.php) is used -(which also needs to be installed). - -Run the script with `bash generate_screendocs/generate_screencast.sh`. - -## Screenshots - -To update the screenshots in `../docs/screenshots` -(used in [📑 App screens](../docs/App-screens.md), -[📑 User instructions](../docs/User-instructions.html), and the -[README](./README.md)), run the following command: -`bash generate_screendocs/generate_screenshots.sh`. - -If the error `The following MissingPluginException was thrown running a test: -MissingPluginException(No implementation found for method captureScreenshot on -channel plugins.flutter.io/integration_test)` occurs, the registration in the -file -`ios/.symlinks/plugins/integration_test/ios/Classes/IntegrationTestPlugin.m` -needs to be adapted (see -[issue](https://github.com/flutter/flutter/issues/91668)): - -```m -+ (void)registerWithRegistrar:(NSObject *)registrar { - [[IntegrationTestPlugin instance] setupChannels:registrar.messenger]; -} -``` +_Check `e60efb4f2fc3ba2efa7735ffb06ec5fdb64d7af6` for a rudimentary script_ +_version, removed afterwards due to too many merge conflicts._ ## Adapting test data If you would like to test with specific test data but you don't have a user with -suitable data available, adapt the code in `utilities/genome_data.dart` as -shown below. +suitable data available, adapt the code that gets the lab results as shown below. ```dart // TODO(after-testing): remove test data adaption - UserData.instance.labData = UserData.instance.labData!.filter( - (labResult) => labResult.gene != 'UGT1A1' - ).toList(); - UserData.instance.labData!.add(LabResult( - gene: 'UGT1A1', - variant: '*28/*28', - phenotype: 'Poor Metabolizer', - allelesTested: '', - )); - UserData.instance.labData = UserData.instance.labData!.filter( - (labResult) => labResult.gene != 'HLA-B' && labResult.variant != '*57:01 negative' - ).toList(); - UserData.instance.labData!.add(LabResult( - gene: 'HLA-B', - variant: '*57:01 positive', - phenotype: '*57:01 positive', - allelesTested: '', - )); +var labResults = json.map(LabResult.fromJson).toList(); +final cyp2c19Result = labResults.firstWhere((labResult) => labResult.gene == "CYP2C19"); +labResults = labResults.filter((labResult) => labResult.gene != "CYP2C19").toList(); +labResults = [...labResults, LabResult(gene: "CYP2C19", variant: "*2/*2", phenotype: "Poor Metabolizer", allelesTested: cyp2c19Result.allelesTested)]; +return labResults; ``` You can use the CPIC API to get reasonable genotype-phenotype pairings, e.g., diff --git a/app/README.md b/app/README.md index 0935616b9..fdb3235b6 100644 --- a/app/README.md +++ b/app/README.md @@ -18,7 +18,7 @@ application offers to export a detailed description. PGx report | Gene details | Medication search | Medication details | :-: | :-: | :-: | :-: | (a) | (b) | (c) | (d) | -![report_screen](../docs/screenshots/gene-report.png) | ![gene_screen](../docs/screenshots/cyp2d6.png) | ![search_screen](../docs/screenshots/drug-search.png) | ![drug_screen](../docs/screenshots/clopidogrel.png) | +![report_screen](../docs/screenshots/gene-report-all.png) | ![gene_screen](../docs/screenshots/cyp2c9.png) | ![search_screen](../docs/screenshots/drug-search.png) | ![drug_screen](../docs/screenshots/ibuprofen.png) | _Please note that these screenshots might not represent the latest app version._ diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 12d4c2fca..7d9a2671d 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -4,8 +4,9 @@ etc.) set the parameter 'android:usesCleartextTraffic' to 'true' --> + + + + \ No newline at end of file diff --git a/app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7..f5258d30b 100644 Binary files a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..d84470aa5 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..6401ee004 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-hdpi/launcher_icon.png deleted file mode 100644 index e0430ced1..000000000 Binary files a/app/android/app/src/main/res/mipmap-hdpi/launcher_icon.png and /dev/null differ diff --git a/app/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 000000000..c792ee30b Binary files /dev/null and b/app/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79b..3bd3cc5f2 100644 Binary files a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..c0b2c39d7 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..378f77e13 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-mdpi/launcher_icon.png deleted file mode 100644 index 643514825..000000000 Binary files a/app/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and /dev/null differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d439148..1cadef499 100644 Binary files a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..2c57e74c7 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..178fd4b8b Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png deleted file mode 100644 index 880fc9cea..000000000 Binary files a/app/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png and /dev/null differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34..1c2c528ed 100644 Binary files a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..8e285cdaa Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..2a8dc2d54 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png deleted file mode 100644 index 67f9aa43d..000000000 Binary files a/app/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and /dev/null differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eeb..c6e98b69c 100644 Binary files a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..cdc766276 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..8e781aa7b Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png deleted file mode 100644 index 8ac5de6cb..000000000 Binary files a/app/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png and /dev/null differ diff --git a/app/android/app/src/main/res/playstore-icon.png b/app/android/app/src/main/res/playstore-icon.png new file mode 100644 index 000000000..96d347546 Binary files /dev/null and b/app/android/app/src/main/res/playstore-icon.png differ diff --git a/app/android/app/src/main/res/values-night/styles.xml b/app/android/app/src/main/res/values-night/styles.xml index 5346da69f..449a9f930 100644 --- a/app/android/app/src/main/res/values-night/styles.xml +++ b/app/android/app/src/main/res/values-night/styles.xml @@ -4,7 +4,7 @@ - @drawable/splashscreen + @drawable/launch_background @@ -14,9 +16,11 @@ + + @@ -28,10 +32,10 @@ - + - + diff --git a/app/lib/common/constants.dart b/app/lib/common/constants.dart index 2a79d849d..1c71a0620 100644 --- a/app/lib/common/constants.dart +++ b/app/lib/common/constants.dart @@ -1,5 +1,11 @@ +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'module.dart'; + +const medicationsIcon = FontAwesomeIcons.pills; +const genesIcon = FontAwesomeIcons.dna; + Uri anniUrl([String slug = '']) => Uri.https('hpi-annotation-service.duckdns.org', 'api/v1/$slug'); @@ -28,6 +34,12 @@ enum SpecialLookup { noResult, } +const indeterminateResult = 'Indeterminate'; +List unknownPhenotypes(BuildContext context) => [ + indeterminateResult, + context.l10n.general_not_tested, +]; + extension SpecialLookupValue on SpecialLookup { String get value { final valueMap = { diff --git a/app/lib/common/models/drug/drug.dart b/app/lib/common/models/drug/drug.dart index ab12020f2..932a56766 100644 --- a/app/lib/common/models/drug/drug.dart +++ b/app/lib/common/models/drug/drug.dart @@ -195,16 +195,26 @@ extension CriticalDrugs on List { List getDrugsWithBrandNames( List? drugNames, - { bool capitalize = false } + { + bool capitalize = false, + String? brandNamesPrefix, + } ) { return drugNames?.map( - (drugName) => _getDrugWithBrandNames(drugName, capitalize: capitalize) + (drugName) => _getDrugWithBrandNames( + drugName, + capitalize: capitalize, + brandNamesPrefix: brandNamesPrefix, + ) ).toList() ?? []; } String _getDrugWithBrandNames( String drugName, - { required bool capitalize } + { + bool capitalize = false, + String? brandNamesPrefix, + } ) { final drug = DrugsWithGuidelines.instance.drugs?.firstOrNullWhere( (drug) => drug.name == drugName @@ -213,5 +223,8 @@ String _getDrugWithBrandNames( if (drug == null || drug.annotations.brandNames.isEmpty) { return displayedDrugName; } - return '$displayedDrugName (${drug.annotations.brandNames.join(', ')})'; + final branNamesString = drug.annotations.brandNames.join(', '); + return brandNamesPrefix != null + ? '$displayedDrugName ($brandNamesPrefix: $branNamesString)' + : '$displayedDrugName ($branNamesString)'; } diff --git a/app/lib/common/models/drug/drug_inhibitors.dart b/app/lib/common/models/drug/drug_inhibitors.dart index 6c6a5c47d..e2a833dba 100644 --- a/app/lib/common/models/drug/drug_inhibitors.dart +++ b/app/lib/common/models/drug/drug_inhibitors.dart @@ -5,10 +5,6 @@ // structure: gene symbol -> drug name -> overwriting lookupkey -import 'package:collection/collection.dart'; - -import '../../module.dart'; - // Inhibit phenotype for gene by overwriting with poor metabolizer const Map> strongDrugInhibitors = { 'CYP2D6': { @@ -33,278 +29,7 @@ const Map> moderateDrugInhibitors = { }, }; -// Private helper functions - -final _inhibitableGenes = List.from({ +final inhibitableGenes = List.from({ ...strongDrugInhibitors.keys, ...moderateDrugInhibitors.keys, }); - -final _drugInhibitorsPerGene = { - for (final gene in _inhibitableGenes) gene: [ - ...?strongDrugInhibitors[gene]?.keys, - ...?moderateDrugInhibitors[gene]?.keys, - ] -}; - -List _inhibitorsFor(String gene) { - return _drugInhibitorsPerGene[gene] ?? []; -} - -bool _isInhibitorOfType( - String drugName, - Map> inhibitorDefinition -) { - final influencingDrugs = inhibitorDefinition.keys.flatMap( - (gene) => inhibitorDefinition[gene]!.keys); - return influencingDrugs.contains(drugName); -} - -bool _isModerateInhibitor(String drugName) { - return _isInhibitorOfType(drugName, moderateDrugInhibitors); -} - -class _DisplayConfig { - _DisplayConfig({ - required this.partSeparator, - required this.userSalutation, - required this.userGenitive, - required this.useConsult, - }); - - final String partSeparator; - final String userSalutation; - final String userGenitive; - final bool useConsult; -} - -_DisplayConfig _getDisplayConfig( - BuildContext context, - { required bool userFacing } -) { - final displayConfigs = { - true: _DisplayConfig( - partSeparator: '\n\n', - userSalutation: context.l10n.inhibitor_direct_salutation, - userGenitive: context.l10n.inhibitor_direct_salutation_genitive, - useConsult: true, - ), - false: _DisplayConfig( - partSeparator: ' ', - userSalutation: context.l10n.inhibitor_third_person_salutation, - userGenitive: context.l10n.inhibitor_third_person_salutation_genitive, - useConsult: false, - ), - }; - return displayConfigs[userFacing]!; -} - -String _getPhenoconversionConsequence( - BuildContext context, - GenotypeResult genotypeResult, - { - required String? drug, - required _DisplayConfig displayConfig, - } -) { - final activeInhibitors = _activeInhibitorsFor( - genotypeResult.gene, - drug: drug, - ); - return activeInhibitors.all(_isModerateInhibitor) - ? context.l10n.inhibitors_consequence_not_adapted( - genotypeResult.geneDisplayString, - displayConfig.userGenitive, - ).capitalize() - : context.l10n.inhibitors_consequence_adapted( - genotypeResult.geneDisplayString, - genotypeResult.phenotypeDisplayString(context), - displayConfig.userGenitive, - ).capitalize(); -} - -String _getInhibitorsString( - BuildContext context, - GenotypeResult genotypeResult, - { required String? drug } -) { - return context.l10n.inhibitors_tooltip(enumerationWithAnd( - getDrugsWithBrandNames(_activeInhibitorsFor( - genotypeResult.gene, - drug: drug, - )), - context, - )); -} - -String _inhibitionTooltipText( - BuildContext context, - GenotypeResult genotypeResult, - { - required String? drug, - required _DisplayConfig displayConfig, - } -) { - final inhibitorsString = _getInhibitorsString( - context, - genotypeResult, - drug: drug, - ); - final consequence = _getPhenoconversionConsequence( - context, - genotypeResult, - drug: drug, - displayConfig: displayConfig, - ); - return '$consequence${ - displayConfig.useConsult ? ' ${context.l10n.consult_text}' : '' - }${displayConfig.partSeparator}$inhibitorsString'; -} - -Widget _drugInteractionTemplate( - BuildContext context, - String tooltipText, - _DisplayConfig displayConfig, -) { - return buildTable([ - TableRowDefinition( - drugInteractionIndicator, - context.l10n.inhibitor_message( - displayConfig.userSalutation, - displayConfig.userGenitive, - ), - tooltip: tooltipText, - )], - boldHeader: false, - ); -} - -List _activeInhibitorsFor(String gene, { required String? drug }) { - return UserData.instance.activeDrugNames == null - ? [] - : UserData.instance.activeDrugNames!.filter( - (activeDrug) => - _inhibitorsFor(gene).contains(activeDrug) && - activeDrug != drug - ).toList(); -} - -// Public helper functions - -bool isInhibitor(String drugName) { - var drugIsInhibitor = false; - for (final gene in _drugInhibitorsPerGene.keys) { - final influencingDrugs = _drugInhibitorsPerGene[gene]; - // WARNING: this does not work for non-unique genes, such as HLA-B - final originalLookup = UserData.lookupFor( - gene, - drug: drugName, - useOverwrite: false, - ); - if (influencingDrugs!.contains(drugName) && originalLookup != '0.0') { - drugIsInhibitor = true; - break; - } - } - return drugIsInhibitor; -} - -bool isInhibited( - GenotypeResult genotypeResult, - { required String? drug } -) { - final activeInhibitors = _activeInhibitorsFor( - genotypeResult.gene, - drug: drug, - ); - final originalPhenotype = genotypeResult.phenotype; - final phenotypeCanBeInhibited = - originalPhenotype?.toLowerCase() != overwritePhenotype.toLowerCase(); - return activeInhibitors.isNotEmpty && phenotypeCanBeInhibited; -} - -List inhibitedGenes(Drug drug) { - return _drugInhibitorsPerGene.keys.filter( - (gene) => _drugInhibitorsPerGene[gene]!.contains(drug.name) - ).toList(); -} - -MapEntry? getOverwrittenLookup ( - String gene, - { required String? drug } -) { - final inhibitors = strongDrugInhibitors[gene]; - if (inhibitors == null) return null; - final lookup = inhibitors.entries.firstWhereOrNull((entry) { - final isActiveInhibitor = - UserData.instance.activeDrugNames?.contains(entry.key) ?? false; - final wouldInhibitItself = drug == entry.key; - return isActiveInhibitor && !wouldInhibitItself; - }); - if (lookup == null) return null; - return lookup; -} - -String possiblyAdaptedPhenotype( - BuildContext context, - GenotypeResult genotypeResult, - { required String? drug } -) { - final originalPhenotype = genotypeResult.phenotypeDisplayString(context); - if (!isInhibited(genotypeResult, drug: drug)) { - return originalPhenotype; - } - final overwrittenLookup = getOverwrittenLookup( - genotypeResult.gene, - drug: drug, - ); - if (overwrittenLookup == null) { - return '$originalPhenotype$drugInteractionIndicator'; - } - return '$overwritePhenotype$drugInteractionIndicator'; -} - -String inhibitionTooltipText( - BuildContext context, - List genotypeResults, - { - required String? drug, - required bool userFacing, - } -) { - final displayConfig = _getDisplayConfig(context, userFacing: userFacing); - final inhibitedGenotypeResults = genotypeResults.filter( - (genotypeResult) => isInhibited(genotypeResult, drug: drug) - ).toList(); - var tooltipText = ''; - for (final (index, genotypeResult) in inhibitedGenotypeResults.indexed) { - final separator = index == 0 ? '' : displayConfig.partSeparator; - // ignore: use_string_buffers - tooltipText = '$tooltipText$separator${ - _inhibitionTooltipText( - context, - genotypeResult, - drug: drug, - displayConfig: displayConfig, - ) - }'; - } - return tooltipText; -} - -Widget buildDrugInteractionInfo( - BuildContext context, - List genotypeResults, - { required String? drug } -) { - return _drugInteractionTemplate( - context, - inhibitionTooltipText( - context, - genotypeResults, - drug: drug, - userFacing: true, - ), - _getDisplayConfig(context, userFacing: true), - ); -} diff --git a/app/lib/common/theme.dart b/app/lib/common/theme.dart index 45d20cad9..65de5f25b 100644 --- a/app/lib/common/theme.dart +++ b/app/lib/common/theme.dart @@ -84,6 +84,8 @@ class PharMeTheme { static const errorColor = Color(0xccf52a2a); static final borderColor = Colors.black.withOpacity(.2); static final iconColor = darkenColor(PharMeTheme.onSurfaceText, -0.1); + static final buttonColor = darkenColor(PharMeTheme.iconColor, -0.1); + static final subheaderColor = Colors.grey[600]; static const smallSpace = 8.0; static const defaultPagePadding = smallSpace; diff --git a/app/lib/common/utilities/drug_inhibitors.dart b/app/lib/common/utilities/drug_inhibitors.dart new file mode 100644 index 000000000..af73c8d6c --- /dev/null +++ b/app/lib/common/utilities/drug_inhibitors.dart @@ -0,0 +1,110 @@ +import 'package:collection/collection.dart'; + +import '../module.dart'; + +final _drugInhibitorsPerGene = { + for (final gene in inhibitableGenes) gene: [ + ...?strongDrugInhibitors[gene]?.keys, + ...?moderateDrugInhibitors[gene]?.keys, + ] +}; + +List _inhibitorsFor(String gene) { + return _drugInhibitorsPerGene[gene] ?? []; +} + +bool _isInhibitorOfType( + String drugName, + Map> inhibitorDefinition +) { + final influencingDrugs = inhibitorDefinition.keys.flatMap( + (gene) => inhibitorDefinition[gene]!.keys); + return influencingDrugs.contains(drugName); +} + +bool isModerateInhibitor(String drugName) { + return _isInhibitorOfType(drugName, moderateDrugInhibitors); +} + +List activeInhibitorsFor(String gene, { required String? drug }) { + return UserData.instance.activeDrugNames == null + ? [] + : UserData.instance.activeDrugNames!.filter( + (activeDrug) => + _inhibitorsFor(gene).contains(activeDrug) && + activeDrug != drug + ).toList(); +} + +bool isInhibitor(String drugName) { + var drugIsInhibitor = false; + for (final gene in _drugInhibitorsPerGene.keys) { + final influencingDrugs = _drugInhibitorsPerGene[gene]; + // WARNING: this does not work for non-unique genes, such as HLA-B + final originalLookup = UserData.lookupFor( + gene, + drug: drugName, + useOverwrite: false, + ); + if (influencingDrugs!.contains(drugName) && originalLookup != '0.0') { + drugIsInhibitor = true; + break; + } + } + return drugIsInhibitor; +} + +bool isInhibited( + GenotypeResult genotypeResult, + { required String? drug } +) { + final activeInhibitors = activeInhibitorsFor( + genotypeResult.gene, + drug: drug, + ); + final originalPhenotype = genotypeResult.phenotype; + final phenotypeCanBeInhibited = + originalPhenotype?.toLowerCase() != overwritePhenotype.toLowerCase(); + return activeInhibitors.isNotEmpty && phenotypeCanBeInhibited; +} + +List inhibitedGenes(Drug drug) { + return _drugInhibitorsPerGene.keys.filter( + (gene) => _drugInhibitorsPerGene[gene]!.contains(drug.name) + ).toList(); +} + +MapEntry? getOverwrittenLookup ( + String gene, + { required String? drug } +) { + final inhibitors = strongDrugInhibitors[gene]; + if (inhibitors == null) return null; + final lookup = inhibitors.entries.firstWhereOrNull((entry) { + final isActiveInhibitor = + UserData.instance.activeDrugNames?.contains(entry.key) ?? false; + final wouldInhibitItself = drug == entry.key; + return isActiveInhibitor && !wouldInhibitItself; + }); + if (lookup == null) return null; + return lookup; +} + +String possiblyAdaptedPhenotype( + BuildContext context, + GenotypeResult genotypeResult, + { required String? drug } +) { + final originalPhenotype = genotypeResult.phenotypeDisplayString(context); + if (!isInhibited(genotypeResult, drug: drug)) { + return originalPhenotype; + } + final overwrittenLookup = getOverwrittenLookup( + genotypeResult.gene, + drug: drug, + ); + if (overwrittenLookup == null) { + return '$originalPhenotype$drugInteractionIndicator'; + } + return '$overwritePhenotype$drugInteractionIndicator'; +} diff --git a/app/lib/common/utilities/drug_utils.dart b/app/lib/common/utilities/drug_utils.dart index e517193ac..3c4e1ab64 100644 --- a/app/lib/common/utilities/drug_utils.dart +++ b/app/lib/common/utilities/drug_utils.dart @@ -4,6 +4,27 @@ import 'package:http/http.dart'; import '../../app.dart'; import '../module.dart'; +List getInhibitedGenotypesForDrug(Drug drug) { + final genotypeResults = getGenotypeResultsForDrug(drug); + return genotypeResults?.filter( + (genotypeResult) => isInhibited(genotypeResult, drug: drug.name) + ).toList() ?? []; +} + +List? getGenotypeResultsForDrug(Drug drug) { + if (drug.userGuideline == null && drug.guidelines.isEmpty) { + return null; + } + return drug.guidelineGenotypes.map((genotypeKey) => + // Should not be null but to be safe + UserData.instance.genotypeResults?[genotypeKey] ?? + GenotypeResult.missingResult( + GenotypeKey.extractGene(genotypeKey), + variant: GenotypeKey.maybeExtractVariant(genotypeKey), + ) + ).toList(); +} + Future maybeUpdateDrugsWithGuidelines() async { final isOnline = await hasConnectionTo(anniUrl().host); if (!isOnline && DrugsWithGuidelines.instance.version == null) { diff --git a/app/lib/common/utilities/hive_utils.dart b/app/lib/common/utilities/hive_utils.dart index 9abc3a5f8..f3a5d8109 100644 --- a/app/lib/common/utilities/hive_utils.dart +++ b/app/lib/common/utilities/hive_utils.dart @@ -1,13 +1,17 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../module.dart'; Future> retrieveExistingOrGenerateKey() async { const secureStorage = FlutterSecureStorage(); // if key not exists return null - final encryprionKey = await secureStorage.read(key: 'key'); - if (encryprionKey == null) { + final encryptionKey = await secureStorage.read(key: 'key'); + if (encryptionKey == null) { final key = Hive.generateSecureKey(); await secureStorage.write( key: 'key', @@ -17,3 +21,35 @@ Future> retrieveExistingOrGenerateKey() async { final key = await secureStorage.read(key: 'key'); return base64Url.decode(key!); } + +Future unsetAllData() async { + await UserData.erase(); + await MetaData.erase(); + await DrugsWithGuidelines.erase(); +} + +Future deleteAllAppData() async { + await unsetAllData(); + await _deleteCacheDir(); + await _deleteAppDir(); +} + +// The folders themselves cannot be deleted on iOS, therefore delete all content +// inside the folders +void _deleteFolderContent(Directory directory) { + if (!directory.existsSync()) return; + for (final item in directory.listSync()) { + item.deleteSync(recursive: true); + } +} + +Future _deleteCacheDir() async { + final tempDir = await getTemporaryDirectory(); + _deleteFolderContent(tempDir); +} + +Future _deleteAppDir() async { + final appDocDir = await getApplicationDocumentsDirectory(); + _deleteFolderContent(appDocDir); +} + diff --git a/app/lib/common/utilities/module.dart b/app/lib/common/utilities/module.dart index a076f0a28..2055a3ca6 100644 --- a/app/lib/common/utilities/module.dart +++ b/app/lib/common/utilities/module.dart @@ -1,5 +1,6 @@ export 'color_utils.dart'; export 'contact.dart'; +export 'drug_inhibitors.dart'; export 'drug_utils.dart'; export 'genome_data.dart'; export 'material_colors.dart'; diff --git a/app/lib/common/utilities/pdf_utils.dart b/app/lib/common/utilities/pdf_utils.dart index 74745d918..9f5c5e8cf 100644 --- a/app/lib/common/utilities/pdf_utils.dart +++ b/app/lib/common/utilities/pdf_utils.dart @@ -141,16 +141,10 @@ String? _getPhenotypeInfo(String genotypeKey, Drug drug, BuildContext context) { } String? _getPhenoconversionInfo(Drug drug, BuildContext context) { - if (drug.guidelines.isEmpty) return null; - final genotypeResults = drug.guidelineGenotypes.map((genotypeKey) => - UserData.instance.genotypeResults![genotypeKey]! - ).toList(); - return '$drugInteractionIndicator ${inhibitionTooltipText( - context, - genotypeResults, - drug: drug.name, - userFacing: false, - )}'; + final phenoconversionExplanation = + getExpertPhenoconversionExplanation(drug, context); + if (phenoconversionExplanation == null) return null; + return '$drugInteractionIndicator $phenoconversionExplanation'; } String? _getActivityScoreInfo( diff --git a/app/lib/common/widgets/annotation_table.dart b/app/lib/common/widgets/annotation_table.dart index b225c29c3..ef21b54f1 100644 --- a/app/lib/common/widgets/annotation_table.dart +++ b/app/lib/common/widgets/annotation_table.dart @@ -2,17 +2,28 @@ import '../../drug/widgets/tooltip_icon.dart'; import '../module.dart'; class TableRowDefinition { - const TableRowDefinition(this.key, this.value, { this.tooltip }); + const TableRowDefinition( + this.key, + this.value, + { + this.keyTooltip, + this.valueTooltip, + this.italicValue = false, + } + ); + final String key; final String value; - final String? tooltip; + final String? keyTooltip; + final String? valueTooltip; + final bool italicValue; } Widget buildTable( List rowDefinitions, { TextStyle? style, - bool boldHeader = true, + bool boldKey = true, } ) { return Column( @@ -22,12 +33,10 @@ Widget buildTable( defaultColumnWidth: IntrinsicColumnWidth(), children: [ _buildRow( - rowDefinition.key, - rowDefinition.value, + rowDefinition, style ?? PharMeTheme.textTheme.bodyMedium!, - boldHeader: boldHeader, + boldKey: boldKey, isLast: index == rowDefinitions.length - 1, - tooltip: rowDefinition.tooltip, ), ], ), @@ -36,17 +45,13 @@ Widget buildTable( } TableRow _buildRow( - String key, - String value, + TableRowDefinition rowDefinition, TextStyle textStyle, { - required bool boldHeader, + required bool boldKey, required bool isLast, - String? tooltip, } ) { - const tooltipSize = 16.0; - return TableRow( children: [ Padding( @@ -54,28 +59,87 @@ TableRow _buildRow( right: PharMeTheme.smallSpace, bottom: isLast ? 0 : PharMeTheme.smallSpace, ), - child: Text( - key, - style: boldHeader - ? textStyle.copyWith(fontWeight: FontWeight.bold) - : textStyle, + child: Text.rich( + TextSpan( + children: [ + TextSpan(text: rowDefinition.key), + ..._maybeBuildTooltip(rowDefinition.keyTooltip), + ], + style: boldKey + ? textStyle.copyWith(fontWeight: FontWeight.bold) + : textStyle, + ), ), ), Text.rich( TextSpan( children: [ - TextSpan(text: value), - if (tooltip.isNotNullOrBlank) ...[ - WidgetSpan(child: SizedBox(width: PharMeTheme.smallSpace)), - WidgetSpan( - child: TooltipIcon(tooltip!, size: tooltipSize), - ), - ], - WidgetSpan(child: SizedBox(height: tooltipSize)), + TextSpan( + text: rowDefinition.value, + style: rowDefinition.italicValue + ? textStyle.copyWith(fontStyle: FontStyle.italic) + : textStyle, + ), + ..._maybeBuildTooltip(rowDefinition.valueTooltip), ], style: textStyle, ), ), ], ); +} + +List _maybeBuildTooltip(String? tooltip) { + const tooltipSize = 16.0; + return tooltip.isNotNullOrBlank + ? [ + WidgetSpan(child: SizedBox(width: PharMeTheme.smallSpace)), + WidgetSpan( + child: TooltipIcon(tooltip!, size: tooltipSize), + ), + WidgetSpan(child: SizedBox(height: tooltipSize)), + ] + : []; +} + +bool testResultIsUnknown(BuildContext context, String phenotype) => + unknownPhenotypes(context).contains(phenotype); + +TableRowDefinition testResultTableRow( + BuildContext context, + { + required GenotypeResult genotypeResult, + required String key, + required String value, + String? keyTooltip, + } +) => TableRowDefinition( + key, + value, + keyTooltip: keyTooltip, + valueTooltip: value == indeterminateResult + ? context.l10n.indeterminate_result_tooltip( + genotypeResult.geneDisplayString, + ) + : null, + italicValue: testResultIsUnknown(context, value), +); + +TableRowDefinition phenotypeTableRow( + BuildContext context, + { + required String key, + required GenotypeResult genotypeResult, + required String? drug, + String? keyTooltip, + } +) { + final value = possiblyAdaptedPhenotype(context, genotypeResult, drug: drug); + return testResultTableRow( + context, + genotypeResult: genotypeResult, + key: key, + value: value, + keyTooltip: keyTooltip, + ); } \ No newline at end of file diff --git a/app/lib/common/widgets/disclaimer_row.dart b/app/lib/common/widgets/disclaimer_row.dart new file mode 100644 index 000000000..534e4e370 --- /dev/null +++ b/app/lib/common/widgets/disclaimer_row.dart @@ -0,0 +1,24 @@ +import '../module.dart'; + +class DisclaimerRow extends StatelessWidget { + const DisclaimerRow({super.key, required this.icon, required this.text}); + + final Widget icon; + final Widget text; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + right: PharMeTheme.smallSpace, + ), + child: icon, + ), + Expanded(child: text), + ], + ); + } +} \ No newline at end of file diff --git a/app/lib/common/widgets/drug_activity_selection.dart b/app/lib/common/widgets/drug_activity_selection.dart index 215736c15..4dc353c54 100644 --- a/app/lib/common/widgets/drug_activity_selection.dart +++ b/app/lib/common/widgets/drug_activity_selection.dart @@ -10,6 +10,7 @@ SwitchListTile buildDrugActivitySelection({ required BuildContext context, required Drug drug, required String title, + TextStyle? titleStyle, String? subtitle, required SetDrugActivityFunction setActivity, required bool isActive, @@ -23,7 +24,7 @@ SwitchListTile buildDrugActivitySelection({ inactiveThumbColor: PharMeTheme.surfaceColor, inactiveTrackColor: PharMeTheme.borderColor, trackOutlineColor: WidgetStatePropertyAll(Colors.transparent), - title: Text(title), + title: Text(title, style: titleStyle), subtitle: subtitle.isNotNullOrBlank ? Text(subtitle!, style: PharMeTheme.textTheme.bodyMedium): null, contentPadding: contentPadding, onChanged: disabled ? null : (newValue) { @@ -33,7 +34,10 @@ SwitchListTile buildDrugActivitySelection({ builder: (context) => DialogWrapper( title: context.l10n.drugs_page_active_warn_header, content: DialogContentText( - context.l10n.drugs_page_active_warn, + context.l10n.drugs_page_active_warn( + drug.name.capitalize(), + enumerationWithAnd(inhibitedGenes(drug), context), + ), ), actions: [ DialogAction( diff --git a/app/lib/common/widgets/drug_list/builder.dart b/app/lib/common/widgets/drug_list/builder.dart index 82c4e6be9..51c8536a1 100644 --- a/app/lib/common/widgets/drug_list/builder.dart +++ b/app/lib/common/widgets/drug_list/builder.dart @@ -9,7 +9,7 @@ typedef DrugItemBuilder = List Function( } ); -class DrugList extends StatelessWidget { +class DrugList extends HookWidget { const DrugList({ super.key, required this.state, @@ -17,9 +17,10 @@ class DrugList extends StatelessWidget { this.noDrugsMessage, this.buildDrugItems = buildDrugCards, this.showDrugInteractionIndicator = false, + this.initiallyExpandFurtherMedications = false, this.searchForDrugClass = true, this.drugActivityChangeable = false, - this.buildContainer, + required this.buildContainer, }); final DrugListState state; @@ -27,17 +28,24 @@ class DrugList extends StatelessWidget { final String? noDrugsMessage; final DrugItemBuilder buildDrugItems; final bool showDrugInteractionIndicator; + final bool initiallyExpandFurtherMedications; final bool searchForDrugClass; // if drugActivityChangeable, active medications are not filtered and repeated // in the "All medications" list to make searching and toggling a medication's // activity less confusing final bool drugActivityChangeable; - final Widget Function(List children)? buildContainer; + final Widget Function({ + List? children, + Widget? indicator, + Widget? noDrugsMessage, + bool? showInactiveDrugs + }) buildContainer; Widget _buildDrugList( BuildContext context, List drugs, FilterState filter, + ValueNotifier otherDrugsExpanded, ) { final filteredDrugs = filter.filter( drugs, @@ -45,10 +53,20 @@ class DrugList extends StatelessWidget { searchForDrugClass: searchForDrugClass, ).sortedBy((drug) => drug.name); if (filteredDrugs.isEmpty && noDrugsMessage != null) { - return errorIndicator(noDrugsMessage!); + return buildContainer( + noDrugsMessage: Column( + children: [ + _buildIncludedMedicationsDescription( + context, + addRightPadding: PharMeTheme.mediumSpace, + ), + errorIndicator(noDrugsMessage!), + ], + ), + ); } List? activeDrugsList; - // Do not show repeated active drugs when searching + // Do not show repeated active drugs when searching in medication selection if (drugActivityChangeable && filteredDrugs.length != drugs.length) { activeDrugsList = null; } else { @@ -66,7 +84,7 @@ class DrugList extends StatelessWidget { final otherDrugs = drugActivityChangeable ? filteredDrugs : filteredDrugs.filter((drug) => !drug.isActive).toList(); - final otherDrugsHeader = drugActivityChangeable + final otherDrugsHeaderText = drugActivityChangeable ? context.l10n.drug_list_subheader_all_drugs : context.l10n.drug_list_subheader_other_drugs; final allDrugsList = buildDrugItems( @@ -75,34 +93,139 @@ class DrugList extends StatelessWidget { showDrugInteractionIndicator: showDrugInteractionIndicator, keyPrefix: 'other-', ); + final otherDrugsHeader = ListDescription( + key: Key('header-other'), + textParts: [boldListDescriptionText(otherDrugsHeaderText)], + detailsText: allDrugsList.length.toString(), + ); + final currentlyExpanded = otherDrugsExpanded.value ?? false; + final currentlyEnabled = !drugActivityChangeable && filter.query.isBlank; final drugLists = [ + _buildIncludedMedicationsDescription(context), if (activeDrugsList != null) ...[ - SubheaderDivider( - text: context.l10n.drug_list_subheader_active_drugs, + ListDescription( key: Key('header-active'), - useLine: false, + textParts: [ + boldListDescriptionText( + context.l10n.drug_list_subheader_active_drugs, + color: PharMeTheme.primaryColor, + ), + ], + detailsText: activeDrugsList.length.toString(), ), ...activeDrugsList, ], - if (activeDrugsList != null && allDrugsList.isNotEmpty) SubheaderDivider( - text: otherDrugsHeader, - key: Key('header-other'), - useLine: false, - ), - ...allDrugsList, + if (activeDrugsList != null && allDrugsList.isNotEmpty) + ...[ + PrettyExpansionTile( + title: otherDrugsHeader, + enabled: currentlyEnabled, + initiallyExpanded: currentlyExpanded || !currentlyEnabled, + onExpansionChanged: (value) => otherDrugsExpanded.value = value, + titlePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + icon: drugActivityChangeable + ? SizedBox.shrink() + : ResizedIconButton( + size: PharMeTheme.largeSpace, + disabledBackgroundColor: currentlyEnabled + ? PharMeTheme.buttonColor + : PharMeTheme.onSurfaceColor, + iconWidgetBuilder: (size) => Icon( + currentlyExpanded + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + size: size, + color: PharMeTheme.surfaceColor, + ), + ), + children: allDrugsList, + ), + ], + if (activeDrugsList == null) ...allDrugsList, ]; - return (buildContainer != null) - ? buildContainer!(drugLists) - : Column(crossAxisAlignment: CrossAxisAlignment.start, children: drugLists); + final indicator = _maybeBuildDrugListIndicator( + context: context, + drugs: drugs, + filter: filter, + activeDrugs: activeDrugs, + otherDrugsExpanded: currentlyExpanded, + currentlyEnabled: currentlyEnabled, + ); + return buildContainer( + children: drugLists, + indicator: indicator, + showInactiveDrugs: !currentlyEnabled || currentlyExpanded, + ); } + Widget _buildIncludedMedicationsDescription( + BuildContext context, + { double addRightPadding = 0.0 } + ) => + Padding( + key: Key('inclusion-description'), + padding: EdgeInsets.only( + left: PharMeTheme.smallSpace, + right: PharMeTheme.smallSpace + addRightPadding, + top: PharMeTheme.mediumSpace, + ), + child: ListInclusionDescription.forMedications(), + ); + @override Widget build(BuildContext context) { + final otherDrugsExpanded = useState(null); return state.when( initial: SizedBox.shrink, error: () => errorIndicator(context.l10n.err_generic), - loaded: (allDrugs, filter) => _buildDrugList(context, allDrugs, filter), + loaded: (allDrugs, filter) { + otherDrugsExpanded.value ??= + drugActivityChangeable || initiallyExpandFurtherMedications; + return _buildDrugList(context, allDrugs, filter, otherDrugsExpanded); + }, loading: loadingIndicator, ); } + + Widget _maybeBuildDrugListIndicator({ + required BuildContext context, + required List drugs, + required FilterState filter, + required ActiveDrugs activeDrugs, + required bool otherDrugsExpanded, + required bool currentlyEnabled, + }) { + var indicatorText = ''; + if (currentlyEnabled && !otherDrugsExpanded) { + final listHelperText = context.l10n.show_all_dropdown_text( + context.l10n.drugs_show_all_dropdown_item, + context.l10n.medications_dropdown_position, + context.l10n.drugs_show_all_dropdown_items, + ); + indicatorText = listHelperText; + } + if (showDrugInteractionIndicator) { + final filteredDrugs = filter.filter( + drugs, + activeDrugs, + searchForDrugClass: searchForDrugClass, + ); + if (filteredDrugs.any((drug) => isInhibitor(drug.name))) { + final inhibitorText = context.l10n.search_page_indicator_explanation( + drugInteractionIndicatorName, + drugInteractionIndicator + ); + if (indicatorText.isNotBlank) { + indicatorText = '$indicatorText\n\n$inhibitorText'; + } else { + indicatorText = inhibitorText; + } + } + } + if (indicatorText.isNotBlank) { + return PageIndicatorExplanation(indicatorText); + } + return SizedBox.shrink(); + } } diff --git a/app/lib/common/widgets/drug_search/builder.dart b/app/lib/common/widgets/drug_search/builder.dart index 4f0bbac84..95d652c42 100644 --- a/app/lib/common/widgets/drug_search/builder.dart +++ b/app/lib/common/widgets/drug_search/builder.dart @@ -31,43 +31,54 @@ class DrugSearch extends HookWidget { @override Widget build(BuildContext context) { final searchController = useTextEditingController(); - return Column( - children: [ - Padding( - padding: EdgeInsets.only( - left: PharMeTheme.smallSpace, - right: PharMeTheme.smallSpace, - bottom: PharMeTheme.smallSpace, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: _buildSearchBarItems(context, searchController), - ), - ), - DrugList( - state: state, - activeDrugs: activeDrugs, - buildDrugItems: buildDrugItems, - showDrugInteractionIndicator: showDrugInteractionIndicator, - noDrugsMessage: context.l10n.search_no_drugs( - showFilter - ? context.l10n.search_no_drugs_with_filter_amendment - : '' - ), - searchForDrugClass: searchForDrugClass, - buildContainer: - (children) => scrollList(keepPosition: keepPosition, children), - drugActivityChangeable: drugActivityChangeable, + final noDrugsMessage = '${context.l10n.search_no_drugs( + showFilter + ? context.l10n.search_no_drugs_with_filter_amendment + : '') + }\n\n${context.l10n.included_content_addition}'; + return DrugList( + state: state, + activeDrugs: activeDrugs, + buildDrugItems: buildDrugItems, + showDrugInteractionIndicator: showDrugInteractionIndicator, + noDrugsMessage: noDrugsMessage, + searchForDrugClass: searchForDrugClass, + buildContainer: ({ + children, + indicator, + noDrugsMessage, + showInactiveDrugs, + }) => Column( + children: [ + Padding( + padding: EdgeInsets.all(PharMeTheme.smallSpace), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildSearchBarItems( + context, + searchController, + showInactiveDrugs: showInactiveDrugs ?? true, + ), + ), + ), + if (children != null) scrollList( + keepPosition: keepPosition, + children, + ), + if (noDrugsMessage != null) noDrugsMessage, + if (indicator != null) indicator, + ], ), - _maybeBuildInteractionIndicator(context, state, activeDrugs) - ?? SizedBox.shrink(), - ], + drugActivityChangeable: drugActivityChangeable, ); } List _buildSearchBarItems( BuildContext context, TextEditingController searchController, + { + required bool showInactiveDrugs, + } ) { return [ Expanded( @@ -85,39 +96,16 @@ class DrugSearch extends HookWidget { ? context.l10n.search_page_tooltip_search : context.l10n.search_page_tooltip_search_no_class ), - if (showFilter) DrugFilters( - cubit: cubit, - state: state, - activeDrugs: activeDrugs, - searchForDrugClass: searchForDrugClass, - ), + if (showFilter) ...[ + SizedBox(width: PharMeTheme.smallToMediumSpace), + DrugFilters( + cubit: cubit, + state: state, + activeDrugs: activeDrugs, + searchForDrugClass: searchForDrugClass, + showInactiveDrugs: showInactiveDrugs, + ), + ], ]; } - - Widget? _maybeBuildInteractionIndicator( - BuildContext context, - DrugListState state, - ActiveDrugs activeDrugs, - ) { - return state.whenOrNull( - loaded: (drugs, filter) { - if (showDrugInteractionIndicator) { - final filteredDrugs = filter.filter( - drugs, - activeDrugs, - searchForDrugClass: searchForDrugClass, - ); - if (filteredDrugs.any((drug) => isInhibitor(drug.name))) { - return PageIndicatorExplanation( - context.l10n.search_page_indicator_explanation( - drugInteractionIndicatorName, - drugInteractionIndicator - ), - ); - } - } - return null; - } - ); - } } diff --git a/app/lib/common/widgets/drug_search/drug_filters.dart b/app/lib/common/widgets/drug_search/drug_filters.dart index 4fc1e9170..aed99ec35 100644 --- a/app/lib/common/widgets/drug_search/drug_filters.dart +++ b/app/lib/common/widgets/drug_search/drug_filters.dart @@ -10,6 +10,7 @@ class WarningLevelFilterChip extends HookWidget { required this.drugs, required this.activeDrugs, required this.searchForDrugClass, + required this.showInactiveDrugs, }); final WarningLevel warningLevel; @@ -18,12 +19,17 @@ class WarningLevelFilterChip extends HookWidget { final List drugs; final ActiveDrugs activeDrugs; final bool searchForDrugClass; + final bool showInactiveDrugs; int _getFilteredNumber({ required FilterState itemFilter, required List drugs, }) { - return itemFilter + final currentFilter = FilterState.from( + itemFilter, + showInactive: showInactiveDrugs, + ); + return currentFilter .filter(drugs, activeDrugs, searchForDrugClass: searchForDrugClass) .length; } @@ -135,25 +141,32 @@ class DrugFilters extends StatelessWidget { required this.state, required this.activeDrugs, required this.searchForDrugClass, + required this.showInactiveDrugs, }); final DrugListCubit cubit; final DrugListState state; final ActiveDrugs activeDrugs; final bool searchForDrugClass; + final bool showInactiveDrugs; bool _showActiveIndicator() => state.whenOrNull( loaded: (allDrugs, filter) { final relevantDrugsFilter = FilterState.from( FilterState.initial(), query: filter.query, + showInactive: showInactiveDrugs, ); final totalNumberOfDrugs = relevantDrugsFilter.filter( allDrugs, activeDrugs, searchForDrugClass: searchForDrugClass, ).length; - final currentNumberOfDrugs = filter.filter( + final currentFilter = FilterState.from( + filter, + showInactive: showInactiveDrugs, + ); + final currentNumberOfDrugs = currentFilter.filter( allDrugs, activeDrugs, searchForDrugClass: searchForDrugClass, @@ -170,7 +183,7 @@ class DrugFilters extends StatelessWidget { child: Container( decoration: BoxDecoration( shape: BoxShape.circle, - color: PharMeTheme.sinaiMagenta, + color: PharMeTheme.sinaiCyan, ), width: indicatorSize, height: indicatorSize, @@ -196,6 +209,7 @@ class DrugFilters extends StatelessWidget { filter: filter, activeDrugs: activeDrugs, searchForDrugClass: searchForDrugClass, + showInactiveDrugs: showInactiveDrugs, ), ); return [ @@ -205,28 +219,33 @@ class DrugFilters extends StatelessWidget { ]; } - IconButton _buildButton({ + Widget _buildButton({ required void Function()? onPressed, required bool enableIndicator, }) { - return IconButton( + return ResizedIconButton( onPressed: onPressed, - icon: Stack( + size: PharMeTheme.largeSpace, + backgroundColor: PharMeTheme.buttonColor, + iconWidgetBuilder: (size) => Stack( children: [ - Icon(Icons.filter_list), + Icon( + Icons.filter_list, + size: size, + color: PharMeTheme.surfaceColor, + ), if (enableIndicator && _showActiveIndicator()) _buildActiveIndicator(), ], ), - color: PharMeTheme.iconColor, ); } - IconButton _buildDisabledButton() { + Widget _buildDisabledButton() { return _buildButton(onPressed: null, enableIndicator: false); } - IconButton _buildEnabledButton( + Widget _buildEnabledButton( BuildContext context, List allDrugs, FilterState filter, diff --git a/app/lib/common/widgets/gene_modulator_list.dart b/app/lib/common/widgets/gene_modulator_list.dart new file mode 100644 index 000000000..a601da10f --- /dev/null +++ b/app/lib/common/widgets/gene_modulator_list.dart @@ -0,0 +1,114 @@ +import '../module.dart'; + +class GeneModulatorList { + const GeneModulatorList({ + required this.geneName, + this.onlyActiveDrugs = false, + this.displayedDrug, + }); + + final String geneName; + final bool onlyActiveDrugs; + final String? displayedDrug; + + List _filterActiveModulatorDrugNames(List allModulatorDrugs) { + final activeModulators = activeInhibitorsFor( + geneName, + drug: displayedDrug, + ); + return allModulatorDrugs.filter(activeModulators.contains).toList(); + } + + List _getModulatorDrugNames( + BuildContext context, + Map>modulatorDefinition, + String geneName, + ) { + final allModulatorDrugs = modulatorDefinition[geneName]!.keys.toList(); + final modulatorDrugNames = onlyActiveDrugs + ? _filterActiveModulatorDrugNames(allModulatorDrugs) + : allModulatorDrugs; + return getDrugsWithBrandNames( + modulatorDrugNames, + capitalize: true, + brandNamesPrefix: context.l10n.drug_item_brand_names.toLowerCase(), + ); + } + + Map> getContent(BuildContext context) { + final contentDefinition = { + context.l10n.strong_inhibitors_description: strongDrugInhibitors, + context.l10n.moderate_inhibitors_description: moderateDrugInhibitors, + }; + final content = >{}; + for (final subdefinition in contentDefinition.entries) { + final modulatorDefinition = subdefinition.value; + final drugNames = + _getModulatorDrugNames(context, modulatorDefinition, geneName); + if (drugNames.isEmpty) continue; + final getDescription = subdefinition.key; + content[getDescription(drugNames.length, geneName)] = drugNames; + } + return content; + } + + String asString(BuildContext context) { + final listString = StringBuffer(); + for (final modulatorContentEntry in getContent(context).entries) { + final entryString = StringBuffer(modulatorContentEntry.key); + if (modulatorContentEntry.value.length == 1) { + entryString.write(' ${modulatorContentEntry.value.first}'); + } else { + for (final drugName in modulatorContentEntry.value) { + entryString.write('\n- $drugName'); + } + } + listString.write('\n\n$entryString'); + } + return listString.toString(); + } + + GeneModulatorListWidget get widget => GeneModulatorListWidget(this); +} + +class GeneModulatorListWidget extends StatelessWidget { + const GeneModulatorListWidget(this.listDefinition); + + final GeneModulatorList listDefinition; + + List _buildModulatorContent(modulatorContentEntry) { + final descriptionText = modulatorContentEntry.key; + final modulatorDrugNames = modulatorContentEntry.value; + return [ + SizedBox(height: PharMeTheme.smallSpace), + if (modulatorDrugNames.length > 1) ...[ + Text( + descriptionText, + style: TextStyle(fontStyle: FontStyle.italic), + ), + SizedBox(height: PharMeTheme.smallSpace), + UnorderedList(modulatorDrugNames), + ], + if (modulatorDrugNames.length == 1) Text.rich(TextSpan( + children: [ + TextSpan( + text: descriptionText, + style: TextStyle(fontStyle: FontStyle.italic), + ), + TextSpan(text: ' '), + TextSpan(text: modulatorDrugNames.first), + ]) + ), + ]; + } + + @override + Widget build(BuildContext context) { + final modulatorContent = listDefinition.getContent(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + modulatorContent.entries.flatMap(_buildModulatorContent).toList(), + ); + } +} \ No newline at end of file diff --git a/app/lib/common/widgets/indicators.dart b/app/lib/common/widgets/indicators.dart index dedc3b7d3..4d08e2a98 100644 --- a/app/lib/common/widgets/indicators.dart +++ b/app/lib/common/widgets/indicators.dart @@ -1,11 +1,21 @@ import '../module.dart'; Widget loadingIndicator() => - genericIndicator(child: CircularProgressIndicator()); + genericIndicator(child: CircularProgressIndicator(), verticalPadding: 100); Widget errorIndicator(String description) => - genericIndicator(child: Text(description, textAlign: TextAlign.center)); + genericIndicator( + child: Text( + description, + textAlign: TextAlign.center, + style: TextStyle(fontStyle: FontStyle.italic), + ), + verticalPadding: PharMeTheme.mediumToLargeSpace, + ); -Widget genericIndicator({required Widget child}) => Center( +Widget genericIndicator({required Widget child, required double verticalPadding}) => Center( child: Padding( - padding: EdgeInsets.symmetric(vertical: 100, horizontal: 20), + padding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: PharMeTheme.mediumToLargeSpace, + ), child: child)); diff --git a/app/lib/common/widgets/list_description.dart b/app/lib/common/widgets/list_description.dart new file mode 100644 index 000000000..1573f5cb4 --- /dev/null +++ b/app/lib/common/widgets/list_description.dart @@ -0,0 +1,46 @@ +import '../module.dart'; + +TextSpan boldListDescriptionText(String text, { Color? color }) => TextSpan( + text: text, + style: TextStyle( + fontWeight: FontWeight.bold, + color: color ?? PharMeTheme.iconColor, + ), +); + +class ListDescription extends StatelessWidget { + const ListDescription({ + super.key, + required this.textParts, + required this.detailsText, + }); + + final List textParts; + final String detailsText; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: PharMeTheme.mediumSpace, + horizontal: PharMeTheme.smallSpace, + ), + child: Text.rich( + style: PharMeTheme.textTheme.bodyMedium, + TextSpan( + children: [ + ...textParts, + TextSpan(text: ' '), + TextSpan(text: context.l10n.list_subheader_postfix), + TextSpan(text: ' '), + TextSpan( + text: '($detailsText)', + style: TextStyle(color: PharMeTheme.buttonColor), + ), + ], + ), + ), + ); + } + +} \ No newline at end of file diff --git a/app/lib/common/widgets/list_inclusion_description.dart b/app/lib/common/widgets/list_inclusion_description.dart new file mode 100644 index 000000000..a86d6cba0 --- /dev/null +++ b/app/lib/common/widgets/list_inclusion_description.dart @@ -0,0 +1,115 @@ +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +import '../module.dart'; + +enum ListInclusionDescriptionType { + medications, + genes, +} + +extension ListInclusionDescriptionContent on ListInclusionDescriptionType { + String getText(BuildContext context) => + context.l10n.included_content_disclaimer_text( + this == ListInclusionDescriptionType.medications + ? context.l10n.included_content_medications + : context.l10n.included_content_genes + ); + + IconData get icon => this == ListInclusionDescriptionType.medications + ? medicationsIcon + : genesIcon; +} + +class ListInclusionDescription extends StatelessWidget { + const ListInclusionDescription({ + super.key, + required this.type, + }); + + factory ListInclusionDescription.forMedications() => + ListInclusionDescription(type: ListInclusionDescriptionType.medications); + factory ListInclusionDescription.forGenes() => + ListInclusionDescription(type: ListInclusionDescriptionType.genes); + + final ListInclusionDescriptionType type; + + @override + Widget build(BuildContext context) { + return DisclaimerRow( + icon: Padding( + padding: EdgeInsets.only( + left: PharMeTheme.smallSpace, + right: PharMeTheme.smallSpace * 0.5, + ), + child: IncludedContentIcon(type: type), + ), + text: Text( + type.getText(context), + style: TextStyle(color: PharMeTheme.iconColor), + ), + ); + } + +} + +class IncludedContentIcon extends StatelessWidget { + const IncludedContentIcon({ + super.key, + required this.type, + this.color, + this.size, + }); + + final ListInclusionDescriptionType type; + final Color? color; + final double? size; + + @override + Widget build(BuildContext context) { + final totalSize = size ?? PharMeTheme.mediumToLargeSpace * 1.5; + final checkIconBackgroundSize = totalSize * 0.5; + final rightShift = type == ListInclusionDescriptionType.medications + ? checkIconBackgroundSize / 2 + : checkIconBackgroundSize / 4; + final bottomShift = type == ListInclusionDescriptionType.medications + ? totalSize * 0.1 + : 0.0; + final iconSize = totalSize * 0.9 - rightShift; + final checkIconSize = checkIconBackgroundSize * 0.8; + return Stack( + children: [ + SizedBox( + height: totalSize, + width: totalSize, + ), + Icon( + type.icon, + size: iconSize, + color: color ?? PharMeTheme.buttonColor, + ), + Positioned( + right: 0, + bottom: bottomShift, + child: Stack( + children: [ + Icon( + FontAwesomeIcons.solidCircle, + size: checkIconBackgroundSize, + color: PharMeTheme.surfaceColor, + ), + Positioned( + top: (checkIconBackgroundSize - checkIconSize) / 2, + left: (checkIconBackgroundSize - checkIconSize) / 2, + child: Icon( + FontAwesomeIcons.solidCircleCheck, + size: checkIconSize, + color: PharMeTheme.sinaiPurple, + )), + ], + ), + ) + ], + ); + } + +} \ No newline at end of file diff --git a/app/lib/common/widgets/module.dart b/app/lib/common/widgets/module.dart index 29e72f91c..5844b6a32 100644 --- a/app/lib/common/widgets/module.dart +++ b/app/lib/common/widgets/module.dart @@ -4,6 +4,7 @@ export 'dialog_action.dart'; export 'dialog_content_text.dart'; export 'dialog_wrapper.dart'; export 'direction_button.dart'; +export 'disclaimer_row.dart'; export 'drug_activity_selection.dart'; export 'drug_list/builder.dart'; export 'drug_list/cubit.dart'; @@ -14,15 +15,21 @@ export 'drug_search/drug_filters.dart'; export 'drug_search/filter_data_wrapper.dart'; export 'error_handler.dart'; export 'full_width_button.dart'; +export 'gene_modulator_list.dart'; export 'headings.dart'; export 'hyperlink.dart'; export 'indicators.dart'; export 'lifecycle_observer.dart'; +export 'list_description.dart'; +export 'list_inclusion_description.dart'; export 'page_description.dart'; export 'page_indicator_explanation.dart'; export 'page_scaffold.dart'; export 'pharme_logo_page.dart'; +export 'phenoconversion_explanation.dart'; +export 'pretty_expansion_tile.dart'; export 'radiant_gradient_mask.dart'; +export 'resized_icon_button.dart'; export 'rounded_card.dart'; export 'scroll_list.dart'; export 'subheader_divider.dart'; diff --git a/app/lib/common/widgets/page_description.dart b/app/lib/common/widgets/page_description.dart index 649309e16..ef0867c7f 100644 --- a/app/lib/common/widgets/page_description.dart +++ b/app/lib/common/widgets/page_description.dart @@ -1,17 +1,18 @@ import '../module.dart'; class PageDescription extends StatelessWidget { - const PageDescription(this.widget); + const PageDescription(this.widget, { this.customPadding }); factory PageDescription.fromText(String text) => PageDescription(Text(text)); final Widget widget; + final EdgeInsets? customPadding; @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.only( + padding: customPadding ?? EdgeInsets.only( left: PharMeTheme.smallSpace, right: PharMeTheme.smallSpace, bottom: PharMeTheme.smallSpace), diff --git a/app/lib/common/widgets/page_indicator_explanation.dart b/app/lib/common/widgets/page_indicator_explanation.dart index acde779fe..1decc0823 100644 --- a/app/lib/common/widgets/page_indicator_explanation.dart +++ b/app/lib/common/widgets/page_indicator_explanation.dart @@ -1,15 +1,25 @@ import '../module.dart'; class PageIndicatorExplanation extends StatelessWidget { - const PageIndicatorExplanation(this.text); + const PageIndicatorExplanation(this.text, {this.indicator}); + final String? indicator; final String text; @override Widget build(BuildContext context) { + final textStyle = PharMeTheme.textTheme.labelMedium!.copyWith( + fontStyle: FontStyle.italic, + ); return Padding( padding: EdgeInsets.all(PharMeTheme.smallSpace), - child: Text(text), + child: indicator.isNotNullOrBlank + ? buildTable( + [TableRowDefinition(indicator!, text)], + boldKey: false, + style: textStyle, + ) + : Text(text, style: textStyle), ); } } \ No newline at end of file diff --git a/app/lib/common/widgets/phenoconversion_explanation.dart b/app/lib/common/widgets/phenoconversion_explanation.dart new file mode 100644 index 000000000..1cd6cd22e --- /dev/null +++ b/app/lib/common/widgets/phenoconversion_explanation.dart @@ -0,0 +1,185 @@ +import '../module.dart'; + +class PhenoconversionDisplayConfig { + PhenoconversionDisplayConfig({ + required this.partSeparator, + required this.userSalutation, + required this.userGenitive, + required this.useConsult, + }); + + final String partSeparator; + final String userSalutation; + final String userGenitive; + final bool useConsult; +} + +enum PhenoconversionDisplayType { + user, + expert +} + +extension on PhenoconversionDisplayType { + PhenoconversionDisplayConfig getConfig(BuildContext context) { + switch (this) { + case PhenoconversionDisplayType.expert: + return PhenoconversionDisplayConfig( + partSeparator: ' ', + userSalutation: context.l10n.inhibitor_third_person_salutation, + userGenitive: context.l10n.inhibitor_third_person_salutation_genitive, + useConsult: false, + ); + default: + return PhenoconversionDisplayConfig( + partSeparator: '\n\n', + userSalutation: context.l10n.inhibitor_direct_salutation, + userGenitive: context.l10n.inhibitor_direct_salutation_genitive, + useConsult: true, + ); + } + } +} + +typedef PhenoconversionExplanationBuilder = dynamic Function( + List inhibitedGenotypes, + String drugName, + BuildContext? context, +); + +dynamic _getPhenoconversionExplanation({ + required Drug drug, + required PhenoconversionExplanationBuilder explanationBuilder, + BuildContext? context, +}) { + final inhibitedGenotypes = getInhibitedGenotypesForDrug(drug); + if (inhibitedGenotypes.isEmpty) return null; + return explanationBuilder(inhibitedGenotypes, drug.name, context); +} + +Widget? getUserPhenoconversionExplanation(Drug drug) { + return _getPhenoconversionExplanation( + drug: drug, + explanationBuilder: (inhibitedGenotypes, drugName, _) => + PhenoconversionExplanation( + inhibitedGenotypes: inhibitedGenotypes, + drugName: drugName, + displayType: PhenoconversionDisplayType.user, + ), + ); +} + +String? getExpertPhenoconversionExplanation(Drug drug, BuildContext context) { + return _getPhenoconversionExplanation( + drug: drug, + explanationBuilder: (inhibitedGenotypes, drugName, context) => + getPhenoconversionExplanationString( + context: context!, + inhibitedGenotypes: inhibitedGenotypes, + drugName: drugName, + displayType: PhenoconversionDisplayType.expert, + ), + context: context, + ); +} + +String? getPhenoconversionExplanationString({ + required BuildContext context, + required List inhibitedGenotypes, + required String drugName, + required PhenoconversionDisplayType displayType, +}) { + final displayConfig = displayType.getConfig(context); + return inhibitedGenotypes.flatMap((genotypeResult) => [ + _getPhenoconversionDetailText( + context, + genotypeResult, drug: drugName, + displayConfig: displayConfig, + ), + GeneModulatorList( + geneName: genotypeResult.gene, + onlyActiveDrugs: true, + displayedDrug: drugName, + ).asString(context), + ]).join(displayConfig.partSeparator); +} + +class PhenoconversionExplanation extends StatelessWidget { + const PhenoconversionExplanation({ + super.key, + required this.inhibitedGenotypes, + required this.drugName, + this.displayType = PhenoconversionDisplayType.user, + }); + + final List inhibitedGenotypes; + final String? drugName; + final PhenoconversionDisplayType displayType; + + @override + Widget build(BuildContext context) { + final displayConfig = displayType.getConfig(context); + return PrettyExpansionTile( + title: buildTable([ + TableRowDefinition( + drugInteractionIndicator, + context.l10n.inhibitor_message( + displayConfig.userSalutation, + enumerationWithAnd( + inhibitedGenotypes.map( + (genotypeResult) => genotypeResult.gene + ).toList(), + context, + ), + displayConfig.userGenitive, + ).capitalize(), + )], + boldKey: false, + ), + titlePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.all(PharMeTheme.mediumSpace), + children: inhibitedGenotypes.flatMap( + (genotypeResult) => [ + Text(_getPhenoconversionDetailText( + context, + genotypeResult, + drug: drugName, + displayConfig: displayConfig, + )), + GeneModulatorList( + geneName: genotypeResult.gene, + onlyActiveDrugs: true, + displayedDrug: drugName, + ).widget, + ] + ).toList(), + ); + } +} + +String _getPhenoconversionDetailText( + BuildContext context, + GenotypeResult genotypeResult, + { + required String? drug, + required PhenoconversionDisplayConfig displayConfig, + }) +{ + final activeInhibitors = activeInhibitorsFor( + genotypeResult.gene, + drug: drug, + ); + final consequence = activeInhibitors.all(isModerateInhibitor) + ? context.l10n.inhibitors_consequence_not_adapted( + displayConfig.userGenitive, + genotypeResult.geneDisplayString, + ).capitalize() + : context.l10n.inhibitors_consequence_adapted( + displayConfig.userGenitive, + genotypeResult.geneDisplayString, + genotypeResult.phenotypeDisplayString(context), + overwritePhenotype, + ).capitalize(); + return '$consequence${ + displayConfig.useConsult ? ' ${context.l10n.consult_text}' : '' + }'; +} diff --git a/app/lib/common/widgets/pretty_expansion_tile.dart b/app/lib/common/widgets/pretty_expansion_tile.dart new file mode 100644 index 000000000..0fdcdfef7 --- /dev/null +++ b/app/lib/common/widgets/pretty_expansion_tile.dart @@ -0,0 +1,50 @@ +import '../module.dart'; + +class PrettyExpansionTile extends StatelessWidget { + const PrettyExpansionTile({ + super.key, + required this.title, + required this.children, + this.onExpansionChanged, + this.visualDensity, + this.titlePadding, + this.childrenPadding, + this.icon, + this.initiallyExpanded = false, + this.enabled = true, + }); + + final Widget title; + // ignore: avoid_positional_boolean_parameters + final void Function(bool)? onExpansionChanged; + final List children; + final VisualDensity? visualDensity; + final EdgeInsets? titlePadding; + final EdgeInsets? childrenPadding; + final Widget? icon; + final bool initiallyExpanded; + final bool enabled; + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + dividerColor: Colors.transparent, + ), + child: ExpansionTile( + key: GlobalKey(), // force to rebuild + enabled: enabled, + initiallyExpanded: initiallyExpanded, + title: title, + iconColor: PharMeTheme.iconColor, + trailing: icon, + collapsedIconColor: PharMeTheme.iconColor, + onExpansionChanged: onExpansionChanged, + visualDensity: visualDensity, + tilePadding: titlePadding, + childrenPadding: childrenPadding, + children: children, + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/common/widgets/resized_icon_button.dart b/app/lib/common/widgets/resized_icon_button.dart new file mode 100644 index 000000000..0be65c197 --- /dev/null +++ b/app/lib/common/widgets/resized_icon_button.dart @@ -0,0 +1,42 @@ +import '../module.dart'; + +Widget defaultIconBuilder(IconData icon, double? size) => Icon( + icon, + size: size, + color: PharMeTheme.iconColor, +); + +class ResizedIconButton extends StatelessWidget { + const ResizedIconButton({ + super.key, + required this.iconWidgetBuilder, + required this.size, + this.onPressed, + this.backgroundColor, + this.disabledBackgroundColor, + }); + + final Widget Function(double? size) iconWidgetBuilder; + final double size; + final void Function()? onPressed; + final Color? backgroundColor; + final Color? disabledBackgroundColor; + + @override + Widget build(BuildContext context) { + final padding = size * 0.2; + return SizedBox( + width: size, + height: size, + child: IconButton( + padding: EdgeInsets.all(padding), + onPressed: onPressed, + icon: iconWidgetBuilder(size - 2 * padding), + style: IconButton.styleFrom( + backgroundColor: backgroundColor, + disabledBackgroundColor: disabledBackgroundColor, + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/common/widgets/subheader_divider.dart b/app/lib/common/widgets/subheader_divider.dart index 43b155224..85c11be32 100644 --- a/app/lib/common/widgets/subheader_divider.dart +++ b/app/lib/common/widgets/subheader_divider.dart @@ -1,5 +1,10 @@ import '../module.dart'; +TextStyle subheaderDividerStyle({Color? color}) => + PharMeTheme.textTheme.bodySmall!.copyWith( + color: color ?? PharMeTheme.subheaderColor, + ); + class SubheaderDivider extends StatelessWidget { const SubheaderDivider({ this.text = '', @@ -16,7 +21,7 @@ class SubheaderDivider extends StatelessWidget { @override Widget build(BuildContext context) { - final widgetColor = color ?? Colors.grey[600]; + final widgetColor = color ?? PharMeTheme.subheaderColor; return Padding( padding: EdgeInsets.all(padding ?? PharMeTheme.smallSpace), child: Column( @@ -25,8 +30,7 @@ class SubheaderDivider extends StatelessWidget { if (useLine) Divider(color: widgetColor, thickness: 0.5), Text( text, - style: - PharMeTheme.textTheme.bodySmall!.copyWith(color: widgetColor), + style: subheaderDividerStyle(color: widgetColor), textAlign: TextAlign.start, ), ], diff --git a/app/lib/common/widgets/tutorial/show_app_tour.dart b/app/lib/common/widgets/tutorial/show_app_tour.dart index 7d5c1070b..ddb0d1407 100644 --- a/app/lib/common/widgets/tutorial/show_app_tour.dart +++ b/app/lib/common/widgets/tutorial/show_app_tour.dart @@ -37,6 +37,10 @@ FutureOr showAppTour( context.l10n.tutorial_app_tour_1_title, content: (context) => _buildContent( context.l10n.tutorial_app_tour_1_body, + trailingSpan: TextSpan( + text: context.l10n.tutorial_app_tour_1_body_bold, + style: TextStyle(fontWeight: FontWeight.bold), + ), ), assetPath: 'assets/images/tutorial/05_bottom_navigation_loopable.gif', @@ -78,6 +82,10 @@ FutureOr showAppTour( context.l10n.tutorial_app_tour_5_title, content: (context) => _buildContent( context.l10n.tutorial_app_tour_5_body, + trailingSpan: TextSpan( + text: context.l10n.tutorial_app_tour_5_body_bold, + style: TextStyle(fontWeight: FontWeight.bold), + ), ), assetPath: 'assets/images/tutorial/09_faq_and_more_loopable.gif', @@ -86,8 +94,11 @@ FutureOr showAppTour( onClose: revisiting ? null : () async { - MetaData.instance.tutorialDone = true; - await MetaData.save(); - }, + MetaData.instance.tutorialDone = true; + await MetaData.save(); + // ignore: use_build_context_synchronously + await overwriteRoutes(context, nextPage: MainRoute()); + }, lastNextButtonText: lastNextButtonText, + firstBackButtonText: revisiting ? null : context.l10n.onboarding_prev, ); \ No newline at end of file diff --git a/app/lib/common/widgets/tutorial/show_drug_selection_intro.dart b/app/lib/common/widgets/tutorial/show_drug_selection_intro.dart index d93601035..2ae99a7c3 100644 --- a/app/lib/common/widgets/tutorial/show_drug_selection_intro.dart +++ b/app/lib/common/widgets/tutorial/show_drug_selection_intro.dart @@ -19,5 +19,6 @@ FutureOr showDrugSelectionIntro(BuildContext context) => onClose: () async { MetaData.instance.initialDrugSelectionInitiated = true; await MetaData.save(); - } + }, + firstBackButtonText: context.l10n.onboarding_prev, ); \ No newline at end of file diff --git a/app/lib/common/widgets/tutorial/tutorial_builder.dart b/app/lib/common/widgets/tutorial/tutorial_builder.dart index 67b7178fa..ca0b2ebcc 100644 --- a/app/lib/common/widgets/tutorial/tutorial_builder.dart +++ b/app/lib/common/widgets/tutorial/tutorial_builder.dart @@ -5,11 +5,15 @@ class TutorialBuilder extends HookWidget { const TutorialBuilder({ super.key, required this.pages, + required this.initiateRouteBack, this.lastNextButtonText, + this.firstBackButtonText, }); final List pages; final String? lastNextButtonText; + final String? firstBackButtonText; + final void Function() initiateRouteBack; Widget getImageAsset(String assetPath) { return Container( @@ -141,20 +145,39 @@ class TutorialBuilder extends HookWidget { ValueNotifier currentPageIndex, ) { final isFirstPage = currentPageIndex.value == 0; + final showFirstButton = !isFirstPage || ( + firstBackButtonText.isNotNullOrBlank && + context.router.canPop( + ignoreChildRoutes: true, + ignorePagelessRoutes: true, + ) + ); final isLastPage = currentPageIndex.value == pages.length - 1; final directionButtonTextStyle = PharMeTheme.textTheme.titleLarge!.copyWith(fontSize: 20); const directionButtonIconSize = 22.0; return Row( - mainAxisAlignment: isFirstPage - ? MainAxisAlignment.end - : MainAxisAlignment.spaceBetween, + mainAxisAlignment: showFirstButton + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: [ - if (!isFirstPage) DirectionButton( + if (showFirstButton) DirectionButton( direction: ButtonDirection.backward, - onPressed: () => currentPageIndex.value = currentPageIndex.value - 1, - text: context.l10n.onboarding_prev, + onPressed: isFirstPage + ? () { + initiateRouteBack(); + final currentRoute = context.router.current.name; + context.router.popUntil( + (route) => + route.settings.name != null && + route.settings.name != currentRoute, + ); + } + : () => currentPageIndex.value = currentPageIndex.value - 1, + text: isFirstPage + ? firstBackButtonText! + : context.l10n.onboarding_prev, buttonTextStyle: directionButtonTextStyle, iconSize: directionButtonIconSize, ), diff --git a/app/lib/common/widgets/tutorial/tutorial_controller.dart b/app/lib/common/widgets/tutorial/tutorial_controller.dart index 612cbb6ca..6b7fb0b22 100644 --- a/app/lib/common/widgets/tutorial/tutorial_controller.dart +++ b/app/lib/common/widgets/tutorial/tutorial_controller.dart @@ -13,15 +13,20 @@ class TutorialController { static TutorialController get instance => _instance; bool _isOpen = false; + bool _wasRoutedBack = false; + + void initiateRouteBack() => _wasRoutedBack = true; FutureOr showTutorial({ required BuildContext context, required List pages, String? lastNextButtonText, + String? firstBackButtonText, FutureOr Function()? onClose, }) async { if (_isOpen) return null; _isOpen = true; + _wasRoutedBack = false; await showModalBottomSheet( context: context, enableDrag: true, @@ -33,9 +38,11 @@ class TutorialController { builder: (context) => TutorialBuilder( pages: pages, lastNextButtonText: lastNextButtonText, + firstBackButtonText: firstBackButtonText, + initiateRouteBack: initiateRouteBack, ), ); - if (onClose != null) await onClose(); _isOpen = false; + if (!_wasRoutedBack && onClose != null) await onClose(); } } diff --git a/app/lib/drug/pages/drug.dart b/app/lib/drug/pages/drug.dart index 69a874747..2a20bb018 100644 --- a/app/lib/drug/pages/drug.dart +++ b/app/lib/drug/pages/drug.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:provider/provider.dart'; import '../../common/module.dart'; @@ -32,7 +34,7 @@ class DrugPage extends StatelessWidget { } Widget _buildDrugsPage(BuildContext context, { required bool loading }) { - return pageScaffold( + return unscrollablePageScaffold( title: isInhibitor(drug.name) ? '${drug.name.capitalize()}$drugInteractionIndicator' : drug.name.capitalize(), @@ -42,29 +44,45 @@ class DrugPage extends StatelessWidget { onPressed: loading ? null : () => context.read().createAndSharePdf(drug, context), icon: Icon( - Icons.ios_share_rounded, + Platform.isIOS ? Icons.ios_share_rounded : Icons.share_rounded, color: PharMeTheme.primaryColor, ), ) ], - body: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DrugAnnotationCards( - drug, - isActive: drug.isActive, - setActivity: context.read().setActivity, - disabled: loading, + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: PharMeTheme.smallToMediumSpace, + vertical: PharMeTheme.smallSpace, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DrugAnnotationCards( + drug, + isActive: drug.isActive, + setActivity: context.read().setActivity, + disabled: loading, + ), + SizedBox(height: PharMeTheme.mediumSpace), + GuidelineAnnotationCard(drug), + ], + ), ), - SizedBox(height: PharMeTheme.mediumSpace), - GuidelineAnnotationCard(drug), - ], + ), ), - ), - ], + if (isInhibitor(drug.name)) PageIndicatorExplanation( + context.l10n.drugs_page_is_inhibitor( + drug.name, + enumerationWithAnd(inhibitedGenes(drug), context), + ), + indicator: drugInteractionIndicator, + ), + ], + ), ); } } diff --git a/app/lib/drug/widgets/annotation_cards/disclaimer.dart b/app/lib/drug/widgets/annotation_cards/disclaimer.dart index 1ad4181cd..89602c3e1 100644 --- a/app/lib/drug/widgets/annotation_cards/disclaimer.dart +++ b/app/lib/drug/widgets/annotation_cards/disclaimer.dart @@ -1,7 +1,9 @@ +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + import '../../../common/module.dart'; -class Disclaimer extends StatelessWidget { - const Disclaimer({ this.userGuideline }); +class GuidelineDisclaimer extends StatelessWidget { + const GuidelineDisclaimer({ this.userGuideline }); final Guideline? userGuideline; @@ -16,36 +18,38 @@ class Disclaimer extends StatelessWidget { color: PharMeTheme.surfaceColor, border: Border.all(color: PharMeTheme.errorColor, width: 1.2), ), - child: Text.rich( - TextSpan(children: [ - WidgetSpan( - child: Icon( + child: Column( + children: [ + DisclaimerRow( + icon: Icon( Icons.warning_rounded, size: PharMeTheme.mediumSpace, color: PharMeTheme.errorColor, ), + text: Text( + context.l10n.drugs_page_main_disclaimer_text, + style: TextStyle(fontWeight: FontWeight.w600), + ), ), - TextSpan(text: ' '), - TextSpan( - text: context.l10n.drugs_page_disclaimer_description, - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: context.l10n.drugs_page_disclaimer_text_part_0, - style: TextStyle(fontWeight: FontWeight.w500), + SizedBox(height: PharMeTheme.smallSpace), + DisclaimerRow( + icon: IncludedContentIcon( + type: ListInclusionDescriptionType.genes, + size: PharMeTheme.mediumSpace, + color: PharMeTheme.onSurfaceText, + ), + text: Text(ListInclusionDescriptionType.genes.getText(context)), ), - if (userGuideline != null) TextSpan( - children: [ - TextSpan(text: '\n\n'), - TextSpan(text: context.l10n.drugs_page_disclaimer_text_part_1), - TextSpan(text: ' '), - TextSpan(text: context.l10n.drugs_page_disclaimer_text_part_2), - ], - style: PharMeTheme.textTheme.labelMedium!.copyWith( - fontWeight: FontWeight.w300, + SizedBox(height: PharMeTheme.smallSpace), + DisclaimerRow( + icon: Icon( + FontAwesomeIcons.puzzlePiece, + size: PharMeTheme.mediumSpace, + color: PharMeTheme.onSurfaceText, ), - ) - ]), + text: Text(context.l10n.drugs_page_puzzle_disclaimer_text), + ), + ], ), ); } diff --git a/app/lib/drug/widgets/annotation_cards/drug.dart b/app/lib/drug/widgets/annotation_cards/drug.dart index 662080028..74fc1b772 100644 --- a/app/lib/drug/widgets/annotation_cards/drug.dart +++ b/app/lib/drug/widgets/annotation_cards/drug.dart @@ -19,60 +19,64 @@ class DrugAnnotationCards extends StatelessWidget { return Column( children: [ RoundedCard( - innerPadding: EdgeInsets.symmetric(horizontal: PharMeTheme.mediumSpace), + innerPadding: EdgeInsets.symmetric( + horizontal: PharMeTheme.mediumSpace, + ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (drug.annotations.brandNames.isNotEmpty) ...[ + SizedBox(height: PharMeTheme.mediumSpace), + buildTable([ + TableRowDefinition( + context.l10n.drug_item_brand_names, + drug.annotations.brandNames.join(', '), + ), + ]), + ], buildDrugActivitySelection( context: context, drug: drug, setActivity: setActivity, title: context.l10n.drugs_page_text_active, + titleStyle: PharMeTheme.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), isActive: isActive, disabled: disabled, contentPadding: EdgeInsets.zero, ), - if (isInhibitor(drug.name)) ...[ - SizedBox(height: PharMeTheme.smallSpace), - buildTable( - [TableRowDefinition( - drugInteractionIndicator, - context.l10n.drugs_page_is_inhibitor( - drug.name, - inhibitedGenes(drug).join(', '), - ), - )], - boldHeader: false, - ), - SizedBox(height: PharMeTheme.mediumSpace), - ], ], - ) + ), ), SizedBox(height: PharMeTheme.smallSpace), - SubHeader(context.l10n.drugs_page_header_drug), - SizedBox(height: PharMeTheme.smallSpace), - RoundedCard( - innerPadding: EdgeInsets.all(PharMeTheme.mediumSpace), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(drug.annotations.indication), - SizedBox(height: PharMeTheme.smallSpace), - buildTable([ - TableRowDefinition( - context.l10n.drugs_page_header_drugclass, - drug.annotations.drugclass, - ), - if (drug.annotations.brandNames.isNotEmpty) - TableRowDefinition( - context.l10n.drug_item_brand_names, - drug.annotations.brandNames.join(', '), - ), - ]), - ], + PrettyExpansionTile( + key: Key('drug-information-expansion-tile'), + title: SubHeader(context.l10n.drugs_page_header_drug), + visualDensity: VisualDensity.compact, + titlePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + children: [ + SizedBox(height: PharMeTheme.smallSpace), + RoundedCard( + innerPadding: EdgeInsets.all(PharMeTheme.mediumSpace), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(drug.annotations.indication), + SizedBox(height: PharMeTheme.smallSpace), + buildTable([ + TableRowDefinition( + context.l10n.drugs_page_header_drugclass, + drug.annotations.drugclass, + ), + ]), + ], + ), + ), ), - ), + ], ), ], ); diff --git a/app/lib/drug/widgets/annotation_cards/guideline.dart b/app/lib/drug/widgets/annotation_cards/guideline.dart index 293f1432d..6d16c68a5 100644 --- a/app/lib/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/drug/widgets/annotation_cards/guideline.dart @@ -24,16 +24,14 @@ class GuidelineAnnotationCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (drug.guidelines.isNotEmpty) ...[ - ..._buildHeader(context), - SizedBox(height: PharMeTheme.mediumSpace), - _buildCard(context), - SizedBox(height: PharMeTheme.mediumSpace), + _buildResultSection(context), + SizedBox(height: PharMeTheme.smallToMediumSpace), _buildSourcesSection(context), + SizedBox(height: PharMeTheme.mediumSpace), + GuidelineDisclaimer(userGuideline: drug.userGuideline), ] else ...[ - ..._buildHeader(context), - SizedBox(height: PharMeTheme.smallSpace), - _buildCard(context), + _buildResultSection(context), ], ], ), @@ -43,59 +41,79 @@ class GuidelineAnnotationCard extends StatelessWidget { ); } - Widget _buildCard(BuildContext context) { + Widget _buildResultSection(BuildContext context) { final implicationText = drug.userGuideline?.annotations.implication; final recommendationText = drug.userGuideline?.annotations.recommendation; - return RoundedCard( - key: Key('annotationCard'), - radius: PharMeTheme.innerCardRadius, - outerHorizontalPadding: 0, - outerVerticalPadding: 0, - color: drug.warningLevel.color, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text.rich( - TextSpan(children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Icon( - drug.warningLevel.icon, - color: PharMeTheme.onSurfaceText, - size: PharMeTheme.mediumToLargeSpace, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPhenotype(context), + SizedBox(height: PharMeTheme.smallToMediumSpace), + RoundedCard( + key: Key('annotationCard'), + radius: PharMeTheme.innerCardRadius, + outerHorizontalPadding: 0, + outerVerticalPadding: 0, + color: drug.warningLevel.color, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan(children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + drug.warningLevel.icon, + color: PharMeTheme.onSurfaceText, + size: PharMeTheme.mediumToLargeSpace, + ), + ), + TextSpan( + text: ' ${drug.warningLevel.getLabel(context)}', + ), + ]), + style: PharMeTheme.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, ), ), - TextSpan( - text: ' ${drug.warningLevel.getLabel(context)}', - ), - ]), - style: PharMeTheme.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: PharMeTheme.smallToMediumSpace), - Text.rich( - TextSpan( - text: - implicationText ?? context.l10n.drugs_page_no_guidelines_text, - ), - ), - if (recommendationText != null) ...[ - SizedBox(height: PharMeTheme.smallToMediumSpace), - Text.rich( - TextSpan(children: [ + SizedBox(height: PharMeTheme.smallToMediumSpace), + Text.rich( TextSpan( - text: context.l10n.drugs_page_recommendation_description, - style: TextStyle(fontWeight: FontWeight.bold), + children: [ + if (implicationText != null) TextSpan( + text: context.l10n.drugs_page_implication_description, + ), + WidgetSpan(child: SizedBox(height: PharMeTheme.mediumSpace * 1.2)), + TextSpan( + text: + implicationText ?? context.l10n.drugs_page_no_guidelines_text, + style: implicationText != null + ? TextStyle(fontWeight: FontWeight.bold) + : TextStyle(fontStyle: FontStyle.italic) + ), + ], ), - TextSpan(text: recommendationText), - ]), - ), - ], - SizedBox(height: PharMeTheme.smallToMediumSpace), - Disclaimer(userGuideline: drug.userGuideline), - ] - ) + ), + if (recommendationText != null) ...[ + SizedBox(height: PharMeTheme.smallToMediumSpace), + Text.rich( + TextSpan(children: [ + TextSpan( + text: context.l10n.drugs_page_recommendation_description, + ), + WidgetSpan(child: SizedBox(height: PharMeTheme.mediumSpace * 1.2)), + TextSpan( + text: recommendationText, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ]), + ), + ], + ] + ) + ), + _maybeBuildPhenoconversionInformation(context) ?? SizedBox.shrink(), + ], ); } @@ -107,47 +125,33 @@ class GuidelineAnnotationCard extends StatelessWidget { : context.l10n.drugs_page_tooltip_guideline_missing; } - List _buildHeader(BuildContext context) { - if (drug.userGuideline == null && drug.guidelines.isEmpty) { - return [ - Text( - context.l10n.drugs_page_guidelines_empty(drug.name), - style: TextStyle(fontStyle: FontStyle.italic), - ), - ]; - } else { - final genotypeResults = drug.guidelineGenotypes.map((genotypeKey) => - UserData.instance.genotypeResults![genotypeKey] ?? - // Should not be null but to be safe - GenotypeResult.missingResult( - GenotypeKey.extractGene(genotypeKey), - variant: GenotypeKey.maybeExtractVariant(genotypeKey), - ) - ).toList(); - final geneDescriptions = genotypeResults.map((genotypeResult) => - TableRowDefinition( - genotypeResult.geneDisplayString, - possiblyAdaptedPhenotype( - context, - genotypeResult, - drug: drug.name, - ), - ) - ).toList(); - return [ - buildTable(geneDescriptions), - if (genotypeResults.any( - (genotypeResult) => isInhibited(genotypeResult, drug: drug.name) - )) ...[ - SizedBox(height: PharMeTheme.smallSpace), - buildDrugInteractionInfo( - context, - genotypeResults, - drug: drug.name, - ), - ], - ]; + Widget _buildPhenotype(BuildContext context) { + final genotypeResults = getGenotypeResultsForDrug(drug); + if (genotypeResults == null) { + return Text( + context.l10n.drugs_page_guidelines_empty(drug.name), + style: TextStyle(fontStyle: FontStyle.italic), + ); } + return buildTable( + genotypeResults.map((genotypeResult) => + phenotypeTableRow( + context, + key: genotypeResult.geneDisplayString, + genotypeResult: genotypeResult, + drug: drug.name, + ), + ).toList(), + ); + } + + Widget? _maybeBuildPhenoconversionInformation(BuildContext context) { + final phenoconversionExplanation = getUserPhenoconversionExplanation(drug); + if (phenoconversionExplanation == null) return null; + return Padding( + padding: EdgeInsets.only(top: PharMeTheme.smallSpace), + child: phenoconversionExplanation, + ); } Widget _buildSourcesSection(BuildContext context) { diff --git a/app/lib/drug/widgets/tooltip_icon.dart b/app/lib/drug/widgets/tooltip_icon.dart index c901165b5..8e3b00654 100644 --- a/app/lib/drug/widgets/tooltip_icon.dart +++ b/app/lib/drug/widgets/tooltip_icon.dart @@ -12,7 +12,8 @@ class TooltipIcon extends StatelessWidget { return Tooltip( key: tooltipKey, message: message, - margin: EdgeInsets.symmetric(horizontal: PharMeTheme.smallSpace), + margin: EdgeInsets.symmetric(horizontal: PharMeTheme.smallToMediumSpace), + padding: EdgeInsets.all(PharMeTheme.smallSpace), triggerMode: TooltipTriggerMode.manual, child: SizedBox( height: size, diff --git a/app/lib/drug_selection/pages/drug_selection.dart b/app/lib/drug_selection/pages/drug_selection.dart index a566ea4f7..bf2c055cd 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -31,19 +31,19 @@ class DrugSelectionPage extends HookWidget { } return unscrollablePageScaffold( title: context.l10n.drug_selection_header, - canNavigateBack: !concludesOnboarding, contextToDismissFocusOnTap: context, + canNavigateBack: context.router.stack.length > 1, body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.symmetric(vertical: PharMeTheme.smallSpace), + padding: EdgeInsets.only(top: PharMeTheme.smallSpace), child: PageDescription.fromText( - context.l10n.drug_selection_settings_description, + context.l10n.drug_selection_description, ), ), Expanded(child: _buildDrugList(context, state)), - if (concludesOnboarding) _buildButton(context, state), + _buildButton(context, state), ], ), ); @@ -61,42 +61,33 @@ class DrugSelectionPage extends HookWidget { } Widget _buildButton(BuildContext context, DrugSelectionState state) { + final buttonText = concludesOnboarding + ? context.l10n.action_proceed_to_app + : context.l10n.action_back_to_app; + final onButtonPressed = concludesOnboarding + ? () async { + MetaData.instance.initialDrugSelectionDone = true; + await MetaData.save(); + // ignore: use_build_context_synchronously + await context.router.push( + MainRoute(), + ); + } + : () { + context.router.maybePop(); + }; return Padding( padding: EdgeInsets.all(PharMeTheme.mediumSpace), child: FullWidthButton( - context.l10n.action_continue, - () async { - 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, - ), - ], - ), - ); - }, + buttonText, + onButtonPressed, enabled: _isEditable(state), ) ); } Widget _buildDrugList(BuildContext context, DrugSelectionState state) { - if (DrugsWithGuidelines.instance.drugs!.isEmpty) { + if (DrugsWithGuidelines.instance.drugs?.isEmpty ?? true) { return Column( children: [ Text( diff --git a/app/lib/faq/pages/content.dart b/app/lib/faq/pages/content.dart index fb9885b6c..10eee6991 100644 --- a/app/lib/faq/pages/content.dart +++ b/app/lib/faq/pages/content.dart @@ -33,32 +33,7 @@ class FaqWidgetAnswerQuestion extends FaqQuestion { }); } -Column _getPhenoconversionString( - Map> modulators, - String Function(String) getDescriptionPerGene, -) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...modulators.keys.flatMap( - (geneName) => [ - Text( - getDescriptionPerGene(geneName), - style: TextStyle(fontStyle: FontStyle.italic), - ), - SizedBox(height: PharMeTheme.smallSpace * 0.5), - UnorderedList( - getDrugsWithBrandNames( - modulators[geneName]!.keys.toList(), - capitalize: true, - ), - ), - ]), - ], - ); -} - -final faqContent = [ +List getFaqContent() => [ FaqSection( title: (context) => context.l10n.faq_section_title_pgx, questions: [ @@ -70,6 +45,14 @@ final faqContent = [ question: context.l10n.faq_question_pgx_why, answer: context.l10n.faq_answer_pgx_why, ), + (context) => FaqTextAnswerQuestion( + question: context.l10n.faq_question_adr_factors, + answer: context.l10n.faq_answer_adr_factors, + ), + (context) => FaqTextAnswerQuestion( + question: context.l10n.faq_question_guidelines_are_developing, + answer: context.l10n.faq_answer_guidelines_are_developing, + ), (context) => FaqWidgetAnswerQuestion( question: context.l10n.faq_question_genetics_info, answer: Column( @@ -81,12 +64,24 @@ final faqContent = [ text: geneticInformationUrl.toString(), onTap: openFurtherGeneticInformation, ), + SizedBox(height: PharMeTheme.smallSpace * 0.5), + Text('\n${context.l10n.consult_text}'), ], ), ), - (context) => FaqTextAnswerQuestion( + (context) => FaqWidgetAnswerQuestion( question: context.l10n.faq_question_which_medications, - answer: context.l10n.faq_answer_which_medications, + answer: Column( + children: [ + Text(context.l10n.faq_answer_which_medications), + SizedBox(height: PharMeTheme.smallSpace * 0.5), + UnorderedList( + context.l10n.faq_answer_which_medications_examples + .split('; ') + .map((example) => example.capitalize()).toList(), + ), + ], + ), ), (context) => FaqWidgetAnswerQuestion( question: context.l10n.faq_question_phenoconversion, @@ -94,15 +89,9 @@ final faqContent = [ crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.l10n.faq_answer_phenoconversion), - SizedBox(height: PharMeTheme.smallSpace * 0.5), - _getPhenoconversionString( - strongDrugInhibitors, - context.l10n.faq_strong_inhibitors, - ), - SizedBox(height: PharMeTheme.smallSpace * 0.5), - _getPhenoconversionString( - moderateDrugInhibitors, - context.l10n.faq_moderate_inhibitors, + SizedBox(height: PharMeTheme.smallSpace), + ...inhibitableGenes.map( + (geneName) => GeneModulatorList(geneName: geneName).widget, ), ], ), diff --git a/app/lib/faq/pages/faq.dart b/app/lib/faq/pages/faq.dart index b5f340cba..9802a0280 100644 --- a/app/lib/faq/pages/faq.dart +++ b/app/lib/faq/pages/faq.dart @@ -2,11 +2,26 @@ import '../../common/module.dart'; import 'content.dart'; @RoutePage() -class FaqPage extends StatelessWidget { +class FaqPage extends HookWidget { const FaqPage({super.key}); @override Widget build(BuildContext context) { + final faqContent = getFaqContent(); + final expandedCards = useState>({}); + final expandQuestion = useState(null); + if (expandQuestion.value != null) { + final questionKey = GlobalKey(); + expandedCards.value[expandQuestion.value!] = questionKey; + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (expandQuestion.value != null) { + _scrollToSelectedContent( + key: expandedCards.value[expandQuestion.value]!, + ); + expandQuestion.value = null; + } + }); return PopScope( canPop: false, child: pageScaffold( @@ -14,14 +29,23 @@ class FaqPage extends StatelessWidget { canNavigateBack: false, body: [ Padding( - padding: const EdgeInsets.all(PharMeTheme.smallSpace), + padding: const EdgeInsets.only( + left: PharMeTheme.smallSpace, + right: PharMeTheme.smallSpace, + bottom: PharMeTheme.smallSpace, + ), child: Column( key: Key('questionsColumn'), crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: PharMeTheme.smallSpace), ...faqContent.flatMap((faqSection) => - _buildTopic(context, faqSection)), + _buildTopic( + context, + faqSection, + expandedCards, + expandQuestion, + faqContent, + )), ..._buildTopicHeader( context.l10n.more_page_contact_us, addSpace: true, @@ -55,12 +79,16 @@ class FaqPage extends StatelessWidget { List _buildTopic( BuildContext context, FaqSection faqSection, + ValueNotifier> expandedCards, + ValueNotifier expandQuestion, + List faqContent, ) { final isFirst = faqContent.indexOf(faqSection) == 0; return [ ..._buildTopicHeader(faqSection.title(context), addSpace: !isFirst), ...faqSection.questions.map( - (questionBuilder) => _buildQuestion(context, questionBuilder) + (questionBuilder) => + _buildQuestion(context, questionBuilder, expandedCards, expandQuestion) ) ]; } @@ -68,35 +96,45 @@ class FaqPage extends StatelessWidget { Widget _buildQuestion( BuildContext context, FaqQuestionBuilder questionBuilder, + ValueNotifier> expandedCards, + ValueNotifier expandQuestion, ) { - final key = GlobalKey(); final question = questionBuilder(context); + final key = expandedCards.value[question.question]; + final expanded = expandedCards.value.containsKey(question.question); return _buildQuestionCard( key: key, - child: Theme( - data: Theme.of(context).copyWith( - dividerColor: Colors.transparent, + child: PrettyExpansionTile( + initiallyExpanded: expanded, + title: Text( + question.question, + style: expanded + ? PharMeTheme.textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ) + : null, ), - child: ExpansionTile( - title: Text(question.question), - iconColor: PharMeTheme.iconColor, - collapsedIconColor: PharMeTheme.iconColor, - onExpansionChanged: (value) { - if (value) _scrollToSelectedContent(key: key); - }, - children: [ - ListTile( - contentPadding: EdgeInsets.only( - left: PharMeTheme.mediumSpace, - right: PharMeTheme.mediumSpace, - bottom: PharMeTheme.smallSpace, - ), - title: question is FaqTextAnswerQuestion - ? Text(question.answer) - : question.answer, + onExpansionChanged: (value) { + if (value) { + expandQuestion.value = question.question; + } else { + expandedCards.value = expandedCards.value.filterKeys( + (questionTitle) => questionTitle != question.question + ); + } + }, + children: [ + ListTile( + contentPadding: EdgeInsets.only( + left: PharMeTheme.mediumSpace, + right: PharMeTheme.mediumSpace, + bottom: PharMeTheme.smallSpace, ), - ], - ), + title: question is FaqTextAnswerQuestion + ? Text(question.answer) + : question.answer, + ), + ], ), ); } @@ -104,11 +142,11 @@ class FaqPage extends StatelessWidget { void _scrollToSelectedContent({required GlobalKey key}) { final keyContext = key.currentContext; if (keyContext != null) { - Future.delayed(Duration(milliseconds: 200)).then((value) { + Future.delayed(Duration(milliseconds: 100)).then((value) { Scrollable.ensureVisible( // ignore: use_build_context_synchronously keyContext, - duration: Duration(milliseconds: 200), + duration: Duration(milliseconds: 500), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); }); diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 9f4155df9..cbeb36f9e 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -8,6 +8,8 @@ "@action_understood": {}, "action_back_to_app": "Back to app", "@action_back_to_app": {}, + "action_proceed_to_app": "Proceed to app", + "@action_proceed_to_app": {}, "action_finish": "Finish", "@action_finish": {}, @@ -53,10 +55,10 @@ "@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": "When you proceed to the next step of the app setup, you will not be able to come back to this page.\n\nYou can always change your current medications 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_description": "You can edit the medications you are currently taking below.", + "@drug_selection_description": {}, "drug_selection_no_drugs_loaded": "No medications loaded", "@drug_selection_no_drugs_loaded": {}, @@ -64,8 +66,10 @@ "@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": "Further medications", "@drug_list_subheader_other_drugs": {}, + "list_subheader_postfix": "with clinical PGx guidelines", + "@list_subheader_postfix": {}, "err_could_not_retrieve_access_token": "An unexpected error occurred while logging in", "@err_could_not_retrieve_access_token": {}, @@ -88,6 +92,16 @@ "general_not_tested": "Not tested", "@general_not_tested": {}, + "indeterminate_result_tooltip": "This means that your gene test did not provide a meaningful outcome for {geneName}. This happens when the impact of your gene result on medications is uncertain.", + "@indeterminate_result_tooltip": { + "placeholders": { + "geneName": { + "type": "String", + "example": "CYP2C19" + } + } + }, + "warning_level_green": "Standard precautions", "@warning_level_green": {}, "warning_level_missing": "Standard precautions (incomplete data)", @@ -97,13 +111,15 @@ "warning_level_red": "Consider alternatives", "@warning_level_red": {}, + "search_content_explanation": "Get an overview on your PGx tests results per medication. Tap on a medication so see what your result means and what to do.", + "@search_content_explanation": {}, "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": "Taking medications with an {indicatorName} ({indicator}) can affect how your body processes and responds to certain medications", "@search_page_indicator_explanation": { "placeholders": { "indicatorName": { @@ -118,34 +134,30 @@ }, "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": "No medications found that match your search term{amendment}. Clinical dosing may apply, consult your pharmacist or doctor for more information.", "@search_no_drugs": { "placeholders": { "amendment": { "type": "String", - "example": " or filters right to the search bar" + "example": " or filters" } } }, - "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_main_disclaimer_text": "Never stop taking or change the dose of your medications without consulting your pharmacist or doctor.", + "@drugs_page_main_disclaimer_text": {}, + "drugs_page_puzzle_disclaimer_text": "The information from PharMe is like one puzzle piece in the big picture of how well a medicine works for you.\n\nThe information provided in this app is based ONLY on your DNA and certain medications that may interact with your genetic result. Other important factors like weight, age, pre-existing conditions, and further medication interactions are not considered.", + "@drugs_page_puzzle_disclaimer_text": {}, + "drugs_page_is_inhibitor": "Taking {drugName} will slow down the activity of {genes}. This will affect how your body processes and responds to associated medications.", "@drugs_page_is_inhibitor": { "placeholders": { - "drugName": { - "type": "String", - "example": "bupropion" - }, - "genes": { + "drugName": { "type": "String", - "example": "CYP2D6, CYP2C19" - } + "example": "bupropion" + }, + "genes": { + "type": "String", + "example": "CYP2D6 and CYP2C19" + } } }, @@ -157,12 +169,16 @@ "@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": "{salutation} taking one or more medications that slow down the activity of {geneName}. This will affect how {salutationGenitive} body processes and responds to associated medications.", "@inhibitor_message": { "placeholders": { "salutation": { "type": "String", - "example": "you" + "example": "you are" + }, + "geneName": { + "type": "String", + "example": "CYP2D6" }, "salutationGenitive": { "type": "String", @@ -170,7 +186,7 @@ } } }, - "inhibitors_consequence_adapted": "{salutationGenitive} {geneName} phenotype was adapted from {originalPhenotype}.", + "inhibitors_consequence_adapted": "{salutationGenitive} {geneName} phenotype was changed from “{originalPhenotype}” to “{currentPhenotype}”; this is because you are currently taking one or more strong {geneName} inhibitors (see below).", "@inhibitors_consequence_adapted": { "placeholders": { "salutationGenitive": { @@ -184,10 +200,14 @@ "originalPhenotype": { "type": "String", "example": "Normal Metabolizer" + }, + "currentPhenotype": { + "type": "String", + "example": "Poor Metabolizer" } } }, - "inhibitors_consequence_not_adapted": "{salutationGenitive} {geneName} phenotype was not adapted but may need to be.", + "inhibitors_consequence_not_adapted": "{salutationGenitive} {geneName} phenotype was not adapted but may need to be; this is because your are currently taking one or more moderate {geneName} inhibitors (see below).", "@inhibitors_consequence_not_adapted": { "placeholders": { "salutationGenitive": { @@ -200,15 +220,6 @@ } } }, - "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": {}, @@ -227,10 +238,21 @@ "@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": "Are you sure you want to change your current medications?", "@drugs_page_active_warn_header": {}, - "drugs_page_active_warn": "This can interact with your results for other medications.", - "@drugs_page_active_warn": {}, + "drugs_page_active_warn": "{drugName} will affect how your body processes and responds to medications associated with {geneName}.\n\nSee {geneName} under “Genes” for a list of all associated medications.", + "@drugs_page_active_warn": { + "placeholders": { + "drugName": { + "type": "String", + "example": "Mirabegron" + }, + "geneName": { + "type": "String", + "example": "CYP2D6" + } + } + }, "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.", @@ -255,16 +277,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_implication_description": "What your result means: ", + "@drugs_page_implication_description": {}, "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": "This is your PGx test report. Tap on a gene name for more details on your results and a list of associated 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": "Phenotypes followed by an {indicatorName} ({indicator}) might be adjusted based on interactions with medications you are currently taking.", "@report_page_indicator_explanation": { "placeholders": { "indicatorName": { @@ -277,8 +299,59 @@ } } }, - "report_no_result_genes": "Genes with no result", - "@report_no_result_genes": {}, + "report_description_prefix": "Gene report for", + "@report_description_prefix": {}, + "report_current_medications": "current medication interactions", + "@report_current_medications": {}, + "report_all_medications": "all medication interactions", + "@report_all_medications": {}, + "report_gene_number": "{geneNumber, plural, =1{1 gene} other{{geneNumber} genes}}", + "@report_gene_number": { + "placeholders": { + "geneNumber": { + "type": "int", + "example": "19" + } + } + }, + "report_medication_number": "{medicationNumber, plural, =1{1 medication} other{{medicationNumber} medications}}", + "@report_medication_number": { + "placeholders": { + "medicationNumber": { + "type": "int", + "example": "3" + } + } + }, + "show_all_dropdown_text": "Having trouble finding a {item}? Use the dropdown button (▾) {position} to show all {items}.", + "@show_all_dropdown_text": { + "placeholders": { + "item": { + "type": "String", + "example": "gene" + }, + "position": { + "type": "String", + "example": "below the current medications" + }, + "items": { + "type": "String", + "example": "medications with clinical PGx guidelines" + } + } + }, + "report_dropdown_position": "below the current medication interaction report", + "@report_dropdown_position": {}, + "medications_dropdown_position": "below the current medications", + "@medications_dropdown_position": {}, + "report_show_all_dropdown_item": "gene", + "@report_show_all_dropdown_item": {}, + "report_show_all_dropdown_items": "genes with clinical PGx guidelines", + "@report_show_all_dropdown_items": {}, + "drugs_show_all_dropdown_item": "medication", + "@drugs_show_all_dropdown_item": {}, + "drugs_show_all_dropdown_items": "medications with clinical PGx guidelines", + "@drugs_show_all_dropdown_items": {}, "gene_page_headline": "{gene} report", "@gene_page_headline": { @@ -309,13 +382,13 @@ }, "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": "The genotype tells you about your DNA and which version of a gene you have.\n\nYou carry two copies for one gene, called “alleles”, each inherited from one of your parents.", "@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": "The phenotype describes how your DNA affects your body.\n\nIn pharmacogenomics, it usually shows if a gene version might cause a bad reaction to a medicine or how fast your body processes a drug.", "@gene_page_phenotype_tooltip": {}, - "gene_page_relevant_drugs": "Implicated medications", + "gene_page_relevant_drugs": "Associated 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": { @@ -342,7 +415,7 @@ "@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": "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": {}, @@ -427,24 +500,28 @@ "@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": "You are almost done with setting up PharMe! We will now quickly guide you through the app's main functions.\nYou can switch between PharMe's main screens using the bottom navigation bar: “Medications”, “Genes”, “FAQ”, and “More”.", "@tutorial_app_tour_1_body": {}, + "tutorial_app_tour_1_body_bold": "If you want to re-watch this app tour later, you can always do so under “More” > “Repeat app tour”.", + "@tutorial_app_tour_1_body_bold": {}, "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": "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.\nBased on your personal gene results, all 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": "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 save a PDF 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": "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.\nTo learn more about genes and how they interact with medications, refer to the FAQ or “More” > “Learn about genetics”.", "@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": "Under “FAQ”, you will find a list of frequently asked questions and further resources.\nUnder “More”, you can find other useful features, such as editing your current medications and getting additional information about the app.", "@tutorial_app_tour_5_body": {}, + "tutorial_app_tour_5_body_bold": "Here you can also find a contact form; please reach out if you have any questions while using the app!", + "@tutorial_app_tour_5_body_bold": {}, "onboarding_get_started": "Get started", "@onboarding_get_started": {}, @@ -454,47 +531,64 @@ "@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": "Your genes affect your health more than you might think, including how you respond to medications.\n\nIn fact, over 90 percent of people can have unexpected reactions to medicines.\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": "Genome power unlocked to improve your 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": "PharMe tells you if your genes make you more likely to have an unexpected reaction to a medication.\n\nThis way, you can avoid medications that might not work or could cause 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": "Tailor-made for you", "@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": "PharMe matches your genetic information with what scientists know about how certain genes and medications interact.\n\nThe information provided by PharMe is based on proven guidelines from trusted organizations like 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": "We care about your privacy", "@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": "Once you download your genetic information, it is stored safely on your phone.\n\nOur system does not collect any information about you, including your name or DNA.", "@onboarding_5_text": {}, + "included_content_medications": "medications", + "@included_content_medications": {}, + "included_content_genes": "genes", + "@included_content_genes": {}, + "included_content_disclaimer_text": "PharMe only includes {content} with clinical PGx guidelines from CPIC and the FDA.", + "@included_content_disclaimer_text": { + "placeholders": { + "content": { + "type": "String", + "example": "medications" + } + } + }, + "included_content_addition": "While only to a small percentage of medications have PGx guidelines, they are among the most commonly prescribed ones.\n\nIf you cannot find a medication in PharMe, there may not be enough evidence for meaningful gene interactions.", + "@included_content_addition": {}, "more_page_account_settings": "Settings", "@more_page_account_settings": {}, + "more_page_edit_current_medications": "Edit current medications", + "@more_page_edit_current_medications": {}, "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": "Your genetic data will be deleted from your phone and it might not be possible to import it again.\n\nAfter deleting the data, you will be asked to close the app.", + "delete_data_restart_title": "All data deleted", + "@delete_data_restart_title": {}, + "delete_data_restart_text": "Please close the app now. To setup PharMe again, please restart the app.", + "@delete_data_restart_text": {}, "@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": "Repeat app intro", "@more_page_onboarding": {}, "more_page_app_tour": "Repeat app tour", "@more_page_app_tour": {}, @@ -510,7 +604,7 @@ "@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": "Help & feedback", "@more_page_help_and_feedback": {}, "more_page_genetic_information": "Learn about genetics (MedlinePlus)", "@more_page_genetic_information": {}, @@ -537,60 +631,54 @@ "@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": "Pharmacogenomics (PGx) is the study of how your genes (DNA) affect your response to medications.\n\nYour DNA is the blueprint that tells your body how to make different enzymes. These enzymes play an important role in how your body processes and responds to medications.\n\nSome enzymes help transport medications through your body, while others make medications more or less strong. Depending on the versions of the genes you have, you may make more or less of certain enzymes, or they function more or less well.", "@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": "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.\n\nHowever, this information ONLY is like one puzzle piece in the big picture of how well a medicine works for you.", "@faq_answer_pgx_why": {}, - "faq_question_genetics_info": "Where can I find out more about genetics in general?", + "faq_question_adr_factors": "Which factors can lead to adverse drug reactions?", + "@faq_question_adr_factors": {}, + "faq_answer_adr_factors": "An adverse drug reaction is a harmful response to a medication.\n\nWhen your immune system mistakes a medication for a threat, it can trigger an allergic reaction, causing symptoms from stomach upset over hives to severe anaphylaxis.\n\nYour genes also play an important role in how your body processes and responds to medications.\n\nWhile some reactions are predictable, others are rare and unexpected. These rare reactions, called idiosyncratic reactions, are individual reactions with not (yet) understood mechanisms that usually do not occur in most patients.\n\nYou can also have multiple types of adverse reactions at the same time, for example a genetic difference that affects how your body processes a drug AND an allergic reaction.\n\nConsult your pharmacist or doctor for more information.", + "@faq_answer_adr_factors": {}, + "faq_question_guidelines_are_developing": "Do my pharmacogenomic results in PharMe cover all known pharmacogenomic causes of adverse reactions?", + "@faq_question_guidelines_are_developing": {}, + "faq_answer_guidelines_are_developing": "No. Scientists are still discovering new genetic differences that affect medications.\n\nFurther, different labs might interpret genetic test results differently because this science is still developing.\n\nPharMe relies on well-established CPIC® and FDA guidelines to deliver standardized and up-to-date pharmacogenomic information to you.\n\nPlease also note that pharmacogenomics is only one factor that can cause adverse reactions, like like one puzzle piece in the big picture of how well a medicine works for you.", + "@faq_answer_guidelines_are_developing": {}, + "faq_question_genetics_info": "I need help with understanding what my gene results mean.", "@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": "Your gene results consist of two types of information:\n\nThe genotype tells you about your DNA and which version of a gene you have. You carry two copies for one gene, called “alleles”, each inherited from one of your parents.\n\nThe phenotype explains how your DNA affects your body. In pharmacogenomics, it usually shows if a gene version might cause a bad reaction to a medicine or how fast your body processes a drug. Processing here means your body either breaks down the medicine into weaker forms or changes it into a stronger form.\n\nTo 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": "Which medications are included in PharMe?", "@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": "PharMe includes medications that are known to have meaningful interactions with genes. This knowledge is science-based, according to CPIC® and FDA guidelines.\n\nExamples of medication classes with known gene interactions include:", + "faq_answer_which_medications_examples": "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); anti-cancer medications (like fluorouracil and irinotecan)", + "@faq_answer_which_medications_examples": { + "description": "A list separated by semicolon and space; this can be used to split the text in the code to format a list" + }, "@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": "Certain medications known as modulators can change how your body responds to medications. Typically, modulators function in two ways: they either slow down the activity of a gene (known as inhibitors) or speed up the activity of a gene (known as inducers).\n\nPharMe currently only supports changing your gene result if you are taking strong modulators. For moderate modulators, a warning is shown that the gene result may need to be adapted.\n\nThe following modulators 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": "We recommend that you share your results with your pharmacists, doctors, and close family members such as parents, siblings, and children.\n\nTo create a PDF that you can share with a healthcare professional, go to a medication page, and use the share button in the upper right corner.", "@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": "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 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": "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": {}, @@ -605,5 +693,32 @@ "@faq_answer_data_security": {}, "faq_contact_us": "Do you have unanswered questions or feedback? Contact us", - "@faq_contact_us": {} + "@faq_contact_us": {}, + + "strong_inhibitors_description": "{inhibitorNumber, plural, =1{Strong {geneName} inhibitor:} other{Strong {geneName} inhibitors:}}", + "@strong_inhibitors_description": { + "placeholders": { + "inhibitorNumber": { + "type": "int", + "example": "2" + }, + "geneName": { + "type": "String", + "example": "CYP2D6" + } + } + }, + "moderate_inhibitors_description": "{inhibitorNumber, plural, =1{Moderate {geneName} inhibitor:} other{Moderate {geneName} inhibitors:}}", + "@moderate_inhibitors_description": { + "placeholders": { + "inhibitorNumber": { + "type": "int", + "example": "2" + }, + "geneName": { + "type": "String", + "example": "CYP2D6" + } + } + } } diff --git a/app/lib/main/module.dart b/app/lib/main/module.dart index d3d6986ac..e3a8c5599 100644 --- a/app/lib/main/module.dart +++ b/app/lib/main/module.dart @@ -3,8 +3,19 @@ import '../common/module.dart'; // For generated route export 'pages/main.dart'; -AutoRoute mainRoute({ required List children }) => AutoRoute( - path: '/main', - page: MainRoute.page, - children: children, -); \ No newline at end of file +const _path = '/main'; +const _page = MainRoute.page; + +AutoRoute mainRoute({ required List children }) => + MetaData.instance.tutorialDone ?? false + ? AutoRoute( + path: _path, + page: _page, + children: children, + ) + : CustomRoute( + path: _path, + page: _page, + children: children, + transitionsBuilder: TransitionsBuilders.noTransition, + ) ; diff --git a/app/lib/main/pages/main.dart b/app/lib/main/pages/main.dart index f5bf77bf9..f0f0774b0 100644 --- a/app/lib/main/pages/main.dart +++ b/app/lib/main/pages/main.dart @@ -1,5 +1,3 @@ -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; - import '../../common/module.dart'; class TabRouteDefinition { @@ -18,12 +16,12 @@ List getTabRoutesDefinition(BuildContext context) { TabRouteDefinition( pageRouteInfo: SearchRoute(), label: context.l10n.nav_drugs, - icon: FontAwesomeIcons.pills, + icon: medicationsIcon, ), TabRouteDefinition( pageRouteInfo: ReportRoute(), label: context.l10n.nav_report, - icon: FontAwesomeIcons.dna, + icon: genesIcon, ), TabRouteDefinition( pageRouteInfo: FaqRoute(), diff --git a/app/lib/more/pages/more.dart b/app/lib/more/pages/more.dart index ca4e9a6d7..84357bd33 100644 --- a/app/lib/more/pages/more.dart +++ b/app/lib/more/pages/more.dart @@ -1,5 +1,7 @@ +import 'dart:io'; + import '../../common/module.dart'; -import '../utils.dart'; +import '../../common/utilities/hive_utils.dart'; @RoutePage() class MorePage extends StatelessWidget { @@ -18,7 +20,7 @@ class MorePage extends StatelessWidget { useLine: false, ), _buildSettingsItem( - title: context.l10n.drug_selection_header, + title: context.l10n.more_page_edit_current_medications, onTap: () => context.router.push( DrugSelectionRoute(concludesOnboarding: false) ), @@ -31,14 +33,12 @@ class MorePage extends StatelessWidget { ), ), SubheaderDivider( - text: context.l10n.more_page_app_information, + text: context.l10n.more_page_help_and_feedback, useLine: false, ), - _buildSettingsItem( - title: context.l10n.more_page_onboarding, - onTap: () => - context.router.push(OnboardingRoute(isRevisiting: true)), - ), + _buildSettingsItem( + title: context.l10n.more_page_contact_us, + onTap: () => sendEmail(context)), _buildSettingsItem( title: context.l10n.more_page_app_tour, onTap: () async => showAppTour( @@ -47,6 +47,18 @@ class MorePage extends StatelessWidget { revisiting: true, ), ), + _buildSettingsItem( + title: context.l10n.more_page_onboarding, + onTap: () => + context.router.push(OnboardingRoute(isRevisiting: true)), + ), + _buildSettingsItem( + title: context.l10n.more_page_genetic_information, + onTap: openFurtherGeneticInformation), + SubheaderDivider( + text: context.l10n.more_page_app_information, + useLine: false, + ), _buildSettingsItem( title: context.l10n.more_page_about_us, onTap: () => context.router.push(AboutRoute()), @@ -59,16 +71,6 @@ class MorePage extends StatelessWidget { title: context.l10n.more_page_terms_and_conditions, onTap: () => context.router.push(TermsRoute()), ), - SubheaderDivider( - text: context.l10n.more_page_help_and_feedback, - useLine: false, - ), - _buildSettingsItem( - title: context.l10n.more_page_genetic_information, - onTap: openFurtherGeneticInformation), - _buildSettingsItem( - title: context.l10n.more_page_contact_us, - onTap: () => sendEmail(context)), ] ), ); @@ -134,7 +136,27 @@ class DeleteDataDialog extends HookWidget { ? () async { await deleteAllAppData(); // ignore: use_build_context_synchronously - await overwriteRoutes(context, nextPage: LoginRoute()); + await context.router.root.maybePop(); + // ignore: use_build_context_synchronously + await showAdaptiveDialog( + // ignore: use_build_context_synchronously + context: context, + builder: (context) => DialogWrapper( + title: context.l10n.delete_data_restart_title, + content: Column( + children: [ + SizedBox(height: PharMeTheme.smallSpace), + Text(context.l10n.delete_data_restart_text), + ], + ), + actions: [ + DialogAction( + text: context.l10n.error_close_app, + onPressed: () => exit(0), + ), + ], + ), + ); } : null, text: context.l10n.action_continue, diff --git a/app/lib/more/utils.dart b/app/lib/more/utils.dart deleted file mode 100644 index 39f3f3415..000000000 --- a/app/lib/more/utils.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:io'; - -import 'package:path_provider/path_provider.dart'; - -import '../common/module.dart'; - -Future deleteAllAppData() async { - await _deleteCacheDir(); - await _deleteAppDir(); - await UserData.erase(); - await MetaData.erase(); - await DrugsWithGuidelines.erase(); -} - -// The folders themself cannot be deleted on iOS, therefore delete all content -// inside the folders -void _deleteFolderContent(Directory directory) { - if (!directory.existsSync()) return; - for (final item in directory.listSync()) { - item.deleteSync(recursive: true); - } -} - -Future _deleteCacheDir() async { - final tempDir = await getTemporaryDirectory(); - _deleteFolderContent(tempDir); -} - -Future _deleteAppDir() async { - final appDocDir = await getApplicationDocumentsDirectory(); - _deleteFolderContent(appDocDir); -} diff --git a/app/lib/onboarding/pages/onboarding.dart b/app/lib/onboarding/pages/onboarding.dart index 720ed5914..6309a1211 100644 --- a/app/lib/onboarding/pages/onboarding.dart +++ b/app/lib/onboarding/pages/onboarding.dart @@ -1,3 +1,5 @@ +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + import '../../../common/module.dart' hide MetaData; import '../../common/models/metadata.dart'; @@ -17,9 +19,8 @@ class OnboardingPage extends HookWidget { 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, + bottom: DisclaimerCard( + text: context.l10n.drugs_page_main_disclaimer_text, ), ), OnboardingSubPage( @@ -37,8 +38,10 @@ class OnboardingPage extends HookWidget { header: context.l10n.onboarding_3_header, text: context.l10n.onboarding_3_text, color: PharMeTheme.sinaiPurple, - child: DisclaimerCard( - text: context.l10n.onboarding_3_disclaimer, + bottom: DisclaimerCard( + icon: FontAwesomeIcons.puzzlePiece, + iconPadding: EdgeInsets.all(PharMeTheme.smallSpace * 0.5), + text: context.l10n.drugs_page_puzzle_disclaimer_text, ), ), OnboardingSubPage( @@ -48,6 +51,17 @@ class OnboardingPage extends HookWidget { header: context.l10n.onboarding_4_header, text: context.l10n.onboarding_4_already_tested_text, color: Colors.grey.shade600, + bottom: DisclaimerCard( + iconWidget: IncludedContentIcon( + type: ListInclusionDescriptionType.medications, + color: PharMeTheme.onSurfaceText, + size: OnboardingDimensions.iconSize, + ), + iconPadding: EdgeInsets.all(PharMeTheme.smallSpace * 0.5), + text: '${context.l10n.included_content_disclaimer_text( + context.l10n.included_content_medications, + )}\n\n${context.l10n.included_content_addition}', + ), ), OnboardingSubPage( availableHeight: @@ -295,7 +309,8 @@ class OnboardingSubPage extends HookWidget { required this.text, required this.color, required this.availableHeight, - this.child, + this.top, + this.bottom, }); final String illustrationPath; @@ -304,7 +319,8 @@ class OnboardingSubPage extends HookWidget { final String text; final double availableHeight; final Color color; - final Widget? child; + final Widget? top; + final Widget? bottom; double? _getContentHeight(GlobalKey contentKey) { return contentKey.currentContext?.size?.height; @@ -331,6 +347,21 @@ class OnboardingSubPage extends HookWidget { return scrollController.offset >= maxScrollOffset; } + double? _getRelativeScrollPosition( + GlobalKey contentKey, + ScrollController scrollController, + ) { + final maxScrollOffset = _getMaxScrollOffset(contentKey); + if (maxScrollOffset == null) return null; + final relativePosition = + 1 - (maxScrollOffset - scrollController.offset) / maxScrollOffset; + return relativePosition < 0 + ? 0 + : relativePosition > 1 + ? 1 + : relativePosition; + } + @override Widget build(BuildContext context) { const scrollbarThickness = 6.5; @@ -339,6 +370,7 @@ class OnboardingSubPage extends HookWidget { const imageHeight = 175.0; final contentKey = GlobalKey(); final showScrollIndicatorButton = useState(false); + final scrollIndicatorButtonOpacity = useState(1); final scrollController = ScrollController(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -350,6 +382,11 @@ class OnboardingSubPage extends HookWidget { scrollController.addListener(() { final hideButton = _scrolledToEnd(contentKey, scrollController) ?? false; showScrollIndicatorButton.value = !hideButton; + final relativeScrollPosition = + _getRelativeScrollPosition(contentKey, scrollController); + if (relativeScrollPosition != null) { + scrollIndicatorButtonOpacity.value = 1 - relativeScrollPosition; + } }); return Stack( @@ -397,15 +434,19 @@ class OnboardingSubPage extends HookWidget { maxLines: 2, ), SizedBox(height: PharMeTheme.mediumToLargeSpace), + if (top != null) ...[ + top!, + SizedBox(height: PharMeTheme.mediumSpace), + ], Text( text, style: PharMeTheme.textTheme.bodyLarge!.copyWith( color: Colors.white, ), ), - if (child != null) ...[ + if (bottom != null) ...[ SizedBox(height: PharMeTheme.mediumSpace), - child!, + bottom!, ], ]), // Empty widget for spaceBetween in this column to work properly @@ -417,25 +458,28 @@ class OnboardingSubPage extends HookWidget { ), if (showScrollIndicatorButton.value) Positioned( bottom: 0, - child: IconButton( + child: Opacity( + opacity: scrollIndicatorButtonOpacity.value, + 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, + 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; + }, ), - onPressed: () async { - await scrollController.animateTo( - _getMaxScrollOffset(contentKey)!, - duration: Duration(milliseconds: 500), - curve: Curves.linearToEaseOut, - ); - showScrollIndicatorButton.value = false; - }, - ) + ), ), ], ); @@ -445,15 +489,19 @@ class OnboardingSubPage extends HookWidget { class DisclaimerCard extends StatelessWidget { const DisclaimerCard({ this.icon, + this.iconWidget, required this.text, this.secondLineText, this.onClick, + this.iconPadding, }); - final Icon? icon; + final IconData? icon; + final Widget? iconWidget; final String text; final String? secondLineText; final GestureTapCallback? onClick; + final EdgeInsets? iconPadding; @override Widget build(BuildContext context) { @@ -467,7 +515,14 @@ class DisclaimerCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - icon ?? Icon(Icons.warning_rounded, size: 32), + Padding( + padding: iconPadding ?? EdgeInsets.zero, + child: iconWidget ?? Icon( + icon ?? Icons.warning_rounded, + size: OnboardingDimensions.iconSize, + color: PharMeTheme.onSurfaceText, + ), + ), SizedBox(width: PharMeTheme.smallSpace), Expanded( child: Column( diff --git a/app/lib/report/pages/gene.dart b/app/lib/report/pages/gene.dart index 823a6a7b2..85f88df8f 100644 --- a/app/lib/report/pages/gene.dart +++ b/app/lib/report/pages/gene.dart @@ -5,7 +5,7 @@ import '../../drug/widgets/module.dart'; @RoutePage() class GenePage extends HookWidget { - GenePage(this.genotypeResult) + GenePage(this.genotypeResult, {this.initiallyExpandFurtherMedications = false}) : cubit = DrugListCubit( initialFilter: FilterState.forGenotypeKey(genotypeResult.key.value), @@ -13,6 +13,7 @@ class GenePage extends HookWidget { final GenotypeResult genotypeResult; final DrugListCubit cubit; + final bool initiallyExpandFurtherMedications; @override Widget build(BuildContext context) { @@ -20,107 +21,94 @@ class GenePage extends HookWidget { builder: (context, activeDrugs, child) => BlocProvider( create: (context) => cubit, child: BlocBuilder( - builder: (context, state) => pageScaffold( + builder: (context, state) => unscrollablePageScaffold( title: context.l10n.gene_page_headline(genotypeResult.geneDisplayString), - body: [ - Padding( - padding: EdgeInsets.symmetric( - horizontal: PharMeTheme.smallToMediumSpace, - vertical: PharMeTheme.mediumSpace - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: DrugList( + state: state, + activeDrugs: activeDrugs, + noDrugsMessage: context.l10n.gene_page_no_relevant_drugs, + initiallyExpandFurtherMedications: initiallyExpandFurtherMedications, + buildContainer: ({ + children, + indicator, + noDrugsMessage, + showInactiveDrugs, + }) => + Column( children: [ - SubHeader( - context.l10n.gene_page_your_result( - genotypeResult.geneDisplayString, + Padding( + padding: EdgeInsets.only( + left: PharMeTheme.smallToMediumSpace, + right: PharMeTheme.smallToMediumSpace, + top: PharMeTheme.smallSpace, + bottom: PharMeTheme.smallSpace, ), - tooltip: context.l10n - .gene_page_name_tooltip( - genotypeResult.gene, - ), - ), - SizedBox(height: PharMeTheme.smallToMediumSpace), - RoundedCard( - radius: PharMeTheme.mediumSpace, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Table( - columnWidths: Map.from({ - 0: IntrinsicColumnWidth(), - 1: IntrinsicColumnWidth(flex: 1), - }), - children: [ - _buildRow( - context.l10n.gene_page_genotype, - genotypeResult.variantDisplayString(context), - tooltip: context.l10n.gene_page_genotype_tooltip - ), - _buildPhenotypeRow(context), - ], + SubHeader( + context.l10n.gene_page_your_result( + genotypeResult.geneDisplayString, + ), + tooltip: context.l10n + .gene_page_name_tooltip( + genotypeResult.gene, + ), ), - if (isInhibited(genotypeResult, drug: null)) ...[ - SizedBox(height: PharMeTheme.smallSpace), - buildDrugInteractionInfo( - context, - [genotypeResult], - drug: null, + SizedBox(height: PharMeTheme.smallToMediumSpace), + RoundedCard( + radius: PharMeTheme.mediumSpace, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildGeneResults(context), + if (isInhibited(genotypeResult, drug: null)) ...[ + SizedBox(height: PharMeTheme.smallSpace), + PhenoconversionExplanation( + inhibitedGenotypes: [genotypeResult], + drugName: null, + ), + ] + ], + )), + SizedBox(height: PharMeTheme.mediumToLargeSpace), + SubHeader( + context.l10n.gene_page_relevant_drugs, + tooltip: context.l10n.gene_page_relevant_drugs_tooltip( + genotypeResult.geneDisplayString ), - ] - ], - )), - SizedBox(height: PharMeTheme.smallToMediumSpace), - SubHeader( - context.l10n.gene_page_relevant_drugs, - tooltip: context.l10n.gene_page_relevant_drugs_tooltip( - genotypeResult.geneDisplayString + ), + ], ), ), - SizedBox(height: PharMeTheme.smallSpace), - DrugList( - state: state, - activeDrugs: activeDrugs, - noDrugsMessage: context.l10n.gene_page_no_relevant_drugs, - ), - ], + if (children != null) scrollList(children), + if (noDrugsMessage != null) noDrugsMessage, + if (indicator != null) indicator, + ] ), - ), - ], + ), ), ), ) ); - } - TableRow _buildPhenotypeRow(BuildContext context) { - return _buildRow( - context.l10n.gene_page_phenotype, - possiblyAdaptedPhenotype(context, genotypeResult, drug: null), - tooltip: - context.l10n.gene_page_phenotype_tooltip, - ); } - - TableRow _buildRow(String key, String value, {String? tooltip}) => - TableRow(children: [ - Padding( - padding: EdgeInsets.fromLTRB(0, 4, 12, 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(key, - style: PharMeTheme.textTheme.bodyMedium! - .copyWith(fontWeight: FontWeight.bold)), - if (tooltip.isNotNullOrEmpty) ...[ - SizedBox(width: PharMeTheme.smallSpace), - TooltipIcon(tooltip!), - ], - ], - ), - ), - Padding(padding: EdgeInsets.fromLTRB(0, 4, 0, 4), child: Text(value)), - ]); + + Widget _buildGeneResults(BuildContext context) => buildTable([ + testResultTableRow( + context, + genotypeResult: genotypeResult, + key: context.l10n.gene_page_genotype, + value: genotypeResult.variantDisplayString(context), + keyTooltip: context.l10n.gene_page_genotype_tooltip, + ), + phenotypeTableRow( + context, + key: context.l10n.gene_page_phenotype, + genotypeResult: genotypeResult, + drug: null, + keyTooltip: context.l10n.gene_page_phenotype_tooltip, + ), + ]); } diff --git a/app/lib/report/pages/report.dart b/app/lib/report/pages/report.dart index b70739b4a..342e4319c 100644 --- a/app/lib/report/pages/report.dart +++ b/app/lib/report/pages/report.dart @@ -1,16 +1,34 @@ +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import '../../common/module.dart'; typedef WarningLevelCounts = Map; +enum SortOption { + alphabetical, + warningSeverity, +} + @RoutePage() -class ReportPage extends StatelessWidget { +class ReportPage extends HookWidget { + const ReportPage({@visibleForTesting this.allGenesInitiallyExpanded = false}); + + final bool allGenesInitiallyExpanded; + @override Widget build(BuildContext context) { + final allGenesExpanded = useState(allGenesInitiallyExpanded); + // Not changeable yet in UI! + final currentSortOption = useState(SortOption.alphabetical); return Consumer( builder: (context, activeDrugs, child) => - _buildReportPage(context, activeDrugs) + _buildReportPage( + context, + activeDrugs, + allGenesExpanded, + currentSortOption, + ) ); } @@ -20,14 +38,54 @@ class ReportPage extends StatelessWidget { ).values.sum(); } - Widget _buildReportPage(BuildContext context, ActiveDrugs activeDrugs) { - final userGenotypes = UserData.instance.genotypeResults!.values; + List _getAffectedDrugs( + String genotypeResultKey, + { + List? drugSubset, + }) { + final allAffectedDrugs = DrugsWithGuidelines.instance.drugs?.filter( + (drug) => drug.guidelineGenotypes.contains(genotypeResultKey) + ).toList() ?? []; + if (drugSubset != null) { + return allAffectedDrugs.filter( + (drug) => drugSubset.contains(drug.name) + ).toList(); + } + return allAffectedDrugs; + } + + Iterable _getRelevantGenotypes( + List? drugSubset, + ) { + if (UserData.instance.genotypeResults == null) return []; + final allGenotypeResults = UserData.instance.genotypeResults!.values; + if (drugSubset == null) return allGenotypeResults; + return allGenotypeResults.filter( + (genotypeResult) => _getAffectedDrugs( + genotypeResult.key.value, + drugSubset: drugSubset, + ).isNotEmpty + ); + } + + List _buildGeneCards({ + required SortOption currentSortOption, + List? drugsToFilterBy, + required bool onlyCurrentMedications, + }) { + final keyPostfix = onlyCurrentMedications + ? 'current-medications' + : 'all-medications'; + final userGenotypes = _getRelevantGenotypes( + drugsToFilterBy, + ); final warningLevelCounts = {}; for (final genotypeResult in userGenotypes) { warningLevelCounts[genotypeResult.key.value] = {}; - final affectedDrugs = DrugsWithGuidelines.instance.drugs?.filter( - (drug) => drug.guidelineGenotypes.contains(genotypeResult.key.value) - ) ?? []; + final affectedDrugs = _getAffectedDrugs( + genotypeResult.key.value, + drugSubset: drugsToFilterBy, + ); for (final warningLevel in WarningLevel.values) { warningLevelCounts[genotypeResult.key.value]![warningLevel] = affectedDrugs.filter( @@ -38,24 +96,76 @@ class ReportPage extends StatelessWidget { var sortedGenotypes = userGenotypes.sortedBy( (genotypeResult) => genotypeResult.key.value ); - final sortedWarningLevelSeverities = Set.from( - WarningLevel.values - .sortedBy((warningLevel) => warningLevel.severity) - .map((warningLevel) => warningLevel.severity) - ); - for (final severity in sortedWarningLevelSeverities) { - sortedGenotypes = sortedGenotypes.sortedByDescending((genotypeResult) => - _getSeverityCount( - warningLevelCounts[genotypeResult.key.value]!, - severity, - ), + if (currentSortOption == SortOption.warningSeverity) { + final sortedWarningLevelSeverities = Set.from( + WarningLevel.values + .sortedBy((warningLevel) => warningLevel.severity) + .map((warningLevel) => warningLevel.severity) ); + for (final severity in sortedWarningLevelSeverities) { + sortedGenotypes = sortedGenotypes.sortedByDescending((genotypeResult) => + _getSeverityCount( + warningLevelCounts[genotypeResult.key.value]!, + severity, + ), + ); + } } - final sortedGenotypesWithResults = sortedGenotypes.filter( - (genotypeResult) => !_hasNoResult(genotypeResult) + return sortedGenotypes.map((genotypeResult) => + GeneCard( + genotypeResult, + warningLevelCounts[genotypeResult.key.value]!, + key: Key('gene-card-${genotypeResult.key.value}-$keyPostfix'), + useColors: false, + onlyCurrentMedications: onlyCurrentMedications, + ) + ).toList(); + } + + Widget _maybeBuildPageIndicators( + BuildContext context, + ActiveDrugs activeDrugs, + { required bool allGenesVisible } + ) { + final drugsToFilterBy = allGenesVisible ? null : activeDrugs.names; + final relevantGenes = _getRelevantGenotypes(drugsToFilterBy); + final hasActiveInhibitors = relevantGenes.any( + (genotypeResult) => activeDrugs.names.any( + (drug) => isInhibited(genotypeResult, drug: drug) + ) ); - final sortedGenotypesWithoutResults = sortedGenotypes.filter(_hasNoResult); - final hasActiveInhibitors = activeDrugs.names.any(isInhibitor); + if (!hasActiveInhibitors && drugsToFilterBy == null) { + return SizedBox.shrink(); + } + var indicatorText = ''; + if (drugsToFilterBy != null) { + final listHelperText = context.l10n.show_all_dropdown_text( + context.l10n.report_show_all_dropdown_item, + context.l10n.report_dropdown_position, + context.l10n.report_show_all_dropdown_items, + ); + indicatorText = listHelperText; + } + if (hasActiveInhibitors) { + final inhibitorText = context.l10n.report_page_indicator_explanation( + drugInteractionIndicatorName, + drugInteractionIndicator + ); + if (indicatorText.isNotBlank) { + indicatorText = '$indicatorText\n\n$inhibitorText'; + } else { + indicatorText = inhibitorText; + } + } + return PageIndicatorExplanation(indicatorText); + } + + Widget _buildReportPage( + BuildContext context, + ActiveDrugs activeDrugs, + ValueNotifier allGenesExpanded, + ValueNotifier currentSortOption, + ) { return PopScope( canPop: false, child: unscrollablePageScaffold( @@ -67,62 +177,142 @@ class ReportPage extends StatelessWidget { children: [ PageDescription.fromText(context.l10n.report_content_explanation), scrollList( - [ - PageDescription( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.report_legend_text), - SizedBox(height: PharMeTheme.smallSpace * 0.5), - Text.rich(buildWarningLevelTextLegend(context)), - ] - ), - ), - ...sortedGenotypesWithResults.map((genotypeResult) => GeneCard( - genotypeResult, - warningLevelCounts[genotypeResult.key.value]!, - key: Key('gene-card-${genotypeResult.key.value}') - )), - if (sortedGenotypesWithoutResults.isNotEmpty) ...[ - SubheaderDivider( - text: context.l10n.report_no_result_genes, - key: Key('header-no-result'), - useLine: false, - ), - ...sortedGenotypesWithoutResults.map((genotypeResult) => - GeneCard( - genotypeResult, - warningLevelCounts[genotypeResult.key.value]!, - key: Key('gene-card-${genotypeResult.key.value}') - ) - ), - ], - ], - ), - if (hasActiveInhibitors) PageIndicatorExplanation( - context.l10n.report_page_indicator_explanation( - drugInteractionIndicatorName, - drugInteractionIndicator + _buildReportLists( + context, + activeDrugs, + allGenesExpanded, + currentSortOption, ), ), + _maybeBuildPageIndicators( + context, + activeDrugs, + allGenesVisible: allGenesExpanded.value, + ), ] ) ), ); } + + Widget _listDescription( + BuildContext context, + String label, + { + required List? drugsToFilterBy, + Color? labelColor, + } + ) { + final genotypes = _getRelevantGenotypes( + drugsToFilterBy, + ); + final affectedDrugs = genotypes.flatMap( + (genotypeResult) => _getAffectedDrugs( + genotypeResult.key.value, + drugSubset: drugsToFilterBy, + ) + ).toSet(); + return ListDescription( + key: Key('report-list-description-$label'), + textParts: [ + TextSpan(text: context.l10n.report_description_prefix), + TextSpan(text: ' '), + boldListDescriptionText(label, color: labelColor), + ], + detailsText: + '${context.l10n.report_gene_number(genotypes.length)}, ' + '${context.l10n.report_medication_number(affectedDrugs.length)}', + ); + } + + List _buildReportLists( + BuildContext context, + ActiveDrugs activeDrugs, + ValueNotifier allGenesExpanded, + ValueNotifier currentSortOption, + ) { + final currentMedicationGenes = _buildGeneCards( + currentSortOption: currentSortOption.value, + drugsToFilterBy: activeDrugs.names, + onlyCurrentMedications: true, + ); + final allMedicationGenesHeader = _listDescription( + context, + context.l10n.report_all_medications, + drugsToFilterBy: null, + ); + final allMedicationGenes = _buildGeneCards( + currentSortOption: currentSortOption.value, + drugsToFilterBy: null, + onlyCurrentMedications: false, + ); + final inclusionDescription = Padding( + key: Key('included-gene-explanation'), + padding: EdgeInsets.only( + left: PharMeTheme.smallSpace, + right: PharMeTheme.smallSpace, + top: PharMeTheme.smallSpace * 1.5, + ), + child: ListInclusionDescription.forGenes(), + ); + if (currentMedicationGenes.isEmpty) { + return [ + inclusionDescription, + allMedicationGenesHeader, + ...allMedicationGenes, + ]; + } + return [ + inclusionDescription, + _listDescription( + context, + context.l10n.report_current_medications, + drugsToFilterBy: activeDrugs.names, + labelColor: PharMeTheme.primaryColor, + ), + ...currentMedicationGenes, + PrettyExpansionTile( + title: allMedicationGenesHeader, + initiallyExpanded: allGenesExpanded.value, + onExpansionChanged: (value) => allGenesExpanded.value = value, + titlePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + icon: ResizedIconButton( + size: PharMeTheme.largeSpace, + disabledBackgroundColor: PharMeTheme.buttonColor, + iconWidgetBuilder: (size) => Icon( + allGenesExpanded.value + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + size: size, + color: PharMeTheme.surfaceColor, + ), + ), + children: allMedicationGenes, + ), + ]; + } } bool _hasNoResult(GenotypeResult genotypeResult) => UserData.lookupFor(genotypeResult.key.value) == SpecialLookup.noResult.value; class GeneCard extends StatelessWidget { - const GeneCard(this.genotypeResult, this.warningLevelCounts, { super.key }); + const GeneCard( + this.genotypeResult, + this.warningLevelCounts, { + super.key, + this.useColors = true, + this.onlyCurrentMedications = false, + }); final GenotypeResult genotypeResult; final WarningLevelCounts warningLevelCounts; + final bool useColors; + final bool onlyCurrentMedications; @visibleForTesting - Color? get color => _hasNoResult(genotypeResult) + Color? get color => !useColors || _hasNoResult(genotypeResult) ? PharMeTheme.onSurfaceColor : _getHighestSeverityColor(warningLevelCounts); @@ -137,70 +327,93 @@ class GeneCard extends StatelessWidget { @override Widget build(BuildContext context) { - final hasLegend = warningLevelCounts.values.any((count) => count > 0); + final medicationIndicators = + warningLevelCounts.values.any((count) => count > 0) + ? DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: PharMeTheme.smallSpace * 0.35, + horizontal: PharMeTheme.smallSpace, + ), + child: Text.rich( + TextSpan(children: [ + WidgetSpan(child: Icon( + FontAwesomeIcons.pills, + size: PharMeTheme.textTheme.bodyMedium!.fontSize, + color: PharMeTheme.buttonColor, + )), + TextSpan(text: ' : '), + buildWarningLevelLegend( + getText: (warningLevel) { + final warningLevelCount = + warningLevelCounts[warningLevel]!; + return warningLevelCount > 0 + ? warningLevelCount.toString() + : null; + } + ), + ]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ) + : null; + final phenotype = possiblyAdaptedPhenotype( + context, + genotypeResult, + drug: null, + ); return RoundedCard( - onTap: () => context.router.push( - GeneRoute(genotypeResult: genotypeResult) - ), + onTap: () async { + // ignore: use_build_context_synchronously + await context.router.push(GeneRoute( + genotypeResult: genotypeResult, + initiallyExpandFurtherMedications: !onlyCurrentMedications, + )); + }, radius: 16, color: color, - child: IntrinsicHeight(child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, + child: Row( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - genotypeResult.geneDisplayString, - style: PharMeTheme.textTheme.titleMedium - ), - SizedBox(height: 8), - Text( - possiblyAdaptedPhenotype(context, genotypeResult, drug: null), - style: PharMeTheme.textTheme.titleSmall, - ), - ], - ), Expanded( - child: hasLegend - ? Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only( - left: PharMeTheme.mediumSpace, - right: PharMeTheme.smallSpace, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + genotypeResult.geneDisplayString, + style: PharMeTheme.textTheme.titleMedium ), - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - child: Padding( - padding: EdgeInsets.symmetric( - vertical: PharMeTheme.smallSpace * 0.35, - horizontal: PharMeTheme.smallSpace, - ), - child: Text.rich( - buildWarningLevelLegend( - getText: (warningLevel) { - final warningLevelCount = - warningLevelCounts[warningLevel]!; - return warningLevelCount > 0 - ? warningLevelCount.toString() - : null; - } + SizedBox(height: PharMeTheme.smallSpace * 0.5), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.spaceBetween, + runSpacing: PharMeTheme.smallSpace * 0.5, + children: [ + Text( + phenotype, + style: testResultIsUnknown(context, phenotype) + ? PharMeTheme.textTheme.titleSmall!.copyWith( + fontStyle: FontStyle.italic, + ) + : PharMeTheme.textTheme.titleSmall, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - )), - ) - : SizedBox.shrink(), + SizedBox(width: PharMeTheme.smallSpace), + medicationIndicators ?? SizedBox.shrink(), + ], + ), + ] + ), ), - Icon(Icons.chevron_right_rounded), - ] - )), + SizedBox(width: PharMeTheme.smallSpace), + Column(mainAxisSize: MainAxisSize.min,children: [Icon(Icons.chevron_right_rounded)],), + ], + ), ); } } diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 3d7cc1b29..7f6abcce9 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -25,15 +25,24 @@ class SearchPage extends HookWidget { title: context.l10n.tab_drugs, canNavigateBack: false, contextToDismissFocusOnTap: context, - body: DrugSearch( - key: Key('drug-search'), - showFilter: true, - buildDrugItems: buildDrugCards, - cubit: cubit, - state: state, - activeDrugs: activeDrugs, - searchForDrugClass: searchForDrugClass, - showDrugInteractionIndicator: false, + body: Column( + children: [ + PageDescription.fromText( + context.l10n.search_content_explanation, + ), + Expanded( + child: DrugSearch( + key: Key('drug-search'), + showFilter: true, + buildDrugItems: buildDrugCards, + cubit: cubit, + state: state, + activeDrugs: activeDrugs, + searchForDrugClass: searchForDrugClass, + showDrugInteractionIndicator: false, + ), + ), + ], ), ), ), diff --git a/app/pubspec.yaml b/app/pubspec.yaml index a7b8779f0..15454f1f2 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,7 +1,7 @@ name: app description: A new Flutter project. publish_to: 'none' -version: 1.0.4+14 +version: 1.0.6+16 environment: sdk: '>=3.0.0' @@ -71,7 +71,8 @@ flutter: - assets/images/onboarding/ - assets/images/tutorial/ -flutter_icons: - android: 'launcher_icon' +flutter_launcher_icons: + android: true ios: true + remove_alpha_ios: true image_path: 'assets/icon/icon.png' diff --git a/docs/App-screens.md b/docs/App-screens.md index 29d3f4fe9..e2d56e283 100644 --- a/docs/App-screens.md +++ b/docs/App-screens.md @@ -1,8 +1,5 @@ # App Screens -_The last export date is September 08, 2023. Changes applied afterwards are not_ -_depicted._ - @@ -11,30 +8,26 @@ Actions are usually tapping (👆) or scrolling down (⏬). If no screen number is given, the action refers to the screen in the previous table row. +_Please note: as screenshot generation is currently not possible, the screens_ +_you see here are the study version screens. Most screens are the same, but_ +_in the study version some additional content is implemented._ + | # | Action | Screen | Description | | - | ------ | ------ | ----------- | -| 1 | App opened the first time | login | Login screen with data provider selection | -| 2 | _Get data_ 👆 | login-redirect | Alert for login redirect | -| 3 | _Continue_ 👆 | keycloak-login | Redirect to Keycloak login page | -| 4 | _Sign In_ 👆 | import-success | Back to app, import was successful | -| 5 | _Continue_ 👆 | onboarding-1 | Onboarding (screen 1 of 5) | -| 6 | _Next_ 👆 | onboarding-2 | Onboarding (screen 2 of 5) | -| 7 | _Next_ 👆 | onboarding-3 | Onboarding (screen 3 of 5) | -| 8 | _Next_ 👆 | onboarding-4 | Onboarding (screen 4 of 5) | -| 9 | _Next_ 👆 | onboarding-5 | Onboarding (screen 5 of 5) | -| 10 | _Get started_ 👆 | drug-selection | Initial selection of current medications | -| 11 | _Continue_ 👆 | gene-report | Gene report, showing all genes that can be mapped to PGx guidelines | -| 12 | _CYP2D6_ tile 👆 | cyp2d6 | Gene details; the notice about influence of other medications is only shown for genes for which phenoconversion implemented | -| 13 | _Amitriptyline_ tile 👆 | amitriptyline | Medication with unknown guideline detail; shown in green since standard dosing is applied without guidelines (_note for this example: the guideline for this genotype was not published in the backend at time of screenshot creation, which is why the guideline in missing in this case_) | -| 14 | _Medications_ navigation tab 👆 | drug-search | Medication search page | -| 15 | _?_ icon 👆 | drug-search-tooltip | Tooltip explaining search feature; tooltips look the same on all pages | -| 16 | Filter icon 👆 | drug-search-filter | Available search filters | -| 17 | _Clopidogrel_ tile 👆 | clopidogrel | Medication with known guideline | -| 18 | ⏬ | clopidogrel-scrolled | At the bottom of a medication, a link to the underlying guideline is given; this link redirects the user to the guideline website | -| 19 | Share icon (in header) 👆 | pdf-export | Create a PDF document to share with others | -| 20 | _FAQ_ navigation tab 👆 | faq | FAQ page | -| 21 | First FAQ list item 👆 | faq-first-item | Extended FAQ item | -| 22 | ⏬ | faq-contact | "Contact us" at the end of the FAQ in case of more questions; will open the user's default email app with the development team address pre-filled | -| 23 | _More_ navigation tab 👆 | more | "More" page with settings and further information; "Onboarding" will start the onboarding again (screens 5 to 10) | -| 24 | #23 _About us_ 👆 | about-us | "About us" page; "Privacy policy" and "Terms of use" have the same page style (currently only lorem ipsum) | -| 25 | #23 _Delete app data_ 👆 | delete-app-data | Deletes all app data and redirects to screen 1; continuing is only possible when the checkmark was clicked | +| 1 | App opened the first time | accept-terms | Notice that by continuing users agree to terms (see screen 23) | +| 2 | _Continue_ 👆 | login | Login screen with "Contact us" link; will open the user's default email app with the study team address pre-filled | +| 3 | #2 _Sign in_ (correct credentials) 👆 | onboarding-1 | Onboarding (screen 1 of 5) | +| 4 | _Next_ 👆 | onboarding-2 | Onboarding (screen 2 of 5) | +| 5 | _Next_ 👆 | onboarding-3 | Onboarding (screen 3 of 5) | +| 6 | _Next_ 👆 | onboarding-4 | Onboarding (screen 4 of 5) | +| 7 | _Next_ 👆 | onboarding-5 | Onboarding (screen 5 of 5) | +| 8 | _Get started_ 👆 | drug-selection | Initial active medication selection | +| 9 | _Continue_ 👆 | gene-report | Gene report, showing all genes that can be mapped to PGx guidelines | +| 10 | #10 _CYP2C9_ tile 👆 | cyp2c9 | Gene details; a notice about influence of other medications is shown for genes where drug-gene interactions are implemented | +| 11 | _Medications_ navigation tab 👆 | drug-search | Medication search page | +| 12 | Filter icon 👆 | drug-search-filter | Available search filters | +| 13 | _Ibuprofen_ tile 👆 | ibuprofen | Medication with "yellow" warning level guideline | +| 14 | _FAQ_ navigation tab 👆 | faq | FAQ page | +| 15 | First FAQ list item 👆 | faq-first-item | Extended FAQ item; the last item is a "Contact us" button (not visible in screenshot) in case of more questions | +| 16 | _More_ navigation tab 👆 | more | "More" page with settings and further information; "Onboarding" will start the onboarding again (screens 5 to 10) | +| 17 | #16 _Delete app data_ 👆 | delete-app-data | Deletes all app data and redirects to screen 1; continuing is only possible when the checkmark was clicked | diff --git a/docs/User-instructions.html b/docs/User-instructions.html index c28111200..e6483f731 100644 --- a/docs/User-instructions.html +++ b/docs/User-instructions.html @@ -1,7 +1,19 @@ -PharMe: Getting Started +PharMe: Getting Started & Further Resources + +
-

Getting Started with PharMe

+

PharMe: Getting Started & Further Resources

- Welcome to PharMe, an application to gain insights on your DNA's influence - on medications! 🧬💊🤗 + In this document you find all information to get started with PharMe and further support while using it:

+

+ If you have any questions or need assistance, please refer to the FAQ section or reach out via e-mail: ehivepgx@mssm.edu +

+

App Intro

+
+
+

Welcome to PharMe!

+

+ Did you know? Your genes help decide how medications work in your body. +
+ In fact, 9 out of 10 people might react differently to medications than expected. +
+ PharMe helps you understand how your genes affect your medications. +

+
+
+ +
+
+ Your genes are just one piece of the puzzle. Many things affect how medications work, like your weight, age, other health conditions, or other medications you are taking. +
+
+
+ +
+
+
+

One Size Does Not Fit All

+

+ Think of medications like shoes - one size doesn't fit all. +
+ A medication that works well for most people might not work the same way for you. +

+
+
+ warning +
+
+ Never change or stop your medications without talking to your pharmacist or doctor first. +
+
+
+ +
+
+
+

Genome Power Unlocked to Improve Your Health

+

+ PharMe tells you if your genes might make certain medications work differently than expected, cause unwanted side effects, or not work well enough. +
+
+ This field of medicine is called pharma-co-genomics (PGx). +

+
+
+ info +
+
+ The medical field of how your genes affect how medications work in your body is called pharma-co-genomics (PGx). +
+
+
+ +
+
+
+

Tailored Just for You

+

+ PharMe works by looking at your gene information and checking it against what scientists know about genes and medications. +
+
+ We use information from trusted clinical PGx guidelines created by medical experts (CPIC and FDA). +

+
+
+ + + +
+
+ PharMe only includes medications with clinical PGx guidelines from CPIC and the FDA. +
+
+ While only to a small percentage of medications have PGx guidelines, they are among the most commonly prescribed ones. If you cannot find a medication in PharMe, there may not be enough evidence for meaningful gene interactions. +
+
+
+ +
+
+
+

Keeping Your Information Safe

+

+ We protect your privacy by storing your gene information and doing calculations only on your phone. We do not share your gene information with anyone. +
+
+ We also do not store any information that could tell someone who you are, such as your name. +

+
+ +
+

App Tour

+ +

Learn About Genetics

+

FAQ

diff --git a/docs/screencasts/01_accept_and_login.gif b/docs/screencasts/01_accept_and_login.gif new file mode 100644 index 000000000..740f9bffc Binary files /dev/null and b/docs/screencasts/01_accept_and_login.gif differ diff --git a/docs/screencasts/01_accept_and_login.mp4 b/docs/screencasts/01_accept_and_login.mp4 new file mode 100644 index 000000000..acebc5fac Binary files /dev/null and b/docs/screencasts/01_accept_and_login.mp4 differ diff --git a/docs/screencasts/02_onboarding.gif b/docs/screencasts/02_onboarding.gif new file mode 100644 index 000000000..ca344e0ae Binary files /dev/null and b/docs/screencasts/02_onboarding.gif differ diff --git a/docs/screencasts/02_onboarding.mp4 b/docs/screencasts/02_onboarding.mp4 new file mode 100644 index 000000000..87da0bc06 Binary files /dev/null and b/docs/screencasts/02_onboarding.mp4 differ diff --git a/docs/screencasts/03_drug_selection.gif b/docs/screencasts/03_drug_selection.gif new file mode 100644 index 000000000..c19aa628d Binary files /dev/null and b/docs/screencasts/03_drug_selection.gif differ diff --git a/docs/screencasts/03_drug_selection.mp4 b/docs/screencasts/03_drug_selection.mp4 new file mode 100644 index 000000000..dc9c58c85 Binary files /dev/null and b/docs/screencasts/03_drug_selection.mp4 differ diff --git a/docs/screencasts/04_tutorial.gif b/docs/screencasts/04_tutorial.gif new file mode 100644 index 000000000..ed6bae973 Binary files /dev/null and b/docs/screencasts/04_tutorial.gif differ diff --git a/docs/screencasts/04_tutorial.mp4 b/docs/screencasts/04_tutorial.mp4 new file mode 100644 index 000000000..2bffb1894 Binary files /dev/null and b/docs/screencasts/04_tutorial.mp4 differ diff --git a/docs/screencasts/05_bottom_navigation_loopable.gif b/docs/screencasts/05_bottom_navigation_loopable.gif new file mode 100644 index 000000000..c73e1b984 Binary files /dev/null and b/docs/screencasts/05_bottom_navigation_loopable.gif differ diff --git a/docs/screencasts/05_bottom_navigation_loopable.mp4 b/docs/screencasts/05_bottom_navigation_loopable.mp4 new file mode 100644 index 000000000..8d3437726 Binary files /dev/null and b/docs/screencasts/05_bottom_navigation_loopable.mp4 differ diff --git a/docs/screencasts/06_drug_search_and_filter_loopable.gif b/docs/screencasts/06_drug_search_and_filter_loopable.gif new file mode 100644 index 000000000..14a4c58e0 Binary files /dev/null and b/docs/screencasts/06_drug_search_and_filter_loopable.gif differ diff --git a/docs/screencasts/06_drug_search_and_filter_loopable.mp4 b/docs/screencasts/06_drug_search_and_filter_loopable.mp4 new file mode 100644 index 000000000..a4a0b9897 Binary files /dev/null and b/docs/screencasts/06_drug_search_and_filter_loopable.mp4 differ diff --git a/docs/screencasts/07_ibuprofen_loopable.gif b/docs/screencasts/07_ibuprofen_loopable.gif new file mode 100644 index 000000000..7a95615c3 Binary files /dev/null and b/docs/screencasts/07_ibuprofen_loopable.gif differ diff --git a/docs/screencasts/07_ibuprofen_loopable.mp4 b/docs/screencasts/07_ibuprofen_loopable.mp4 new file mode 100644 index 000000000..1df174552 Binary files /dev/null and b/docs/screencasts/07_ibuprofen_loopable.mp4 differ diff --git a/docs/screencasts/08_report_and_cyp2c9_loopable.gif b/docs/screencasts/08_report_and_cyp2c9_loopable.gif new file mode 100644 index 000000000..e1856b761 Binary files /dev/null and b/docs/screencasts/08_report_and_cyp2c9_loopable.gif differ diff --git a/docs/screencasts/08_report_and_cyp2c9_loopable.mp4 b/docs/screencasts/08_report_and_cyp2c9_loopable.mp4 new file mode 100644 index 000000000..bb691d9d2 Binary files /dev/null and b/docs/screencasts/08_report_and_cyp2c9_loopable.mp4 differ diff --git a/docs/screencasts/09_faq_and_more_loopable.gif b/docs/screencasts/09_faq_and_more_loopable.gif new file mode 100644 index 000000000..e2f545fa4 Binary files /dev/null and b/docs/screencasts/09_faq_and_more_loopable.gif differ diff --git a/docs/screencasts/09_faq_and_more_loopable.mp4 b/docs/screencasts/09_faq_and_more_loopable.mp4 new file mode 100644 index 000000000..33fa058c6 Binary files /dev/null and b/docs/screencasts/09_faq_and_more_loopable.mp4 differ diff --git a/docs/screencasts/10_delete_data.gif b/docs/screencasts/10_delete_data.gif new file mode 100644 index 000000000..eba8d9a6e Binary files /dev/null and b/docs/screencasts/10_delete_data.gif differ diff --git a/docs/screencasts/10_delete_data.mp4 b/docs/screencasts/10_delete_data.mp4 new file mode 100644 index 000000000..4f1b3cf88 Binary files /dev/null and b/docs/screencasts/10_delete_data.mp4 differ diff --git a/docs/screencasts/full.mov b/docs/screencasts/full.mov new file mode 100644 index 000000000..185da9328 Binary files /dev/null and b/docs/screencasts/full.mov differ diff --git a/docs/screencasts/full_clean.mp4 b/docs/screencasts/full_clean.mp4 new file mode 100644 index 000000000..9c7acea2f Binary files /dev/null and b/docs/screencasts/full_clean.mp4 differ diff --git a/docs/screenshots/about-us.png b/docs/screenshots/about-us.png deleted file mode 100644 index 241005b78..000000000 Binary files a/docs/screenshots/about-us.png and /dev/null differ diff --git a/docs/screenshots/accept-terms.png b/docs/screenshots/accept-terms.png new file mode 100644 index 000000000..4cf78dee7 Binary files /dev/null and b/docs/screenshots/accept-terms.png differ diff --git a/docs/screenshots/cyp2c19.png b/docs/screenshots/cyp2c19.png deleted file mode 100644 index d6384872b..000000000 Binary files a/docs/screenshots/cyp2c19.png and /dev/null differ diff --git a/docs/screenshots/cyp2c9-expanded.png b/docs/screenshots/cyp2c9-expanded.png new file mode 100644 index 000000000..548c9c67d Binary files /dev/null and b/docs/screenshots/cyp2c9-expanded.png differ diff --git a/docs/screenshots/cyp2d6.png b/docs/screenshots/cyp2d6.png deleted file mode 100644 index ca5d7ab95..000000000 Binary files a/docs/screenshots/cyp2d6.png and /dev/null differ diff --git a/docs/screenshots/delete-app-data.png b/docs/screenshots/delete-app-data.png index c4b0f0eeb..6039e5566 100644 Binary files a/docs/screenshots/delete-app-data.png and b/docs/screenshots/delete-app-data.png differ diff --git a/docs/screenshots/drug-search-filter.png b/docs/screenshots/drug-search-filter.png index a52dc8a17..0a47444ce 100644 Binary files a/docs/screenshots/drug-search-filter.png and b/docs/screenshots/drug-search-filter.png differ diff --git a/docs/screenshots/drug-search-tooltip.png b/docs/screenshots/drug-search-tooltip.png deleted file mode 100644 index 8655d5df1..000000000 Binary files a/docs/screenshots/drug-search-tooltip.png and /dev/null differ diff --git a/docs/screenshots/drug-selection-intro.png b/docs/screenshots/drug-selection-intro.png new file mode 100644 index 000000000..13aa42878 Binary files /dev/null and b/docs/screenshots/drug-selection-intro.png differ diff --git a/docs/screenshots/drug-selection.png b/docs/screenshots/drug-selection.png index ecb633c87..6ef302d02 100644 Binary files a/docs/screenshots/drug-selection.png and b/docs/screenshots/drug-selection.png differ diff --git a/docs/screenshots/faq-first-item.png b/docs/screenshots/faq-first-item.png index c8bfdb1b5..a61772aad 100644 Binary files a/docs/screenshots/faq-first-item.png and b/docs/screenshots/faq-first-item.png differ diff --git a/docs/screenshots/gene-report-all.png b/docs/screenshots/gene-report-all.png new file mode 100644 index 000000000..5e2fde0a1 Binary files /dev/null and b/docs/screenshots/gene-report-all.png differ diff --git a/docs/screenshots/onboarding-1.png b/docs/screenshots/onboarding-1.png index b82fad945..5f5736d56 100644 Binary files a/docs/screenshots/onboarding-1.png and b/docs/screenshots/onboarding-1.png differ diff --git a/docs/screenshots/onboarding-2.png b/docs/screenshots/onboarding-2.png index a828e6af6..8b50a3367 100644 Binary files a/docs/screenshots/onboarding-2.png and b/docs/screenshots/onboarding-2.png differ diff --git a/docs/screenshots/onboarding-3.png b/docs/screenshots/onboarding-3.png index 5033006e5..0ac37146c 100644 Binary files a/docs/screenshots/onboarding-3.png and b/docs/screenshots/onboarding-3.png differ diff --git a/docs/screenshots/onboarding-4.png b/docs/screenshots/onboarding-4.png index 5b269dea8..151993a73 100644 Binary files a/docs/screenshots/onboarding-4.png and b/docs/screenshots/onboarding-4.png differ diff --git a/docs/screenshots/onboarding-5.png b/docs/screenshots/onboarding-5.png index 1167712c4..d8b4da86d 100644 Binary files a/docs/screenshots/onboarding-5.png and b/docs/screenshots/onboarding-5.png differ diff --git a/docs/screenshots/setup-complete.png b/docs/screenshots/setup-complete.png new file mode 100644 index 000000000..6ee90a8ab Binary files /dev/null and b/docs/screenshots/setup-complete.png differ diff --git a/docs/screenshots/tutorial-1.png b/docs/screenshots/tutorial-1.png new file mode 100644 index 000000000..b29b01b42 Binary files /dev/null and b/docs/screenshots/tutorial-1.png differ diff --git a/docs/screenshots/tutorial-2.png b/docs/screenshots/tutorial-2.png new file mode 100644 index 000000000..b54bb84fc Binary files /dev/null and b/docs/screenshots/tutorial-2.png differ diff --git a/docs/screenshots/tutorial-3.png b/docs/screenshots/tutorial-3.png new file mode 100644 index 000000000..b2e03f90a Binary files /dev/null and b/docs/screenshots/tutorial-3.png differ diff --git a/docs/screenshots/tutorial-4.png b/docs/screenshots/tutorial-4.png new file mode 100644 index 000000000..fe14d3774 Binary files /dev/null and b/docs/screenshots/tutorial-4.png differ diff --git a/docs/screenshots/tutorial-5.png b/docs/screenshots/tutorial-5.png new file mode 100644 index 000000000..dd897c978 Binary files /dev/null and b/docs/screenshots/tutorial-5.png differ diff --git a/pharme.code-workspace b/pharme.code-workspace index d02f87700..4fd2cc1a2 100644 --- a/pharme.code-workspace +++ b/pharme.code-workspace @@ -138,6 +138,7 @@ "Statins", "subfolders", "tacrolimus", + "tamslo", "terbinafine", "tobramycin", "tramadol",