Redesign. Improve dashboard page loading.

This commit is contained in:
Igor Kulikov
2021-06-03 18:53:17 +03:00
parent 00038f6c35
commit 068dbbdf0c
43 changed files with 1573 additions and 811 deletions

View File

@@ -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))
)
]
)
],
)
],
)
],
)
]
]
)
)
)
)
]
)
]
)
],
);
}
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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;
//}
//),
);
}

View File

@@ -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);
}
}

View File

@@ -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
),
)
)
],
)

View File

@@ -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();
}

View 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);
}
)
);
}
}

View 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);
}
}
)
);
}
}

View File

@@ -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))
],
),
);

View File

@@ -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);
}

View File

@@ -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,
))
],
)

View 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();
}
}

View File

@@ -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(

View File

@@ -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();
}

View File

@@ -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);
}
);
}
}

View File

@@ -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;
}

View File

@@ -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,
));
}
},
)

View File

@@ -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);