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..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 @@ -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,203 @@ 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?.requiresPadding == true ? 0 : LARGE_SPACE), ), - 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); + } + + @override + bool get requiresPadding => true; +} + +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; + } - if (hasCloseButton == true) { - size += (MEDIUM_SPACE * 2) + (Theme.of(context).iconTheme.size ?? 20.0); - } else { - size += Theme.of(context).textTheme.titleLarge?.fontSize ?? 15.0; - } + @override + bool get requiresPadding => true; +} + +class SmoothModalSheetHeaderCloseButton extends StatelessWidget + implements SizeWidget { + const SmoothModalSheetHeaderCloseButton({ + this.semanticsOrder, + }); - return size; + final double? semanticsOrder; + + @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); + + @override + bool get requiresPadding => false; +} + +abstract class SizeWidget implements Widget { + double widgetHeight(BuildContext context); + + bool get requiresPadding; } 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/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..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,15 +1764,23 @@ } } }, - "confirm_delete_user_list": "You're about to delete this list ({name}): are 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": "Yes, I confirm", + "@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", @@ -2344,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 a4e0924d31f..cf915da2394 100644 --- a/packages/smooth_app/lib/pages/all_product_list_page.dart +++ b/packages/smooth_app/lib/pages/all_product_list_page.dart @@ -8,70 +8,105 @@ 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), + 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), ), - ), - 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; - }, - ), - onTap: () => Navigator.of(context).pop(productList), - onLongPress: () async => ProductListUserDialogHelper(daoProductList) - .showDeleteUserListDialog(context, 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), - ], - ), - ), ); } } + +enum PopupMenuEntries { deleteList } 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..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 @@ -11,6 +11,9 @@ class UserPreferencesListTile extends StatelessWidget { this.onTap, this.onLongPress, this.shape, + this.selected, + this.selectedColor, + this.contentPadding, }); final Widget title; @@ -20,6 +23,9 @@ class UserPreferencesListTile extends StatelessWidget { final VoidCallback? onTap; final VoidCallback? onLongPress; final ShapeBorder? shape; + final bool? selected; + final Color? selectedColor; + final EdgeInsetsGeometry? contentPadding; /// Icon (leading or trailing) with the standard color. static Icon getTintedIcon( @@ -38,10 +44,13 @@ class UserPreferencesListTile extends StatelessWidget { style: Theme.of(context).textTheme.headlineMedium, child: title, ), - contentPadding: EdgeInsets.symmetric( - horizontal: LARGE_SPACE, - vertical: subtitle != null ? VERY_SMALL_SPACE : 2.0, - ), + selected: selected ?? false, + selectedTileColor: selectedColor, + contentPadding: contentPadding ?? + EdgeInsets.symmetric( + horizontal: LARGE_SPACE, + vertical: subtitle != null ? VERY_SMALL_SPACE : 2.0, + ), trailing: trailing, onTap: onTap, onLongPress: onLongPress, 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 82% 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..9ea648181be 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,15 +26,20 @@ 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'; /// 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(); @@ -125,50 +132,64 @@ 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), - onPressed: () async { - final ProductList? selected = await Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => const AllProductListPage(), - fullscreenDialog: true, - ), - ); - if (selected == null) { - return; - } - if (context.mounted) { - await daoProductList.get(selected); - if (context.mounted) { - setState(() => productList = selected); - } - } - }, - ), - if (enableClear || enableRename) - PopupMenuButton( - onSelected: (final ProductListPopupItem action) async { - final ProductList? differentProductList = - await action.doSomething( - productList: productList, - localDatabase: localDatabase, + 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), ); - if (differentProductList != null) { - setState(() => productList = differentProductList); + + if (selected == null) { + return; + } + if (context.mounted) { + await daoProductList.get(selected); + if (context.mounted) { + setState(() => productList = selected); + } } }, - 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 +276,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 +325,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..4abb34f4bf7 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'; @@ -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(() {}); 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..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 @@ -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 { @@ -154,19 +155,24 @@ 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(productList.parameters), + appLocalizations.confirm_delete_user_list_message( + 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) {