408 lines
16 KiB
Dart
408 lines
16 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|
import 'package:thingsboard_app/constants/app_constants.dart';
|
|
import 'package:thingsboard_app/core/context/tb_context.dart';
|
|
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
|
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
|
|
import 'package:thingsboard_app/widgets/two_value_listenable_builder.dart';
|
|
import 'package:universal_platform/universal_platform.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class DashboardController {
|
|
|
|
final ValueNotifier<bool> canGoBack = ValueNotifier(false);
|
|
final ValueNotifier<bool> hasRightLayout = ValueNotifier(false);
|
|
final ValueNotifier<bool> rightLayoutOpened = ValueNotifier(false);
|
|
|
|
final _DashboardState dashboardState;
|
|
DashboardController(this.dashboardState);
|
|
|
|
Future<void> openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async {
|
|
return await dashboardState._openDashboard(dashboardId, state: state, hideToolbar: hideToolbar, fullscreen: fullscreen);
|
|
}
|
|
|
|
Future<bool> goBack() async {
|
|
return dashboardState._goBack();
|
|
}
|
|
|
|
onHistoryUpdated(Future<bool> canGoBackFuture) async {
|
|
canGoBack.value = await canGoBackFuture;
|
|
}
|
|
|
|
onHasRightLayout(bool _hasRightLayout) {
|
|
hasRightLayout.value = _hasRightLayout;
|
|
}
|
|
|
|
onRightLayoutOpened(bool _rightLayoutOpened) {
|
|
rightLayoutOpened.value = _rightLayoutOpened;
|
|
}
|
|
|
|
Future<void> toggleRightLayout() async {
|
|
await dashboardState._toggleRightLayout();
|
|
}
|
|
|
|
Future<void> activateDashboard() async {
|
|
await dashboardState._activateDashboard();
|
|
}
|
|
|
|
Future<void> deactivateDashboard() async {
|
|
await dashboardState._deactivateDashboard();
|
|
}
|
|
|
|
dispose() {
|
|
canGoBack.dispose();
|
|
hasRightLayout.dispose();
|
|
rightLayoutOpened.dispose();
|
|
}
|
|
|
|
}
|
|
|
|
typedef DashboardTitleCallback = void Function(String title);
|
|
|
|
typedef DashboardControllerCallback = void Function(DashboardController controller);
|
|
|
|
class Dashboard extends TbContextWidget {
|
|
|
|
final bool? _home;
|
|
final bool _activeByDefault;
|
|
final DashboardTitleCallback? _titleCallback;
|
|
final DashboardControllerCallback? _controllerCallback;
|
|
|
|
Dashboard(TbContext tbContext, {Key? key, bool? home, bool activeByDefault = true, DashboardTitleCallback? titleCallback, DashboardControllerCallback? controllerCallback}):
|
|
this._home = home,
|
|
this._activeByDefault = activeByDefault,
|
|
this._titleCallback = titleCallback,
|
|
this._controllerCallback = controllerCallback,
|
|
super(tbContext);
|
|
|
|
@override
|
|
_DashboardState createState() => _DashboardState();
|
|
|
|
}
|
|
|
|
class _DashboardState extends TbContextState<Dashboard> {
|
|
|
|
final Completer<InAppWebViewController> _controller = Completer<InAppWebViewController>();
|
|
|
|
bool webViewLoading = true;
|
|
final ValueNotifier<bool> dashboardLoading = ValueNotifier(true);
|
|
final ValueNotifier<bool> dashboardActive = ValueNotifier(true);
|
|
final ValueNotifier<bool> readyState = ValueNotifier(false);
|
|
|
|
final GlobalKey webViewKey = GlobalKey();
|
|
|
|
late final DashboardController _dashboardController;
|
|
|
|
bool _fullscreen = false;
|
|
|
|
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
|
|
crossPlatform: InAppWebViewOptions(
|
|
useShouldOverrideUrlLoading: true,
|
|
mediaPlaybackRequiresUserGesture: false,
|
|
javaScriptEnabled: true,
|
|
cacheEnabled: true,
|
|
supportZoom: false,
|
|
// useOnDownloadStart: true
|
|
),
|
|
android: AndroidInAppWebViewOptions(
|
|
useHybridComposition: true,
|
|
thirdPartyCookiesEnabled: true
|
|
),
|
|
ios: IOSInAppWebViewOptions(
|
|
allowsInlineMediaPlayback: true,
|
|
allowsBackForwardNavigationGestures: false
|
|
));
|
|
|
|
late Uri _initialUrl;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
dashboardActive.value = widget._activeByDefault;
|
|
_dashboardController = DashboardController(this);
|
|
if (widget._controllerCallback != null) {
|
|
widget._controllerCallback!(_dashboardController);
|
|
}
|
|
tbContext.isAuthenticatedListenable.addListener(_onAuthenticated);
|
|
if (tbContext.isAuthenticated) {
|
|
_onAuthenticated();
|
|
}
|
|
}
|
|
|
|
void _onAuthenticated() async {
|
|
if (tbContext.isAuthenticated) {
|
|
if (!readyState.value) {
|
|
_initialUrl = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}');
|
|
readyState.value = true;
|
|
} else {
|
|
var windowMessage = <String, dynamic>{
|
|
'type': 'reloadUserMessage',
|
|
'data': <String, dynamic>{
|
|
'accessToken': tbClient.getJwtToken()!,
|
|
'refreshToken': tbClient.getRefreshToken()!
|
|
}
|
|
};
|
|
if (!UniversalPlatform.isWeb) {
|
|
var controller = await _controller.future;
|
|
await controller.postWebMessage(
|
|
message: WebMessage(data: jsonEncode(windowMessage)),
|
|
targetOrigin: Uri.parse('*'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<bool> _goBack() async {
|
|
if (!UniversalPlatform.isWeb) {
|
|
if (_dashboardController.rightLayoutOpened.value) {
|
|
await _toggleRightLayout();
|
|
return false;
|
|
}
|
|
var controller = await _controller.future;
|
|
if (await controller.canGoBack()) {
|
|
await controller.goBack();
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
tbContext.isAuthenticatedListenable.removeListener(_onAuthenticated);
|
|
readyState.dispose();
|
|
dashboardLoading.dispose();
|
|
_dashboardController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _activateDashboard() async {
|
|
if (!dashboardActive.value) {
|
|
dashboardActive.value = true;
|
|
}
|
|
}
|
|
|
|
Future<void> _deactivateDashboard() async {
|
|
if (dashboardActive.value) {
|
|
dashboardActive.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async {
|
|
_fullscreen = fullscreen;
|
|
dashboardLoading.value = true;
|
|
InAppWebViewController? controller;
|
|
if (!UniversalPlatform.isWeb) {
|
|
controller = await _controller.future;
|
|
}
|
|
var windowMessage = <String, dynamic>{
|
|
'type': 'openDashboardMessage',
|
|
'data': <String, dynamic>{
|
|
'dashboardId': dashboardId
|
|
}
|
|
};
|
|
if (state != null) {
|
|
windowMessage['data']['state'] = state;
|
|
}
|
|
if (widget._home == true) {
|
|
windowMessage['data']['embedded'] = true;
|
|
}
|
|
if (hideToolbar == true) {
|
|
windowMessage['data']['hideToolbar'] = true;
|
|
}
|
|
var webMessage = WebMessage(data: jsonEncode(windowMessage));
|
|
if (!UniversalPlatform.isWeb) {
|
|
await controller!.postWebMessage(
|
|
message: webMessage, targetOrigin: Uri.parse('*'));
|
|
}
|
|
}
|
|
|
|
Future<void> _toggleRightLayout() async {
|
|
var controller = await _controller.future;
|
|
var windowMessage = <String, dynamic>{
|
|
'type': 'toggleDashboardLayout'
|
|
};
|
|
var webMessage = WebMessage(data: jsonEncode(windowMessage));
|
|
await controller.postWebMessage(message: webMessage, targetOrigin: Uri.parse('*'));
|
|
}
|
|
|
|
Future<void> tryLocalNavigation(String? path) async {
|
|
log.debug("path: $path");
|
|
if (path != null) {
|
|
var parts = path.split("/");
|
|
if ([
|
|
'profile',
|
|
'devices',
|
|
'assets',
|
|
'dashboards',
|
|
'dashboard',
|
|
'customers',
|
|
'auditLogs'
|
|
].contains(parts[0])) {
|
|
if ((parts[0] == 'dashboard' || parts[0] == 'dashboards') && parts.length > 1) {
|
|
var dashboardId = parts[1];
|
|
await navigateToDashboard(dashboardId);
|
|
} else if (parts[0] != 'dashboard') {
|
|
var targetPath = '/$path';
|
|
if (parts[0] == 'devices' && widget._home != true) {
|
|
targetPath = '/devicesPage';
|
|
}
|
|
await navigateTo(targetPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return WillPopScope(
|
|
onWillPop: () async {
|
|
if (widget._home == true && !tbContext.isHomePage()) {
|
|
return true;
|
|
}
|
|
if (readyState.value) {
|
|
return await _goBack();
|
|
} else {
|
|
return true;
|
|
}
|
|
},
|
|
child:
|
|
ValueListenableBuilder(
|
|
valueListenable: readyState,
|
|
builder: (BuildContext context, bool ready, child) {
|
|
if (!ready) {
|
|
return SizedBox.shrink();
|
|
} else {
|
|
return Stack(
|
|
children: [
|
|
UniversalPlatform.isWeb ? Center(child: Text('Not implemented!')) :
|
|
InAppWebView(
|
|
key: webViewKey,
|
|
initialUrlRequest: URLRequest(url: _initialUrl),
|
|
initialOptions: options,
|
|
onWebViewCreated: (webViewController) {
|
|
log.debug("onWebViewCreated");
|
|
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardLoadedHandler", callback: (args) async {
|
|
bool hasRightLayout = args[0];
|
|
bool rightLayoutOpened = args[1];
|
|
log.debug("Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened");
|
|
_dashboardController.onHasRightLayout(hasRightLayout);
|
|
_dashboardController.onRightLayoutOpened(rightLayoutOpened);
|
|
dashboardLoading.value = false;
|
|
});
|
|
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardLayoutHandler", callback: (args) async {
|
|
bool rightLayoutOpened = args[0];
|
|
log.debug("Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened");
|
|
_dashboardController.onRightLayoutOpened(rightLayoutOpened);
|
|
});
|
|
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardStateNameHandler", callback: (args) async {
|
|
log.debug("Invoked tbMobileDashboardStateNameHandler: $args");
|
|
if (args.isNotEmpty && args[0] is String) {
|
|
if (widget._titleCallback != null) {
|
|
widget._titleCallback!(args[0]);
|
|
}
|
|
}
|
|
});
|
|
webViewController.addJavaScriptHandler(handlerName: "tbMobileNavigationHandler", callback: (args) async {
|
|
log.debug("Invoked tbMobileNavigationHandler: $args");
|
|
if (args.length > 0) {
|
|
String? path = args[0];
|
|
Map<String, dynamic>? params;
|
|
if (args.length > 1) {
|
|
params = args[1];
|
|
}
|
|
log.debug("path: $path");
|
|
log.debug("params: $params");
|
|
tryLocalNavigation(path);
|
|
}
|
|
});
|
|
webViewController.addJavaScriptHandler(handlerName: "tbMobileHandler", callback: (args) async {
|
|
log.debug("Invoked tbMobileHandler: $args");
|
|
return await widgetActionHandler.handleWidgetMobileAction(args, webViewController);
|
|
});
|
|
},
|
|
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
|
var uri = navigationAction.request.url!;
|
|
var uriString = uri.toString();
|
|
log.debug('shouldOverrideUrlLoading $uriString');
|
|
if (Platform.isAndroid || Platform.isIOS && navigationAction.iosWKNavigationType == IOSWKNavigationType.LINK_ACTIVATED) {
|
|
if (uriString.startsWith(ThingsboardAppConstants.thingsBoardApiEndpoint)) {
|
|
var target = uriString.substring(ThingsboardAppConstants.thingsBoardApiEndpoint.length);
|
|
if (!target.startsWith("?accessToken")) {
|
|
if (target.startsWith("/")) {
|
|
target = target.substring(1);
|
|
}
|
|
await tryLocalNavigation(target);
|
|
return NavigationActionPolicy.CANCEL;
|
|
}
|
|
} else if (await canLaunch(uriString)) {
|
|
await launch(
|
|
uriString,
|
|
);
|
|
return NavigationActionPolicy.CANCEL;
|
|
}
|
|
}
|
|
return Platform.isIOS ? NavigationActionPolicy.ALLOW : NavigationActionPolicy.CANCEL;
|
|
},
|
|
onUpdateVisitedHistory: (controller, url, androidIsReload) async {
|
|
log.debug('onUpdateVisitedHistory: $url');
|
|
_dashboardController.onHistoryUpdated(controller.canGoBack());
|
|
},
|
|
onConsoleMessage: (controller, consoleMessage) {
|
|
log.debug('[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}');
|
|
},
|
|
onLoadStart: (controller, url) async {
|
|
log.debug('onLoadStart: $url');
|
|
},
|
|
onLoadStop: (controller, url) async {
|
|
log.debug('onLoadStop: $url');
|
|
if (webViewLoading) {
|
|
webViewLoading = false;
|
|
_controller.complete(controller);
|
|
}
|
|
},
|
|
androidOnPermissionRequest: (controller, origin, resources) async {
|
|
log.debug('androidOnPermissionRequest origin: $origin, resources: $resources');
|
|
return PermissionRequestResponse(
|
|
resources: resources,
|
|
action: PermissionRequestResponseAction.GRANT);
|
|
},
|
|
),
|
|
if (!UniversalPlatform.isWeb)
|
|
TwoValueListenableBuilder(
|
|
firstValueListenable: dashboardLoading,
|
|
secondValueListenable: dashboardActive,
|
|
builder: (BuildContext context, bool loading, bool active, child) {
|
|
if (!loading && active) {
|
|
return SizedBox.shrink();
|
|
} else {
|
|
var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window);
|
|
var bottomPadding = data.padding.top;
|
|
if (widget._home != true) {
|
|
bottomPadding += kToolbarHeight;
|
|
}
|
|
return Container(
|
|
padding: EdgeInsets.only(bottom: bottomPadding),
|
|
alignment: Alignment.center,
|
|
color: Colors.white,
|
|
child: TbProgressIndicator(
|
|
size: 50.0
|
|
),
|
|
);
|
|
}
|
|
}
|
|
)
|
|
]
|
|
);
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|
|
}
|