From 67b7ce437efe77d51e6d95101ad4c7779a089269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:39:33 +0100 Subject: [PATCH 1/9] Reworking product page --- packages/smooth_app/ios/Podfile.lock | 58 ++--- packages/smooth_app/lib/l10n/app_en.arb | 6 +- .../product_page/header/product_tabs.dart | 239 ++++++++++++++++++ .../header/reorder_bottom_sheet.dart | 196 ++++++++++++++ .../product_page/new_product_page.dart | 206 ++++++--------- .../lib/pages/product/summary_card.dart | 67 +---- packages/smooth_app/pubspec.lock | 2 +- 7 files changed, 559 insertions(+), 215 deletions(-) create mode 100644 packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart create mode 100644 packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart diff --git a/packages/smooth_app/ios/Podfile.lock b/packages/smooth_app/ios/Podfile.lock index f8e8cc465609..626145d3bb14 100644 --- a/packages/smooth_app/ios/Podfile.lock +++ b/packages/smooth_app/ios/Podfile.lock @@ -250,53 +250,53 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_settings: 3507c575c2b18a462c99948f61d5de21d4420999 - audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab - camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf - connectivity_plus: 2256d3e20624a7749ed21653aafe291a46446fee - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc + audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 + camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 + connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_custom_tabs_ios: dd647919edd75e82ba6b00009eb3460a28c011b8 - flutter_email_sender: 2397f5e84aaacfb61af569637a963e7c687858d8 - flutter_icmp_ping: 47c1df3440c18ddd39fc457e607bb3b42d4a339f - flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 - flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 - flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + flutter_custom_tabs_ios: a651b18786388923b62de8c0537607de87c2eccf + flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40 + flutter_icmp_ping: 2b159955eee0c487c766ad83fec224ae35e7c935 + flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e + flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - iso_countries: 7ca741b02ae533b86f1f18a8bb52961d776ac77e + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + iso_countries: eb09d40f388e4c65e291e0bb36a701dfe7de6c74 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e - mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 + mobile_scanner: fd0054c52ede661e80bf5a4dea477a2467356bee MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - qr_code_scanner: d77f94ecc9abf96d9b9b8fc04ef13f611e5a147a - rive_common: dd421daaf9ae69f0125aa761dd96abd278399952 + qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e + rive_common: 4743dbfd2911c99066547a3c6454681e0fa907df SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: 38ed8bf38eab5812787274bf591e528074c19e02 - sentry_flutter: a72ca0eb6e78335db7c4ddcddd1b9f6c8ed5b764 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - torch_light: d093d579a221a59ef8a6b8c0eca20d52f7178087 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + sentry_flutter: 7d1f1df30f3768c411603ed449519bbb90a7d87b + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + torch_light: 682062fa12102172fa38b6b14c106d93b060f83e + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 PODFILE CHECKSUM: 6ac49c02151268e5844d0422787205b7cbdd62d2 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index b0a0854c449a..f12cdff2f93e 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -3597,5 +3597,9 @@ "type": "String" } } - } + }, + "for_me": "For me", + "website": "Website", + "prices": "Prices", + "folksonomy": "Folksonomy" } diff --git a/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart b/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart new file mode 100644 index 000000000000..c88faa246f30 --- /dev/null +++ b/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_simple_button.dart'; +import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; +import 'package:smooth_app/pages/folksonomy/folksonomy_card.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; +import 'package:smooth_app/pages/prices/prices_card.dart'; +import 'package:smooth_app/pages/product/product_page/header/reorder_bottom_sheet.dart'; +import 'package:smooth_app/pages/product/website_card.dart'; +import 'package:smooth_app/themes/smooth_theme.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; + +class ProductTab { + ProductTab({ + required this.labelBuilder, + required this.builder, + }); + + final Widget Function(BuildContext) labelBuilder; + final Widget Function(Product) builder; +} + +class ProductTabBar extends StatelessWidget { + const ProductTabBar({ + required this.tabController, + required this.tabs, + }); + + final TabController tabController; + final List tabs; + + @override + Widget build(BuildContext context) { + final SmoothColorsThemeExtension themeExtension = + context.extension(); + + return SliverPersistentHeader( + delegate: TabBarDelegate( + TabBar( + controller: tabController, + isScrollable: true, + tabs: tabs + .map((ProductTab tab) => Tab( + child: tab.labelBuilder(context), + )) + .toList(growable: false), + labelColor: Theme.of(context).primaryColor, + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15.0, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15.0, + ), + overlayColor: WidgetStateProperty.all( + themeExtension.primaryLight, + ), + dividerColor: themeExtension.primaryNormal, + automaticIndicatorColorAdjustment: false, + indicatorSize: TabBarIndicatorSize.tab, + indicator: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).primaryColor, + width: 3.0, + ), + ), + color: themeExtension.primaryLight, + ), + tabAlignment: TabAlignment.start, + ), + ), + pinned: true, + ); + } + + static List extractTabsFromProduct({ + required BuildContext context, + required Product product, + }) { + final List tabs = []; + + final List roots = + KnowledgePanelsBuilder.getRootPanelElements(product); + for (final KnowledgePanelElement root in roots) { + final List children = KnowledgePanelsBuilder.getChildren( + context, + panelElement: root, + product: product, + onboardingMode: false, + ); + + tabs.add( + ProductTab( + labelBuilder: (BuildContext c) => + Text((children.first as KnowledgePanelTitle).title), + builder: (Product p) => ListView( + padding: EdgeInsets.zero, + children: KnowledgePanelsBuilder.getChildren( + context, + panelElement: root, + product: p, + onboardingMode: false, + ).sublist(1), + ), + ), + ); + } + + return _addHardCodedTabs(context, product, tabs); + } + + static List _addHardCodedTabs( + BuildContext context, + Product product, + List tabs, + ) { + tabs.insert( + 0, + ProductTab( + labelBuilder: (BuildContext c) => + Text(AppLocalizations.of(context).for_me), + builder: (Product p) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SmoothSimpleButton( + onPressed: () { + showSmoothReorderBottomSheet( + context, + items: tabs.map((ProductTab tab) { + return ReorderableItem(data: tab); + }).toList(), + onReorder: + (List> reorderedItems) { + tabs.clear(); + tabs.addAll( + reorderedItems.map((ReorderableItem item) { + return item.data; + }).toList(), + ); + }, + labelBuilder: ( + BuildContext context, + ReorderableItem item, + int index, + ) { + return DefaultTextStyle.merge( + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + child: item.data.labelBuilder(context), + ); + }, + ); + }, + child: const Text('Reorder tabs'), + ), + ], + ), + ), + ); + if (product.website != null && product.website!.trim().isNotEmpty) { + tabs.add( + ProductTab( + labelBuilder: (BuildContext c) => + Text(AppLocalizations.of(context).website), + builder: (Product p) => ListView( + padding: EdgeInsets.zero, + children: [ + WebsiteCard(p.website!), + ], + ), + ), + ); + } + tabs.add( + ProductTab( + labelBuilder: (BuildContext c) => + Text(AppLocalizations.of(context).prices), + builder: (Product p) => ListView( + padding: EdgeInsets.zero, + children: [ + PricesCard(p), + ], + ), + ), + ); + + if (context.read().getFlag( + UserPreferencesDevMode.userPreferencesFlagHideFolksonomy) == + false) { + tabs.add( + ProductTab( + labelBuilder: (BuildContext c) => + Text(AppLocalizations.of(context).folksonomy), + builder: (Product p) => ListView( + padding: EdgeInsets.zero, + children: [FolksonomyCard(p)], + ), + ), + ); + } + + return tabs; + } +} + +class TabBarDelegate extends SliverPersistentHeaderDelegate { + TabBarDelegate(this.tabBar); + + final TabBar tabBar; + + @override + double get minExtent => tabBar.preferredSize.height; + + @override + double get maxExtent => tabBar.preferredSize.height; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: tabBar, + ); + } + + @override + bool shouldRebuild(covariant TabBarDelegate oldDelegate) { + return tabBar != oldDelegate.tabBar; + } +} diff --git a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart new file mode 100644 index 000000000000..c5794dc2303d --- /dev/null +++ b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.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/resources/app_icons.dart' as icons; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; + +class ReorderBottomSheet extends StatelessWidget { + const ReorderBottomSheet({ + required this.items, + required this.onReorder, + required this.labelBuilder, + this.onVisibilityToggle, + required this.title, + }); + + final List> items; + final ValueChanged>> onReorder; + final LabelBuilder labelBuilder; + final ValueChanged>? onVisibilityToggle; + final String title; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider>( + create: (_) => ReorderBottomSheetProvider(items), + child: Consumer>( + builder: + (BuildContext context, ReorderBottomSheetProvider provider, _) { + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.6, + maxChildSize: 0.9, + minChildSize: 0.4, + builder: (_, ScrollController scrollController) { + return ClipRRect( + borderRadius: const BorderRadius.vertical(top: ROUNDED_RADIUS), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).canvasColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SmoothModalSheetHeader( + title: 'Reorder tabs', + ), + Expanded( + child: ReorderableListView.builder( + scrollController: scrollController, + padding: const EdgeInsets.all(MEDIUM_SPACE), + proxyDecorator: ( + Widget child, + int index, + Animation animation, + ) => + Transform.scale( + scale: 1.0 + (0.05 * animation.value), + child: Opacity( + opacity: 0.8, + child: child, + ), + ), + itemBuilder: (BuildContext context, int index) { + final ReorderableItem item = + provider.items[index]; + return Container( + key: ValueKey(item.data), + margin: + const EdgeInsets.only(bottom: MEDIUM_SPACE), + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + color: item.visible + ? SmoothColorsThemeExtension.defaultValues() + .primaryMedium + : SmoothColorsThemeExtension.defaultValues() + .primaryLight, + borderRadius: ROUNDED_BORDER_RADIUS, + ), + child: Row( + children: [ + if (onVisibilityToggle != null) + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: SmoothColorsThemeExtension + .defaultValues() + .primarySemiDark, + ), + child: IconButton( + visualDensity: VisualDensity.compact, + iconSize: 16.0, + icon: const icons.Eye.visible( + color: Colors.white, + ), + onPressed: () => + onVisibilityToggle?.call(item), + ), + ), + if (onVisibilityToggle != null) + const SizedBox(width: MEDIUM_SPACE), + labelBuilder(context, item, index), + const Spacer(), + Icon( + Icons.drag_handle, + color: SmoothColorsThemeExtension + .defaultValues() + .primaryDark, + ), + ], + ), + ); + }, + itemCount: provider.items.length, + onReorder: (int oldIndex, int newIndex) { + provider.reorder(oldIndex, newIndex); + onReorder(provider.items); + }, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } +} + +class ReorderableItem { + ReorderableItem({required this.data, this.visible = true}); + + final T data; + bool visible; + + ReorderableItem copyWith({bool? visible}) { + return ReorderableItem( + data: data, + visible: visible ?? this.visible, + ); + } +} + +class ReorderBottomSheetProvider extends ChangeNotifier { + ReorderBottomSheetProvider(this.items); + + List> items; + + void reorder(int oldIndex, int newIndex) { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final ReorderableItem item = items.removeAt(oldIndex); + items.insert(newIndex, item); + notifyListeners(); + } + + void toggleVisibility(ReorderableItem item) { + final int index = items.indexOf(item); + if (index != -1) { + items[index] = item.copyWith(visible: !item.visible); + notifyListeners(); + } + } +} + +typedef LabelBuilder = Widget Function( + BuildContext context, + ReorderableItem item, + int index, +); + +void showSmoothReorderBottomSheet( + BuildContext context, { + required List> items, + required ValueChanged>> onReorder, + required LabelBuilder labelBuilder, + String title = 'Reorder Items', +}) { + showSmoothModalSheet( + context: context, + minHeight: 0.6, + builder: (_) => ReorderBottomSheet( + items: items, + onReorder: onReorder, + labelBuilder: labelBuilder, + title: title, + ), + ); +} diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart index f1c99d0088b7..2a2ea49e2383 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; -import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/data_models/product_list.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/up_to_date_changes.dart'; @@ -12,26 +10,14 @@ import 'package:smooth_app/data_models/up_to_date_mixin.dart'; import 'package:smooth_app/database/dao_product_last_access.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; -import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/product_compatibility_helper.dart'; -import 'package:smooth_app/pages/folksonomy/folksonomy_card.dart'; -import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; -import 'package:smooth_app/pages/prices/prices_card.dart'; -import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/product_page/footer/new_product_footer.dart'; +import 'package:smooth_app/pages/product/product_page/header/product_tabs.dart'; import 'package:smooth_app/pages/product/product_page/new_product_header.dart'; import 'package:smooth_app/pages/product/product_page/new_product_page_loading_indicator.dart'; import 'package:smooth_app/pages/product/product_questions_widget.dart'; -import 'package:smooth_app/pages/product/reorderable_knowledge_panel_page.dart'; -import 'package:smooth_app/pages/product/reordered_knowledge_panel_cards.dart'; -import 'package:smooth_app/pages/product/standard_knowledge_panel_cards.dart'; import 'package:smooth_app/pages/product/summary_card.dart'; -import 'package:smooth_app/pages/product/website_card.dart'; import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; -import 'package:smooth_app/themes/smooth_theme.dart'; -import 'package:smooth_app/themes/smooth_theme_colors.dart'; -import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/widget_height.dart'; @@ -56,8 +42,12 @@ class ProductPage extends StatefulWidget { } class ProductPageState extends State - with TraceableClientMixin, UpToDateMixin { + with TraceableClientMixin, UpToDateMixin, SingleTickerProviderStateMixin { final ScrollController _scrollController = ScrollController(); + + late final TabController _tabController; + late List _tabs; + late ProductPreferences _productPreferences; bool _keepRobotoffQuestionsAlive = true; @@ -72,6 +62,18 @@ class ProductPageState extends State final LocalDatabase localDatabase = context.read(); initUpToDate(widget.product, localDatabase); DaoProductLastAccess(localDatabase).put(barcode); + + _tabs = ProductTabBar.extractTabsFromProduct( + context: context, + product: upToDateProduct, + ); + + _tabController = TabController( + length: _tabs.length, + vsync: this, + initialIndex: 1, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { _updateLocalDatabaseWithProductHistory(context); }); @@ -82,8 +84,6 @@ class ProductPageState extends State final ExternalScanCarouselManagerState carouselManager = ExternalScanCarouselManager.read(context); carouselManager.currentBarcode = barcode; - final SmoothColorsThemeExtension themeExtension = - context.extension(); _productPreferences = context.watch(); final LocalDatabase localDatabase = context.watch(); @@ -118,42 +118,76 @@ class ProductPageState extends State spaceBehindStatusBar: false, changeStatusBarBrightness: false, statusBarBackgroundColor: Colors.transparent, - backgroundColor: - !context.darkTheme() ? themeExtension.primaryLight : null, - body: Stack( - children: [ - _buildProductBody(context, bottomPadding), - Positioned( - left: 0.0, - right: 0.0, - top: 0.0, - child: ProductHeader( - backButtonType: widget.backButton, + body: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: (BuildContext context, bool value) { + return [ + SliverAppBar( + floating: false, + pinned: true, + leading: const SizedBox(), + leadingWidth: 0.0, + titleSpacing: 0.0, + title: ProductHeader( + backButtonType: widget.backButton, + ), ), - ), - Positioned( - left: 0.0, - right: 0.0, - bottom: 0.0, - child: MeasureSize( - onChange: (Size size) { - if (size.height != bottomPadding) { - setState(() => bottomPadding = size.height); - } - }, - child: hasPendingOperations - ? const ProductPageLoadingIndicator() - : KeepQuestionWidgetAlive( - keepWidgetAlive: _keepRobotoffQuestionsAlive, - child: ProductQuestionsWidget(upToDateProduct), - ), + SliverToBoxAdapter( + child: SummaryCard(upToDateProduct, _productPreferences), + ), + ProductTabBar( + tabController: _tabController, + tabs: _tabs, ), + ]; + }, + body: TabBarView( + controller: _tabController, + children: _tabs + .map((ProductTab tab) => tab.builder(upToDateProduct)) + .toList(), + ), + ), + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MeasureSize( + onChange: (Size size) { + if (size.height != bottomPadding) { + setState(() => bottomPadding = size.height); + } + }, + child: hasPendingOperations + ? const ProductPageLoadingIndicator() + : KeepQuestionWidgetAlive( + keepWidgetAlive: _keepRobotoffQuestionsAlive, + child: ProductQuestionsWidget(upToDateProduct), + ), ), + const ProductFooter(), ], ), - bottomNavigationBar: const ProductFooter(), ), ); + + // TODO(primael): Decide where to move these cards with new tab view + /* if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: SmoothLargeButtonWithIcon( + text: appLocalizations.reorder_attribute_action, + leadingIcon: const Icon(Icons.sort), + onPressed: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + ReorderableKnowledgePanelPage(upToDateProduct), + ), + ), + ), + ), */ } Future _updateLocalDatabaseWithProductHistory( @@ -167,84 +201,6 @@ class ProductPageState extends State localDatabase.notifyListeners(); } - Widget _buildProductBody(BuildContext context, double bottomPadding) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - final UserPreferences userPreferences = context.watch(); - - return SafeArea( - child: RefreshIndicator( - onRefresh: () async => ProductRefresher().fetchAndRefresh( - barcode: barcode, - context: context, - ), - child: ListView( - // /!\ Smart Dart - // `physics: const AlwaysScrollableScrollPhysics()` - // means that we will always scroll, even if it's pointless. - // Why do we need to? For the RefreshIndicator, that wouldn't be - // triggered on a ListView smaller than the screen - // (as there will be no scroll). - physics: const AlwaysScrollableScrollPhysics(), - controller: _scrollController, - padding: const EdgeInsets.only( - top: kToolbarHeight + LARGE_SPACE, - bottom: LARGE_SPACE, - ), - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: SMALL_SPACE, - ), - child: HeroMode( - enabled: widget.withHeroAnimation && - widget.heroTag?.isNotEmpty == true, - child: SummaryCard( - upToDateProduct, - _productPreferences, - heroTag: widget.heroTag, - isFullVersion: true, - ), - ), - ), - if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? - false) - ReorderedKnowledgePanelCards(upToDateProduct) - else - StandardKnowledgePanelCards(upToDateProduct), - // TODO(monsieurtanuki): include website in reordered knowledge panels - if (upToDateProduct.website != null && - upToDateProduct.website!.trim().isNotEmpty) - WebsiteCard(upToDateProduct.website!), - PricesCard(upToDateProduct), - if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagHideFolksonomy) == - false) - FolksonomyCard(upToDateProduct), - if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? - false) - Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: SmoothLargeButtonWithIcon( - text: appLocalizations.reorder_attribute_action, - leadingIcon: const Icon(Icons.sort), - onPressed: () async => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - ReorderableKnowledgePanelPage(upToDateProduct), - ), - ), - ), - ), - if (bottomPadding > 0) SizedBox(height: bottomPadding), - ], - ), - ), - ); - } - void startRobotoffQuestion() { setState(() => _keepRobotoffQuestionsAlive = true); } diff --git a/packages/smooth_app/lib/pages/product/summary_card.dart b/packages/smooth_app/lib/pages/product/summary_card.dart index 47bae3cb5680..2c0679facc81 100644 --- a/packages/smooth_app/lib/pages/product/summary_card.dart +++ b/packages/smooth_app/lib/pages/product/summary_card.dart @@ -144,67 +144,15 @@ class _SummaryCardState extends State with UpToDateMixin { child: ClipRRect( borderRadius: ROUNDED_BORDER_RADIUS, child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: buildProductSmoothCard( - body: Padding( - padding: widget.contentPadding ?? SMOOTH_CARD_PADDING, - child: _buildSummaryCardContent(context), - ), - borderRadius: const BorderRadius.vertical(top: ROUNDED_RADIUS), - margin: EdgeInsets.zero, - ), - ), - Container( - width: double.infinity, - padding: widget.buttonPadding ?? - const EdgeInsets.symmetric( - vertical: SMALL_SPACE, - ), - decoration: BoxDecoration( - color: context.lightTheme() - ? themeExtension.primaryDark - : themeExtension.primarySemiDark, - borderRadius: - const BorderRadius.vertical(bottom: ROUNDED_RADIUS), - ), - child: Padding( - padding: const EdgeInsetsDirectional.only( - start: SMALL_SPACE, - end: SMALL_SPACE, - bottom: 2.0, - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context).tap_for_more, - style: const TextStyle( - color: Colors.white, - fontSize: 15.0, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox( - width: BALANCED_SPACE, - ), - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: themeExtension.orange, - ), - padding: const EdgeInsets.all(VERY_SMALL_SPACE), - child: const icons.Arrow.right( - color: Colors.white, - size: 12.0, - ), - ), - ], - ), - ), + buildProductSmoothCard( + body: Padding( + padding: widget.contentPadding ?? SMOOTH_CARD_PADDING, + child: _buildSummaryCardContent(context), ), + borderRadius: const BorderRadius.vertical(top: ROUNDED_RADIUS), + margin: EdgeInsets.zero, ), ], ), @@ -310,6 +258,7 @@ class _SummaryCardState extends State with UpToDateMixin { } final Widget child = Column( + mainAxisSize: MainAxisSize.min, children: [ ProductTitleCard( upToDateProduct, diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock index 4f86027d0da9..e4911164d756 100644 --- a/packages/smooth_app/pubspec.lock +++ b/packages/smooth_app/pubspec.lock @@ -1865,5 +1865,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.6.1 <4.0.0" flutter: ">=3.24.0" From 729e128462e22580e4948c91c49455fc57aab200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:58:57 +0100 Subject: [PATCH 2/9] dart analyze --- .../pages/product/product_page/header/product_tabs.dart | 2 +- .../product/product_page/header/reorder_bottom_sheet.dart | 2 ++ packages/smooth_app/lib/pages/product/summary_card.dart | 7 ------- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart b/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart index c88faa246f30..c7965d278155 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart @@ -126,7 +126,7 @@ class ProductTabBar extends StatelessWidget { Text(AppLocalizations.of(context).for_me), builder: (Product p) => Row( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ SmoothSimpleButton( onPressed: () { showSmoothReorderBottomSheet( diff --git a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart index c5794dc2303d..09874a3c21a5 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart @@ -180,6 +180,7 @@ void showSmoothReorderBottomSheet( BuildContext context, { required List> items, required ValueChanged>> onReorder, + ValueChanged>? onVisibilityToggle, required LabelBuilder labelBuilder, String title = 'Reorder Items', }) { @@ -189,6 +190,7 @@ void showSmoothReorderBottomSheet( builder: (_) => ReorderBottomSheet( items: items, onReorder: onReorder, + onVisibilityToggle: onVisibilityToggle, labelBuilder: labelBuilder, title: title, ), diff --git a/packages/smooth_app/lib/pages/product/summary_card.dart b/packages/smooth_app/lib/pages/product/summary_card.dart index 2c0679facc81..43d112aace95 100644 --- a/packages/smooth_app/lib/pages/product/summary_card.dart +++ b/packages/smooth_app/lib/pages/product/summary_card.dart @@ -21,10 +21,6 @@ import 'package:smooth_app/pages/product/hideable_container.dart'; import 'package:smooth_app/pages/product/product_field_editor.dart'; import 'package:smooth_app/pages/product/product_incomplete_card.dart'; import 'package:smooth_app/pages/product/summary_attribute_group.dart'; -import 'package:smooth_app/resources/app_icons.dart' as icons; -import 'package:smooth_app/themes/smooth_theme.dart'; -import 'package:smooth_app/themes/smooth_theme_colors.dart'; -import 'package:smooth_app/themes/theme_provider.dart'; const List _ATTRIBUTE_GROUP_ORDER = [ AttributeGroup.ATTRIBUTE_GROUP_ALLERGENS, @@ -132,9 +128,6 @@ class _SummaryCardState extends State with UpToDateMixin { } Widget _buildLimitedSizeSummaryCard() { - final SmoothColorsThemeExtension themeExtension = - context.extension(); - return Padding( padding: widget.margin ?? const EdgeInsets.symmetric( From a94ffec40361122a29f2616766c5aba1e00e47ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:29:47 +0100 Subject: [PATCH 3/9] Implemented review comments --- packages/smooth_app/lib/l10n/app_en.arb | 20 ++- ...oduct_tabs.dart => product_page_tabs.dart} | 165 +++++++++--------- .../header/reorder_bottom_sheet.dart | 95 +++++----- .../product_page/new_product_page.dart | 10 +- .../smooth_app/lib/widgets/smooth_tabbar.dart | 12 +- 5 files changed, 146 insertions(+), 156 deletions(-) rename packages/smooth_app/lib/pages/product/product_page/header/{product_tabs.dart => product_page_tabs.dart} (52%) diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index f12cdff2f93e..1aed63b4a5a7 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -3598,8 +3598,20 @@ } } }, - "for_me": "For me", - "website": "Website", - "prices": "Prices", - "folksonomy": "Folksonomy" + "product_page_tab_for_me": "For me", + "@product_page_tab_for_me": { + "description": "Label of the for me tab on the product page" + }, + "product_page_tab_website": "Website", + "@product_page_tab_website": { + "description": "Label of the website tab on the product page" + }, + "product_page_tab_prices": "Prices", + "@product_page_tab_prices": { + "description": "Label of the prices tab on the product page" + }, + "product_page_tab_folksonomy": "Folksonomy", + "@product_page_tab_folksonomy": { + "description": "Label of the folksonomy tab on the product page" + } } diff --git a/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart b/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart similarity index 52% rename from packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart rename to packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart index c7965d278155..6a5184d4db15 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/product_tabs.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; +import 'package:smooth_app/generic_lib/bottom_sheets/smooth_bottom_sheet.dart'; import 'package:smooth_app/generic_lib/buttons/smooth_simple_button.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; import 'package:smooth_app/pages/folksonomy/folksonomy_card.dart'; @@ -10,102 +11,76 @@ import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/pages/prices/prices_card.dart'; import 'package:smooth_app/pages/product/product_page/header/reorder_bottom_sheet.dart'; import 'package:smooth_app/pages/product/website_card.dart'; -import 'package:smooth_app/themes/smooth_theme.dart'; -import 'package:smooth_app/themes/smooth_theme_colors.dart'; +import 'package:smooth_app/widgets/smooth_tabbar.dart'; -class ProductTab { - ProductTab({ +class ProductPageTab { + const ProductPageTab({ required this.labelBuilder, required this.builder, }); - final Widget Function(BuildContext) labelBuilder; + final String Function(BuildContext) labelBuilder; final Widget Function(Product) builder; } -class ProductTabBar extends StatelessWidget { - const ProductTabBar({ +class ProductPageTabBar extends StatelessWidget { + const ProductPageTabBar({ required this.tabController, required this.tabs, }); final TabController tabController; - final List tabs; + final List tabs; @override Widget build(BuildContext context) { - final SmoothColorsThemeExtension themeExtension = - context.extension(); - return SliverPersistentHeader( - delegate: TabBarDelegate( - TabBar( - controller: tabController, - isScrollable: true, - tabs: tabs - .map((ProductTab tab) => Tab( - child: tab.labelBuilder(context), - )) - .toList(growable: false), - labelColor: Theme.of(context).primaryColor, - labelStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15.0, - ), - unselectedLabelStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15.0, - ), - overlayColor: WidgetStateProperty.all( - themeExtension.primaryLight, - ), - dividerColor: themeExtension.primaryNormal, - automaticIndicatorColorAdjustment: false, - indicatorSize: TabBarIndicatorSize.tab, - indicator: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).primaryColor, - width: 3.0, - ), - ), - color: themeExtension.primaryLight, - ), - tabAlignment: TabAlignment.start, - ), + delegate: _TabBarDelegate( + PreferredSize( + preferredSize: const Size.fromHeight(SmoothTabBar.TAB_BAR_HEIGHT), + child: SmoothTabBar( + tabController: tabController, + items: tabs.map((ProductPageTab tab) { + return SmoothTabBarItem( + label: tab.labelBuilder(context), + value: tab, + ); + }).toList(), + onTabChanged: (ProductPageTab tab) {}, + )), ), pinned: true, ); } - static List extractTabsFromProduct({ + static List extractTabsFromProduct({ required BuildContext context, required Product product, }) { - final List tabs = []; + final List tabs = []; final List roots = KnowledgePanelsBuilder.getRootPanelElements(product); for (final KnowledgePanelElement root in roots) { - final List children = KnowledgePanelsBuilder.getChildren( + List children = KnowledgePanelsBuilder.getChildren( context, panelElement: root, product: product, onboardingMode: false, ); + final KnowledgePanelTitle knowledgePanelTitle = + children.first as KnowledgePanelTitle; + + children = children.sublist(1); + tabs.add( - ProductTab( - labelBuilder: (BuildContext c) => - Text((children.first as KnowledgePanelTitle).title), - builder: (Product p) => ListView( + ProductPageTab( + labelBuilder: (BuildContext c) => knowledgePanelTitle.title, + builder: (Product p) => ListView.builder( padding: EdgeInsets.zero, - children: KnowledgePanelsBuilder.getChildren( - context, - panelElement: root, - product: p, - onboardingMode: false, - ).sublist(1), + itemCount: children.length - 1, + itemBuilder: (BuildContext context, int index) => children[index], ), ), ); @@ -114,45 +89,40 @@ class ProductTabBar extends StatelessWidget { return _addHardCodedTabs(context, product, tabs); } - static List _addHardCodedTabs( + static List _addHardCodedTabs( BuildContext context, Product product, - List tabs, + List tabs, ) { tabs.insert( 0, - ProductTab( + ProductPageTab( labelBuilder: (BuildContext c) => - Text(AppLocalizations.of(context).for_me), + AppLocalizations.of(c).product_page_tab_for_me, builder: (Product p) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SmoothSimpleButton( onPressed: () { - showSmoothReorderBottomSheet( + showSmoothReorderBottomSheet( context, - items: tabs.map((ProductTab tab) { - return ReorderableItem(data: tab); + items: tabs.map((ProductPageTab tab) { + return tab; }).toList(), - onReorder: - (List> reorderedItems) { + onReorder: (List reorderedItems) { tabs.clear(); - tabs.addAll( - reorderedItems.map((ReorderableItem item) { - return item.data; - }).toList(), - ); + tabs.addAll(reorderedItems); }, labelBuilder: ( BuildContext context, - ReorderableItem item, + ProductPageTab item, int index, ) { return DefaultTextStyle.merge( style: const TextStyle( fontWeight: FontWeight.bold, ), - child: item.data.labelBuilder(context), + child: Text(item.labelBuilder(context)), ); }, ); @@ -163,11 +133,11 @@ class ProductTabBar extends StatelessWidget { ), ), ); - if (product.website != null && product.website!.trim().isNotEmpty) { + if (product.website?.trim().isNotEmpty == true) { tabs.add( - ProductTab( + ProductPageTab( labelBuilder: (BuildContext c) => - Text(AppLocalizations.of(context).website), + AppLocalizations.of(c).product_page_tab_website, builder: (Product p) => ListView( padding: EdgeInsets.zero, children: [ @@ -178,9 +148,9 @@ class ProductTabBar extends StatelessWidget { ); } tabs.add( - ProductTab( + ProductPageTab( labelBuilder: (BuildContext c) => - Text(AppLocalizations.of(context).prices), + AppLocalizations.of(c).product_page_tab_prices, builder: (Product p) => ListView( padding: EdgeInsets.zero, children: [ @@ -194,9 +164,9 @@ class ProductTabBar extends StatelessWidget { UserPreferencesDevMode.userPreferencesFlagHideFolksonomy) == false) { tabs.add( - ProductTab( + ProductPageTab( labelBuilder: (BuildContext c) => - Text(AppLocalizations.of(context).folksonomy), + AppLocalizations.of(c).product_page_tab_folksonomy, builder: (Product p) => ListView( padding: EdgeInsets.zero, children: [FolksonomyCard(p)], @@ -207,18 +177,39 @@ class ProductTabBar extends StatelessWidget { return tabs; } + + static void showSmoothReorderBottomSheet( + BuildContext context, { + required List items, + required ValueChanged> onReorder, + ValueChanged? onVisibilityToggle, + required LabelBuilder labelBuilder, + String title = 'Reorder Items', + }) { + showSmoothModalSheet( + context: context, + minHeight: 0.6, + builder: (_) => ReorderBottomSheet( + items: items, + onReorder: onReorder, + onVisibilityToggle: onVisibilityToggle, + labelBuilder: labelBuilder, + title: title, + ), + ); + } } -class TabBarDelegate extends SliverPersistentHeaderDelegate { - TabBarDelegate(this.tabBar); +class _TabBarDelegate extends SliverPersistentHeaderDelegate { + _TabBarDelegate(this.tabBar); - final TabBar tabBar; + final PreferredSizeWidget tabBar; @override double get minExtent => tabBar.preferredSize.height; @override - double get maxExtent => tabBar.preferredSize.height; + double get maxExtent => minExtent; @override Widget build( @@ -233,7 +224,7 @@ class TabBarDelegate extends SliverPersistentHeaderDelegate { } @override - bool shouldRebuild(covariant TabBarDelegate oldDelegate) { + bool shouldRebuild(covariant _TabBarDelegate oldDelegate) { return tabBar != oldDelegate.tabBar; } } diff --git a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart index 09874a3c21a5..ab07bfe2876a 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart @@ -3,30 +3,40 @@ import 'package:provider/provider.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/resources/app_icons.dart' as icons; +import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/themes/smooth_theme_colors.dart'; +typedef LabelBuilder = Widget Function( + BuildContext context, + T item, + int index, +); + class ReorderBottomSheet extends StatelessWidget { - const ReorderBottomSheet({ - required this.items, + ReorderBottomSheet({ + required List items, required this.onReorder, required this.labelBuilder, this.onVisibilityToggle, required this.title, - }); + }) : _items = items.map((T data) => _ReorderableItem(data: data)).toList(); - final List> items; - final ValueChanged>> onReorder; + final List<_ReorderableItem> _items; + final ValueChanged> onReorder; final LabelBuilder labelBuilder; - final ValueChanged>? onVisibilityToggle; + final ValueChanged? onVisibilityToggle; final String title; @override Widget build(BuildContext context) { - return ChangeNotifierProvider>( - create: (_) => ReorderBottomSheetProvider(items), - child: Consumer>( + final SmoothColorsThemeExtension theme = + context.extension(); + + return ChangeNotifierProvider<_ReorderBottomSheetProvider>( + create: (_) => _ReorderBottomSheetProvider(_items), + child: Consumer<_ReorderBottomSheetProvider>( builder: - (BuildContext context, ReorderBottomSheetProvider provider, _) { + (BuildContext context, _ReorderBottomSheetProvider provider, _) { return DraggableScrollableSheet( expand: false, initialChildSize: 0.6, @@ -65,19 +75,17 @@ class ReorderBottomSheet extends StatelessWidget { ), ), itemBuilder: (BuildContext context, int index) { - final ReorderableItem item = + final _ReorderableItem item = provider.items[index]; return Container( key: ValueKey(item.data), - margin: - const EdgeInsets.only(bottom: MEDIUM_SPACE), + margin: const EdgeInsetsDirectional.only( + bottom: MEDIUM_SPACE), padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: item.visible - ? SmoothColorsThemeExtension.defaultValues() - .primaryMedium - : SmoothColorsThemeExtension.defaultValues() - .primaryLight, + ? theme.primaryMedium + : theme.primaryLight, borderRadius: ROUNDED_BORDER_RADIUS, ), child: Row( @@ -97,12 +105,12 @@ class ReorderBottomSheet extends StatelessWidget { color: Colors.white, ), onPressed: () => - onVisibilityToggle?.call(item), + onVisibilityToggle?.call(item.data), ), ), if (onVisibilityToggle != null) const SizedBox(width: MEDIUM_SPACE), - labelBuilder(context, item, index), + labelBuilder(context, item.data, index), const Spacer(), Icon( Icons.drag_handle, @@ -117,7 +125,9 @@ class ReorderBottomSheet extends StatelessWidget { itemCount: provider.items.length, onReorder: (int oldIndex, int newIndex) { provider.reorder(oldIndex, newIndex); - onReorder(provider.items); + onReorder(provider.items + .map((_ReorderableItem item) => item.data) + .toList()); }, ), ), @@ -133,35 +143,35 @@ class ReorderBottomSheet extends StatelessWidget { } } -class ReorderableItem { - ReorderableItem({required this.data, this.visible = true}); +class _ReorderableItem { + _ReorderableItem({required this.data, this.visible = true}); final T data; bool visible; - ReorderableItem copyWith({bool? visible}) { - return ReorderableItem( + _ReorderableItem copyWith({bool? visible}) { + return _ReorderableItem( data: data, visible: visible ?? this.visible, ); } } -class ReorderBottomSheetProvider extends ChangeNotifier { - ReorderBottomSheetProvider(this.items); +class _ReorderBottomSheetProvider extends ChangeNotifier { + _ReorderBottomSheetProvider(this.items); - List> items; + List<_ReorderableItem> items; void reorder(int oldIndex, int newIndex) { if (newIndex > oldIndex) { newIndex -= 1; } - final ReorderableItem item = items.removeAt(oldIndex); + final _ReorderableItem item = items.removeAt(oldIndex); items.insert(newIndex, item); notifyListeners(); } - void toggleVisibility(ReorderableItem item) { + void toggleVisibility(_ReorderableItem item) { final int index = items.indexOf(item); if (index != -1) { items[index] = item.copyWith(visible: !item.visible); @@ -169,30 +179,3 @@ class ReorderBottomSheetProvider extends ChangeNotifier { } } } - -typedef LabelBuilder = Widget Function( - BuildContext context, - ReorderableItem item, - int index, -); - -void showSmoothReorderBottomSheet( - BuildContext context, { - required List> items, - required ValueChanged>> onReorder, - ValueChanged>? onVisibilityToggle, - required LabelBuilder labelBuilder, - String title = 'Reorder Items', -}) { - showSmoothModalSheet( - context: context, - minHeight: 0.6, - builder: (_) => ReorderBottomSheet( - items: items, - onReorder: onReorder, - onVisibilityToggle: onVisibilityToggle, - labelBuilder: labelBuilder, - title: title, - ), - ); -} diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart index 2a2ea49e2383..60852d92a58e 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart @@ -12,7 +12,7 @@ import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/helpers/product_compatibility_helper.dart'; import 'package:smooth_app/pages/product/product_page/footer/new_product_footer.dart'; -import 'package:smooth_app/pages/product/product_page/header/product_tabs.dart'; +import 'package:smooth_app/pages/product/product_page/header/product_page_tabs.dart'; import 'package:smooth_app/pages/product/product_page/new_product_header.dart'; import 'package:smooth_app/pages/product/product_page/new_product_page_loading_indicator.dart'; import 'package:smooth_app/pages/product/product_questions_widget.dart'; @@ -46,7 +46,7 @@ class ProductPageState extends State final ScrollController _scrollController = ScrollController(); late final TabController _tabController; - late List _tabs; + late List _tabs; late ProductPreferences _productPreferences; bool _keepRobotoffQuestionsAlive = true; @@ -63,7 +63,7 @@ class ProductPageState extends State initUpToDate(widget.product, localDatabase); DaoProductLastAccess(localDatabase).put(barcode); - _tabs = ProductTabBar.extractTabsFromProduct( + _tabs = ProductPageTabBar.extractTabsFromProduct( context: context, product: upToDateProduct, ); @@ -135,7 +135,7 @@ class ProductPageState extends State SliverToBoxAdapter( child: SummaryCard(upToDateProduct, _productPreferences), ), - ProductTabBar( + ProductPageTabBar( tabController: _tabController, tabs: _tabs, ), @@ -144,7 +144,7 @@ class ProductPageState extends State body: TabBarView( controller: _tabController, children: _tabs - .map((ProductTab tab) => tab.builder(upToDateProduct)) + .map((ProductPageTab tab) => tab.builder(upToDateProduct)) .toList(), ), ), diff --git a/packages/smooth_app/lib/widgets/smooth_tabbar.dart b/packages/smooth_app/lib/widgets/smooth_tabbar.dart index c1093d96530a..d771371a3491 100644 --- a/packages/smooth_app/lib/widgets/smooth_tabbar.dart +++ b/packages/smooth_app/lib/widgets/smooth_tabbar.dart @@ -123,11 +123,13 @@ class _SmoothTabBarState extends State> { class SmoothTabBarItem { const SmoothTabBarItem({ - required this.label, + this.label, + this.labelBuilder, required this.value, - }) : assert(label.length > 0); + }) : assert((label != null && label.length > 0) || labelBuilder != null); - final String label; + final String? label; + final WidgetBuilder? labelBuilder; final T value; } @@ -148,7 +150,9 @@ class _SmoothTab extends StatelessWidget { Widget build(BuildContext context) { final Widget child; if (leading == null && trailing == null) { - child = Text(item.label); + child = item.labelBuilder != null + ? item.labelBuilder!(context) + : Text(item.label!); } else { child = IconTheme( data: IconThemeData( From 9fce3635efb02f9f4b10c9f819cfd2c13da402f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:39:57 +0100 Subject: [PATCH 4/9] Reverted breaking changes in smooth_tabbar.dart --- packages/smooth_app/lib/widgets/smooth_tabbar.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/smooth_app/lib/widgets/smooth_tabbar.dart b/packages/smooth_app/lib/widgets/smooth_tabbar.dart index d771371a3491..c1093d96530a 100644 --- a/packages/smooth_app/lib/widgets/smooth_tabbar.dart +++ b/packages/smooth_app/lib/widgets/smooth_tabbar.dart @@ -123,13 +123,11 @@ class _SmoothTabBarState extends State> { class SmoothTabBarItem { const SmoothTabBarItem({ - this.label, - this.labelBuilder, + required this.label, required this.value, - }) : assert((label != null && label.length > 0) || labelBuilder != null); + }) : assert(label.length > 0); - final String? label; - final WidgetBuilder? labelBuilder; + final String label; final T value; } @@ -150,9 +148,7 @@ class _SmoothTab extends StatelessWidget { Widget build(BuildContext context) { final Widget child; if (leading == null && trailing == null) { - child = item.labelBuilder != null - ? item.labelBuilder!(context) - : Text(item.label!); + child = Text(item.label); } else { child = IconTheme( data: IconThemeData( From d39ef884cc5a999dc1d11a6e422baa5fd120fe67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:49:10 +0100 Subject: [PATCH 5/9] Small fixes --- .../product/product_page/header/product_page_tabs.dart | 8 ++++---- .../product/product_page/header/reorder_bottom_sheet.dart | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart b/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart index 6a5184d4db15..a2265926aa52 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart @@ -78,7 +78,7 @@ class ProductPageTabBar extends StatelessWidget { ProductPageTab( labelBuilder: (BuildContext c) => knowledgePanelTitle.title, builder: (Product p) => ListView.builder( - padding: EdgeInsets.zero, + padding: EdgeInsetsDirectional.zero, itemCount: children.length - 1, itemBuilder: (BuildContext context, int index) => children[index], ), @@ -139,7 +139,7 @@ class ProductPageTabBar extends StatelessWidget { labelBuilder: (BuildContext c) => AppLocalizations.of(c).product_page_tab_website, builder: (Product p) => ListView( - padding: EdgeInsets.zero, + padding: EdgeInsetsDirectional.zero, children: [ WebsiteCard(p.website!), ], @@ -152,7 +152,7 @@ class ProductPageTabBar extends StatelessWidget { labelBuilder: (BuildContext c) => AppLocalizations.of(c).product_page_tab_prices, builder: (Product p) => ListView( - padding: EdgeInsets.zero, + padding: EdgeInsetsDirectional.zero, children: [ PricesCard(p), ], @@ -168,7 +168,7 @@ class ProductPageTabBar extends StatelessWidget { labelBuilder: (BuildContext c) => AppLocalizations.of(c).product_page_tab_folksonomy, builder: (Product p) => ListView( - padding: EdgeInsets.zero, + padding: EdgeInsetsDirectional.zero, children: [FolksonomyCard(p)], ), ), diff --git a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart index ab07bfe2876a..cad93f61ee0a 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart @@ -81,7 +81,7 @@ class ReorderBottomSheet extends StatelessWidget { key: ValueKey(item.data), margin: const EdgeInsetsDirectional.only( bottom: MEDIUM_SPACE), - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsetsDirectional.all(12.0), decoration: BoxDecoration( color: item.visible ? theme.primaryMedium From 57f7f22757f044609356f8f081cc1645e372b4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Mon, 27 Jan 2025 08:57:55 +0100 Subject: [PATCH 6/9] Code optimization --- .../product/product_page/header/reorder_bottom_sheet.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart index cad93f61ee0a..8010932324be 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart @@ -94,9 +94,7 @@ class ReorderBottomSheet extends StatelessWidget { Container( decoration: BoxDecoration( shape: BoxShape.circle, - color: SmoothColorsThemeExtension - .defaultValues() - .primarySemiDark, + color: theme.primarySemiDark, ), child: IconButton( visualDensity: VisualDensity.compact, @@ -114,9 +112,7 @@ class ReorderBottomSheet extends StatelessWidget { const Spacer(), Icon( Icons.drag_handle, - color: SmoothColorsThemeExtension - .defaultValues() - .primaryDark, + color: theme.primaryDark, ), ], ), From 368a44ca332628e2faff57cf996fea358516b6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:23:39 +0100 Subject: [PATCH 7/9] Implemented review comments + Dev mode --- .../bottom_sheets/smooth_bottom_sheet.dart | 22 +++ .../smooth_reorder_bottom_sheet.dart | 0 packages/smooth_app/lib/l10n/app_en.arb | 8 ++ .../user_preferences_dev_mode.dart | 13 ++ .../header/product_page_tabs.dart | 79 ++++------ .../header/reorder_bottom_sheet.dart | 20 +-- .../product_page/new_product_page.dart | 136 ++++++++++++++---- 7 files changed, 193 insertions(+), 85 deletions(-) create mode 100644 packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart 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 ca10bd62440f..bebf44c16b52 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 @@ -6,6 +6,7 @@ import 'package:smooth_app/generic_lib/bottom_sheets/smooth_draggable_bottom_she import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/color_extension.dart'; import 'package:smooth_app/helpers/haptic_feedback_helper.dart'; +import 'package:smooth_app/pages/product/product_page/header/reorder_bottom_sheet.dart'; import 'package:smooth_app/resources/app_icons.dart' as icons; import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/themes/smooth_theme_colors.dart'; @@ -273,6 +274,27 @@ Future showSmoothAlertModalSheet({ ); } +void showSmoothReorderBottomSheet( + BuildContext context, { + required List items, + required ValueChanged> onReorder, + ValueChanged? onVisibilityToggle, + required LabelBuilder labelBuilder, + required String title, +}) { + showSmoothModalSheet( + context: context, + minHeight: 0.6, + builder: (_) => ReorderBottomSheet( + items: items, + onReorder: onReorder, + onVisibilityToggle: onVisibilityToggle, + labelBuilder: labelBuilder, + title: title, + ), + ); +} + class _SmoothListOfChoicesEndArrow extends StatelessWidget { const _SmoothListOfChoicesEndArrow(); diff --git a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 0894fd5ca832..0957daf50a7c 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -3824,5 +3824,13 @@ "product_page_tab_folksonomy": "Folksonomy", "@product_page_tab_folksonomy": { "description": "Label of the folksonomy tab on the product page" + }, + "product_page_reorder_tabs": "Reorder tabs", + "@product_page_reorder_tabs": { + "description": "Used for reorder tabs button and bottom sheet" + }, + "dev_preferences_use_product_tabs_title": "Use tabs", + "@dev_preferences_use_product_tabs_title": { + "description": "Title for the preference to use tabs on the product page" } } \ No newline at end of file diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart index da956d525e0f..5d78db82999a 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart @@ -46,6 +46,7 @@ class UserPreferencesDevMode extends AbstractUserPreferences { static const String userPreferencesFolksonomyHost = '__folksonomyHost'; static const String userPreferencesFlagEditIngredients = '__editIngredients'; static const String userPreferencesFlagHideFolksonomy = '__hideFolksonomy'; + static const String userPreferencesFlagUseProductTabs = '__useProductTabs'; static const String userPreferencesFlagBoostedComparison = '__boostedComparison'; static const String userPreferencesEnumScanMode = '__scanMode'; @@ -386,6 +387,18 @@ class UserPreferencesDevMode extends AbstractUserPreferences { _showSuccessMessage(); }, ), + UserPreferencesItemSwitch( + title: appLocalizations.dev_preferences_use_product_tabs_title, + value: userPreferences.getFlag(userPreferencesFlagUseProductTabs) ?? + false, + onChanged: (bool value) async { + await userPreferences.setFlag( + userPreferencesFlagUseProductTabs, + value, + ); + _showSuccessMessage(); + }, + ), UserPreferencesItemSection( label: appLocalizations.dev_mode_section_ui, ), diff --git a/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart b/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart index a2265926aa52..ce0906f2d32c 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart @@ -9,7 +9,6 @@ import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; import 'package:smooth_app/pages/folksonomy/folksonomy_card.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/pages/prices/prices_card.dart'; -import 'package:smooth_app/pages/product/product_page/header/reorder_bottom_sheet.dart'; import 'package:smooth_app/pages/product/website_card.dart'; import 'package:smooth_app/widgets/smooth_tabbar.dart'; @@ -20,7 +19,7 @@ class ProductPageTab { }); final String Function(BuildContext) labelBuilder; - final Widget Function(Product) builder; + final Widget Function(BuildContext, Product) builder; } class ProductPageTabBar extends StatelessWidget { @@ -45,8 +44,8 @@ class ProductPageTabBar extends StatelessWidget { label: tab.labelBuilder(context), value: tab, ); - }).toList(), - onTabChanged: (ProductPageTab tab) {}, + }).toList(growable: false), + onTabChanged: (_) {}, )), ), pinned: true, @@ -76,8 +75,8 @@ class ProductPageTabBar extends StatelessWidget { tabs.add( ProductPageTab( - labelBuilder: (BuildContext c) => knowledgePanelTitle.title, - builder: (Product p) => ListView.builder( + labelBuilder: (_) => knowledgePanelTitle.title, + builder: (_, __) => ListView.builder( padding: EdgeInsetsDirectional.zero, itemCount: children.length - 1, itemBuilder: (BuildContext context, int index) => children[index], @@ -97,9 +96,9 @@ class ProductPageTabBar extends StatelessWidget { tabs.insert( 0, ProductPageTab( - labelBuilder: (BuildContext c) => - AppLocalizations.of(c).product_page_tab_for_me, - builder: (Product p) => Row( + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_for_me, + builder: (BuildContext context, __) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SmoothSimpleButton( @@ -108,26 +107,29 @@ class ProductPageTabBar extends StatelessWidget { context, items: tabs.map((ProductPageTab tab) { return tab; - }).toList(), + }).toList(growable: false), onReorder: (List reorderedItems) { - tabs.clear(); - tabs.addAll(reorderedItems); + tabs + ..clear() + ..addAll(reorderedItems); }, labelBuilder: ( BuildContext context, ProductPageTab item, int index, ) { - return DefaultTextStyle.merge( + return Text( + item.labelBuilder(context), style: const TextStyle( fontWeight: FontWeight.bold, ), - child: Text(item.labelBuilder(context)), ); }, + title: AppLocalizations.of(context).product_page_reorder_tabs, ); }, - child: const Text('Reorder tabs'), + child: + Text(AppLocalizations.of(context).product_page_reorder_tabs), ), ], ), @@ -136,12 +138,12 @@ class ProductPageTabBar extends StatelessWidget { if (product.website?.trim().isNotEmpty == true) { tabs.add( ProductPageTab( - labelBuilder: (BuildContext c) => - AppLocalizations.of(c).product_page_tab_website, - builder: (Product p) => ListView( + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_website, + builder: (_, Product product) => ListView( padding: EdgeInsetsDirectional.zero, children: [ - WebsiteCard(p.website!), + WebsiteCard(product.website!), ], ), ), @@ -149,12 +151,12 @@ class ProductPageTabBar extends StatelessWidget { } tabs.add( ProductPageTab( - labelBuilder: (BuildContext c) => - AppLocalizations.of(c).product_page_tab_prices, - builder: (Product p) => ListView( + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_prices, + builder: (_, Product product) => ListView( padding: EdgeInsetsDirectional.zero, children: [ - PricesCard(p), + PricesCard(product), ], ), ), @@ -165,11 +167,11 @@ class ProductPageTabBar extends StatelessWidget { false) { tabs.add( ProductPageTab( - labelBuilder: (BuildContext c) => - AppLocalizations.of(c).product_page_tab_folksonomy, - builder: (Product p) => ListView( + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_folksonomy, + builder: (_, Product product) => ListView( padding: EdgeInsetsDirectional.zero, - children: [FolksonomyCard(p)], + children: [FolksonomyCard(product)], ), ), ); @@ -177,27 +179,6 @@ class ProductPageTabBar extends StatelessWidget { return tabs; } - - static void showSmoothReorderBottomSheet( - BuildContext context, { - required List items, - required ValueChanged> onReorder, - ValueChanged? onVisibilityToggle, - required LabelBuilder labelBuilder, - String title = 'Reorder Items', - }) { - showSmoothModalSheet( - context: context, - minHeight: 0.6, - builder: (_) => ReorderBottomSheet( - items: items, - onReorder: onReorder, - onVisibilityToggle: onVisibilityToggle, - labelBuilder: labelBuilder, - title: title, - ), - ); - } } class _TabBarDelegate extends SliverPersistentHeaderDelegate { @@ -217,7 +198,7 @@ class _TabBarDelegate extends SliverPersistentHeaderDelegate { double shrinkOffset, bool overlapsContent, ) { - return Container( + return ColoredBox( color: Theme.of(context).scaffoldBackgroundColor, child: tabBar, ); diff --git a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart index 8010932324be..acb65234331b 100644 --- a/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart +++ b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart @@ -19,7 +19,9 @@ class ReorderBottomSheet extends StatelessWidget { required this.labelBuilder, this.onVisibilityToggle, required this.title, - }) : _items = items.map((T data) => _ReorderableItem(data: data)).toList(); + }) : _items = items + .map((T data) => _ReorderableItem(data: data)) + .toList(growable: false); final List<_ReorderableItem> _items; final ValueChanged> onReorder; @@ -45,7 +47,7 @@ class ReorderBottomSheet extends StatelessWidget { builder: (_, ScrollController scrollController) { return ClipRRect( borderRadius: const BorderRadius.vertical(top: ROUNDED_RADIUS), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).canvasColor, borderRadius: const BorderRadius.vertical( @@ -55,8 +57,8 @@ class ReorderBottomSheet extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SmoothModalSheetHeader( - title: 'Reorder tabs', + SmoothModalSheetHeader( + title: title, ), Expanded( child: ReorderableListView.builder( @@ -80,8 +82,10 @@ class ReorderBottomSheet extends StatelessWidget { return Container( key: ValueKey(item.data), margin: const EdgeInsetsDirectional.only( - bottom: MEDIUM_SPACE), - padding: const EdgeInsetsDirectional.all(12.0), + bottom: MEDIUM_SPACE, + ), + padding: + const EdgeInsetsDirectional.all(MEDIUM_SPACE), decoration: BoxDecoration( color: item.visible ? theme.primaryMedium @@ -123,7 +127,7 @@ class ReorderBottomSheet extends StatelessWidget { provider.reorder(oldIndex, newIndex); onReorder(provider.items .map((_ReorderableItem item) => item.data) - .toList()); + .toList(growable: false)); }, ), ), @@ -143,7 +147,7 @@ class _ReorderableItem { _ReorderableItem({required this.data, this.visible = true}); final T data; - bool visible; + final bool visible; _ReorderableItem copyWith({bool? visible}) { return _ReorderableItem( diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart index 60852d92a58e..53621b9c7282 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/data_models/product_list.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/up_to_date_changes.dart'; @@ -10,13 +12,23 @@ import 'package:smooth_app/data_models/up_to_date_mixin.dart'; import 'package:smooth_app/database/dao_product_last_access.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/product_compatibility_helper.dart'; +import 'package:smooth_app/pages/folksonomy/folksonomy_card.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; +import 'package:smooth_app/pages/prices/prices_card.dart'; +import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/product_page/footer/new_product_footer.dart'; import 'package:smooth_app/pages/product/product_page/header/product_page_tabs.dart'; import 'package:smooth_app/pages/product/product_page/new_product_header.dart'; import 'package:smooth_app/pages/product/product_page/new_product_page_loading_indicator.dart'; import 'package:smooth_app/pages/product/product_questions_widget.dart'; +import 'package:smooth_app/pages/product/reorderable_knowledge_panel_page.dart'; +import 'package:smooth_app/pages/product/reordered_knowledge_panel_cards.dart'; +import 'package:smooth_app/pages/product/standard_knowledge_panel_cards.dart'; import 'package:smooth_app/pages/product/summary_card.dart'; +import 'package:smooth_app/pages/product/website_card.dart'; import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/widget_height.dart'; @@ -97,6 +109,11 @@ class ProductPageState extends State final bool hasPendingOperations = UpToDateChanges(localDatabase) .hasNotTerminatedOperations(upToDateProduct.barcode!); + final UserPreferences userPreferences = context.watch(); + final bool useTabView = userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUseProductTabs) ?? + false; + return MultiProvider( providers: [ Provider.value(value: upToDateProduct), @@ -125,7 +142,7 @@ class ProductPageState extends State SliverAppBar( floating: false, pinned: true, - leading: const SizedBox(), + leading: EMPTY_WIDGET, leadingWidth: 0.0, titleSpacing: 0.0, title: ProductHeader( @@ -133,20 +150,32 @@ class ProductPageState extends State ), ), SliverToBoxAdapter( - child: SummaryCard(upToDateProduct, _productPreferences), - ), - ProductPageTabBar( - tabController: _tabController, - tabs: _tabs, + child: HeroMode( + enabled: widget.withHeroAnimation && + widget.heroTag?.isNotEmpty == true, + child: SummaryCard(upToDateProduct, _productPreferences), + ), ), + if (useTabView) + ProductPageTabBar( + tabController: _tabController, + tabs: _tabs, + ), ]; }, - body: TabBarView( - controller: _tabController, - children: _tabs - .map((ProductPageTab tab) => tab.builder(upToDateProduct)) - .toList(), - ), + body: useTabView + ? TabBarView( + controller: _tabController, + children: _tabs + .map( + (ProductPageTab tab) => tab.builder( + context, + upToDateProduct, + ), + ) + .toList(growable: false), + ) + : _buildOldLayout(userPreferences), ), bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, @@ -169,25 +198,76 @@ class ProductPageState extends State ), ), ); + } - // TODO(primael): Decide where to move these cards with new tab view - /* if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? - false) - Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: SmoothLargeButtonWithIcon( - text: appLocalizations.reorder_attribute_action, - leadingIcon: const Icon(Icons.sort), - onPressed: () async => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - ReorderableKnowledgePanelPage(upToDateProduct), + Widget _buildOldLayout( + UserPreferences userPreferences, + ) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + return RefreshIndicator( + onRefresh: () async => ProductRefresher().fetchAndRefresh( + barcode: barcode, + context: context, + ), + child: ListView( + padding: const EdgeInsets.only( + bottom: LARGE_SPACE, + ), + children: [ + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + ReorderedKnowledgePanelCards(upToDateProduct) + else + StandardKnowledgePanelCards(upToDateProduct), + // TODO(monsieurtanuki): include website in reordered knowledge panels + if (upToDateProduct.website != null && + upToDateProduct.website!.trim().isNotEmpty) + WebsiteCard(upToDateProduct.website!), + PricesCard(upToDateProduct), + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagHideFolksonomy) == + false) + FolksonomyCard(upToDateProduct), + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: SmoothLargeButtonWithIcon( + text: appLocalizations.reorder_attribute_action, + leadingIcon: const Icon(Icons.sort), + onPressed: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + ReorderableKnowledgePanelPage(upToDateProduct), + ), + ), ), ), - ), - ), */ + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: SmoothLargeButtonWithIcon( + text: appLocalizations.reorder_attribute_action, + leadingIcon: const Icon(Icons.sort), + onPressed: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + ReorderableKnowledgePanelPage(upToDateProduct), + ), + ), + ), + ), + if (bottomPadding > 0) SizedBox(height: bottomPadding), + ], + ), + ); } Future _updateLocalDatabaseWithProductHistory( From 2a2a47d7ecf931aebf2e0ea1ec57f3ca0b745df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:42:51 +0100 Subject: [PATCH 8/9] Rollback to old layout when not using tabs --- .../product_page/new_product_page.dart | 275 +++++++++++------- 1 file changed, 169 insertions(+), 106 deletions(-) diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart index 53621b9c7282..f0e2f067ea6f 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart @@ -30,6 +30,9 @@ import 'package:smooth_app/pages/product/standard_knowledge_panel_cards.dart'; import 'package:smooth_app/pages/product/summary_card.dart'; import 'package:smooth_app/pages/product/website_card.dart'; import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; +import 'package:smooth_app/themes/smooth_theme.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/widget_height.dart'; @@ -130,57 +133,109 @@ class ProductPageState extends State value: _scrollController, ), ], - child: SmoothScaffold( - contentBehindStatusBar: true, - spaceBehindStatusBar: false, - changeStatusBarBrightness: false, - statusBarBackgroundColor: Colors.transparent, - body: NestedScrollView( - controller: _scrollController, - headerSliverBuilder: (BuildContext context, bool value) { - return [ - SliverAppBar( - floating: false, - pinned: true, - leading: EMPTY_WIDGET, - leadingWidth: 0.0, - titleSpacing: 0.0, - title: ProductHeader( - backButtonType: widget.backButton, - ), + child: useTabView + ? _buildTabLayout(hasPendingOperations) + : _buildOldLayout(userPreferences, hasPendingOperations), + ); + } + + Widget _buildTabLayout(bool hasPendingOperations) { + return SmoothScaffold( + contentBehindStatusBar: true, + spaceBehindStatusBar: false, + changeStatusBarBrightness: false, + statusBarBackgroundColor: Colors.transparent, + body: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: (BuildContext context, bool value) { + return [ + SliverAppBar( + floating: false, + pinned: true, + leading: EMPTY_WIDGET, + leadingWidth: 0.0, + titleSpacing: 0.0, + title: ProductHeader( + backButtonType: widget.backButton, ), - SliverToBoxAdapter( - child: HeroMode( - enabled: widget.withHeroAnimation && - widget.heroTag?.isNotEmpty == true, - child: SummaryCard(upToDateProduct, _productPreferences), - ), + ), + SliverToBoxAdapter( + child: HeroMode( + enabled: widget.withHeroAnimation && + widget.heroTag?.isNotEmpty == true, + child: SummaryCard(upToDateProduct, _productPreferences), ), - if (useTabView) - ProductPageTabBar( - tabController: _tabController, - tabs: _tabs, + ), + ProductPageTabBar( + tabController: _tabController, + tabs: _tabs, + ), + ]; + }, + body: TabBarView( + controller: _tabController, + children: _tabs + .map( + (ProductPageTab tab) => tab.builder( + context, + upToDateProduct, ), - ]; - }, - body: useTabView - ? TabBarView( - controller: _tabController, - children: _tabs - .map( - (ProductPageTab tab) => tab.builder( - context, - upToDateProduct, - ), - ) - .toList(growable: false), - ) - : _buildOldLayout(userPreferences), + ) + .toList(growable: false), ), - bottomNavigationBar: Column( - mainAxisSize: MainAxisSize.min, - children: [ - MeasureSize( + ), + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MeasureSize( + onChange: (Size size) { + if (size.height != bottomPadding) { + setState(() => bottomPadding = size.height); + } + }, + child: hasPendingOperations + ? const ProductPageLoadingIndicator() + : KeepQuestionWidgetAlive( + keepWidgetAlive: _keepRobotoffQuestionsAlive, + child: ProductQuestionsWidget(upToDateProduct), + ), + ), + const ProductFooter(), + ], + ), + ); + } + + Widget _buildOldLayout( + UserPreferences userPreferences, + bool hasPendingOperations, + ) { + final SmoothColorsThemeExtension themeExtension = + context.extension(); + + return SmoothScaffold( + contentBehindStatusBar: true, + spaceBehindStatusBar: false, + changeStatusBarBrightness: false, + statusBarBackgroundColor: Colors.transparent, + backgroundColor: + !context.darkTheme() ? themeExtension.primaryLight : null, + body: Stack( + children: [ + _buildProductBody(context, bottomPadding), + Positioned( + left: 0.0, + right: 0.0, + top: 0.0, + child: ProductHeader( + backButtonType: widget.backButton, + ), + ), + Positioned( + left: 0.0, + right: 0.0, + bottom: 0.0, + child: MeasureSize( onChange: (Size size) { if (size.height != bottomPadding) { setState(() => bottomPadding = size.height); @@ -193,79 +248,87 @@ class ProductPageState extends State child: ProductQuestionsWidget(upToDateProduct), ), ), - const ProductFooter(), - ], - ), + ), + ], ), + bottomNavigationBar: const ProductFooter(), ); } - Widget _buildOldLayout( - UserPreferences userPreferences, - ) { + Widget _buildProductBody(BuildContext context, double bottomPadding) { final AppLocalizations appLocalizations = AppLocalizations.of(context); + final UserPreferences userPreferences = context.watch(); - return RefreshIndicator( - onRefresh: () async => ProductRefresher().fetchAndRefresh( - barcode: barcode, - context: context, - ), - child: ListView( - padding: const EdgeInsets.only( - bottom: LARGE_SPACE, + return SafeArea( + child: RefreshIndicator( + onRefresh: () async => ProductRefresher().fetchAndRefresh( + barcode: barcode, + context: context, ), - children: [ - if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? - false) - ReorderedKnowledgePanelCards(upToDateProduct) - else - StandardKnowledgePanelCards(upToDateProduct), - // TODO(monsieurtanuki): include website in reordered knowledge panels - if (upToDateProduct.website != null && - upToDateProduct.website!.trim().isNotEmpty) - WebsiteCard(upToDateProduct.website!), - PricesCard(upToDateProduct), - if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagHideFolksonomy) == - false) - FolksonomyCard(upToDateProduct), - if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? - false) + child: ListView( + // /!\ Smart Dart + // `physics: const AlwaysScrollableScrollPhysics()` + // means that we will always scroll, even if it's pointless. + // Why do we need to? For the RefreshIndicator, that wouldn't be + // triggered on a ListView smaller than the screen + // (as there will be no scroll). + physics: const AlwaysScrollableScrollPhysics(), + controller: _scrollController, + padding: const EdgeInsets.only( + top: kToolbarHeight + LARGE_SPACE, + bottom: LARGE_SPACE, + ), + children: [ Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: SmoothLargeButtonWithIcon( - text: appLocalizations.reorder_attribute_action, - leadingIcon: const Icon(Icons.sort), - onPressed: () async => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - ReorderableKnowledgePanelPage(upToDateProduct), - ), + padding: const EdgeInsets.symmetric( + horizontal: SMALL_SPACE, + ), + child: HeroMode( + enabled: widget.withHeroAnimation && + widget.heroTag?.isNotEmpty == true, + child: SummaryCard( + upToDateProduct, + _productPreferences, + heroTag: widget.heroTag, + isFullVersion: true, ), ), ), - if (userPreferences.getFlag( - UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? - false) - Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: SmoothLargeButtonWithIcon( - text: appLocalizations.reorder_attribute_action, - leadingIcon: const Icon(Icons.sort), - onPressed: () async => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - ReorderableKnowledgePanelPage(upToDateProduct), + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + ReorderedKnowledgePanelCards(upToDateProduct) + else + StandardKnowledgePanelCards(upToDateProduct), + // TODO(monsieurtanuki): include website in reordered knowledge panels + if (upToDateProduct.website != null && + upToDateProduct.website!.trim().isNotEmpty) + WebsiteCard(upToDateProduct.website!), + PricesCard(upToDateProduct), + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagHideFolksonomy) == + false) + FolksonomyCard(upToDateProduct), + if (userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagUserOrderedKP) ?? + false) + Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: SmoothLargeButtonWithIcon( + text: appLocalizations.reorder_attribute_action, + leadingIcon: const Icon(Icons.sort), + onPressed: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + ReorderableKnowledgePanelPage(upToDateProduct), + ), ), ), ), - ), - if (bottomPadding > 0) SizedBox(height: bottomPadding), - ], + if (bottomPadding > 0) SizedBox(height: bottomPadding), + ], + ), ), ); } From 8aab910da13aed5fb0246dd5731966f981ca1cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prima=C3=ABl=20Qu=C3=A9merais?= <34269530+PrimaelQuemerais@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:05:36 +0100 Subject: [PATCH 9/9] Fixed formatting issue --- .../generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart index e69de29bb2d1..8b137891791f 100644 --- a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart +++ b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart @@ -0,0 +1 @@ +