From cad89c9072987b3ff5067bda8771f0e60a72cd84 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 18 Jun 2025 21:05:01 +0500 Subject: [PATCH 1/4] added support for showMenu which allows user to show bottom menu outside page groups --- .../framework/view/bottom_nav_controller.dart | 182 ++++++++++ .../framework/view/bottom_nav_page_group.dart | 135 ++++++- modules/ensemble/lib/framework/view/page.dart | 334 +++++++++++------- 3 files changed, 517 insertions(+), 134 deletions(-) create mode 100644 modules/ensemble/lib/framework/view/bottom_nav_controller.dart diff --git a/modules/ensemble/lib/framework/view/bottom_nav_controller.dart b/modules/ensemble/lib/framework/view/bottom_nav_controller.dart new file mode 100644 index 000000000..1f013be05 --- /dev/null +++ b/modules/ensemble/lib/framework/view/bottom_nav_controller.dart @@ -0,0 +1,182 @@ +import 'package:ensemble/framework/menu.dart'; +import 'package:flutter/material.dart'; + +/// Singleton controller that stores and provides access to built bottom navigation widgets +class GlobalBottomNavController extends ChangeNotifier { + static GlobalBottomNavController? _instance; + + // Private constructor + GlobalBottomNavController._(); + + // Singleton instance getter + static GlobalBottomNavController get instance { + _instance ??= GlobalBottomNavController._(); + return _instance!; + } + + // Stored widgets + Widget? _storedBottomNavWidget; + Widget? _storedFloatingActionButton; + FloatingActionButtonLocation? _storedFloatingActionButtonLocation; + Function(int)? _originalOnTabSelected; + late BuildContext _originalContext; + late PageController _pageController; + + String? _activePageGroupId; + BottomNavBarMenu? _currentMenu; + + // Debug tracking + final List _debugLog = []; + + // Getters + Widget? get bottomNavWidget => _storedBottomNavWidget; + Widget? get floatingActionButton => _storedFloatingActionButton; + FloatingActionButtonLocation? get floatingActionButtonLocation => _storedFloatingActionButtonLocation; + bool get hasBottomNav => _storedBottomNavWidget != null; + String? get currentPageGroup => _activePageGroupId; + BottomNavBarMenu? get currentMenu => _currentMenu; + List get debugLog => List.unmodifiable(_debugLog); + + // Original context getter + BuildContext get originalContext => _originalContext; + + // PageController getter + PageController get pageController => _pageController; + + // Original context setter + set originalContext(BuildContext context) { + _originalContext = context; + notifyListeners(); + } + + // PageController setter + set pageController(PageController controller) { + _pageController = controller; + notifyListeners(); + } + + /// Register the built widgets from PageGroup + void registerBottomNavWidgets({ + required Widget? bottomNavWidget, + required Function(int) onTabSelected, + required Widget? floatingActionButton, + required FloatingActionButtonLocation? floatingActionButtonLocation, + required String pageGroupId, + BottomNavBarMenu? menu, + PageController? pageController, // Add optional PageController parameter + }) { + + _storedBottomNavWidget = bottomNavWidget; + _storedFloatingActionButton = floatingActionButton; + _storedFloatingActionButtonLocation = floatingActionButtonLocation; + _activePageGroupId = pageGroupId; + _currentMenu = menu; + _originalOnTabSelected = onTabSelected; // Store the callback correctly + + // Store PageController if provided + if (pageController != null) { + _pageController = pageController; + } + + + notifyListeners(); + } + + /// Handle tab selection from external context + void selectTab(int index) { + if (_originalOnTabSelected != null) { + _originalOnTabSelected!(index); + } + } + + /// Jump to page using stored PageController + void jumpToPage(int index) { + try { + if (_pageController.hasClients) { + _pageController.jumpToPage(index); + } else { + } + } catch (e) { + _addDebugLog('Error jumping to page: $e'); + } + } + + /// Animate to page using stored PageController + void animateToPage(int index, { + Duration duration = const Duration(milliseconds: 300), + Curve curve = Curves.ease, + }) { + try { + if (_pageController.hasClients) { + _pageController.animateToPage( + index, + duration: duration, + curve: curve, + ); + } else { + _addDebugLog('❌ PageController has no clients'); + } + } catch (e) { + _addDebugLog('❌ Error animating to page: $e'); + } + } + + /// Unregister widgets when PageGroup is disposed + void unregisterBottomNavWidgets(String pageGroupId) { + if (_activePageGroupId == pageGroupId) { + _storedBottomNavWidget = null; + _storedFloatingActionButton = null; + _storedFloatingActionButtonLocation = null; + _originalOnTabSelected = null; + _activePageGroupId = null; + _currentMenu = null; + + notifyListeners(); + } + } + + /// Check if a specific PageGroup is currently active + bool isPageGroupActive(String pageGroupId) { + return _activePageGroupId == pageGroupId; + } + + +/// Get bottom nav widget +Widget? getBottomNavWidget() { + return _storedBottomNavWidget != null + ? Container(child: _storedBottomNavWidget) + : null; +} + +/// Get floating action button +Widget? getFloatingActionButton() { + return _storedFloatingActionButton != null + ? Container(child: _storedFloatingActionButton) + : null; +} + + // Debug helpers + void _addDebugLog(String message) { + final timestamp = DateTime.now().toIso8601String().substring(11, 23); + final logEntry = '[$timestamp] $message'; + _debugLog.add(logEntry); + + // Keep only last 100 entries + if (_debugLog.length > 100) { + _debugLog.removeAt(0); + } + + debugPrint('🌍 GlobalBottomNav: $logEntry'); + } + + @override + void dispose() { + _addDebugLog('🗑️ Controller disposed'); + super.dispose(); + } +} + +/// Extension to make it easier to access the controller +extension GlobalBottomNavControllerExtension on BuildContext { + GlobalBottomNavController get globalBottomNav => GlobalBottomNavController.instance; +} \ No newline at end of file diff --git a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart index 70e59c8cf..b1ade9fcd 100644 --- a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart +++ b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart @@ -15,6 +15,8 @@ import 'package:ensemble/util/utils.dart'; import 'package:ensemble/framework/widget/icon.dart' as ensemble; import 'package:ensemble/widget/helpers/controllers.dart'; import 'package:flutter/material.dart'; +// Import the global controller +import 'package:ensemble/framework/view/bottom_nav_controller.dart'; class BottomNavBarItem { BottomNavBarItem({ @@ -27,6 +29,7 @@ class BottomNavBarItem { this.switchScreen = true, this.onTap, this.onTapHaptic, + this.page, }); Widget icon; @@ -38,6 +41,7 @@ class BottomNavBarItem { bool? switchScreen; EnsembleAction? onTap; String? onTapHaptic; + String? page; } enum FloatingAlignment { @@ -113,10 +117,15 @@ class _BottomNavPageGroupState extends State FloatingAlignment floatingAlignment = FloatingAlignment.center; int? floatingMargin; MenuItem? fabMenuItem; + late String _pageGroupId; @override void initState() { super.initState(); + + // Generate unique page group ID + _pageGroupId = 'pagegroup_${widget.hashCode}'; + if (widget.menu.reloadView == false) { controller = PageController(initialPage: widget.selectedPage); } @@ -152,6 +161,9 @@ class _BottomNavPageGroupState extends State @override void dispose() { + // Unregister from global controller + GlobalBottomNavController.instance.unregisterBottomNavWidgets(_pageGroupId); + if (widget.menu.reloadView == false) { controller.dispose(); } @@ -221,18 +233,109 @@ class _BottomNavPageGroupState extends State Utils.getColor(widget.menu.runtimeStyles?['notchColor']) ?? Theme.of(context).scaffoldBackgroundColor; + // Build the widgets + Widget? bottomNavBar = _buildBottomNavBar(); + Widget? floatingButton = _buildFloatingButton(); + FloatingActionButtonLocation? floatingLocation = + floatingAlignment == FloatingAlignment.none + ? null + : floatingAlignment.location; + +// Register the built widgets with global controller + WidgetsBinding.instance.addPostFrameCallback((_) { + GlobalBottomNavController.instance.registerBottomNavWidgets( + bottomNavWidget: bottomNavBar, + floatingActionButton: floatingButton, + floatingActionButtonLocation: floatingLocation, + pageGroupId: _pageGroupId, + onTabSelected: (index) { + List navItems = []; + + final unselectedColor = + Utils.getColor(widget.menu.runtimeStyles?['color']) ?? + Theme.of(context).unselectedWidgetColor; + final selectedColor = + Utils.getColor(widget.menu.runtimeStyles?['selectedColor']) ?? + Theme.of(context).primaryColor; + + // final menu = widget.menu; + for (int i = 0; i < menuItems.length; i++) { + MenuItem item = menuItems[i]; + final dynamic customIcon = _buildCustomIcon(item); + final dynamic customActiveIcon = + _buildCustomIcon(item, isActive: true); + + final isCustom = customIcon != null || customActiveIcon != null; + final label = + isCustom ? '' : Utils.translate(item.label ?? '', context); + + final icon = customIcon ?? + (item.icon != null + ? ensemble.Icon.fromModel(item.icon!, + fallbackLibrary: item.iconLibrary, + fallbackColor: unselectedColor) + : ensemble.Icon('')); + + final activeIcon = customActiveIcon ?? + (item.activeIcon != null || item.icon != null + ? ensemble.Icon.fromModel((item.activeIcon ?? item.icon)!, + fallbackColor: selectedColor, + fallbackLibrary: item.iconLibrary) + : null); + + navItems.add( + BottomNavBarItem( + icon: icon, + activeIcon: activeIcon, + isCustom: isCustom, + text: label, + switchScreen: Menu.evalSwitchScreen( + widget.scopeManager.dataContext, item), + onTap: EnsembleAction.from(item.onTap), + onTapHaptic: item.onTapHaptic, + page: item.page), + ); + } + // Copy this EXACT code from your _buildBottomNavBar() method: + final isSwitchScreen = + Utils.getBool(navItems[index].switchScreen, fallback: true); + print('clicked inside the register!!!'); + if (isSwitchScreen) { + if (widget.menu.reloadView == true) { + viewGroupNotifier.updatePage(index); + } else { + PageGroupWidget.getPageController(context)!.jumpToPage(index); + viewGroupNotifier.updatePage(index); + } + + _onTap(navItems[index]); + } else { + // Execute only onTap action. Page switching is handled by the developer with onTap + _onTap(navItems[index]); + } + + // Executing haptic feedback action + if (navItems[index].onTapHaptic != null) { + ScreenController().executeAction( + context, + HapticAction( + type: navItems[index].onTapHaptic!, onComplete: null), + ); + } + }, + menu: widget.menu as BottomNavBarMenu, + ); + }); + return PageGroupWidgetWrapper( reloadView: widget.menu.reloadView, scopeManager: widget.scopeManager, child: Scaffold( resizeToAvoidBottomInset: true, backgroundColor: notchColor, - bottomNavigationBar: _buildBottomNavBar(), - floatingActionButtonLocation: - floatingAlignment == FloatingAlignment.none - ? null - : floatingAlignment.location, - floatingActionButton: _buildFloatingButton(), + bottomNavigationBar: bottomNavBar, + floatingActionButtonLocation: floatingLocation, + floatingActionButton: floatingButton, body: widget.menu.reloadView == true ? ListenableBuilder( listenable: viewGroupNotifier, @@ -313,7 +416,10 @@ class _BottomNavPageGroupState extends State listenable: viewGroupNotifier, builder: (context, _) { final viewIndex = viewGroupNotifier.viewIndex; - + if (PageGroupWidget.getPageController(context) != null) { + GlobalBottomNavController.instance.pageController = + PageGroupWidget.getPageController(context)!; + } return EnsembleBottomAppBar( key: UniqueKey(), selectedIndex: viewIndex, @@ -347,7 +453,18 @@ class _BottomNavPageGroupState extends State if (widget.menu.reloadView == true) { viewGroupNotifier.updatePage(index); } else { - PageGroupWidget.getPageController(context)!.jumpToPage(index); + // Continue the old flow if PageController is available + if (PageGroupWidget.getPageController(context) != null) { + PageGroupWidget.getPageController(context)!.jumpToPage(index); + } + else { + // If PageController is not available, navigate to the screen + ScreenController().navigateToScreen(context, + screenName: navItems[index].page, + pageArgs: viewGroupNotifier.payload); + // Update the global controller to reflect the current page + GlobalBottomNavController.instance.pageController.jumpToPage(index); + } viewGroupNotifier.updatePage(index); } @@ -590,4 +707,4 @@ class EnsembleBottomAppBarState extends State { ), ); } -} +} \ No newline at end of file diff --git a/modules/ensemble/lib/framework/view/page.dart b/modules/ensemble/lib/framework/view/page.dart index 84dba503d..49e9c6c68 100644 --- a/modules/ensemble/lib/framework/view/page.dart +++ b/modules/ensemble/lib/framework/view/page.dart @@ -1,5 +1,6 @@ import 'dart:developer'; - +import 'bottom_nav_page_group.dart'; // To access bottomNavVisibilityNotifier +import 'bottom_nav_controller.dart'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/action.dart'; import 'package:ensemble/ensemble_app.dart'; @@ -94,11 +95,40 @@ class PageState extends State @override bool get wantKeepAlive => true; + // Helper method to check if we're in a bottom nav context +// bool _isInBottomNavContext() { +// try { +// final pageGroupWidget = context.findAncestorWidgetOfExactType(); +// return pageGroupWidget != null; +// } catch (e) { +// return false; +// } +// } + +// // Handle bottom nav visibility based on page styles +// void _handleBottomNavVisibility() { +// if (_isInBottomNavContext()) { +// final hideBottomNavBar = Utils.optionalBool( +// widget._pageModel.runtimeStyles?['hideBottomNavBar']) ?? false; + +// // Use the controller that the UI is actually listening to +// if (hideBottomNavBar) { +// bottomNavVisibilityNotifier.hide(); // ← Use this one +// } else { +// print('calling show() from PageState'); +// bottomNavVisibilityNotifier.show(); // ← Use this one +// } +// } +// } + @override void didUpdateWidget(covariant Page oldWidget) { super.didUpdateWidget(oldWidget); // widget can be re-created at any time, we need to keep the Scope intact. widget.rootScopeManager = _scopeManager; + // WidgetsBinding.instance.addPostFrameCallback((_) { + // _handleBottomNavVisibility(); + // }); } @override @@ -106,11 +136,12 @@ class PageState extends State super.didChangeDependencies(); // if our widget changes, we need to save the scopeManager to it. widget.rootScopeManager = _scopeManager; - - // see if we are part of a ViewGroup or not BottomNavScreen? bottomNavRootScreen = BottomNavScreen.getScreen(context); if (bottomNavRootScreen != null) { bottomNavRootScreen.onReVisited(() { + // WidgetsBinding.instance.addPostFrameCallback((_) { + // _handleBottomNavVisibility(); + // }); if (widget._pageModel.viewBehavior.onResume != null) { ScreenController().executeActionWithScope( context, _scopeManager, widget._pageModel.viewBehavior.onResume!, @@ -176,6 +207,9 @@ class PageState extends State @override void didPush() { log("didPush() for ${widget.hashCode}"); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // _handleBottomNavVisibility(); + // }); } DateTime? screenLastPaused; @@ -196,6 +230,10 @@ class PageState extends State @override void didPopNext() { super.didPopNext(); + // print('inside didPopNext() for ${widget.hashCode}'); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // _handleBottomNavVisibility(); + // }); if (widget._pageModel.viewBehavior.onResume != null) { ScreenController().executeActionWithScope( context, _scopeManager, widget._pageModel.viewBehavior.onResume!, @@ -236,6 +274,10 @@ class PageState extends State } } } + // _handleBottomNavVisibility(); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // _handleBottomNavVisibility(); + // }); // execute view behavior if (widget._pageModel.viewBehavior.onLoad != null) { @@ -452,143 +494,185 @@ class PageState extends State return null; } - @override - Widget build(BuildContext context) { - super.build(context); - //log("View build() $hashCode"); - - // drawer might be injected from the PageGroup, so check for it first. - // Note that if the drawer already exists, we will ignore any new drawer - Widget? _drawer = PageGroupWidget.getNavigationDrawer(context); - Widget? _endDrawer = PageGroupWidget.getNavigationEndDrawer(context); - bool hasDrawer = _drawer != null || _endDrawer != null; - - Widget? _bottomNavBar; - if (widget._pageModel.menu != null) { - EnsembleThemeManager().configureStyles(_scopeManager.dataContext, - widget._pageModel.menu!, widget._pageModel.menu!); - } - // build the navigation menu (bottom nav bar or drawer). Note that menu is not applicable on modal pages - if (widget._pageModel.menu != null && - widget._pageModel.screenOptions?.pageType != PageType.modal) { - if (widget._pageModel.menu is BottomNavBarMenu) { - _bottomNavBar = _buildBottomNavBar( - context, widget._pageModel.menu as BottomNavBarMenu); - } else if (widget._pageModel.menu is DrawerMenu) { - if (!(widget._pageModel.menu as DrawerMenu).atStart) { - _endDrawer ??= - _buildDrawer(context, widget._pageModel.menu as DrawerMenu); - } else { - _drawer ??= - _buildDrawer(context, widget._pageModel.menu as DrawerMenu); - } +@override +Widget build(BuildContext context) { + super.build(context); + //log("View build() $hashCode"); + + // drawer might be injected from the PageGroup, so check for it first. + // Note that if the drawer already exists, we will ignore any new drawer + Widget? _drawer = PageGroupWidget.getNavigationDrawer(context); + Widget? _endDrawer = PageGroupWidget.getNavigationEndDrawer(context); + bool hasDrawer = _drawer != null || _endDrawer != null; + + Widget? _bottomNavBar; + if (widget._pageModel.menu != null) { + EnsembleThemeManager().configureStyles(_scopeManager.dataContext, + widget._pageModel.menu!, widget._pageModel.menu!); + } + final globalBottomNav = GlobalBottomNavController.instance; + Widget? _floatingActionButton; + FloatingActionButtonLocation? _floatingActionButtonLocation; + if (globalBottomNav.hasBottomNav && + widget._pageModel.runtimeStyles?['showMenu'] == true) { + _bottomNavBar = globalBottomNav.bottomNavWidget; + _floatingActionButton = globalBottomNav.floatingActionButton; + _floatingActionButtonLocation = globalBottomNav.floatingActionButtonLocation; + } + // build the navigation menu (bottom nav bar or drawer). Note that menu is not applicable on modal pages + if (widget._pageModel.menu != null && + widget._pageModel.screenOptions?.pageType != PageType.modal) { + if (widget._pageModel.menu is BottomNavBarMenu) { + _bottomNavBar = _buildBottomNavBar( + context, widget._pageModel.menu as BottomNavBarMenu); + } else if (widget._pageModel.menu is DrawerMenu) { + if (!(widget._pageModel.menu as DrawerMenu).atStart) { + _endDrawer ??= + _buildDrawer(context, widget._pageModel.menu as DrawerMenu); + } else { + _drawer ??= + _buildDrawer(context, widget._pageModel.menu as DrawerMenu); } - // sidebar navBar will be rendered as part of the body } + // sidebar navBar will be rendered as part of the body + } - LinearGradient? backgroundGradient = Utils.getBackgroundGradient( - widget._pageModel.runtimeStyles?['backgroundGradient']); - Color? backgroundColor = Utils.getColor(_scopeManager.dataContext - .eval(widget._pageModel.runtimeStyles?['backgroundColor'])); - // if we have a background image, set the background color to transparent - // since our image is outside the Scaffold - dynamic evaluatedBackgroundImg = _scopeManager.dataContext - .eval(widget._pageModel.runtimeStyles?['backgroundImage']); - BackgroundImage? backgroundImage = - Utils.getBackgroundImage(evaluatedBackgroundImg); - if (backgroundImage != null || backgroundGradient != null) { - backgroundColor = Colors.transparent; + // Use context bottom nav if available and page wants to show menu + bool showMenu = widget._pageModel.runtimeStyles?['showMenu'] ?? false; + if (showMenu && globalBottomNav.hasBottomNav) { + final storedWidget = globalBottomNav.bottomNavWidget; + if (storedWidget is EnsembleBottomAppBar) { + _bottomNavBar = EnsembleBottomAppBar( + // Copy all properties from stored widget + items: storedWidget.items, + selectedIndex: storedWidget.selectedIndex, + backgroundColor: storedWidget.backgroundColor, + height: storedWidget.height, + margin: storedWidget.margin, + padding: storedWidget.padding, + borderRadius: storedWidget.borderRadius, + color: storedWidget.color, + selectedColor: storedWidget.selectedColor, + boxShadow: storedWidget.boxShadow, + notchedShape: storedWidget.notchedShape, + isFloating: storedWidget.isFloating, + floatingAlignment: storedWidget.floatingAlignment, + floatingMargin: storedWidget.floatingMargin, + + // OVERRIDE with global controller callback + onTabSelected: (index) { + print('Tab $index tapped in Page.dart'); + globalBottomNav.selectTab(index); // This calls your registration callback + } + , + ); + } else { + _bottomNavBar = storedWidget; + } } + LinearGradient? backgroundGradient = Utils.getBackgroundGradient( + widget._pageModel.runtimeStyles?['backgroundGradient']); + Color? backgroundColor = Utils.getColor(_scopeManager.dataContext + .eval(widget._pageModel.runtimeStyles?['backgroundColor'])); + // if we have a background image, set the background color to transparent + // since our image is outside the Scaffold + dynamic evaluatedBackgroundImg = _scopeManager.dataContext + .eval(widget._pageModel.runtimeStyles?['backgroundImage']); + BackgroundImage? backgroundImage = + Utils.getBackgroundImage(evaluatedBackgroundImg); + if (backgroundImage != null || backgroundGradient != null) { + backgroundColor = Colors.transparent; + } // whether to usse CustomScrollView for the entire page - bool isScrollableView = - widget._pageModel.runtimeStyles?['scrollableView'] == true; + bool isScrollableView = + widget._pageModel.runtimeStyles?['scrollableView'] == true; - PreferredSizeWidget? fixedAppBar; - if (!isScrollableView) { - fixedAppBar = buildFixedAppBar(widget._pageModel, hasDrawer); - } + PreferredSizeWidget? fixedAppBar; + if (!isScrollableView) { + fixedAppBar = buildFixedAppBar(widget._pageModel, hasDrawer); + } // whether we have a header and if the close button is already there- - bool hasHeader = widget._pageModel.headerModel != null || hasDrawer; - bool? showNavigationIcon = - widget._pageModel.runtimeStyles?['showNavigationIcon']; - - // add close button for modal page - Widget? closeModalButton; - if (widget._pageModel.screenOptions?.pageType == PageType.modal && - !hasHeader && - showNavigationIcon != false) { - closeModalButton = FloatingActionButton( - elevation: 3, - backgroundColor: Colors.white, - foregroundColor: Colors.black, - mini: true, - onPressed: () { - Navigator.maybePop(context); - }, - child: const Icon(Icons.close_rounded), - ); - } + bool hasHeader = widget._pageModel.headerModel != null || hasDrawer; + bool? showNavigationIcon = + widget._pageModel.runtimeStyles?['showNavigationIcon']; + + // add close button for modal page + Widget? closeModalButton; + if (widget._pageModel.screenOptions?.pageType == PageType.modal && + !hasHeader && + showNavigationIcon != false) { + closeModalButton = FloatingActionButton( + elevation: 3, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + mini: true, + onPressed: () { + Navigator.maybePop(context); + }, + child: const Icon(Icons.close_rounded), + ); + } - Widget rtn = DataScopeWidget( - scopeManager: _scopeManager, - child: Unfocus( - isUnfocus: Utils.getBool(widget._pageModel.runtimeStyles?['unfocus'], - fallback: false), - child: Scaffold( - resizeToAvoidBottomInset: true, - // slight optimization, if body background is set, let's paint - // the entire screen including the Safe Area - backgroundColor: backgroundColor, - - // appBar is inside CustomScrollView if defined - appBar: fixedAppBar, - body: FooterLayout( - body: isScrollableView - ? buildScrollablePageContent(hasDrawer) - : buildFixedPageContent(fixedAppBar != null), - footer: footerWidget, - ), - bottomNavigationBar: _bottomNavBar, - drawer: _drawer, - endDrawer: _endDrawer, - floatingActionButton: closeModalButton, - floatingActionButtonLocation: + Widget rtn = DataScopeWidget( + scopeManager: _scopeManager, + child: Unfocus( + isUnfocus: Utils.getBool(widget._pageModel.runtimeStyles?['unfocus'], + fallback: false), + child: Scaffold( + resizeToAvoidBottomInset: true, + // slight optimization, if body background is set, let's paint + // the entire screen including the Safe Area + backgroundColor: backgroundColor, + + // appBar is inside CustomScrollView if defined + appBar: fixedAppBar, + body: FooterLayout( + body: isScrollableView + ? buildScrollablePageContent(hasDrawer) + : buildFixedPageContent(fixedAppBar != null), + footer: footerWidget, + ), + bottomNavigationBar: _bottomNavBar, + drawer: _drawer, + endDrawer: _endDrawer, + floatingActionButton: _floatingActionButton ?? closeModalButton, + floatingActionButtonLocation: _floatingActionButtonLocation ?? + ( widget._pageModel.runtimeStyles?['navigationIconPosition'] == - 'start' + 'start' ? FloatingActionButtonLocation.startTop - : FloatingActionButtonLocation.endTop), - ), - ); - DevMode.pageDataContext = _scopeManager.dataContext; - // selectableText at the root + : FloatingActionButtonLocation.endTop)), + ), + ); + DevMode.pageDataContext = _scopeManager.dataContext; + // selectableText at the root if (Utils.optionalBool(widget._pageModel.runtimeStyles?['selectable']) == - true) { - rtn = HasSelectableText(child: rtn); - } - - // if backgroundImage is set, put it outside of the Scaffold so - // keyboard sliding up (when entering value) won't resize the background - if (backgroundImage != null) { - return Stack( - children: [ - Positioned.fill( - child: backgroundImage.getImageAsWidget(_scopeManager), - ), - rtn, - ], - ); - } else if (backgroundGradient != null) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - decoration: BoxDecoration(gradient: backgroundGradient), - child: rtn)); - } - return rtn; + true) { + rtn = HasSelectableText(child: rtn); } + // if backgroundImage is set, put it outside of the Scaffold so + // keyboard sliding up (when entering value) won't resize the background + if (backgroundImage != null) { + return Stack( + children: [ + Positioned.fill( + child: backgroundImage.getImageAsWidget(_scopeManager), + ), + rtn, + ], + ); + } else if (backgroundGradient != null) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + decoration: BoxDecoration(gradient: backgroundGradient), + child: rtn)); + } + return rtn; +} /// determine if we should wraps the body in a SafeArea or not bool useSafeArea() { bool? useSafeArea = From b74c870888378858237adbe7648b5491f37c8ee7 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 18 Jun 2025 22:16:07 +0500 Subject: [PATCH 2/4] removed extra comments --- modules/ensemble/lib/framework/view/page.dart | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/modules/ensemble/lib/framework/view/page.dart b/modules/ensemble/lib/framework/view/page.dart index 49e9c6c68..210e19ef6 100644 --- a/modules/ensemble/lib/framework/view/page.dart +++ b/modules/ensemble/lib/framework/view/page.dart @@ -95,40 +95,11 @@ class PageState extends State @override bool get wantKeepAlive => true; - // Helper method to check if we're in a bottom nav context -// bool _isInBottomNavContext() { -// try { -// final pageGroupWidget = context.findAncestorWidgetOfExactType(); -// return pageGroupWidget != null; -// } catch (e) { -// return false; -// } -// } - -// // Handle bottom nav visibility based on page styles -// void _handleBottomNavVisibility() { -// if (_isInBottomNavContext()) { -// final hideBottomNavBar = Utils.optionalBool( -// widget._pageModel.runtimeStyles?['hideBottomNavBar']) ?? false; - -// // Use the controller that the UI is actually listening to -// if (hideBottomNavBar) { -// bottomNavVisibilityNotifier.hide(); // ← Use this one -// } else { -// print('calling show() from PageState'); -// bottomNavVisibilityNotifier.show(); // ← Use this one -// } -// } -// } - @override void didUpdateWidget(covariant Page oldWidget) { super.didUpdateWidget(oldWidget); // widget can be re-created at any time, we need to keep the Scope intact. widget.rootScopeManager = _scopeManager; - // WidgetsBinding.instance.addPostFrameCallback((_) { - // _handleBottomNavVisibility(); - // }); } @override @@ -139,9 +110,6 @@ class PageState extends State BottomNavScreen? bottomNavRootScreen = BottomNavScreen.getScreen(context); if (bottomNavRootScreen != null) { bottomNavRootScreen.onReVisited(() { - // WidgetsBinding.instance.addPostFrameCallback((_) { - // _handleBottomNavVisibility(); - // }); if (widget._pageModel.viewBehavior.onResume != null) { ScreenController().executeActionWithScope( context, _scopeManager, widget._pageModel.viewBehavior.onResume!, @@ -207,9 +175,6 @@ class PageState extends State @override void didPush() { log("didPush() for ${widget.hashCode}"); - // WidgetsBinding.instance.addPostFrameCallback((_) { - // _handleBottomNavVisibility(); - // }); } DateTime? screenLastPaused; @@ -230,10 +195,6 @@ class PageState extends State @override void didPopNext() { super.didPopNext(); - // print('inside didPopNext() for ${widget.hashCode}'); - // WidgetsBinding.instance.addPostFrameCallback((_) { - // _handleBottomNavVisibility(); - // }); if (widget._pageModel.viewBehavior.onResume != null) { ScreenController().executeActionWithScope( context, _scopeManager, widget._pageModel.viewBehavior.onResume!, @@ -274,10 +235,6 @@ class PageState extends State } } } - // _handleBottomNavVisibility(); - // WidgetsBinding.instance.addPostFrameCallback((_) { - // _handleBottomNavVisibility(); - // }); // execute view behavior if (widget._pageModel.viewBehavior.onLoad != null) { From 7d09f6a390d2e5beaa13fa6f02f00776f3a5a17a Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Thu, 19 Jun 2025 08:32:32 +0500 Subject: [PATCH 3/4] code cleanup --- .../framework/view/bottom_nav_controller.dart | 153 ++------ .../framework/view/bottom_nav_page_group.dart | 108 +---- modules/ensemble/lib/framework/view/page.dart | 369 ++++++++---------- 3 files changed, 203 insertions(+), 427 deletions(-) diff --git a/modules/ensemble/lib/framework/view/bottom_nav_controller.dart b/modules/ensemble/lib/framework/view/bottom_nav_controller.dart index 1f013be05..f5da701ad 100644 --- a/modules/ensemble/lib/framework/view/bottom_nav_controller.dart +++ b/modules/ensemble/lib/framework/view/bottom_nav_controller.dart @@ -7,7 +7,7 @@ class GlobalBottomNavController extends ChangeNotifier { // Private constructor GlobalBottomNavController._(); - + // Singleton instance getter static GlobalBottomNavController get instance { _instance ??= GlobalBottomNavController._(); @@ -18,165 +18,60 @@ class GlobalBottomNavController extends ChangeNotifier { Widget? _storedBottomNavWidget; Widget? _storedFloatingActionButton; FloatingActionButtonLocation? _storedFloatingActionButtonLocation; - Function(int)? _originalOnTabSelected; - late BuildContext _originalContext; - late PageController _pageController; - - String? _activePageGroupId; - BottomNavBarMenu? _currentMenu; - - // Debug tracking - final List _debugLog = []; // Getters Widget? get bottomNavWidget => _storedBottomNavWidget; Widget? get floatingActionButton => _storedFloatingActionButton; - FloatingActionButtonLocation? get floatingActionButtonLocation => _storedFloatingActionButtonLocation; - bool get hasBottomNav => _storedBottomNavWidget != null; - String? get currentPageGroup => _activePageGroupId; - BottomNavBarMenu? get currentMenu => _currentMenu; - List get debugLog => List.unmodifiable(_debugLog); - - // Original context getter - BuildContext get originalContext => _originalContext; - - // PageController getter - PageController get pageController => _pageController; - - // Original context setter - set originalContext(BuildContext context) { - _originalContext = context; - notifyListeners(); - } + FloatingActionButtonLocation? get floatingActionButtonLocation => + _storedFloatingActionButtonLocation; - // PageController setter - set pageController(PageController controller) { - _pageController = controller; + set bottomNavWidget(Widget? widget) { + _storedBottomNavWidget = widget; notifyListeners(); } - /// Register the built widgets from PageGroup - void registerBottomNavWidgets({ - required Widget? bottomNavWidget, - required Function(int) onTabSelected, - required Widget? floatingActionButton, - required FloatingActionButtonLocation? floatingActionButtonLocation, - required String pageGroupId, - BottomNavBarMenu? menu, - PageController? pageController, // Add optional PageController parameter - }) { - - _storedBottomNavWidget = bottomNavWidget; - _storedFloatingActionButton = floatingActionButton; - _storedFloatingActionButtonLocation = floatingActionButtonLocation; - _activePageGroupId = pageGroupId; - _currentMenu = menu; - _originalOnTabSelected = onTabSelected; // Store the callback correctly - - // Store PageController if provided - if (pageController != null) { - _pageController = pageController; - } - - + set floatingActionButton(Widget? widget) { + _storedFloatingActionButton = widget; notifyListeners(); } - /// Handle tab selection from external context - void selectTab(int index) { - if (_originalOnTabSelected != null) { - _originalOnTabSelected!(index); - } - } - - /// Jump to page using stored PageController - void jumpToPage(int index) { - try { - if (_pageController.hasClients) { - _pageController.jumpToPage(index); - } else { - } - } catch (e) { - _addDebugLog('Error jumping to page: $e'); - } + set floatingLocation(FloatingActionButtonLocation? widget) { + _storedFloatingActionButtonLocation = widget; + notifyListeners(); } - /// Animate to page using stored PageController - void animateToPage(int index, { - Duration duration = const Duration(milliseconds: 300), - Curve curve = Curves.ease, - }) { - try { - if (_pageController.hasClients) { - _pageController.animateToPage( - index, - duration: duration, - curve: curve, - ); - } else { - _addDebugLog('❌ PageController has no clients'); - } - } catch (e) { - _addDebugLog('❌ Error animating to page: $e'); - } - } /// Unregister widgets when PageGroup is disposed - void unregisterBottomNavWidgets(String pageGroupId) { - if (_activePageGroupId == pageGroupId) { + void unregisterBottomNavWidgets() { _storedBottomNavWidget = null; _storedFloatingActionButton = null; _storedFloatingActionButtonLocation = null; - _originalOnTabSelected = null; - _activePageGroupId = null; - _currentMenu = null; - + notifyListeners(); - } } - /// Check if a specific PageGroup is currently active - bool isPageGroupActive(String pageGroupId) { - return _activePageGroupId == pageGroupId; + /// Get bottom nav widget + Widget? getBottomNavWidget() { + return _storedBottomNavWidget != null + ? Container(child: _storedBottomNavWidget) + : null; } - -/// Get bottom nav widget -Widget? getBottomNavWidget() { - return _storedBottomNavWidget != null - ? Container(child: _storedBottomNavWidget) - : null; -} - -/// Get floating action button -Widget? getFloatingActionButton() { - return _storedFloatingActionButton != null - ? Container(child: _storedFloatingActionButton) - : null; -} - - // Debug helpers - void _addDebugLog(String message) { - final timestamp = DateTime.now().toIso8601String().substring(11, 23); - final logEntry = '[$timestamp] $message'; - _debugLog.add(logEntry); - - // Keep only last 100 entries - if (_debugLog.length > 100) { - _debugLog.removeAt(0); - } - - debugPrint('🌍 GlobalBottomNav: $logEntry'); + /// Get floating action button + Widget? getFloatingActionButton() { + return _storedFloatingActionButton != null + ? Container(child: _storedFloatingActionButton) + : null; } @override void dispose() { - _addDebugLog('🗑️ Controller disposed'); super.dispose(); } } /// Extension to make it easier to access the controller extension GlobalBottomNavControllerExtension on BuildContext { - GlobalBottomNavController get globalBottomNav => GlobalBottomNavController.instance; + GlobalBottomNavController get globalBottomNav => + GlobalBottomNavController.instance; } \ No newline at end of file diff --git a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart index b1ade9fcd..6dbdff2bf 100644 --- a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart +++ b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart @@ -122,10 +122,10 @@ class _BottomNavPageGroupState extends State @override void initState() { super.initState(); - + // Generate unique page group ID _pageGroupId = 'pagegroup_${widget.hashCode}'; - + if (widget.menu.reloadView == false) { controller = PageController(initialPage: widget.selectedPage); } @@ -162,8 +162,8 @@ class _BottomNavPageGroupState extends State @override void dispose() { // Unregister from global controller - GlobalBottomNavController.instance.unregisterBottomNavWidgets(_pageGroupId); - + GlobalBottomNavController.instance.unregisterBottomNavWidgets(); + if (widget.menu.reloadView == false) { controller.dispose(); } @@ -240,93 +240,11 @@ class _BottomNavPageGroupState extends State floatingAlignment == FloatingAlignment.none ? null : floatingAlignment.location; - -// Register the built widgets with global controller WidgetsBinding.instance.addPostFrameCallback((_) { - GlobalBottomNavController.instance.registerBottomNavWidgets( - bottomNavWidget: bottomNavBar, - floatingActionButton: floatingButton, - floatingActionButtonLocation: floatingLocation, - pageGroupId: _pageGroupId, - onTabSelected: (index) { - List navItems = []; - - final unselectedColor = - Utils.getColor(widget.menu.runtimeStyles?['color']) ?? - Theme.of(context).unselectedWidgetColor; - final selectedColor = - Utils.getColor(widget.menu.runtimeStyles?['selectedColor']) ?? - Theme.of(context).primaryColor; - - // final menu = widget.menu; - for (int i = 0; i < menuItems.length; i++) { - MenuItem item = menuItems[i]; - final dynamic customIcon = _buildCustomIcon(item); - final dynamic customActiveIcon = - _buildCustomIcon(item, isActive: true); - - final isCustom = customIcon != null || customActiveIcon != null; - final label = - isCustom ? '' : Utils.translate(item.label ?? '', context); - - final icon = customIcon ?? - (item.icon != null - ? ensemble.Icon.fromModel(item.icon!, - fallbackLibrary: item.iconLibrary, - fallbackColor: unselectedColor) - : ensemble.Icon('')); - - final activeIcon = customActiveIcon ?? - (item.activeIcon != null || item.icon != null - ? ensemble.Icon.fromModel((item.activeIcon ?? item.icon)!, - fallbackColor: selectedColor, - fallbackLibrary: item.iconLibrary) - : null); - - navItems.add( - BottomNavBarItem( - icon: icon, - activeIcon: activeIcon, - isCustom: isCustom, - text: label, - switchScreen: Menu.evalSwitchScreen( - widget.scopeManager.dataContext, item), - onTap: EnsembleAction.from(item.onTap), - onTapHaptic: item.onTapHaptic, - page: item.page), - ); - } - // Copy this EXACT code from your _buildBottomNavBar() method: - final isSwitchScreen = - Utils.getBool(navItems[index].switchScreen, fallback: true); - print('clicked inside the register!!!'); - if (isSwitchScreen) { - if (widget.menu.reloadView == true) { - viewGroupNotifier.updatePage(index); - } else { - PageGroupWidget.getPageController(context)!.jumpToPage(index); - viewGroupNotifier.updatePage(index); - } - - _onTap(navItems[index]); - } else { - // Execute only onTap action. Page switching is handled by the developer with onTap - _onTap(navItems[index]); - } - - // Executing haptic feedback action - if (navItems[index].onTapHaptic != null) { - ScreenController().executeAction( - context, - HapticAction( - type: navItems[index].onTapHaptic!, onComplete: null), - ); - } - }, - menu: widget.menu as BottomNavBarMenu, - ); + GlobalBottomNavController.instance.bottomNavWidget = bottomNavBar; + GlobalBottomNavController.instance.floatingActionButton = floatingButton; + GlobalBottomNavController.instance.floatingLocation = floatingLocation; }); - return PageGroupWidgetWrapper( reloadView: widget.menu.reloadView, scopeManager: widget.scopeManager, @@ -416,10 +334,6 @@ class _BottomNavPageGroupState extends State listenable: viewGroupNotifier, builder: (context, _) { final viewIndex = viewGroupNotifier.viewIndex; - if (PageGroupWidget.getPageController(context) != null) { - GlobalBottomNavController.instance.pageController = - PageGroupWidget.getPageController(context)!; - } return EnsembleBottomAppBar( key: UniqueKey(), selectedIndex: viewIndex, @@ -456,14 +370,12 @@ class _BottomNavPageGroupState extends State // Continue the old flow if PageController is available if (PageGroupWidget.getPageController(context) != null) { PageGroupWidget.getPageController(context)!.jumpToPage(index); - } - else { - // If PageController is not available, navigate to the screen + } else if (GlobalBottomNavController.instance.bottomNavWidget != + null) { + // This to make sure that if nav buttons are used outside the page group, they would still function ScreenController().navigateToScreen(context, screenName: navItems[index].page, pageArgs: viewGroupNotifier.payload); - // Update the global controller to reflect the current page - GlobalBottomNavController.instance.pageController.jumpToPage(index); } viewGroupNotifier.updatePage(index); } diff --git a/modules/ensemble/lib/framework/view/page.dart b/modules/ensemble/lib/framework/view/page.dart index 210e19ef6..2a54b70a8 100644 --- a/modules/ensemble/lib/framework/view/page.dart +++ b/modules/ensemble/lib/framework/view/page.dart @@ -356,8 +356,8 @@ class PageState extends State Color? shadowColor = Utils.getColor(evaluatedHeader?['shadowColor']); double? elevation = Utils.optionalInt(evaluatedHeader?['elevation'], min: 0)?.toDouble(); - ScrollMode scrollMode = - Utils.getEnum(evaluatedHeader?['scrollMode'], ScrollMode.values); + ScrollMode scrollMode = Utils.getEnum( + evaluatedHeader?['scrollMode'], ScrollMode.values); final titleBarHeight = Utils.optionalInt(evaluatedHeader?['titleBarHeight'], min: 0) ?.toDouble() ?? @@ -375,7 +375,8 @@ class PageState extends State animationEnabled = Utils.getBool(animation!['enabled'], fallback: false); duration = Utils.getInt(animation!['duration'], fallback: 0); curve = Utils.getCurve(animation!['curve']); - animationType = Utils.getEnum(animation!['animationType'], AnimationType.values); + animationType = Utils.getEnum( + animation!['animationType'], AnimationType.values); } // applicable only to Sliver scrolling double? flexibleMaxHeight = @@ -451,185 +452,153 @@ class PageState extends State return null; } -@override -Widget build(BuildContext context) { - super.build(context); - //log("View build() $hashCode"); - - // drawer might be injected from the PageGroup, so check for it first. - // Note that if the drawer already exists, we will ignore any new drawer - Widget? _drawer = PageGroupWidget.getNavigationDrawer(context); - Widget? _endDrawer = PageGroupWidget.getNavigationEndDrawer(context); - bool hasDrawer = _drawer != null || _endDrawer != null; - - Widget? _bottomNavBar; - if (widget._pageModel.menu != null) { - EnsembleThemeManager().configureStyles(_scopeManager.dataContext, - widget._pageModel.menu!, widget._pageModel.menu!); - } + @override + Widget build(BuildContext context) { + super.build(context); + //log("View build() $hashCode"); + + // drawer might be injected from the PageGroup, so check for it first. + // Note that if the drawer already exists, we will ignore any new drawer + Widget? _drawer = PageGroupWidget.getNavigationDrawer(context); + Widget? _endDrawer = PageGroupWidget.getNavigationEndDrawer(context); + bool hasDrawer = _drawer != null || _endDrawer != null; + + Widget? _bottomNavBar; + if (widget._pageModel.menu != null) { + EnsembleThemeManager().configureStyles(_scopeManager.dataContext, + widget._pageModel.menu!, widget._pageModel.menu!); + } final globalBottomNav = GlobalBottomNavController.instance; Widget? _floatingActionButton; - FloatingActionButtonLocation? _floatingActionButtonLocation; - if (globalBottomNav.hasBottomNav && - widget._pageModel.runtimeStyles?['showMenu'] == true) { - _bottomNavBar = globalBottomNav.bottomNavWidget; - _floatingActionButton = globalBottomNav.floatingActionButton; - _floatingActionButtonLocation = globalBottomNav.floatingActionButtonLocation; - } - // build the navigation menu (bottom nav bar or drawer). Note that menu is not applicable on modal pages - if (widget._pageModel.menu != null && - widget._pageModel.screenOptions?.pageType != PageType.modal) { - if (widget._pageModel.menu is BottomNavBarMenu) { - _bottomNavBar = _buildBottomNavBar( - context, widget._pageModel.menu as BottomNavBarMenu); - } else if (widget._pageModel.menu is DrawerMenu) { - if (!(widget._pageModel.menu as DrawerMenu).atStart) { - _endDrawer ??= - _buildDrawer(context, widget._pageModel.menu as DrawerMenu); - } else { - _drawer ??= - _buildDrawer(context, widget._pageModel.menu as DrawerMenu); + FloatingActionButtonLocation? _floatingActionButtonLocation; + // build the navigation menu (bottom nav bar or drawer). Note that menu is not applicable on modal pages + if (widget._pageModel.menu != null && + widget._pageModel.screenOptions?.pageType != PageType.modal) { + if (widget._pageModel.menu is BottomNavBarMenu) { + _bottomNavBar = _buildBottomNavBar( + context, widget._pageModel.menu as BottomNavBarMenu); + } else if (widget._pageModel.menu is DrawerMenu) { + if (!(widget._pageModel.menu as DrawerMenu).atStart) { + _endDrawer ??= + _buildDrawer(context, widget._pageModel.menu as DrawerMenu); + } else { + _drawer ??= + _buildDrawer(context, widget._pageModel.menu as DrawerMenu); + } } + // sidebar navBar will be rendered as part of the body } - // sidebar navBar will be rendered as part of the body - } - // Use context bottom nav if available and page wants to show menu - bool showMenu = widget._pageModel.runtimeStyles?['showMenu'] ?? false; - if (showMenu && globalBottomNav.hasBottomNav) { - final storedWidget = globalBottomNav.bottomNavWidget; - if (storedWidget is EnsembleBottomAppBar) { - _bottomNavBar = EnsembleBottomAppBar( - // Copy all properties from stored widget - items: storedWidget.items, - selectedIndex: storedWidget.selectedIndex, - backgroundColor: storedWidget.backgroundColor, - height: storedWidget.height, - margin: storedWidget.margin, - padding: storedWidget.padding, - borderRadius: storedWidget.borderRadius, - color: storedWidget.color, - selectedColor: storedWidget.selectedColor, - boxShadow: storedWidget.boxShadow, - notchedShape: storedWidget.notchedShape, - isFloating: storedWidget.isFloating, - floatingAlignment: storedWidget.floatingAlignment, - floatingMargin: storedWidget.floatingMargin, - - // OVERRIDE with global controller callback - onTabSelected: (index) { - print('Tab $index tapped in Page.dart'); - globalBottomNav.selectTab(index); // This calls your registration callback - } - , - ); - } else { - _bottomNavBar = storedWidget; - } + if (globalBottomNav.bottomNavWidget != null && + widget._pageModel.runtimeStyles?['showMenu'] == true) { + _bottomNavBar = globalBottomNav.bottomNavWidget; + _floatingActionButton = globalBottomNav.floatingActionButton; + _floatingActionButtonLocation = + globalBottomNav.floatingActionButtonLocation; } LinearGradient? backgroundGradient = Utils.getBackgroundGradient( - widget._pageModel.runtimeStyles?['backgroundGradient']); - Color? backgroundColor = Utils.getColor(_scopeManager.dataContext - .eval(widget._pageModel.runtimeStyles?['backgroundColor'])); - // if we have a background image, set the background color to transparent - // since our image is outside the Scaffold - dynamic evaluatedBackgroundImg = _scopeManager.dataContext - .eval(widget._pageModel.runtimeStyles?['backgroundImage']); - BackgroundImage? backgroundImage = - Utils.getBackgroundImage(evaluatedBackgroundImg); - if (backgroundImage != null || backgroundGradient != null) { - backgroundColor = Colors.transparent; - } + widget._pageModel.runtimeStyles?['backgroundGradient']); + Color? backgroundColor = Utils.getColor(_scopeManager.dataContext + .eval(widget._pageModel.runtimeStyles?['backgroundColor'])); + // if we have a background image, set the background color to transparent + // since our image is outside the Scaffold + dynamic evaluatedBackgroundImg = _scopeManager.dataContext + .eval(widget._pageModel.runtimeStyles?['backgroundImage']); + BackgroundImage? backgroundImage = + Utils.getBackgroundImage(evaluatedBackgroundImg); + if (backgroundImage != null || backgroundGradient != null) { + backgroundColor = Colors.transparent; + } // whether to usse CustomScrollView for the entire page - bool isScrollableView = - widget._pageModel.runtimeStyles?['scrollableView'] == true; + bool isScrollableView = + widget._pageModel.runtimeStyles?['scrollableView'] == true; - PreferredSizeWidget? fixedAppBar; - if (!isScrollableView) { - fixedAppBar = buildFixedAppBar(widget._pageModel, hasDrawer); - } + PreferredSizeWidget? fixedAppBar; + if (!isScrollableView) { + fixedAppBar = buildFixedAppBar(widget._pageModel, hasDrawer); + } // whether we have a header and if the close button is already there- - bool hasHeader = widget._pageModel.headerModel != null || hasDrawer; - bool? showNavigationIcon = - widget._pageModel.runtimeStyles?['showNavigationIcon']; - - // add close button for modal page - Widget? closeModalButton; - if (widget._pageModel.screenOptions?.pageType == PageType.modal && - !hasHeader && - showNavigationIcon != false) { - closeModalButton = FloatingActionButton( - elevation: 3, - backgroundColor: Colors.white, - foregroundColor: Colors.black, - mini: true, - onPressed: () { - Navigator.maybePop(context); - }, - child: const Icon(Icons.close_rounded), - ); - } + bool hasHeader = widget._pageModel.headerModel != null || hasDrawer; + bool? showNavigationIcon = + widget._pageModel.runtimeStyles?['showNavigationIcon']; + + // add close button for modal page + Widget? closeModalButton; + if (widget._pageModel.screenOptions?.pageType == PageType.modal && + !hasHeader && + showNavigationIcon != false) { + closeModalButton = FloatingActionButton( + elevation: 3, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + mini: true, + onPressed: () { + Navigator.maybePop(context); + }, + child: const Icon(Icons.close_rounded), + ); + } - Widget rtn = DataScopeWidget( - scopeManager: _scopeManager, - child: Unfocus( - isUnfocus: Utils.getBool(widget._pageModel.runtimeStyles?['unfocus'], - fallback: false), - child: Scaffold( - resizeToAvoidBottomInset: true, - // slight optimization, if body background is set, let's paint - // the entire screen including the Safe Area - backgroundColor: backgroundColor, - - // appBar is inside CustomScrollView if defined - appBar: fixedAppBar, - body: FooterLayout( - body: isScrollableView - ? buildScrollablePageContent(hasDrawer) - : buildFixedPageContent(fixedAppBar != null), - footer: footerWidget, - ), - bottomNavigationBar: _bottomNavBar, - drawer: _drawer, - endDrawer: _endDrawer, - floatingActionButton: _floatingActionButton ?? closeModalButton, - floatingActionButtonLocation: _floatingActionButtonLocation ?? - ( - widget._pageModel.runtimeStyles?['navigationIconPosition'] == - 'start' + Widget rtn = DataScopeWidget( + scopeManager: _scopeManager, + child: Unfocus( + isUnfocus: Utils.getBool(widget._pageModel.runtimeStyles?['unfocus'], + fallback: false), + child: Scaffold( + resizeToAvoidBottomInset: true, + // slight optimization, if body background is set, let's paint + // the entire screen including the Safe Area + backgroundColor: backgroundColor, + + // appBar is inside CustomScrollView if defined + appBar: fixedAppBar, + body: FooterLayout( + body: isScrollableView + ? buildScrollablePageContent(hasDrawer) + : buildFixedPageContent(fixedAppBar != null), + footer: footerWidget, + ), + bottomNavigationBar: _bottomNavBar, + drawer: _drawer, + endDrawer: _endDrawer, + floatingActionButton: _floatingActionButton ?? closeModalButton, + floatingActionButtonLocation: _floatingActionButtonLocation ?? + ( + widget._pageModel.runtimeStyles?['navigationIconPosition'] == + 'start' ? FloatingActionButtonLocation.startTop : FloatingActionButtonLocation.endTop)), - ), - ); - DevMode.pageDataContext = _scopeManager.dataContext; - // selectableText at the root + ), + ); + DevMode.pageDataContext = _scopeManager.dataContext; + // selectableText at the root if (Utils.optionalBool(widget._pageModel.runtimeStyles?['selectable']) == - true) { - rtn = HasSelectableText(child: rtn); - } + true) { + rtn = HasSelectableText(child: rtn); + } - // if backgroundImage is set, put it outside of the Scaffold so - // keyboard sliding up (when entering value) won't resize the background - if (backgroundImage != null) { - return Stack( - children: [ - Positioned.fill( - child: backgroundImage.getImageAsWidget(_scopeManager), - ), - rtn, - ], - ); - } else if (backgroundGradient != null) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - decoration: BoxDecoration(gradient: backgroundGradient), - child: rtn)); + // if backgroundImage is set, put it outside of the Scaffold so + // keyboard sliding up (when entering value) won't resize the background + if (backgroundImage != null) { + return Stack( + children: [ + Positioned.fill( + child: backgroundImage.getImageAsWidget(_scopeManager), + ), + rtn, + ], + ); + } else if (backgroundGradient != null) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + decoration: BoxDecoration(gradient: backgroundGradient), + child: rtn)); + } + return rtn; } - return rtn; -} /// determine if we should wraps the body in a SafeArea or not bool useSafeArea() { bool? useSafeArea = @@ -945,26 +914,26 @@ class AnimatedAppBar extends StatefulWidget { final duration; AnimatedAppBar( {Key? key, - this.automaticallyImplyLeading, - this.leadingWidget, - this.titleWidget, - this.centerTitle, - this.backgroundColor, - this.surfaceTintColor, - this.foregroundColor, - this.elevation, - this.shadowColor, - this.titleBarHeight, - this.backgroundWidget, - this.animated, - this.floating, - this.pinned, - this.collapsedBarHeight, - this.expandedBarHeight, - required this.scrollController, - this.curve, - this.animationType, - this.duration}) + this.automaticallyImplyLeading, + this.leadingWidget, + this.titleWidget, + this.centerTitle, + this.backgroundColor, + this.surfaceTintColor, + this.foregroundColor, + this.elevation, + this.shadowColor, + this.titleBarHeight, + this.backgroundWidget, + this.animated, + this.floating, + this.pinned, + this.collapsedBarHeight, + this.expandedBarHeight, + required this.scrollController, + this.curve, + this.animationType, + this.duration}) : super(key: key); @override @@ -973,7 +942,7 @@ class AnimatedAppBar extends StatefulWidget { class _AnimatedAppBarState extends State with WidgetsBindingObserver{ bool isCollapsed = false; - + @override void initState() { super.initState(); @@ -1038,21 +1007,21 @@ class _AnimatedAppBarState extends State with WidgetsBindingObse centerTitle: widget.centerTitle, title: widget.animated ? switch (widget.animationType) { - AnimationType.fade => AnimatedOpacity( - opacity: isCollapsed ? 1.0 : 0.0, - duration: Duration(milliseconds: widget.duration ?? 300), - curve: widget.curve ?? Curves.easeIn, - child: widget.titleWidget, - ), - AnimationType.drop => AnimatedSlide( - offset: isCollapsed ? Offset(0, 0) : Offset(0, -2), - duration: Duration(milliseconds: widget.duration ?? 300), - curve: widget.curve ?? Curves.easeIn, - child: widget.titleWidget, - ), - _ => widget.titleWidget, - } - : widget.titleWidget, + AnimationType.fade => AnimatedOpacity( + opacity: isCollapsed ? 1.0 : 0.0, + duration: Duration(milliseconds: widget.duration ?? 300), + curve: widget.curve ?? Curves.easeIn, + child: widget.titleWidget, + ), + AnimationType.drop => AnimatedSlide( + offset: isCollapsed ? Offset(0, 0) : Offset(0, -2), + duration: Duration(milliseconds: widget.duration ?? 300), + curve: widget.curve ?? Curves.easeIn, + child: widget.titleWidget, + ), + _ => widget.titleWidget, + } + : widget.titleWidget, elevation: widget.elevation, backgroundColor: widget.backgroundColor, flexibleSpace: wrapsInFlexible(widget.backgroundWidget), From c613b42b1e70add477a60d94e9c96bb2d583a4d1 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Thu, 19 Jun 2025 08:45:16 +0500 Subject: [PATCH 4/4] code cleanup --- .../lib/framework/view/bottom_nav_page_group.dart | 12 +++--------- modules/ensemble/lib/framework/view/page.dart | 11 ++++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart index 6dbdff2bf..fa5bd14f3 100644 --- a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart +++ b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart @@ -15,7 +15,6 @@ import 'package:ensemble/util/utils.dart'; import 'package:ensemble/framework/widget/icon.dart' as ensemble; import 'package:ensemble/widget/helpers/controllers.dart'; import 'package:flutter/material.dart'; -// Import the global controller import 'package:ensemble/framework/view/bottom_nav_controller.dart'; class BottomNavBarItem { @@ -117,15 +116,10 @@ class _BottomNavPageGroupState extends State FloatingAlignment floatingAlignment = FloatingAlignment.center; int? floatingMargin; MenuItem? fabMenuItem; - late String _pageGroupId; @override void initState() { super.initState(); - - // Generate unique page group ID - _pageGroupId = 'pagegroup_${widget.hashCode}'; - if (widget.menu.reloadView == false) { controller = PageController(initialPage: widget.selectedPage); } @@ -161,9 +155,8 @@ class _BottomNavPageGroupState extends State @override void dispose() { - // Unregister from global controller + // Unregister the bottom nav widgets when the PageGroup is disposed GlobalBottomNavController.instance.unregisterBottomNavWidgets(); - if (widget.menu.reloadView == false) { controller.dispose(); } @@ -334,6 +327,7 @@ class _BottomNavPageGroupState extends State listenable: viewGroupNotifier, builder: (context, _) { final viewIndex = viewGroupNotifier.viewIndex; + return EnsembleBottomAppBar( key: UniqueKey(), selectedIndex: viewIndex, @@ -619,4 +613,4 @@ class EnsembleBottomAppBarState extends State { ), ); } -} \ No newline at end of file +} diff --git a/modules/ensemble/lib/framework/view/page.dart b/modules/ensemble/lib/framework/view/page.dart index 2a54b70a8..15920c35a 100644 --- a/modules/ensemble/lib/framework/view/page.dart +++ b/modules/ensemble/lib/framework/view/page.dart @@ -1,5 +1,4 @@ import 'dart:developer'; -import 'bottom_nav_page_group.dart'; // To access bottomNavVisibilityNotifier import 'bottom_nav_controller.dart'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/action.dart'; @@ -107,6 +106,8 @@ class PageState extends State super.didChangeDependencies(); // if our widget changes, we need to save the scopeManager to it. widget.rootScopeManager = _scopeManager; + + // see if we are part of a ViewGroup or not BottomNavScreen? bottomNavRootScreen = BottomNavScreen.getScreen(context); if (bottomNavRootScreen != null) { bottomNavRootScreen.onReVisited(() { @@ -356,8 +357,8 @@ class PageState extends State Color? shadowColor = Utils.getColor(evaluatedHeader?['shadowColor']); double? elevation = Utils.optionalInt(evaluatedHeader?['elevation'], min: 0)?.toDouble(); - ScrollMode scrollMode = Utils.getEnum( - evaluatedHeader?['scrollMode'], ScrollMode.values); + ScrollMode scrollMode = + Utils.getEnum(evaluatedHeader?['scrollMode'], ScrollMode.values); final titleBarHeight = Utils.optionalInt(evaluatedHeader?['titleBarHeight'], min: 0) ?.toDouble() ?? @@ -375,8 +376,7 @@ class PageState extends State animationEnabled = Utils.getBool(animation!['enabled'], fallback: false); duration = Utils.getInt(animation!['duration'], fallback: 0); curve = Utils.getCurve(animation!['curve']); - animationType = Utils.getEnum( - animation!['animationType'], AnimationType.values); + animationType = Utils.getEnum(animation!['animationType'], AnimationType.values); } // applicable only to Sliver scrolling double? flexibleMaxHeight = @@ -599,6 +599,7 @@ class PageState extends State } return rtn; } + /// determine if we should wraps the body in a SafeArea or not bool useSafeArea() { bool? useSafeArea =