diff --git a/assets/schema/ensemble_schema.json b/assets/schema/ensemble_schema.json index 4f858cbe6..6499aea97 100644 --- a/assets/schema/ensemble_schema.json +++ b/assets/schema/ensemble_schema.json @@ -2870,8 +2870,13 @@ "type": "boolean", "description": "Moves the label on top of the Input Field. Default (False)." }, + "floatingLabelStyle": { + "$ref": "#/$defs/TextStyle", + "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" } } } @@ -4257,6 +4262,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)", @@ -4320,9 +4329,26 @@ "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" } } }, + "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)", @@ -4412,6 +4438,10 @@ }, { "properties": { + "reloadView": { + "type": "boolean", + "description": "It will reload the page each time when clicking the menu item" + }, "styles": { "properties": { "borderColor": { 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..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 11229d4e2..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 1a0026ce3..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 71606ce9b..fc4e365e7 100644 --- a/lib/action/invoke_api_action.dart +++ b/lib/action/invoke_api_action.dart @@ -13,6 +13,52 @@ 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, + {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, realDataContext, scopeManager, + scopeManager.pageData.apiMap); + } +} + + + class InvokeAPIController { Future executeWithContext( BuildContext context, InvokeAPIAction action, diff --git a/lib/action/misc_action.dart b/lib/action/misc_action.dart new file mode 100644 index 000000000..fccd20e57 --- /dev/null +++ b/lib/action/misc_action.dart @@ -0,0 +1,108 @@ +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'; +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, + {DataContext? dataContext}) { + 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, + {DataContext? dataContext}) { + 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, + {DataContext? dataContext}) { + // 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..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 new file mode 100644 index 000000000..39c3fa8b4 --- /dev/null +++ b/lib/action/notification_action.dart @@ -0,0 +1,48 @@ +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'; +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, + {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!, + event: EnsembleEvent(initiator, + error: 'Unable to get the device token.')); + } + } +} diff --git a/lib/framework/action.dart b/lib/framework/action.dart index c3230c5ec..d4dcd764c 100644 --- a/lib/framework/action.dart +++ b/lib/framework/action.dart @@ -4,7 +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'; @@ -18,42 +21,10 @@ 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'; -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, YamlMap? 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, @@ -69,7 +40,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 +66,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 +92,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 +127,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 +174,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 +202,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 +228,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 +277,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 +305,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 +329,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 +340,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 +351,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 +389,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 +441,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 +488,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,74 +517,6 @@ 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({ this.id, @@ -636,7 +538,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 +567,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 +586,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 +594,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 +601,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 +615,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 +635,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 +653,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 +669,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 +696,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 +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); } @@ -912,7 +796,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 +856,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..a75b4f622 100644 --- a/lib/framework/data_context.dart +++ b/lib/framework/data_context.dart @@ -3,6 +3,9 @@ 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/invoke_api_action.dart'; import 'package:ensemble/action/navigation_action.dart'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/config.dart'; @@ -310,25 +313,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 +340,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 +353,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 +423,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/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/notification_manager.dart b/lib/framework/notification_manager.dart new file mode 100644 index 000000000..c0b8b5648 --- /dev/null +++ b/lib/framework/notification_manager.dart @@ -0,0 +1,131 @@ +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 { + 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 + NotificationSettings settings = await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + ); + + 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; + } + + + } 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/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/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/framework/view/bottom_nav_page_group.dart b/lib/framework/view/bottom_nav_page_group.dart index 04f08b482..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, + ), ), ); } @@ -237,11 +246,22 @@ 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(), 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, @@ -264,7 +284,9 @@ class EnsembleBottomAppBar extends StatefulWidget { required this.items, required this.selectedIndex, this.height, + this.margin, this.padding, + this.borderRadius, this.iconSize = 24.0, required this.backgroundColor, required this.color, @@ -281,6 +303,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; @@ -290,6 +313,7 @@ class EnsembleBottomAppBar extends StatefulWidget { final bool isFloating; final FloatingAlignment floatingAlignment; final NotchedShape notchedShape; + final BorderRadius? borderRadius; final VoidCallback? onFabTapped; final ValueChanged onTabSelected; @@ -361,13 +385,17 @@ 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( + child: Container( + margin: Utils.optionalInsets(widget.margin) ?? EdgeInsets.zero, + decoration: BoxDecoration( + borderRadius: widget.borderRadius ?? BorderRadius.zero, + ), + clipBehavior: widget.borderRadius != null ? Clip.hardEdge : Clip.none, + child: BottomAppBar( padding: Utils.optionalInsets(widget.padding) ?? EdgeInsets.zero, + shape: widget.notchedShape, + color: widget.backgroundColor, + notchMargin: _defaultFloatingNotch, child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, diff --git a/lib/framework/view/page_group.dart b/lib/framework/view/page_group.dart index 02ee371d9..314162501 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; + 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 && widget.menu.reloadView == false) { + sidebarPageController = PageController(initialPage: 0); + } } @override @@ -125,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, @@ -150,7 +161,12 @@ 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: 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; @@ -245,6 +261,9 @@ class PageGroupState extends State with MediaQueryCapability { setState(() { selectedPage = index; }); + if (widget.menu.reloadView == false) { + sidebarPageController?.jumpToPage(index); + } }, ); } diff --git a/lib/screen_controller.dart b/lib/screen_controller.dart index 4f1e15566..282c96eed 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 @@ -474,49 +471,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(); @@ -676,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); } } diff --git a/lib/util/utils.dart b/lib/util/utils.dart index 73b88060f..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']), + color: Utils.getColor(style['color']) ?? + ThemeManager().defaultTextColor(), backgroundColor: Utils.getColor(style['backgroundColor']), decoration: getDecoration(style['decoration']), decorationStyle: 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, ); }