diff --git a/packages/smooth_app/lib/data_models/continuous_scan_model.dart b/packages/smooth_app/lib/data_models/continuous_scan_model.dart index 58f13349b52..158a808e1db 100644 --- a/packages/smooth_app/lib/data_models/continuous_scan_model.dart +++ b/packages/smooth_app/lib/data_models/continuous_scan_model.dart @@ -37,6 +37,8 @@ class ContinuousScanModel with ChangeNotifier { final String languageCode; final String countryCode; + QRViewController? _qrViewController; + bool get hasMoreThanOneProduct => getBarcodes().length > 1; ProductList get productList => _productList; @@ -92,8 +94,13 @@ class ContinuousScanModel with ChangeNotifier { Product getProduct(final String barcode) => _productList.getProduct(barcode); - void setupScanner(QRViewController controller) => - controller.scannedDataStream.listen((Barcode barcode) => onScan(barcode)); + void setupScanner(QRViewController controller) { + _qrViewController = controller; + controller.scannedDataStream.listen((Barcode barcode) => onScan(barcode)); + } + + //Used when navigating away from the QRView itself + void stopQRView() => _qrViewController?.stopCamera(); Future onScan(final Barcode barcode) async { if (barcode.code == null) { diff --git a/packages/smooth_app/lib/main.dart b/packages/smooth_app/lib/main.dart index e2f8917c528..e47ef077f1d 100644 --- a/packages/smooth_app/lib/main.dart +++ b/packages/smooth_app/lib/main.dart @@ -11,7 +11,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; +import 'package:smooth_app/pages/page_manager.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/themes/theme_provider.dart'; @@ -173,7 +173,7 @@ class SmoothAppGetLanguage extends StatelessWidget { DefaultAssetBundle.of(context), languageCode, ); - return SmoothBottomNavigationBar.getDefaultPage(); + return PageManager(); } Future _refresh( diff --git a/packages/smooth_app/lib/pages/page_manager.dart b/packages/smooth_app/lib/pages/page_manager.dart new file mode 100644 index 00000000000..a81d8db88ef --- /dev/null +++ b/packages/smooth_app/lib/pages/page_manager.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_app/widgets/tab_navigator.dart'; + +enum BottomNavigationTab { + Profile, + Scan, + History, +} + +/// Here the different tabs in the bottom navigation bar are taken care of, +/// so that they are stateful, that is not only things like the scroll position +/// but also keeping the navigation on the different tabs. +/// +/// Scan Page is an exception here as it needs a little more work so that the +/// camera is not kept unnecessarily kept active. +class PageManager extends StatefulWidget { + @override + State createState() => PageManagerState(); +} + +class PageManagerState extends State { + static const List _pageKeys = [ + BottomNavigationTab.Profile, + BottomNavigationTab.Scan, + BottomNavigationTab.History, + ]; + + final Map> _navigatorKeys = + >{ + BottomNavigationTab.Profile: GlobalKey(), + BottomNavigationTab.Scan: GlobalKey(), + BottomNavigationTab.History: GlobalKey(), + }; + + BottomNavigationTab _currentPage = BottomNavigationTab.Scan; + + void _selectTab(BottomNavigationTab tabItem, int index) { + if (tabItem == _currentPage) { + _navigatorKeys[tabItem]! + .currentState! + .popUntil((Route route) => route.isFirst); + } else { + setState(() { + _currentPage = _pageKeys[index]; + }); + } + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + final bool isFirstRouteInCurrentTab = + !await _navigatorKeys[_currentPage]!.currentState!.maybePop(); + if (isFirstRouteInCurrentTab) { + if (_currentPage != BottomNavigationTab.Scan) { + _selectTab(BottomNavigationTab.Scan, 1); + return false; + } + } + // let system handle back button if we're on the first route + return isFirstRouteInCurrentTab; + }, + child: Scaffold( + body: Stack(children: [ + _buildOffstageNavigator(BottomNavigationTab.Profile), + _buildOffstageNavigator(BottomNavigationTab.Scan), + _buildOffstageNavigator(BottomNavigationTab.History), + ]), + bottomNavigationBar: BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + selectedItemColor: Colors.white, + backgroundColor: Theme.of(context).appBarTheme.backgroundColor, + onTap: (int index) { + _selectTab(_pageKeys[index], index); + }, + currentIndex: _currentPage.index, + items: const [ + // TODO(M123): Translate + BottomNavigationBarItem( + icon: Icon(Icons.account_circle), + label: 'Profile', + ), + BottomNavigationBarItem( + icon: Icon(Icons.search), + label: 'Scan or Search', + ), + BottomNavigationBarItem( + icon: Icon(Icons.history), + label: 'History', + ), + ], + ), + ), + ); + } + + Widget _buildOffstageNavigator(BottomNavigationTab tabItem) { + final bool offstage = _currentPage != tabItem; + return Offstage( + offstage: offstage, + child: TabNavigator( + offstage: offstage, + navigatorKey: _navigatorKeys[tabItem]!, + tabItem: tabItem, + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart b/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart index b6c7f293d07..dca09938585 100644 --- a/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart +++ b/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart @@ -55,7 +55,8 @@ class ProductDialogHelper { return; } _popEd = true; - Navigator.pop(context, fetchedProduct); + // Here we use the root navigator so that we can pop dialog while using multiple navigators. + Navigator.of(context, rootNavigator: true).pop(fetchedProduct); } Widget _getSearchingDialog() => SmoothAlertDialog( diff --git a/packages/smooth_app/lib/pages/product/common/product_list_page.dart b/packages/smooth_app/lib/pages/product/common/product_list_page.dart index 7dde3dcd974..05e2fb66d49 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_page.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_page.dart @@ -9,7 +9,6 @@ import 'package:smooth_app/pages/personalized_ranking_page.dart'; import 'package:smooth_app/pages/product/common/product_list_dialog_helper.dart'; import 'package:smooth_app/pages/product/common/product_list_item_simple.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; -import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; class ProductListPage extends StatefulWidget { const ProductListPage(this.productList); @@ -47,9 +46,6 @@ class _ProductListPageState extends State { dismissible = false; } return Scaffold( - bottomNavigationBar: const SmoothBottomNavigationBar( - tab: SmoothBottomNavigationTab.History, - ), appBar: AppBar( title: Row( children: [ diff --git a/packages/smooth_app/lib/pages/product/new_product_page.dart b/packages/smooth_app/lib/pages/product/new_product_page.dart index 412b25579db..7550e3351ae 100644 --- a/packages/smooth_app/lib/pages/product/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/new_product_page.dart @@ -15,7 +15,6 @@ import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/pages/product/common/product_dialog_helper.dart'; import 'package:smooth_app/pages/product/summary_card.dart'; -import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_ui_library/util/ui_helpers.dart'; @@ -54,7 +53,6 @@ class _ProductPageState extends State { final MaterialColor materialColor = SmoothTheme.getMaterialColor(themeProvider); return Scaffold( - bottomNavigationBar: const SmoothBottomNavigationBar(), backgroundColor: SmoothTheme.getColor( colorScheme, materialColor, diff --git a/packages/smooth_app/lib/pages/product/product_page.dart b/packages/smooth_app/lib/pages/product/product_page.dart index d55f75d53f6..4f4b0c161f0 100644 --- a/packages/smooth_app/lib/pages/product/product_page.dart +++ b/packages/smooth_app/lib/pages/product/product_page.dart @@ -21,7 +21,6 @@ import 'package:smooth_app/helpers/product_translation_helper.dart'; import 'package:smooth_app/pages/product/common/product_dialog_helper.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/product/new_product_page.dart'; -import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; import 'package:smooth_app/pages/user_preferences_page.dart'; import 'package:smooth_app/themes/constant_icons.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; @@ -103,7 +102,6 @@ class _ProductPageState extends State { } } return Scaffold( - bottomNavigationBar: const SmoothBottomNavigationBar(), appBar: AppBar( title: Text(_getProductName(appLocalizations)), actions: [ diff --git a/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart b/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart index fd8c446a4f5..94240d722ab 100644 --- a/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart @@ -4,19 +4,26 @@ import 'package:provider/provider.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; import 'package:smooth_app/pages/personalized_ranking_page.dart'; -import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; import 'package:smooth_app/widgets/smooth_product_carousel.dart'; import 'package:smooth_ui_library/smooth_ui_library.dart'; import 'package:smooth_ui_library/util/ui_helpers.dart'; -class ContinuousScanPage extends StatelessWidget { +class ContinuousScanPage extends StatefulWidget { + const ContinuousScanPage(); + + @override + State createState() => _ContinuousScanPageState(); +} + +class _ContinuousScanPageState extends State { final GlobalKey _scannerViewKey = GlobalKey(debugLabel: 'Barcode Scanner'); + ContinuousScanModel? model; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - final ContinuousScanModel model = context.watch(); + model = context.watch(); final Size screenSize = MediaQuery.of(context).size; final Size scannerSize = Size( screenSize.width * 0.6, @@ -24,7 +31,7 @@ class ContinuousScanPage extends StatelessWidget { ); final double carouselHeight = constraints.maxHeight / 1.81; // roughly 55% of the available height - final double buttonRowHeight = areButtonsRendered(model) ? 48 : 0; + final double buttonRowHeight = areButtonsRendered(model!) ? 48 : 0; final double availableScanHeight = constraints.maxHeight - carouselHeight - buttonRowHeight; // Padding for the qr code scanner. This ensures the scanner has equal spacing between buttons and carousel. @@ -34,9 +41,6 @@ class ContinuousScanPage extends StatelessWidget { final double viewFinderBottomOffset = carouselHeight / 2.0; return Scaffold( appBar: AppBar(toolbarHeight: 0.0), - bottomNavigationBar: const SmoothBottomNavigationBar( - tab: SmoothBottomNavigationTab.Scan, - ), body: Stack( children: [ Container( @@ -64,7 +68,7 @@ class ContinuousScanPage extends StatelessWidget { cutOutBottomOffset: viewFinderBottomOffset, ), key: _scannerViewKey, - onQRViewCreated: model.setupScanner, + onQRViewCreated: model!.setupScanner, ), ), SmoothRevealAnimation( @@ -91,7 +95,7 @@ class ContinuousScanPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - _buildButtonsRow(context, model), + _buildButtonsRow(context, model!), const Spacer(), SmoothProductCarousel( showSearchCard: true, diff --git a/packages/smooth_app/lib/pages/scan/scan_page.dart b/packages/smooth_app/lib/pages/scan/scan_page.dart index ab10ca852c5..d11473dc09c 100644 --- a/packages/smooth_app/lib/pages/scan/scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/scan_page.dart @@ -6,7 +6,10 @@ import 'package:smooth_app/database/product_query.dart'; import 'package:smooth_app/pages/scan/continuous_scan_page.dart'; class ScanPage extends StatefulWidget { - const ScanPage(); + const ScanPage({required this.offstage, required this.navigatorKey}); + + final bool offstage; + final GlobalKey navigatorKey; @override State createState() => _ScanPageState(); @@ -39,9 +42,31 @@ class _ScanPageState extends State { if (_model == null) { return const Center(child: CircularProgressIndicator()); } + return ChangeNotifierProvider( create: (BuildContext context) => _model!, - child: ContinuousScanPage(), + child: Navigator( + key: widget.navigatorKey, + onGenerateRoute: (RouteSettings routeSettings) { + return MaterialPageRoute( + builder: (BuildContext context) => _buildChild(), + ); + }, + ), ); } + + //This has to be build inside of the ChangeNotifierProvider to prevent the model to be disposed. + Widget _buildChild() { + //Don't build Scanner (+activate camera) when not on the Scan Tab + if (widget.offstage) { + _model?.stopQRView(); + return const Center( + child: Text( + "This shouldn't be visible since only build when offstage, when you see this page send a email to contact@openfoodfacts.org", + )); + } else { + return const ContinuousScanPage(); + } + } } diff --git a/packages/smooth_app/lib/pages/smooth_bottom_navigation_bar.dart b/packages/smooth_app/lib/pages/smooth_bottom_navigation_bar.dart deleted file mode 100644 index d28492f0312..00000000000 --- a/packages/smooth_app/lib/pages/smooth_bottom_navigation_bar.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:smooth_app/pages/history_page.dart'; -import 'package:smooth_app/pages/scan/scan_page.dart'; -import 'package:smooth_app/pages/user_preferences_page.dart'; - -class _Page { - const _Page({required this.name, required this.icon, required this.body}); - final String name; - final IconData icon; - final Widget body; -} - -enum SmoothBottomNavigationTab { - Profile, - Scan, - History, -} - -class SmoothBottomNavigationBar extends StatelessWidget { - const SmoothBottomNavigationBar({ - this.tab = _defaultTab, - }); - - final SmoothBottomNavigationTab tab; - - static const SmoothBottomNavigationTab _defaultTab = - SmoothBottomNavigationTab.Scan; - - static const List _tabs = - [ - SmoothBottomNavigationTab.Profile, - SmoothBottomNavigationTab.Scan, - SmoothBottomNavigationTab.History, - ]; - - static const Map _pages = - { - SmoothBottomNavigationTab.Profile: _Page( - name: 'Profile', // TODO(monsieurtanuki): translate - icon: Icons.account_circle, - body: UserPreferencesPage(), - ), - SmoothBottomNavigationTab.Scan: _Page( - name: 'Scan or Search', - icon: Icons.search, - body: ScanPage(), - ), - SmoothBottomNavigationTab.History: _Page( - name: 'History', - icon: Icons.history, - body: HistoryPage(), - ), - }; - - static Widget getDefaultPage() => _getTabPage(_defaultTab); - - static Widget _getTabPage(final SmoothBottomNavigationTab tab) => - _pages[tab]!.body; - - @override - Widget build(BuildContext context) => BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - selectedItemColor: Colors.white, - backgroundColor: Theme.of(context).appBarTheme.backgroundColor, - currentIndex: _tabs.indexOf(tab), - onTap: (final int index) async => Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => _getTabPage(_tabs[index]), - ), - ), - items: [ - _buildItem(_pages[_tabs[0]]!), - _buildItem(_pages[_tabs[1]]!), - _buildItem(_pages[_tabs[2]]!), - ], - ); - - BottomNavigationBarItem _buildItem(final _Page page) => - BottomNavigationBarItem( - icon: Icon(page.icon, size: 28), - label: page.name, - ); -} diff --git a/packages/smooth_app/lib/pages/user_preferences_page.dart b/packages/smooth_app/lib/pages/user_preferences_page.dart index c0ef2c9a64f..744d842a747 100644 --- a/packages/smooth_app/lib/pages/user_preferences_page.dart +++ b/packages/smooth_app/lib/pages/user_preferences_page.dart @@ -6,7 +6,6 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/pages/settings_page.dart'; -import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/widgets/attribute_button.dart'; @@ -36,9 +35,6 @@ class UserPreferencesPage extends StatelessWidget { final List orderedImportantAttributeIds = productPreferences.getOrderedImportantAttributeIds(); return Scaffold( - bottomNavigationBar: const SmoothBottomNavigationBar( - tab: SmoothBottomNavigationTab.Profile, - ), appBar: AppBar( title: Text(appLocalizations.myPreferences), actions: [ diff --git a/packages/smooth_app/lib/widgets/tab_navigator.dart b/packages/smooth_app/lib/widgets/tab_navigator.dart new file mode 100644 index 00000000000..56cbeea3bc3 --- /dev/null +++ b/packages/smooth_app/lib/widgets/tab_navigator.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_app/pages/history_page.dart'; +import 'package:smooth_app/pages/page_manager.dart'; +import 'package:smooth_app/pages/scan/scan_page.dart'; +import 'package:smooth_app/pages/user_preferences_page.dart'; + +class TabNavigator extends StatelessWidget { + const TabNavigator({ + required this.navigatorKey, + required this.tabItem, + required this.offstage, + }); + final GlobalKey navigatorKey; + final BottomNavigationTab tabItem; + final bool offstage; + + @override + Widget build(BuildContext context) { + late final Widget child; + + if (tabItem == BottomNavigationTab.Profile) { + child = const UserPreferencesPage(); + } else if (tabItem == BottomNavigationTab.History) { + child = const HistoryPage(); + } else if (tabItem == BottomNavigationTab.Scan) { + // The ScanPage doesn't use the here build Navigator, so that it can update when its offstage status changes + return ScanPage(offstage: offstage, navigatorKey: navigatorKey); + } + + return Navigator( + key: navigatorKey, + onGenerateRoute: (RouteSettings routeSettings) { + return MaterialPageRoute( + builder: (BuildContext context) => child); + }, + ); + } +} diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-dark.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-dark.png index 6bb1d6d701b..9661c1764db 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-dark.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-dark.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-light.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-light.png index bb5c90f514e..75e5803b649 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-light.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-blue-light.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-dark.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-dark.png index 6bb1d6d701b..9661c1764db 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-dark.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-dark.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-light.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-light.png index 37f298db813..330e0122bcd 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-light.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-brown-light.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-green-dark.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-green-dark.png index 6bb1d6d701b..9661c1764db 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-green-dark.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-green-dark.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-green-light.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-green-light.png index f7a787eeec2..4fa8829ad5b 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-green-light.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-green-light.png differ