From f9fd84d9b1cebf7a83f19e743318c9d7308c3636 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 09:53:56 +0100 Subject: [PATCH 01/19] feat(#479): checkbox for activity --- app/lib/common/pages/drug/cubit.dart | 12 +++++++----- app/lib/common/pages/drug/drug.dart | 12 ++++++------ .../drug/widgets/annotation_cards/drug.dart | 19 ++++++++++++++++++- app/lib/l10n/app_en.arb | 5 ++++- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/app/lib/common/pages/drug/cubit.dart b/app/lib/common/pages/drug/cubit.dart index edbf7130b..8f3a67b4c 100644 --- a/app/lib/common/pages/drug/cubit.dart +++ b/app/lib/common/pages/drug/cubit.dart @@ -11,19 +11,21 @@ class DrugCubit extends Cubit { final Drug _drug; - Future toggleActive() async { + // ignore: avoid_positional_boolean_parameters + Future setActivity(bool? value) async { + if (value == null) return; final drug = state.whenOrNull(loaded: (drug, _) => drug); if (drug == null) return; final active = UserData.instance.activeDrugNames ?? []; - if (drug.isActive()) { + if (value) { + UserData.instance.activeDrugNames = active + [_drug.name]; + } else { UserData.instance.activeDrugNames = active.filter((element) => element != _drug.name).toList(); - } else { - UserData.instance.activeDrugNames = active + [_drug.name]; } await UserData.save(); - emit(DrugState.loaded(drug, isActive: drug.isActive())); + emit(DrugState.loaded(drug, isActive: value)); } } diff --git a/app/lib/common/pages/drug/drug.dart b/app/lib/common/pages/drug/drug.dart index 5e51ba9ce..d0a15b808 100644 --- a/app/lib/common/pages/drug/drug.dart +++ b/app/lib/common/pages/drug/drug.dart @@ -31,10 +31,6 @@ class DrugPage extends StatelessWidget { loaded: (drug, isActive) => pageScaffold( title: drugName, actions: [ - IconButton( - onPressed: () => context.read().toggleActive(), - icon: PharMeTheme.activeDrugIcon(isActive: isActive), - ), IconButton( onPressed: () => sharePdf(drug), icon: Icon( @@ -64,9 +60,13 @@ class DrugPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SubHeader(context.l10n.drugs_page_header_druginfo), + SubHeader(context.l10n.drugs_page_header_drug), SizedBox(height: 12), - DrugAnnotationCard(drug), + DrugAnnotationCard( + drug, + isActive: isActive, + setActivity: context.read().setActivity, + ), SizedBox(height: 20), SubHeader( context.l10n.drugs_page_header_guideline, diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart index 91f71d106..ae4ca6bdb 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart @@ -1,9 +1,16 @@ import '../../../../module.dart'; +import '../sub_header.dart'; class DrugAnnotationCard extends StatelessWidget { - const DrugAnnotationCard(this.drug); + const DrugAnnotationCard( + this.drug, { + required this.isActive, + required this.setActivity, + }); final Drug drug; + final bool isActive; + final void Function(bool?) setActivity; @override Widget build(BuildContext context) { @@ -12,6 +19,8 @@ class DrugAnnotationCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SubHeader(context.l10n.drugs_page_header_druginfo), + SizedBox(height: 12), Text(drug.annotations.indication), SizedBox(height: 8), Table(defaultColumnWidth: IntrinsicColumnWidth(), children: [ @@ -22,6 +31,14 @@ class DrugAnnotationCard extends StatelessWidget { drug.annotations.brandNames.join(', ')), ] ]), + SizedBox(height: 12), + SubHeader(context.l10n.drugs_page_header_active), + CheckboxListTile( + title: Text(context.l10n.drugs_page_active), + value: isActive, + onChanged: setActivity, + controlAffinity: ListTileControlAffinity.leading, + ), ], ), ), diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index dec0f2ea8..fd74bcd68 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -33,7 +33,10 @@ "drugs_page_header_further_info": "Further Information", "drugs_page_header_synonyms": "Synonyms", "drugs_page_header_drugclass": "Drug class", - "drugs_page_header_druginfo": "Drug Information", + "drugs_page_header_drug": "Drug", + "drugs_page_header_druginfo": "Information", + "drugs_page_header_active": "Activity", + "drugs_page_active": "I am actively taking this drug.", "drugs_page_header_guideline": "Clinical Guideline", "drugs_page_header_recommendation": "Recommendation", "drugs_page_no_guidelines_for_phenotype": "We couldn't find a guideline matching your genetics.", From 19b9facda1d9053fe8d12cc15eddb36ba7146934 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 09:56:47 +0100 Subject: [PATCH 02/19] feat(#479): add section dividers --- app/lib/common/pages/drug/widgets/annotation_cards/drug.dart | 4 +++- .../common/pages/drug/widgets/annotation_cards/guideline.dart | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart index ae4ca6bdb..d8e009e05 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart @@ -31,7 +31,9 @@ class DrugAnnotationCard extends StatelessWidget { drug.annotations.brandNames.join(', ')), ] ]), - SizedBox(height: 12), + SizedBox(height: 4), + Divider(color: PharMeTheme.borderColor), + SizedBox(height: 4), SubHeader(context.l10n.drugs_page_header_active), CheckboxListTile( title: Text(context.l10n.drugs_page_active), diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart index 35bb5a42c..1cf0af79f 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart @@ -17,7 +17,9 @@ class GuidelineAnnotationCard extends StatelessWidget { _buildHeader(context), SizedBox(height: 12), _buildCard(context), - SizedBox(height: 16), + SizedBox(height: 8), + Divider(color: PharMeTheme.borderColor), + SizedBox(height: 8), _buildSourcesSection(context), SizedBox(height: 12), ]), From 3627ab739b050bd0748c24e42bc670b7f4ccef93 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 10:21:52 +0100 Subject: [PATCH 03/19] feat(#479): avoid duplicate active drugs --- app/lib/common/models/userdata/userdata.dart | 1 + app/lib/common/pages/drug/cubit.dart | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index 54e4802f3..b1d0158e0 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -55,6 +55,7 @@ class UserData { return UserData.instance.lookups?[gene]?.lookupkey; } + // hive can't deal with sets so we have to use a list :( @HiveField(2) List? activeDrugNames; } diff --git a/app/lib/common/pages/drug/cubit.dart b/app/lib/common/pages/drug/cubit.dart index 8f3a67b4c..42a33e61b 100644 --- a/app/lib/common/pages/drug/cubit.dart +++ b/app/lib/common/pages/drug/cubit.dart @@ -17,13 +17,13 @@ class DrugCubit extends Cubit { final drug = state.whenOrNull(loaded: (drug, _) => drug); if (drug == null) return; - final active = UserData.instance.activeDrugNames ?? []; + final active = (UserData.instance.activeDrugNames ?? []) + .filter((name) => name != _drug.name) + .toList(); if (value) { - UserData.instance.activeDrugNames = active + [_drug.name]; - } else { - UserData.instance.activeDrugNames = - active.filter((element) => element != _drug.name).toList(); + active.add(_drug.name); } + UserData.instance.activeDrugNames = active; await UserData.save(); emit(DrugState.loaded(drug, isActive: value)); } From 408ea53b02e7fb2a742ab791f51ebb2427a0f959 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 10:46:19 +0100 Subject: [PATCH 04/19] feat(#479): show activity change warning --- .../drug/widgets/annotation_cards/drug.dart | 26 ++++++++++++++++++- app/lib/l10n/app_en.arb | 7 +++-- app/lib/settings/pages/settings.dart | 4 +-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart index d8e009e05..13b702dde 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart @@ -1,3 +1,5 @@ +import 'package:flutter/cupertino.dart'; + import '../../../../module.dart'; import '../sub_header.dart'; @@ -36,9 +38,31 @@ class DrugAnnotationCard extends StatelessWidget { SizedBox(height: 4), SubHeader(context.l10n.drugs_page_header_active), CheckboxListTile( + activeColor: PharMeTheme.primaryColor, title: Text(context.l10n.drugs_page_active), value: isActive, - onChanged: setActivity, + onChanged: (newValue) => showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoAlertDialog( + title: Text(context.l10n.drugs_page_active_warn_header), + content: Text(context.l10n.drugs_page_active_warn), + actions: [ + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () => Navigator.pop(context, 'Cancel'), + child: Text(context.l10n.action_cancel), + ), + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.pop(context, 'OK'); + setActivity(newValue); + }, + child: Text(context.l10n.action_continue), + ), + ], + ), + ), controlAffinity: ListTileControlAffinity.leading, ), ], diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index fd74bcd68..09b0877d1 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1,4 +1,7 @@ { + "action_cancel": "Cancel", + "action_continue": "Continue", + "auth_choose_lab": "Please select your lab", "auth_sign_in": "Sign in", "auth_success": "Successfully imported data", @@ -37,6 +40,8 @@ "drugs_page_header_druginfo": "Information", "drugs_page_header_active": "Activity", "drugs_page_active": "I am actively taking this drug.", + "drugs_page_active_warn_header": "Are you sure you want to change the activity status?", + "drugs_page_active_warn": "This may adjust the guidelines you receive for other drugs.", "drugs_page_header_guideline": "Clinical Guideline", "drugs_page_header_recommendation": "Recommendation", "drugs_page_no_guidelines_for_phenotype": "We couldn't find a guideline matching your genetics.", @@ -108,8 +113,6 @@ "onboarding_5_text": "After signing in to the lab, your genetic data is sent straight onto your phone.\n\nOur servers know nothing about you, neither your identity nor your genetics.", "settings_page_account_settings": "Account Settings", - "settings_page_cancel": "Cancel", - "settings_page_continue": "Continue", "settings_page_delete_data": "Delete App Data", "settings_page_delete_data_text": "Are you sure that you want to delete all app data? This also includes your genomic data.", "settings_page_more": "More", diff --git a/app/lib/settings/pages/settings.dart b/app/lib/settings/pages/settings.dart index 08ce86751..7cbbef48c 100644 --- a/app/lib/settings/pages/settings.dart +++ b/app/lib/settings/pages/settings.dart @@ -63,7 +63,7 @@ class SettingsPage extends StatelessWidget { actions: [ TextButton( onPressed: context.router.root.pop, - child: Text(context.l10n.settings_page_cancel), + child: Text(context.l10n.action_cancel), ), TextButton( onPressed: () async { @@ -71,7 +71,7 @@ class SettingsPage extends StatelessWidget { await context.router.replaceAll([LoginRouter()]); }, child: Text( - context.l10n.settings_page_continue, + context.l10n.action_continue, style: TextStyle(color: PharMeTheme.errorColor), ), ), From 09b16adc6fb6690565c3fc315f4ca62e9355c39b Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 10:56:43 +0100 Subject: [PATCH 05/19] feat(#479): remove star icon --- app/lib/common/theme.dart | 5 ----- app/lib/search/pages/search.dart | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/lib/common/theme.dart b/app/lib/common/theme.dart index 57b7e7d4b..c8afc6ac7 100644 --- a/app/lib/common/theme.dart +++ b/app/lib/common/theme.dart @@ -70,11 +70,6 @@ class PharMeTheme { static const backgroundColor = Colors.white; static const errorColor = Color(0xccf52a2a); static final borderColor = Colors.black.withOpacity(.2); - - static Icon activeDrugIcon({required bool isActive, double? size}) { - return Icon(isActive ? Icons.star_rounded : Icons.star_border_rounded, - size: size, color: primaryColor); - } } extension WarningLevelColor on WarningLevel { diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 4d9bca472..1aa462910 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -33,8 +33,7 @@ class SearchPage extends HookWidget { TooltipIcon(context.l10n.search_page_tooltip_search), IconButton( onPressed: () => context.read().toggleFilter(), - icon: PharMeTheme.activeDrugIcon( - isActive: context.read().filterActive)), + icon: Icon(Icons.filter_list_rounded)), ]), body: state.when( initial: () => [Container()], From 3601e4bd0d9b4750d951b597abf3ad80c6b0d6ac Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 12:01:36 +0100 Subject: [PATCH 06/19] dev(#479): add popover dep --- app/pubspec.lock | 11 +++++++++-- app/pubspec.yaml | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index 3da36244f..fc2038de5 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -834,6 +834,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.1" + popover: + dependency: "direct main" + description: + name: popover + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8+1" process: dependency: transitive description: @@ -1246,5 +1253,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 97d1b7f67..ac7aae1c5 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: intl: ^0.17.0 json_annotation: ^4.6.0 path_provider: ^2.0.11 + popover: ^0.2.8+1 pdf: ^3.8.1 shared_preferences: ^2.0.15 url_launcher: ^6.1.4 From e7cfa4fc6f4158e816e4a1f28b2ae2726d7004d2 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 13:14:04 +0100 Subject: [PATCH 07/19] feat(#479): popover context menu --- app/lib/common/widgets/context_menu.dart | 87 ++++++++++++++++++++++++ app/lib/common/widgets/module.dart | 1 + 2 files changed, 88 insertions(+) create mode 100644 app/lib/common/widgets/context_menu.dart diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart new file mode 100644 index 000000000..b1e1d6412 --- /dev/null +++ b/app/lib/common/widgets/context_menu.dart @@ -0,0 +1,87 @@ +import 'package:popover/popover.dart'; + +import '../module.dart'; + +class ContextMenu extends StatelessWidget { + const ContextMenu({ + super.key, + required this.items, + required this.child, + }); + + final List items; + final Widget child; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + showPopover( + context: context, + bodyBuilder: (context) => Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Container( + decoration: BoxDecoration( + color: PharMeTheme.onSurfaceColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .mapIndexed((index, item) => (index == items.count() - 1) + ? item + : Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0.5, color: PharMeTheme.borderColor), + )), + child: item)) + .toList(), + ), + ), + ), + direction: PopoverDirection.bottom, + arrowHeight: 0, + arrowWidth: 0, + transitionDuration: Duration(milliseconds: 100), + barrierColor: Color.fromRGBO(0, 0, 0, 0.05), + backgroundColor: Color.fromRGBO(1, 1, 1, 0), + shadow: [], + ); + }, + child: child); + } +} + +class ContextMenuAction extends StatelessWidget { + const ContextMenuAction({ + super.key, + required this.label, + required this.action, + this.icon, + }); + + final String label; + final void Function() action; + final IconData? icon; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: action, + child: Padding( + padding: EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) Icon(icon!, size: 24) else SizedBox(width: 24), + SizedBox(width: 8), + Text(label), + ], + ), + ), + ); + } +} diff --git a/app/lib/common/widgets/module.dart b/app/lib/common/widgets/module.dart index 2cbbfd4ef..75550056b 100644 --- a/app/lib/common/widgets/module.dart +++ b/app/lib/common/widgets/module.dart @@ -1,4 +1,5 @@ export 'app.dart'; +export 'context_menu.dart'; export 'headings.dart'; export 'indicators.dart'; export 'page_scaffold.dart'; From 2e966c18efe8b767b0d644c8262fc3c15072f13e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:16:56 +0100 Subject: [PATCH 08/19] feat(#479): add 'none' warning level --- app/lib/common/models/drug/warning_level.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/lib/common/models/drug/warning_level.dart b/app/lib/common/models/drug/warning_level.dart index 1b7726ec0..600dfe6ad 100644 --- a/app/lib/common/models/drug/warning_level.dart +++ b/app/lib/common/models/drug/warning_level.dart @@ -10,7 +10,9 @@ enum WarningLevel { @HiveField(1) yellow, @HiveField(2) - green + green, + @HiveField(3) + none, } extension WarningLevelIcon on WarningLevel { From ed791de5d441628ba1417d4f83cc62547f811e9e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:17:22 +0100 Subject: [PATCH 09/19] feat(#479): search filtering --- app/lib/search/pages/cubit.dart | 92 ++++++++++++++++++++++---------- app/lib/search/pages/search.dart | 39 +++++++++++--- 2 files changed, 96 insertions(+), 35 deletions(-) diff --git a/app/lib/search/pages/cubit.dart b/app/lib/search/pages/cubit.dart index 20456d478..ab374d611 100644 --- a/app/lib/search/pages/cubit.dart +++ b/app/lib/search/pages/cubit.dart @@ -14,29 +14,34 @@ class SearchCubit extends Cubit { Timer? searchTimeout; String searchValue = ''; - bool filterActive = false; final duration = Duration(milliseconds: 500); - void search({String? query, bool? filterActive}) { - this.filterActive = filterActive ?? this.filterActive; - searchValue = query ?? searchValue; - + void search({ + String? query, + bool? toggleInactive, + WarningLevel? toggleWarningLevel, + }) { state.whenOrNull( - initial: loadDrugs, - loaded: (allDrugs, _) => emit(SearchState.loaded( - allDrugs, - allDrugs - .filter((drug) => - (!this.filterActive || drug.isActive()) && - drug.matches(query: searchValue)) - .toList())), - error: loadDrugs); + initial: loadDrugs, + loaded: (allDrugs, filter) => emit( + SearchState.loaded( + allDrugs, + FilterState.from( + filter, + query: query, + toggleInactive: toggleInactive, + toggleWarningLevel: toggleWarningLevel, + ), + ), + ), + error: loadDrugs, + ); } Future loadDrugs({bool updateIfNull = true}) async { final drugs = CachedDrugs.instance.drugs; if (drugs != null) { - _emitFilteredLoaded(drugs); + emit(SearchState.loaded(drugs, FilterState.initial())); return; } if (!updateIfNull) { @@ -53,19 +58,50 @@ class SearchCubit extends Cubit { } } - void toggleFilter() { - search(filterActive: !filterActive); - } + FilterState? get filter => state.whenOrNull(loaded: (_, filter) => filter); +} + +class FilterState { + FilterState({ + required this.query, + required this.showInactive, + required this.showWarningLevel, + }); - void _emitFilteredLoaded(List drugs) { - emit(SearchState.loaded( - drugs, - drugs - .filter((drug) => - (!filterActive || drug.isActive()) && - drug.matches(query: searchValue)) - .toList())); + FilterState.initial() + : this(query: '', showInactive: true, showWarningLevel: { + for (var level in WarningLevel.values) level: true + }); + + FilterState.from( + FilterState other, { + String? query, + bool? toggleInactive, + WarningLevel? toggleWarningLevel, + }) : this( + query: query ?? other.query, + showInactive: (toggleInactive == true) + ? !other.showInactive + : other.showInactive, + showWarningLevel: { + for (var entry in other.showWarningLevel.entries) + entry.key: + (entry.key == toggleWarningLevel) ? !entry.value : entry.value + }, + ); + + final String query; + final bool showInactive; + Map showWarningLevel; + + bool isAccepted(Drug drug) { + final warningLevel = drug.userGuideline()?.annotations.warningLevel; + return drug.matches(query: query) && + (drug.isActive() || showInactive) && + showWarningLevel[warningLevel]!; } + + List filter(List drugs) => drugs.filter(isAccepted).toList(); } @freezed @@ -73,6 +109,8 @@ class SearchState with _$SearchState { const factory SearchState.initial() = _InitialState; const factory SearchState.loading() = _LoadingState; const factory SearchState.loaded( - List allDrugs, List filteredDrugs) = _LoadedState; + List allDrugs, + FilterState filter, + ) = _LoadedState; const factory SearchState.error() = _ErrorState; } diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 1aa462910..3f4f47785 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -31,26 +31,49 @@ class SearchPage extends HookWidget { )), SizedBox(width: 12), TooltipIcon(context.l10n.search_page_tooltip_search), - IconButton( - onPressed: () => context.read().toggleFilter(), - icon: Icon(Icons.filter_list_rounded)), + buildFilter(context.read()), ]), body: state.when( initial: () => [Container()], error: () => [errorIndicator(context.l10n.err_generic)], - loaded: (_, drugs) => _buildDrugsList(context, drugs), + loaded: (drugs, filter) => + _buildDrugsList(context, drugs, filter), loading: () => [loadingIndicator()], )); })); } - List _buildDrugsList(BuildContext context, List drugs) { - if (drugs.isEmpty && context.read().filterActive) { - return [errorIndicator(context.l10n.err_no_active_drugs)]; + Widget buildFilter(SearchCubit cubit) { + final filter = cubit.filter; + print('build filter'); + return ContextMenu( + items: [ + ContextMenuCheckmark( + label: 'show inactive', + setState: () => cubit.search(toggleInactive: true), + icon: _checkmark(isEnabled: filter?.showInactive)), + ...WarningLevel.values.map((level) => ContextMenuCheckmark( + label: 'placeholder', + setState: () => cubit.search(toggleWarningLevel: level), + icon: _checkmark(isEnabled: filter?.showWarningLevel[level]))) + ], + child: Padding( + padding: EdgeInsets.all(8), child: Icon(Icons.filter_list_rounded)), + ); + } + + IconData? _checkmark({required bool? isEnabled}) => + isEnabled ?? false ? Icons.check_rounded : null; + + List _buildDrugsList( + BuildContext context, List drugs, FilterState filter) { + final filteredDrugs = filter.filter(drugs); + if (filteredDrugs.isEmpty) { + return [errorIndicator(context.l10n.err_no_drugs)]; } return [ SizedBox(height: 8), - ...drugs.map((drug) => Column(children: [ + ...filteredDrugs.map((drug) => Column(children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: DrugCard( From b8a7e9c60c823d6861014fbd171757db0a3c17f0 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:25:46 +0100 Subject: [PATCH 10/19] feat(#479): no drugs error text --- app/lib/l10n/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 09b0877d1..677c6a549 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -9,7 +9,7 @@ "err_could_not_retrieve_access_token": "An unexpected error occurred while retrieving the access token", "err_fetch_user_data_failed": "An unexpected error occurred while fetching your genomic data", "err_generic": "Error!", - "err_no_active_drugs": "You have no active drugs! Try disabling the filter next to the search bar above.", + "err_no_drugs": "Try adjusting the filters next to the search bar above.", "faq_page_description": "Here you can find answers to common questions about PGx", "faq_pharmacogenomics": "Pharmacogenomics", From 6a8d6eb26fcb6f3b2331212b9d365f1413058d44 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:26:29 +0100 Subject: [PATCH 11/19] feat(#479): refresh checkmark icons --- app/lib/common/widgets/context_menu.dart | 51 ++++++++++++++---------- app/lib/search/pages/cubit.dart | 23 +++++------ app/lib/search/pages/search.dart | 12 ++---- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index b1e1d6412..e2bc9d782 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -9,7 +9,7 @@ class ContextMenu extends StatelessWidget { required this.child, }); - final List items; + final List items; final Widget child; @override @@ -55,31 +55,40 @@ class ContextMenu extends StatelessWidget { } } -class ContextMenuAction extends StatelessWidget { - const ContextMenuAction({ - super.key, - required this.label, - required this.action, - this.icon, - }); +class ContextMenuCheckmark extends StatelessWidget { + ContextMenuCheckmark( + {super.key, + required this.label, + required this.setState, + this.state = false}); final String label; - final void Function() action; - final IconData? icon; + final void Function(bool) setState; + bool state; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: action, - child: Padding( - padding: EdgeInsets.all(12), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) Icon(icon!, size: 24) else SizedBox(width: 24), - SizedBox(width: 8), - Text(label), - ], + return StatefulBuilder( + builder: (context, rebuild) => GestureDetector( + onTap: () { + rebuild(() { + state = !state; + setState(state); + }); + }, + child: Padding( + padding: EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (state) + Icon(Icons.check_rounded, size: 24) + else + SizedBox(width: 24), + SizedBox(width: 8), + Text(label), + ], + ), ), ), ); diff --git a/app/lib/search/pages/cubit.dart b/app/lib/search/pages/cubit.dart index ab374d611..58b73c8d2 100644 --- a/app/lib/search/pages/cubit.dart +++ b/app/lib/search/pages/cubit.dart @@ -18,8 +18,8 @@ class SearchCubit extends Cubit { void search({ String? query, - bool? toggleInactive, - WarningLevel? toggleWarningLevel, + bool? showInactive, + Map? showWarningLevel, }) { state.whenOrNull( initial: loadDrugs, @@ -29,8 +29,8 @@ class SearchCubit extends Cubit { FilterState.from( filter, query: query, - toggleInactive: toggleInactive, - toggleWarningLevel: toggleWarningLevel, + showInactive: showInactive, + showWarningLevel: showWarningLevel, ), ), ), @@ -76,23 +76,20 @@ class FilterState { FilterState.from( FilterState other, { String? query, - bool? toggleInactive, - WarningLevel? toggleWarningLevel, + bool? showInactive, + Map? showWarningLevel, }) : this( query: query ?? other.query, - showInactive: (toggleInactive == true) - ? !other.showInactive - : other.showInactive, + showInactive: showInactive ?? other.showInactive, showWarningLevel: { - for (var entry in other.showWarningLevel.entries) - entry.key: - (entry.key == toggleWarningLevel) ? !entry.value : entry.value + for (var level in WarningLevel.values) + level: showWarningLevel?[level] ?? other.showWarningLevel[level]! }, ); final String query; final bool showInactive; - Map showWarningLevel; + final Map showWarningLevel; bool isAccepted(Drug drug) { final warningLevel = drug.userGuideline()?.annotations.warningLevel; diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 3f4f47785..8c73a072a 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -45,26 +45,22 @@ class SearchPage extends HookWidget { Widget buildFilter(SearchCubit cubit) { final filter = cubit.filter; - print('build filter'); return ContextMenu( items: [ ContextMenuCheckmark( label: 'show inactive', - setState: () => cubit.search(toggleInactive: true), - icon: _checkmark(isEnabled: filter?.showInactive)), + setState: (state) => cubit.search(showInactive: state), + state: filter?.showInactive ?? false), ...WarningLevel.values.map((level) => ContextMenuCheckmark( label: 'placeholder', - setState: () => cubit.search(toggleWarningLevel: level), - icon: _checkmark(isEnabled: filter?.showWarningLevel[level]))) + setState: (state) => cubit.search(showWarningLevel: {level: state}), + state: filter?.showWarningLevel[level] ?? false)) ], child: Padding( padding: EdgeInsets.all(8), child: Icon(Icons.filter_list_rounded)), ); } - IconData? _checkmark({required bool? isEnabled}) => - isEnabled ?? false ? Icons.check_rounded : null; - List _buildDrugsList( BuildContext context, List drugs, FilterState filter) { final filteredDrugs = filter.filter(drugs); From f42c7f73f88e0f281a46093c8f1525022583fd6f Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:27:34 +0100 Subject: [PATCH 12/19] feat(#479): adjust icons --- app/lib/common/widgets/context_menu.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index e2bc9d782..c84548db2 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -82,9 +82,9 @@ class ContextMenuCheckmark extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (state) - Icon(Icons.check_rounded, size: 24) + Icon(Icons.check_rounded, size: 16) else - SizedBox(width: 24), + SizedBox(width: 16, height: 16), SizedBox(width: 8), Text(label), ], From 972147bd2561a48ca52f0a01c54974deab218e3e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:38:15 +0100 Subject: [PATCH 13/19] feat(#479): filter labels --- app/lib/l10n/app_en.arb | 5 +++++ app/lib/search/pages/search.dart | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 677c6a549..ca32974a9 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -20,6 +20,11 @@ "general_retry": "Retry", "search_page_tooltip_search": "Search for drugs by their name, brand name or class.", + "search_page_filter_inactive": "Inactive drugs", + "search_page_filter_green": "Green warning level", + "search_page_filter_yellow": "Yellow warning level", + "search_page_filter_red": "Red warning level", + "search_page_filter_gray": "No warning level", "drugs_page_disclaimer": "This assessment is ONLY based on your genetics. Important factors like weight, age and pre-existing conditions are not considered.", "drugs_page_overwritten_phenotype": "adjusted based on you taking {drugName}", diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 8c73a072a..5798ebec8 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -31,7 +31,7 @@ class SearchPage extends HookWidget { )), SizedBox(width: 12), TooltipIcon(context.l10n.search_page_tooltip_search), - buildFilter(context.read()), + buildFilter(context), ]), body: state.when( initial: () => [Container()], @@ -43,16 +43,22 @@ class SearchPage extends HookWidget { })); } - Widget buildFilter(SearchCubit cubit) { + Widget buildFilter(BuildContext context) { + final cubit = context.read(); final filter = cubit.filter; return ContextMenu( items: [ ContextMenuCheckmark( - label: 'show inactive', + label: context.l10n.search_page_filter_inactive, setState: (state) => cubit.search(showInactive: state), state: filter?.showInactive ?? false), ...WarningLevel.values.map((level) => ContextMenuCheckmark( - label: 'placeholder', + label: { + WarningLevel.green: context.l10n.search_page_filter_green, + WarningLevel.yellow: context.l10n.search_page_filter_yellow, + WarningLevel.red: context.l10n.search_page_filter_red, + WarningLevel.none: context.l10n.search_page_filter_gray, + }[level]!, setState: (state) => cubit.search(showWarningLevel: {level: state}), state: filter?.showWarningLevel[level] ?? false)) ], From e29ac908fb2d6b81191e35a1db999ce6f1601b7a Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:59:02 +0100 Subject: [PATCH 14/19] feat(#479): improve checkmark row layout --- app/lib/common/widgets/context_menu.dart | 35 +++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index c84548db2..299d67b5e 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -25,20 +25,24 @@ class ContextMenu extends StatelessWidget { color: PharMeTheme.onSurfaceColor, borderRadius: BorderRadius.circular(8), ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: items - .mapIndexed((index, item) => (index == items.count() - 1) - ? item - : Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0.5, color: PharMeTheme.borderColor), - )), - child: item)) - .toList(), + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .mapIndexed( + (index, item) => (index == items.count() - 1) + ? item + : Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0.5, + color: PharMeTheme.borderColor), + )), + child: item)) + .toList(), + ), ), ), ), @@ -79,14 +83,13 @@ class ContextMenuCheckmark extends StatelessWidget { child: Padding( padding: EdgeInsets.all(12), child: Row( - mainAxisSize: MainAxisSize.min, children: [ if (state) Icon(Icons.check_rounded, size: 16) else SizedBox(width: 16, height: 16), SizedBox(width: 8), - Text(label), + Expanded(child: Text(label)), ], ), ), From 311fffaefc233b1ef95cb96611f09546bf51419a Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:00:55 +0100 Subject: [PATCH 15/19] chore(app): order deps --- app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index ac7aae1c5..3681435af 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -36,8 +36,8 @@ dependencies: intl: ^0.17.0 json_annotation: ^4.6.0 path_provider: ^2.0.11 - popover: ^0.2.8+1 pdf: ^3.8.1 + popover: ^0.2.8+1 shared_preferences: ^2.0.15 url_launcher: ^6.1.4 From f41ba33321f76abd738a94d17ffab679e5024049 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:01:16 +0100 Subject: [PATCH 16/19] test(#479): fix settings page test --- app/integration_test/settings_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/integration_test/settings_test.dart b/app/integration_test/settings_test.dart index 8a43fa50e..8954b95e7 100644 --- a/app/integration_test/settings_test.dart +++ b/app/integration_test/settings_test.dart @@ -44,7 +44,7 @@ void main() { ); // close dialog - await tester.tap(find.text(context.l10n.settings_page_cancel)); + await tester.tap(find.text(context.l10n.action_cancel)); await tester.pumpAndSettle(); // test onboarding button From 005f23b291af39975177d67b9e8e3bebfacf2343 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:16:04 +0100 Subject: [PATCH 17/19] fix(#479): linting --- app/lib/common/widgets/context_menu.dart | 7 ++++--- app/lib/search/pages/search.dart | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index 299d67b5e..bb59f9565 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -60,18 +60,19 @@ class ContextMenu extends StatelessWidget { } class ContextMenuCheckmark extends StatelessWidget { - ContextMenuCheckmark( + const ContextMenuCheckmark( {super.key, required this.label, required this.setState, - this.state = false}); + this.initialState = false}); final String label; final void Function(bool) setState; - bool state; + final bool initialState; @override Widget build(BuildContext context) { + var state = initialState; return StatefulBuilder( builder: (context, rebuild) => GestureDetector( onTap: () { diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 5798ebec8..b06e5284d 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -51,7 +51,7 @@ class SearchPage extends HookWidget { ContextMenuCheckmark( label: context.l10n.search_page_filter_inactive, setState: (state) => cubit.search(showInactive: state), - state: filter?.showInactive ?? false), + initialState: filter?.showInactive ?? false), ...WarningLevel.values.map((level) => ContextMenuCheckmark( label: { WarningLevel.green: context.l10n.search_page_filter_green, @@ -60,7 +60,7 @@ class SearchPage extends HookWidget { WarningLevel.none: context.l10n.search_page_filter_gray, }[level]!, setState: (state) => cubit.search(showWarningLevel: {level: state}), - state: filter?.showWarningLevel[level] ?? false)) + initialState: filter?.showWarningLevel[level] ?? false)) ], child: Padding( padding: EdgeInsets.all(8), child: Icon(Icons.filter_list_rounded)), From c0b9fac79ba69211b03c279e6ec7379cc66e6660 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:26:11 +0100 Subject: [PATCH 18/19] test(#479): fix search test --- app/integration_test/search_test.dart | 4 ++-- app/lib/search/pages/cubit.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/integration_test/search_test.dart b/app/integration_test/search_test.dart index 22884a99c..9f0cffc71 100644 --- a/app/integration_test/search_test.dart +++ b/app/integration_test/search_test.dart @@ -8,7 +8,7 @@ import 'package:mocktail/mocktail.dart'; class MockSearchCubit extends MockCubit implements SearchCubit { @override - bool get filterActive => false; + FilterState get filter => FilterState.initial(); } void main() { @@ -71,7 +71,7 @@ void main() { testWidgets('test search page in loaded state', (tester) async { when(() => mockSearchCubit.state) - .thenReturn(SearchState.loaded(loadedDrugs, loadedDrugs)); + .thenReturn(SearchState.loaded(loadedDrugs, FilterState.initial())); await tester.pumpWidget(BlocProvider.value( value: mockSearchCubit, diff --git a/app/lib/search/pages/cubit.dart b/app/lib/search/pages/cubit.dart index 58b73c8d2..2a0de9c82 100644 --- a/app/lib/search/pages/cubit.dart +++ b/app/lib/search/pages/cubit.dart @@ -95,7 +95,7 @@ class FilterState { final warningLevel = drug.userGuideline()?.annotations.warningLevel; return drug.matches(query: query) && (drug.isActive() || showInactive) && - showWarningLevel[warningLevel]!; + (showWarningLevel[warningLevel] ?? true); } List filter(List drugs) => drugs.filter(isAccepted).toList(); From 5148954280b856b4d0ce322855df723f3a9463ac Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 25 Feb 2023 09:25:38 -0500 Subject: [PATCH 19/19] ci: upgrade flutter action --- .github/workflows/app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index c8466b860..1d55943d6 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -13,7 +13,7 @@ defaults: env: JAVA_VERSION: 12.x FLUTTER_CHANNEL: stable - FLUTTER_VERSION: 3.0.3 + FLUTTER_VERSION: 3.3.4 jobs: lint: @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-java@v1 with: java-version: ${{ env.JAVA_VERSION }} - - uses: subosito/flutter-action@v1 + - uses: subosito/flutter-action@v2 with: channel: ${{ env.FLUTTER_CHANNEL }} flutter-version: ${{ env.FLUTTER_VERSION }}