diff --git a/.android/gradle.properties b/.android/gradle.properties index 94adc3a3f..598d13fee 100644 --- a/.android/gradle.properties +++ b/.android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/assets/schema/ensemble_schema.json b/assets/schema/ensemble_schema.json index 8e82df494..83d9f16e3 100644 --- a/assets/schema/ensemble_schema.json +++ b/assets/schema/ensemble_schema.json @@ -166,6 +166,10 @@ "type": "boolean", "description": "Specify if the content of this screen is scrollable with a global scrollbar. Using this also allow you to customize the scrolling experience of the header." }, + "unfocus": { + "type": "boolean", + "description": "Specify if the keyboard should dismissed automatically when tapping in the non interactive widgets. Default (false)" + }, "showNavigationIcon": { "type": "boolean", "description": "For a screen with header, the App will automatically show the Menu, Back, or Close icon (for modal screen) before the title. On modal screen without the header, the Close icon will be shown. Set this flag to false if you wish to hide the icons and handle the navigation yourself." diff --git a/lib/action/action_invokable.dart b/lib/action/action_invokable.dart index 8b1f95367..22e2ba5f3 100644 --- a/lib/action/action_invokable.dart +++ b/lib/action/action_invokable.dart @@ -1,4 +1,3 @@ -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'; @@ -22,6 +21,9 @@ abstract class ActionInvokable with Invokable { ActionType.copyToClipboard, ActionType.getDeviceToken, ActionType.getPhoneContacts, + ActionType.showBottomModal, + ActionType.dismissBottomModal, + ActionType.showDialog, ]); } diff --git a/lib/action/bottom_modal_action.dart b/lib/action/bottom_modal_action.dart index 72063baa6..0df9206fd 100644 --- a/lib/action/bottom_modal_action.dart +++ b/lib/action/bottom_modal_action.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:ensemble/framework/action.dart'; import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/error_handling.dart'; @@ -9,9 +7,7 @@ import 'package:ensemble/framework/view/context_scope_widget.dart'; import 'package:ensemble/screen_controller.dart'; import 'package:ensemble/util/utils.dart'; import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:yaml/yaml.dart'; /// open a Modal Bottom Sheet class ShowBottomModalAction extends EnsembleAction { @@ -68,15 +64,19 @@ class ShowBottomModalAction extends EnsembleAction { if (widget != null) { showModalBottomSheet( - context: context, - backgroundColor: _backgroundColor(scopeManager), - barrierColor: _barrierColor(scopeManager), - isScrollControlled: true, - enableDrag: _enableDrag(scopeManager), - showDragHandle: _enableDragHandler(scopeManager), - builder: (modalContext) => - ContextScopeWidget(rootContext: modalContext, child: widget!)) - .then((payload) { + context: context, + backgroundColor: _backgroundColor(scopeManager), + barrierColor: _barrierColor(scopeManager), + isScrollControlled: true, + enableDrag: _enableDrag(scopeManager), + showDragHandle: _enableDragHandler(scopeManager), + builder: (context) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: ContextScopeWidget(rootContext: context, child: widget!), + ), + ).then((payload) { if (onDismiss != null) { return ScreenController().executeActionWithScope( context, scopeManager, onDismiss!, @@ -106,6 +106,6 @@ class DismissBottomModalAction extends EnsembleAction { return Navigator.maybePop( bottomModalContext, scopeManager.dataContext.eval(payload)); } - return Future.value(null); + return Navigator.maybePop(context, scopeManager.dataContext.eval(payload)); } } diff --git a/lib/ensemble_app.dart b/lib/ensemble_app.dart index 242d1d476..6e87d8a1b 100644 --- a/lib/ensemble_app.dart +++ b/lib/ensemble_app.dart @@ -117,33 +117,12 @@ class EnsembleAppState extends State with WidgetsBindingObserver { Workmanager().initialize(callbackDispatcher, isInDebugMode: false); initDeepLink(AppLifecycleState.resumed); } - - WidgetsBinding.instance.addPostFrameCallback((_) async { - executeCallbacks(); - }); } @override void didChangeAppLifecycleState(AppLifecycleState state) async { super.didChangeAppLifecycleState(state); initDeepLink(state); - if (state == AppLifecycleState.resumed) { - executeCallbacks(); - } - } - - Future executeCallbacks() async { - final callbacks = List.from(Ensemble().getCallbacksAfterInitialization()); - - callbacks.asMap().forEach((index, function) async { - // Removing a method and getting the function with index to execute it - Ensemble().getCallbacksAfterInitialization().remove(function); - try { - await Function.apply(function, null); - } catch (e) { - print('Failed to execute a method: $e'); - } - }); } void initDeepLink(AppLifecycleState state) { diff --git a/lib/framework/action.dart b/lib/framework/action.dart index f51235012..e2a813c6d 100644 --- a/lib/framework/action.dart +++ b/lib/framework/action.dart @@ -15,7 +15,6 @@ import 'package:ensemble/framework/extensions.dart'; import 'package:ensemble/framework/keychain_manager.dart'; import 'package:ensemble/framework/permissions_manager.dart'; import 'package:ensemble/framework/scope.dart'; -import 'package:ensemble/framework/view/bottom_nav_page_group.dart'; import 'package:ensemble/framework/view/page_group.dart'; import 'package:ensemble/framework/widget/view_util.dart'; import 'package:ensemble/receive_intent_manager.dart'; @@ -67,17 +66,20 @@ class ShowDialogAction extends EnsembleAction { final Map? options; final EnsembleAction? onDialogDismiss; - factory ShowDialogAction.fromYaml({Invokable? initiator, Map? payload}) { + factory ShowDialogAction.from({Invokable? initiator, Map? payload}) { if (payload == null || payload['widget'] == null) { throw LanguageError( "${ActionType.showDialog.name} requires the 'widget' for the Dialog's content."); } return ShowDialogAction( - initiator: initiator, - widget: payload['widget'], - //inputs: Utils.getMap(payload["inputs"]), - options: Utils.getMap(payload['options']), - onDialogDismiss: EnsembleAction.fromYaml(payload['onDialogDismiss'])); + initiator: initiator, + widget: payload['widget'], + //inputs: Utils.getMap(payload["inputs"]), + options: Utils.getMap(payload['options']), + onDialogDismiss: payload['onDialogDismiss'] == null + ? null + : EnsembleAction.fromYaml(payload['onDialogDismiss']), + ); } } @@ -1006,7 +1008,7 @@ abstract class EnsembleAction { } else if (actionType == ActionType.openCamera) { return ShowCameraAction.fromYaml(initiator: initiator, payload: payload); } else if (actionType == ActionType.showDialog) { - return ShowDialogAction.fromYaml(initiator: initiator, payload: payload); + return ShowDialogAction.from(initiator: initiator, payload: payload); } else if (actionType == ActionType.closeAllDialogs) { return CloseAllDialogsAction(); } else if (actionType == ActionType.startTimer) { diff --git a/lib/framework/data_context.dart b/lib/framework/data_context.dart index 1a6eaf3c6..8cb063f08 100644 --- a/lib/framework/data_context.dart +++ b/lib/framework/data_context.dart @@ -1,17 +1,14 @@ import 'dart:async'; -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/bottom_modal_action.dart'; import 'package:ensemble/action/haptic_action.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/ensemble.dart'; -import 'package:ensemble/ensemble_app.dart'; import 'package:ensemble/framework/all_countries.dart'; -import 'package:ensemble/framework/bindings.dart'; import 'package:ensemble/framework/config.dart'; import 'package:ensemble/framework/device.dart'; import 'package:ensemble/framework/error_handling.dart'; @@ -365,6 +362,20 @@ class NativeInvokable extends ActionInvokable { final scope = ScreenController().getScopeManager(buildContext); callNativeMethod(buildContext, scope, inputs); }, + ActionType.showBottomModal.name: (inputs) => + ScreenController().executeAction( + buildContext, + ShowBottomModalAction.from(payload: inputs), + ), + ActionType.dismissBottomModal.name: (inputs) => + ScreenController().executeAction( + buildContext, + DismissBottomModalAction.from(payload: inputs), + ), + ActionType.showDialog.name: (inputs) => ScreenController() + .executeAction(buildContext, ShowDialogAction.from(payload: inputs)), + ActionType.rateApp.name: (inputs) => ScreenController() + .executeAction(buildContext, RateAppAction.from(payload: inputs)), 'connectSocket': (String socketName, Map? inputs) { connectSocket(buildContext, socketName, inputs: inputs); }, diff --git a/lib/framework/notification_manager.dart b/lib/framework/notification_manager.dart index ed76d97c9..cc015c444 100644 --- a/lib/framework/notification_manager.dart +++ b/lib/framework/notification_manager.dart @@ -95,18 +95,36 @@ class NotificationManager { }); _handleNotification(); }); + } + + void initGetInitialMessage() { + // This is called when the user taps on the notification and the app is opened from the terminated state + FirebaseMessaging.instance.getInitialMessage().then((message) { + if (message == null) return; - // TODO We need to handle the notification when the app was terminated + Ensemble.externalDataContext.addAll({ + 'title': message.notification?.title, + 'body': message.notification?.body, + 'data': message.data + }); + Ensemble() + .addCallbackAfterInitialization(method: () => _handleNotification()); + }).catchError((err) { + // ignore: avoid_print + print('Failed to get the remote notification'); + }); } - void _handleNotification() { + Future _handleNotification() async { 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); + ScreenController().navigateToScreen( + Utils.globalAppKey.currentContext!, + screenId: messageData!['screenId'], + screenName: messageData['screenName'], + pageArgs: messageData, + ); } else { log('No screenId nor screenName provided on the notification. Ignoring ...'); } diff --git a/lib/framework/view/page.dart b/lib/framework/view/page.dart index 03bd61faa..770102694 100644 --- a/lib/framework/view/page.dart +++ b/lib/framework/view/page.dart @@ -1,7 +1,6 @@ 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/extensions.dart'; import 'package:ensemble/framework/menu.dart'; @@ -12,14 +11,10 @@ import 'package:ensemble/framework/view/data_scope_widget.dart'; import 'package:ensemble/framework/view/has_selectable_text.dart'; import 'package:ensemble/framework/view/page_group.dart'; import 'package:ensemble/framework/widget/icon.dart' as ensemble; -import 'package:ensemble/framework/widget/view_util.dart'; -import 'package:ensemble/layout/list_view.dart' as ensemblelist; -import 'package:ensemble/layout/grid_view.dart' as ensembleGrid; import 'package:ensemble/page_model.dart'; import 'package:ensemble/screen_controller.dart'; import 'package:ensemble/util/utils.dart'; -import 'package:ensemble/widget/helpers/controllers.dart'; -import 'package:ensemble/widget/helpers/widgets.dart'; +import 'package:ensemble/widget/helpers/unfocus.dart'; import 'package:flutter/material.dart'; import '../widget/custom_view.dart'; @@ -415,26 +410,31 @@ class PageState extends State Widget rtn = DataScopeWidget( scopeManager: _scopeManager, - child: Scaffold( - resizeToAvoidBottomInset: true, - // slight optimization, if body background is set, let's paint - // the entire screen including the Safe Area - backgroundColor: backgroundColor, - - // appBar is inside CustomScrollView if defined - appBar: fixedAppBar, - body: isScrollableView - ? buildScrollablePageContent(hasDrawer) - : buildFixedPageContent(fixedAppBar != null), - bottomNavigationBar: _bottomNavBar, - drawer: _drawer, - endDrawer: _endDrawer, - bottomSheet: footerWidget, - floatingActionButton: closeModalButton, - floatingActionButtonLocation: - widget._pageModel.pageStyles?['navigationIconPosition'] == 'start' - ? FloatingActionButtonLocation.startTop - : FloatingActionButtonLocation.endTop), + child: Unfocus( + isUnfocus: Utils.getBool(widget._pageModel.pageStyles?['unfocus'], + fallback: false), + child: Scaffold( + resizeToAvoidBottomInset: true, + // slight optimization, if body background is set, let's paint + // the entire screen including the Safe Area + backgroundColor: backgroundColor, + + // appBar is inside CustomScrollView if defined + appBar: fixedAppBar, + body: isScrollableView + ? buildScrollablePageContent(hasDrawer) + : buildFixedPageContent(fixedAppBar != null), + bottomNavigationBar: _bottomNavBar, + drawer: _drawer, + endDrawer: _endDrawer, + bottomSheet: footerWidget, + floatingActionButton: closeModalButton, + floatingActionButtonLocation: + widget._pageModel.pageStyles?['navigationIconPosition'] == + 'start' + ? FloatingActionButtonLocation.startTop + : FloatingActionButtonLocation.endTop), + ), ); // selectableText at the root diff --git a/lib/framework/widget/screen.dart b/lib/framework/widget/screen.dart index a735b056d..861573195 100644 --- a/lib/framework/widget/screen.dart +++ b/lib/framework/widget/screen.dart @@ -91,13 +91,82 @@ class _ScreenState extends State { } }); - if (pageModel is PageGroupModel && pageModel.menu != null) { + return PageInitializer( + pageModel: pageModel, + dataContext: dataContext, + screenPayload: widget.screenPayload, + ); + } +} + +class PageInitializer extends StatefulWidget { + const PageInitializer({ + super.key, + required this.pageModel, + required this.dataContext, + required this.screenPayload, + }); + + final PageModel pageModel; + final DataContext dataContext; + final ScreenPayload? screenPayload; + + @override + State createState() => _PageInitializerState(); +} + +class _PageInitializerState extends State + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) async { + executeCallbacks(); + }); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + executeCallbacks(); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + Future executeCallbacks() async { + final callbacks = List.from(Ensemble().getCallbacksAfterInitialization()); + + callbacks.asMap().forEach((index, function) async { + // Removing a method and getting the function with index to execute it + Ensemble().getCallbacksAfterInitialization().remove(function); + try { + await Function.apply(function, null); + } catch (e) { + print('Failed to execute a method: $e'); + } + }); + } + + @override + Widget build(BuildContext context) { + if (widget.pageModel is PageGroupModel && widget.pageModel.menu != null) { + final pageModel = widget.pageModel as PageGroupModel; + return PageGroup( pageArgs: widget.screenPayload?.arguments, - initialDataContext: dataContext, + initialDataContext: widget.dataContext, model: pageModel, menu: pageModel.menu!); - } else if (pageModel is SinglePageModel) { + } else if (widget.pageModel is SinglePageModel) { + final pageModel = widget.pageModel as SinglePageModel; + // overwrite the pageType as modal only if specified in the payload if (widget.screenPayload?.pageType == PageType.modal) { if (pageModel.screenOptions != null) { @@ -107,7 +176,10 @@ class _ScreenState extends State { ScreenOptions(pageType: widget.screenPayload!.pageType!); } } - return ensemble.Page(dataContext: dataContext, pageModel: pageModel); + return ensemble.Page( + dataContext: widget.dataContext, + pageModel: pageModel, + ); } throw LanguageError("Invalid Screen Definition"); diff --git a/lib/widget/calendar.dart b/lib/widget/calendar.dart index 1c28ac7f4..6bcdd2859 100644 --- a/lib/widget/calendar.dart +++ b/lib/widget/calendar.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:yaml/yaml.dart'; +import 'package:collection/collection.dart'; final kToday = DateTime.now(); final kFirstDay = DateTime(kToday.year, kToday.month - 12, kToday.day); @@ -89,6 +90,7 @@ class EnsembleCalendar extends StatefulWidget _controller.rowSpanLimit = Utils.getInt(value['spanPerRow'], fallback: -1); _controller.overlapOverflowBuilder = value['overflowWidget']; + _controller.topMargin = Utils.getInt(value['topMargin'], fallback: 0); if (value['children'] is List) { for (var span in value['children']) { setRowSpan(span['span']); @@ -343,6 +345,7 @@ class EnsembleCalendar extends StatefulWidget endDay: Utils.getDate(data['end']), widget: data['widget'], inputs: data['inputs'], + id: Utils.generateRandomId(6), ); spans.add(rowSpan); } @@ -387,15 +390,26 @@ class RowSpanConfig { DateTime? endDay; dynamic widget; Map? inputs; + String id; RowSpanConfig({ this.startDay, this.endDay, this.widget, this.inputs, + required this.id, }); bool get isValid => startDay != null && endDay != null; + + Map toJson() { + return { + 'start': startDay, + 'end': endDay, + 'widget': widget, + 'inputs': inputs, + }; + } } class CellConfig { @@ -440,6 +454,7 @@ class CalendarController extends WidgetController { Cell rangeBetweenCell = Cell(); int rowSpanLimit = -1; + int topMargin = 0; dynamic overlapOverflowBuilder; DateTime? selectedDate; @@ -641,12 +656,19 @@ class CalendarState extends WidgetState { toolTipBackgroundColor: widget._controller.tooltipBackgroundColor, toolTipStyle: widget._controller.tooltipTextStyle, showTooltip: widget._controller.showTooltip, + topMargin: widget._controller.topMargin, calendarBuilders: CalendarBuilders( - overlayDefaultBuilder: (context, collapsedLength) { + overlayDefaultBuilder: (context, collapsedLength, children) { Map data = {}; + final collapsedSpans = widget._controller.rowSpans.value + .where((object) => children.contains(object.id)) + .toList(); if (collapsedLength != null) { - data['collapsedLength'] = collapsedLength; + data['collapsedLength'] = collapsedSpans.length; + data['collapsedSpans'] = + collapsedSpans.map((e) => e.toJson()).toList(); } + if (widget._controller.overlapOverflowBuilder == null) { return null; } @@ -657,20 +679,17 @@ class CalendarState extends WidgetState { ? null : (context, range) { final spans = widget._controller.rowSpans; - for (var span in spans.value) { - if (span.startDay == null || span.endDay == null) { - return const SizedBox.shrink(); - } - if (DateTimeRange( - start: span.startDay!, end: span.endDay!) == - range) { - return widgetBuilder( - context, - span.widget, - span.inputs?.cast() ?? {}, - ); - } + + final span = spans.value + .firstWhereOrNull((element) => element.id == range.id); + if (span != null) { + return widgetBuilder( + context, + span.widget, + span.inputs?.cast() ?? {}, + ); } + return const SizedBox.shrink(); }, disabledBuilder: (context, day, focusedDay) { @@ -780,12 +799,15 @@ class CalendarState extends WidgetState { ); } - List getOverlayRange() { - final overlayRange = []; + List getOverlayRange() { + final overlayRange = []; for (var span in widget._controller.rowSpans.value) { if (span.endDay != null && span.startDay != null) { - overlayRange - .add(DateTimeRange(start: span.startDay!, end: span.endDay!)); + overlayRange.add(CustomRange( + start: span.startDay!, + end: span.endDay!, + id: span.id, + )); } } return overlayRange; diff --git a/lib/widget/helpers/unfocus.dart b/lib/widget/helpers/unfocus.dart new file mode 100644 index 000000000..1ca273363 --- /dev/null +++ b/lib/widget/helpers/unfocus.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +/// [Unfocus] - A gesture detector widget +/// +/// It hides the keyboard when tapping outside of non intractive widgetss +class Unfocus extends StatelessWidget { + const Unfocus({ + Key? key, + required this.child, + this.isUnfocus = true, + }) : super(key: key); + + final Widget child; + final bool isUnfocus; + + @override + Widget build(BuildContext context) { + // If false, Just return the child + if (!isUnfocus) return child; + + return Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (_) => FocusManager.instance.primaryFocus?.unfocus(), + child: child, + ); + } +} diff --git a/lib/widget/map.dart b/lib/widget/map.dart deleted file mode 100644 index 5f3b1bbbf..000000000 --- a/lib/widget/map.dart +++ /dev/null @@ -1,413 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; - -import 'package:ensemble/framework/device.dart'; -import 'package:ensemble/ensemble.dart'; -import 'package:ensemble/framework/action.dart'; -import 'package:ensemble/framework/event.dart'; -import 'package:ensemble/framework/scope.dart'; -import 'package:ensemble/framework/view/data_scope_widget.dart'; -import 'package:ensemble/framework/view/page.dart'; -import 'package:ensemble/layout/templated.dart'; -import 'package:ensemble/page_model.dart'; -import 'package:ensemble/screen_controller.dart'; -import 'package:ensemble/util/utils.dart'; -import 'package:ensemble/framework/widget/widget.dart'; -import 'package:ensemble/widget/helpers/controllers.dart'; -import 'package:flutter/material.dart'; -import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:yaml/yaml.dart'; - -class EnsembleMap extends StatefulWidget - with Invokable, HasController { - static const type = 'Map'; - EnsembleMap({Key? key}) : super(key: key); - - @override - MapState createState() => MapState(); - - final MyController _controller = MyController(); - @override - MyController get controller => _controller; - - @override - Map setters() { - return { - 'width': (value) => _controller.width = Utils.optionalInt(value), - 'height': (value) => _controller.height = Utils.optionalInt(value), - 'currentLocation': _controller.updateCurrentLocationStatus, - 'markers': _controller.updateMarkerTemplate, - 'markerWidth': (width) => _controller.markerWidth = width, - 'markerHeight': (height) => _controller.markerHeight = height, - 'boundaryPadding': (padding) => - _controller.boundaryPadding = Utils.optionalInsets(padding), - 'selectedMarker': (index) => - _controller.updateSelectedMarker(Utils.optionalInt(index)), - 'onMarkerTap': (action) => _controller.onMarkerTap = - EnsembleAction.fromYaml(action, initiator: this), - }; - } - - @override - Map getters() { - return { - 'selectedMarker': () => _controller.selectedMarker, - }; - } - - @override - Map methods() { - return { - 'fitBoundary': (padding) => _controller.refitBoundary( - paddingOverride: Utils.optionalInsets(padding)), - }; - } -} - -class MyController extends WidgetController with LocationCapability { - // Map needs to be constrained by the parent, or required a size when inside Flex - int? height; - int? width; - - final MapController _mapController = MapController(); - EdgeInsets? boundaryPadding; - EnsembleAction? onMarkerTap; - - int? selectedMarker; - void updateSelectedMarker(int? markerIndex) { - log("selected marker is $markerIndex"); - selectedMarker = markerIndex; - notifyListeners(); - } - - // unfortunately these are needed for flutter_map - int? markerWidth; - int? markerHeight; - - MarkerTemplate? _markerTemplate; - List _markers = []; - set markers(List items) { - _markers = items; - fitBoundary(); - } - - Position? currentLocation; // user's location if enabled & given permission - dynamic customLocationWidget; - - void updateMarkerTemplate(dynamic markerData) { - if (markerData is YamlMap) { - String? data = markerData['data']; - String? name = markerData['name']; - - String? lat = markerData['location']?['lat']; - String? lng = markerData['location']?['lng']; - dynamic widget = markerData['widget']; - if (data != null && - name != null && - widget != null && - lat != null && - lng != null) { - _markerTemplate = MarkerTemplate( - data: data, - name: name, - template: widget, - lat: lat, - lng: lng, - selectedWidget: markerData['selectedWidget'], - selectedWidgetOverlay: markerData['selectedWidgetOverlay']); - } - } - } - - void updateCurrentLocationStatus(dynamic locationData) { - if (locationData is YamlMap) { - if (locationData['enabled'] == true) { - customLocationWidget = locationData['widget']; - if (currentLocation == null) { - requestUserLocation(); - } - } - } - } - - void requestUserLocation() async { - currentLocation = (await getLocation()).location; - fitBoundary(); - notifyListeners(); - } - - void refitBoundary({EdgeInsets? paddingOverride}) { - log("refitting boundary"); - fitBoundary(paddingOverride: paddingOverride); - notifyListeners(); - } - - /// zoom the map to fit our markers and current location - void fitBoundary({EdgeInsets? paddingOverride}) { - _mapController.onReady.then((value) { - List points = []; - if (currentLocation != null) { - points - .add(LatLng(currentLocation!.latitude, currentLocation!.longitude)); - } - points.addAll(_markers.map((item) => item.point).toList()); - - if (points.isNotEmpty) { - EdgeInsets? padding = paddingOverride ?? boundaryPadding; - _mapController.fitBounds(LatLngBounds.fromPoints(points), - options: FitBoundsOptions(padding: padding ?? EdgeInsets.zero)); - } //170 115 - }); - } -} - -class MapState extends WidgetState with TemplatedWidgetState { - // Mapbox raster's max zoom is 18 - static const double mapboxMaxZoom = 18; - static const double defaultMarkerWidth = 60; - static const double defaultMarkerHeight = 30; - - late final String _mapAccessToken; - Marker? selectedWidget; - Widget? overlayWidget; - - @override - void initState() { - super.initState(); - // TODO: use Provider to inject account in - _mapAccessToken = Ensemble().getAccount()?.mapAccessToken ?? ''; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - // listen for changes - if (widget._controller._markerTemplate != null) { - registerItemTemplate(context, widget._controller._markerTemplate!, - evaluateInitialValue: true, onDataChanged: (List dataList) { - setState(() { - widget._controller.markers = buildMarkersFromTemplate(dataList); - }); - }); - } - } - - /// build the markers from our item-template data - List buildMarkersFromTemplate(List dataList) { - List? markerWidgets = buildWidgetsFromTemplate( - context, dataList, widget._controller._markerTemplate!); - - List markers = []; - if (markerWidgets != null && widget._controller._markerTemplate != null) { - for (int i = 0; i < markerWidgets.length; i++) { - DataScopeWidget markerWidget = markerWidgets[i]; - ScopeManager scopeManager = markerWidget.scopeManager; - - // evaluate the lat/lng - double? lat = Utils.optionalDouble(scopeManager.dataContext - .eval(widget._controller._markerTemplate!.lat)); - double? lng = Utils.optionalDouble(scopeManager.dataContext - .eval(widget._controller._markerTemplate!.lng)); - if (lat != null && lng != null) { - LatLng point = LatLng(lat, lng); - Widget w; - // if either the selectedWidget or selectedWidgetOverlay is specified, - // wrap our marker widget to listen for tap events - if (widget._controller._markerTemplate!.selectedWidget != null || - widget._controller._markerTemplate!.selectedWidgetOverlay != - null) { - w = InkWell( - child: markerWidget, - onTap: () => selectMarker(i, point, markerWidget), - ); - // if a widget is selected - if (widget._controller.selectedMarker != null) { - selectMarker(i, point, markerWidget); - } - } else { - w = markerWidget; - } - - // add the marker - markers.add(buildMarker(w, point)); - } - } - } - return markers; - } - - Marker buildMarker(Widget childWidget, LatLng point) { - return Marker( - point: point, - width: widget._controller.markerWidth?.toDouble() ?? defaultMarkerWidth, - height: - widget._controller.markerHeight?.toDouble() ?? defaultMarkerHeight, - builder: (context) => childWidget); - } - - void selectMarker( - int selectedIndex, LatLng point, DataScopeWidget markerScope) { - bool markerChanges = false; - - // Note that selectedWidget and overlayWidget's parent is the map, so we need - // to wrap them in their proper item template DataScopeWidget. This enables - // the onAction to properly traverse up to the parent DataScopeWidget. - - // selected widget - if (widget._controller._markerTemplate!.selectedWidget != null) { - Widget childWidget = markerScope.scopeManager - .buildWidgetWithScopeFromDefinition( - widget._controller._markerTemplate!.selectedWidget); - selectedWidget = buildMarker(childWidget, point); - widget._controller.selectedMarker = selectedIndex; - markerChanges = true; - } - - // overlay widget - if (widget._controller._markerTemplate!.selectedWidgetOverlay != null) { - overlayWidget = markerScope.scopeManager - .buildWidgetWithScopeFromDefinition( - widget._controller._markerTemplate!.selectedWidgetOverlay); - markerChanges = true; - } - - if (markerChanges) { - setState(() {}); - } - - // send the event - if (widget._controller.onMarkerTap != null) { - ScreenController().executeAction(context, widget._controller.onMarkerTap!, - event: EnsembleEvent(widget)); - } - } - - @override - Widget buildWidget(BuildContext context) { - List items = getCurrentMarkers(); - - // render the current location widget - if (widget._controller.currentLocation != null) { - Widget? locationWidget; - if (widget._controller.customLocationWidget != null) { - locationWidget = DataScopeWidget.getScope(context) - ?.buildWidgetFromDefinition( - widget._controller.customLocationWidget); - } - locationWidget ??= const Icon(Icons.filter_tilt_shift); - - items.add(Marker( - point: LatLng(widget._controller.currentLocation!.latitude, - widget._controller.currentLocation!.longitude), - builder: (context) => locationWidget!)); - } - - Widget map = FlutterMap( - mapController: widget._controller._mapController, - options: MapOptions( - maxZoom: mapboxMaxZoom, - interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate), - layers: [ - TileLayerOptions( - urlTemplate: - "https://api.mapbox.com/styles/v1/ensembleui/cla1963q0000e15p00zuejxwu/tiles/512/{z}/{x}/{y}@2x?access_token=$_mapAccessToken", - additionalOptions: {"access_token": _mapAccessToken}, - ), - MarkerLayerOptions(markers: items) - ]); - - Widget rtn = Stack( - children: [ - map, - overlayWidget != null && widget._controller.selectedMarker != null - ? Container( - margin: const EdgeInsets.only(bottom: 110, left: 10, right: 10), - alignment: Alignment.bottomCenter, - child: ConstrainedBox( - constraints: - BoxConstraints(maxHeight: Device().screenHeight / 2), - child: SingleChildScrollView( - child: overlayWidget, - ))) - : const SizedBox.shrink() - ], - ); - - if (widget._controller.width != null || widget._controller.height != null) { - rtn = SizedBox( - width: widget._controller.width?.toDouble(), - height: widget._controller.height?.toDouble(), - child: rtn); - } - return rtn; - } - - List getCurrentMarkers() { - if (widget._controller.selectedMarker == null) { - return widget._controller._markers; - } else { - List rtn = []; - Marker? selected; - for (int i = 0; i < widget._controller._markers.length; i++) { - // use the selected widget version - if (i == widget._controller.selectedMarker && selectedWidget != null) { - selected = selectedWidget!; - } else { - rtn.add(widget._controller._markers[i]); - } - } - // add selected widget to the end so its z-index is on top - if (selected != null) { - rtn.add(selected); - } - return rtn; - } - } - - /* - - void initGoogleMap() { - _googleMapController = Completer(); - if (Platform.isAndroid) { - AndroidGoogleMapsFlutter.useAndroidViewSurface = true; - } - } - - Widget buildGoogleMap() { - return GoogleMap( - mapType: MapType.normal, - initialCameraPosition: const CameraPosition( - target: LatLng(37.42796133580664, -122.085749655962), - zoom: 14.4746, - ), - onMapCreated: (GoogleMapController controller) { - _googleMapController.complete(controller); - }, - ); - } - */ -} - -class MarkerTemplate extends ItemTemplate { - MarkerTemplate( - {required String data, - required String name, - required dynamic - template, // this is the marker widget, just piggyback on the name - required this.lat, - required this.lng, - this.selectedWidget, - this.selectedWidgetOverlay}) - : super(data, name, template); - - String lat; - String lng; - YamlMap? selectedWidget; - YamlMap? selectedWidgetOverlay; -} diff --git a/lib/widget/widget_registry.dart b/lib/widget/widget_registry.dart index 84b41f78b..e6ac45cec 100644 --- a/lib/widget/widget_registry.dart +++ b/lib/widget/widget_registry.dart @@ -38,7 +38,6 @@ import 'package:ensemble/widget/input/form_time.dart'; import 'package:ensemble/widget/input/slider.dart'; import 'package:ensemble/widget/loading_container.dart'; import 'package:ensemble/widget/lottie/lottie.dart'; -import 'package:ensemble/widget/map.dart'; import 'package:ensemble/widget/maps/maps.dart'; import 'package:ensemble/widget/markdown.dart'; import 'package:ensemble/widget/popup_menu.dart'; @@ -107,7 +106,6 @@ class WidgetRegistry { QRCode.type: () => QRCode(), EnsembleProgressIndicator.type: () => EnsembleProgressIndicator(), Maps.type: () => Maps(), - EnsembleMap.type: () => EnsembleMap(), // legacy maps Carousel.type: () => Carousel(), Video.type: () => Video(), YouTube.type: () => YouTube(), diff --git a/pubspec.yaml b/pubspec.yaml index aaed23bac..7516466a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: flutter_svg_provider: ^1.0.4 qr_flutter: ^4.1.0 device_info_plus: ^8.2.0 - flutter_map: ^2.2.0 + flutter_map: ^4.0.0 carousel_slider: ^4.1.1 fluttertoast: 8.2.2 video_player: ^2.6.1 @@ -78,7 +78,7 @@ dependencies: flutter_markdown: ^0.6.10+5 url_launcher: ^6.1.11 provider: ^6.0.3 - cloud_firestore: ^4.12.0 + cloud_firestore: 4.9.3 geolocator: ^9.0.2 get_storage: ^2.0.3 flutter_html: ^3.0.0-beta.1 # flutter_html ^2.0.0 requires flutter_svg <1.0.0, conflicts with ours