diff --git a/packages/smooth_app/lib/generic_lib/dialogs/smooth_alert_dialog.dart b/packages/smooth_app/lib/generic_lib/dialogs/smooth_alert_dialog.dart index 3503bd1d69e..c5c26eb820a 100644 --- a/packages/smooth_app/lib/generic_lib/dialogs/smooth_alert_dialog.dart +++ b/packages/smooth_app/lib/generic_lib/dialogs/smooth_alert_dialog.dart @@ -5,6 +5,7 @@ import 'package:smooth_app/generic_lib/buttons/smooth_simple_button.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_responsive.dart'; import 'package:smooth_app/helpers/app_helper.dart'; +import 'package:smooth_app/helpers/keyboard_helper.dart'; /// Custom Dialog to use in the app /// @@ -538,3 +539,77 @@ class SmoothSimpleErrorAlertDialog extends StatelessWidget { ); } } + +class SmoothListAlertDialog extends StatelessWidget { + SmoothListAlertDialog({ + required this.title, + required this.list, + this.header, + ScrollController? scrollController, + this.positiveAction, + this.negativeAction, + this.actionsAxis, + this.actionsOrder, + }) : _scrollController = scrollController ?? ScrollController(); + + final String title; + final Widget? header; + final Widget list; + final SmoothActionButton? positiveAction; + final SmoothActionButton? negativeAction; + final Axis? actionsAxis; + final SmoothButtonsBarOrder? actionsOrder; + final ScrollController _scrollController; + + @override + Widget build(BuildContext context) { + return SmoothAlertDialog( + contentPadding: const EdgeInsetsDirectional.symmetric( + horizontal: 0.0, + vertical: SMALL_SPACE, + ), + body: SizedBox( + height: MediaQuery.of(context).size.height / + (context.keyboardVisible ? 1.0 : 1.5), + width: MediaQuery.of(context).size.width, + child: Column( + children: [ + Container( + alignment: AlignmentDirectional.centerStart, + padding: const EdgeInsetsDirectional.only( + start: 23.0, + end: 24.0, + top: SMALL_SPACE, + ), + child: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20.0, + ), + ), + ), + if (header != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: SMALL_SPACE, + vertical: MEDIUM_SPACE, + ), + child: header, + ), + Expanded( + child: Scrollbar( + controller: _scrollController, + child: list, + ), + ) + ], + ), + ), + positiveAction: positiveAction, + negativeAction: negativeAction, + actionsAxis: actionsAxis, + actionsOrder: actionsOrder, + ); + } +} diff --git a/packages/smooth_app/lib/generic_lib/widgets/language_selector.dart b/packages/smooth_app/lib/generic_lib/widgets/language_selector.dart index 7d5f726f962..71f1424cc14 100644 --- a/packages/smooth_app/lib/generic_lib/widgets/language_selector.dart +++ b/packages/smooth_app/lib/generic_lib/widgets/language_selector.dart @@ -6,6 +6,7 @@ import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; import 'package:smooth_app/pages/preferences/user_preferences_languages_list.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/widgets/smooth_text.dart'; class LanguageSelector extends StatelessWidget { const LanguageSelector({ @@ -100,6 +101,7 @@ class LanguageSelector extends StatelessWidget { final BuildContext context, { final Iterable? selectedLanguages, }) async { + final ScrollController scrollController = ScrollController(); final AppLocalizations appLocalizations = AppLocalizations.of(context); final TextEditingController languageSelectorController = TextEditingController(); @@ -135,70 +137,67 @@ class LanguageSelector extends StatelessWidget { BuildContext context, void Function(VoidCallback fn) setState, ) => - SmoothAlertDialog( - body: SizedBox( - height: MediaQuery.of(context).size.height / 2, - width: MediaQuery.of(context).size.width, - child: Column( - children: [ - SmoothTextFormField( - type: TextFieldTypes.PLAIN_TEXT, - hintText: appLocalizations.search, - prefixIcon: const Icon(Icons.search), - controller: languageSelectorController, - onChanged: (String? query) { - setState( - () { - filteredList = leftovers - .where((OpenFoodFactsLanguage item) => - _languages - .getNameInEnglish(item) - .toLowerCase() - .contains(query!.toLowerCase()) || - _languages - .getNameInLanguage(item) - .toLowerCase() - .contains(query.toLowerCase()) || - item.code.contains(query)) - .toList(); - }, - ); - }, - ), - Expanded( - child: ListView.builder( - itemBuilder: (BuildContext context, int index) { - final OpenFoodFactsLanguage language = - filteredList[index]; - final String nameInLanguage = - _languages.getNameInLanguage(language); - final String nameInEnglish = - _languages.getNameInEnglish(language); - final bool selected = selectedLanguages != null && - selectedLanguages.contains(language); - return ListTile( - dense: true, - trailing: selected ? const Icon(Icons.check) : null, - title: Text( - '$nameInLanguage ($nameInEnglish)', - softWrap: false, - overflow: TextOverflow.fade, - style: selected - ? const TextStyle(fontWeight: FontWeight.bold) - : null, - ), - onTap: () => Navigator.of(context).pop(language), - ); - }, - itemCount: filteredList.length, - shrinkWrap: true, - ), + SmoothListAlertDialog( + title: appLocalizations.language_selector_title, + header: SmoothTextFormField( + type: TextFieldTypes.PLAIN_TEXT, + hintText: appLocalizations.search, + prefixIcon: const Icon(Icons.search), + controller: languageSelectorController, + onChanged: (String? query) { + query = query?.trim().toLowerCase(); + + setState( + () { + filteredList = leftovers + .where((OpenFoodFactsLanguage item) => + _languages + .getNameInEnglish(item) + .toLowerCase() + .contains(query!.toLowerCase()) || + _languages + .getNameInLanguage(item) + .toLowerCase() + .contains(query.toLowerCase()) || + item.code.contains(query)) + .toList(); + }, + ); + }, + ), + scrollController: scrollController, + list: ListView.separated( + controller: scrollController, + itemBuilder: (BuildContext context, int index) { + final OpenFoodFactsLanguage language = filteredList[index]; + final String nameInLanguage = + _languages.getNameInLanguage(language); + final String nameInEnglish = + _languages.getNameInEnglish(language); + final bool selected = selectedLanguages != null && + selectedLanguages.contains(language); + return ListTile( + dense: true, + trailing: selected ? const Icon(Icons.check) : null, + title: TextHighlighter( + text: '$nameInLanguage ($nameInEnglish)', + filter: languageSelectorController.text, + selected: selected, ), - ], + onTap: () => Navigator.of(context).pop(language), + ); + }, + separatorBuilder: (_, __) => const Divider( + height: 1.0, ), + itemCount: filteredList.length, + shrinkWrap: true, ), positiveAction: SmoothActionButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + languageSelectorController.clear(); + Navigator.of(context).pop(); + }, text: appLocalizations.cancel, ), ), diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 77c0d5293f2..7bbc2c81782 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -2353,6 +2353,10 @@ "@country_selector_title": { "description": "Label written as the title of the dialog to select the user country" }, + "language_selector_title": "Select your language:", + "@language_selector_title": { + "description": "Label written as the title of the dialog to select the user language" + }, "action_delete_list": "Delete", "@action_delete_list": { "description": "Delete a list action in a menu" diff --git a/packages/smooth_app/lib/pages/onboarding/country_selector.dart b/packages/smooth_app/lib/pages/onboarding/country_selector.dart index e2672113de2..0c005dc4292 100644 --- a/packages/smooth_app/lib/pages/onboarding/country_selector.dart +++ b/packages/smooth_app/lib/pages/onboarding/country_selector.dart @@ -8,8 +8,8 @@ import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; -import 'package:smooth_app/helpers/keyboard_helper.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/widgets/smooth_text.dart'; /// A selector for selecting user's country. class CountrySelector extends StatefulWidget { @@ -28,6 +28,7 @@ class CountrySelector extends StatefulWidget { } class _CountrySelectorState extends State { + final ScrollController _scrollController = ScrollController(); final TextEditingController _countryController = TextEditingController(); late List _countryList; late Future _initFuture; @@ -87,103 +88,61 @@ class _CountrySelectorState extends State { void Function(VoidCallback fn) setState) { const double horizontalPadding = 16.0 + SMALL_SPACE; - return SmoothAlertDialog( - contentPadding: const EdgeInsetsDirectional.symmetric( - horizontal: 0.0, - vertical: SMALL_SPACE, + return SmoothListAlertDialog( + title: appLocalizations.country_selector_title, + header: SmoothTextFormField( + type: TextFieldTypes.PLAIN_TEXT, + prefixIcon: const Icon(Icons.search), + controller: _countryController, + onChanged: (String? query) { + query = query?.trim().toLowerCase(); + + setState( + () { + filteredList = _countryList + .where( + (Country item) => + item.name.toLowerCase().contains( + query!, + ) || + item.countryCode.toLowerCase().contains( + query, + ), + ) + .toList(growable: false); + }, + ); + }, + hintText: appLocalizations.search, ), - body: SizedBox( - height: MediaQuery.of(context).size.height / - (context.keyboardVisible ? 1.0 : 1.5), - width: MediaQuery.of(context).size.width, - child: Column( - children: [ - Container( - alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsetsDirectional.only( - start: horizontalPadding - 1.0, - end: horizontalPadding, - top: SMALL_SPACE, - ), - child: Text( - appLocalizations.country_selector_title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0, - ), - ), + scrollController: _scrollController, + list: ListView.separated( + controller: _scrollController, + itemBuilder: (BuildContext context, int index) { + final Country country = filteredList[index]; + final bool selected = country == selectedCountry; + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: horizontalPadding, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: SMALL_SPACE, - vertical: MEDIUM_SPACE, - ), - child: SmoothTextFormField( - type: TextFieldTypes.PLAIN_TEXT, - prefixIcon: const Icon(Icons.search), - controller: _countryController, - onChanged: (String? query) { - setState( - () { - filteredList = _countryList - .where( - (Country item) => - item.name - .toLowerCase() - .contains( - query!.toLowerCase(), - ) || - item.countryCode - .toLowerCase() - .contains( - query.toLowerCase(), - ), - ) - .toList(growable: false); - }, - ); - }, - hintText: appLocalizations.search, - ), + trailing: selected ? const Icon(Icons.check) : null, + title: TextHighlighter( + text: country.name, + filter: _countryController.text, + selected: selected, ), - Expanded( - child: Scrollbar( - child: ListView.separated( - itemBuilder: - (BuildContext context, int index) { - final Country country = filteredList[index]; - final bool selected = - country == selectedCountry; - return ListTile( - dense: true, - contentPadding: - const EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - trailing: selected - ? const Icon(Icons.check) - : null, - title: _FilterableText( - text: country.name, - filter: _countryController.text, - selected: selected, - ), - onTap: () { - Navigator.of(context).pop(country); - _countryController.clear(); - }, - ); - }, - separatorBuilder: (_, __) => const Divider( - height: 1.0, - ), - itemCount: filteredList.length, - shrinkWrap: true, - ), - ), - ) - ], + onTap: () { + Navigator.of(context).pop(country); + _countryController.clear(); + }, + ); + }, + separatorBuilder: (_, __) => const Divider( + height: 1.0, ), + itemCount: filteredList.length, + shrinkWrap: true, ), positiveAction: SmoothActionButton( onPressed: () { @@ -330,80 +289,3 @@ class _CountrySelectorState extends State { super.dispose(); } } - -class _FilterableText extends StatelessWidget { - const _FilterableText({ - required this.text, - required this.filter, - required this.selected, - }); - - final String text; - final String filter; - final bool selected; - - @override - Widget build(BuildContext context) { - final List<(String, TextStyle?)> parts = _getParts( - defaultStyle: TextStyle(fontWeight: selected ? FontWeight.bold : null), - highlightedStyle: TextStyle( - fontWeight: selected ? FontWeight.bold : null, - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2), - ), - ); - - final TextStyle defaultTextStyle = DefaultTextStyle.of(context).style; - - return Text.rich( - TextSpan( - children: parts.map(((String, TextStyle?) part) { - return TextSpan( - text: part.$1, - style: defaultTextStyle.merge(part.$2), - ); - }).toList(growable: false), - ), - softWrap: false, - overflow: TextOverflow.fade, - ); - } - - /// Returns a List containing parts of the text with the right style - /// according to the [filter] - List<(String, TextStyle?)> _getParts({ - required TextStyle? defaultStyle, - required TextStyle? highlightedStyle, - }) { - final Iterable highlightedParts = - RegExp(filter.toLowerCase()).allMatches( - text.toLowerCase(), - ); - - final List<(String, TextStyle?)> parts = <(String, TextStyle?)>[]; - - if (highlightedParts.isEmpty) { - parts.add((text, defaultStyle)); - } else { - parts - .add((text.substring(0, highlightedParts.first.start), defaultStyle)); - for (int i = 0; i != highlightedParts.length; i++) { - final RegExpMatch subPart = highlightedParts.elementAt(i); - - parts.add( - (text.substring(subPart.start, subPart.end), highlightedStyle), - ); - - if (i < highlightedParts.length - 1) { - parts.add(( - text.substring( - subPart.end, highlightedParts.elementAt(i + 1).start), - defaultStyle - )); - } else if (subPart.end < text.length) { - parts.add((text.substring(subPart.end, text.length), defaultStyle)); - } - } - } - return parts; - } -} diff --git a/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart b/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart index 126a35e5dc4..b513ed827bf 100644 --- a/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart +++ b/packages/smooth_app/lib/pages/product/nutrition_add_nutrient_button.dart @@ -7,6 +7,7 @@ import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; import 'package:smooth_app/pages/product/nutrition_container.dart'; import 'package:smooth_app/pages/text_field_helper.dart'; +import 'package:smooth_app/widgets/smooth_text.dart'; /// Button that opens an "add nutrient" dialog. /// @@ -36,6 +37,8 @@ class NutritionAddNutrientButton extends StatelessWidget { List.from(leftovers); final TextEditingControllerWithInitialValue nutritionTextController = TextEditingControllerWithInitialValue(); + final ScrollController controller = ScrollController(); + final OrderedNutrient? selected = await showDialog( context: context, builder: (BuildContext context) => StatefulBuilder( @@ -43,45 +46,44 @@ class NutritionAddNutrientButton extends StatelessWidget { BuildContext context, void Function(VoidCallback fn) setState, ) => - SmoothAlertDialog( - body: SizedBox( - height: MediaQuery.of(context).size.height / 2, - width: MediaQuery.of(context).size.width, - child: Column( - children: [ - SmoothTextFormField( - prefixIcon: const Icon(Icons.search), - hintText: appLocalizations.search, - type: TextFieldTypes.PLAIN_TEXT, - controller: nutritionTextController, - onChanged: (String? query) => setState( - () => filteredList = leftovers - .where((OrderedNutrient item) => - removeDiacritics(item.name!) - .toLowerCase() - .contains( - removeDiacritics(query!).toLowerCase())) - .toList(), - ), - ), - Expanded( - child: ListView.builder( - itemBuilder: (BuildContext context, int index) { - final OrderedNutrient nutrient = filteredList[index]; - return ListTile( - title: Text(nutrient.name!), - onTap: () => Navigator.of(context).pop(nutrient), - ); - }, - itemCount: filteredList.length, - shrinkWrap: true, - ), + SmoothListAlertDialog( + title: appLocalizations.nutrition_page_add_nutrient, + header: SmoothTextFormField( + prefixIcon: const Icon(Icons.search), + hintText: appLocalizations.search, + type: TextFieldTypes.PLAIN_TEXT, + controller: nutritionTextController, + onChanged: (String? query) => setState( + () => filteredList = leftovers + .where((OrderedNutrient item) => + removeDiacritics(item.name!).toLowerCase().contains( + removeDiacritics(query!).toLowerCase().trim())) + .toList(), + ), + ), + scrollController: controller, + list: ListView.separated( + itemBuilder: (BuildContext context, int index) { + final OrderedNutrient nutrient = filteredList[index]; + return ListTile( + title: TextHighlighter( + text: nutrient.name!, + filter: nutritionTextController.text, ), - ], + onTap: () => Navigator.of(context).pop(nutrient), + ); + }, + itemCount: filteredList.length, + shrinkWrap: true, + separatorBuilder: (_, __) => const Divider( + height: 1.0, ), ), positiveAction: SmoothActionButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + nutritionTextController.clear(); + Navigator.pop(context); + }, text: appLocalizations.cancel, ), ), diff --git a/packages/smooth_app/lib/widgets/smooth_text.dart b/packages/smooth_app/lib/widgets/smooth_text.dart index 3719c8749f2..41591e5ec3d 100644 --- a/packages/smooth_app/lib/widgets/smooth_text.dart +++ b/packages/smooth_app/lib/widgets/smooth_text.dart @@ -1,4 +1,4 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; /// An extension on [TextStyle] that allows to have "well spaced" variant extension TextStyleExtension on TextStyle { @@ -41,3 +41,80 @@ class WellSpacedTextHelper { textWidthBasis: textWidthBasis, ); } + +class TextHighlighter extends StatelessWidget { + const TextHighlighter({ + required this.text, + required this.filter, + this.selected = false, + }); + + final String text; + final String filter; + final bool selected; + + @override + Widget build(BuildContext context) { + final List<(String, TextStyle?)> parts = _getParts( + defaultStyle: TextStyle(fontWeight: selected ? FontWeight.bold : null), + highlightedStyle: TextStyle( + fontWeight: selected ? FontWeight.bold : null, + backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2), + ), + ); + + final TextStyle defaultTextStyle = DefaultTextStyle.of(context).style; + + return Text.rich( + TextSpan( + children: parts.map(((String, TextStyle?) part) { + return TextSpan( + text: part.$1, + style: defaultTextStyle.merge(part.$2), + ); + }).toList(growable: false), + ), + softWrap: false, + overflow: TextOverflow.fade, + ); + } + + /// Returns a List containing parts of the text with the right style + /// according to the [filter] + List<(String, TextStyle?)> _getParts({ + required TextStyle? defaultStyle, + required TextStyle? highlightedStyle, + }) { + final Iterable highlightedParts = + RegExp(filter.toLowerCase().trim()).allMatches( + text.toLowerCase(), + ); + + final List<(String, TextStyle?)> parts = <(String, TextStyle?)>[]; + + if (highlightedParts.isEmpty) { + parts.add((text, defaultStyle)); + } else { + parts + .add((text.substring(0, highlightedParts.first.start), defaultStyle)); + for (int i = 0; i != highlightedParts.length; i++) { + final RegExpMatch subPart = highlightedParts.elementAt(i); + + parts.add( + (text.substring(subPart.start, subPart.end), highlightedStyle), + ); + + if (i < highlightedParts.length - 1) { + parts.add(( + text.substring( + subPart.end, highlightedParts.elementAt(i + 1).start), + defaultStyle + )); + } else if (subPart.end < text.length) { + parts.add((text.substring(subPart.end, text.length), defaultStyle)); + } + } + } + return parts; + } +}