Redesign. Improve dashboard page loading.
This commit is contained in:
@@ -114,109 +114,126 @@ class _AlarmCardState extends TbContextState<AlarmCard, _AlarmCardState> {
|
||||
if (this.loading) {
|
||||
return Container( height: 134, alignment: Alignment.center, child: RefreshProgressIndicator());
|
||||
} else {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child:
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: AutoSizeText(alarm.type,
|
||||
maxLines: 2,
|
||||
minFontSize: 8,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF282828),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
height: 20 / 14)
|
||||
)
|
||||
),
|
||||
Text(alarmSeverityTranslations[alarm.severity]!,
|
||||
style: TextStyle(
|
||||
color: alarmSeverityColors[alarm.severity]!,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
height: 16 / 12)
|
||||
)
|
||||
]
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: Text(alarm.originatorName != null ? alarm.originatorName! : '',
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: alarmSeverityColors[alarm.severity]!,
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4))
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
SizedBox(width: 4),
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child:
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: AutoSizeText(alarm.type,
|
||||
maxLines: 2,
|
||||
minFontSize: 8,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF282828),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
height: 20 / 14)
|
||||
)
|
||||
),
|
||||
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(alarm.createdTime!)),
|
||||
style: TextStyle(
|
||||
color: Color(0xFFAFAFAF),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 12,
|
||||
height: 16 / 12)
|
||||
)
|
||||
),
|
||||
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(alarm.createdTime!)),
|
||||
style: TextStyle(
|
||||
color: Color(0xFFAFAFAF),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 12,
|
||||
height: 16 / 12)
|
||||
)
|
||||
]
|
||||
),
|
||||
SizedBox(height: 22),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: Text(alarmStatusTranslations[alarm.status]!,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF282828),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
height: 20 / 14)
|
||||
)
|
||||
]
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK].contains(alarm.status))
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Color(0xffF0F4F9),
|
||||
child: IconButton(icon: Icon(Icons.done), padding: EdgeInsets.all(6.0), onPressed: () => _ackAlarm(alarm))
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: Text(alarm.originatorName != null ? alarm.originatorName! : '',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFAFAFAF),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 12,
|
||||
height: 16 / 12)
|
||||
)
|
||||
),
|
||||
if ([AlarmStatus.ACTIVE_UNACK, AlarmStatus.ACTIVE_ACK].contains(alarm.status))
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 4),
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Color(0xffF0F4F9),
|
||||
child: IconButton(icon: Icon(Icons.clear), padding: EdgeInsets.all(6.0), onPressed: () => _clearAlarm(alarm))
|
||||
)
|
||||
]
|
||||
Text(alarmSeverityTranslations[alarm.severity]!,
|
||||
style: TextStyle(
|
||||
color: alarmSeverityColors[alarm.severity]!,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
height: 16 / 12)
|
||||
)
|
||||
]
|
||||
),
|
||||
SizedBox(height: 22),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: Text(alarmStatusTranslations[alarm.status]!,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF282828),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
height: 20 / 14)
|
||||
)
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK].contains(alarm.status))
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Color(0xffF0F4F9),
|
||||
child: IconButton(icon: Icon(Icons.done, size: 18), padding: EdgeInsets.all(7.0), onPressed: () => _ackAlarm(alarm))
|
||||
),
|
||||
if ([AlarmStatus.ACTIVE_UNACK, AlarmStatus.ACTIVE_ACK].contains(alarm.status))
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 4),
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Color(0xffF0F4F9),
|
||||
child: IconButton(icon: Icon(Icons.clear, size: 18), padding: EdgeInsets.all(7.0), onPressed: () => _clearAlarm(alarm))
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
]
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,18 @@ class AlarmsPage extends TbContextWidget<AlarmsPage, _AlarmsPageState> {
|
||||
|
||||
}
|
||||
|
||||
class _AlarmsPageState extends TbContextState<AlarmsPage, _AlarmsPageState> {
|
||||
class _AlarmsPageState extends TbContextState<AlarmsPage, _AlarmsPageState> with AutomaticKeepAliveClientMixin<AlarmsPage> {
|
||||
|
||||
final AlarmQueryController _alarmQueryController = AlarmQueryController();
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
var alarmsList = AlarmsList(tbContext, _alarmQueryController, searchMode: widget.searchMode);
|
||||
PreferredSizeWidget appBar;
|
||||
if (widget.searchMode) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,29 +8,47 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:thingsboard_app/constants/api_path.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class DashboardController {
|
||||
|
||||
final ValueNotifier<bool> canGoBack = 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;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
canGoBack.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
typedef DashboardTitleCallback = void Function(String title);
|
||||
|
||||
typedef DashboardControllerCallback = void Function(DashboardController controller);
|
||||
|
||||
class Dashboard extends TbContextWidget<Dashboard, _DashboardState> {
|
||||
|
||||
final String _dashboardId;
|
||||
final String? _state;
|
||||
final bool? _home;
|
||||
final bool? _hideToolbar;
|
||||
final bool _fullscreen;
|
||||
final DashboardTitleCallback? _titleCallback;
|
||||
final DashboardControllerCallback? _controllerCallback;
|
||||
|
||||
Dashboard(TbContext tbContext, {required String dashboardId, required bool fullscreen,
|
||||
DashboardTitleCallback? titleCallback, String? state, bool? home,
|
||||
bool? hideToolbar}):
|
||||
this._dashboardId = dashboardId,
|
||||
this._fullscreen = fullscreen,
|
||||
this._titleCallback = titleCallback,
|
||||
this._state = state,
|
||||
Dashboard(TbContext tbContext, {Key? key, bool? home, DashboardTitleCallback? titleCallback, DashboardControllerCallback? controllerCallback}):
|
||||
this._home = home,
|
||||
this._hideToolbar = hideToolbar,
|
||||
this._titleCallback = titleCallback,
|
||||
this._controllerCallback = controllerCallback,
|
||||
super(tbContext);
|
||||
|
||||
@override
|
||||
@@ -41,15 +60,23 @@ class _DashboardState extends TbContextState<Dashboard, _DashboardState> {
|
||||
|
||||
final Completer<InAppWebViewController> _controller = Completer<InAppWebViewController>();
|
||||
|
||||
final ValueNotifier<bool> webViewLoading = ValueNotifier(true);
|
||||
bool webViewLoading = true;
|
||||
final ValueNotifier<bool> dashboardLoading = 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(
|
||||
@@ -60,37 +87,88 @@ class _DashboardState extends TbContextState<Dashboard, _DashboardState> {
|
||||
allowsInlineMediaPlayback: true,
|
||||
));
|
||||
|
||||
late String _dashboardUrl;
|
||||
late String _currentDashboardId;
|
||||
late String? _currentDashboardState;
|
||||
late Uri _initialUrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_dashboardUrl = thingsBoardApiEndpoint + '/dashboard/' + widget._dashboardId;
|
||||
List<String> params = [];
|
||||
params.add("accessToken=${tbClient.getJwtToken()!}");
|
||||
params.add("refreshToken=${tbClient.getRefreshToken()!}");
|
||||
if (widget._state != null) {
|
||||
params.add('state=${widget._state}');
|
||||
_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(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()!
|
||||
}
|
||||
};
|
||||
var controller = await _controller.future;
|
||||
await controller.postWebMessage(message: WebMessage(data: jsonEncode(windowMessage)), targetOrigin: Uri.parse('*'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _goBack() async {
|
||||
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> _openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async {
|
||||
_fullscreen = fullscreen;
|
||||
dashboardLoading.value = true;
|
||||
var 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) {
|
||||
params.add('embedded=true');
|
||||
windowMessage['data']['embedded'] = true;
|
||||
}
|
||||
if (widget._hideToolbar == true) {
|
||||
params.add('hideToolbar=true');
|
||||
if (hideToolbar == true) {
|
||||
windowMessage['data']['hideToolbar'] = true;
|
||||
}
|
||||
if (params.isNotEmpty) {
|
||||
_dashboardUrl += '?${params.join('&')}';
|
||||
}
|
||||
_currentDashboardId = widget._dashboardId;
|
||||
_currentDashboardState = widget._state;
|
||||
var webMessage = WebMessage(data: jsonEncode(windowMessage));
|
||||
await controller.postWebMessage(message: webMessage, targetOrigin: Uri.parse('*'));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (widget._home == true && !tbContext.isHomePage()) {
|
||||
return true;
|
||||
}
|
||||
var controller = await _controller.future;
|
||||
if (await controller.canGoBack()) {
|
||||
await controller.goBack();
|
||||
@@ -98,191 +176,147 @@ class _DashboardState extends TbContextState<Dashboard, _DashboardState> {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
InAppWebView(
|
||||
key: webViewKey,
|
||||
initialUrlRequest: URLRequest(url: Uri.parse(_dashboardUrl)),
|
||||
initialOptions: options,
|
||||
onWebViewCreated: (webViewController) {
|
||||
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardStateNameHandler", callback: (args) async {
|
||||
log.debug("Invoked tbMobileDashboardStateNameHandler: $args");
|
||||
webViewLoading.value = false;
|
||||
if (args.isNotEmpty && args[0] is String) {
|
||||
if (widget._titleCallback != null) {
|
||||
widget._titleCallback!(args[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
webViewController.addJavaScriptHandler(handlerName: "tbMobileHandler", callback: (args) async {
|
||||
log.debug("Invoked tbMobileHandler: $args");
|
||||
return await widgetActionHandler.handleWidgetMobileAction(args, webViewController);
|
||||
});
|
||||
_controller.complete(webViewController);
|
||||
},
|
||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||
var uri = navigationAction.request.url!;
|
||||
var uriString = uri.toString();
|
||||
log.debug('shouldOverrideUrlLoading $uriString');
|
||||
if (![
|
||||
"http",
|
||||
"https",
|
||||
"file",
|
||||
"chrome",
|
||||
"data",
|
||||
"javascript",
|
||||
"about"
|
||||
].contains(uri.scheme)) {
|
||||
if (await canLaunch(uriString)) {
|
||||
// Launch the App
|
||||
await launch(
|
||||
uriString,
|
||||
);
|
||||
// and cancel the request
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
}
|
||||
child:
|
||||
ValueListenableBuilder(
|
||||
valueListenable: readyState,
|
||||
builder: (BuildContext context, bool ready, child) {
|
||||
if (!ready) {
|
||||
return SizedBox.shrink();
|
||||
} else {
|
||||
return Container(
|
||||
decoration: BoxDecoration(color: Colors.white),
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
InAppWebView(
|
||||
key: webViewKey,
|
||||
initialUrlRequest: URLRequest(url: _initialUrl),
|
||||
initialOptions: options,
|
||||
onWebViewCreated: (webViewController) {
|
||||
log.debug("onWebViewCreated");
|
||||
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardLoadedHandler", callback: (args) async {
|
||||
log.debug("Invoked tbMobileDashboardLoadedHandler");
|
||||
dashboardLoading.value = false;
|
||||
});
|
||||
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");
|
||||
if (path != null) {
|
||||
if ([
|
||||
'profile',
|
||||
'devices',
|
||||
'assets',
|
||||
'dashboards',
|
||||
'customers',
|
||||
'auditLogs'
|
||||
].contains(path)) {
|
||||
var targetPath = '/$path';
|
||||
if (path == 'devices' && widget._home != true) {
|
||||
targetPath = '/devicesPage';
|
||||
}
|
||||
navigateTo(targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
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 (![
|
||||
"http",
|
||||
"https",
|
||||
"file",
|
||||
"chrome",
|
||||
"data",
|
||||
"javascript",
|
||||
"about"
|
||||
].contains(uri.scheme)) {
|
||||
if (await canLaunch(uriString)) {
|
||||
// Launch the App
|
||||
await launch(
|
||||
uriString,
|
||||
);
|
||||
// and cancel the request
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
}
|
||||
|
||||
return Platform.isIOS ? NavigationActionPolicy.ALLOW : NavigationActionPolicy.CANCEL;
|
||||
},
|
||||
onUpdateVisitedHistory: (controller, url, androidIsReload) async {
|
||||
if (url != null) {
|
||||
String newStateId = url.pathSegments.last;
|
||||
log.debug('onUpdateVisitedHistory: $newStateId');
|
||||
if (newStateId == 'profile') {
|
||||
webViewLoading.value = true;
|
||||
await controller.goBack();
|
||||
await navigateTo('/profile');
|
||||
webViewLoading.value = false;
|
||||
return;
|
||||
} else if (newStateId == 'login') {
|
||||
webViewLoading.value = true;
|
||||
await controller.pauseTimers();
|
||||
await controller.stopLoading();
|
||||
await tbClient.logout();
|
||||
return;
|
||||
} else if (['devices', 'assets', 'dashboards'].contains(newStateId)) {
|
||||
var controller = await _controller.future;
|
||||
await controller.goBack();
|
||||
navigateTo('/$newStateId');
|
||||
return;
|
||||
} else {
|
||||
if (url.pathSegments.length > 1) {
|
||||
var segmentName = url.pathSegments[url.pathSegments.length-2];
|
||||
if (segmentName == 'dashboards' && widget._home != true) {
|
||||
webViewLoading.value = true;
|
||||
var targetPath = _createDashboardNavigationPath(newStateId, fullscreen: widget._fullscreen);
|
||||
await navigateTo(targetPath, replace: true);
|
||||
return;
|
||||
} else if (segmentName == 'dashboard') {
|
||||
_currentDashboardId = newStateId;
|
||||
_currentDashboardState = url.queryParameters['state'];
|
||||
return;
|
||||
}
|
||||
}
|
||||
webViewLoading.value = true;
|
||||
if (widget._home == true) {
|
||||
await navigateTo('/home', replace: true);
|
||||
} else {
|
||||
var targetPath = _createDashboardNavigationPath(_currentDashboardId, state: _currentDashboardState, fullscreen: widget._fullscreen);
|
||||
await navigateTo(targetPath, replace: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onConsoleMessage: (controller, consoleMessage) {
|
||||
log.debug('[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}');
|
||||
},
|
||||
onLoadStart: (controller, url) async {
|
||||
log.debug('onLoadStart: $url');
|
||||
// await _setTokens(controller.webStorage.localStorage);
|
||||
},
|
||||
onLoadStop: (controller, url) async {
|
||||
log.debug('onLoadStop: $url');
|
||||
// await _setTokens(controller.webStorage.localStorage);
|
||||
},
|
||||
androidOnPermissionRequest: (controller, origin, resources) async {
|
||||
log.debug('androidOnPermissionRequest origin: $origin, resources: $resources');
|
||||
return PermissionRequestResponse(
|
||||
resources: resources,
|
||||
action: PermissionRequestResponseAction.GRANT);
|
||||
},
|
||||
/* onDownloadStart: (controller, url) async {
|
||||
log.debug("onDownloadStart $url");
|
||||
final taskId = await FlutterDownloader.enqueue(
|
||||
url: url.toString(),
|
||||
savedDir: (await getExternalStorageDirectory())!.path,
|
||||
showNotification: true,
|
||||
openFileFromNotification: true,
|
||||
);
|
||||
} */
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: webViewLoading,
|
||||
builder: (BuildContext context, bool loading, child) {
|
||||
if (!loading) {
|
||||
return SizedBox.shrink();
|
||||
} else {
|
||||
return Container(
|
||||
decoration: BoxDecoration(color: Colors.white),
|
||||
child: Center(
|
||||
child: RefreshProgressIndicator()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
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);
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: dashboardLoading,
|
||||
builder: (BuildContext context, bool loading, child) {
|
||||
if (!loading) {
|
||||
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
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
String _createDashboardNavigationPath(String dashboardId, {bool? fullscreen, String? state}) {
|
||||
var targetPath = '/dashboard/$dashboardId';
|
||||
List<String> params = [];
|
||||
if (state != null) {
|
||||
params.add('state=$state');
|
||||
}
|
||||
if (fullscreen != null) {
|
||||
params.add('fullscreen=$fullscreen');
|
||||
}
|
||||
if (params.isNotEmpty) {
|
||||
targetPath += '?${params.join('&')}';
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
Future<void> _setTokens(Storage storage) async {
|
||||
String jwtToken = tbClient.getJwtToken()!;
|
||||
int jwtTokenExpiration = _getClientExpiration(jwtToken);
|
||||
String refreshToken = tbClient.getRefreshToken()!;
|
||||
int refreshTokenExpiration = _getClientExpiration(refreshToken);
|
||||
await storage.setItem(key: 'jwt_token', value: jwtToken);
|
||||
await storage.setItem(key: 'jwt_token_expiration', value: jwtTokenExpiration);
|
||||
await storage.setItem(key: 'refresh_token', value: refreshToken);
|
||||
await storage.setItem(key: 'refresh_token_expiration', value: refreshTokenExpiration);
|
||||
}
|
||||
|
||||
/* String _setTokensJavaScript() {
|
||||
String jwtToken = tbClient.getJwtToken()!;
|
||||
int jwtTokenExpiration = _getClientExpiration(jwtToken);
|
||||
String refreshToken = tbClient.getRefreshToken()!;
|
||||
int refreshTokenExpiration = _getClientExpiration(refreshToken);
|
||||
return "window.localStorage.setItem('jwt_token','$jwtToken');\n"+
|
||||
"window.localStorage.setItem('jwt_token_expiration','$jwtTokenExpiration');\n"+
|
||||
"window.localStorage.setItem('refresh_token','$refreshToken');\n"+
|
||||
"window.localStorage.setItem('refresh_token_expiration','$refreshTokenExpiration');";
|
||||
} */
|
||||
|
||||
int _getClientExpiration(String token) {
|
||||
var decodedToken = JwtDecoder.decode(tbClient.getJwtToken()!);
|
||||
int issuedAt = decodedToken['iat'];
|
||||
int expTime = decodedToken['exp'];
|
||||
int ttl = expTime - issuedAt;
|
||||
int clientExpiration = DateTime.now().millisecondsSinceEpoch + ttl * 1000;
|
||||
return clientExpiration;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
class DashboardPage extends TbPageWidget<DashboardPage, _DashboardPageState> {
|
||||
|
||||
final String? _dashboardTitle;
|
||||
final String _dashboardId;
|
||||
final String? _dashboardId;
|
||||
final String? _state;
|
||||
final bool _fullscreen;
|
||||
final bool? _fullscreen;
|
||||
|
||||
DashboardPage(TbContext tbContext, {required String dashboardId, required bool fullscreen, String? dashboardTitle, String? state}):
|
||||
DashboardPage(TbContext tbContext, {String? dashboardId, bool? fullscreen, String? dashboardTitle, String? state}):
|
||||
_dashboardId = dashboardId,
|
||||
_fullscreen = fullscreen,
|
||||
_dashboardTitle = dashboardTitle,
|
||||
@@ -52,10 +52,11 @@ class _DashboardPageState extends TbPageState<DashboardPage, _DashboardPageState
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
|
||||
fullscreen: widget._fullscreen, titleCallback: (title) {
|
||||
dashboardTitleValue.value = title;
|
||||
}),
|
||||
body: Text('Deprecated') //Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
|
||||
//fullscreen: widget._fullscreen, titleCallback: (title) {
|
||||
//dashboardTitleValue.value = title;
|
||||
//}
|
||||
//),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/config/routes/router.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/modules/dashboard/dashboards_page.dart';
|
||||
import 'package:thingsboard_app/modules/dashboard/fullscreen_dashboard_page.dart';
|
||||
|
||||
import 'dashboard_page.dart';
|
||||
|
||||
@@ -20,12 +21,17 @@ class DashboardRoutes extends TbRoutes {
|
||||
dashboardTitle: dashboardTitle, state: state);
|
||||
});
|
||||
|
||||
late var fullscreenDashboardHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return FullscreenDashboardPage(tbContext, params["id"]![0]);
|
||||
});
|
||||
|
||||
DashboardRoutes(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
void doRegisterRoutes(router) {
|
||||
router.define("/dashboards", handler: dashboardsHandler);
|
||||
router.define("/dashboard/:id", handler: dashboardDetailsHandler);
|
||||
router.define("/fullscreenDashboard/:id", handler: fullscreenDashboardHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
|
||||
|
||||
@override
|
||||
void onEntityTap(DashboardInfo dashboard) {
|
||||
navigateTo('/dashboard/${dashboard.id!.id}?title=${dashboard.title}');
|
||||
navigateToDashboard(dashboard.id!.id!, dashboardTitle: dashboard.title);
|
||||
// navigateTo('/dashboard/${dashboard.id!.id}?title=${dashboard.title}');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -152,57 +153,50 @@ class _DashboardGridCardState extends TbContextState<DashboardGridCard, _Dashboa
|
||||
Widget build(BuildContext context) {
|
||||
var hasImage = widget.dashboard.image != null;
|
||||
Widget image;
|
||||
BoxFit imageFit;
|
||||
if (hasImage) {
|
||||
var uriData = UriData.parse(widget.dashboard.image!);
|
||||
image = Image.memory(uriData.contentAsBytes());
|
||||
imageFit = BoxFit.contain;
|
||||
} else {
|
||||
image = Image.asset(ThingsboardImage.dashboardPlaceholder);
|
||||
imageFit = BoxFit.cover;
|
||||
}
|
||||
return
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Stack(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Column(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: FittedBox(
|
||||
fit: imageFit,
|
||||
child: image,
|
||||
Expanded(
|
||||
child: Stack (
|
||||
children: [
|
||||
SizedBox.expand(
|
||||
child: FittedBox(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
fit: BoxFit.cover,
|
||||
child: image
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
hasImage ? Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x00000000),
|
||||
Color(0xb7000000)
|
||||
],
|
||||
stops: [0.4219, 1]
|
||||
Divider(height: 1),
|
||||
Container(
|
||||
height: 44,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
child:
|
||||
Center(
|
||||
child: AutoSizeText(widget.dashboard.title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
minFontSize: 12,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
height: 20 / 14
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
) : Container(),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: AutoSizeText(widget.dashboard.title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
minFontSize: 8,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: hasImage ? Colors.white : Color(0xFF282828),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
height: 20 / 14
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/core/entity/entities_base.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
|
||||
import 'dashboards_list.dart';
|
||||
import 'dashboards_grid.dart';
|
||||
|
||||
class DashboardsPage extends TbPageWidget<DashboardsPage, _DashboardsPageState> {
|
||||
|
||||
@@ -17,23 +16,19 @@ class DashboardsPage extends TbPageWidget<DashboardsPage, _DashboardsPageState>
|
||||
|
||||
class _DashboardsPageState extends TbPageState<DashboardsPage, _DashboardsPageState> {
|
||||
|
||||
final PageLinkController _pageLinkController = PageLinkController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var dashboardsList = DashboardsList(tbContext, _pageLinkController);
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
title: Text(dashboardsList.title)
|
||||
title: Text('Dashboards')
|
||||
),
|
||||
body: dashboardsList
|
||||
body: DashboardsGridWidget(tbContext)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageLinkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
90
lib/modules/dashboard/fullscreen_dashboard_page.dart
Normal file
90
lib/modules/dashboard/fullscreen_dashboard_page.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/modules/dashboard/dashboard.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
|
||||
class FullscreenDashboardPage extends TbPageWidget<FullscreenDashboardPage, _FullscreenDashboardPageState> {
|
||||
|
||||
final String fullscreenDashboardId;
|
||||
final String? _dashboardTitle;
|
||||
|
||||
FullscreenDashboardPage(TbContext tbContext, this.fullscreenDashboardId, {String? dashboardTitle}):
|
||||
_dashboardTitle = dashboardTitle,
|
||||
super(tbContext);
|
||||
|
||||
@override
|
||||
_FullscreenDashboardPageState createState() => _FullscreenDashboardPageState();
|
||||
|
||||
}
|
||||
|
||||
class _FullscreenDashboardPageState extends TbPageState<FullscreenDashboardPage, _FullscreenDashboardPageState> {
|
||||
|
||||
late ValueNotifier<String> dashboardTitleValue;
|
||||
final ValueNotifier<bool> showBackValue = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_onCanGoBack(bool canGoBack) {
|
||||
showBackValue.value = canGoBack;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(kToolbarHeight),
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: showBackValue,
|
||||
builder: (context, canGoBack, widget) {
|
||||
return TbAppBar(
|
||||
tbContext,
|
||||
leading: canGoBack ? BackButton(
|
||||
onPressed: () {
|
||||
maybePop();
|
||||
}
|
||||
) : null,
|
||||
showLoadingIndicator: false,
|
||||
elevation: 0,
|
||||
title: ValueListenableBuilder<String>(
|
||||
valueListenable: dashboardTitleValue,
|
||||
builder: (context, title, widget) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title)
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(icon: Icon(Icons.settings), onPressed: () => navigateTo('/profile?fullscreen=true'))
|
||||
]
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
body: Dashboard(
|
||||
tbContext,
|
||||
titleCallback: (title) {
|
||||
dashboardTitleValue.value = title;
|
||||
},
|
||||
controllerCallback: (controller) {
|
||||
controller.canGoBack.addListener(() {
|
||||
_onCanGoBack(controller.canGoBack.value);
|
||||
});
|
||||
controller.openDashboard(widget.fullscreenDashboardId, fullscreen: true);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/modules/dashboard/main_dashboard_page.dart
Normal file
113
lib/modules/dashboard/main_dashboard_page.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/modules/dashboard/dashboard.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
|
||||
class MainDashboardPageController {
|
||||
|
||||
DashboardController? _dashboardController;
|
||||
_MainDashboardPageState? _mainDashboardPageState;
|
||||
|
||||
_setMainDashboardPageState(_MainDashboardPageState state) {
|
||||
_mainDashboardPageState = state;
|
||||
}
|
||||
|
||||
_setDashboardController(DashboardController controller) {
|
||||
_dashboardController = controller;
|
||||
}
|
||||
|
||||
Future<bool> dashboardGoBack() {
|
||||
if (_dashboardController != null) {
|
||||
return _dashboardController!.goBack();
|
||||
} else {
|
||||
return Future.value(true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar}) async {
|
||||
if (dashboardTitle != null) {
|
||||
_mainDashboardPageState?._updateTitle(dashboardTitle);
|
||||
}
|
||||
await _dashboardController?.openDashboard(dashboardId, state: state, hideToolbar: hideToolbar);
|
||||
}
|
||||
}
|
||||
|
||||
class MainDashboardPage extends TbContextWidget<MainDashboardPage, _MainDashboardPageState> {
|
||||
|
||||
final String? _dashboardTitle;
|
||||
final MainDashboardPageController? _controller;
|
||||
|
||||
MainDashboardPage(TbContext tbContext,
|
||||
{MainDashboardPageController? controller,
|
||||
String? dashboardTitle}):
|
||||
_controller = controller,
|
||||
_dashboardTitle = dashboardTitle,
|
||||
super(tbContext);
|
||||
|
||||
@override
|
||||
_MainDashboardPageState createState() => _MainDashboardPageState();
|
||||
|
||||
}
|
||||
|
||||
class _MainDashboardPageState extends TbContextState<MainDashboardPage, _MainDashboardPageState> {
|
||||
|
||||
late ValueNotifier<String> dashboardTitleValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget._controller != null) {
|
||||
widget._controller!._setMainDashboardPageState(this);
|
||||
}
|
||||
dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_updateTitle(String newTitle) {
|
||||
dashboardTitleValue.value = newTitle;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
maybePop();
|
||||
}
|
||||
),
|
||||
showLoadingIndicator: false,
|
||||
elevation: 0,
|
||||
title: ValueListenableBuilder<String>(
|
||||
valueListenable: dashboardTitleValue,
|
||||
builder: (context, title, widget) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title)
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
body: Dashboard(
|
||||
tbContext,
|
||||
titleCallback: (title) {
|
||||
dashboardTitleValue.value = title;
|
||||
},
|
||||
controllerCallback: (controller) {
|
||||
if (widget._controller != null) {
|
||||
widget._controller!._setDashboardController(controller);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/core/entity/entities_base.dart';
|
||||
import 'package:thingsboard_app/utils/services/device_profile_cache.dart';
|
||||
import 'package:thingsboard_app/utils/services/entity_query_api.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
mixin DeviceProfilesBase on EntitiesBase<DeviceProfileInfo, PageLink> {
|
||||
@@ -50,6 +51,11 @@ mixin DeviceProfilesBase on EntitiesBase<DeviceProfileInfo, PageLink> {
|
||||
return DeviceProfileCard(tbContext, deviceProfile);
|
||||
}
|
||||
|
||||
@override
|
||||
double? gridChildAspectRatio() {
|
||||
return 156 / 200;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RefreshDeviceCounts {
|
||||
@@ -93,8 +99,10 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
|
||||
Future<int> inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, active: false);
|
||||
Future<List<int>> countsFuture = Future.wait([activeDevicesCount, inactiveDevicesCount]);
|
||||
countsFuture.then((counts) {
|
||||
_activeDevicesCount.add(counts[0]);
|
||||
_inactiveDevicesCount.add(counts[1]);
|
||||
if (this.mounted) {
|
||||
_activeDevicesCount.add(counts[0]);
|
||||
_inactiveDevicesCount.add(counts[1]);
|
||||
}
|
||||
});
|
||||
return countsFuture;
|
||||
}
|
||||
@@ -107,32 +115,31 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
|
||||
child:
|
||||
Container(
|
||||
child: Card(
|
||||
color: Theme.of(tbContext.currentState!.context).colorScheme.primary,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
elevation: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
Padding(padding: EdgeInsets.fromLTRB(16, 12, 16, 15),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('All devices',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
height: 20 / 14
|
||||
)
|
||||
),
|
||||
Icon(Icons.arrow_forward, color: Colors.white)
|
||||
Icon(Icons.arrow_forward, size: 18)
|
||||
],
|
||||
)
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8),
|
||||
Divider(height: 1),
|
||||
Padding(padding: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
@@ -150,7 +157,7 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var deviceCount = snapshot.data!;
|
||||
return _buildDeviceCount(context, true, deviceCount, displayStatusText: true);
|
||||
return _buildDeviceCount(context, true, deviceCount);
|
||||
} else {
|
||||
return Center(child:
|
||||
Container(height: 20, width: 20,
|
||||
@@ -166,7 +173,11 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
|
||||
}
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
// SizedBox(width: 4),
|
||||
Container(width: 1,
|
||||
height: 40,
|
||||
child: VerticalDivider(width: 1)
|
||||
),
|
||||
Flexible(fit: FlexFit.tight,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -181,7 +192,7 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var deviceCount = snapshot.data!;
|
||||
return _buildDeviceCount(context, false, deviceCount, displayStatusText: true);
|
||||
return _buildDeviceCount(context, false, deviceCount);
|
||||
} else {
|
||||
return Center(child:
|
||||
Container(height: 20, width: 20,
|
||||
@@ -206,15 +217,10 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(25),
|
||||
blurRadius: 10.0,
|
||||
color: Colors.black.withAlpha((255 * 0.05).ceil()),
|
||||
blurRadius: 6.0,
|
||||
offset: Offset(0, 4)
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(18),
|
||||
blurRadius: 30.0,
|
||||
offset: Offset(0, 10)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -275,124 +281,98 @@ class _DeviceProfileCardState extends TbContextState<DeviceProfileCard, _DeviceP
|
||||
}
|
||||
return
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: FittedBox(
|
||||
fit: imageFit,
|
||||
child: image,
|
||||
)
|
||||
),
|
||||
hasImage ? Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x00000000),
|
||||
Color(0xb7000000)
|
||||
],
|
||||
stops: [0.4219, 1]
|
||||
)
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack (
|
||||
children: [
|
||||
SizedBox.expand(
|
||||
child: FittedBox(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
fit: imageFit,
|
||||
child: image
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
) : Container(),
|
||||
Positioned(
|
||||
bottom: 56,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: AutoSizeText(entity.name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
minFontSize: 8,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: hasImage ? Colors.white : Color(0xFF282828),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
height: 20 / 14
|
||||
),
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
left: 4,
|
||||
right: 4,
|
||||
height: 40,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(fit: FlexFit.tight,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FutureBuilder<int>(
|
||||
future: activeDevicesCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
|
||||
var deviceCount = snapshot.data!;
|
||||
return _buildDeviceCount(context, true, deviceCount);
|
||||
} else {
|
||||
return Center(child:
|
||||
Container(height: 20, width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
|
||||
strokeWidth: 2.5)));
|
||||
}
|
||||
},
|
||||
)
|
||||
),
|
||||
onTap: () {
|
||||
navigateTo('/deviceList?active=true&deviceType=${entity.name}');
|
||||
}
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Flexible(fit: FlexFit.tight,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FutureBuilder<int>(
|
||||
future: inactiveDevicesCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
|
||||
var deviceCount = snapshot.data!;
|
||||
return _buildDeviceCount(context, false, deviceCount);
|
||||
} else {
|
||||
return Center(child:
|
||||
Container(height: 20, width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
|
||||
strokeWidth: 2.5)));
|
||||
}
|
||||
},
|
||||
)
|
||||
),
|
||||
onTap: () {
|
||||
navigateTo('/deviceList?active=false&deviceType=${entity.name}');
|
||||
}
|
||||
Container(
|
||||
height: 44,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Center(
|
||||
child: AutoSizeText(entity.name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
minFontSize: 12,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
height: 20 / 14
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
Divider(height: 1),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: FutureBuilder<int>(
|
||||
future: activeDevicesCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
|
||||
var deviceCount = snapshot.data!;
|
||||
return _buildDeviceCount(context, true, deviceCount);
|
||||
} else {
|
||||
return Container(height: 40,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 20, width: 20,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
|
||||
strokeWidth: 2.5))));
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
navigateTo('/deviceList?active=true&deviceType=${entity.name}');
|
||||
}
|
||||
),
|
||||
Divider(height: 1),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: FutureBuilder<int>(
|
||||
future: inactiveDevicesCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
|
||||
var deviceCount = snapshot.data!;
|
||||
return _buildDeviceCount(context, false, deviceCount);
|
||||
} else {
|
||||
return Container(height: 40,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 20, width: 20,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
|
||||
strokeWidth: 2.5))));
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
navigateTo('/deviceList?active=false&deviceType=${entity.name}');
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDeviceCount(BuildContext context, bool active, int count, {bool displayStatusText = false}) {
|
||||
Widget _buildDeviceCount(BuildContext context, bool active, int count) {
|
||||
Color color = active ? Color(0xFF008A00) : Color(0xFFAFAFAF);
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
@@ -412,23 +392,23 @@ Widget _buildDeviceCount(BuildContext context, bool active, int count, {bool dis
|
||||
)
|
||||
],
|
||||
),
|
||||
if (displayStatusText)
|
||||
SizedBox(width: 8.67),
|
||||
if (displayStatusText)
|
||||
Text(active ? 'Active' : 'Inactive', style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 16 / 12,
|
||||
color: color
|
||||
)),
|
||||
SizedBox(width: 8.67),
|
||||
Text(count.toString(), style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 16 / 12,
|
||||
color: color
|
||||
))
|
||||
],
|
||||
),
|
||||
Text(count.toString(), style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 16 / 12,
|
||||
color: color
|
||||
))
|
||||
Icon(Icons.chevron_right, size: 16, color: Color(0xFFACACAC))
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,10 +2,11 @@ import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/config/routes/router.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_page.dart';
|
||||
import 'package:thingsboard_app/modules/main/main_page.dart';
|
||||
|
||||
import 'device_details_page.dart';
|
||||
import 'devices_page.dart';
|
||||
import 'devices_list_page.dart';
|
||||
|
||||
class DeviceRoutes extends TbRoutes {
|
||||
|
||||
@@ -13,12 +14,16 @@ class DeviceRoutes extends TbRoutes {
|
||||
return MainPage(tbContext, path: '/devices');
|
||||
});
|
||||
|
||||
late var devicesPageHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return DevicesPage(tbContext);
|
||||
});
|
||||
|
||||
late var deviceListHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
var searchMode = params['search']?.first == 'true';
|
||||
var deviceType = params['deviceType']?.first;
|
||||
String? activeStr = params['active']?.first;
|
||||
bool? active = activeStr != null ? activeStr == 'true' : null;
|
||||
return DevicesPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active);
|
||||
return DevicesListPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active);
|
||||
});
|
||||
|
||||
late var deviceDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
@@ -30,6 +35,7 @@ class DeviceRoutes extends TbRoutes {
|
||||
@override
|
||||
void doRegisterRoutes(router) {
|
||||
router.define("/devices", handler: devicesHandler);
|
||||
router.define("/devicesPage", handler: devicesPageHandler);
|
||||
router.define("/deviceList", handler: deviceListHandler);
|
||||
router.define("/device/:id", handler: deviceDetailsHandler);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:core';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:thingsboard_app/constants/assets_path.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/core/entity/entities_base.dart';
|
||||
@@ -30,7 +31,8 @@ mixin DevicesBase on EntitiesBase<EntityData, EntityDataQuery> {
|
||||
if (profile.defaultDashboardId != null) {
|
||||
var dashboardId = profile.defaultDashboardId!.id!;
|
||||
var state = Utils.createDashboardEntityState(device.entityId, entityName: device.field('name')!, entityLabel: device.field('label')!);
|
||||
navigateTo('/dashboard/$dashboardId?title=${device.field('name')!}&state=$state');
|
||||
// navigateTo('/dashboard/$dashboardId?title=${device.field('name')!}&state=$state');
|
||||
navigateToDashboard(dashboardId, dashboardTitle: device.field('name'), state: state);
|
||||
} else {
|
||||
// navigateTo('/device/${device.entityId.id}');
|
||||
if (tbClient.isTenantAdmin()) {
|
||||
@@ -127,49 +129,37 @@ class _DeviceCardState extends TbContextState<DeviceCard, _DeviceCardState> {
|
||||
width: widget.listWidgetCard ? 58 : 60,
|
||||
height: widget.listWidgetCard ? 58 : 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFEEEEEE),
|
||||
borderRadius: BorderRadius.horizontal(left: Radius.circular(widget.listWidgetCard ? 4 : 6))
|
||||
// color: Color(0xFFEEEEEE),
|
||||
borderRadius: BorderRadius.horizontal(left: Radius.circular(4))
|
||||
),
|
||||
child: FutureBuilder<DeviceProfileInfo>(
|
||||
future: deviceProfileFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
|
||||
var profile = snapshot.data!;
|
||||
Widget image;
|
||||
BoxFit imageFit;
|
||||
if (profile.image != null) {
|
||||
var uriData = UriData.parse(profile.image!);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.horizontal(left: Radius.circular(widget.listWidgetCard ? 4 : 6)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Image.memory(uriData.contentAsBytes()),
|
||||
)
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x00000000),
|
||||
Color(0xb7000000)
|
||||
],
|
||||
stops: [0.4219, 1]
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
image = Image.memory(uriData.contentAsBytes());
|
||||
imageFit = BoxFit.contain;
|
||||
} else {
|
||||
return Center(
|
||||
child: Icon(Icons.devices_other, color: Color(0xFFC2C2C2))
|
||||
);
|
||||
image = Image.asset(ThingsboardImage.deviceProfilePlaceholder);
|
||||
imageFit = BoxFit.cover;
|
||||
}
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.horizontal(left: Radius.circular(4)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: FittedBox(
|
||||
fit: imageFit,
|
||||
child: image,
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return Center(child: RefreshProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary)
|
||||
@@ -200,12 +190,12 @@ class _DeviceCardState extends TbContextState<DeviceCard, _DeviceCardState> {
|
||||
height: 20 / 14
|
||||
))
|
||||
),
|
||||
if (!widget.listWidgetCard) Text(widget.device.attribute('active') == 'true' ? 'Active' : 'Inactive',
|
||||
if (!widget.listWidgetCard) Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(widget.device.createdTime!)),
|
||||
style: TextStyle(
|
||||
color: widget.device.attribute('active') == 'true' ? Color(0xFF008A00) : Color(0xFFAFAFAF),
|
||||
fontSize: 12,
|
||||
height: 12 /12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFFAFAFAF),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 16 / 12
|
||||
))
|
||||
]
|
||||
),
|
||||
@@ -221,12 +211,12 @@ class _DeviceCardState extends TbContextState<DeviceCard, _DeviceCardState> {
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 16 / 12
|
||||
)),
|
||||
if (!widget.listWidgetCard) Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(widget.device.createdTime!)),
|
||||
if (!widget.listWidgetCard) Text(widget.device.attribute('active') == 'true' ? 'Active' : 'Inactive',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFAFAFAF),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 16 / 12
|
||||
color: widget.device.attribute('active') == 'true' ? Color(0xFF008A00) : Color(0xFFAFAFAF),
|
||||
fontSize: 12,
|
||||
height: 16 / 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
))
|
||||
],
|
||||
)
|
||||
|
||||
98
lib/modules/device/devices_list_page.dart
Normal file
98
lib/modules/device/devices_list_page.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_base.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_list.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
|
||||
class DevicesListPage extends TbPageWidget<DevicesListPage, _DevicesListPageState> {
|
||||
|
||||
final String? deviceType;
|
||||
final bool? active;
|
||||
final bool searchMode;
|
||||
|
||||
DevicesListPage(TbContext tbContext, {this.deviceType, this.active, this.searchMode = false}) : super(tbContext);
|
||||
|
||||
@override
|
||||
_DevicesListPageState createState() => _DevicesListPageState();
|
||||
|
||||
}
|
||||
|
||||
class _DevicesListPageState extends TbPageState<DevicesListPage, _DevicesListPageState> {
|
||||
|
||||
late final DeviceQueryController _deviceQueryController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_deviceQueryController = DeviceQueryController(deviceType: widget.deviceType, active: widget.active);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var devicesList = DevicesList(tbContext, _deviceQueryController, searchMode: widget.searchMode, displayDeviceImage: widget.deviceType == null);
|
||||
PreferredSizeWidget appBar;
|
||||
if (widget.searchMode) {
|
||||
appBar = TbAppSearchBar(
|
||||
tbContext,
|
||||
onSearch: (searchText) => _deviceQueryController.onSearchText(searchText),
|
||||
);
|
||||
} else {
|
||||
String titleText = widget.deviceType != null ? widget.deviceType! : 'All devices';
|
||||
String? subTitleText;
|
||||
if (widget.active != null) {
|
||||
subTitleText = widget.active == true ? 'Active' : 'Inactive';
|
||||
}
|
||||
Column title = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(titleText, style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: subTitleText != null ? 16 : 20,
|
||||
height: subTitleText != null ? 20 / 16 : 24 / 20
|
||||
)),
|
||||
if (subTitleText != null)
|
||||
Text(subTitleText, style: TextStyle(
|
||||
color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 16 / 12
|
||||
))
|
||||
]
|
||||
);
|
||||
|
||||
appBar = TbAppBar(
|
||||
tbContext,
|
||||
title: title,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.search
|
||||
),
|
||||
onPressed: () {
|
||||
List<String> params = [];
|
||||
params.add('search=true');
|
||||
if (widget.deviceType != null) {
|
||||
params.add('deviceType=${widget.deviceType}');
|
||||
}
|
||||
if (widget.active != null) {
|
||||
params.add('active=${widget.active}');
|
||||
}
|
||||
navigateTo('/deviceList?${params.join('&')}');
|
||||
},
|
||||
)
|
||||
]);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
body: devicesList
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_deviceQueryController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,12 +14,18 @@ class DevicesMainPage extends TbContextWidget<DevicesMainPage, _DevicesMainPageS
|
||||
|
||||
}
|
||||
|
||||
class _DevicesMainPageState extends TbContextState<DevicesMainPage, _DevicesMainPageState> {
|
||||
class _DevicesMainPageState extends TbContextState<DevicesMainPage, _DevicesMainPageState> with AutomaticKeepAliveClientMixin<DevicesMainPage> {
|
||||
|
||||
final PageLinkController _pageLinkController = PageLinkController();
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController);
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_base.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_list.dart';
|
||||
import 'package:thingsboard_app/core/entity/entities_base.dart';
|
||||
import 'package:thingsboard_app/modules/device/device_profiles_grid.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
|
||||
class DevicesPage extends TbPageWidget<DevicesPage, _DevicesPageState> {
|
||||
|
||||
final String? deviceType;
|
||||
final bool? active;
|
||||
final bool searchMode;
|
||||
|
||||
DevicesPage(TbContext tbContext, {this.deviceType, this.active, this.searchMode = false}) : super(tbContext);
|
||||
DevicesPage(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
_DevicesPageState createState() => _DevicesPageState();
|
||||
@@ -20,79 +16,23 @@ class DevicesPage extends TbPageWidget<DevicesPage, _DevicesPageState> {
|
||||
|
||||
class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
|
||||
|
||||
late final DeviceQueryController _deviceQueryController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_deviceQueryController = DeviceQueryController(deviceType: widget.deviceType, active: widget.active);
|
||||
}
|
||||
final PageLinkController _pageLinkController = PageLinkController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var devicesList = DevicesList(tbContext, _deviceQueryController, searchMode: widget.searchMode, displayDeviceImage: widget.deviceType == null);
|
||||
PreferredSizeWidget appBar;
|
||||
if (widget.searchMode) {
|
||||
appBar = TbAppSearchBar(
|
||||
tbContext,
|
||||
onSearch: (searchText) => _deviceQueryController.onSearchText(searchText),
|
||||
);
|
||||
} else {
|
||||
String titleText = widget.deviceType != null ? widget.deviceType! : 'All devices';
|
||||
String? subTitleText;
|
||||
if (widget.active != null) {
|
||||
subTitleText = widget.active == true ? 'Active' : 'Inactive';
|
||||
}
|
||||
Column title = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(titleText, style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: subTitleText != null ? 16 : 20,
|
||||
height: subTitleText != null ? 20 / 16 : 24 / 20
|
||||
)),
|
||||
if (subTitleText != null)
|
||||
Text(subTitleText, style: TextStyle(
|
||||
color: Color(0x61FFFFFF),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 16 / 12
|
||||
))
|
||||
]
|
||||
);
|
||||
|
||||
appBar = TbAppBar(
|
||||
tbContext,
|
||||
title: title,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.search
|
||||
),
|
||||
onPressed: () {
|
||||
List<String> params = [];
|
||||
params.add('search=true');
|
||||
if (widget.deviceType != null) {
|
||||
params.add('deviceType=${widget.deviceType}');
|
||||
}
|
||||
if (widget.active != null) {
|
||||
params.add('active=${widget.active}');
|
||||
}
|
||||
navigateTo('/deviceList?${params.join('&')}');
|
||||
},
|
||||
)
|
||||
]);
|
||||
}
|
||||
var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
body: devicesList
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
title: Text(deviceProfilesList.title)
|
||||
),
|
||||
body: deviceProfilesList
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_deviceQueryController.dispose();
|
||||
_pageLinkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:thingsboard_app/constants/assets_path.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/core/entity/entities_list_widget.dart';
|
||||
@@ -45,8 +47,15 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> with Autom
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
elevation: dashboardState ? 0 : null,
|
||||
title: const Text('Home'),
|
||||
elevation: dashboardState ? 0 : 8,
|
||||
title: Center(
|
||||
child: Container(
|
||||
height: 24,
|
||||
child: SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle,
|
||||
color: Theme.of(context).primaryColor,
|
||||
semanticsLabel: 'ThingsBoard Logo')
|
||||
)
|
||||
),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
@@ -61,8 +70,7 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> with Autom
|
||||
}
|
||||
|
||||
Widget _buildDashboardHome(BuildContext context, HomeDashboardInfo dashboard) {
|
||||
return dashboardUi.Dashboard(tbContext, dashboardId: dashboard.dashboardId!.id!,
|
||||
fullscreen: false, home: true, hideToolbar: dashboard.hideDashboardToolbar);
|
||||
return HomeDashboard(tbContext, dashboard);
|
||||
}
|
||||
|
||||
Widget _buildDefaultHome(BuildContext context) {
|
||||
@@ -108,3 +116,29 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> with Autom
|
||||
];
|
||||
} */
|
||||
}
|
||||
|
||||
class HomeDashboard extends TbContextWidget<HomeDashboard, _HomeDashboardState> {
|
||||
|
||||
final HomeDashboardInfo dashboard;
|
||||
|
||||
HomeDashboard(TbContext tbContext, this.dashboard) : super(tbContext);
|
||||
|
||||
@override
|
||||
_HomeDashboardState createState() => _HomeDashboardState();
|
||||
|
||||
}
|
||||
|
||||
class _HomeDashboardState extends TbContextState<HomeDashboard, _HomeDashboardState> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return dashboardUi.Dashboard(tbContext,
|
||||
home: true,
|
||||
controllerCallback: (controller) {
|
||||
controller.openDashboard(widget.dashboard.dashboardId!.id!,
|
||||
hideToolbar: widget.dashboard.hideDashboardToolbar);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/modules/alarm/alarms_page.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_main_page.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_page.dart';
|
||||
import 'package:thingsboard_app/modules/home/home_page.dart';
|
||||
import 'package:thingsboard_app/modules/more/more_page.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
@@ -97,12 +95,7 @@ class MainPage extends TbPageWidget<MainPage, _MainPageState> {
|
||||
final String _path;
|
||||
|
||||
MainPage(TbContext tbContext, {required String path}):
|
||||
_path = path, super(tbContext) {
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: Theme.of(tbContext.currentState!.context).colorScheme.primary,
|
||||
systemNavigationBarIconBrightness: Brightness.dark
|
||||
));
|
||||
}
|
||||
_path = path, super(tbContext);
|
||||
|
||||
@override
|
||||
_MainPageState createState() => _MainPageState();
|
||||
@@ -122,9 +115,24 @@ class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainSt
|
||||
int currentIndex = _indexFromPath(widget._path);
|
||||
_tabController = TabController(initialIndex: currentIndex, length: _tabItems.length, vsync: this);
|
||||
_currentIndexNotifier = ValueNotifier(currentIndex);
|
||||
_tabController.addListener(() {
|
||||
_currentIndexNotifier.value = _tabController.index;
|
||||
});
|
||||
_tabController.animation!.addListener(_onTabAnimation);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.animation!.removeListener(_onTabAnimation);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_onTabAnimation () {
|
||||
var value = _tabController.animation!.value;
|
||||
var targetIndex;
|
||||
if (value >= _tabController.previousIndex) {
|
||||
targetIndex = value.round();
|
||||
} else {
|
||||
targetIndex = value.floor();
|
||||
}
|
||||
_currentIndexNotifier.value = targetIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -142,25 +150,18 @@ class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainSt
|
||||
controller: _tabController,
|
||||
children: _tabItems.map((item) => item.page).toList(),
|
||||
),
|
||||
bottomNavigationBar: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
canvasColor: Theme.of(context).colorScheme.primary
|
||||
bottomNavigationBar: ValueListenableBuilder<int>(
|
||||
valueListenable: _currentIndexNotifier,
|
||||
builder: (context, index, child) => BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
currentIndex: index,
|
||||
onTap: (int index) => _setIndex(index) /*_currentIndex = index*/,
|
||||
items: _tabItems.map((item) => BottomNavigationBarItem(
|
||||
icon: item.icon,
|
||||
label: item.title
|
||||
)).toList()
|
||||
),
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: _currentIndexNotifier,
|
||||
builder: (context, index, child) => BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedItemColor: Colors.white,
|
||||
unselectedItemColor: Colors.white.withAlpha(97),
|
||||
currentIndex: index,
|
||||
onTap: (int index) => _setIndex(index) /*_currentIndex = index*/,
|
||||
items: _tabItems.map((item) => BottomNavigationBarItem(
|
||||
icon: item.icon,
|
||||
label: item.title
|
||||
)).toList()
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -180,6 +181,11 @@ class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainSt
|
||||
_setIndex(targetIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
bool isHomePage() {
|
||||
return _tabController.index == 0;
|
||||
}
|
||||
|
||||
_setIndex(int index) {
|
||||
_tabController.index = index;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import 'package:thingsboard_app/widgets/tb_app_bar.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_client/thingsboard_client.dart';
|
||||
|
||||
class ProfilePage extends TbPageWidget<ProfilePage, _ProfilePageState> {
|
||||
|
||||
ProfilePage(TbContext tbContext) : super(tbContext);
|
||||
final bool _fullscreen;
|
||||
|
||||
ProfilePage(TbContext tbContext, {bool fullscreen = false}) : _fullscreen = fullscreen, super(tbContext);
|
||||
|
||||
@override
|
||||
_ProfilePageState createState() => _ProfilePageState();
|
||||
@@ -30,7 +33,17 @@ class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
title: const Text('Profile')
|
||||
title: const Text('Profile'),
|
||||
actions: [
|
||||
if (widget._fullscreen) IconButton(
|
||||
icon: Icon(
|
||||
Icons.logout
|
||||
),
|
||||
onPressed: () {
|
||||
tbClient.logout();
|
||||
}
|
||||
)
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<User>(
|
||||
future: userFuture,
|
||||
@@ -42,7 +55,9 @@ class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
|
||||
subtitle: Text('${user.firstName} ${user.lastName}'),
|
||||
);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
return Center(child: TbProgressIndicator(
|
||||
size: 50.0,
|
||||
));
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,7 +8,8 @@ import 'profile_page.dart';
|
||||
class ProfileRoutes extends TbRoutes {
|
||||
|
||||
late var profileHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return ProfilePage(tbContext);
|
||||
var fullscreen = params['fullscreen']?.first == 'true';
|
||||
return ProfilePage(tbContext, fullscreen: fullscreen);
|
||||
});
|
||||
|
||||
ProfileRoutes(TbContext tbContext) : super(tbContext);
|
||||
|
||||
Reference in New Issue
Block a user