From a9e216c72d467533da9ea815eb69131f5cc909bf Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Mon, 22 Jul 2024 10:50:43 +0200 Subject: [PATCH] feat: Popup Menu now uses the iOS actions sheet (#5494) * iOS popup menu (cherry picked from commit 493f3d2ebbf444579f8c03ff0ac7d5f8b9db55e7) * Reorder mandatory fields --- packages/smooth_app/lib/l10n/app_en.arb | 4 + .../lib/pages/personalized_ranking_page.dart | 9 +- .../common/product_list_item_popup_items.dart | 18 ++- .../product/common/product_list_page.dart | 11 +- .../common/product_list_popup_items.dart | 18 ++- .../lib/widgets/smooth_menu_button.dart | 118 ++++++++++++++++++ packages/smooth_app/macos/Podfile.lock | 2 +- 7 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 packages/smooth_app/lib/widgets/smooth_menu_button.dart diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 4950480c499..01a2f992a77 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -2960,5 +2960,9 @@ "prices_feedback_form": "Click here to send us your feedback about this new feature!", "@prices_feedback_form": { "description": "A button to send feedback about the prices feature" + }, + "menu_button_list_actions": "Select an action", + "@menu_button_list_actions": { + "description": "Button to select an action in a list (eg: Share, Delete, …)" } } \ No newline at end of file diff --git a/packages/smooth_app/lib/pages/personalized_ranking_page.dart b/packages/smooth_app/lib/pages/personalized_ranking_page.dart index 4b6d42ea9d8..8441d94c516 100644 --- a/packages/smooth_app/lib/pages/personalized_ranking_page.dart +++ b/packages/smooth_app/lib/pages/personalized_ranking_page.dart @@ -16,6 +16,7 @@ import 'package:smooth_app/pages/product/common/loading_status.dart'; import 'package:smooth_app/pages/product/common/product_list_item_simple.dart'; import 'package:smooth_app/pages/product_list_user_dialog_helper.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; +import 'package:smooth_app/widgets/smooth_menu_button.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; class PersonalizedRankingPage extends StatefulWidget { @@ -97,13 +98,13 @@ class _PersonalizedRankingPageState extends State appBar: SmoothAppBar( title: Text(widget.title, overflow: TextOverflow.fade), actions: [ - PopupMenuButton( + SmoothPopupMenuButton( onSelected: _handlePopUpClick, itemBuilder: (BuildContext context) { - return >[ - PopupMenuItem( + return >[ + SmoothPopupMenuItem( value: 'add_to_list', - child: Text(appLocalizations.user_list_button_add_product), + label: appLocalizations.user_list_button_add_product, ), ]; }, diff --git a/packages/smooth_app/lib/pages/product/common/product_list_item_popup_items.dart b/packages/smooth_app/lib/pages/product/common/product_list_item_popup_items.dart index b97f58f32b0..ed67bb929bd 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_item_popup_items.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_item_popup_items.dart @@ -10,6 +10,7 @@ import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/pages/personalized_ranking_page.dart'; import 'package:smooth_app/pages/product/compare_products3_page.dart'; import 'package:smooth_app/pages/product/ordered_nutrients_cache.dart'; +import 'package:smooth_app/widgets/smooth_menu_button.dart'; /// Popup menu item entries for the product list page, for selected items. enum ProductListItemPopupMenuEntry { @@ -26,6 +27,9 @@ abstract class ProductListItemPopupItem { /// IconData of the popup menu item. IconData getIconData(); + /// Is-it a destructive action? + bool isDestructive() => false; + /// Action of the popup menu item. /// /// Returns true if the caller must refresh (setState) (e.g. after deleting). @@ -37,17 +41,16 @@ abstract class ProductListItemPopupItem { }); /// Returns the popup menu item. - PopupMenuItem getMenuItem( + SmoothPopupMenuItem getMenuItem( final AppLocalizations appLocalizations, final bool enabled, ) => - PopupMenuItem( + SmoothPopupMenuItem( value: this, + icon: getIconData(), + label: getTitle(appLocalizations), enabled: enabled, - child: ListTile( - leading: Icon(getIconData()), - title: Text(getTitle(appLocalizations)), - ), + type: isDestructive() ? SmoothPopupMenuItemType.destructive : null, ); } @@ -139,6 +142,9 @@ class ProductListItemPopupDelete extends ProductListItemPopupItem { @override IconData getIconData() => Icons.delete; + @override + bool isDestructive() => true; + @override Future doSomething({ required final ProductList productList, diff --git a/packages/smooth_app/lib/pages/product/common/product_list_page.dart b/packages/smooth_app/lib/pages/product/common/product_list_page.dart index 59d9b4e671a..2336258b8a8 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_page.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_page.dart @@ -31,6 +31,7 @@ import 'package:smooth_app/pages/product_list_user_dialog_helper.dart'; import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; +import 'package:smooth_app/widgets/smooth_menu_button.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -181,7 +182,7 @@ class _ProductListPageState extends State } }, ), - PopupMenuButton( + SmoothPopupMenuButton( onSelected: (final ProductListPopupItem action) async { final ProductList? differentProductList = await action.doSomething( @@ -193,8 +194,7 @@ class _ProductListPageState extends State setState(() => productList = differentProductList); } }, - itemBuilder: (BuildContext context) => - >[ + itemBuilder: (_) => >[ if (enableRename) _rename.getMenuItem(appLocalizations), _share.getMenuItem(appLocalizations), _openInWeb.getMenuItem(appLocalizations), @@ -215,7 +215,7 @@ class _ProductListPageState extends State }, actionModeTitle: Text('${_selectedBarcodes.length}'), actionModeActions: [ - PopupMenuButton( + SmoothPopupMenuButton( onSelected: (final ProductListItemPopupItem action) async { final bool andThenSetState = await action.doSomething( productList: productList, @@ -229,8 +229,7 @@ class _ProductListPageState extends State } } }, - itemBuilder: (BuildContext context) => - >[ + itemBuilder: (_) => >[ if (userPreferences.getFlag(UserPreferencesDevMode .userPreferencesFlagBoostedComparison) == true) diff --git a/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart b/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart index 8fdcbd345d9..fbbd20e1796 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart @@ -8,6 +8,7 @@ import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/helpers/temp_product_list_share_helper.dart'; import 'package:smooth_app/pages/product_list_user_dialog_helper.dart'; +import 'package:smooth_app/widgets/smooth_menu_button.dart'; import 'package:url_launcher/url_launcher.dart'; /// Popup menu item entries for the product list page. @@ -30,6 +31,9 @@ abstract class ProductListPopupItem { /// Popup menu entry of the popup menu item. ProductListPopupMenuEntry getEntry(); + /// Is-it a destructive action? + bool isDestructive() => false; + /// Action of the popup menu item. /// /// Returns a different product list if there are changes, else null. @@ -40,15 +44,14 @@ abstract class ProductListPopupItem { }); /// Returns the popup menu item. - PopupMenuItem getMenuItem( + SmoothPopupMenuItem getMenuItem( final AppLocalizations appLocalizations, ) => - PopupMenuItem( + SmoothPopupMenuItem( value: this, - child: ListTile( - leading: Icon(getIconData()), - title: Text(getTitle(appLocalizations)), - ), + icon: getIconData(), + label: getTitle(appLocalizations), + type: isDestructive() ? SmoothPopupMenuItemType.destructive : null, ); } @@ -64,6 +67,9 @@ class ProductListPopupClear extends ProductListPopupItem { @override ProductListPopupMenuEntry getEntry() => ProductListPopupMenuEntry.clear; + @override + bool isDestructive() => true; + @override Future doSomething({ required final ProductList productList, diff --git a/packages/smooth_app/lib/widgets/smooth_menu_button.dart b/packages/smooth_app/lib/widgets/smooth_menu_button.dart new file mode 100644 index 00000000000..0485b522183 --- /dev/null +++ b/packages/smooth_app/lib/widgets/smooth_menu_button.dart @@ -0,0 +1,118 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +/// A Button similar to a [PopupMenuButton] for non Apple platforms. +/// On iOS and macOS, it's still an [IconButton], but that opens a +/// [CupertinoActionSheet]. +class SmoothPopupMenuButton extends StatefulWidget { + const SmoothPopupMenuButton({ + required this.onSelected, + required this.itemBuilder, + this.actionsTitle, + this.buttonIcon, + this.buttonLabel, + }) : assert(buttonLabel == null || buttonLabel.length > 0), + assert(actionsTitle == null || actionsTitle.length > 0); + + final void Function(T value) onSelected; + final Iterable> Function(BuildContext context) + itemBuilder; + final Icon? buttonIcon; + final String? buttonLabel; + final String? actionsTitle; + + @override + State> createState() => + _SmoothPopupMenuButtonState(); +} + +class _SmoothPopupMenuButtonState extends State> { + @override + Widget build(BuildContext context) { + if (Platform.isIOS || Platform.isMacOS) { + return IconButton( + icon: widget.buttonIcon ?? Icon(Icons.adaptive.more), + tooltip: widget.buttonLabel ?? + MaterialLocalizations.of(context).showMenuTooltip, + onPressed: _openModalSheet, + ); + } else { + return PopupMenuButton( + icon: widget.buttonIcon ?? Icon(Icons.adaptive.more), + tooltip: widget.buttonLabel ?? + MaterialLocalizations.of(context).showMenuTooltip, + onSelected: widget.onSelected, + itemBuilder: (BuildContext context) { + return widget.itemBuilder(context).map((SmoothPopupMenuItem item) { + return PopupMenuItem( + value: item.value, + enabled: item.enabled, + child: ListTile( + leading: Icon(item.icon), + title: Text(item.label), + ), + ); + }).toList(growable: false); + }, + ); + } + } + + // iOS and macOS behavior + void _openModalSheet() { + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text( + widget.actionsTitle ?? + AppLocalizations.of(context).menu_button_list_actions, + ), + actions: widget + .itemBuilder(context) + .where((SmoothPopupMenuItem item) => item.enabled) + .map((SmoothPopupMenuItem item) { + return CupertinoActionSheetAction( + isDefaultAction: + item.type == SmoothPopupMenuItemType.highlighted, + isDestructiveAction: + item.type == SmoothPopupMenuItemType.destructive, + onPressed: () { + widget.onSelected(item.value); + Navigator.of(context).maybePop(); + }, + child: Text(item.label), + ); + }).toList(growable: false), + ); + }); + } +} + +class SmoothPopupMenuItem { + const SmoothPopupMenuItem({ + required this.value, + required this.label, + this.icon, + this.type, + this.enabled = true, + }) : assert(label.length > 0); + + final T value; + final String label; + final IconData? icon; + final SmoothPopupMenuItemType? type; + final bool enabled; +} + +/// The style of an item in the menu +/// On Material platforms, all values behave the same. +/// On iOS, the [highlighted] value is in black and [destructive] in red. +enum SmoothPopupMenuItemType { + normal, + highlighted, + destructive, +} diff --git a/packages/smooth_app/macos/Podfile.lock b/packages/smooth_app/macos/Podfile.lock index a01e826ae0d..c802b6a34ca 100644 --- a/packages/smooth_app/macos/Podfile.lock +++ b/packages/smooth_app/macos/Podfile.lock @@ -114,7 +114,7 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0 mobile_scanner: 54ceceae0c8da2457e26a362a6be5c61154b1829 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 2128f3a8c9107e1ad33574c6e58e8285d460b149 rive_common: cf5ab646aa576b2d742d0e2d528126fbf032c856