diff --git a/app/lib/common/models/drug/warning_level.dart b/app/lib/common/models/drug/warning_level.dart index bb6e88ac..c8e0cda4 100644 --- a/app/lib/common/models/drug/warning_level.dart +++ b/app/lib/common/models/drug/warning_level.dart @@ -1,6 +1,7 @@ -import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; +import '../../module.dart'; + part 'warning_level.g.dart'; @HiveType(typeId: 11) @@ -20,10 +21,18 @@ extension WarningLevelIcon on WarningLevel { WarningLevel.red.name: Icons.dangerous_rounded, WarningLevel.yellow.name: Icons.warning_rounded, WarningLevel.green.name: Icons.check_circle_rounded, + WarningLevel.none.name: Icons.help_rounded, + }; + + static final _outlinedIconMap = { + WarningLevel.red.name: Icons.dangerous_outlined, + WarningLevel.yellow.name: Icons.warning_amber_rounded, + WarningLevel.green.name: Icons.check_circle_outline_outlined, WarningLevel.none.name: Icons.help_outline_rounded, }; IconData get icon => WarningLevelIcon._iconMap[name]!; + IconData get outlinedIcon => WarningLevelIcon._outlinedIconMap[name]!; } extension WarningLevelSeverity on WarningLevel { @@ -35,3 +44,15 @@ extension WarningLevelSeverity on WarningLevel { }; int get severity => WarningLevelSeverity._severityMap[name]!; } + +extension WarningLevelColor on WarningLevel { + static final _colorMap = { + WarningLevel.red.name: Color(0xffffafaf), + WarningLevel.yellow.name: Color(0xffffebcc), + WarningLevel.green.name: Color(0xffcfe8cf), + WarningLevel.none.name: Color(0xffcfe8cf), + }; + + Color get color => WarningLevelColor._colorMap[name]!; + Color get textColor => darkenColor(color, 0.4); +} diff --git a/app/lib/common/theme.dart b/app/lib/common/theme.dart index 6a7cad6c..27167b0c 100644 --- a/app/lib/common/theme.dart +++ b/app/lib/common/theme.dart @@ -113,14 +113,3 @@ class AppBarTheme { final elevation = 0.0; final centerTitle = false; } - -extension WarningLevelColor on WarningLevel { - static final _colorMap = { - WarningLevel.red.name: Color(0xffffafaf), - WarningLevel.yellow.name: Color(0xffffebcc), - WarningLevel.green.name: Color(0xffcfe8cf), - WarningLevel.none.name: Color(0xffcfe8cf), - }; - - Color get color => WarningLevelColor._colorMap[name]!; -} diff --git a/app/lib/common/utilities/color_utils.dart b/app/lib/common/utilities/color_utils.dart index f8450573..c2a47c7b 100644 --- a/app/lib/common/utilities/color_utils.dart +++ b/app/lib/common/utilities/color_utils.dart @@ -7,4 +7,4 @@ Color darkenColor(Color color, [double amount = 0.1]) { final hsl = HSLColor.fromColor(color); final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); return hslDark.toColor(); -} \ No newline at end of file +} diff --git a/app/lib/common/widgets/drug_list/drug_items/drug_cards.dart b/app/lib/common/widgets/drug_list/drug_items/drug_cards.dart index dd21c9db..a41c223d 100644 --- a/app/lib/common/widgets/drug_list/drug_items/drug_cards.dart +++ b/app/lib/common/widgets/drug_list/drug_items/drug_cards.dart @@ -55,15 +55,20 @@ class DrugCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Icon(drug.warningLevel.icon), - SizedBox(width: 4), - Text( - drugName, - style: PharMeTheme.textTheme.titleMedium! - .copyWith(fontWeight: FontWeight.bold), - ), - ]), + Row( + children: [ + Icon( + drug.warningLevel.icon, + color: PharMeTheme.onSurfaceText, + ), + SizedBox(width: 4), + Text( + drugName, + style: PharMeTheme.textTheme.titleMedium! + .copyWith(fontWeight: FontWeight.bold), + ), + ], + ), SizedBox(height: PharMeTheme.smallSpace / 2), if (drug.annotations.brandNames.isNotEmpty) ...[ SizedBox(width: PharMeTheme.smallSpace / 2), diff --git a/app/lib/common/widgets/drug_search.dart b/app/lib/common/widgets/drug_search.dart deleted file mode 100644 index 2cdefe87..00000000 --- a/app/lib/common/widgets/drug_search.dart +++ /dev/null @@ -1,165 +0,0 @@ - -import 'package:flutter/cupertino.dart'; -import 'package:provider/provider.dart'; - -import '../../../common/module.dart'; -import '../../drug/widgets/tooltip_icon.dart'; - -class DrugSearch extends HookWidget { - DrugSearch({ - super.key, - required this.showFilter, - required this.buildDrugItems, - required this.showDrugInteractionIndicator, - this.useDrugClass = true, - this.keepPosition = false, - this.drugItemsBuildParams, - DrugListCubit? cubit, - }) : cubit = cubit ?? DrugListCubit(); - - final bool showFilter; - final bool useDrugClass; - final bool keepPosition; - final List Function( - BuildContext context, - List drugs, - { - Map? buildParams, - bool showDrugInteractionIndicator, - } - ) buildDrugItems; - final bool showDrugInteractionIndicator; - final DrugListCubit cubit; - final Map? drugItemsBuildParams; - - @override - Widget build(BuildContext context) { - final searchController = useTextEditingController(); - final amendment = showFilter - ? context.l10n.search_no_drugs_with_filter_amendment - : ''; - final noDrugsMessage = context.l10n.search_no_drugs(amendment); - return Consumer( - builder: (context, activeDrugs, child) => BlocProvider( - create: (context) => cubit, - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: PharMeTheme.smallSpace), - child: Row( - children: [ - Expanded( - child: CupertinoSearchTextField( - controller: searchController, - onChanged: (value) { - context.read().search(query: value); - }, - ), - ), - SizedBox(width: PharMeTheme.smallToMediumSpace), - TooltipIcon(useDrugClass - ? context.l10n.search_page_tooltip_search - : context.l10n.search_page_tooltip_search_no_class - ), - if (showFilter) buildFilter(context), - ] - ), - ), - scrollList( - keepPosition: keepPosition, - buildDrugList( - context, - state, - activeDrugs, - buildDrugItems: buildDrugItems, - noDrugsMessage: noDrugsMessage, - drugItemsBuildParams: drugItemsBuildParams, - showDrugInteractionIndicator: - showDrugInteractionIndicator, - useDrugClass: useDrugClass, - ) - ), - state.when( - initial: SizedBox.shrink, - loading: SizedBox.shrink, - loaded: (allDrugs, filter) => - maybeBuildInteractionIndicator( - context, - activeDrugs, - allDrugs, - filter, - ), - error: SizedBox.shrink, - ) - ], - ); - } - ) - ) - ); - } - - Widget buildFilter(BuildContext context) { - final cubit = context.read(); - final filter = cubit.filter; - return FilterMenu( - items: [ - ...WarningLevel.values - .filter((level) => level != WarningLevel.none) - .map((level) => FilterMenuItem( - title: { - WarningLevel.green: context.l10n.search_page_filter_green, - WarningLevel.yellow: context.l10n.search_page_filter_yellow, - WarningLevel.red: context.l10n.search_page_filter_red, - }[level]!, - updateSearch: ({ required isChecked }) => - cubit.search(showWarningLevel: { level: isChecked }), - isChecked: filter?.showWarningLevel[level] ?? false - ) - ), - FilterMenuItem( - title: context.l10n.search_page_filter_only_active, - // Invert state as filter has opposite meaning ('only show active' vs. - // 'show inactive') - updateSearch: ({ required isChecked }) => cubit.search(showInactive: !isChecked), - isChecked: !(filter?.showInactive ?? false) - ), - FilterMenuItem( - title: context.l10n.search_page_filter_only_with_guidelines, - // Invert state as filter has opposite meaning ('show only with - // guidelines' vs. 'show with unknown warning level') - updateSearch: ({ required isChecked }) => cubit.search( - showWarningLevel: { WarningLevel.none: !isChecked } - ), - isChecked: !(filter?.showWarningLevel[WarningLevel.none] ?? false), - ) - ], - iconData: Icons.filter_list_rounded, - ); - } - Widget maybeBuildInteractionIndicator( - BuildContext context, - ActiveDrugs activeDrugs, - List drugs, - FilterState filter, - ) { - if (showDrugInteractionIndicator) { - final filteredDrugs = filter.filter( - drugs, - activeDrugs, - useDrugClass: useDrugClass, - ); - if (filteredDrugs.any((drug) => isInhibitor(drug.name))) { - return PageIndicatorExplanation( - context.l10n.search_page_indicator_explanation( - drugInteractionIndicatorName, - drugInteractionIndicator - ), - ); - } - } - return SizedBox.shrink(); - } -} diff --git a/app/lib/common/widgets/drug_search/builder.dart b/app/lib/common/widgets/drug_search/builder.dart new file mode 100644 index 00000000..35a2b263 --- /dev/null +++ b/app/lib/common/widgets/drug_search/builder.dart @@ -0,0 +1,137 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; + +import '../../../../common/module.dart'; +import '../../../drug/widgets/tooltip_icon.dart'; +import 'filter_menu.dart'; + +class DrugSearch extends HookWidget { + DrugSearch({ + super.key, + required this.showFilter, + required this.buildDrugItems, + required this.showDrugInteractionIndicator, + this.useDrugClass = true, + this.keepPosition = false, + this.drugItemsBuildParams, + DrugListCubit? cubit, + }) : cubit = cubit ?? DrugListCubit(); + + final bool showFilter; + final bool useDrugClass; + final bool keepPosition; + final List Function( + BuildContext context, + List drugs, + { + Map? buildParams, + bool showDrugInteractionIndicator, + } + ) buildDrugItems; + final bool showDrugInteractionIndicator; + final DrugListCubit cubit; + final Map? drugItemsBuildParams; + + @override + Widget build(BuildContext context) { + final searchController = useTextEditingController(); + return Consumer( + builder: (context, activeDrugs, child) => BlocProvider( + create: (context) => cubit, + child: BlocBuilder( + builder: (context, state) { + 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), + if (showFilter) FilterMenu(cubit, state), + ], + ), + ), + scrollList( + keepPosition: keepPosition, + buildDrugList( + context, + state, + activeDrugs, + buildDrugItems: buildDrugItems, + noDrugsMessage: context.l10n.search_no_drugs( + showFilter + ? context.l10n.search_no_drugs_with_filter_amendment + : '' + ), + drugItemsBuildParams: drugItemsBuildParams, + showDrugInteractionIndicator: + showDrugInteractionIndicator, + useDrugClass: useDrugClass, + ) + ), + _maybeBuildInteractionIndicator(context, state, activeDrugs) + ?? SizedBox.shrink(), + ], + ); + } + ) + ) + ); + } + + List _buildSearchBarItems( + BuildContext context, + TextEditingController searchController, + ) { + return [ + Expanded( + child: CupertinoSearchTextField( + controller: searchController, + onChanged: (value) { + context.read().search( + query: value, + ); + }, + ), + ), + SizedBox(width: PharMeTheme.smallToMediumSpace), + TooltipIcon(useDrugClass + ? context.l10n.search_page_tooltip_search + : context.l10n.search_page_tooltip_search_no_class + ), + ]; + } + + Widget? _maybeBuildInteractionIndicator( + BuildContext context, + DrugListState state, + ActiveDrugs activeDrugs, + ) { + return state.whenOrNull( + loaded: (drugs, filter) { + if (showDrugInteractionIndicator) { + final filteredDrugs = filter.filter( + drugs, + activeDrugs, + useDrugClass: useDrugClass, + ); + 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/filter_menu.dart b/app/lib/common/widgets/drug_search/filter_menu.dart new file mode 100644 index 00000000..bc735b17 --- /dev/null +++ b/app/lib/common/widgets/drug_search/filter_menu.dart @@ -0,0 +1,92 @@ +import '../../module.dart'; + +class FilterMenuItem { + FilterMenuItem({ + required bool initialValue, + required this.updateSearch, + required this.build, + }) : _value = initialValue; + + bool _value; + // ignore: avoid_positional_boolean_parameters + final void Function(bool newValue) updateSearch; + final Widget Function(BuildContext context, { + required bool value, + required Function statefulOnChange, + }) build; + + set value(newValue) => _value = newValue; + bool get value => _value; +} + +class FilterMenu extends HookWidget { + const FilterMenu(this.cubit, this.state); + + final DrugListCubit cubit; + final DrugListState state; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: Icon(Icons.filter_list), + color: PharMeTheme.onSurfaceColor, + elevation: 0, + itemBuilder: (context) => _menuItems.map( + (item) => PopupMenuItem(child: StatefulBuilder( + builder: (context, setState) { + return item.build( + context, + value: item.value, + statefulOnChange: ([_]) { + final newValue = !item.value; + setState(() => item.value = newValue); + item.updateSearch(newValue); + }, + ); + }), + ), + ).toList(), + ); + } + + List get _menuItems => [ + FilterMenuItem( + initialValue: cubit.filter?.showInactive ?? false, + updateSearch: (newValue) => cubit.search(showInactive: newValue), + build: (context, { required value, required statefulOnChange }) => + DropdownButton( + value: value, + items: [ + DropdownMenuItem( + value: true, + child: Text('${context.l10n.search_page_filter_all_drugs} '), + ), + DropdownMenuItem( + value: false, + child: Text('${context.l10n.search_page_filter_only_active_drugs} '), + ), + ], + onChanged: (newValue) => statefulOnChange(newValue), + ), + ), + ...WarningLevel.values + .filter((warningLevel) => warningLevel != WarningLevel.none) + .map((warningLevel) => FilterMenuItem( + initialValue: cubit.filter?.showWarningLevel[warningLevel] ?? false, + updateSearch: (newValue) => cubit.search( + showWarningLevel: { warningLevel: newValue }, + ), + build: (context, { required value, required statefulOnChange }) => + ActionChip( + onPressed: () => statefulOnChange(!value), + avatar: Icon( + value ? warningLevel.icon : warningLevel.outlinedIcon, + color: value ? PharMeTheme.onSurfaceText : warningLevel.textColor, + ), + label: Text('', style: TextStyle(color: PharMeTheme.onSurfaceText)), + visualDensity: VisualDensity.compact, + color: MaterialStatePropertyAll(value ? warningLevel.color : Colors.transparent), + ), + )), + ]; +} diff --git a/app/lib/common/widgets/filter_menu.dart b/app/lib/common/widgets/filter_menu.dart deleted file mode 100644 index 06fb25f2..00000000 --- a/app/lib/common/widgets/filter_menu.dart +++ /dev/null @@ -1,57 +0,0 @@ -import '../module.dart'; - -class FilterMenuItem { - FilterMenuItem({ - required this.title, - required this.updateSearch, - required bool isChecked, - }) : _isChecked = isChecked; - - final String title; - final void Function({ required bool isChecked }) updateSearch; - bool _isChecked; - - set checked(newValue) => _isChecked = newValue; - bool get checked => _isChecked; -} - -class FilterMenu extends HookWidget { - const FilterMenu({ - super.key, - this.headerItem, - required this.items, - required this.iconData, - }); - - final Widget? headerItem; - final List items; - final IconData iconData; - - @override - Widget build(BuildContext context) { - return PopupMenuButton( - icon: Icon(iconData), - color: PharMeTheme.onSurfaceColor, - elevation: 0, - itemBuilder: (context) => items.map( - (item) => PopupMenuItem( - child: StatefulBuilder(builder: (context, setState) { - void toggleCheckbox([_]) { - final newValue = !item.checked; - setState(() => item.checked = newValue); - item.updateSearch(isChecked: newValue); - } - return ListTile( - title: Text(item.title), - leading: CheckboxWrapper( - isChecked: item.checked, - onChanged: toggleCheckbox, - ), - onTap: toggleCheckbox, - ); - }), - ) - ).toList(), - ); - } -} \ No newline at end of file diff --git a/app/lib/common/widgets/module.dart b/app/lib/common/widgets/module.dart index b9a33213..e3e93aa9 100644 --- a/app/lib/common/widgets/module.dart +++ b/app/lib/common/widgets/module.dart @@ -6,8 +6,8 @@ export 'dialog_content_text.dart'; export 'dialog_wrapper.dart'; export 'drug_list/builder.dart'; export 'drug_list/cubit.dart'; +export 'drug_search/builder.dart'; export 'error_handler.dart'; -export 'filter_menu.dart'; export 'full_width_button.dart'; export 'headings.dart'; export 'indicators.dart'; diff --git a/app/lib/drug_selection/pages/drug_selection.dart b/app/lib/drug_selection/pages/drug_selection.dart index ec6ec38a..160d42e7 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -3,7 +3,6 @@ import 'package:provider/provider.dart'; import '../../common/models/metadata.dart'; import '../../common/module.dart' hide MetaData; import '../../common/widgets/drug_list/drug_items/drug_checkbox_list.dart'; -import '../../common/widgets/drug_search.dart'; import '../cubit.dart'; @RoutePage() diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index c51d7926..60116189 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -56,7 +56,8 @@ "search_page_tooltip_search": "Search for drugs by their name, brand name or class.", "search_page_tooltip_search_no_class": "Search for drugs by their name or brand name.", - "search_page_filter_only_active": "Only active drugs", + "search_page_filter_all_drugs": "All drugs", + "search_page_filter_only_active_drugs": "Currently taken drugs", "search_page_filter_green": "Green warning level", "search_page_filter_yellow": "Yellow warning level", "search_page_filter_red": "Red warning level", diff --git a/app/lib/report/pages/report.dart b/app/lib/report/pages/report.dart index edd93649..e5910627 100644 --- a/app/lib/report/pages/report.dart +++ b/app/lib/report/pages/report.dart @@ -92,7 +92,6 @@ class GeneCard extends StatelessWidget { final warningLevelCount = affectedDrugs.filter( (drug) => drug.warningLevel == warningLevel ).length; - final textColor = darkenColor(warningLevel.color, 0.4); return warningLevelCount > 0 ? Row( textDirection: TextDirection.ltr, @@ -100,12 +99,12 @@ class GeneCard extends StatelessWidget { Icon( warningLevel.icon, size: PharMeTheme.mediumSpace, - color: textColor, + color: warningLevel.textColor, ), SizedBox(width: PharMeTheme.smallSpace * 0.4), Text( warningLevelCount.toString(), - style: TextStyle(color: textColor), + style: TextStyle(color: warningLevel.textColor), ), SizedBox(width: PharMeTheme.smallSpace * 0.8), ] diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 7ba0bed0..566e00f3 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -1,6 +1,5 @@ import '../../../common/module.dart'; import '../../common/widgets/drug_list/drug_items/drug_cards.dart'; -import '../../common/widgets/drug_search.dart'; @RoutePage() class SearchPage extends HookWidget {