From abdeb4cd65ab6a24fbb678cb448463ff09cfd23a Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Sat, 22 Jul 2023 20:38:00 +0200 Subject: [PATCH 1/4] CW --- .../dialogs/smooth_alert_dialog.dart | 30 +++++++++++-------- packages/smooth_app/lib/l10n/app_en.arb | 6 +++- .../lib/pages/all_product_list_page.dart | 22 ++++++++++++-- .../product_list_user_dialog_helper.dart | 17 +++++++---- 4 files changed, 53 insertions(+), 22 deletions(-) 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 be77eea6e9f..3503bd1d69e 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 @@ -388,19 +388,22 @@ class _SmoothActionElevatedButton extends StatelessWidget { value: buttonData.text, button: true, excludeSemantics: true, - child: SmoothSimpleButton( - onPressed: buttonData.onPressed, - minWidth: buttonData.minWidth ?? 20.0, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - buttonData.text.toUpperCase(), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: buttonData.lines ?? 2, - style: themeData.textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - color: buttonData.textColor ?? themeData.colorScheme.onPrimary, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30.0), + child: SmoothSimpleButton( + onPressed: buttonData.onPressed, + minWidth: buttonData.minWidth ?? 20.0, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + buttonData.text.toUpperCase(), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: buttonData.lines ?? 2, + style: themeData.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + color: buttonData.textColor ?? themeData.colorScheme.onPrimary, + ), ), ), ), @@ -444,6 +447,7 @@ class _SmoothActionFlatButton extends StatelessWidget { padding: const EdgeInsets.symmetric( horizontal: SMALL_SPACE, ), + minimumSize: const Size(0, 50.0), ), child: SizedBox( height: buttonData.lines != null diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index b26e33f39b0..9b54224df6c 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1764,7 +1764,7 @@ } } }, - "confirm_delete_user_list": "You're about to delete this list ({name}): are you sure you want to continue?", + "confirm_delete_user_list": "You're about to delete the list \"{name}\".\nAre you sure you want to continue?", "@confirm_delete_user_list": { "description": "Asking about whether to delete the list or not", "placeholders": { @@ -1773,6 +1773,10 @@ } } }, + "confirm_delete_user_list_button": "Delete list", + "@confirm_delete_user_list_button": { + "description": "Button to delete a list" + }, "importance_label": "{name} importance: {id}", "@importance_label": { "description": "Used when user selects a food preference. example: Vegan importance; mandatory", diff --git a/packages/smooth_app/lib/pages/all_product_list_page.dart b/packages/smooth_app/lib/pages/all_product_list_page.dart index a4e0924d31f..3f5080be6ef 100644 --- a/packages/smooth_app/lib/pages/all_product_list_page.dart +++ b/packages/smooth_app/lib/pages/all_product_list_page.dart @@ -56,9 +56,25 @@ class AllProductListPage extends StatelessWidget { return EMPTY_WIDGET; }, ), + trailing: PopupMenuButton( + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + value: PopupMenuEntries.deleteList, + child: const ListTile( + leading: Icon(Icons.delete), + title: Text('Delete'), + contentPadding: EdgeInsets.zero, + ), + onTap: () async => + ProductListUserDialogHelper(daoProductList) + .showDeleteUserListDialog(context, productList), + ) + ]; + }, + icon: const Icon(Icons.more_vert), + ), onTap: () => Navigator.of(context).pop(productList), - onLongPress: () async => ProductListUserDialogHelper(daoProductList) - .showDeleteUserListDialog(context, productList), ); }, ), @@ -75,3 +91,5 @@ class AllProductListPage extends StatelessWidget { ); } } + +enum PopupMenuEntries { deleteList } diff --git a/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart b/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart index 648899f37bc..85696703460 100644 --- a/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart +++ b/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart @@ -8,6 +8,7 @@ import 'package:smooth_app/database/dao_product_list.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/pages/product/common/product_query_page_helper.dart'; /// Dialog helper class for user product list. class ProductListUserDialogHelper { @@ -155,18 +156,22 @@ class ProductListUserDialogHelper { context: context, builder: (final BuildContext context) => SmoothAlertDialog( body: Text( - appLocalizations.confirm_delete_user_list(productList.parameters), + appLocalizations.confirm_delete_user_list( + ProductQueryPageHelper.getProductListLabel( + productList, + appLocalizations, + ), + ), ), negativeAction: SmoothActionButton( onPressed: () => Navigator.pop(context), - text: appLocalizations.cancel, + text: appLocalizations.no, ), positiveAction: SmoothActionButton( - onPressed: () { - Navigator.pop(context, true); - }, - text: appLocalizations.okay, + onPressed: () => Navigator.pop(context, true), + text: appLocalizations.confirm_delete_user_list_button, ), + actionsAxis: Axis.vertical, ), ); if (deleted == null) { From 666b2365e1911ad1fd69a4279859e75b7db4ddb6 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Sun, 23 Jul 2023 10:00:11 +0200 Subject: [PATCH 2/4] List picker with a modal sheet --- .../lib/data_models/product_list.dart | 2 + .../bottom_sheets/smooth_bottom_sheet.dart | 236 +++++++++++++----- .../smooth_draggable_bottom_sheet.dart | 10 +- packages/smooth_app/lib/l10n/app_en.arb | 30 ++- .../lib/pages/all_product_list_page.dart | 137 +++++----- .../smooth_app/lib/pages/history_page.dart | 2 +- .../user_preferences_list_tile.dart | 6 + .../preferences/user_preferences_widgets.dart | 64 ++--- ...list_page.dart => product_list_modal.dart} | 118 +++++---- .../lib/pages/product/new_product_page.dart | 2 +- .../product_list_user_dialog_helper.dart | 3 +- 11 files changed, 399 insertions(+), 211 deletions(-) rename packages/smooth_app/lib/pages/product/common/{product_list_page.dart => product_list_modal.dart} (84%) diff --git a/packages/smooth_app/lib/data_models/product_list.dart b/packages/smooth_app/lib/data_models/product_list.dart index 83aa7efb673..c34af31a05b 100644 --- a/packages/smooth_app/lib/data_models/product_list.dart +++ b/packages/smooth_app/lib/data_models/product_list.dart @@ -254,4 +254,6 @@ class ProductList { ',${country?.offTag ?? ''}'; } } + + bool get isEditable => listType == ProductListType.USER; } diff --git a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart index 06c03ee2a1b..842363ee75c 100644 --- a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart +++ b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:smooth_app/generic_lib/bottom_sheets/smooth_draggable_bottom_sheet_route.dart'; @@ -27,30 +29,36 @@ Future showSmoothDraggableModalSheet({ /// You must return a Sliver Widget required WidgetBuilder bodyBuilder, + double? initHeight, }) { return showDraggableModalSheet( context: context, borderRadius: const BorderRadius.vertical(top: ROUNDED_RADIUS), headerBuilder: (_) => header, - headerHeight: - SmoothModalSheetHeader.computeHeight(context, header.closeButton), + headerHeight: header.computeHeight(context), bodyBuilder: bodyBuilder, + initHeight: initHeight, ); } /// A non scrollable modal sheet class SmoothModalSheet extends StatelessWidget { - const SmoothModalSheet({ - required this.title, + SmoothModalSheet({ + required String title, required this.body, - this.closeButton = true, + bool closeButton = true, this.bodyPadding, - this.closeButtonSemanticsOrder, - }); + double? closeButtonSemanticsOrder, + }) : header = SmoothModalSheetHeader( + title: title, + suffix: closeButton + ? SmoothModalSheetHeaderCloseButton( + semanticsOrder: closeButtonSemanticsOrder, + ) + : null, + ); - final String title; - final bool closeButton; - final double? closeButtonSemanticsOrder; + final SmoothModalSheetHeader header; final Widget body; final EdgeInsetsGeometry? bodyPadding; @@ -65,11 +73,7 @@ class SmoothModalSheet extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - SmoothModalSheetHeader( - title: title, - closeButton: closeButton, - closeButtonSemanticsOrder: closeButtonSemanticsOrder, - ), + header, Padding( padding: bodyPadding ?? const EdgeInsets.all(MEDIUM_SPACE), child: body, @@ -78,81 +82,191 @@ class SmoothModalSheet extends StatelessWidget { )), ); } + + double computeHeaderHeight(BuildContext context) => + header.computeHeight(context); } -class SmoothModalSheetHeader extends StatelessWidget { +class SmoothModalSheetHeader extends StatelessWidget implements SizeWidget { const SmoothModalSheetHeader({ required this.title, - this.closeButton = true, - this.closeButtonSemanticsOrder, + this.suffix, }); + static const double MIN_HEIGHT = 50.0; + final String title; - final bool closeButton; - final double? closeButtonSemanticsOrder; + final SizeWidget? suffix; @override Widget build(BuildContext context) { final Color primaryColor = Theme.of(context).primaryColor; return Container( + height: suffix is SmoothModalSheetHeaderButton ? double.infinity : null, color: primaryColor.withOpacity(0.2), + constraints: const BoxConstraints(minHeight: MIN_HEIGHT), padding: EdgeInsetsDirectional.only( start: VERY_LARGE_SPACE, top: VERY_SMALL_SPACE, bottom: VERY_SMALL_SPACE, - end: VERY_LARGE_SPACE - (closeButton ? LARGE_SPACE : 0), + end: VERY_LARGE_SPACE - (suffix != null ? LARGE_SPACE : 0), ), - child: Row( - children: [ - Expanded( - child: Semantics( - sortKey: const OrdinalSortKey(1.0), - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleLarge, + child: IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: Semantics( + sortKey: const OrdinalSortKey(1.0), + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge, + ), ), ), + if (suffix != null) suffix! + ], + ), + ), + ); + } + + double computeHeight(BuildContext context) { + return math.max( + widgetHeight(context), + suffix?.widgetHeight(context) ?? 0.0, + ); + } + + @override + double widgetHeight(BuildContext context) { + final double size = VERY_SMALL_SPACE * 2 + + (Theme.of(context).textTheme.titleLarge?.fontSize ?? 15.0); + + return math.max(MIN_HEIGHT, size); + } +} + +class SmoothModalSheetHeaderButton extends StatelessWidget + implements SizeWidget { + const SmoothModalSheetHeaderButton({ + required this.label, + this.prefix, + this.suffix, + this.onTap, + this.tooltip, + }); + + static const EdgeInsetsGeometry _padding = EdgeInsetsDirectional.symmetric( + horizontal: 15.0, + vertical: 20.0, + ); + + final String label; + final Widget? prefix; + final Widget? suffix; + final String? tooltip; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Semantics( + value: tooltip, + button: true, + excludeSemantics: true, + child: Tooltip( + message: tooltip ?? '', + child: TextButton( + onPressed: onTap, + style: TextButton.styleFrom( + padding: _padding, + shape: const RoundedRectangleBorder( + borderRadius: ROUNDED_BORDER_RADIUS, + ), + foregroundColor: Colors.white, + backgroundColor: Theme.of(context).primaryColor, + iconColor: Colors.white, ), - if (closeButton) - Semantics( - value: MaterialLocalizations.of(context).closeButtonTooltip, - button: true, - excludeSemantics: true, - onScrollDown: () {}, - sortKey: OrdinalSortKey(closeButtonSemanticsOrder ?? 2.0), - child: Tooltip( - message: MaterialLocalizations.of(context).closeButtonTooltip, - enableFeedback: true, - child: InkWell( - onTap: () => Navigator.of(context).pop(), - customBorder: const CircleBorder(), - child: const Padding( - padding: EdgeInsets.all(MEDIUM_SPACE), - child: Icon(Icons.clear), + child: IconTheme( + data: const IconThemeData( + color: Colors.white, + size: 20.0, + ), + child: Row( + children: [ + if (prefix != null) ...[ + prefix!, + const SizedBox( + width: SMALL_SPACE, + ), + ], + Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 17.0, ), + maxLines: 1, ), - ), - ) - ], + if (suffix != null) ...[ + const SizedBox( + width: SMALL_SPACE, + ), + suffix!, + ], + ], + ), + ), + ), ), ); } - static double computeHeight( - BuildContext context, - bool hasCloseButton, - ) { - double size = VERY_SMALL_SPACE * 2; + @override + double widgetHeight(BuildContext context) { + return math.max(17.0 * MediaQuery.textScaleFactorOf(context), + suffix is Icon || prefix is Icon ? 20.0 : 0.0) + + _padding.vertical; + } +} + +class SmoothModalSheetHeaderCloseButton extends StatelessWidget + implements SizeWidget { + const SmoothModalSheetHeaderCloseButton({ + this.semanticsOrder, + }); - if (hasCloseButton == true) { - size += (MEDIUM_SPACE * 2) + (Theme.of(context).iconTheme.size ?? 20.0); - } else { - size += Theme.of(context).textTheme.titleLarge?.fontSize ?? 15.0; - } + final double? semanticsOrder; - return size; + @override + Widget build(BuildContext context) { + return Semantics( + value: MaterialLocalizations.of(context).closeButtonTooltip, + button: true, + excludeSemantics: true, + sortKey: OrdinalSortKey(semanticsOrder ?? 2.0), + child: Tooltip( + message: MaterialLocalizations.of(context).closeButtonTooltip, + enableFeedback: true, + child: InkWell( + onTap: () => Navigator.of(context).pop(), + customBorder: const CircleBorder(), + child: const Padding( + padding: EdgeInsets.all(MEDIUM_SPACE), + child: Icon(Icons.clear), + ), + ), + ), + ); } + + @override + double widgetHeight(BuildContext context) => + (MEDIUM_SPACE * 2) + (Theme.of(context).iconTheme.size ?? 20.0); +} + +abstract class SizeWidget implements Widget { + double widgetHeight(BuildContext context); } diff --git a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_draggable_bottom_sheet.dart b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_draggable_bottom_sheet.dart index 28725355700..f9029adb8cc 100644 --- a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_draggable_bottom_sheet.dart +++ b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_draggable_bottom_sheet.dart @@ -157,6 +157,7 @@ class _SmoothDraggableContentState extends State<_SmoothDraggableContent> { @override Widget build(BuildContext context) { return Scrollbar( + controller: widget.scrollController, child: CustomScrollView( cacheExtent: widget.cacheExtent, key: _contentKey, @@ -178,15 +179,18 @@ class _SmoothDraggableContentState extends State<_SmoothDraggableContent> { /// A fixed header class _SliverHeader extends SliverPersistentHeaderDelegate { - _SliverHeader({required this.child, required this.height}) - : assert(height > 0.0); + _SliverHeader({ + required this.child, + required this.height, + }) : assert(height > 0.0); final Widget child; final double height; @override Widget build(BuildContext context, _, __) { - return child; + // Align is mandatory here (a known-bug in the framework) + return Align(child: child); } @override diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 9b54224df6c..77c0d5293f2 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -827,7 +827,7 @@ "count": {} } }, - "compare_products_mode": "Compare", + "compare_products_mode": "Compare products", "@compare_products_mode": { "description": "Button to switch to 'compare products mode'" }, @@ -1764,16 +1764,20 @@ } } }, - "confirm_delete_user_list": "You're about to delete the list \"{name}\".\nAre you sure you want to continue?", - "@confirm_delete_user_list": { - "description": "Asking about whether to delete the list or not", + "confirm_delete_user_list_title": "Delete the list?", + "@confirm_delete_user_list_title": { + "description": "Title when asking about whether to delete the list or not" + }, + "confirm_delete_user_list_message": "You're about to delete the list \"{name}\".\nAre you sure you want to continue?", + "@confirm_delete_user_list_message": { + "description": "Message when asking about whether to delete the list or not", "placeholders": { "name": { "type": "String" } } }, - "confirm_delete_user_list_button": "Delete list", + "confirm_delete_user_list_button": "Yes, I confirm", "@confirm_delete_user_list_button": { "description": "Button to delete a list" }, @@ -2348,5 +2352,21 @@ "country_selector_title": "Select your country:", "@country_selector_title": { "description": "Label written as the title of the dialog to select the user country" + }, + "action_delete_list": "Delete", + "@action_delete_list": { + "description": "Delete a list action in a menu" + }, + "action_change_list": "Change the current list", + "@action_change_list": { + "description": "Action to change the current visible list" + }, + "product_list_create": "Create", + "@product_list_create": { + "description": "Button label to create a new list (short word)" + }, + "product_list_create_tooltip": "Create a new list", + "@product_list_create_tooltip": { + "description": "Button description to create a new list (long sentence)" } } diff --git a/packages/smooth_app/lib/pages/all_product_list_page.dart b/packages/smooth_app/lib/pages/all_product_list_page.dart index 3f5080be6ef..8bae26d0298 100644 --- a/packages/smooth_app/lib/pages/all_product_list_page.dart +++ b/packages/smooth_app/lib/pages/all_product_list_page.dart @@ -8,86 +8,97 @@ import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.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_scaffold.dart'; /// Page that lists all product lists. -class AllProductListPage extends StatelessWidget { - const AllProductListPage(); +class AllProductListModal extends StatelessWidget { + AllProductListModal({ + required this.currentList, + }); + + final ProductList currentList; + + final List hardcodedProductLists = [ + ProductList.scanSession(), + ProductList.scanHistory(), + ProductList.history(), + ]; @override Widget build(BuildContext context) { final LocalDatabase localDatabase = context.watch(); final DaoProductList daoProductList = DaoProductList(localDatabase); - final List productLists = [ - ProductList.scanSession(), - ProductList.scanHistory(), - ProductList.history(), - ]; + final List userLists = daoProductList.getUserLists(); + final List productLists = + List.from(hardcodedProductLists); for (final String userList in userLists) { productLists.add(ProductList.user(userList)); } final AppLocalizations appLocalizations = AppLocalizations.of(context); - return SmoothScaffold( - appBar: SmoothAppBar(title: Text(appLocalizations.product_list_select)), - body: ListView.builder( - itemCount: productLists.length, - itemBuilder: (final BuildContext context, final int index) { + + return SliverList( + delegate: SliverChildBuilderDelegate( + childCount: productLists.length, + (final BuildContext context, final int index) { final ProductList productList = productLists[index]; - return UserPreferencesListTile( - title: Text( - ProductQueryPageHelper.getProductListLabel( - productList, - appLocalizations, + return Column( + children: [ + UserPreferencesListTile( + title: Text( + ProductQueryPageHelper.getProductListLabel( + productList, + appLocalizations, + ), + ), + subtitle: FutureBuilder( + future: daoProductList.getLength(productList), + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.data != null) { + return Text( + appLocalizations.user_list_length(snapshot.data!), + ); + } + return EMPTY_WIDGET; + }, + ), + trailing: productList.isEditable + ? PopupMenuButton( + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + value: PopupMenuEntries.deleteList, + child: ListTile( + leading: const Icon(Icons.delete), + title: + Text(appLocalizations.action_delete_list), + contentPadding: EdgeInsets.zero, + ), + onTap: () { + WidgetsBinding.instance + .addPostFrameCallback((_) { + ProductListUserDialogHelper(daoProductList) + .showDeleteUserListDialog( + context, productList); + }); + }) + ]; + }, + icon: const Icon(Icons.more_vert), + ) + : null, + selected: productList.listType == currentList.listType && + productList.parameters == currentList.parameters, + selectedColor: Theme.of(context).primaryColor.withOpacity(0.2), + onTap: () => Navigator.of(context).pop(productList), ), - ), - subtitle: FutureBuilder( - future: daoProductList.getLength(productList), - builder: ( - final BuildContext context, - final AsyncSnapshot snapshot, - ) { - if (snapshot.data != null) { - return Text( - appLocalizations.user_list_length(snapshot.data!), - ); - } - return EMPTY_WIDGET; - }, - ), - trailing: PopupMenuButton( - itemBuilder: (BuildContext context) { - return >[ - PopupMenuItem( - value: PopupMenuEntries.deleteList, - child: const ListTile( - leading: Icon(Icons.delete), - title: Text('Delete'), - contentPadding: EdgeInsets.zero, - ), - onTap: () async => - ProductListUserDialogHelper(daoProductList) - .showDeleteUserListDialog(context, productList), - ) - ]; - }, - icon: const Icon(Icons.more_vert), - ), - onTap: () => Navigator.of(context).pop(productList), + if (index < productLists.length - 1) const Divider(height: 1.0), + ], ); }, ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () async => ProductListUserDialogHelper(daoProductList) - .showCreateUserListDialog(context), - label: Row( - children: [ - const Icon(Icons.add), - Text(appLocalizations.add_list_label), - ], - ), - ), ); } } diff --git a/packages/smooth_app/lib/pages/history_page.dart b/packages/smooth_app/lib/pages/history_page.dart index 68be6bdf733..b4bb6a1361e 100644 --- a/packages/smooth_app/lib/pages/history_page.dart +++ b/packages/smooth_app/lib/pages/history_page.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/product_list.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/pages/product/common/product_list_page.dart'; +import 'package:smooth_app/pages/product/common/product_list_modal.dart'; class HistoryPage extends StatefulWidget { const HistoryPage(); diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart index 37d16ed1f9e..ef97153a4ae 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart @@ -11,6 +11,8 @@ class UserPreferencesListTile extends StatelessWidget { this.onTap, this.onLongPress, this.shape, + this.selected, + this.selectedColor, }); final Widget title; @@ -20,6 +22,8 @@ class UserPreferencesListTile extends StatelessWidget { final VoidCallback? onTap; final VoidCallback? onLongPress; final ShapeBorder? shape; + final bool? selected; + final Color? selectedColor; /// Icon (leading or trailing) with the standard color. static Icon getTintedIcon( @@ -38,6 +42,8 @@ class UserPreferencesListTile extends StatelessWidget { style: Theme.of(context).textTheme.headlineMedium, child: title, ), + selected: selected ?? false, + selectedTileColor: selectedColor, contentPadding: EdgeInsets.symmetric( horizontal: LARGE_SPACE, vertical: subtitle != null ? VERY_SMALL_SPACE : 2.0, diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_widgets.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_widgets.dart index 6a322667116..abc0774860a 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_widgets.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_widgets.dart @@ -194,8 +194,9 @@ class UserPreferencesMultipleChoicesItem extends StatelessWidget { // If there is not enough space, we use the scrolling sheet final T? res; - if ((itemHeight * labels.length + - SmoothModalSheetHeader.computeHeight(context, true)) > + final SmoothModalSheetHeader header = + SmoothModalSheetHeader(title: title); + if ((itemHeight * labels.length + header.computeHeight(context)) > (queryData.size.height * 0.9) - queryData.viewPadding.top) { res = await showSmoothDraggableModalSheet( context: context, @@ -224,39 +225,40 @@ class UserPreferencesMultipleChoicesItem extends StatelessWidget { ); }); } else { + final SmoothModalSheet smoothModalSheet = SmoothModalSheet( + title: title, + bodyPadding: EdgeInsets.zero, + body: SizedBox( + height: itemHeight * labels.length, + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + itemCount: labels.length, + itemBuilder: (BuildContext context, int position) { + final bool selected = + currentValue == values.elementAt(position); + + return _ChoiceItem( + selected: selected, + label: labels.elementAt(position), + value: values.elementAt(position), + description: descriptions?.elementAt(position), + leading: leadingBuilder != null + ? Builder(builder: leadingBuilder!.elementAt(position)) + : null, + hasDivider: false, + ); + }, + separatorBuilder: (_, __) => const Divider(height: 1.0), + ), + ), + ); + res = await showSmoothModalSheet( context: context, - minHeight: SmoothModalSheetHeader.computeHeight(context, false) + + minHeight: smoothModalSheet.computeHeaderHeight(context) + itemHeight * labels.length, builder: (BuildContext context) { - return SmoothModalSheet( - title: title, - bodyPadding: EdgeInsets.zero, - body: SizedBox( - height: itemHeight * labels.length, - child: ListView.separated( - physics: const NeverScrollableScrollPhysics(), - itemCount: labels.length, - itemBuilder: (BuildContext context, int position) { - final bool selected = - currentValue == values.elementAt(position); - - return _ChoiceItem( - selected: selected, - label: labels.elementAt(position), - value: values.elementAt(position), - description: descriptions?.elementAt(position), - leading: leadingBuilder != null - ? Builder( - builder: leadingBuilder!.elementAt(position)) - : null, - hasDivider: false, - ); - }, - separatorBuilder: (_, __) => const Divider(height: 1.0), - ), - ), - ); + return smoothModalSheet; }, ); } diff --git a/packages/smooth_app/lib/pages/product/common/product_list_page.dart b/packages/smooth_app/lib/pages/product/common/product_list_modal.dart similarity index 84% rename from packages/smooth_app/lib/pages/product/common/product_list_page.dart rename to packages/smooth_app/lib/pages/product/common/product_list_modal.dart index 9842e08c206..a1650d2b445 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_page.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_modal.dart @@ -11,10 +11,12 @@ import 'package:smooth_app/data_models/up_to_date_product_list_mixin.dart'; import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/generic_lib/bottom_sheets/smooth_bottom_sheet.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/duration_constants.dart'; import 'package:smooth_app/generic_lib/loading_dialog.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_responsive.dart'; import 'package:smooth_app/helpers/app_helper.dart'; import 'package:smooth_app/helpers/robotoff_insight_helper.dart'; import 'package:smooth_app/pages/all_product_list_page.dart'; @@ -24,6 +26,7 @@ import 'package:smooth_app/pages/product/common/product_list_item_simple.dart'; import 'package:smooth_app/pages/product/common/product_list_popup_items.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; +import 'package:smooth_app/pages/product_list_user_dialog_helper.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -125,18 +128,32 @@ class _ProductListPageState extends State icon: const Icon(Icons.compare_arrows), ), appBar: SmoothAppBar( - centerTitle: _selectionMode ? false : null, + centerTitle: false, actions: [ IconButton( icon: const Icon(CupertinoIcons.square_list), + tooltip: appLocalizations.action_change_list, onPressed: () async { - final ProductList? selected = await Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => const AllProductListPage(), - fullscreenDialog: true, + final ProductList? selected = + await showSmoothDraggableModalSheet( + context: context, + header: SmoothModalSheetHeader( + title: appLocalizations.product_list_select, + suffix: SmoothModalSheetHeaderButton( + label: appLocalizations.product_list_create, + prefix: const Icon(Icons.add_circle_outline_sharp), + tooltip: appLocalizations.product_list_create_tooltip, + onTap: () async => + ProductListUserDialogHelper(daoProductList) + .showCreateUserListDialog(context), + ), + ), + bodyBuilder: (BuildContext context) => AllProductListModal( + currentList: productList, ), + initHeight: _computeModalInitHeight(context), ); + if (selected == null) { return; } @@ -148,27 +165,26 @@ class _ProductListPageState extends State } }, ), - if (enableClear || enableRename) - PopupMenuButton( - onSelected: (final ProductListPopupItem action) async { - final ProductList? differentProductList = - await action.doSomething( - productList: productList, - localDatabase: localDatabase, - context: context, - ); - if (differentProductList != null) { - setState(() => productList = differentProductList); - } - }, - itemBuilder: (BuildContext context) => - >[ - if (enableRename) _rename.getMenuItem(appLocalizations), - _share.getMenuItem(appLocalizations), - _openInWeb.getMenuItem(appLocalizations), - if (enableClear) _clear.getMenuItem(appLocalizations), - ], - ), + PopupMenuButton( + onSelected: (final ProductListPopupItem action) async { + final ProductList? differentProductList = + await action.doSomething( + productList: productList, + localDatabase: localDatabase, + context: context, + ); + if (differentProductList != null) { + setState(() => productList = differentProductList); + } + }, + itemBuilder: (BuildContext context) => + >[ + if (enableRename) _rename.getMenuItem(appLocalizations), + _share.getMenuItem(appLocalizations), + _openInWeb.getMenuItem(appLocalizations), + if (enableClear) _clear.getMenuItem(appLocalizations), + ], + ), ], title: AutoSizeText( ProductQueryPageHelper.getProductListLabel( @@ -255,25 +271,27 @@ class _ProductListPageState extends State ], ), body: products.isEmpty - ? Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - SvgPicture.asset( - 'assets/misc/empty-list.svg', - package: AppHelper.APP_PACKAGE, - width: MediaQuery.of(context).size.width / 2, - ), - Text( - appLocalizations.product_list_empty_message, - textAlign: TextAlign.center, - style: themeData.textTheme.bodyMedium?.apply( - color: themeData.colorScheme.onBackground, + ? Center( + child: Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SvgPicture.asset( + 'assets/misc/empty-list.svg', + package: AppHelper.APP_PACKAGE, + width: MediaQuery.of(context).size.width / 2, ), - ), - EMPTY_WIDGET, - ], + Text( + appLocalizations.product_list_empty_message, + textAlign: TextAlign.center, + style: themeData.textTheme.bodyMedium?.apply( + color: themeData.colorScheme.onBackground, + ), + ), + EMPTY_WIDGET, + ], + ), ), ) : WillPopScope( @@ -302,6 +320,16 @@ class _ProductListPageState extends State ); } + double _computeModalInitHeight(BuildContext context) { + if (context.isSmallDevice()) { + return 0.7; + } else if (context.isSmartphoneDevice()) { + return 0.55; + } else { + return 0.45; + } + } + Widget _buildItem( final bool dismissible, final List barcodes, diff --git a/packages/smooth_app/lib/pages/product/new_product_page.dart b/packages/smooth_app/lib/pages/product/new_product_page.dart index 7c307fcda69..ba745a16e90 100644 --- a/packages/smooth_app/lib/pages/product/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/new_product_page.dart @@ -23,7 +23,7 @@ import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_product_cards.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; import 'package:smooth_app/pages/inherited_data_manager.dart'; -import 'package:smooth_app/pages/product/common/product_list_page.dart'; +import 'package:smooth_app/pages/product/common/product_list_modal.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/edit_product_page.dart'; import 'package:smooth_app/pages/product/product_questions_widget.dart'; diff --git a/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart b/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart index 85696703460..e0b2b2d86c0 100644 --- a/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart +++ b/packages/smooth_app/lib/pages/product_list_user_dialog_helper.dart @@ -155,8 +155,9 @@ class ProductListUserDialogHelper { final bool? deleted = await showDialog( context: context, builder: (final BuildContext context) => SmoothAlertDialog( + title: appLocalizations.confirm_delete_user_list_title, body: Text( - appLocalizations.confirm_delete_user_list( + appLocalizations.confirm_delete_user_list_message( ProductQueryPageHelper.getProductListLabel( productList, appLocalizations, From 90ddf8fcd93e107a3127b80a2216c9ef413dfbfe Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Sun, 23 Jul 2023 10:05:57 +0200 Subject: [PATCH 3/4] From the product page, we shouldn't be able to switch between lists --- .../product/common/product_list_modal.dart | 69 ++++++++++--------- .../lib/pages/product/new_product_page.dart | 6 +- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/smooth_app/lib/pages/product/common/product_list_modal.dart b/packages/smooth_app/lib/pages/product/common/product_list_modal.dart index a1650d2b445..9ea648181be 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_modal.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_modal.dart @@ -33,9 +33,13 @@ import 'package:smooth_app/widgets/smooth_scaffold.dart'; /// Displays the products of a product list, with access to other lists. class ProductListPage extends StatefulWidget { - const ProductListPage(this.productList); + const ProductListPage( + this.productList, { + this.allowToSwitchBetweenLists = true, + }); final ProductList productList; + final bool allowToSwitchBetweenLists; @override State createState() => _ProductListPageState(); @@ -130,41 +134,42 @@ class _ProductListPageState extends State appBar: SmoothAppBar( centerTitle: false, actions: [ - IconButton( - icon: const Icon(CupertinoIcons.square_list), - tooltip: appLocalizations.action_change_list, - onPressed: () async { - final ProductList? selected = - await showSmoothDraggableModalSheet( - context: context, - header: SmoothModalSheetHeader( - title: appLocalizations.product_list_select, - suffix: SmoothModalSheetHeaderButton( - label: appLocalizations.product_list_create, - prefix: const Icon(Icons.add_circle_outline_sharp), - tooltip: appLocalizations.product_list_create_tooltip, - onTap: () async => - ProductListUserDialogHelper(daoProductList) - .showCreateUserListDialog(context), + if (widget.allowToSwitchBetweenLists) + IconButton( + icon: const Icon(CupertinoIcons.square_list), + tooltip: appLocalizations.action_change_list, + onPressed: () async { + final ProductList? selected = + await showSmoothDraggableModalSheet( + context: context, + header: SmoothModalSheetHeader( + title: appLocalizations.product_list_select, + suffix: SmoothModalSheetHeaderButton( + label: appLocalizations.product_list_create, + prefix: const Icon(Icons.add_circle_outline_sharp), + tooltip: appLocalizations.product_list_create_tooltip, + onTap: () async => + ProductListUserDialogHelper(daoProductList) + .showCreateUserListDialog(context), + ), ), - ), - bodyBuilder: (BuildContext context) => AllProductListModal( - currentList: productList, - ), - initHeight: _computeModalInitHeight(context), - ); + bodyBuilder: (BuildContext context) => AllProductListModal( + currentList: productList, + ), + initHeight: _computeModalInitHeight(context), + ); - if (selected == null) { - return; - } - if (context.mounted) { - await daoProductList.get(selected); + if (selected == null) { + return; + } if (context.mounted) { - setState(() => productList = selected); + await daoProductList.get(selected); + if (context.mounted) { + setState(() => productList = selected); + } } - } - }, - ), + }, + ), PopupMenuButton( onSelected: (final ProductListPopupItem action) async { final ProductList? differentProductList = diff --git a/packages/smooth_app/lib/pages/product/new_product_page.dart b/packages/smooth_app/lib/pages/product/new_product_page.dart index ba745a16e90..4abb34f4bf7 100644 --- a/packages/smooth_app/lib/pages/product/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/new_product_page.dart @@ -448,8 +448,10 @@ class _ProductPageState extends State await Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => - ProductListPage(productList), + builder: (BuildContext context) => ProductListPage( + productList, + allowToSwitchBetweenLists: false, + ), ), ); setState(() {}); From f12d0ff60b1b7beb6b87f2a9a0f042170cae0b41 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Sun, 23 Jul 2023 10:30:51 +0200 Subject: [PATCH 4/4] Ensure everything is perfectly aligned --- .../bottom_sheets/smooth_bottom_sheet.dart | 14 +++++++++++++- .../lib/pages/all_product_list_page.dart | 6 ++++++ .../preferences/user_preferences_list_tile.dart | 11 +++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart index 842363ee75c..7c751f53949 100644 --- a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart +++ b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart @@ -110,7 +110,8 @@ class SmoothModalSheetHeader extends StatelessWidget implements SizeWidget { start: VERY_LARGE_SPACE, top: VERY_SMALL_SPACE, bottom: VERY_SMALL_SPACE, - end: VERY_LARGE_SPACE - (suffix != null ? LARGE_SPACE : 0), + end: VERY_LARGE_SPACE - + (suffix?.requiresPadding == true ? 0 : LARGE_SPACE), ), child: IntrinsicHeight( child: Row( @@ -147,6 +148,9 @@ class SmoothModalSheetHeader extends StatelessWidget implements SizeWidget { return math.max(MIN_HEIGHT, size); } + + @override + bool get requiresPadding => true; } class SmoothModalSheetHeaderButton extends StatelessWidget @@ -230,6 +234,9 @@ class SmoothModalSheetHeaderButton extends StatelessWidget suffix is Icon || prefix is Icon ? 20.0 : 0.0) + _padding.vertical; } + + @override + bool get requiresPadding => true; } class SmoothModalSheetHeaderCloseButton extends StatelessWidget @@ -265,8 +272,13 @@ class SmoothModalSheetHeaderCloseButton extends StatelessWidget @override double widgetHeight(BuildContext context) => (MEDIUM_SPACE * 2) + (Theme.of(context).iconTheme.size ?? 20.0); + + @override + bool get requiresPadding => false; } abstract class SizeWidget implements Widget { double widgetHeight(BuildContext context); + + bool get requiresPadding; } diff --git a/packages/smooth_app/lib/pages/all_product_list_page.dart b/packages/smooth_app/lib/pages/all_product_list_page.dart index 8bae26d0298..cf915da2394 100644 --- a/packages/smooth_app/lib/pages/all_product_list_page.dart +++ b/packages/smooth_app/lib/pages/all_product_list_page.dart @@ -92,6 +92,12 @@ class AllProductListModal extends StatelessWidget { selected: productList.listType == currentList.listType && productList.parameters == currentList.parameters, selectedColor: Theme.of(context).primaryColor.withOpacity(0.2), + contentPadding: const EdgeInsetsDirectional.only( + start: VERY_LARGE_SPACE, + end: LARGE_SPACE, + top: VERY_SMALL_SPACE, + bottom: VERY_SMALL_SPACE, + ), onTap: () => Navigator.of(context).pop(productList), ), if (index < productLists.length - 1) const Divider(height: 1.0), diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart index ef97153a4ae..f72e0c8ddf6 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_list_tile.dart @@ -13,6 +13,7 @@ class UserPreferencesListTile extends StatelessWidget { this.shape, this.selected, this.selectedColor, + this.contentPadding, }); final Widget title; @@ -24,6 +25,7 @@ class UserPreferencesListTile extends StatelessWidget { final ShapeBorder? shape; final bool? selected; final Color? selectedColor; + final EdgeInsetsGeometry? contentPadding; /// Icon (leading or trailing) with the standard color. static Icon getTintedIcon( @@ -44,10 +46,11 @@ class UserPreferencesListTile extends StatelessWidget { ), selected: selected ?? false, selectedTileColor: selectedColor, - contentPadding: EdgeInsets.symmetric( - horizontal: LARGE_SPACE, - vertical: subtitle != null ? VERY_SMALL_SPACE : 2.0, - ), + contentPadding: contentPadding ?? + EdgeInsets.symmetric( + horizontal: LARGE_SPACE, + vertical: subtitle != null ? VERY_SMALL_SPACE : 2.0, + ), trailing: trailing, onTap: onTap, onLongPress: onLongPress,