From 5758180266b32d25e5437ff47ed1e75a88559cb4 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 13 Oct 2023 18:50:43 +0530 Subject: [PATCH 01/20] feat: add borderRadius for the bottom nav bar --- lib/framework/view/bottom_nav_page_group.dart | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/framework/view/bottom_nav_page_group.dart b/lib/framework/view/bottom_nav_page_group.dart index 04f08b482..47a930e2d 100644 --- a/lib/framework/view/bottom_nav_page_group.dart +++ b/lib/framework/view/bottom_nav_page_group.dart @@ -190,7 +190,7 @@ class _BottomNavPageGroupState extends State ); } - EnsembleBottomAppBar? _buildBottomNavBar() { + Widget? _buildBottomNavBar() { List navItems = []; final unselectedColor = Utils.getColor(widget.menu.styles?['color']) ?? @@ -198,6 +198,8 @@ class _BottomNavPageGroupState extends State final selectedColor = Utils.getColor(widget.menu.styles?['selectedColor']) ?? Theme.of(context).primaryColor; + final borderRadius = + Utils.getBorderRadius(widget.menu.styles?['borderRadius'])?.getValue(); // final menu = widget.menu; for (int i = 0; i < menuItems.length; i++) { @@ -232,20 +234,24 @@ class _BottomNavPageGroupState extends State ); } - return EnsembleBottomAppBar( - selectedIndex: widget.selectedPage, - backgroundColor: Utils.getColor(widget.menu.styles?['backgroundColor']) ?? - Colors.white, - height: Utils.optionalDouble(widget.menu.styles?['height'] ?? 60), - padding: widget.menu.styles?['padding'], - color: unselectedColor, - selectedColor: selectedColor, - notchedShape: const CircularNotchedRectangle(), - onTabSelected: controller.jumpToPage, - items: navItems, - isFloating: fabMenuItem != null, - floatingAlignment: floatingAlignment, - floatingMargin: floatingMargin, + return ClipRRect( + borderRadius: borderRadius ?? BorderRadius.zero, + child: EnsembleBottomAppBar( + selectedIndex: widget.selectedPage, + backgroundColor: + Utils.getColor(widget.menu.styles?['backgroundColor']) ?? + Colors.white, + height: Utils.optionalDouble(widget.menu.styles?['height'] ?? 60), + padding: widget.menu.styles?['padding'], + color: unselectedColor, + selectedColor: selectedColor, + notchedShape: const CircularNotchedRectangle(), + onTabSelected: controller.jumpToPage, + items: navItems, + isFloating: fabMenuItem != null, + floatingAlignment: floatingAlignment, + floatingMargin: floatingMargin, + ), ); } From a41236e15fab61d5ac77cb2545cc5224e4ee9601 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 13 Oct 2023 18:57:31 +0530 Subject: [PATCH 02/20] refactor: move border radius inside the EnsembleBottomAppBar --- lib/framework/view/bottom_nav_page_group.dart | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/lib/framework/view/bottom_nav_page_group.dart b/lib/framework/view/bottom_nav_page_group.dart index 47a930e2d..dc1844227 100644 --- a/lib/framework/view/bottom_nav_page_group.dart +++ b/lib/framework/view/bottom_nav_page_group.dart @@ -190,7 +190,7 @@ class _BottomNavPageGroupState extends State ); } - Widget? _buildBottomNavBar() { + EnsembleBottomAppBar? _buildBottomNavBar() { List navItems = []; final unselectedColor = Utils.getColor(widget.menu.styles?['color']) ?? @@ -198,8 +198,6 @@ class _BottomNavPageGroupState extends State final selectedColor = Utils.getColor(widget.menu.styles?['selectedColor']) ?? Theme.of(context).primaryColor; - final borderRadius = - Utils.getBorderRadius(widget.menu.styles?['borderRadius'])?.getValue(); // final menu = widget.menu; for (int i = 0; i < menuItems.length; i++) { @@ -234,24 +232,22 @@ class _BottomNavPageGroupState extends State ); } - return ClipRRect( - borderRadius: borderRadius ?? BorderRadius.zero, - child: EnsembleBottomAppBar( - selectedIndex: widget.selectedPage, - backgroundColor: - Utils.getColor(widget.menu.styles?['backgroundColor']) ?? - Colors.white, - height: Utils.optionalDouble(widget.menu.styles?['height'] ?? 60), - padding: widget.menu.styles?['padding'], - color: unselectedColor, - selectedColor: selectedColor, - notchedShape: const CircularNotchedRectangle(), - onTabSelected: controller.jumpToPage, - items: navItems, - isFloating: fabMenuItem != null, - floatingAlignment: floatingAlignment, - floatingMargin: floatingMargin, - ), + return EnsembleBottomAppBar( + selectedIndex: widget.selectedPage, + backgroundColor: Utils.getColor(widget.menu.styles?['backgroundColor']) ?? + Colors.white, + height: Utils.optionalDouble(widget.menu.styles?['height'] ?? 60), + padding: widget.menu.styles?['padding'], + borderRadius: Utils.getBorderRadius(widget.menu.styles?['borderRadius']) + ?.getValue(), + color: unselectedColor, + selectedColor: selectedColor, + notchedShape: const CircularNotchedRectangle(), + onTabSelected: controller.jumpToPage, + items: navItems, + isFloating: fabMenuItem != null, + floatingAlignment: floatingAlignment, + floatingMargin: floatingMargin, ); } @@ -271,6 +267,7 @@ class EnsembleBottomAppBar extends StatefulWidget { required this.selectedIndex, this.height, this.padding, + this.borderRadius, this.iconSize = 24.0, required this.backgroundColor, required this.color, @@ -296,6 +293,7 @@ class EnsembleBottomAppBar extends StatefulWidget { final bool isFloating; final FloatingAlignment floatingAlignment; final NotchedShape notchedShape; + final BorderRadius? borderRadius; final VoidCallback? onFabTapped; final ValueChanged onTabSelected; @@ -367,17 +365,20 @@ class EnsembleBottomAppBarState extends State { return Theme( data: ThemeData(useMaterial3: false), - child: BottomAppBar( - padding: const EdgeInsets.all(0), - shape: widget.notchedShape, - color: widget.backgroundColor, - notchMargin: _defaultFloatingNotch, - child: Padding( - padding: Utils.optionalInsets(widget.padding) ?? EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: items, + child: ClipRRect( + borderRadius: widget.borderRadius ?? BorderRadius.zero, + child: BottomAppBar( + padding: const EdgeInsets.all(0), + shape: widget.notchedShape, + color: widget.backgroundColor, + notchMargin: _defaultFloatingNotch, + child: Padding( + padding: Utils.optionalInsets(widget.padding) ?? EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: items, + ), ), ), ), From 7642d9c7269f41d7c6797206cc2836de911d008e Mon Sep 17 00:00:00 2001 From: snehmehta Date: Tue, 17 Oct 2023 17:53:47 +0530 Subject: [PATCH 03/20] set default color black --- lib/util/utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util/utils.dart b/lib/util/utils.dart index 73b88060f..c65fa3070 100644 --- a/lib/util/utils.dart +++ b/lib/util/utils.dart @@ -428,7 +428,7 @@ class Utils { fontStyle: Utils.optionalBool(style['isItalic']) == true ? FontStyle.italic : FontStyle.normal, - color: Utils.getColor(style['color']), + color: Utils.getColor(style['color']) ?? Colors.black, backgroundColor: Utils.getColor(style['backgroundColor']), decoration: getDecoration(style['decoration']), decorationStyle: From 1a35c7a2dcf5b471ea5441ad349b584543ad01af Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Wed, 18 Oct 2023 12:43:07 +0530 Subject: [PATCH 04/20] refactor: add margin to and remove clip rrect from the bottom nav bar --- lib/framework/view/bottom_nav_page_group.dart | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/framework/view/bottom_nav_page_group.dart b/lib/framework/view/bottom_nav_page_group.dart index dc1844227..cec8ab164 100644 --- a/lib/framework/view/bottom_nav_page_group.dart +++ b/lib/framework/view/bottom_nav_page_group.dart @@ -237,6 +237,7 @@ class _BottomNavPageGroupState extends State backgroundColor: Utils.getColor(widget.menu.styles?['backgroundColor']) ?? Colors.white, height: Utils.optionalDouble(widget.menu.styles?['height'] ?? 60), + margin: widget.menu.styles?['margin'], padding: widget.menu.styles?['padding'], borderRadius: Utils.getBorderRadius(widget.menu.styles?['borderRadius']) ?.getValue(), @@ -266,6 +267,7 @@ class EnsembleBottomAppBar extends StatefulWidget { required this.items, required this.selectedIndex, this.height, + this.margin, this.padding, this.borderRadius, this.iconSize = 24.0, @@ -284,6 +286,7 @@ class EnsembleBottomAppBar extends StatefulWidget { final List items; final int selectedIndex; final double? height; + final dynamic margin; final dynamic padding; final double iconSize; final int? floatingMargin; @@ -365,20 +368,21 @@ class EnsembleBottomAppBarState extends State { return Theme( data: ThemeData(useMaterial3: false), - child: ClipRRect( - borderRadius: widget.borderRadius ?? BorderRadius.zero, + child: Container( + margin: Utils.optionalInsets(widget.margin) ?? EdgeInsets.zero, + decoration: BoxDecoration( + borderRadius: widget.borderRadius ?? BorderRadius.zero, + ), + clipBehavior: Clip.hardEdge, child: BottomAppBar( - padding: const EdgeInsets.all(0), + padding: Utils.optionalInsets(widget.padding) ?? EdgeInsets.zero, shape: widget.notchedShape, color: widget.backgroundColor, notchMargin: _defaultFloatingNotch, - child: Padding( - padding: Utils.optionalInsets(widget.padding) ?? EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: items, - ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: items, ), ), ), From 93e30f6b104a105530e0413fc5db05c14c304f45 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Wed, 18 Oct 2023 13:43:22 +0530 Subject: [PATCH 05/20] feat(schema): add height, borderRadius, etc properties to BottomNavBar --- assets/schema/ensemble_schema.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/assets/schema/ensemble_schema.json b/assets/schema/ensemble_schema.json index cbd0c2ea1..dbccb5623 100644 --- a/assets/schema/ensemble_schema.json +++ b/assets/schema/ensemble_schema.json @@ -4318,6 +4318,19 @@ "floatingIconColor": { "$ref": "#/$defs/typeColors", "description": "Floating item icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC" + }, + "height": { + "type": "integer", + "description": "Set the height of the BottomNavBar." + }, + "padding": { + "$ref": "#/$defs/Padding-payload" + }, + "margin": { + "$ref": "#/$defs/Margin-payload" + }, + "borderRadius": { + "$ref": "#/$defs/borderRadius" } } }, From d98acb41030523c74e3b0bc6616365f3f8aa14f0 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Thu, 19 Oct 2023 12:56:59 +0530 Subject: [PATCH 06/20] Add floatingLabelStyle property and update decoration --- assets/schema/ensemble_schema.json | 3 + lib/widget/input/form_helper.dart | 148 +++++++++++++-------------- lib/widget/input/form_textfield.dart | 6 +- 3 files changed, 78 insertions(+), 79 deletions(-) diff --git a/assets/schema/ensemble_schema.json b/assets/schema/ensemble_schema.json index cbd0c2ea1..843d53f6a 100644 --- a/assets/schema/ensemble_schema.json +++ b/assets/schema/ensemble_schema.json @@ -2870,6 +2870,9 @@ "type": "boolean", "description": "Moves the label on top of the Input Field. Default (False)." }, + "floatingLabelStyle": { + "$ref": "#/$defs/TextStyle" + }, "labelStyle": { "$ref": "#/$defs/TextStyle" } diff --git a/lib/widget/input/form_helper.dart b/lib/widget/input/form_helper.dart index 2a4ac15b5..321d6e575 100644 --- a/lib/widget/input/form_helper.dart +++ b/lib/widget/input/form_helper.dart @@ -36,6 +36,7 @@ class FormFieldController extends WidgetController { Color? focusedBorderColor; Color? focusedErrorBorderColor; TextStyle? labelStyle; + TextStyle? floatingLabelStyle; @override Map getBaseGetters() { @@ -77,6 +78,8 @@ class FormFieldController extends WidgetController { 'focusedErrorBorderColor': (color) => focusedErrorBorderColor = Utils.getColor(color), 'labelStyle': (style) => labelStyle = Utils.getTextStyle(style), + 'floatingLabelStyle': (style) => + floatingLabelStyle = Utils.getTextStyle(style), }); return setters; } @@ -171,81 +174,78 @@ abstract class FormFieldWidgetState ThemeManager().getInputDefaultBorderRadius(variant); return InputDecoration( - // consistent with the theme. We need dense so user have granular control of contentPadding - isDense: true, - floatingLabelBehavior: FloatingLabelBehavior.always, - filled: filled, - fillColor: myController.fillColor, - // labelText: shouldShowLabel() ? myController.label : null, - hintText: myController.hintText, - prefixIcon: myController.icon == null - ? null - : framework.Icon( - myController.icon!.icon, - library: myController.icon!.library, - size: myController.icon!.size ?? - ThemeManager().getInputIconSize(context), - color: myController.icon!.color ?? - Theme.of(context).inputDecorationTheme.iconColor, - ), - contentPadding: myController.contentPadding, + // consistent with the theme. We need dense so user have granular control of contentPadding + isDense: true, + filled: filled, + fillColor: myController.fillColor, + // labelText: shouldShowLabel() ? myController.label : null, + hintText: myController.hintText, + prefixIcon: myController.icon == null + ? null + : framework.Icon( + myController.icon!.icon, + library: myController.icon!.library, + size: myController.icon!.size ?? + ThemeManager().getInputIconSize(context), + color: myController.icon!.color ?? + Theme.of(context).inputDecorationTheme.iconColor, + ), + contentPadding: myController.contentPadding, - // only redraw the border if necessary, as we will fallback - // to theme - border: myController.borderColor == null && !redrawAllBorders - ? null - : ThemeManager().getInputBorder( - variant: variant, - borderWidth: borderWidth, - borderRadius: borderRadius, - borderColor: myController.borderColor ?? - themeDecoration.border?.borderSide.color), - enabledBorder: - myController.enabledBorderColor == null && !redrawAllBorders - ? null - : ThemeManager().getInputBorder( - variant: variant, - borderWidth: borderWidth, - borderRadius: borderRadius, - borderColor: myController.enabledBorderColor ?? - themeDecoration.enabledBorder?.borderSide.color ?? - themeDecoration.border?.borderSide.color), - disabledBorder: - myController.disabledBorderColor == null && !redrawAllBorders - ? null - : ThemeManager().getInputBorder( - variant: variant, - borderWidth: borderWidth, - borderRadius: borderRadius, - borderColor: myController.disabledBorderColor ?? - themeDecoration.disabledBorder?.borderSide.color), - errorBorder: myController.errorBorderColor == null && !redrawAllBorders - ? null - : ThemeManager().getInputBorder( - variant: variant, - borderWidth: borderWidth, - borderRadius: borderRadius, - borderColor: myController.errorBorderColor ?? - themeDecoration.errorBorder?.borderSide.color), - focusedBorder: - myController.focusedBorderColor == null && !redrawAllBorders - ? null - : ThemeManager().getInputBorder( - variant: variant, - borderWidth: borderWidth, - borderRadius: borderRadius, - borderColor: myController.focusedBorderColor ?? - themeDecoration.focusedBorder?.borderSide.color), - focusedErrorBorder: - myController.focusedErrorBorderColor == null && !redrawAllBorders - ? null - : ThemeManager().getInputBorder( - variant: variant, - borderWidth: borderWidth, - borderRadius: borderRadius, - borderColor: myController.focusedErrorBorderColor ?? - themeDecoration.focusedErrorBorder?.borderSide.color), - ); + // only redraw the border if necessary, as we will fallback + // to theme + border: myController.borderColor == null && !redrawAllBorders + ? null + : ThemeManager().getInputBorder( + variant: variant, + borderWidth: borderWidth, + borderRadius: borderRadius, + borderColor: myController.borderColor ?? + themeDecoration.border?.borderSide.color), + enabledBorder: myController.enabledBorderColor == null && !redrawAllBorders + ? null + : ThemeManager().getInputBorder( + variant: variant, + borderWidth: borderWidth, + borderRadius: borderRadius, + borderColor: myController.enabledBorderColor ?? + themeDecoration.enabledBorder?.borderSide.color ?? + themeDecoration.border?.borderSide.color), + disabledBorder: myController.disabledBorderColor == null && !redrawAllBorders + ? null + : ThemeManager().getInputBorder( + variant: variant, + borderWidth: borderWidth, + borderRadius: borderRadius, + borderColor: myController.disabledBorderColor ?? + themeDecoration.disabledBorder?.borderSide.color), + errorBorder: myController.errorBorderColor == null && !redrawAllBorders + ? null + : ThemeManager().getInputBorder( + variant: variant, + borderWidth: borderWidth, + borderRadius: borderRadius, + borderColor: myController.errorBorderColor ?? + themeDecoration.errorBorder?.borderSide.color), + focusedBorder: myController.focusedBorderColor == null && !redrawAllBorders + ? null + : ThemeManager().getInputBorder( + variant: variant, + borderWidth: borderWidth, + borderRadius: borderRadius, + borderColor: myController.focusedBorderColor ?? + themeDecoration.focusedBorder?.borderSide.color), + focusedErrorBorder: + myController.focusedErrorBorderColor == null && !redrawAllBorders + ? null + : ThemeManager().getInputBorder( + variant: variant, + borderWidth: borderWidth, + borderRadius: borderRadius, + borderColor: myController.focusedErrorBorderColor ?? + themeDecoration.focusedErrorBorder?.borderSide.color), + labelStyle: myController.labelStyle, + floatingLabelStyle: myController.floatingLabelStyle); } return const InputDecoration(); } diff --git a/lib/widget/input/form_textfield.dart b/lib/widget/input/form_textfield.dart index 373e8e4fd..77752c8b2 100644 --- a/lib/widget/input/form_textfield.dart +++ b/lib/widget/input/form_textfield.dart @@ -301,13 +301,9 @@ class TextInputState extends FormFieldWidgetState hintStyle: widget._controller.hintStyle, ); - final showInlineLabel = widget._controller.labelStyle != null && - widget._controller.label != null && - widget._controller.floatLabel == true; - if (showInlineLabel) { + if (widget._controller.floatLabel == true) { decoration = decoration.copyWith( labelText: widget._controller.label, - labelStyle: widget._controller.labelStyle, ); } From 345bd47fc4a9d13e7ef3d7eb89e16be48821256b Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Thu, 19 Oct 2023 18:31:54 +0530 Subject: [PATCH 07/20] fix: optimize widget rebuild for the sidebar menu before clicking the menu item --- lib/framework/view/page_group.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/framework/view/page_group.dart b/lib/framework/view/page_group.dart index 02ee371d9..67a3a4205 100644 --- a/lib/framework/view/page_group.dart +++ b/lib/framework/view/page_group.dart @@ -77,6 +77,7 @@ class PageGroupWidget extends DataScopeWidget { class PageGroupState extends State with MediaQueryCapability { late ScopeManager _scopeManager; + late PageController sidebarPageController; // managing the list of pages List pageWidgets = []; @@ -109,6 +110,10 @@ class PageGroupState extends State with MediaQueryCapability { selectedPage = i; } } + + if (widget.menu is SidebarMenu) { + sidebarPageController = PageController(initialPage: 0); + } } @override @@ -150,7 +155,10 @@ class PageGroupState extends State with MediaQueryCapability { Widget sidebar = _buildSidebar(context, menu); Widget? separator = _buildSidebarSeparator(menu); Widget content = Expanded( - child: IndexedStack(index: selectedPage, children: pageWidgets), + child: PageView( + controller: sidebarPageController, + children: pageWidgets, + ), ); // figuring out the direction to lay things out bool rtlLocale = Directionality.of(context) == TextDirection.rtl; @@ -245,6 +253,7 @@ class PageGroupState extends State with MediaQueryCapability { setState(() { selectedPage = index; }); + sidebarPageController.jumpToPage(index); }, ); } From 46adb129bde182f84e53ec039f5e3dc95ae030f0 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Thu, 19 Oct 2023 19:56:45 +0530 Subject: [PATCH 08/20] feat: add reloadView property for the menu and update sidebar menu --- lib/framework/menu.dart | 43 +++++++++++++++++++++--------- lib/framework/view/page_group.dart | 18 ++++++++----- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/lib/framework/menu.dart b/lib/framework/menu.dart index caf61cbca..8022d4f45 100644 --- a/lib/framework/menu.dart +++ b/lib/framework/menu.dart @@ -7,12 +7,14 @@ import 'package:ensemble/util/utils.dart'; import 'package:yaml/yaml.dart'; abstract class Menu { - Menu(this.menuItems, {this.styles, this.headerModel, this.footerModel}); + Menu(this.menuItems, + {this.styles, this.headerModel, this.footerModel, this.reloadView}); List menuItems; Map? styles; WidgetModel? headerModel; WidgetModel? footerModel; + bool? reloadView = true; static Menu fromYaml( dynamic menu, Map? customViewDefinitions) { @@ -94,8 +96,10 @@ abstract class Menu { } Map? styles = Utils.getMap(payload['styles']); + final isReloadView = payload['reloadView'] as bool? ?? true; if (menuType == MenuDisplay.BottomNavBar) { - return BottomNavBarMenu.fromYaml(menuItems: menuItems, styles: styles); + return BottomNavBarMenu.fromYaml( + menuItems: menuItems, styles: styles, reloadView: isReloadView); } else if (menuType == MenuDisplay.Drawer || menuType == MenuDisplay.EndDrawer) { return DrawerMenu.fromYaml( @@ -103,7 +107,8 @@ abstract class Menu { styles: styles, atStart: menuType != MenuDisplay.EndDrawer, headerModel: menuHeaderModel, - footerModel: menuFooterModel); + footerModel: menuFooterModel, + reloadView: isReloadView); } else if (menuType == MenuDisplay.Sidebar || menuType == MenuDisplay.EndSidebar) { return SidebarMenu.fromYaml( @@ -111,7 +116,8 @@ abstract class Menu { styles: styles, atStart: menuType != MenuDisplay.EndSidebar, headerModel: menuHeaderModel, - footerModel: menuFooterModel); + footerModel: menuFooterModel, + reloadView: isReloadView); } } throw LanguageError("Invalid Menu type.", @@ -120,17 +126,20 @@ abstract class Menu { } class BottomNavBarMenu extends Menu { - BottomNavBarMenu._(super.menuItems, {super.styles}); + BottomNavBarMenu._(super.menuItems, {super.styles, super.reloadView}); factory BottomNavBarMenu.fromYaml( - {required List menuItems, Map? styles}) { - return BottomNavBarMenu._(menuItems, styles: styles); + {required List menuItems, + Map? styles, + bool? reloadView}) { + return BottomNavBarMenu._(menuItems, + styles: styles, reloadView: reloadView); } } class DrawerMenu extends Menu { DrawerMenu._(super.menuItems, this.atStart, - {super.styles, super.headerModel, super.footerModel}); + {super.styles, super.headerModel, super.footerModel, super.reloadView}); // show the drawer at start (left for LTR languages) or at the end bool atStart = true; @@ -139,15 +148,19 @@ class DrawerMenu extends Menu { required bool atStart, Map? styles, WidgetModel? headerModel, - WidgetModel? footerModel}) { + WidgetModel? footerModel, + bool? reloadView}) { return DrawerMenu._(menuItems, atStart, - styles: styles, headerModel: headerModel, footerModel: footerModel); + styles: styles, + headerModel: headerModel, + footerModel: footerModel, + reloadView: reloadView); } } class SidebarMenu extends Menu { SidebarMenu._(super.menuItems, this.atStart, - {super.styles, super.headerModel, super.footerModel}); + {super.styles, super.headerModel, super.footerModel, super.reloadView}); // show the sidebar at start (left for LTR languages) or at the end bool atStart = true; @@ -156,9 +169,13 @@ class SidebarMenu extends Menu { required bool atStart, Map? styles, WidgetModel? headerModel, - WidgetModel? footerModel}) { + WidgetModel? footerModel, + bool? reloadView}) { return SidebarMenu._(menuItems, atStart, - styles: styles, headerModel: headerModel, footerModel: footerModel); + styles: styles, + headerModel: headerModel, + footerModel: footerModel, + reloadView: reloadView); } } diff --git a/lib/framework/view/page_group.dart b/lib/framework/view/page_group.dart index 67a3a4205..e8c4d8b3f 100644 --- a/lib/framework/view/page_group.dart +++ b/lib/framework/view/page_group.dart @@ -77,7 +77,7 @@ class PageGroupWidget extends DataScopeWidget { class PageGroupState extends State with MediaQueryCapability { late ScopeManager _scopeManager; - late PageController sidebarPageController; + PageController? sidebarPageController; // managing the list of pages List pageWidgets = []; @@ -111,7 +111,7 @@ class PageGroupState extends State with MediaQueryCapability { } } - if (widget.menu is SidebarMenu) { + if (widget.menu is SidebarMenu && widget.menu.reloadView == false) { sidebarPageController = PageController(initialPage: 0); } } @@ -155,10 +155,12 @@ class PageGroupState extends State with MediaQueryCapability { Widget sidebar = _buildSidebar(context, menu); Widget? separator = _buildSidebarSeparator(menu); Widget content = Expanded( - child: PageView( - controller: sidebarPageController, - children: pageWidgets, - ), + child: menu.reloadView == true + ? IndexedStack(index: selectedPage, children: pageWidgets) + : PageView( + controller: sidebarPageController, + children: pageWidgets, + ), ); // figuring out the direction to lay things out bool rtlLocale = Directionality.of(context) == TextDirection.rtl; @@ -253,7 +255,9 @@ class PageGroupState extends State with MediaQueryCapability { setState(() { selectedPage = index; }); - sidebarPageController.jumpToPage(index); + if (widget.menu.reloadView == false) { + sidebarPageController?.jumpToPage(index); + } }, ); } From be2f62674c58581b934bfe53e7e9117bb97f68cc Mon Sep 17 00:00:00 2001 From: Vu Le Date: Thu, 19 Oct 2023 18:09:37 -0700 Subject: [PATCH 09/20] refactored Action --- lib/action/action_invokable.dart | 47 +++++++++ lib/action/badge_action.dart | 4 +- lib/action/bottom_modal_action.dart | 4 +- lib/action/call_external_method.dart | 2 +- lib/action/misc_action.dart | 104 +++++++++++++++++++ lib/action/navigation_action.dart | 4 +- lib/action/notification_action.dart | 57 ++++++++++ lib/framework/action.dart | 149 ++++++--------------------- lib/framework/data_context.dart | 48 ++++----- lib/screen_controller.dart | 43 -------- 10 files changed, 270 insertions(+), 192 deletions(-) create mode 100644 lib/action/action_invokable.dart create mode 100644 lib/action/misc_action.dart create mode 100644 lib/action/notification_action.dart diff --git a/lib/action/action_invokable.dart b/lib/action/action_invokable.dart new file mode 100644 index 000000000..d4150a558 --- /dev/null +++ b/lib/action/action_invokable.dart @@ -0,0 +1,47 @@ +import 'package:ensemble/action/call_external_method.dart'; +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/error_handling.dart'; +import 'package:ensemble/framework/scope.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; +import 'package:flutter/cupertino.dart'; + +/// expose Ensemble Actions as Invokables +abstract class ActionInvokable with Invokable { + ActionInvokable(this.buildContext); + final BuildContext buildContext; + + @override + Map methods() { + return _generateFromActionTypes([ + ActionType.callExternalMethod, + ActionType.share, + ActionType.rateApp, + ActionType.copyToClipboard, + ActionType.getDeviceToken + ]); + } + + Map _generateFromActionTypes(List actionTypes) { + Map functions = {}; + for (ActionType actionType in actionTypes) { + functions[actionType.name] = (payload) { + if (payload != null && payload is! Map) { + throw LanguageError("${actionType.name} has an invalid payload."); + } + EnsembleAction? action = EnsembleAction.fromActionType( + actionType, payload: payload); + return action?.execute(buildContext, _getScopeManager(buildContext)); + }; + } + return functions; + } + + ScopeManager _getScopeManager(BuildContext context) { + ScopeManager? scopeManager = ScreenController().getScopeManager(context); + if (scopeManager == null) { + throw LanguageError("Cannot look up ScopeManager"); + } + return scopeManager; + } +} diff --git a/lib/action/badge_action.dart b/lib/action/badge_action.dart index de291335f..79ca7a7e4 100644 --- a/lib/action/badge_action.dart +++ b/lib/action/badge_action.dart @@ -19,7 +19,7 @@ class UpdateBadgeCount extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager) { int? count = Utils.optionalInt(scopeManager.dataContext.eval(_count)); if (count != null) { return FlutterAppBadger.updateBadgeCount(count); @@ -30,7 +30,7 @@ class UpdateBadgeCount extends EnsembleAction { class ClearBadgeCount extends EnsembleAction { @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager) { return FlutterAppBadger.removeBadge(); } } diff --git a/lib/action/bottom_modal_action.dart b/lib/action/bottom_modal_action.dart index 11229d4e2..cd9756aa6 100644 --- a/lib/action/bottom_modal_action.dart +++ b/lib/action/bottom_modal_action.dart @@ -59,7 +59,7 @@ class ShowBottomModalAction extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager) { Widget? widget; if (body != null) { widget = scopeManager.buildWidgetFromDefinition(body); @@ -96,7 +96,7 @@ class DismissBottomModalAction extends EnsembleAction { DismissBottomModalAction(payload: payload?['payload']); @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager) { BuildContext? bottomModalContext = ContextScopeWidget.getRootContext(context); if (bottomModalContext != null) { diff --git a/lib/action/call_external_method.dart b/lib/action/call_external_method.dart index 1a0026ce3..c7247c756 100644 --- a/lib/action/call_external_method.dart +++ b/lib/action/call_external_method.dart @@ -32,7 +32,7 @@ class CallExternalMethod extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) async { + Future execute(BuildContext context, ScopeManager scopeManager) async { String? name = Utils.optionalString(scopeManager.dataContext.eval(_name)); String? errorReason; diff --git a/lib/action/misc_action.dart b/lib/action/misc_action.dart new file mode 100644 index 000000000..bf6eea916 --- /dev/null +++ b/lib/action/misc_action.dart @@ -0,0 +1,104 @@ +import 'dart:io'; + +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/error_handling.dart'; +import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/framework/scope.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble/util/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:rate_my_app/rate_my_app.dart'; +import 'package:share_plus/share_plus.dart'; + +class CopyToClipboardAction extends EnsembleAction { + CopyToClipboardAction(this._value, + {super.initiator, this.onSuccess, this.onFailure}); + + dynamic _value; + EnsembleAction? onSuccess; + EnsembleAction? onFailure; + + factory CopyToClipboardAction.from({Map? payload}) { + if (payload == null || payload['value'] == null) { + throw LanguageError( + '${ActionType.copyToClipboard.name} requires the value.'); + } + return CopyToClipboardAction( + payload['value'], + onSuccess: EnsembleAction.fromYaml(payload['onSuccess']), + onFailure: EnsembleAction.fromYaml(payload['onFailure']), + ); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) { + String? value = Utils.optionalString(scopeManager.dataContext.eval(_value)); + if (value != null) { + Clipboard.setData(ClipboardData(text: value)).then((_) { + if (onSuccess != null) { + ScreenController().executeAction(context, onSuccess!, + event: EnsembleEvent(initiator)); + } + }).catchError((_) { + if (onFailure != null) { + ScreenController().executeAction(context, onFailure!, + event: EnsembleEvent(initiator)); + } + }); + } else { + if (onFailure != null) { + ScreenController().executeAction(context, onFailure!, + event: EnsembleEvent(initiator)); + } + } + return Future.value(null); + } +} + +/// Share a text (an optionally a title) to external Apps +class ShareAction extends EnsembleAction { + ShareAction(this._text, {String? title}) : _title = title; + String? _title; + dynamic _text; + + factory ShareAction.from({Map? payload}) { + if (payload == null || payload['text'] == null) { + throw LanguageError("${ActionType.share.name} requires 'text'"); + } + return ShareAction(payload['text'], title: payload['title']?.toString()); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) { + Share.share(scopeManager.dataContext.eval(_text), + subject: Utils.optionalString(scopeManager.dataContext.eval(_title))); + return Future.value(null); + } +} + +/// Rate an App (currently only works for iOS) +class RateAppAction extends EnsembleAction { + RateAppAction({dynamic title, dynamic message}) + : _title = title, + _message = message; + + // not exposed yet + final dynamic _title; + final dynamic _message; + + factory RateAppAction.from({Map? payload}) { + return RateAppAction( + title: payload?['title'], message: payload?['message']); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) { + // what a mess of options on Android. TODO: add them + if (Platform.isIOS) { + RateMyApp rateMyApp = RateMyApp(minDays: 0, minLaunches: 0); + rateMyApp.init().then((_) => rateMyApp.showStarRateDialog(context)); + } + return Future.value(null); + } +} diff --git a/lib/action/navigation_action.dart b/lib/action/navigation_action.dart index fbbf88ba7..f8ee4441d 100644 --- a/lib/action/navigation_action.dart +++ b/lib/action/navigation_action.dart @@ -25,7 +25,7 @@ class NavigateExternalScreen extends BaseNavigateScreenAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager) { // payload Map? payload; if (inputs != null) { @@ -58,7 +58,7 @@ class NavigateBackAction extends EnsembleAction { NavigateBackAction(payload: payload?['payload'] ?? payload?['data']); @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager) { return Navigator.of(context) .maybePop(scopeManager.dataContext.eval(payload)); } diff --git a/lib/action/notification_action.dart b/lib/action/notification_action.dart new file mode 100644 index 000000000..16d98f444 --- /dev/null +++ b/lib/action/notification_action.dart @@ -0,0 +1,57 @@ + +import 'dart:developer'; + +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/error_handling.dart'; +import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/framework/scope.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/cupertino.dart'; + +class GetDeviceTokenAction extends EnsembleAction { + GetDeviceTokenAction({super.initiator, required this.onSuccess, this.onError}); + + EnsembleAction? onSuccess; + EnsembleAction? onError; + + factory GetDeviceTokenAction.fromMap({dynamic payload}) { + if (payload is Map) { + EnsembleAction? successAction = + EnsembleAction.fromYaml(payload['onSuccess']); + if (successAction == null) { + throw LanguageError("onSuccess() is required for Get Token Action"); + } + return GetDeviceTokenAction( + onSuccess: successAction, + onError: EnsembleAction.fromYaml(payload['onError'])); + } + throw LanguageError("Missing inputs for getDeviceToken.}"); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) async { + String? deviceToken; + try { + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + ); + // need to get APNS first + await FirebaseMessaging.instance.getAPNSToken(); + // then get device token + deviceToken = await FirebaseMessaging.instance.getToken(); + if (deviceToken != null && onSuccess != null) { + return ScreenController().executeAction(context, onSuccess!, + event: EnsembleEvent(initiator, data: {'token': deviceToken})); + } + } on Exception catch (e) { + log(e.toString()); + log('Error getting device token'); + } + if (deviceToken == null && onError != null) { + return ScreenController().executeAction(context, onError!); + } + } +} \ No newline at end of file diff --git a/lib/framework/action.dart b/lib/framework/action.dart index c3230c5ec..598873d2f 100644 --- a/lib/framework/action.dart +++ b/lib/framework/action.dart @@ -4,6 +4,7 @@ import 'package:app_settings/app_settings.dart'; import 'package:ensemble/action/badge_action.dart'; import 'package:ensemble/action/bottom_modal_action.dart'; import 'package:ensemble/action/call_external_method.dart'; +import 'package:ensemble/action/misc_action.dart'; import 'package:ensemble/action/navigation_action.dart'; import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; @@ -18,6 +19,7 @@ import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:rate_my_app/rate_my_app.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:source_span/source_span.dart'; import 'package:yaml/yaml.dart'; @@ -36,7 +38,7 @@ class InvokeAPIAction extends EnsembleAction { EnsembleAction? onResponse; EnsembleAction? onError; - factory InvokeAPIAction.fromYaml({Invokable? initiator, YamlMap? payload}) { + factory InvokeAPIAction.fromYaml({Invokable? initiator, Map? payload}) { if (payload == null || payload['name'] == null) { throw LanguageError( "${ActionType.invokeAPI.name} requires the 'name' of the API."); @@ -69,7 +71,7 @@ class ShowCameraAction extends EnsembleAction { EnsembleAction? onClose; EnsembleAction? onCapture; - factory ShowCameraAction.fromYaml({Invokable? initiator, YamlMap? payload}) { + factory ShowCameraAction.fromYaml({Invokable? initiator, Map? payload}) { return ShowCameraAction( initiator: initiator, options: Utils.getMap(payload?['options']), @@ -95,7 +97,7 @@ class ShowDialogAction extends EnsembleAction { final Map? options; final EnsembleAction? onDialogDismiss; - factory ShowDialogAction.fromYaml({Invokable? initiator, YamlMap? payload}) { + factory ShowDialogAction.fromYaml({Invokable? initiator, Map? payload}) { if (payload == null || payload['widget'] == null) { throw LanguageError( "${ActionType.showDialog.name} requires the 'widget' for the Dialog's content."); @@ -121,8 +123,7 @@ class NavigateScreenAction extends BaseNavigateScreenAction { : super(asModal: false); EnsembleAction? onNavigateBack; - factory NavigateScreenAction.fromYaml( - {Invokable? initiator, YamlMap? payload}) { + factory NavigateScreenAction.fromYaml({Invokable? initiator, Map? payload}) { if (payload == null || payload['name'] == null) { throw LanguageError( "${ActionType.navigateScreen.name} requires the 'name' of the screen to navigate to."); @@ -157,7 +158,7 @@ class NavigateModalScreenAction extends BaseNavigateScreenAction { EnsembleAction? onModalDismiss; factory NavigateModalScreenAction.fromYaml( - {Invokable? initiator, YamlMap? payload}) { + {Invokable? initiator, Map? payload}) { if (payload == null || payload['name'] == null) { throw LanguageError( "${ActionType.navigateModalScreen.name} requires the 'name' of the screen to navigate to."); @@ -204,7 +205,7 @@ class PlaidLinkAction extends EnsembleAction { String getLinkToken(dataContext) => Utils.getString(dataContext.eval(linkToken), fallback: ''); - factory PlaidLinkAction.fromYaml({Invokable? initiator, YamlMap? payload}) { + factory PlaidLinkAction.fromYaml({Invokable? initiator, Map? payload}) { if (payload == null || payload['linkToken'] == null) { throw LanguageError( "${ActionType.openPlaidLink.name} action requires the plaid's link_token"); @@ -232,7 +233,7 @@ class AppSettingAction extends EnsembleAction { AppSettingsType.values.from(dataContext.eval(target)) ?? AppSettingsType.settings; - factory AppSettingAction.fromYaml({Invokable? initiator, YamlMap? payload}) { + factory AppSettingAction.fromYaml({Invokable? initiator, Map? payload}) { return AppSettingAction( initiator: initiator, target: Utils.getString(payload?['target'], fallback: 'settings'), @@ -258,8 +259,7 @@ class PhoneContactAction extends EnsembleAction { EnsembleAction? getOnError(DataContext dataContext) => dataContext.eval(onError); - factory PhoneContactAction.fromYaml( - {Invokable? initiator, YamlMap? payload}) { + factory PhoneContactAction.fromYaml({Invokable? initiator, Map? payload}) { if (payload == null) { throw LanguageError( "${ActionType.getPhoneContacts.name} action requires payload"); @@ -308,7 +308,7 @@ class StartTimerAction extends EnsembleAction { bool? isGlobal(dataContext) => Utils.optionalBool(dataContext.eval(_options?['isGlobal'])); - factory StartTimerAction.fromYaml({Invokable? initiator, YamlMap? payload}) { + factory StartTimerAction.fromYaml({Invokable? initiator, Map? payload}) { EnsembleAction? onTimer = EnsembleAction.fromYaml(payload?['onTimer'], initiator: initiator); if (payload == null || onTimer == null) { @@ -336,7 +336,7 @@ class StopTimerAction extends EnsembleAction { String id; - factory StopTimerAction.fromYaml({YamlMap? payload}) { + factory StopTimerAction.fromYaml({Map? payload}) { if (payload?['id'] == null) { throw LanguageError( "${ActionType.stopTimer.name} requires a timer Id to stop."); @@ -360,7 +360,7 @@ class ExecuteCodeAction extends EnsembleAction { EnsembleAction? onComplete; SourceSpan codeBlockSpan; - factory ExecuteCodeAction.fromYaml({Invokable? initiator, YamlMap? payload}) { + factory ExecuteCodeAction.fromYaml({Invokable? initiator, Map? payload}) { if (payload == null || payload['body'] == null) { throw LanguageError( "${ActionType.executeCode.name} requires a 'body' code block."); @@ -371,7 +371,8 @@ class ExecuteCodeAction extends EnsembleAction { codeBlock: payload['body'].toString(), onComplete: EnsembleAction.fromYaml(payload['onComplete'], initiator: initiator), - codeBlockSpan: ViewUtil.optDefinition(payload.nodes['body'])); + codeBlockSpan: + ViewUtil.optDefinition((payload as YamlMap).nodes['body'])); } } @@ -381,7 +382,7 @@ class OpenUrlAction extends EnsembleAction { String url; bool openInExternalApp; - factory OpenUrlAction.fromYaml({YamlMap? payload}) { + factory OpenUrlAction.fromYaml({Map? payload}) { if (payload == null || payload['url'] == null) { throw LanguageError("${ActionType.openUrl.name} requires the 'url'."); } @@ -419,7 +420,7 @@ class ShowToastAction extends EnsembleAction { final int? duration; // the during in seconds before toast is dismissed final Map? styles; - factory ShowToastAction.fromYaml({YamlMap? payload}) { + factory ShowToastAction.fromYaml({Map? payload}) { if (payload == null || (payload['message'] == null && payload['widget'] == null)) { throw LanguageError( @@ -471,7 +472,7 @@ class FilePickerAction extends EnsembleAction { EnsembleAction? onComplete; EnsembleAction? onError; - factory FilePickerAction.fromYaml({YamlMap? payload}) { + factory FilePickerAction.fromYaml({Map? payload}) { if (payload == null || payload['id'] == null) { throw LanguageError("${ActionType.pickFiles.name} requires 'id'."); } @@ -518,7 +519,7 @@ class FileUploadAction extends EnsembleAction { bool? requiresBatteryNotLow; bool showNotification; - factory FileUploadAction.fromYaml({YamlMap? payload}) { + factory FileUploadAction.fromYaml({Map? payload}) { if (payload == null || payload['uploadApi'] == null) { throw LanguageError("${ActionType.uploadFiles.name} requires ' '."); } @@ -547,73 +548,7 @@ class FileUploadAction extends EnsembleAction { } } -class CopyToClipboardAction extends EnsembleAction { - CopyToClipboardAction({ - this.value, - this.onSuccess, - this.onFailure, - }); - - dynamic value; - EnsembleAction? onSuccess; - EnsembleAction? onFailure; - String? getValue(DataContext dataContext) => - Utils.optionalString(dataContext.eval(value)); - - factory CopyToClipboardAction.fromYaml({YamlMap? payload}) { - if (payload == null || payload['value'] == null) { - throw LanguageError( - '${ActionType.copyToClipboard.name} requires the value.'); - } - return CopyToClipboardAction( - value: payload['value'], - onSuccess: EnsembleAction.fromYaml(payload['onSuccess']), - onFailure: EnsembleAction.fromYaml(payload['onFailure']), - ); - } -} - -class ShareAction extends EnsembleAction { - ShareAction(this._text, {String? title}) : _title = title; - String? _title; - dynamic _text; - - dynamic getText(DataContext dataContext) => dataContext.eval(_text); - - String? getTitle(DataContext datContext) => - Utils.optionalString(datContext.eval(_title)); - - factory ShareAction.from({Map? payload}) { - if (payload == null || payload['text'] == null) { - throw LanguageError("${ActionType.share.name} requires 'text'"); - } - return ShareAction(payload['text'], title: payload['title']?.toString()); - } -} - -class RateAppAction extends EnsembleAction { - RateAppAction({dynamic title, dynamic message}) - : _title = title, - _message = message; - final dynamic _title; - final dynamic _message; - - factory RateAppAction.from({Map? payload}) { - return RateAppAction( - title: payload?['title'], message: payload?['message']); - } - - @override - Future execute(BuildContext context, ScopeManager scopeManager) { - // what a mess of options on Android. TODO: add them - if (Platform.isIOS) { - RateMyApp rateMyApp = RateMyApp(minDays: 0, minLaunches: 0); - rateMyApp.init().then((_) => rateMyApp.showStarRateDialog(context)); - } - return Future.value(null); - } -} class WalletConnectAction extends EnsembleAction { WalletConnectAction({ @@ -636,7 +571,7 @@ class WalletConnectAction extends EnsembleAction { EnsembleAction? onComplete; EnsembleAction? onError; - factory WalletConnectAction.fromYaml({YamlMap? payload}) { + factory WalletConnectAction.fromYaml({Map? payload}) { if (payload == null || (payload['wcProjectId'] == null || payload['appMetaData']?['name'] == null)) { @@ -665,7 +600,7 @@ class AuthorizeOAuthAction extends EnsembleAction { EnsembleAction? onResponse; EnsembleAction? onError; - factory AuthorizeOAuthAction.fromYaml({YamlMap? payload}) { + factory AuthorizeOAuthAction.fromYaml({Map? payload}) { if (payload == null || payload['id'] == null) { throw LanguageError( '${ActionType.authorizeOAuthService.name} requires the service ID.'); @@ -684,8 +619,7 @@ class NotificationAction extends EnsembleAction { EnsembleAction? onTap; EnsembleAction? onReceive; - factory NotificationAction.fromYaml( - {Invokable? initiator, YamlMap? payload}) { + factory NotificationAction.fromYaml({Invokable? initiator, Map? payload}) { return NotificationAction( onTap: EnsembleAction.fromYaml(payload?['onTap']), onReceive: EnsembleAction.fromYaml(payload?['onReceive']), @@ -693,27 +627,6 @@ class NotificationAction extends EnsembleAction { } } -class GetDeviceTokenAction extends EnsembleAction { - GetDeviceTokenAction({required this.onSuccess, this.onError}); - - EnsembleAction? onSuccess; - EnsembleAction? onError; - - factory GetDeviceTokenAction.fromMap({dynamic payload}) { - if (payload is Map) { - EnsembleAction? successAction = - EnsembleAction.fromYaml(payload['onSuccess']); - if (successAction == null) { - throw LanguageError("onSuccess() is required for Get Token Action"); - } - return GetDeviceTokenAction( - onSuccess: successAction, - onError: EnsembleAction.fromYaml(payload['onError'])); - } - throw LanguageError("Missing inputs for getDeviceToken.}"); - } -} - class RequestNotificationAction extends EnsembleAction { EnsembleAction? onAccept; EnsembleAction? onReject; @@ -721,7 +634,7 @@ class RequestNotificationAction extends EnsembleAction { RequestNotificationAction({this.onAccept, this.onReject}); factory RequestNotificationAction.fromYaml( - {Invokable? initiator, YamlMap? payload}) { + {Invokable? initiator, Map? payload}) { return RequestNotificationAction( onAccept: EnsembleAction.fromYaml(payload?['onAccept']), onReject: EnsembleAction.fromYaml(payload?['onReject']), @@ -735,7 +648,7 @@ class ShowNotificationAction extends EnsembleAction { ShowNotificationAction({this.title = '', this.body = ''}); - factory ShowNotificationAction.fromYaml({YamlMap? payload}) { + factory ShowNotificationAction.fromYaml({Map? payload}) { return ShowNotificationAction( title: Utils.getString(payload?['title'], fallback: ''), body: Utils.getString(payload?['body'], fallback: ''), @@ -755,7 +668,7 @@ class ConnectSocketAction extends EnsembleAction { Map? inputs, }) : super(inputs: inputs); - factory ConnectSocketAction.fromYaml({YamlMap? payload}) { + factory ConnectSocketAction.fromYaml({Map? payload}) { if (payload == null || payload['name'] == null) { throw ConfigError('connectSocket requires a name'); } @@ -773,7 +686,7 @@ class DisconnectSocketAction extends EnsembleAction { DisconnectSocketAction({required this.name}); - factory DisconnectSocketAction.fromYaml({YamlMap? payload}) { + factory DisconnectSocketAction.fromYaml({Map? payload}) { if (payload == null || payload['name'] == null) { throw ConfigError('disconnectSocket requires a name'); } @@ -789,7 +702,7 @@ class MessageSocketAction extends EnsembleAction { MessageSocketAction({required this.name, required this.message}); - factory MessageSocketAction.fromYaml({YamlMap? payload}) { + factory MessageSocketAction.fromYaml({Map? payload}) { if (payload == null || payload['name'] == null) { throw ConfigError('messageSocket requires a name'); } @@ -816,7 +729,7 @@ class CheckPermission extends EnsembleAction { Permission? getType(DataContext dataContext) => Permission.values.from(dataContext.eval(_type)); - factory CheckPermission.fromYaml({YamlMap? payload}) { + factory CheckPermission.fromYaml({Map? payload}) { if (payload == null || payload['type'] == null) { throw ConfigError('checkPermission requires a type.'); } @@ -882,7 +795,7 @@ abstract class EnsembleAction { Map? inputs; /// TODO: each Action does all the execution in here - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager) { // placeholder until all Actions are implemented return Future.value(null); } @@ -912,7 +825,7 @@ abstract class EnsembleAction { } static EnsembleAction? fromActionType(ActionType actionType, - {Invokable? initiator, YamlMap? payload}) { + {Invokable? initiator, Map? payload}) { if (actionType == ActionType.navigateScreen) { return NavigateScreenAction.fromYaml( initiator: initiator, payload: payload); @@ -972,7 +885,7 @@ abstract class EnsembleAction { } else if (actionType == ActionType.requestNotificationAccess) { return RequestNotificationAction.fromYaml(payload: payload); } else if (actionType == ActionType.copyToClipboard) { - return CopyToClipboardAction.fromYaml(payload: payload); + return CopyToClipboardAction.from(payload: payload); } else if (actionType == ActionType.share) { return ShareAction.from(payload: payload); } else if (actionType == ActionType.rateApp) { diff --git a/lib/framework/data_context.dart b/lib/framework/data_context.dart index 58c469b5c..14c4a355c 100644 --- a/lib/framework/data_context.dart +++ b/lib/framework/data_context.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io' as io; import 'dart:ui'; +import 'package:ensemble/action/action_invokable.dart'; +import 'package:ensemble/action/call_external_method.dart'; import 'package:ensemble/action/navigation_action.dart'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/config.dart'; @@ -310,25 +312,25 @@ class DataContext { } /// built-in helpers/utils accessible to all DataContext -class NativeInvokable with Invokable { - final BuildContext _buildContext; - - NativeInvokable(this._buildContext); +class NativeInvokable extends ActionInvokable { + NativeInvokable(super.buildContext); @override Map getters() { return { - 'storage': () => EnsembleStorage(_buildContext), + 'storage': () => EnsembleStorage(buildContext), 'user': () => UserInfo(), - 'formatter': () => Formatter(_buildContext), + 'formatter': () => Formatter(buildContext), }; } @override Map methods() { - return { + // see super method for Actions already exposed there + Map methods = super.methods(); + methods.addAll({ ActionType.navigateScreen.name: (inputs) => ScreenController() - .executeAction(_buildContext, NavigateScreenAction.fromMap(inputs)), + .executeAction(buildContext, NavigateScreenAction.fromMap(inputs)), ActionType.navigateModalScreen.name: navigateToModalScreen, ActionType.showDialog.name: showDialog, ActionType.invokeAPI.name: invokeAPI, @@ -337,14 +339,11 @@ class NativeInvokable with Invokable { ActionType.openCamera.name: showCamera, ActionType.navigateBack.name: navigateBack, ActionType.showToast.name: (inputs) => ScreenController() - .executeAction(_buildContext, ShowToastAction.fromMap(inputs)), + .executeAction(buildContext, ShowToastAction.fromMap(inputs)), ActionType.startTimer.name: (inputs) => ScreenController() - .executeAction(_buildContext, StartTimerAction.fromMap(inputs)), + .executeAction(buildContext, StartTimerAction.fromMap(inputs)), ActionType.uploadFiles.name: uploadFiles, 'debug': (value) => debugPrint('Debug: $value'), - 'copyToClipboard': (value) => - Clipboard.setData(ClipboardData(text: value)), - ActionType.share.name: (payload) => ShareAction.from(payload: payload), 'initNotification': () => notificationUtils.initNotifications(), 'updateSystemAuthorizationToken': (token) => GetIt.instance() @@ -353,17 +352,18 @@ class NativeInvokable with Invokable { saveToKeychain(key, value), ActionType.clearKeychain.name: (key) => clearKeychain(key), 'connectSocket': (String socketName, Map? inputs) { - connectSocket(_buildContext, socketName, inputs: inputs); + connectSocket(buildContext, socketName, inputs: inputs); }, 'disconnectSocket': (String socketName) { disconnectSocket(socketName); }, 'messageSocket': (String socketName, dynamic message) { - final scope = ScreenController().getScopeManager(_buildContext); + final scope = ScreenController().getScopeManager(buildContext); final evalMessage = scope?.dataContext.eval(message); messageSocket(socketName, evalMessage); }, - }; + }); + return methods; } @override @@ -422,47 +422,47 @@ class NativeInvokable with Invokable { Map? inputMap = Utils.getMap(inputs); if (inputMap == null) throw LanguageError('UploadFiles need inputs'); ScreenController().executeAction( - _buildContext, + buildContext, FileUploadAction.fromYaml(payload: YamlMap.wrap(inputMap)), ); } void navigateToModalScreen(String screenName, [dynamic inputs]) { Map? inputMap = Utils.getMap(inputs); - ScreenController().navigateToScreen(_buildContext, + ScreenController().navigateToScreen(buildContext, screenName: screenName, pageArgs: inputMap, asModal: true); // how do we handle onModalDismiss in Typescript? } void showDialog(dynamic widget) { ScreenController() - .executeAction(_buildContext, ShowDialogAction(widget: widget)); + .executeAction(buildContext, ShowDialogAction(widget: widget)); } void openUrl([dynamic inputs]) { Map? inputMap = Utils.getMap(inputs); inputMap ??= {}; ScreenController() - .executeAction(_buildContext, OpenUrlAction.fromMap(inputMap)); + .executeAction(buildContext, OpenUrlAction.fromMap(inputMap)); } void invokeAPI(String apiName, [dynamic inputs]) { Map? inputMap = Utils.getMap(inputs); ScreenController().executeAction( - _buildContext, InvokeAPIAction(apiName: apiName, inputs: inputMap)); + buildContext, InvokeAPIAction(apiName: apiName, inputs: inputMap)); } void stopTimer(String timerId) { - ScreenController().executeAction(_buildContext, StopTimerAction(timerId)); + ScreenController().executeAction(buildContext, StopTimerAction(timerId)); } void showCamera() { - ScreenController().executeAction(_buildContext, ShowCameraAction()); + ScreenController().executeAction(buildContext, ShowCameraAction()); } void navigateBack([dynamic payload]) { ScreenController().executeAction( - _buildContext, NavigateBackAction.from(payload: payload)); + buildContext, NavigateBackAction.from(payload: payload)); } } diff --git a/lib/screen_controller.dart b/lib/screen_controller.dart index 4f1e15566..5cd2e1e92 100644 --- a/lib/screen_controller.dart +++ b/lib/screen_controller.dart @@ -474,49 +474,6 @@ class ScreenController { scopeManager: scopeManager); } else if (action is FilePickerAction) { GetIt.I().pickFiles(context, action, scopeManager); - } else if (action is CopyToClipboardAction) { - if (action.value != null) { - String? clipboardValue = action.getValue(dataContext); - if (clipboardValue != null) { - Clipboard.setData(ClipboardData(text: clipboardValue)).then((value) { - if (action.onSuccess != null) { - executeAction(context, action.onSuccess!); - } - }).catchError((_) { - if (action.onFailure != null) { - executeAction(context, action.onFailure!); - } - }); - } - } else { - if (action.onFailure != null) executeAction(context, action.onFailure!); - } - } else if (action is ShareAction) { - Share.share(action.getText(dataContext), - subject: action.getTitle(dataContext)); - } else if (action is GetDeviceTokenAction) { - String? deviceToken; - try { - await FirebaseMessaging.instance.requestPermission( - alert: true, - badge: true, - sound: true, - ); - // need to get APNS first - await FirebaseMessaging.instance.getAPNSToken(); - // then get device token - deviceToken = await FirebaseMessaging.instance.getToken(); - if (deviceToken != null && action.onSuccess != null) { - return ScreenController().executeAction(context, action.onSuccess!, - event: EnsembleEvent(null, data: {'token': deviceToken})); - } - } on Exception catch (e) { - log(e.toString()); - log('Error getting device token'); - } - if (deviceToken == null && action.onError != null) { - return ScreenController().executeAction(context, action.onError!); - } } else if (action is WalletConnectAction) { // TODO store session: WalletConnectSession? session = await sessionStorage.getSession(); From 7aca172150c4cd9bf0b4aec7376ff6ad83f4ed8e Mon Sep 17 00:00:00 2001 From: Vu Le Date: Thu, 19 Oct 2023 20:58:04 -0700 Subject: [PATCH 10/20] update InvokeAPIAction --- lib/action/invoke_api_action.dart | 44 ++++++++++++++++++++++++ lib/framework/action.dart | 35 ++----------------- lib/framework/data_context.dart | 1 + lib/framework/stub/oauth_controller.dart | 1 + lib/screen_controller.dart | 5 +-- 5 files changed, 49 insertions(+), 37 deletions(-) diff --git a/lib/action/invoke_api_action.dart b/lib/action/invoke_api_action.dart index 71606ce9b..617cfc91e 100644 --- a/lib/action/invoke_api_action.dart +++ b/lib/action/invoke_api_action.dart @@ -13,6 +13,50 @@ import 'package:flutter/cupertino.dart'; import 'package:yaml/yaml.dart'; import 'package:http/http.dart' as http; +class InvokeAPIAction extends EnsembleAction { + InvokeAPIAction( + {Invokable? initiator, + required this.apiName, + this.id, + Map? inputs, + this.onResponse, + this.onError}) + : super(initiator: initiator, inputs: inputs); + + String? id; + final String apiName; + EnsembleAction? onResponse; + EnsembleAction? onError; + + factory InvokeAPIAction.fromYaml({Invokable? initiator, Map? payload}) { + if (payload == null || payload['name'] == null) { + throw LanguageError( + "${ActionType.invokeAPI.name} requires the 'name' of the API."); + } + + return InvokeAPIAction( + initiator: initiator, + apiName: payload['name'], + id: Utils.optionalString(payload['id']), + inputs: Utils.getMap(payload['inputs']), + onResponse: EnsembleAction.fromYaml(payload['onResponse'], + initiator: initiator), + onError: + EnsembleAction.fromYaml(payload['onError'], initiator: initiator)); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) { + var evalApiName = scopeManager.dataContext.eval(apiName); + var cloneAction = InvokeAPIAction(apiName: evalApiName, initiator: initiator, id: id, inputs: inputs, onResponse: onResponse, onError: onError); + return InvokeAPIController() + .execute(cloneAction, context, scopeManager.dataContext, scopeManager, + scopeManager.pageData.apiMap); + } +} + + + class InvokeAPIController { Future executeWithContext( BuildContext context, InvokeAPIAction action, diff --git a/lib/framework/action.dart b/lib/framework/action.dart index 598873d2f..d859c405a 100644 --- a/lib/framework/action.dart +++ b/lib/framework/action.dart @@ -4,8 +4,10 @@ import 'package:app_settings/app_settings.dart'; import 'package:ensemble/action/badge_action.dart'; import 'package:ensemble/action/bottom_modal_action.dart'; import 'package:ensemble/action/call_external_method.dart'; +import 'package:ensemble/action/invoke_api_action.dart'; import 'package:ensemble/action/misc_action.dart'; import 'package:ensemble/action/navigation_action.dart'; +import 'package:ensemble/action/notification_action.dart'; import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/event.dart'; @@ -23,39 +25,6 @@ import 'package:share_plus/share_plus.dart'; import 'package:source_span/source_span.dart'; import 'package:yaml/yaml.dart'; -class InvokeAPIAction extends EnsembleAction { - InvokeAPIAction( - {Invokable? initiator, - required this.apiName, - this.id, - Map? inputs, - this.onResponse, - this.onError}) - : super(initiator: initiator, inputs: inputs); - - String? id; - final String apiName; - EnsembleAction? onResponse; - EnsembleAction? onError; - - factory InvokeAPIAction.fromYaml({Invokable? initiator, Map? payload}) { - if (payload == null || payload['name'] == null) { - throw LanguageError( - "${ActionType.invokeAPI.name} requires the 'name' of the API."); - } - - return InvokeAPIAction( - initiator: initiator, - apiName: payload['name'], - id: Utils.optionalString(payload['id']), - inputs: Utils.getMap(payload['inputs']), - onResponse: EnsembleAction.fromYaml(payload['onResponse'], - initiator: initiator), - onError: - EnsembleAction.fromYaml(payload['onError'], initiator: initiator)); - } -} - class ShowCameraAction extends EnsembleAction { ShowCameraAction({ Invokable? initiator, diff --git a/lib/framework/data_context.dart b/lib/framework/data_context.dart index 14c4a355c..a75b4f622 100644 --- a/lib/framework/data_context.dart +++ b/lib/framework/data_context.dart @@ -5,6 +5,7 @@ import 'dart:io' as io; import 'dart:ui'; import 'package:ensemble/action/action_invokable.dart'; import 'package:ensemble/action/call_external_method.dart'; +import 'package:ensemble/action/invoke_api_action.dart'; import 'package:ensemble/action/navigation_action.dart'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/config.dart'; diff --git a/lib/framework/stub/oauth_controller.dart b/lib/framework/stub/oauth_controller.dart index 82c776b8b..8c85ae583 100644 --- a/lib/framework/stub/oauth_controller.dart +++ b/lib/framework/stub/oauth_controller.dart @@ -1,3 +1,4 @@ +import 'package:ensemble/action/invoke_api_action.dart'; import 'package:ensemble/framework/action.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/stub/token_manager.dart'; diff --git a/lib/screen_controller.dart b/lib/screen_controller.dart index 5cd2e1e92..b8f946543 100644 --- a/lib/screen_controller.dart +++ b/lib/screen_controller.dart @@ -147,10 +147,7 @@ class ScreenController { dataContext.addInvokableContext('event', event); } - if (action is InvokeAPIAction) { - await InvokeAPIController() - .execute(action, context, dataContext, scopeManager, apiMap); - } else if (action is NavigateExternalScreen) { + if (action is NavigateExternalScreen) { return action.execute(context, scopeManager!); } else if (action is BaseNavigateScreenAction) { // process input parameters From 853fa877dbc55a76a467246eb1b33f630c8bd2a0 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 20 Oct 2023 10:45:35 +0530 Subject: [PATCH 11/20] refactor(schema): add description to the floatingLabelStyle property --- assets/schema/ensemble_schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/schema/ensemble_schema.json b/assets/schema/ensemble_schema.json index 843d53f6a..b40c6a992 100644 --- a/assets/schema/ensemble_schema.json +++ b/assets/schema/ensemble_schema.json @@ -2871,7 +2871,8 @@ "description": "Moves the label on top of the Input Field. Default (False)." }, "floatingLabelStyle": { - "$ref": "#/$defs/TextStyle" + "$ref": "#/$defs/TextStyle", + "description": "Set the label's styles when it is in floating mode" }, "labelStyle": { "$ref": "#/$defs/TextStyle" From 4c77170778cbe0f3371943d528a0e162d7278aed Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 20 Oct 2023 10:47:01 +0530 Subject: [PATCH 12/20] fix: dart format --- lib/deep_link_manager.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/deep_link_manager.dart b/lib/deep_link_manager.dart index 9195054a8..c8b1e5779 100644 --- a/lib/deep_link_manager.dart +++ b/lib/deep_link_manager.dart @@ -15,7 +15,9 @@ class DeepLinkNavigator { ?.toString(); if (screenId != null || screenName != null) { ScreenController().navigateToScreen(Utils.globalAppKey.currentContext!, - screenId: screenId, screenName: screenName, pageArgs: uri.queryParameters); + screenId: screenId, + screenName: screenName, + pageArgs: uri.queryParameters); } } } From c187b894eba02320d5a9aa47fa998b847236f9e2 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 20 Oct 2023 10:54:46 +0530 Subject: [PATCH 13/20] refactor (schema): add description for labelStyle --- assets/schema/ensemble_schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/schema/ensemble_schema.json b/assets/schema/ensemble_schema.json index b40c6a992..14bbbf5b1 100644 --- a/assets/schema/ensemble_schema.json +++ b/assets/schema/ensemble_schema.json @@ -2875,7 +2875,8 @@ "description": "Set the label's styles when it is in floating mode" }, "labelStyle": { - "$ref": "#/$defs/TextStyle" + "$ref": "#/$defs/TextStyle", + "description": "Set the label's styles" } } } From bf742ed87dfe50933ca330e439307c13691c5bec Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 20 Oct 2023 11:26:42 +0530 Subject: [PATCH 14/20] fix: set clipping to container if border radius is not null --- lib/framework/view/bottom_nav_page_group.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/framework/view/bottom_nav_page_group.dart b/lib/framework/view/bottom_nav_page_group.dart index cec8ab164..c93b275ab 100644 --- a/lib/framework/view/bottom_nav_page_group.dart +++ b/lib/framework/view/bottom_nav_page_group.dart @@ -373,7 +373,7 @@ class EnsembleBottomAppBarState extends State { decoration: BoxDecoration( borderRadius: widget.borderRadius ?? BorderRadius.zero, ), - clipBehavior: Clip.hardEdge, + clipBehavior: widget.borderRadius != null ? Clip.hardEdge : Clip.none, child: BottomAppBar( padding: Utils.optionalInsets(widget.padding) ?? EdgeInsets.zero, shape: widget.notchedShape, From e00b4bd418e00fc3743184be6f298e85b959514c Mon Sep 17 00:00:00 2001 From: snehmehta Date: Fri, 20 Oct 2023 18:29:12 +0530 Subject: [PATCH 15/20] replace hardcoded to theme manager function --- lib/framework/theme/theme_loader.dart | 8 +++++--- lib/framework/theme/theme_manager.dart | 4 ++++ lib/util/utils.dart | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/framework/theme/theme_loader.dart b/lib/framework/theme/theme_loader.dart index 1ff9bdcd8..508cd5a4e 100644 --- a/lib/framework/theme/theme_loader.dart +++ b/lib/framework/theme/theme_loader.dart @@ -1,5 +1,6 @@ import 'package:ensemble/framework/extensions.dart'; import 'package:ensemble/framework/theme/default_theme.dart'; +import 'package:ensemble/framework/theme/theme_manager.dart'; import 'package:ensemble/util/utils.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -144,12 +145,13 @@ mixin ThemeLoader { } TextTheme _buildTextTheme([YamlMap? textTheme]) { + final defaultThemeColor = ThemeManager().defaultTextColor(); TextStyle defaultStyle = - Utils.getTextStyle(textTheme)?.copyWith(color: Colors.black) ?? - const TextStyle( + Utils.getTextStyle(textTheme)?.copyWith(color: defaultThemeColor) ?? + TextStyle( fontFamily: 'Inter', fontWeight: FontWeight.w400, - color: Colors.black); + color: defaultThemeColor); return ThemeData.light() .textTheme diff --git a/lib/framework/theme/theme_manager.dart b/lib/framework/theme/theme_manager.dart index 1a1d97bb6..28c405080 100644 --- a/lib/framework/theme/theme_manager.dart +++ b/lib/framework/theme/theme_manager.dart @@ -38,6 +38,10 @@ class ThemeManager with ThemeLoader { : const Color(0xffffffff); } + Color defaultTextColor() { + return Colors.black; + } + getShadowRadius(BuildContext context) { return 0; } diff --git a/lib/util/utils.dart b/lib/util/utils.dart index c65fa3070..8fdd49973 100644 --- a/lib/util/utils.dart +++ b/lib/util/utils.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; import 'dart:ui'; +import 'package:ensemble/framework/theme/theme_manager.dart'; import 'package:path/path.dart' as p; import 'package:ensemble/framework/error_handling.dart'; @@ -428,7 +429,8 @@ class Utils { fontStyle: Utils.optionalBool(style['isItalic']) == true ? FontStyle.italic : FontStyle.normal, - color: Utils.getColor(style['color']) ?? Colors.black, + color: Utils.getColor(style['color']) ?? + ThemeManager().defaultTextColor(), backgroundColor: Utils.getColor(style['backgroundColor']), decoration: getDecoration(style['decoration']), decorationStyle: From e037fc21dd3b79f27acc2d5f03875af2d8939311 Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 20 Oct 2023 19:08:49 +0530 Subject: [PATCH 16/20] refactor: add reload view support for bottom navigation bar --- lib/framework/view/bottom_nav_page_group.dart | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/framework/view/bottom_nav_page_group.dart b/lib/framework/view/bottom_nav_page_group.dart index c93b275ab..4f12dd923 100644 --- a/lib/framework/view/bottom_nav_page_group.dart +++ b/lib/framework/view/bottom_nav_page_group.dart @@ -75,6 +75,7 @@ class _BottomNavPageGroupState extends State with RouteAware { late List menuItems; late PageController controller; + late int selectedPage; FloatingAlignment floatingAlignment = FloatingAlignment.center; int? floatingMargin; MenuItem? fabMenuItem; @@ -82,7 +83,11 @@ class _BottomNavPageGroupState extends State @override void initState() { super.initState(); - controller = PageController(); + if (widget.menu.reloadView == true) { + selectedPage = widget.selectedPage; + } else { + controller = PageController(initialPage: widget.selectedPage); + } menuItems = widget.menu.menuItems .where((element) => element.floating != true) .toList(); @@ -114,7 +119,9 @@ class _BottomNavPageGroupState extends State @override void dispose() { - controller.dispose(); + if (widget.menu.reloadView == false) { + controller.dispose(); + } Ensemble.routeObserver.unsubscribe(this); super.dispose(); } @@ -182,10 +189,12 @@ class _BottomNavPageGroupState extends State floatingActionButton: _buildFloatingButton(), body: PageGroupWidget( scopeManager: widget.scopeManager, - child: BottomNavPageView( - controller: controller, - children: widget.children, - ), + child: widget.menu.reloadView == true + ? widget.children[selectedPage] + : BottomNavPageView( + controller: controller, + children: widget.children, + ), ), ); } @@ -244,7 +253,15 @@ class _BottomNavPageGroupState extends State color: unselectedColor, selectedColor: selectedColor, notchedShape: const CircularNotchedRectangle(), - onTabSelected: controller.jumpToPage, + onTabSelected: (index) { + if (widget.menu.reloadView == true) { + setState(() { + selectedPage = index; + }); + } else { + controller.jumpToPage(index); + } + }, items: navItems, isFloating: fabMenuItem != null, floatingAlignment: floatingAlignment, From ca8705622f8dc0fcb7e1d7b21ade2524688f3e2b Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 20 Oct 2023 19:24:12 +0530 Subject: [PATCH 17/20] refactor: update drawer widget content not to rebuild when changing the drawer menu item --- lib/framework/view/page_group.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/framework/view/page_group.dart b/lib/framework/view/page_group.dart index e8c4d8b3f..314162501 100644 --- a/lib/framework/view/page_group.dart +++ b/lib/framework/view/page_group.dart @@ -130,10 +130,16 @@ class PageGroupState extends State with MediaQueryCapability { Drawer? drawer = _buildDrawer(context, widget.menu); bool atStart = (widget.menu as DrawerMenu).atStart; return PageGroupWidget( - scopeManager: _scopeManager, - navigationDrawer: atStart ? drawer : null, - navigationEndDrawer: !atStart ? drawer : null, - child: pageWidgets[selectedPage]); + scopeManager: _scopeManager, + navigationDrawer: atStart ? drawer : null, + navigationEndDrawer: !atStart ? drawer : null, + child: widget.menu.reloadView == true + ? pageWidgets[selectedPage] + : IndexedStack( + index: selectedPage, + children: pageWidgets, + ), + ); } else if (widget.menu is SidebarMenu) { return PageGroupWidget( scopeManager: _scopeManager, From 392c3102545fcb5b186021b166463f404edce2fa Mon Sep 17 00:00:00 2001 From: vinothvino42 Date: Fri, 20 Oct 2023 19:57:30 +0530 Subject: [PATCH 18/20] feat(schema): add reloadView to menu --- assets/schema/ensemble_schema.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/assets/schema/ensemble_schema.json b/assets/schema/ensemble_schema.json index 02b48f5a4..d907c7886 100644 --- a/assets/schema/ensemble_schema.json +++ b/assets/schema/ensemble_schema.json @@ -4260,6 +4260,10 @@ "items" ], "properties": { + "reloadView": { + "type": "boolean", + "description": "It will reload the page each time when clicking the menu item" + }, "items": { "type": "array", "description": "List of menu items (minimum 2)", @@ -4339,6 +4343,10 @@ } } }, + "reloadView": { + "type": "boolean", + "description": "It will reload the page each time when clicking the menu item" + }, "items": { "type": "array", "description": "List of menu items (minimum 2)", @@ -4428,6 +4436,10 @@ }, { "properties": { + "reloadView": { + "type": "boolean", + "description": "It will reload the page each time when clicking the menu item" + }, "styles": { "properties": { "borderColor": { From 46e77efb1973c7e0e56fd34f0e12f6a7364903ac Mon Sep 17 00:00:00 2001 From: Vu Le Date: Mon, 23 Oct 2023 16:36:03 -0700 Subject: [PATCH 19/20] add Push Notifications to framework --- lib/action/badge_action.dart | 7 +- lib/action/bottom_modal_action.dart | 7 +- lib/action/call_external_method.dart | 4 +- lib/action/invoke_api_action.dart | 8 +- lib/action/misc_action.dart | 10 +- lib/action/navigation_action.dart | 7 +- lib/action/notification_action.dart | 30 ++---- lib/framework/action.dart | 8 +- lib/framework/notification_manager.dart | 123 ++++++++++++++++++++++++ lib/screen_controller.dart | 2 +- 10 files changed, 169 insertions(+), 37 deletions(-) create mode 100644 lib/framework/notification_manager.dart diff --git a/lib/action/badge_action.dart b/lib/action/badge_action.dart index 79ca7a7e4..1c96eced2 100644 --- a/lib/action/badge_action.dart +++ b/lib/action/badge_action.dart @@ -1,4 +1,5 @@ import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/scope.dart'; import 'package:ensemble/util/utils.dart'; @@ -19,7 +20,8 @@ class UpdateBadgeCount extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { int? count = Utils.optionalInt(scopeManager.dataContext.eval(_count)); if (count != null) { return FlutterAppBadger.updateBadgeCount(count); @@ -30,7 +32,8 @@ class UpdateBadgeCount extends EnsembleAction { class ClearBadgeCount extends EnsembleAction { @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { return FlutterAppBadger.removeBadge(); } } diff --git a/lib/action/bottom_modal_action.dart b/lib/action/bottom_modal_action.dart index cd9756aa6..3edd95181 100644 --- a/lib/action/bottom_modal_action.dart +++ b/lib/action/bottom_modal_action.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/event.dart'; import 'package:ensemble/framework/scope.dart'; @@ -59,7 +60,8 @@ class ShowBottomModalAction extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { Widget? widget; if (body != null) { widget = scopeManager.buildWidgetFromDefinition(body); @@ -96,7 +98,8 @@ class DismissBottomModalAction extends EnsembleAction { DismissBottomModalAction(payload: payload?['payload']); @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { BuildContext? bottomModalContext = ContextScopeWidget.getRootContext(context); if (bottomModalContext != null) { diff --git a/lib/action/call_external_method.dart b/lib/action/call_external_method.dart index c7247c756..4a2681f72 100644 --- a/lib/action/call_external_method.dart +++ b/lib/action/call_external_method.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/event.dart'; import 'package:ensemble/framework/scope.dart'; @@ -32,7 +33,8 @@ class CallExternalMethod extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) async { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) async { String? name = Utils.optionalString(scopeManager.dataContext.eval(_name)); String? errorReason; diff --git a/lib/action/invoke_api_action.dart b/lib/action/invoke_api_action.dart index 617cfc91e..fc4e365e7 100644 --- a/lib/action/invoke_api_action.dart +++ b/lib/action/invoke_api_action.dart @@ -46,11 +46,13 @@ class InvokeAPIAction extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { - var evalApiName = scopeManager.dataContext.eval(apiName); + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { + DataContext realDataContext = dataContext ?? scopeManager.dataContext; + var evalApiName = realDataContext.eval(apiName); var cloneAction = InvokeAPIAction(apiName: evalApiName, initiator: initiator, id: id, inputs: inputs, onResponse: onResponse, onError: onError); return InvokeAPIController() - .execute(cloneAction, context, scopeManager.dataContext, scopeManager, + .execute(cloneAction, context, realDataContext, scopeManager, scopeManager.pageData.apiMap); } } diff --git a/lib/action/misc_action.dart b/lib/action/misc_action.dart index bf6eea916..fccd20e57 100644 --- a/lib/action/misc_action.dart +++ b/lib/action/misc_action.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/event.dart'; import 'package:ensemble/framework/scope.dart'; @@ -32,7 +33,8 @@ class CopyToClipboardAction extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { String? value = Utils.optionalString(scopeManager.dataContext.eval(_value)); if (value != null) { Clipboard.setData(ClipboardData(text: value)).then((_) { @@ -70,7 +72,8 @@ class ShareAction extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { Share.share(scopeManager.dataContext.eval(_text), subject: Utils.optionalString(scopeManager.dataContext.eval(_title))); return Future.value(null); @@ -93,7 +96,8 @@ class RateAppAction extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { // what a mess of options on Android. TODO: add them if (Platform.isIOS) { RateMyApp rateMyApp = RateMyApp(minDays: 0, minLaunches: 0); diff --git a/lib/action/navigation_action.dart b/lib/action/navigation_action.dart index f8ee4441d..dc8220045 100644 --- a/lib/action/navigation_action.dart +++ b/lib/action/navigation_action.dart @@ -1,4 +1,5 @@ import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/scope.dart'; import 'package:ensemble/host_platform_manager.dart'; @@ -25,7 +26,8 @@ class NavigateExternalScreen extends BaseNavigateScreenAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { // payload Map? payload; if (inputs != null) { @@ -58,7 +60,8 @@ class NavigateBackAction extends EnsembleAction { NavigateBackAction(payload: payload?['payload'] ?? payload?['data']); @override - Future execute(BuildContext context, ScopeManager scopeManager) { + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { return Navigator.of(context) .maybePop(scopeManager.dataContext.eval(payload)); } diff --git a/lib/action/notification_action.dart b/lib/action/notification_action.dart index 16d98f444..218ccb164 100644 --- a/lib/action/notification_action.dart +++ b/lib/action/notification_action.dart @@ -2,8 +2,10 @@ import 'dart:developer'; import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/framework/notification_manager.dart'; import 'package:ensemble/framework/scope.dart'; import 'package:ensemble/screen_controller.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -30,28 +32,16 @@ class GetDeviceTokenAction extends EnsembleAction { } @override - Future execute(BuildContext context, ScopeManager scopeManager) async { - String? deviceToken; - try { - await FirebaseMessaging.instance.requestPermission( - alert: true, - badge: true, - sound: true, - ); - // need to get APNS first - await FirebaseMessaging.instance.getAPNSToken(); - // then get device token - deviceToken = await FirebaseMessaging.instance.getToken(); - if (deviceToken != null && onSuccess != null) { - return ScreenController().executeAction(context, onSuccess!, - event: EnsembleEvent(initiator, data: {'token': deviceToken})); - } - } on Exception catch (e) { - log(e.toString()); - log('Error getting device token'); + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) async { + String? deviceToken = await NotificationManager().getDeviceToken(); + if (deviceToken != null && onSuccess != null) { + return ScreenController().executeAction(context, onSuccess!, + event: EnsembleEvent(initiator, data: {'token': deviceToken})); } if (deviceToken == null && onError != null) { - return ScreenController().executeAction(context, onError!); + return ScreenController().executeAction(context, onError!, + event: EnsembleEvent(initiator)); } } } \ No newline at end of file diff --git a/lib/framework/action.dart b/lib/framework/action.dart index d859c405a..d4dcd764c 100644 --- a/lib/framework/action.dart +++ b/lib/framework/action.dart @@ -517,8 +517,6 @@ class FileUploadAction extends EnsembleAction { } } - - class WalletConnectAction extends EnsembleAction { WalletConnectAction({ this.id, @@ -764,7 +762,11 @@ abstract class EnsembleAction { Map? inputs; /// TODO: each Action does all the execution in here - Future execute(BuildContext context, ScopeManager scopeManager) { + /// use DataContext to eval properties. ScopeManager should be refactored + /// so it contains the update data context (its DataContext might not have + /// the latest data) + Future execute(BuildContext context, ScopeManager scopeManager, + {DataContext? dataContext}) { // placeholder until all Actions are implemented return Future.value(null); } diff --git a/lib/framework/notification_manager.dart b/lib/framework/notification_manager.dart new file mode 100644 index 000000000..c41708092 --- /dev/null +++ b/lib/framework/notification_manager.dart @@ -0,0 +1,123 @@ +import 'dart:developer'; + +import 'package:ensemble/ensemble.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble/util/utils.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; + +/// Firebase Push Notification handler +class NotificationManager { + static final NotificationManager _instance = NotificationManager._internal(); + + NotificationManager._internal(); + + factory NotificationManager() => _instance; + + var _init = false; + + // Store the last known device token + String? deviceToken; + + Future init(FirebasePayload payload) async { + if (!_init) { + await Firebase.initializeApp( + options: payload.getFirebaseOptions(), + ); + _initListener(); + _init = true; + } + } + + /// get the device token. This guarantees the token (if available) + /// is the latest correct token + Future getDeviceToken() async { + String? deviceToken; + try { + // request permission + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + ); + + // need to get APNS token first + await FirebaseMessaging.instance.getAPNSToken(); + + // then get device token + deviceToken = await FirebaseMessaging.instance.getToken(); + return deviceToken; + } on Exception catch (e) { + log('Error getting device token: ${e.toString()}'); + } + return null; + } + + void _initListener() { + /// listen for token changes and store a copy + FirebaseMessaging.instance.onTokenRefresh.listen((String newToken) { + deviceToken = newToken; + }); + + /// This is when the app is in the foreground + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + Ensemble.externalDataContext.addAll({ + 'title': message.notification?.title, + 'body': message.notification?.body, + 'data': message.data + }); + _handleNotification(); + }); + + /// This is when the app is in the background and the user taps on the notification + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + Ensemble.externalDataContext.addAll({ + 'title': message.notification?.title, + 'body': message.notification?.body, + 'data': message.data + }); + _handleNotification(); + }); + + // TODO We need to handle the notification when the app was terminated + + } + + void _handleNotification() { + Map? messageData = Ensemble.externalDataContext['data']; + if (messageData?['screenId'] != null || + messageData?['screenName'] != null) { + ScreenController().navigateToScreen( + Utils.globalAppKey.currentContext!, + screenId: messageData!['screenId'], + screenName: messageData!['screenName'], + pageArgs: messageData); + } else { + log( + 'No screenId nor screenName provided on the notification. Ignoring ...'); + } + } + + +} + + +/// abstract to just the absolute must need Firebase options +class FirebasePayload { + FirebasePayload({required this.apiKey, required this.projectId, + required this.messagingSenderId, required this.appId}); + + String apiKey; + String projectId; + String messagingSenderId; + String appId; + + FirebaseOptions getFirebaseOptions() => + FirebaseOptions( + apiKey: apiKey, + appId: appId, + messagingSenderId: messagingSenderId, + projectId: projectId); + + +} \ No newline at end of file diff --git a/lib/screen_controller.dart b/lib/screen_controller.dart index b8f946543..282c96eed 100644 --- a/lib/screen_controller.dart +++ b/lib/screen_controller.dart @@ -630,7 +630,7 @@ class ScreenController { } // catch-all. All Actions should just be using this else { - action.execute(context, scopeManager!); + action.execute(context, scopeManager!, dataContext: dataContext); } } From 610bbe79555c23863c7ac7f3f06f484e229e0025 Mon Sep 17 00:00:00 2001 From: Vu Le Date: Mon, 23 Oct 2023 17:37:02 -0700 Subject: [PATCH 20/20] slight tweak to Notifications --- lib/action/notification_action.dart | 11 ++++++----- lib/framework/notification_manager.dart | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/action/notification_action.dart b/lib/action/notification_action.dart index 218ccb164..39c3fa8b4 100644 --- a/lib/action/notification_action.dart +++ b/lib/action/notification_action.dart @@ -1,4 +1,3 @@ - import 'dart:developer'; import 'package:ensemble/framework/action.dart'; @@ -12,7 +11,8 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; class GetDeviceTokenAction extends EnsembleAction { - GetDeviceTokenAction({super.initiator, required this.onSuccess, this.onError}); + GetDeviceTokenAction( + {super.initiator, required this.onSuccess, this.onError}); EnsembleAction? onSuccess; EnsembleAction? onError; @@ -20,7 +20,7 @@ class GetDeviceTokenAction extends EnsembleAction { factory GetDeviceTokenAction.fromMap({dynamic payload}) { if (payload is Map) { EnsembleAction? successAction = - EnsembleAction.fromYaml(payload['onSuccess']); + EnsembleAction.fromYaml(payload['onSuccess']); if (successAction == null) { throw LanguageError("onSuccess() is required for Get Token Action"); } @@ -41,7 +41,8 @@ class GetDeviceTokenAction extends EnsembleAction { } if (deviceToken == null && onError != null) { return ScreenController().executeAction(context, onError!, - event: EnsembleEvent(initiator)); + event: EnsembleEvent(initiator, + error: 'Unable to get the device token.')); } } -} \ No newline at end of file +} diff --git a/lib/framework/notification_manager.dart b/lib/framework/notification_manager.dart index c41708092..c0b8b5648 100644 --- a/lib/framework/notification_manager.dart +++ b/lib/framework/notification_manager.dart @@ -1,10 +1,12 @@ import 'dart:developer'; +import 'dart:io' show Platform; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/screen_controller.dart'; import 'package:ensemble/util/utils.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; /// Firebase Push Notification handler class NotificationManager { @@ -35,18 +37,24 @@ class NotificationManager { String? deviceToken; try { // request permission - await FirebaseMessaging.instance.requestPermission( + NotificationSettings settings = await FirebaseMessaging.instance.requestPermission( alert: true, badge: true, sound: true, ); - // need to get APNS token first - await FirebaseMessaging.instance.getAPNSToken(); + if (settings.authorizationStatus == AuthorizationStatus.authorized) { + // on iOS we need to get APNS token first + if (!kIsWeb && Platform.isIOS) { + await FirebaseMessaging.instance.getAPNSToken(); + } + + // get device token + deviceToken = await FirebaseMessaging.instance.getToken(); + return deviceToken; + } + - // then get device token - deviceToken = await FirebaseMessaging.instance.getToken(); - return deviceToken; } on Exception catch (e) { log('Error getting device token: ${e.toString()}'); }