diff --git a/packages/smooth_app/ios/Podfile.lock b/packages/smooth_app/ios/Podfile.lock index f8e8cc46560..626145d3bb1 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/data_models/preferences/user_preferences.dart b/packages/smooth_app/lib/data_models/preferences/user_preferences.dart index cd22ea695e5..0b2cb7b065f 100644 --- a/packages/smooth_app/lib/data_models/preferences/user_preferences.dart +++ b/packages/smooth_app/lib/data_models/preferences/user_preferences.dart @@ -92,6 +92,7 @@ class UserPreferences extends ChangeNotifier { static const String _TAG_SEARCH_SHOW_PRODUCT_TYPE_FILTER = '_search_show_product_type_filter'; static const String _TAG_PRODUCT_PAGE_ACTIONS = '_product_page_actions'; + static const String _TAG_PRODUCT_PAGE_TABS = '_product_page_tabs'; /// Camera preferences @@ -544,4 +545,17 @@ class UserPreferences extends ChangeNotifier { ); notifyListeners(); } + + List get productPageTabs => + _sharedPreferences.getStringList(_TAG_PRODUCT_PAGE_TABS) ?? []; + + Future setProductPageTabs( + final List value, + ) async { + await _sharedPreferences.setStringList( + _TAG_PRODUCT_PAGE_TABS, + value, + ); + notifyListeners(); + } } 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 ca10bd62440..bebf44c16b5 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 00000000000..8b137891791 --- /dev/null +++ b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_reorder_bottom_sheet.dart @@ -0,0 +1 @@ + diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index b331374cafa..56d17ea2ed2 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -3813,5 +3813,29 @@ "type": "String" } } + }, + "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" + }, + "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 da956d525e0..5d78db82999 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 new file mode 100644 index 00000000000..9c44828fb1a --- /dev/null +++ b/packages/smooth_app/lib/pages/product/product_page/header/product_page_tabs.dart @@ -0,0 +1,245 @@ +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/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'; +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/website_card.dart'; +import 'package:smooth_app/widgets/smooth_tabbar.dart'; + +class ProductPageTab { + const ProductPageTab({ + required this.id, + required this.labelBuilder, + required this.builder, + }); + + final String id; + final String Function(BuildContext) labelBuilder; + final Widget Function(BuildContext, Product) builder; +} + +class ProductPageTabBar extends StatelessWidget { + const ProductPageTabBar({ + required this.tabController, + required this.tabs, + }); + + final TabController tabController; + final List tabs; + + @override + Widget build(BuildContext context) { + return SliverPersistentHeader( + 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(growable: false), + onTabChanged: (_) {}, + )), + ), + 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 String? id = root.panelElement?.panelId; + if (id == null) { + continue; + } + + List children = KnowledgePanelsBuilder.getChildren( + context, + panelElement: root, + product: product, + onboardingMode: false, + ); + + final KnowledgePanelTitle knowledgePanelTitle = + children.first as KnowledgePanelTitle; + + children = children.sublist(1); + + tabs.add( + ProductPageTab( + id: id, + labelBuilder: (_) => knowledgePanelTitle.title, + builder: (_, __) => ListView.builder( + padding: EdgeInsetsDirectional.zero, + itemCount: children.length - 1, + itemBuilder: (BuildContext context, int index) => children[index], + ), + ), + ); + } + + _addHardCodedTabs(context, product, tabs); + + final List order = context.read().productPageTabs; + + if (order.isNotEmpty) { + tabs.sort((ProductPageTab a, ProductPageTab b) { + final int indexA = order.indexOf(a.id); + final int indexB = order.indexOf(b.id); + if (indexA < 0) { + return 1; + } + if (indexB < 0) { + return -1; + } + return indexA - indexB; + }); + } + + return tabs; + } + + static List _addHardCodedTabs( + BuildContext context, + Product product, + List tabs, + ) { + tabs.insert( + 0, + ProductPageTab( + id: 'for_me', + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_for_me, + builder: (BuildContext context, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SmoothSimpleButton( + onPressed: () { + showSmoothReorderBottomSheet( + context, + items: tabs.map((ProductPageTab tab) { + return tab; + }).toList(growable: false), + onReorder: (List reorderedItems) { + context.read().setProductPageTabs( + reorderedItems.map((ProductPageTab tab) { + return tab.id; + }).toList(growable: false)); + tabs + ..clear() + ..addAll(reorderedItems); + }, + labelBuilder: ( + BuildContext context, + ProductPageTab item, + int index, + ) { + return Text( + item.labelBuilder(context), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ); + }, + title: AppLocalizations.of(context).product_page_reorder_tabs, + ); + }, + child: + Text(AppLocalizations.of(context).product_page_reorder_tabs), + ), + ], + ), + ), + ); + if (product.website?.trim().isNotEmpty == true) { + tabs.add( + ProductPageTab( + id: 'website', + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_website, + builder: (_, Product product) => ListView( + padding: EdgeInsetsDirectional.zero, + children: [ + WebsiteCard(product.website!), + ], + ), + ), + ); + } + tabs.add( + ProductPageTab( + id: 'prices', + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_prices, + builder: (_, Product product) => ListView( + padding: EdgeInsetsDirectional.zero, + children: [ + PricesCard(product), + ], + ), + ), + ); + + if (context.read().getFlag( + UserPreferencesDevMode.userPreferencesFlagHideFolksonomy) == + false) { + tabs.add( + ProductPageTab( + id: 'folksonomy', + labelBuilder: (BuildContext context) => + AppLocalizations.of(context).product_page_tab_folksonomy, + builder: (_, Product product) => ListView( + padding: EdgeInsetsDirectional.zero, + children: [FolksonomyCard(product)], + ), + ), + ); + } + + return tabs; + } +} + +class _TabBarDelegate extends SliverPersistentHeaderDelegate { + _TabBarDelegate(this.tabBar); + + final PreferredSizeWidget tabBar; + + @override + double get minExtent => tabBar.preferredSize.height; + + @override + double get maxExtent => minExtent; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return ColoredBox( + 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 00000000000..b9d0fd9d35b --- /dev/null +++ b/packages/smooth_app/lib/pages/product/product_page/header/reorder_bottom_sheet.dart @@ -0,0 +1,181 @@ +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.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; + +typedef LabelBuilder = Widget Function( + BuildContext context, + T item, + int index, +); + +class ReorderBottomSheet extends StatelessWidget { + ReorderBottomSheet({ + required List items, + required this.onReorder, + required this.labelBuilder, + this.onVisibilityToggle, + required this.title, + }) : _items = items + .map((T data) => _ReorderableItem(data: data)) + .toList(growable: true); + + final List<_ReorderableItem> _items; + final ValueChanged> onReorder; + final LabelBuilder labelBuilder; + final ValueChanged? onVisibilityToggle; + final String title; + + @override + Widget build(BuildContext context) { + final SmoothColorsThemeExtension theme = + context.extension(); + + return ChangeNotifierProvider<_ReorderBottomSheetProvider>( + create: (_) => _ReorderBottomSheetProvider(_items), + child: Consumer<_ReorderBottomSheetProvider>( + 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: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).canvasColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SmoothModalSheetHeader( + title: title, + ), + 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 EdgeInsetsDirectional.only( + bottom: MEDIUM_SPACE, + ), + padding: + const EdgeInsetsDirectional.all(MEDIUM_SPACE), + decoration: BoxDecoration( + color: item.visible + ? theme.primaryMedium + : theme.primaryLight, + borderRadius: ROUNDED_BORDER_RADIUS, + ), + child: Row( + children: [ + if (onVisibilityToggle != null) + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: theme.primarySemiDark, + ), + child: IconButton( + visualDensity: VisualDensity.compact, + iconSize: 16.0, + icon: const icons.Eye.visible( + color: Colors.white, + ), + onPressed: () => + onVisibilityToggle?.call(item.data), + ), + ), + if (onVisibilityToggle != null) + const SizedBox(width: MEDIUM_SPACE), + labelBuilder(context, item.data, index), + const Spacer(), + Icon( + Icons.drag_handle, + color: theme.primaryDark, + ), + ], + ), + ); + }, + itemCount: provider.items.length, + onReorder: (int oldIndex, int newIndex) { + provider.reorder(oldIndex, newIndex); + onReorder(provider.items + .map((_ReorderableItem item) => item.data) + .toList(growable: false)); + }, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } +} + +class _ReorderableItem { + _ReorderableItem({required this.data, this.visible = true}); + + final T data; + final bool visible; + + _ReorderableItem copyWith({bool? visible}) { + return _ReorderableItem( + data: data, + visible: visible ?? this.visible, + ); + } +} + +class _ReorderBottomSheetProvider extends ChangeNotifier { + _ReorderBottomSheetProvider(this.items); + + List<_ReorderableItem> 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(); + } + } +} 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 f1c99d0088b..f0e2f067ea6 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 @@ -20,6 +20,7 @@ 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'; @@ -56,8 +57,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 +77,18 @@ class ProductPageState extends State final LocalDatabase localDatabase = context.read(); initUpToDate(widget.product, localDatabase); DaoProductLastAccess(localDatabase).put(barcode); + + _tabs = ProductPageTabBar.extractTabsFromProduct( + context: context, + product: upToDateProduct, + ); + + _tabController = TabController( + length: _tabs.length, + vsync: this, + initialIndex: 1, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { _updateLocalDatabaseWithProductHistory(context); }); @@ -82,8 +99,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(); @@ -97,6 +112,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), @@ -113,58 +133,126 @@ class ProductPageState extends State value: _scrollController, ), ], - child: 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( + 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, ), ), - 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: HeroMode( + enabled: widget.withHeroAnimation && + widget.heroTag?.isNotEmpty == true, + child: SummaryCard(upToDateProduct, _productPreferences), ), ), - ], + ProductPageTabBar( + tabController: _tabController, + tabs: _tabs, + ), + ]; + }, + body: TabBarView( + controller: _tabController, + children: _tabs + .map( + (ProductPageTab tab) => tab.builder( + context, + upToDateProduct, + ), + ) + .toList(growable: false), ), - bottomNavigationBar: const ProductFooter(), + ), + 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(), + ], ), ); } - Future _updateLocalDatabaseWithProductHistory( - final BuildContext context, - ) async { - final LocalDatabase localDatabase = context.read(); - await DaoProductList(localDatabase).push( - ProductList.history(), - barcode, + 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); + } + }, + child: hasPendingOperations + ? const ProductPageLoadingIndicator() + : KeepQuestionWidgetAlive( + keepWidgetAlive: _keepRobotoffQuestionsAlive, + child: ProductQuestionsWidget(upToDateProduct), + ), + ), + ), + ], + ), + bottomNavigationBar: const ProductFooter(), ); - localDatabase.notifyListeners(); } Widget _buildProductBody(BuildContext context, double bottomPadding) { @@ -245,6 +333,17 @@ class ProductPageState extends State ); } + Future _updateLocalDatabaseWithProductHistory( + final BuildContext context, + ) async { + final LocalDatabase localDatabase = context.read(); + await DaoProductList(localDatabase).push( + ProductList.history(), + barcode, + ); + localDatabase.notifyListeners(); + } + 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 47bae3cb568..43d112aace9 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( @@ -144,67 +137,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 +251,7 @@ class _SummaryCardState extends State with UpToDateMixin { } final Widget child = Column( + mainAxisSize: MainAxisSize.min, children: [ ProductTitleCard( upToDateProduct,