From 2e50de8b2f317044690dbc30bf21a1102ac1a2bc Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 3 Apr 2024 17:10:08 +0300 Subject: [PATCH 01/61] Corrects mobile navigation issues when a user sets up the Navigation Card Widget on the dashboard. --- lib/modules/dashboard/dashboard.dart | 179 ++++++++++++++++++--------- 1 file changed, 119 insertions(+), 60 deletions(-) diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index 3521d76c..6eed43cd 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -233,8 +233,8 @@ class _DashboardState extends TbContextState { Future tryLocalNavigation(String? path) async { log.debug("path: $path"); - if (path != null) { - var parts = path.split("/"); + if (path != null && path != '/home') { + final parts = path.split("/"); if ([ 'profile', 'devices', @@ -242,19 +242,32 @@ class _DashboardState extends TbContextState { 'dashboards', 'dashboard', 'customers', - 'auditLogs' - ].contains(parts[0])) { - if ((parts[0] == 'dashboard' || parts[0] == 'dashboards') && + 'auditLogs', + 'deviceGroups', + 'assetGroups', + 'customerGroups', + 'dashboardGroups', + 'alarms', + ].contains(parts[1])) { + var firstPart = parts[1]; + if (firstPart.endsWith('Groups')) { + firstPart = firstPart.replaceFirst('Groups', 's'); + } + + if ((firstPart == 'dashboard' || firstPart == 'dashboards') && parts.length > 1) { - var dashboardId = parts[1]; + final dashboardId = parts[1]; await navigateToDashboard(dashboardId); - } else if (parts[0] != 'dashboard') { - var targetPath = '/$path'; - if (parts[0] == 'devices' && widget._home != true) { + } else if (firstPart != 'dashboard') { + var targetPath = '/$firstPart'; + if (firstPart == 'devices' && widget._home != true) { targetPath = '/devicesPage'; } + await navigateTo(targetPath); } + } else { + throw UnimplementedError('The path $path is currently not supported.'); } } } @@ -290,62 +303,84 @@ class _DashboardState extends TbContextState { onWebViewCreated: (webViewController) { log.debug("onWebViewCreated"); webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardLoadedHandler", - callback: (args) async { - bool hasRightLayout = args[0]; - bool rightLayoutOpened = args[1]; - log.debug( - "Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened"); - _dashboardController - .onHasRightLayout(hasRightLayout); - _dashboardController - .onRightLayoutOpened(rightLayoutOpened); - dashboardLoading.value = false; - }); + handlerName: "tbMobileDashboardLoadedHandler", + callback: (args) async { + bool hasRightLayout = args[0]; + bool rightLayoutOpened = args[1]; + log.debug( + "Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened"); + _dashboardController + .onHasRightLayout(hasRightLayout); + _dashboardController + .onRightLayoutOpened(rightLayoutOpened); + dashboardLoading.value = false; + }, + ); webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardLayoutHandler", - callback: (args) async { - bool rightLayoutOpened = args[0]; - log.debug( - "Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened"); - _dashboardController - .onRightLayoutOpened(rightLayoutOpened); - }); + handlerName: "tbMobileDashboardLayoutHandler", + callback: (args) async { + bool rightLayoutOpened = args[0]; + log.debug( + "Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened"); + _dashboardController + .onRightLayoutOpened(rightLayoutOpened); + }, + ); webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardStateNameHandler", - callback: (args) async { - log.debug( - "Invoked tbMobileDashboardStateNameHandler: $args"); - if (args.isNotEmpty && args[0] is String) { - if (widget._titleCallback != null) { - widget._titleCallback!(args[0]); - } + 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? params; - if (args.length > 1) { - params = args[1]; - } - log.debug("path: $path"); - log.debug("params: $params"); - tryLocalNavigation(path); + handlerName: "tbMobileNavigationHandler", + callback: (args) async { + log.debug( + "Invoked tbMobileNavigationHandler: $args", + ); + if (args.isNotEmpty) { + late String path; + + if (args.first.contains('.')) { + path = '/${args.first.split('.').last}'; + } else { + path = '/${args.first}'; + } + + Map? params; + if (args.length > 1) { + params = args[1]; + } + + log.debug("path: $path"); + log.debug("params: $params"); + try { + await tryLocalNavigation(path); + } on UnimplementedError catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + _buildWarnSnackBar(e.message!), + ); } - }); + } + }, + ); webViewController.addJavaScriptHandler( - handlerName: "tbMobileHandler", - callback: (args) async { - log.debug("Invoked tbMobileHandler: $args"); - return await widgetActionHandler - .handleWidgetMobileAction( - args, webViewController); - }); + handlerName: "tbMobileHandler", + callback: (args) async { + log.debug("Invoked tbMobileHandler: $args"); + return await widgetActionHandler + .handleWidgetMobileAction( + args, + webViewController, + ); + }, + ); }, shouldOverrideUrlLoading: (controller, navigationAction) async { @@ -365,7 +400,13 @@ class _DashboardState extends TbContextState { if (target.startsWith("/")) { target = target.substring(1); } - await tryLocalNavigation(target); + try { + await tryLocalNavigation(target); + } on UnimplementedError catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + _buildWarnSnackBar(e.message!), + ); + } return NavigationActionPolicy.CANCEL; } } else if (await canLaunchUrlString(uriString)) { @@ -437,4 +478,22 @@ class _DashboardState extends TbContextState { ), ); } + + SnackBar _buildWarnSnackBar(String message) { + return SnackBar( + duration: const Duration(seconds: 10), + backgroundColor: Color(0xFFdc6d1b), + content: Text( + message, + style: TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Close', + textColor: Colors.white, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + ); + } } From 3e18f804296c4bac0490308ad82f16e87c40f0bb Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 4 Apr 2024 11:21:36 +0300 Subject: [PATCH 02/61] Fixed an issue: the Counter on Notification page does not increase. --- .../notification/service/notifications_local_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/modules/notification/service/notifications_local_service.dart b/lib/modules/notification/service/notifications_local_service.dart index b05dd984..88e66109 100644 --- a/lib/modules/notification/service/notifications_local_service.dart +++ b/lib/modules/notification/service/notifications_local_service.dart @@ -53,5 +53,6 @@ final class NotificationsLocalService implements INotificationsLocalService { @override Future updateNotificationsCount(int count) async { storage.setItem(notificationCounterKey, count.toString()); + notificationsNumberStream.add(count); } } From 478eb0e966d290ac3cb29902951f0fd16ec0f1fe Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 4 Apr 2024 13:47:10 +0300 Subject: [PATCH 03/61] Fixed an issue: From device's notification center doesn't open the link and the dashboard --- lib/utils/services/notification_service.dart | 26 +++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 775d0dc4..19d03dd6 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -238,20 +238,28 @@ class NotificationService { Map data, TbContext tbContext, ) { - if (data['enabled'] == true) { - switch (data['linkType']) { + if (data['enabled'] == true || data['onClick.enabled'] == 'true') { + switch (data['linkType'] ?? data['onClick.linkType']) { case 'DASHBOARD': - final dashboardId = data['dashboardId']; + final dashboardId = + data['dashboardId'] ?? data['onClick.dashboardId']; var entityId; - if (data['stateEntityId'] != null && - data['stateEntityType'] != null) { + if ((data['stateEntityId'] ?? data['onClick.stateEntityId']) != + null && + (data['stateEntityType'] ?? data['onClick.stateEntityType']) != + null) { entityId = EntityId.fromTypeAndUuid( - entityTypeFromString(data['stateEntityType']), - data['stateEntityId']); + entityTypeFromString( + data['stateEntityType'] ?? data['onClick.stateEntityType']), + data['stateEntityId'] ?? data['onClick.stateEntityId'], + ); } - final state = Utils.createDashboardEntityState(entityId, - stateId: data['dashboardState']); + final state = Utils.createDashboardEntityState( + entityId, + stateId: data['dashboardState'] ?? data['onClick.dashboardState'], + ); + if (dashboardId != null) { tbContext.navigateToDashboard(dashboardId, state: state); } From 5ab510c08985cebd239aa01255771c486c755042 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 4 Apr 2024 16:45:49 +0300 Subject: [PATCH 04/61] Fixed an issue: Android - The app icon badge doesn't disappear when user is logged out. --- lib/utils/services/notification_service.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 19d03dd6..6c8723fe 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -101,6 +101,7 @@ class NotificationService { await _messaging.setAutoInitEnabled(false); await _messaging.deleteToken(); + await flutterLocalNotificationsPlugin.cancelAll(); await _localService.clearNotificationBadgeCount(); } @@ -113,9 +114,9 @@ class NotificationService { AndroidInitializationSettings('@mipmap/thingsboard'); const initializationSettingsIOS = DarwinInitializationSettings( - defaultPresentSound: false, - defaultPresentAlert: false, - defaultPresentBadge: false, + defaultPresentSound: true, + defaultPresentAlert: true, + defaultPresentBadge: true, ); const initializationSettings = InitializationSettings( @@ -143,8 +144,7 @@ class NotificationService { showWhen: false, ); - const DarwinNotificationDetails iOSPlatformChannelSpecifics = - DarwinNotificationDetails(); + const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); _notificationDetails = NotificationDetails( android: androidPlatformChannelSpecifics, @@ -206,7 +206,7 @@ class NotificationService { } void showNotification(RemoteMessage message) async { - RemoteNotification? notification = message.notification; + final notification = message.notification; if (notification != null) { flutterLocalNotificationsPlugin.show( From 92da508fa97d6c3ff458054d4ca4c3fc37cd09fa Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 5 Apr 2024 13:37:28 +0300 Subject: [PATCH 05/61] Fixed an issue: From device's notification center doesn't open the link. --- lib/utils/services/notification_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 6c8723fe..dbfbe519 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -266,7 +266,7 @@ class NotificationService { break; case 'LINK': - final link = data['link']; + final link = data['link'] ?? data['onClick.link']; if (link != null) { if (Uri.parse(link).isAbsolute) { tbContext.navigateTo('/url/${Uri.encodeComponent(link)}'); From 1ab17c3e28c184462b9929e73697c10754a80374 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 8 Apr 2024 16:50:31 +0300 Subject: [PATCH 06/61] Notification fixes. --- lib/modules/more/more_page.dart | 2 +- lib/utils/services/notification_service.dart | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart index ec364d78..12a2a118 100644 --- a/lib/modules/more/more_page.dart +++ b/lib/modules/more/more_page.dart @@ -300,7 +300,7 @@ class MoreMenuItem { } static Widget _notificationNumberWidget(ThingsboardClient tbClient) { - NotificationsLocalService().triggerNotificationCountStream(); + NotificationService().updateNotificationsCount(); return StreamBuilder( stream: NotificationsLocalService.notificationsNumberStream.stream, diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 6c8723fe..e4a01ac6 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -18,6 +18,7 @@ class NotificationService { late ThingsboardClient _tbClient; late TbContext _tbContext; late INotificationsLocalService _localService; + late StreamSubscription _foregroundMessageSubscription; String? _fcmToken; @@ -99,6 +100,7 @@ class NotificationService { _tbClient.getUserService().removeMobileSession(_fcmToken!); } + await _foregroundMessageSubscription.cancel(); await _messaging.setAutoInitEnabled(false); await _messaging.deleteToken(); await flutterLocalNotificationsPlugin.cancelAll(); @@ -113,11 +115,7 @@ class NotificationService { const initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/thingsboard'); - const initializationSettingsIOS = DarwinInitializationSettings( - defaultPresentSound: true, - defaultPresentAlert: true, - defaultPresentBadge: true, - ); + const initializationSettingsIOS = DarwinInitializationSettings(); const initializationSettings = InitializationSettings( android: initializationSettingsAndroid, @@ -222,7 +220,8 @@ class NotificationService { } void _subscribeOnForegroundMessage() { - FirebaseMessaging.onMessage.listen((message) { + _foregroundMessageSubscription = + FirebaseMessaging.onMessage.listen((message) { _log.debug('Message:' + message.toString()); if (message.sentTime == null) { final map = message.toMap(); From 42b2e49cfe9d7dfbc02475723ff4171facb8e843 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 10 Apr 2024 12:35:16 +0300 Subject: [PATCH 07/61] Reworking getting token logic. --- lib/utils/services/notification_service.dart | 29 +++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 6432e85f..6d1cea79 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -61,10 +60,19 @@ class NotificationService { 'Notification authorizationStatus: ${settings.authorizationStatus}'); if (settings.authorizationStatus == AuthorizationStatus.authorized || settings.authorizationStatus == AuthorizationStatus.provisional) { - _getAndSaveToken(); + await _getAndSaveToken(); + FirebaseMessaging.instance.onTokenRefresh.listen((token) { + if (_fcmToken != null) { + _tbClient.getUserService().removeMobileSession(_fcmToken!).then((_) { + _fcmToken = token; + if (_fcmToken != null) { + _saveToken(_fcmToken!); + } + }); + } + }); await _initFlutterLocalNotificationsPlugin(); - await _configFirebaseMessaging(); _subscribeOnForegroundMessage(); updateNotificationsCount(); @@ -80,13 +88,14 @@ class NotificationService { } Future getToken() async { - if (Platform.isIOS) { - var apnsToken = await _messaging.getAPNSToken(); - _log.debug('APNS token: $apnsToken'); - if (apnsToken == null) { - return null; - } - } + // if (Platform.isIOS) { + // final apnsToken = await _messaging.getAPNSToken(); + // _log.debug('APNS token: $apnsToken'); + // if (apnsToken == null) { + // return null; + // } + // } + _fcmToken = await _messaging.getToken(); return _fcmToken; } From b78dbc78648713dc0ab1b9b8877d38a6326cd51b Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 10 Apr 2024 15:14:15 +0300 Subject: [PATCH 08/61] Fix notifications icon may be a null. --- lib/modules/notification/widgets/notification_icon.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/notification/widgets/notification_icon.dart b/lib/modules/notification/widgets/notification_icon.dart index 77ce0a9c..9682d011 100644 --- a/lib/modules/notification/widgets/notification_icon.dart +++ b/lib/modules/notification/widgets/notification_icon.dart @@ -10,7 +10,7 @@ class NotificationIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final iconData = _toIcon(notification.additionalConfig?['icon']); + final iconData = _toIcon(notification.additionalConfig?['icon'] ?? {}); return iconData; } From 8867abb45b5e154fc94f36af9621f5ff8f2c9f94 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 10 Apr 2024 17:17:28 +0300 Subject: [PATCH 09/61] Fixed an issue with notifications read status pagination loading. --- .../notification/controllers/notification_query_ctrl.dart | 5 ++++- .../repository/notification_pagination_repository.dart | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/modules/notification/controllers/notification_query_ctrl.dart b/lib/modules/notification/controllers/notification_query_ctrl.dart index b3b38579..3b3c7384 100644 --- a/lib/modules/notification/controllers/notification_query_ctrl.dart +++ b/lib/modules/notification/controllers/notification_query_ctrl.dart @@ -17,7 +17,10 @@ class NotificationQueryCtrl extends PageKeyController { @override PushNotificationQuery nextPageKey(PushNotificationQuery pageKey) { - return PushNotificationQuery(pageKey.pageLink.nextPageLink()); + return PushNotificationQuery( + pageKey.pageLink.nextPageLink(), + unreadOnly: value.pageKey.unreadOnly, + ); } void onSearchText(String searchText) { diff --git a/lib/modules/notification/repository/notification_pagination_repository.dart b/lib/modules/notification/repository/notification_pagination_repository.dart index b26a5b81..6bf60f07 100644 --- a/lib/modules/notification/repository/notification_pagination_repository.dart +++ b/lib/modules/notification/repository/notification_pagination_repository.dart @@ -39,8 +39,6 @@ class NotificationPaginationRepository { pageKey, ); - print(pageData.totalElements); - final isLastPage = !pageData.hasNext; if (refresh) { var state = pagingController.value; From baf8a3c273c60f8c72ba2693252ab0be710f9289 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 26 Apr 2024 11:56:14 +0300 Subject: [PATCH 10/61] App Links. (#81) * App links configuration Android. * App links Android. * App links configuration IOS. * Improved app link errors handling. * UseCase refactoring. * Unit Tests. * Firebase initialization exception handling. * UI Fixes. * Notification Service code PE align. * Fixed an issue when two instances of the app was opened by app-links. * When the user switches to another TB host through the QR code, the dashboards do not open and the dashboard image is not displayed. * Fix notification issue. --- android/app/src/main/AndroidManifest.xml | 12 +- ios/Runner/Runner.entitlements | 4 + lib/config/routes/router.dart | 3 + lib/constants/app_constants.dart | 6 +- lib/constants/database_keys.dart | 4 + lib/core/auth/auth_routes.dart | 14 +- .../remote/i_noauth_remote_datasource.dart | 19 + .../remote/noauth_remote_datasource.dart | 69 ++++ .../data/repository/noauth_repository.dart | 48 +++ lib/core/auth/noauth/di/noauth_di.dart | 52 +++ .../repository/i_noauth_repository.dart | 19 + .../usecases/switch_endpoint_usecase.dart | 82 +++++ .../auth/noauth/presentation/bloc/bloc.dart | 3 + .../noauth/presentation/bloc/noauth_bloc.dart | 80 +++++ .../presentation/bloc/noauth_events.dart | 39 +++ .../presentation/bloc/noauth_states.dart | 34 ++ .../view/switch_endpoint_noauth_view.dart | 142 ++++++++ .../widgets/endpoint_name_widget.dart | 29 ++ .../widgets/noauth_loading_widget.dart | 18 + .../auth/noauth/routes/noauth_routes.dart | 23 ++ lib/core/auth/oauth2/tb_oauth2_client.dart | 8 +- lib/core/context/tb_context.dart | 328 +++++++++++------- lib/core/logger/tb_log_output.dart | 11 + lib/core/logger/tb_logger.dart | 44 +++ lib/core/logger/tb_logs_filter.dart | 13 + lib/locator.dart | 40 +++ lib/main.dart | 177 ++++++---- lib/modules/dashboard/dashboard.dart | 24 +- lib/modules/main/main_page.dart | 7 +- lib/modules/more/more_page.dart | 50 ++- .../service/notifications_local_service.dart | 7 +- .../widgets/notification_icon.dart | 6 + .../widgets/notification_slidable_widget.dart | 2 +- .../services/endpoint/endpoint_service.dart | 41 +++ .../services/endpoint/i_endpoint_service.dart | 14 + .../services/firebase/firebase_service.dart | 53 +++ .../services/firebase/i_firebase_service.dart | 11 + .../i_local_database_service.dart | 6 + .../local_database_service.dart | 31 ++ lib/utils/services/notification_service.dart | 58 ++-- lib/utils/usecase.dart | 9 + lib/utils/utils.dart | 11 +- lib/widgets/two_page_view.dart | 2 +- pubspec.yaml | 6 + test/ core/noauth/switch_endpoint_test.dart | 250 +++++++++++++ test/mocks.dart | 10 + 46 files changed, 1649 insertions(+), 270 deletions(-) create mode 100644 lib/constants/database_keys.dart create mode 100644 lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart create mode 100644 lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart create mode 100644 lib/core/auth/noauth/data/repository/noauth_repository.dart create mode 100644 lib/core/auth/noauth/di/noauth_di.dart create mode 100644 lib/core/auth/noauth/domain/repository/i_noauth_repository.dart create mode 100644 lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/bloc.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/noauth_events.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/noauth_states.dart create mode 100644 lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart create mode 100644 lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart create mode 100644 lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart create mode 100644 lib/core/auth/noauth/routes/noauth_routes.dart create mode 100644 lib/core/logger/tb_log_output.dart create mode 100644 lib/core/logger/tb_logger.dart create mode 100644 lib/core/logger/tb_logs_filter.dart create mode 100644 lib/locator.dart create mode 100644 lib/utils/services/endpoint/endpoint_service.dart create mode 100644 lib/utils/services/endpoint/i_endpoint_service.dart create mode 100644 lib/utils/services/firebase/firebase_service.dart create mode 100644 lib/utils/services/firebase/i_firebase_service.dart create mode 100644 lib/utils/services/local_database/i_local_database_service.dart create mode 100644 lib/utils/services/local_database/local_database_service.dart create mode 100644 lib/utils/usecase.dart create mode 100644 test/ core/noauth/switch_endpoint_test.dart create mode 100644 test/mocks.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 922bb509..b43c30eb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -38,7 +38,7 @@ + + + + + + + aps-environment development + com.apple.developer.associated-domains + + applinks:qr.thingsboard.fun?mode=developer + diff --git a/lib/config/routes/router.dart b/lib/config/routes/router.dart index 2a037cf6..4c09604c 100644 --- a/lib/config/routes/router.dart +++ b/lib/config/routes/router.dart @@ -1,6 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/auth/auth_routes.dart'; +import 'package:thingsboard_app/core/auth/noauth/routes/noauth_routes.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/init/init_routes.dart'; import 'package:thingsboard_app/modules/alarm/alarm_routes.dart'; @@ -29,6 +30,7 @@ class ThingsboardAppRouter { body: Center(child: Text('Route not defined: ${settings!.name}')), ); }); + InitRoutes(_tbContext).registerRoutes(); AuthRoutes(_tbContext).registerRoutes(); UiUtilsRoutes(_tbContext).registerRoutes(); @@ -43,6 +45,7 @@ class ThingsboardAppRouter { TenantRoutes(_tbContext).registerRoutes(); NotificationRoutes(_tbContext).registerRoutes(); UrlPageRoutes(_tbContext).registerRoutes(); + NoAuthRoutes(_tbContext).registerRoutes(); } TbContext get tbContext => _tbContext; diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index 8a85a500..be31ca13 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -1,7 +1,7 @@ abstract class ThingsboardAppConstants { - static final thingsBoardApiEndpoint = 'http://localhost:8080'; - static final thingsboardOAuth2CallbackUrlScheme = 'org.thingsboard.app.auth'; + static const thingsBoardApiEndpoint = 'http://localhost:8080'; + static const thingsboardOAuth2CallbackUrlScheme = 'org.thingsboard.app.auth'; /// Not for production (only for debugging) - static final thingsboardOAuth2AppSecret = 'Your app secret here'; + static const thingsboardOAuth2AppSecret = 'Your app secret here'; } diff --git a/lib/constants/database_keys.dart b/lib/constants/database_keys.dart new file mode 100644 index 00000000..4c1eeaba --- /dev/null +++ b/lib/constants/database_keys.dart @@ -0,0 +1,4 @@ +abstract final class DatabaseKeys { + static const thingsBoardApiEndpointKey = 'thingsBoardApiEndpoint'; + static const initialAppLink = 'initialAppLink'; +} diff --git a/lib/core/auth/auth_routes.dart b/lib/core/auth/auth_routes.dart index ba6ddb81..527eb967 100644 --- a/lib/core/auth/auth_routes.dart +++ b/lib/core/auth/auth_routes.dart @@ -28,9 +28,15 @@ class AuthRoutes extends TbRoutes { @override void doRegisterRoutes(router) { - router.define("/login", handler: loginHandler); - router.define("/login/resetPasswordRequest", - handler: resetPasswordRequestHandler); - router.define("/login/mfa", handler: twoFactorAuthenticationHandler); + router + ..define('/login', handler: loginHandler) + ..define( + '/login/resetPasswordRequest', + handler: resetPasswordRequestHandler, + ) + ..define( + '/login/mfa', + handler: twoFactorAuthenticationHandler, + ); } } diff --git a/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart new file mode 100644 index 00000000..80c39920 --- /dev/null +++ b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract interface class INoAuthRemoteDatasource { + Future getJwtToken({ + required String host, + required String key, + }); + + Future setUserFromJwtToken(LoginResponse loginData); + + Future logout({RequestConfig? requestConfig, bool notifyUser = true}); + + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }); +} diff --git a/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart new file mode 100644 index 00000000..712fa35d --- /dev/null +++ b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart @@ -0,0 +1,69 @@ +import 'dart:ui'; + +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NoAuthRemoteDatasource implements INoAuthRemoteDatasource { + const NoAuthRemoteDatasource({ + required this.thingsboardClient, + required this.tbLogger, + required this.tbContext, + }); + + final ThingsboardClient thingsboardClient; + final TbLogger tbLogger; + final TbContext tbContext; + + @override + Future getJwtToken({ + required String host, + required String key, + }) async { + try { + final data = await thingsboardClient.getLoginDataBySecretKey( + host: host, + key: key, + ); + + return data; + } catch (e) { + tbLogger.error('NoAuthRemoteDatasource:getJwtToken() message $e'); + rethrow; + } + } + + @override + Future setUserFromJwtToken(LoginResponse loginData) async { + await thingsboardClient.setUserFromJwtToken( + loginData.token, + loginData.refreshToken, + false, + ); + } + + @override + Future logout({ + RequestConfig? requestConfig, + bool notifyUser = true, + }) async { + await tbContext.logout( + requestConfig: requestConfig, + notifyUser: notifyUser, + ); + } + + @override + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }) async { + await tbContext.reInit( + endpoint: endpoint, + onDone: onDone, + onError: onError, + ); + } +} diff --git a/lib/core/auth/noauth/data/repository/noauth_repository.dart b/lib/core/auth/noauth/data/repository/noauth_repository.dart new file mode 100644 index 00000000..625cb937 --- /dev/null +++ b/lib/core/auth/noauth/data/repository/noauth_repository.dart @@ -0,0 +1,48 @@ +import 'dart:ui'; + +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NoAuthRepository implements INoAuthRepository { + const NoAuthRepository({required this.remoteDatasource}); + + final INoAuthRemoteDatasource remoteDatasource; + + @override + Future getJwtToken({ + required String host, + required String key, + }) { + return remoteDatasource.getJwtToken(host: host, key: key); + } + + @override + Future setUserFromJwtToken(LoginResponse loginData) async { + await remoteDatasource.setUserFromJwtToken(loginData); + } + + @override + Future logout({ + RequestConfig? requestConfig, + bool notifyUser = true, + }) async { + await remoteDatasource.logout( + requestConfig: requestConfig, + notifyUser: notifyUser, + ); + } + + @override + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }) async { + await remoteDatasource.reInit( + endpoint: endpoint, + onDone: onDone, + onError: onError, + ); + } +} diff --git a/lib/core/auth/noauth/di/noauth_di.dart b/lib/core/auth/noauth/di/noauth_di.dart new file mode 100644 index 00000000..2e1bf4c8 --- /dev/null +++ b/lib/core/auth/noauth/di/noauth_di.dart @@ -0,0 +1,52 @@ +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/auth/noauth/data/repository/noauth_repository.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; + +abstract final class NoAuthDi { + static void init({required TbContext tbContext}) { + getIt.pushNewScope( + scopeName: 'NoAuthDi', + init: (locator) { + // Datasource + locator.registerFactory( + () => NoAuthRemoteDatasource( + thingsboardClient: tbContext.tbClient, + tbLogger: locator(), + tbContext: tbContext, + ), + ); + + // Repository + locator.registerFactory( + () => NoAuthRepository( + remoteDatasource: locator(), + ), + ); + + // UseCases + locator.registerFactory( + () => SwitchEndpointUseCase( + repository: locator(), + logger: locator(), + ), + ); + + // Bloc + locator.registerLazySingleton( + () => NoAuthBloc( + switchEndpointUseCase: locator(), + ), + ); + }, + ); + } + + static void dispose() { + getIt.dropScope('NoAuthDi'); + } +} diff --git a/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart new file mode 100644 index 00000000..43039ab4 --- /dev/null +++ b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract interface class INoAuthRepository { + Future getJwtToken({ + required String host, + required String key, + }); + + Future setUserFromJwtToken(LoginResponse loginData); + + Future logout({RequestConfig? requestConfig, bool notifyUser = true}); + + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }); +} diff --git a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart new file mode 100644 index 00000000..a3707d65 --- /dev/null +++ b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/usecase.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +final class SwitchEndpointParams { + const SwitchEndpointParams({ + required this.data, + required this.onDone, + required this.onError, + }); + + final Map data; + final VoidCallback onDone; + final Function(String) onError; + + dynamic operator [](String key) => data[key]; +} + +class SwitchEndpointUseCase extends UseCase { + SwitchEndpointUseCase({ + required this.repository, + required this.logger, + }) : _progressSteamCtrl = StreamController.broadcast(); + + final INoAuthRepository repository; + + final TbLogger logger; + late final StreamController _progressSteamCtrl; + + Stream get stream => _progressSteamCtrl.stream; + + @override + Future call(SwitchEndpointParams params) async { + final uri = params['uri']!; + final host = params['host'] ?? uri.origin; + final key = params['secret']!; + + try { + _progressSteamCtrl.add('Getting data from your host $host'); + final loginData = await repository.getJwtToken(host: host, key: key); + _progressSteamCtrl.add('Logout you ...'); + + await repository.logout( + requestConfig: RequestConfig(ignoreErrors: true), + notifyUser: false, + ); + + _progressSteamCtrl.add('Switching you to the new host $host'); + await repository.setUserFromJwtToken(loginData); + + logger.debug('SwitchEndpointUseCase:deleteFB App'); + await getIt().removeApp(); + + await getIt().setEndpoint(host); + await repository.reInit( + endpoint: host, + onDone: params.onDone, + onError: (error) { + logger.error('SwitchEndpointUseCase:onError $error'); + params.onError(error.message ?? error.toString()); + }, + ); + } on ThingsboardError catch (e) { + logger.error('SwitchEndpointUseCase:ThingsboardError $e', e); + params.onError(e.message ?? e.toString()); + } catch (e) { + logger.error('SwitchEndpointUseCase:catch $e', e); + params.onError(e.toString()); + } + } + + void dispose() { + _progressSteamCtrl.close(); + } +} diff --git a/lib/core/auth/noauth/presentation/bloc/bloc.dart b/lib/core/auth/noauth/presentation/bloc/bloc.dart new file mode 100644 index 00000000..6b18dcaa --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'noauth_bloc.dart'; +export 'noauth_events.dart'; +export 'noauth_states.dart'; diff --git a/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart b/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart new file mode 100644 index 00000000..a8289e76 --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; + +class NoAuthBloc extends Bloc { + NoAuthBloc({required this.switchEndpointUseCase}) + : super(const NoAuthLoadingState()) { + switchEndpointProgressSubscription = switchEndpointUseCase.stream.listen( + (event) => add(SwitchEndpointProgressUpdateEvent(progressMessage: event)), + ); + + _switchEndpointEventHandler(); + _switchEndpointUpdatesHandler(); + _switchEndpointDoneEvent(); + _switchEndpointErrorEvent(); + } + + final SwitchEndpointUseCase switchEndpointUseCase; + late final StreamSubscription switchEndpointProgressSubscription; + + void _switchEndpointEventHandler() { + on( + (event, emit) async { + if (event.parameters == null) { + emit( + const NoAuthErrorState( + message: 'An empty request data received.', + ), + ); + return; + } + + switchEndpointUseCase( + SwitchEndpointParams( + data: event.parameters!, + onDone: () { + add(const SwitchEndpointDoneEvent()); + }, + onError: (message) { + add(SwitchEndpointErrorEvent(message: message)); + }, + ), + ); + }, + ); + } + + void _switchEndpointUpdatesHandler() { + on( + (event, emit) async { + emit(NoAuthWipState(currentStateMessage: event.progressMessage)); + }, + ); + } + + void _switchEndpointDoneEvent() { + on( + (event, emit) async { + emit(const NoAuthDoneState()); + }, + ); + } + + void _switchEndpointErrorEvent() { + on( + (event, emit) async { + emit(NoAuthErrorState(message: event.message)); + }, + ); + } + + @override + Future close() { + switchEndpointProgressSubscription.cancel(); + switchEndpointUseCase.dispose(); + return super.close(); + } +} diff --git a/lib/core/auth/noauth/presentation/bloc/noauth_events.dart b/lib/core/auth/noauth/presentation/bloc/noauth_events.dart new file mode 100644 index 00000000..b3411e5c --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/noauth_events.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +sealed class NoAuthEvent extends Equatable { + const NoAuthEvent(); + + @override + List get props => []; +} + +final class SwitchToAnotherEndpointEvent extends NoAuthEvent { + const SwitchToAnotherEndpointEvent({required this.parameters}); + + final Map? parameters; + + @override + List get props => [parameters]; +} + +final class SwitchEndpointProgressUpdateEvent extends NoAuthEvent { + const SwitchEndpointProgressUpdateEvent({required this.progressMessage}); + + final String progressMessage; + + @override + List get props => [progressMessage]; +} + +final class SwitchEndpointDoneEvent extends NoAuthEvent { + const SwitchEndpointDoneEvent(); +} + +final class SwitchEndpointErrorEvent extends NoAuthEvent { + const SwitchEndpointErrorEvent({required this.message}); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/core/auth/noauth/presentation/bloc/noauth_states.dart b/lib/core/auth/noauth/presentation/bloc/noauth_states.dart new file mode 100644 index 00000000..aefd94f3 --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/noauth_states.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +sealed class NoAuthState extends Equatable { + const NoAuthState(); + + @override + List get props => []; +} + +final class NoAuthLoadingState extends NoAuthState { + const NoAuthLoadingState(); +} + +final class NoAuthWipState extends NoAuthState { + const NoAuthWipState({required this.currentStateMessage}); + + final String currentStateMessage; + + @override + List get props => [currentStateMessage]; +} + +final class NoAuthErrorState extends NoAuthState { + const NoAuthErrorState({required this.message}); + + final String message; + + @override + List get props => [message]; +} + +final class NoAuthDoneState extends NoAuthState { + const NoAuthDoneState(); +} diff --git a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart new file mode 100644 index 00000000..4c43ac70 --- /dev/null +++ b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/auth/noauth/di/noauth_di.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; + +class SwitchEndpointNoAuthView extends StatefulWidget { + SwitchEndpointNoAuthView({ + required this.tbContext, + required this.arguments, + }); + + final Map? arguments; + final TbContext tbContext; + + @override + State createState() => _SwitchEndpointNoAuthViewState(); +} + +class _SwitchEndpointNoAuthViewState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: GetIt.instance() + ..add( + SwitchToAnotherEndpointEvent( + parameters: widget.arguments, + ), + ), + child: SafeArea( + bottom: false, + child: Scaffold( + body: BlocConsumer( + listener: (context, state) { + if (state is NoAuthErrorState) { + widget.tbContext.showErrorNotification(state.message); + Future.delayed(const Duration(seconds: 5), () { + if (mounted) { + widget.tbContext.pop(); + } + }); + } else if (state is NoAuthDoneState) { + getIt().router.navigateTo( + context, + '/home', + replace: true, + maintainState: false, + ); + } + }, + buildWhen: (_, state) => state is! NoAuthDoneState, + builder: (context, state) { + switch (state) { + case NoAuthLoadingState(): + return const NoAuthLoadingWidget(); + + case NoAuthWipState(): + return Stack( + alignment: AlignmentDirectional.center, + children: [ + const NoAuthLoadingWidget(), + Positioned( + top: MediaQuery.of(context).size.height / 2 + 80, + child: BlocBuilder( + buildWhen: (_, state) => state is NoAuthWipState, + builder: (context, state) { + if (state is NoAuthWipState) { + return SizedBox( + width: MediaQuery.of(context).size.width - 20, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + state.currentStateMessage, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge, + ), + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + }, + ), + ), + ], + ); + + case NoAuthErrorState(): + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 10), + Text( + 'Something went wrong ... Rollback', + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.w500), + ), + ], + ), + ); + + default: + return const SizedBox.shrink(); + } + }, + ), + ), + ), + ); + } + + @override + void initState() { + NoAuthDi.init(tbContext: widget.tbContext); + super.initState(); + } + + @override + void dispose() { + GetIt.instance().close(); + NoAuthDi.dispose(); + super.dispose(); + } +} diff --git a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart new file mode 100644 index 00000000..72364335 --- /dev/null +++ b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class EndpointNameWidget extends StatelessWidget { + const EndpointNameWidget({required this.endpoint}); + + final String endpoint; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all( + color: Color(0xFF305680), + ), + ), + padding: const EdgeInsets.all(5), + child: Center( + child: Text( + Uri.parse(endpoint).host, + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(color: Color(0xFF305680)), + ), + ), + ); + } +} diff --git a/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart b/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart new file mode 100644 index 00000000..4ee5d30d --- /dev/null +++ b/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; + +class NoAuthLoadingWidget extends StatelessWidget { + const NoAuthLoadingWidget(); + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Container( + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator(size: 50.0), + ), + ), + ); + } +} diff --git a/lib/core/auth/noauth/routes/noauth_routes.dart b/lib/core/auth/noauth/routes/noauth_routes.dart new file mode 100644 index 00000000..bf396f90 --- /dev/null +++ b/lib/core/auth/noauth/routes/noauth_routes.dart @@ -0,0 +1,23 @@ +import 'package:fluro/fluro.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart'; + +class NoAuthRoutes extends TbRoutes { + NoAuthRoutes(super.tbContext); + + static const noAuthPageRoutes = '/api/noauth/qr'; + + late final noAuthQrHandler = Handler( + handlerFunc: (context, params) { + return SwitchEndpointNoAuthView( + tbContext: tbContext, + arguments: context?.settings?.arguments as Map?, + ); + }, + ); + + @override + void doRegisterRoutes(FluroRouter router) { + router.define(noAuthPageRoutes, handler: noAuthQrHandler); + } +} diff --git a/lib/core/auth/oauth2/tb_oauth2_client.dart b/lib/core/auth/oauth2/tb_oauth2_client.dart index aada0433..d9435251 100644 --- a/lib/core/auth/oauth2/tb_oauth2_client.dart +++ b/lib/core/auth/oauth2/tb_oauth2_client.dart @@ -1,11 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_app/core/auth/web/tb_web_auth.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; -import 'package:crypto/crypto.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'app_secret_provider.dart'; @@ -45,7 +47,7 @@ class TbOAuth2Client { final appToken = jwt.sign(key, algorithm: _HMACBase64Algorithm.HS512, expiresIn: Duration(minutes: 2)); var url = - Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + oauth2Url); + Uri.parse(await getIt().getEndpoint() + oauth2Url); final params = Map.from(url.queryParameters); params['pkg'] = pkgName; params['appToken'] = appToken; diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index 8f03eb03..dccc0043 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -1,83 +1,29 @@ import 'dart:async'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:fluro/fluro.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:logger/logger.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:thingsboard_app/constants/app_constants.dart'; +import 'package:thingsboard_app/constants/database_keys.dart'; import 'package:thingsboard_app/core/auth/oauth2/app_secret_provider.dart'; import 'package:thingsboard_app/core/auth/oauth2/tb_oauth2_client.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/main/main_page.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; import 'package:thingsboard_app/utils/services/notification_service.dart'; -import 'package:thingsboard_app/utils/services/tb_app_storage.dart'; import 'package:thingsboard_app/utils/services/widget_action_handler.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:uni_links/uni_links.dart'; import 'package:universal_platform/universal_platform.dart'; enum NotificationType { info, warn, success, error } -class TbLogOutput extends LogOutput { - @override - void output(OutputEvent event) { - for (var line in event.lines) { - debugPrint(line); - } - } -} - -class TbLogsFilter extends LogFilter { - @override - bool shouldLog(LogEvent event) { - if (kReleaseMode) { - return event.level.index >= Level.warning.index; - } else { - return true; - } - } -} - -class TbLogger { - final _logger = Logger( - filter: TbLogsFilter(), - printer: PrefixPrinter(PrettyPrinter( - methodCount: 0, - errorMethodCount: 8, - lineLength: 200, - colors: false, - printEmojis: true, - printTime: false)), - output: TbLogOutput()); - - void trace(dynamic message, [dynamic error, StackTrace? stackTrace]) { - _logger.t(message, error: error, stackTrace: stackTrace); - } - - void debug(dynamic message, [dynamic error, StackTrace? stackTrace]) { - _logger.d(message, error: error, stackTrace: stackTrace); - } - - void info(dynamic message, [dynamic error, StackTrace? stackTrace]) { - _logger.i(message, error: error, stackTrace: stackTrace); - } - - void warn(dynamic message, [dynamic error, StackTrace? stackTrace]) { - _logger.w(message, error: error, stackTrace: stackTrace); - } - - void error(dynamic message, [dynamic error, StackTrace? stackTrace]) { - _logger.e(message, error: error, stackTrace: stackTrace); - } - - void fatal(dynamic message, [dynamic error, StackTrace? stackTrace]) { - _logger.f(message, error: error, stackTrace: stackTrace); - } -} - typedef OpenDashboardCallback = void Function(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar}); @@ -105,7 +51,7 @@ class TbContext implements PopEntry { static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); bool _initialized = false; bool isUserLoaded = false; - final ValueNotifier _isAuthenticated = ValueNotifier(false); + final _isAuthenticated = ValueNotifier(false); PlatformType? _oauth2PlatformType; List? oauth2ClientInfos; List? twoFactorAuthProviders; @@ -119,6 +65,7 @@ class TbContext implements PopEntry { late final String packageName; TbMainDashboardHolder? _mainDashboardHolder; bool _closeMainFirst = false; + StreamSubscription? _appLinkStreamSubscription; final ValueNotifier canPopNotifier = ValueNotifier(false); @@ -126,12 +73,19 @@ class TbContext implements PopEntry { GlobalKey messengerKey = GlobalKey(); - late final ThingsboardClient tbClient; - late final TbOAuth2Client oauth2Client; + late ThingsboardClient tbClient; + late TbOAuth2Client oauth2Client; final FluroRouter router; final RouteObserver routeObserver = RouteObserver(); + Listenable get isAuthenticatedListenable => _isAuthenticated; + + bool get isAuthenticated => _isAuthenticated.value; + + bool get hasOAuthClients => + oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty; + TbContextState? currentState; TbContext(this.router) { @@ -149,18 +103,26 @@ class TbContext implements PopEntry { } return true; }()); + _initialized = true; - var storage = createAppStorage(); - tbClient = ThingsboardClient(ThingsboardAppConstants.thingsBoardApiEndpoint, - storage: storage, - onUserLoaded: onUserLoaded, - onError: onError, - onLoadStarted: onLoadStarted, - onLoadFinished: onLoadFinished, - computeFunc: (callback, message) => compute(callback, message)); + + final endpoint = await getIt().getEndpoint(); + log.debug('TbContext::init() endpoint: $endpoint'); + + tbClient = ThingsboardClient( + endpoint, + storage: getIt(), + onUserLoaded: onUserLoaded, + onError: onError, + onLoadStarted: onLoadStarted, + onLoadFinished: onLoadFinished, + computeFunc: (callback, message) => compute(callback, message), + ); oauth2Client = TbOAuth2Client( - tbContext: this, appSecretProvider: AppSecretProvider.local()); + tbContext: this, + appSecretProvider: AppSecretProvider.local(), + ); try { if (UniversalPlatform.isAndroid) { @@ -185,6 +147,34 @@ class TbContext implements PopEntry { } } + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }) async { + log.debug('TbContext:reinit()'); + + _initialized = false; + + tbClient = ThingsboardClient( + endpoint, + storage: getIt(), + onUserLoaded: () => onUserLoaded(handleRouteState: false, onDone: onDone), + onError: onError, + onLoadStarted: onLoadStarted, + onLoadFinished: onLoadFinished, + computeFunc: (callback, message) => compute(callback, message), + ); + + oauth2Client = TbOAuth2Client( + tbContext: this, + appSecretProvider: AppSecretProvider.local(), + ); + + await tbClient.init(); + _initialized = true; + } + void setMainDashboardHolder(TbMainDashboardHolder holder) { _mainDashboardHolder = holder; } @@ -263,18 +253,20 @@ class TbContext implements PopEntry { } void onLoadStarted() { - log.debug('On load started.'); + log.debug('TbContext: On load started.'); _isLoadingNotifier.value = true; } - void onLoadFinished() { - log.debug('On load finished.'); + void onLoadFinished() async { + log.debug('TbContext: On load finished.'); _isLoadingNotifier.value = false; } - Future onUserLoaded() async { + Future onUserLoaded( + {bool handleRouteState = true, VoidCallback? onDone}) async { try { - log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}'); + log.debug( + 'TbContext.onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}'); isUserLoaded = true; if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { log.debug('authUser: ${tbClient.getAuthUser()}'); @@ -300,45 +292,106 @@ class TbContext implements PopEntry { } else { twoFactorAuthProviders = null; } + userDetails = null; homeDashboard = null; oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients( - pkgName: packageName, platform: _oauth2PlatformType); + pkgName: packageName, + platform: _oauth2PlatformType, + ); } + _isAuthenticated.value = tbClient.isAuthenticated() && !tbClient.isPreVerificationToken(); - await updateRouteState(); + + if (isAuthenticated) { + onDone?.call(); + } + + if (handleRouteState) { + await updateRouteState(); + } + if (tbClient.getAuthUser()?.userId != null) { - if (Firebase.apps.isNotEmpty) { - NotificationService().init(tbClient, log, this); + if (getIt().apps.isNotEmpty) { + await NotificationService().init(tbClient, log, this); } } } catch (e, s) { - log.error('Error: $e', e, s); + log.error('TbContext.onUserLoaded: $e', e, s); + if (_isConnectionError(e)) { - var res = await confirm( - title: 'Connection error', - message: 'Failed to connect to server', - cancel: 'Cancel', - ok: 'Retry'); + final res = await confirm( + title: 'Connection error', + message: 'Failed to connect to server', + cancel: 'Cancel', + ok: 'Retry', + ); if (res == true) { onUserLoaded(); } else { - navigateTo('/login', - replace: true, - clearStack: true, - transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750)); + navigateTo( + '/login', + replace: true, + clearStack: true, + transition: TransitionType.fadeIn, + transitionDuration: Duration(milliseconds: 750), + ); } } + } finally { + try { + final link = await getIt().getItem( + DatabaseKeys.initialAppLink, + ); + _navigateByAppLink(link); + } catch (e) { + log.error('TbContext:getInitialUri() exception $e'); + } + + if (_appLinkStreamSubscription == null) { + _appLinkStreamSubscription = linkStream.listen((link) { + _navigateByAppLink(link); + }, onError: (err) { + log.error('linkStream.listen $err'); + }); + } } } - Future logout({RequestConfig? requestConfig}) async { - if (Firebase.apps.isNotEmpty) { + Future _navigateByAppLink(String? link) async { + if (link != null) { + final uri = Uri.parse(link); + await getIt().deleteItem( + DatabaseKeys.initialAppLink, + ); + + log.debug('TbContext: navigate by appLink $uri'); + router.navigateTo( + currentState!.context, + uri.path, + routeSettings: RouteSettings( + arguments: {...uri.queryParameters, 'uri': uri}, + ), + ); + } + } + + Future logout({ + RequestConfig? requestConfig, + bool notifyUser = true, + }) async { + if (getIt().apps.isNotEmpty) { await NotificationService().logout(); } - tbClient.logout(requestConfig: requestConfig); + + await tbClient.logout( + requestConfig: requestConfig, + notifyUser: notifyUser, + ); + + _appLinkStreamSubscription?.cancel(); + _appLinkStreamSubscription = null; } bool _isConnectionError(e) { @@ -347,41 +400,45 @@ class TbContext implements PopEntry { e.message == 'Unable to connect'; } - Listenable get isAuthenticatedListenable => _isAuthenticated; - - bool get isAuthenticated => _isAuthenticated.value; - - bool get hasOAuthClients => - oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty; - Future updateRouteState() async { - if (currentState != null) { + log.debug( + 'TbContext:updateRouteState() ${currentState != null && currentState!.mounted}'); + if (currentState != null && currentState!.mounted) { if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { - var defaultDashboardId = _defaultDashboardId(); + final defaultDashboardId = _defaultDashboardId(); if (defaultDashboardId != null) { bool fullscreen = _userForceFullscreen(); if (!fullscreen) { await navigateToDashboard(defaultDashboardId, animate: false); - navigateTo('/home', - replace: true, - closeDashboard: false, - transition: TransitionType.none); + navigateTo( + '/home', + replace: true, + closeDashboard: false, + transition: TransitionType.none, + ); } else { - navigateTo('/fullscreenDashboard/$defaultDashboardId', - replace: true, transition: TransitionType.fadeIn); - } - } else { - navigateTo('/home', + navigateTo( + '/fullscreenDashboard/$defaultDashboardId', replace: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750)); - } - } else { - navigateTo('/login', + ); + } + } else { + navigateTo( + '/home', replace: true, - clearStack: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750)); + transitionDuration: Duration(milliseconds: 750), + ); + } + } else { + navigateTo( + '/login', + replace: true, + clearStack: true, + transition: TransitionType.fadeIn, + transitionDuration: Duration(milliseconds: 750), + ); } } } @@ -433,13 +490,16 @@ class TbContext implements PopEntry { return false; } - Future navigateTo(String path, - {bool replace = false, - bool clearStack = false, - closeDashboard = true, - TransitionType? transition, - Duration? transitionDuration, - bool restoreDashboard = true}) async { + Future navigateTo( + String path, { + bool replace = false, + bool clearStack = false, + closeDashboard = true, + TransitionType? transition, + Duration? transitionDuration, + bool restoreDashboard = true, + RouteSettings? routeSettings, + }) async { if (currentState != null) { hideNotification(); bool isOpenedDashboard = @@ -468,11 +528,15 @@ class TbContext implements PopEntry { } } _closeMainFirst = isOpenedDashboard; - return await router.navigateTo(currentState!.context, path, - transition: transition, - transitionDuration: transitionDuration, - replace: replace, - clearStack: clearStack); + return await router.navigateTo( + currentState!.context, + path, + transition: transition, + transitionDuration: transitionDuration, + replace: replace, + clearStack: clearStack, + routeSettings: routeSettings, + ); } } diff --git a/lib/core/logger/tb_log_output.dart b/lib/core/logger/tb_log_output.dart new file mode 100644 index 00000000..2eaaf6e8 --- /dev/null +++ b/lib/core/logger/tb_log_output.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class TbLogOutput extends LogOutput { + @override + void output(OutputEvent event) { + for (final line in event.lines) { + debugPrint(line); + } + } +} diff --git a/lib/core/logger/tb_logger.dart b/lib/core/logger/tb_logger.dart new file mode 100644 index 00000000..a57332ee --- /dev/null +++ b/lib/core/logger/tb_logger.dart @@ -0,0 +1,44 @@ +import 'package:logger/logger.dart'; +import 'package:thingsboard_app/core/logger/tb_log_output.dart'; +import 'package:thingsboard_app/core/logger/tb_logs_filter.dart'; + +class TbLogger { + final _logger = Logger( + filter: TbLogsFilter(), + printer: PrefixPrinter( + PrettyPrinter( + methodCount: 0, + errorMethodCount: 8, + lineLength: 200, + colors: false, + printEmojis: true, + printTime: false, + ), + ), + output: TbLogOutput(), + ); + + void trace(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.t(message, error: error, stackTrace: stackTrace); + } + + void debug(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.d(message, error: error, stackTrace: stackTrace); + } + + void info(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.i(message, error: error, stackTrace: stackTrace); + } + + void warn(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.w(message, error: error, stackTrace: stackTrace); + } + + void error(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.e(message, error: error, stackTrace: stackTrace); + } + + void fatal(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.f(message, error: error, stackTrace: stackTrace); + } +} diff --git a/lib/core/logger/tb_logs_filter.dart b/lib/core/logger/tb_logs_filter.dart new file mode 100644 index 00000000..4711a0e1 --- /dev/null +++ b/lib/core/logger/tb_logs_filter.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class TbLogsFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + if (kReleaseMode) { + return event.level.index >= Level.warning.index; + } else { + return true; + } + } +} diff --git a/lib/locator.dart b/lib/locator.dart new file mode 100644 index 00000000..6b679151 --- /dev/null +++ b/lib/locator.dart @@ -0,0 +1,40 @@ +import 'package:get_it/get_it.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/utils/services/endpoint/endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/local_database_service.dart'; +import 'package:thingsboard_app/utils/services/tb_app_storage.dart'; + +import 'utils/services/firebase/firebase_service.dart'; + +final getIt = GetIt.instance; + +void setUpRootDependencies() { + getIt + ..registerSingleton( + ThingsboardAppRouter(), + ) + ..registerLazySingleton( + () => TbLogger(), + ) + ..registerLazySingleton( + () => LocalDatabaseService( + storage: createAppStorage(), + logger: getIt(), + ), + ) + ..registerLazySingleton( + () => EndpointService( + databaseService: getIt(), + ), + ) + ..registerLazySingleton( + () => FirebaseService( + logger: getIt(), + endpointService: getIt(), + ), + ); +} diff --git a/lib/main.dart b/lib/main.dart index 26162ed5..0824e0a2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,37 +1,52 @@ import 'dart:developer'; -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/constants/database_keys.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/firebase_options.dart'; +import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/dashboard/main_dashboard_page.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; import 'package:thingsboard_app/widgets/two_page_view.dart'; +import 'package:uni_links/uni_links.dart'; import 'package:universal_platform/universal_platform.dart'; import 'config/themes/tb_theme.dart'; import 'generated/l10n.dart'; -final appRouter = ThingsboardAppRouter(); - void main() async { WidgetsFlutterBinding.ensureInitialized(); // await FlutterDownloader.initialize(); // await Permission.storage.request(); + setUpRootDependencies(); if (UniversalPlatform.isAndroid) { await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true); } try { - await Firebase.initializeApp( + getIt().initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); } catch (e) { - log(e.toString()); + log('main::FirebaseService.initializeApp() exception $e', error: e); + } + + try { + final uri = await getInitialUri(); + if (uri != null) { + await getIt().setItem( + DatabaseKeys.initialAppLink, + uri.toString(), + ); + } + } catch (e) { + log('main::getInitialUri() exception $e', error: e); } runApp(ThingsboardApp()); @@ -47,39 +62,47 @@ class ThingsboardApp extends StatefulWidget { class ThingsboardAppState extends State with TickerProviderStateMixin implements TbMainDashboardHolder { - final TwoPageViewController _mainPageViewController = TwoPageViewController(); - final MainDashboardPageController _mainDashboardPageController = - MainDashboardPageController(); + final _mainPageViewController = TwoPageViewController(); + final _mainDashboardPageController = MainDashboardPageController(); - final GlobalKey mainAppKey = GlobalKey(); - final GlobalKey dashboardKey = GlobalKey(); + final mainAppKey = GlobalKey(); + final dashboardKey = GlobalKey(); @override void initState() { super.initState(); - appRouter.tbContext.setMainDashboardHolder(this); + getIt().tbContext.setMainDashboardHolder(this); } @override - Future navigateToDashboard(String dashboardId, - {String? dashboardTitle, - String? state, - bool? hideToolbar, - bool animate = true}) async { - await _mainDashboardPageController.openDashboard(dashboardId, - dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar); + Future navigateToDashboard( + String dashboardId, { + String? dashboardTitle, + String? state, + bool? hideToolbar, + bool animate = true, + }) async { + await _mainDashboardPageController.openDashboard( + dashboardId, + dashboardTitle: dashboardTitle, + state: state, + hideToolbar: hideToolbar, + ); + _openDashboard(animate: animate); } @override Future dashboardGoBack() async { if (_mainPageViewController.index == 1) { - var canGoBack = await _mainDashboardPageController.dashboardGoBack(); + final canGoBack = await _mainDashboardPageController.dashboardGoBack(); if (canGoBack) { closeDashboard(); } + return false; } + return true; } @@ -108,10 +131,11 @@ class ThingsboardAppState extends State } Future _openMain({bool animate = true}) async { - var res = await _mainPageViewController.open(0, animate: animate); + final res = await _mainPageViewController.open(0, animate: animate); if (res) { await _mainDashboardPageController.deactivateDashboard(); } + return res; } @@ -119,6 +143,7 @@ class ThingsboardAppState extends State if (!isDashboardOpen()) { await _mainDashboardPageController.activateDashboard(); } + return _mainPageViewController.close(0, animate: animate); } @@ -126,68 +151,84 @@ class ThingsboardAppState extends State if (!isDashboardOpen()) { _mainDashboardPageController.activateDashboard(); } + return _mainPageViewController.open(1, animate: animate); } Future _closeDashboard({bool animate = true}) async { - var res = await _mainPageViewController.close(1, animate: animate); + final res = await _mainPageViewController.close(1, animate: animate); if (res) { _mainDashboardPageController.deactivateDashboard(); } + return res; } @override Widget build(BuildContext context) { - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( systemNavigationBarColor: Colors.white, statusBarColor: Colors.white, - systemNavigationBarIconBrightness: Brightness.light)); + systemNavigationBarIconBrightness: Brightness.light, + ), + ); + return MaterialApp( - localizationsDelegates: [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - onGenerateTitle: (BuildContext context) => S.of(context).appTitle, - themeMode: ThemeMode.light, - home: TwoPageView( - controller: _mainPageViewController, - first: MaterialApp( - key: mainAppKey, - scaffoldMessengerKey: appRouter.tbContext.messengerKey, - localizationsDelegates: [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - onGenerateTitle: (BuildContext context) => S.of(context).appTitle, - theme: tbTheme, - themeMode: ThemeMode.light, - darkTheme: tbDarkTheme, - onGenerateRoute: appRouter.router.generator, - navigatorObservers: [appRouter.tbContext.routeObserver], - ), - second: MaterialApp( - key: dashboardKey, - // scaffoldMessengerKey: appRouter.tbContext.messengerKey, - localizationsDelegates: [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - onGenerateTitle: (BuildContext context) => S.of(context).appTitle, - theme: tbTheme, - themeMode: ThemeMode.light, - darkTheme: tbDarkTheme, - home: MainDashboardPage(appRouter.tbContext, - controller: _mainDashboardPageController), - ))); + debugShowCheckedModeBanner: false, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + onGenerateTitle: (BuildContext context) => S.of(context).appTitle, + themeMode: ThemeMode.light, + home: TwoPageView( + controller: _mainPageViewController, + first: MaterialApp( + debugShowCheckedModeBanner: false, + key: mainAppKey, + scaffoldMessengerKey: + getIt().tbContext.messengerKey, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + onGenerateTitle: (BuildContext context) => S.of(context).appTitle, + theme: tbTheme, + themeMode: ThemeMode.light, + darkTheme: tbDarkTheme, + onGenerateRoute: getIt().router.generator, + navigatorObservers: [ + getIt().tbContext.routeObserver, + ], + ), + second: MaterialApp( + debugShowCheckedModeBanner: false, + key: dashboardKey, + // scaffoldMessengerKey: appRouter.tbContext.messengerKey, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + onGenerateTitle: (BuildContext context) => S.of(context).appTitle, + theme: tbTheme, + themeMode: ThemeMode.light, + darkTheme: tbDarkTheme, + home: MainDashboardPage( + getIt().tbContext, + controller: _mainDashboardPageController, + ), + ), + ), + ); } } diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index 6eed43cd..38d4de1a 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -4,9 +4,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_app/widgets/two_value_listenable_builder.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -139,8 +140,11 @@ class _DashboardState extends TbContextState { void _onAuthenticated() async { if (tbContext.isAuthenticated) { if (!readyState.value) { - _initialUrl = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + - '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}'); + _initialUrl = Uri.parse( + await getIt().getEndpoint() + + '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}', + ); + readyState.value = true; } else { var windowMessage = { @@ -384,18 +388,18 @@ class _DashboardState extends TbContextState { }, shouldOverrideUrlLoading: (controller, navigationAction) async { - var uri = navigationAction.request.url!; - var uriString = uri.toString(); + final uri = navigationAction.request.url!; + final uriString = uri.toString(); + final endpoint = + await getIt().getEndpoint(); + log.debug('shouldOverrideUrlLoading $uriString'); if (Platform.isAndroid || Platform.isIOS && navigationAction.iosWKNavigationType == IOSWKNavigationType.LINK_ACTIVATED) { - if (uriString.startsWith(ThingsboardAppConstants - .thingsBoardApiEndpoint)) { - var target = uriString.substring( - ThingsboardAppConstants - .thingsBoardApiEndpoint.length); + if (uriString.startsWith(endpoint)) { + var target = uriString.substring(endpoint.length); if (!target.startsWith("?accessToken")) { if (target.startsWith("/")) { target = target.substring(1); diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart index 5b86f37e..03999d80 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -124,9 +124,12 @@ class _MainPageState extends TbPageState void initState() { super.initState(); _tabItems = TbMainNavigationItem.getItems(tbContext); - int currentIndex = _indexFromPath(widget._path); + final currentIndex = _indexFromPath(widget._path); _tabController = TabController( - initialIndex: currentIndex, length: _tabItems.length, vsync: this); + initialIndex: currentIndex, + length: _tabItems.length, + vsync: this, + ); _currentIndexNotifier = ValueNotifier(currentIndex); _tabController.animation!.addListener(_onTabAnimation); } diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart index 12a2a118..b924746c 100644 --- a/lib/modules/more/more_page.dart +++ b/lib/modules/more/more_page.dart @@ -1,9 +1,12 @@ -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_app/utils/services/notification_service.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; @@ -41,12 +44,29 @@ class _MorePageState extends TbContextState ], ), SizedBox(height: 22), - Text(_getUserDisplayName(), - style: TextStyle( - color: Color(0xFF282828), - fontWeight: FontWeight.w500, - fontSize: 20, - height: 23 / 20)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + _getUserDisplayName(), + style: TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 20, + height: 23 / 20, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: EndpointNameWidget( + endpoint: + getIt().getCachedEndpoint(), + ), + ), + ], + ), SizedBox(height: 2), Text(_getAuthorityName(context), style: TextStyle( @@ -106,7 +126,9 @@ class _MorePageState extends TbContextState @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { - NotificationService().updateNotificationsCount(); + if (getIt().apps.isNotEmpty) { + NotificationService().updateNotificationsCount(); + } } } @@ -235,7 +257,7 @@ class MoreMenuItem { path: '/notifications', showAdditionalIcon: true, additionalIcon: _notificationNumberWidget(tbContext.tbClient), - disabled: Firebase.apps.isEmpty, + disabled: getIt().apps.isEmpty, disabledReasonMessage: 'Firebase is not configured.' ' Please refer to the official Firebase documentation for' ' guidance on how to do so.', @@ -262,10 +284,10 @@ class MoreMenuItem { path: '/notifications', showAdditionalIcon: true, additionalIcon: _notificationNumberWidget(tbContext.tbClient), - disabled: Firebase.apps.isEmpty, + disabled: getIt().apps.isEmpty, disabledReasonMessage: 'Notifications are not configured. ' 'Please contact your system administrator.', - ) + ), ]); break; case Authority.CUSTOMER_USER: @@ -280,7 +302,7 @@ class MoreMenuItem { path: '/notifications', showAdditionalIcon: true, additionalIcon: _notificationNumberWidget(tbContext.tbClient), - disabled: Firebase.apps.isEmpty, + disabled: getIt().apps.isEmpty, disabledReasonMessage: 'Notifications are not configured. ' 'Please contact your system administrator.', ), @@ -300,7 +322,9 @@ class MoreMenuItem { } static Widget _notificationNumberWidget(ThingsboardClient tbClient) { - NotificationService().updateNotificationsCount(); + if (getIt().apps.isNotEmpty) { + NotificationService().updateNotificationsCount(); + } return StreamBuilder( stream: NotificationsLocalService.notificationsNumberStream.stream, diff --git a/lib/modules/notification/service/notifications_local_service.dart b/lib/modules/notification/service/notifications_local_service.dart index 88e66109..36ee09c5 100644 --- a/lib/modules/notification/service/notifications_local_service.dart +++ b/lib/modules/notification/service/notifications_local_service.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/notification/service/i_notifications_local_service.dart'; -import 'package:thingsboard_app/utils/services/_tb_secure_storage.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; final class NotificationsLocalService implements INotificationsLocalService { - NotificationsLocalService() : storage = createAppStorage(); + NotificationsLocalService() : storage = getIt(); static const notificationCounterKey = 'notifications_counter'; static final notificationsNumberStream = StreamController.broadcast(); @@ -46,7 +47,7 @@ final class NotificationsLocalService implements INotificationsLocalService { @override Future clearNotificationBadgeCount() async { FlutterAppBadger.removeBadge(); - createAppStorage().deleteItem(notificationCounterKey); + getIt().deleteItem(notificationCounterKey); notificationsNumberStream.add(0); } diff --git a/lib/modules/notification/widgets/notification_icon.dart b/lib/modules/notification/widgets/notification_icon.dart index 9682d011..eb9d9867 100644 --- a/lib/modules/notification/widgets/notification_icon.dart +++ b/lib/modules/notification/widgets/notification_icon.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; @@ -38,6 +39,11 @@ class NotificationIcon extends StatelessWidget { if (imageData != null) { if (imageData!.contains('mdi')) { + return Icon( + MdiIcons.fromString(imageData.split('mdi:').last), + color: _toColor(data['color']), + ); + return SvgPicture.network( '${ThingsboardAppConstants.thingsBoardApiEndpoint}/assets/mdi/${imageData.split('mdi:').last}.svg', color: _toColor(data['color']), diff --git a/lib/modules/notification/widgets/notification_slidable_widget.dart b/lib/modules/notification/widgets/notification_slidable_widget.dart index 60daeb9c..03117b26 100644 --- a/lib/modules/notification/widgets/notification_slidable_widget.dart +++ b/lib/modules/notification/widgets/notification_slidable_widget.dart @@ -94,7 +94,7 @@ class _NotificationSlidableWidget extends State { final type = notification.type; if (type == PushNotificationType.ALARM) { final status = notification.info?.alarmStatus; - final id = notification.info?.stateEntityId?.id; + final id = notification.info?.alarmId; if (id != null) { if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK] diff --git a/lib/utils/services/endpoint/endpoint_service.dart b/lib/utils/services/endpoint/endpoint_service.dart new file mode 100644 index 00000000..27c8ca93 --- /dev/null +++ b/lib/utils/services/endpoint/endpoint_service.dart @@ -0,0 +1,41 @@ +import 'package:thingsboard_app/constants/app_constants.dart'; +import 'package:thingsboard_app/constants/database_keys.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; + +class EndpointService implements IEndpointService { + EndpointService({required this.databaseService}); + + final ILocalDatabaseService databaseService; + String? _cachedEndpoint; + + @override + Future setEndpoint(String endpoint) async { + _cachedEndpoint = endpoint; + + await databaseService.setItem( + DatabaseKeys.thingsBoardApiEndpointKey, + endpoint, + ); + } + + @override + Future getEndpoint() async { + _cachedEndpoint ??= await databaseService.getItem( + DatabaseKeys.thingsBoardApiEndpointKey, + ); + + return _cachedEndpoint ?? ThingsboardAppConstants.thingsBoardApiEndpoint; + } + + @override + Future isCustomEndpoint() async { + _cachedEndpoint ??= await getEndpoint(); + return _cachedEndpoint != ThingsboardAppConstants.thingsBoardApiEndpoint; + } + + @override + String getCachedEndpoint() { + return _cachedEndpoint ?? ThingsboardAppConstants.thingsBoardApiEndpoint; + } +} diff --git a/lib/utils/services/endpoint/i_endpoint_service.dart b/lib/utils/services/endpoint/i_endpoint_service.dart new file mode 100644 index 00000000..6892c71f --- /dev/null +++ b/lib/utils/services/endpoint/i_endpoint_service.dart @@ -0,0 +1,14 @@ +/// This service provides information about the current active endpoint. +/// Since we have a feature that allows for changing endpoints, there is some +/// logic associated with the active endpoint, such as dashboard loading and OAuth2A. +abstract interface class IEndpointService { + Future setEndpoint(String endpoint); + + Future getEndpoint(); + + Future isCustomEndpoint(); + + /// At times, we need to retrieve the endpoint synchronously. + /// We might consider using Hive in the future. + String getCachedEndpoint(); +} diff --git a/lib/utils/services/firebase/firebase_service.dart b/lib/utils/services/firebase/firebase_service.dart new file mode 100644 index 00000000..09dcd01b --- /dev/null +++ b/lib/utils/services/firebase/firebase_service.dart @@ -0,0 +1,53 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; + +class FirebaseService implements IFirebaseService { + FirebaseService({ + required this.logger, + required this.endpointService, + }); + + final TbLogger logger; + final _apps = []; + final IEndpointService endpointService; + + @override + List get apps => _apps; + + @override + Future initializeApp({ + String name = defaultFirebaseAppName, + FirebaseOptions? options, + }) async { + try { + if (await endpointService.isCustomEndpoint()) { + throw UnimplementedError( + 'The current limitation is that Firebase can only be ' + 'used with the endpoint with which the app was initially initialized.', + ); + } + + final app = await Firebase.initializeApp(options: options); + _apps.add(name); + + return app; + } catch (e) { + logger.error('FirebaseService:initializeApp $e'); + } + + return null; + } + + @override + Future removeApp({String name = defaultFirebaseAppName}) async { + try { + await Firebase.app(name).delete(); + } catch (e) { + logger.error('FirebaseService:removeApp $e'); + } finally { + _apps.remove(name); + } + } +} diff --git a/lib/utils/services/firebase/i_firebase_service.dart b/lib/utils/services/firebase/i_firebase_service.dart new file mode 100644 index 00000000..a908fa17 --- /dev/null +++ b/lib/utils/services/firebase/i_firebase_service.dart @@ -0,0 +1,11 @@ +import 'package:firebase_core/firebase_core.dart'; + +abstract interface class IFirebaseService { + const IFirebaseService(); + + Future initializeApp({String name, FirebaseOptions? options}); + + Future removeApp({String name}); + + List get apps; +} diff --git a/lib/utils/services/local_database/i_local_database_service.dart b/lib/utils/services/local_database/i_local_database_service.dart new file mode 100644 index 00000000..e3d324c4 --- /dev/null +++ b/lib/utils/services/local_database/i_local_database_service.dart @@ -0,0 +1,6 @@ +import 'package:thingsboard_client/thingsboard_client.dart'; + +/// The aim of this service is to consolidate operations with +/// the local database provider into one centralized location. + +abstract interface class ILocalDatabaseService implements TbStorage {} diff --git a/lib/utils/services/local_database/local_database_service.dart b/lib/utils/services/local_database/local_database_service.dart new file mode 100644 index 00000000..ee57cb86 --- /dev/null +++ b/lib/utils/services/local_database/local_database_service.dart @@ -0,0 +1,31 @@ +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class LocalDatabaseService implements ILocalDatabaseService { + const LocalDatabaseService({ + required this.storage, + required this.logger, + }); + + final TbStorage storage; + final TbLogger logger; + + @override + Future deleteItem(String key) async { + logger.debug('LocalDatabaseService::deleteItem($key)'); + await storage.deleteItem(key); + } + + @override + Future getItem(String key) async { + logger.debug('LocalDatabaseService::getItem($key)'); + return storage.getItem(key); + } + + @override + Future setItem(String key, String value) async { + logger.debug('LocalDatabaseService::setItem($key, $value)'); + await storage.setItem(key, value); + } +} diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 6d1cea79..6ea62644 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -4,6 +4,8 @@ import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/notification/service/i_notifications_local_service.dart'; import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; import 'package:thingsboard_app/utils/utils.dart'; @@ -16,8 +18,10 @@ class NotificationService { late TbLogger _log; late ThingsboardClient _tbClient; late TbContext _tbContext; - late INotificationsLocalService _localService; - late StreamSubscription _foregroundMessageSubscription; + final INotificationsLocalService _localService = NotificationsLocalService(); + StreamSubscription? _foregroundMessageSubscription; + StreamSubscription? _onMessageOpenedAppSubscription; + StreamSubscription? _onTokenRefreshSubscription; String? _fcmToken; @@ -36,7 +40,8 @@ class NotificationService { _log = log; _tbClient = tbClient; _tbContext = context; - _localService = NotificationsLocalService(); + + _log.debug('NotificationService::init()'); final message = await FirebaseMessaging.instance.getInitialMessage(); if (message != null) { @@ -46,7 +51,8 @@ class NotificationService { ); } - FirebaseMessaging.onMessageOpenedApp.listen( + _onMessageOpenedAppSubscription = + FirebaseMessaging.onMessageOpenedApp.listen( (message) async { NotificationService.handleClickOnNotification( message.data, @@ -55,13 +61,15 @@ class NotificationService { }, ); - var settings = await _requestPermission(); + final settings = await _requestPermission(); _log.debug( 'Notification authorizationStatus: ${settings.authorizationStatus}'); if (settings.authorizationStatus == AuthorizationStatus.authorized || settings.authorizationStatus == AuthorizationStatus.provisional) { await _getAndSaveToken(); - FirebaseMessaging.instance.onTokenRefresh.listen((token) { + + _onTokenRefreshSubscription = + FirebaseMessaging.instance.onTokenRefresh.listen((token) { if (_fcmToken != null) { _tbClient.getUserService().removeMobileSession(_fcmToken!).then((_) { _fcmToken = token; @@ -75,27 +83,19 @@ class NotificationService { await _initFlutterLocalNotificationsPlugin(); await _configFirebaseMessaging(); _subscribeOnForegroundMessage(); - updateNotificationsCount(); + await updateNotificationsCount(); } } Future updateNotificationsCount() async { final localService = NotificationsLocalService(); - localService.updateNotificationsCount( + await localService.updateNotificationsCount( await _getNotificationsCountRemote(), ); } Future getToken() async { - // if (Platform.isIOS) { - // final apnsToken = await _messaging.getAPNSToken(); - // _log.debug('APNS token: $apnsToken'); - // if (apnsToken == null) { - // return null; - // } - // } - _fcmToken = await _messaging.getToken(); return _fcmToken; } @@ -105,13 +105,19 @@ class NotificationService { } Future logout() async { + getIt().debug('NotificationService::logout()'); if (_fcmToken != null) { + getIt().debug( + 'NotificationService::logout() removeMobileSession', + ); _tbClient.getUserService().removeMobileSession(_fcmToken!); } - await _foregroundMessageSubscription.cancel(); - await _messaging.setAutoInitEnabled(false); + await _foregroundMessageSubscription?.cancel(); + await _onMessageOpenedAppSubscription?.cancel(); + await _onTokenRefreshSubscription?.cancel(); await _messaging.deleteToken(); + await _messaging.setAutoInitEnabled(false); await flutterLocalNotificationsPlugin.cancelAll(); await _localService.clearNotificationBadgeCount(); } @@ -161,7 +167,7 @@ class NotificationService { Future _requestPermission() async { _messaging = FirebaseMessaging.instance; - var result = await _messaging.requestPermission( + final result = await _messaging.requestPermission( alert: true, announcement: false, badge: true, @@ -170,6 +176,7 @@ class NotificationService { provisional: true, sound: true, ); + if (result.authorizationStatus == AuthorizationStatus.denied) { return result; } @@ -181,6 +188,7 @@ class NotificationService { if (token != null) { _tbClient.getUserService().removeMobileSession(token); } + await _messaging.deleteToken(); return await getToken(); } @@ -279,7 +287,7 @@ class NotificationService { if (Uri.parse(link).isAbsolute) { tbContext.navigateTo('/url/${Uri.encodeComponent(link)}'); } else { - tbContext.navigateTo(link, replace: true); + tbContext.navigateTo(link); } } @@ -291,8 +299,12 @@ class NotificationService { } Future _getNotificationsCountRemote() async { - return _tbClient - .getNotificationService() - .getUnreadNotificationsCount('MOBILE_APP'); + try { + return _tbClient + .getNotificationService() + .getUnreadNotificationsCount('MOBILE_APP'); + } catch (_) { + return 0; + } } } diff --git a/lib/utils/usecase.dart b/lib/utils/usecase.dart new file mode 100644 index 00000000..1ea37513 --- /dev/null +++ b/lib/utils/usecase.dart @@ -0,0 +1,9 @@ +abstract class UseCase { + const UseCase(); + + Output call(Input params); +} + +class NoParams { + const NoParams(); +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index c4becbb8..9e4b3679 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:jovial_svg/jovial_svg.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -import '../constants/app_constants.dart'; - abstract class Utils { static const _tbImagePrefix = 'tb-image;'; static const _imageBase64UrlPrefix = 'data:image/'; @@ -21,9 +21,7 @@ abstract class Utils { static String createDashboardEntityState(EntityId? entityId, {String? entityName, String? entityLabel, String? stateId}) { var stateObj = [ - { - 'params': {} - } + {'params': {}} ]; if (entityId != null) { stateObj[0]['params']['entityId'] = entityId.toJson(); @@ -96,7 +94,8 @@ abstract class Utils { parts[parts.length - 1] = Uri.encodeComponent(key); var encodedUrl = parts.join('/'); var imageLink = - ThingsboardAppConstants.thingsBoardApiEndpoint + encodedUrl; + getIt().getCachedEndpoint() + encodedUrl; + return _networkImage(context, imageLink, headers: {_authHeaderName: _authScheme + jwtToken}, color: color, diff --git a/lib/widgets/two_page_view.dart b/lib/widgets/two_page_view.dart index ad152ea2..ef44a4e8 100644 --- a/lib/widgets/two_page_view.dart +++ b/lib/widgets/two_page_view.dart @@ -93,7 +93,7 @@ class _TwoPageViewState extends State { @override Widget build(BuildContext context) { - return new PreloadPageView( + return PreloadPageView( children: _pages, physics: NeverScrollableScrollPhysics(), reverse: _reverse, diff --git a/pubspec.yaml b/pubspec.yaml index 9cfb980a..a7d76a30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,11 +52,17 @@ dependencies: flutter_app_badger: ^1.5.0 timeago: ^3.6.1 flutter_slidable: ^3.0.1 + flutter_bloc: ^8.1.5 + get_it: ^7.6.7 + equatable: ^2.0.5 + uni_links: ^0.5.1 dev_dependencies: flutter_test: sdk: flutter flutter_launcher_icons: ^0.13.1 + mocktail: ^1.0.3 + bloc_test: ^9.1.7 flutter: uses-material-design: true diff --git a/test/ core/noauth/switch_endpoint_test.dart b/test/ core/noauth/switch_endpoint_test.dart new file mode 100644 index 00000000..fe44294f --- /dev/null +++ b/test/ core/noauth/switch_endpoint_test.dart @@ -0,0 +1,250 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:thingsboard_app/core/auth/noauth/di/noauth_di.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +import '../../mocks.dart'; + +void main() { + late final TbContext tbContext; + late final ThingsboardClient tbClient; + late final IFirebaseService firebaseService; + + setUpAll(() { + tbContext = MockTbContext(); + tbClient = MockTbClient(); + firebaseService = MockFirebaseService(); + + when(() => tbContext.tbClient).thenReturn(tbClient); + when(() => firebaseService.removeApp()).thenAnswer((_) => Future.value()); + + getIt.registerLazySingleton(() => TbLogger()); + getIt.registerLazySingleton(() => firebaseService); + + NoAuthDi.init(tbContext: tbContext); + }); + + tearDownAll(() { + getIt().close(); + NoAuthDi.dispose(); + }); + + group('Switch Endpoint Group Test', () { + group('SwitchToAnotherEndpointEvent', () { + blocTest( + 'An empty request data', + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent(parameters: null), + ), + expect: () => [ + isA().having( + (e) => e.message, + 'error message', + 'An empty request data received.', + ) + ], + ); + + blocTest( + 'JWT Token is invalid', + setUp: () { + when( + () => tbClient.getLoginDataBySecretKey( + host: any(named: 'host'), + key: any(named: 'key'), + ), + ).thenThrow( + ThingsboardError(message: 'JWT Token is invalid'), + ); + }, + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent( + parameters: { + 'host': 'host', + 'secret': 'key', + 'uri': 'uri', + }, + ), + ), + expect: () => [ + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Getting data from your host host', + ), + isA().having( + (e) => e.message, + 'error message', + 'JWT Token is invalid', + ), + ], + ); + + blocTest( + 'An error on TbClient re-init', + setUp: () { + when( + () => tbClient.getLoginDataBySecretKey( + host: any(named: 'host'), + key: any(named: 'key'), + ), + ).thenAnswer( + (_) => Future.value(LoginResponse('token', 'refreshToken')), + ); + + when( + () => tbContext.logout( + requestConfig: any(named: 'requestConfig'), + notifyUser: any(named: 'notifyUser'), + ), + ).thenAnswer((_) => Future.value()); + + when( + () => tbClient.setUserFromJwtToken(any(), any(), any()), + ).thenAnswer((_) => Future.value()); + + when( + () => tbContext.reInit( + endpoint: any(named: 'endpoint'), + onDone: any(named: 'onDone'), + onError: any(named: 'onError'), + ), + ).thenAnswer( + (invocation) { + final onError = invocation.namedArguments[Symbol('onError')]; + onError( + ThingsboardError(message: 'TBClient re-init error message'), + ); + + return Future.value(); + }, + ); + }, + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent( + parameters: { + 'host': 'https://host.com', + 'secret': 'key', + 'uri': 'uri', + }, + ), + ), + expect: () => [ + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Getting data from your host https://host.com', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Logout you ...', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Switching you to the new host https://host.com', + ), + isA().having( + (e) => e.message, + 'error message', + 'TBClient re-init error message', + ), + ], + ); + + blocTest( + 'Switch endpoint success', + setUp: () { + when( + () => tbClient.getLoginDataBySecretKey( + host: any(named: 'host'), + key: any(named: 'key'), + ), + ).thenAnswer( + (_) => Future.value(LoginResponse('token', 'refreshToken')), + ); + + when( + () => tbContext.logout( + requestConfig: any(named: 'requestConfig'), + notifyUser: any(named: 'notifyUser'), + ), + ).thenAnswer((_) => Future.value()); + + when( + () => tbClient.setUserFromJwtToken(any(), any(), any()), + ).thenAnswer((_) => Future.value()); + + when( + () => tbContext.reInit( + endpoint: any(named: 'endpoint'), + onDone: any(named: 'onDone'), + onError: any(named: 'onError'), + ), + ).thenAnswer( + (invocation) { + final onDone = invocation.namedArguments[Symbol('onDone')]; + onDone(); + + return Future.value(); + }, + ); + }, + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent( + parameters: { + 'host': 'https://host.com', + 'secret': 'key', + 'uri': 'uri', + }, + ), + ), + expect: () => [ + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Getting data from your host https://host.com', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Logout you ...', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Switching you to the new host https://host.com', + ), + isA(), + ], + ); + }); + }); +} diff --git a/test/mocks.dart b/test/mocks.dart new file mode 100644 index 00000000..d2a4065e --- /dev/null +++ b/test/mocks.dart @@ -0,0 +1,10 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/utils/services/firebase/firebase_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class MockTbContext extends Mock implements TbContext {} + +class MockTbClient extends Mock implements ThingsboardClient {} + +class MockFirebaseService extends Mock implements FirebaseService {} From b26425261632681720ec7c8ac4ac7dabef316b9d Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 2 May 2024 13:03:36 +0300 Subject: [PATCH 11/61] Added login by qr code option. (#82) * Added login by qr code option. * App links configuration Android. * Fixed an issue with login from custom endpoint. * Fixed an issue dashboard not loaded after switching endpoint without closing app. * Fixed dashboard issues. --- assets/images/qr-demo-thingsboard.svg | 91 +++++++++ lib/constants/assets_path.dart | 3 +- lib/core/auth/login/login_page.dart | 180 +++++++++++++++--- .../noauth/presentation/bloc/noauth_bloc.dart | 4 + .../view/switch_endpoint_noauth_view.dart | 11 +- lib/core/context/tb_context.dart | 23 ++- lib/modules/dashboard/dashboard.dart | 3 +- .../dashboard/fullscreen_dashboard_page.dart | 90 +++++---- .../dashboard/main_dashboard_page.dart | 48 +++-- .../services/endpoint/endpoint_service.dart | 21 +- .../services/endpoint/i_endpoint_service.dart | 4 + test/ core/noauth/switch_endpoint_test.dart | 7 + test/mocks.dart | 3 + 13 files changed, 378 insertions(+), 110 deletions(-) create mode 100644 assets/images/qr-demo-thingsboard.svg diff --git a/assets/images/qr-demo-thingsboard.svg b/assets/images/qr-demo-thingsboard.svg new file mode 100644 index 00000000..08b58dcd --- /dev/null +++ b/assets/images/qr-demo-thingsboard.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/constants/assets_path.dart b/lib/constants/assets_path.dart index 6821c3a6..973953c6 100644 --- a/lib/constants/assets_path.dart +++ b/lib/constants/assets_path.dart @@ -12,6 +12,7 @@ abstract class ThingsboardImage { 'google-logo': 'assets/images/google-logo.svg', 'github-logo': 'assets/images/github-logo.svg', 'facebook-logo': 'assets/images/facebook-logo.svg', - 'apple-logo': 'assets/images/apple-logo.svg' + 'apple-logo': 'assets/images/apple-logo.svg', + 'qr-code-logo': 'assets/images/qr-demo-thingsboard.svg' }; } diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index ca6c330d..65d3bf77 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; @@ -89,22 +90,72 @@ class _LoginPageState extends TbPageState { SizedBox(height: 48), if (tbContext.hasOAuthClients) _buildOAuth2Buttons( - tbContext.oauth2ClientInfos!), - if (tbContext.hasOAuthClients) - Padding( - padding: - EdgeInsets.only(top: 10, bottom: 16), - child: Row( + tbContext.oauth2ClientInfos!, + ), + Visibility( + visible: !tbContext.hasOAuthClients, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 16), + child: const Center( + child: Text('LOGIN WITH'), + ), + ), + Row( children: [ - Flexible(child: Divider()), - Padding( - padding: EdgeInsets.symmetric( - horizontal: 16), - child: Text('${S.of(context).OR}'), - ), - Flexible(child: Divider()) + OutlinedButton( + style: _oauth2IconButtonStyle, + onPressed: () async { + try { + final barcode = + await tbContext.navigateTo( + '/qrCodeScan', + transition: + TransitionType.nativeModal, + ); + + if (barcode != null && + barcode.code != null) { + tbContext.navigateByAppLink( + barcode.code, + ); + } else {} + } catch (e) { + log.error( + 'Login with qr code error', + e, + ); + } + }, + child: SvgPicture.asset( + ThingsboardImage + .oauth2Logos['qr-code-logo']!, + height: 24, + ), + ) ], - )), + ) + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 10, bottom: 16), + child: Row( + children: [ + Flexible(child: Divider()), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 16), + child: Text('${S.of(context).OR}'), + ), + Flexible(child: Divider()) + ], + ), + ), FormBuilder( key: _loginFormKey, autovalidateMode: AutovalidateMode.disabled, @@ -232,35 +283,106 @@ class _LoginPageState extends TbPageState { Widget _buildOAuth2Buttons(List clients) { if (clients.length == 1 || clients.length > 6) { return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: clients + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...clients .asMap() - .map((index, client) => MapEntry( + .map( + (index, client) => MapEntry( index, - _buildOAuth2Button(client, 'Login with ${client.name}', false, - index == clients.length - 1))) + _buildOAuth2Button( + client, + 'Login with ${client.name}', + false, + index == clients.length - 1, + ), + ), + ) .values - .toList()); + .toList(), + OutlinedButton( + style: _oauth2IconButtonStyle, + onPressed: () async { + try { + final barcode = await tbContext.navigateTo( + '/qrCodeScan', + transition: TransitionType.nativeModal, + ); + + if (barcode != null && barcode.code != null) { + tbContext.navigateByAppLink( + barcode.code, + ); + } else {} + } catch (e) { + log.error( + 'Login with qr code error', + e, + ); + } + }, + child: SvgPicture.asset( + ThingsboardImage.oauth2Logos['qr-code-logo']!, + height: 24, + ), + ), + ], + ); } else { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center(child: Text('LOGIN WITH')), + padding: const EdgeInsets.symmetric(vertical: 16), + child: const Center( + child: Text('LOGIN WITH'), + ), ), Row( - children: clients + children: [ + ...clients .asMap() - .map((index, client) => MapEntry( + .map( + (index, client) => MapEntry( index, _buildOAuth2Button( - client, - clients.length == 2 ? client.name : null, - true, - index == clients.length - 1))) + client, + clients.length == 2 ? client.name : null, + true, + index == clients.length - 1, + ), + ), + ) .values - .toList()) + .toList(), + OutlinedButton( + style: _oauth2IconButtonStyle, + onPressed: () async { + try { + final barcode = await tbContext.navigateTo( + '/qrCodeScan', + transition: TransitionType.nativeModal, + ); + + if (barcode != null && barcode.code != null) { + tbContext.navigateByAppLink( + barcode.code, + ); + } else {} + } catch (e) { + log.error( + 'Login with qr code error', + e, + ); + } + }, + child: SvgPicture.asset( + ThingsboardImage.oauth2Logos['qr-code-logo']!, + height: 24, + ), + ), + ], + ) ], ); } diff --git a/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart b/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart index a8289e76..849ab7d5 100644 --- a/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart +++ b/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart @@ -23,6 +23,10 @@ class NoAuthBloc extends Bloc { void _switchEndpointEventHandler() { on( (event, emit) async { + if (isClosed) { + return; + } + if (event.parameters == null) { emit( const NoAuthErrorState( diff --git a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart index 4c43ac70..8d102a4b 100644 --- a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart +++ b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart @@ -24,8 +24,14 @@ class SwitchEndpointNoAuthView extends StatefulWidget { class _SwitchEndpointNoAuthViewState extends State { @override Widget build(BuildContext context) { + if (getIt().isClosed) { + return const Scaffold( + body: NoAuthLoadingWidget(), + ); + } + return BlocProvider.value( - value: GetIt.instance() + value: getIt() ..add( SwitchToAnotherEndpointEvent( parameters: widget.arguments, @@ -44,11 +50,13 @@ class _SwitchEndpointNoAuthViewState extends State { } }); } else if (state is NoAuthDoneState) { + GetIt.instance().close(); getIt().router.navigateTo( context, '/home', replace: true, maintainState: false, + clearStack: true, ); } }, @@ -135,7 +143,6 @@ class _SwitchEndpointNoAuthViewState extends State { @override void dispose() { - GetIt.instance().close(); NoAuthDi.dispose(); super.dispose(); } diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index dccc0043..9e153b03 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -66,6 +66,7 @@ class TbContext implements PopEntry { TbMainDashboardHolder? _mainDashboardHolder; bool _closeMainFirst = false; StreamSubscription? _appLinkStreamSubscription; + late bool _handleRootState; final ValueNotifier canPopNotifier = ValueNotifier(false); @@ -103,7 +104,7 @@ class TbContext implements PopEntry { } return true; }()); - + _handleRootState = true; _initialized = true; final endpoint = await getIt().getEndpoint(); @@ -154,12 +155,13 @@ class TbContext implements PopEntry { }) async { log.debug('TbContext:reinit()'); + _handleRootState = false; _initialized = false; tbClient = ThingsboardClient( endpoint, storage: getIt(), - onUserLoaded: () => onUserLoaded(handleRouteState: false, onDone: onDone), + onUserLoaded: () => onUserLoaded(onDone: onDone), onError: onError, onLoadStarted: onLoadStarted, onLoadFinished: onLoadFinished, @@ -262,8 +264,7 @@ class TbContext implements PopEntry { _isLoadingNotifier.value = false; } - Future onUserLoaded( - {bool handleRouteState = true, VoidCallback? onDone}) async { + Future onUserLoaded({VoidCallback? onDone}) async { try { log.debug( 'TbContext.onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}'); @@ -308,7 +309,7 @@ class TbContext implements PopEntry { onDone?.call(); } - if (handleRouteState) { + if (_handleRootState) { await updateRouteState(); } @@ -344,14 +345,14 @@ class TbContext implements PopEntry { final link = await getIt().getItem( DatabaseKeys.initialAppLink, ); - _navigateByAppLink(link); + navigateByAppLink(link); } catch (e) { log.error('TbContext:getInitialUri() exception $e'); } if (_appLinkStreamSubscription == null) { _appLinkStreamSubscription = linkStream.listen((link) { - _navigateByAppLink(link); + navigateByAppLink(link); }, onError: (err) { log.error('linkStream.listen $err'); }); @@ -359,7 +360,7 @@ class TbContext implements PopEntry { } } - Future _navigateByAppLink(String? link) async { + Future navigateByAppLink(String? link) async { if (link != null) { final uri = Uri.parse(link); await getIt().deleteItem( @@ -367,8 +368,7 @@ class TbContext implements PopEntry { ); log.debug('TbContext: navigate by appLink $uri'); - router.navigateTo( - currentState!.context, + navigateTo( uri.path, routeSettings: RouteSettings( arguments: {...uri.queryParameters, 'uri': uri}, @@ -381,6 +381,9 @@ class TbContext implements PopEntry { RequestConfig? requestConfig, bool notifyUser = true, }) async { + log.debug('TbContext::logout($requestConfig, $notifyUser)'); + _handleRootState = true; + if (getIt().apps.isNotEmpty) { await NotificationService().logout(); } diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index 38d4de1a..44ed9f71 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -84,7 +84,7 @@ class Dashboard extends TbContextWidget { this._activeByDefault = activeByDefault, this._titleCallback = titleCallback, this._controllerCallback = controllerCallback, - super(tbContext); + super(tbContext, key: key); @override _DashboardState createState() => _DashboardState(); @@ -278,7 +278,6 @@ class _DashboardState extends TbContextState { @override Widget build(BuildContext context) { - // ignore: deprecated_member_use return WillPopScope( onWillPop: () async { if (widget._home == true && !tbContext.isHomePage()) { diff --git a/lib/modules/dashboard/fullscreen_dashboard_page.dart b/lib/modules/dashboard/fullscreen_dashboard_page.dart index 44385f54..8bb0455d 100644 --- a/lib/modules/dashboard/fullscreen_dashboard_page.dart +++ b/lib/modules/dashboard/fullscreen_dashboard_page.dart @@ -1,7 +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/locator.dart'; import 'package:thingsboard_app/modules/dashboard/dashboard.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class FullscreenDashboardPage extends TbPageWidget { @@ -41,45 +43,55 @@ class _FullscreenDashboardPageState @override Widget build(BuildContext context) { return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(kToolbarHeight), - child: ValueListenableBuilder( - valueListenable: showBackValue, - builder: (context, canGoBack, widget) { - return TbAppBar(tbContext, - leading: canGoBack - ? BackButton(onPressed: () { - maybePop(); - }) - : null, - showLoadingIndicator: false, - elevation: 1, - shadowColor: Colors.transparent, - title: ValueListenableBuilder( - 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')) - ]); - }), + appBar: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: ValueListenableBuilder( + valueListenable: showBackValue, + builder: (context, canGoBack, widget) { + return TbAppBar(tbContext, + leading: canGoBack + ? BackButton(onPressed: () { + maybePop(); + }) + : null, + showLoadingIndicator: false, + elevation: 1, + shadowColor: Colors.transparent, + title: ValueListenableBuilder( + 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: ValueListenableBuilder( + valueListenable: getIt().listenEndpointChanges, + builder: (context, _, __) => Dashboard( + tbContext, + key: UniqueKey(), + titleCallback: (title) { + dashboardTitleValue.value = title; + }, + controllerCallback: (controller) { + controller.canGoBack.addListener(() { + _onCanGoBack(controller.canGoBack.value); + }); + controller.openDashboard( + widget.fullscreenDashboardId, + 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); - })); + ), + ); } } diff --git a/lib/modules/dashboard/main_dashboard_page.dart b/lib/modules/dashboard/main_dashboard_page.dart index d4dcadd4..5468c1b5 100644 --- a/lib/modules/dashboard/main_dashboard_page.dart +++ b/lib/modules/dashboard/main_dashboard_page.dart @@ -1,7 +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/locator.dart'; import 'package:thingsboard_app/modules/dashboard/dashboard.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class MainDashboardPageController { @@ -127,27 +129,33 @@ class _MainDashboardPageState extends TbContextState ) ], ), - body: Dashboard( - tbContext, - activeByDefault: false, - titleCallback: (title) { - dashboardTitleValue.value = title; - }, - controllerCallback: (controller) { - _dashboardController = controller; - if (widget._controller != null) { - widget._controller!._setDashboardController(controller); - controller.hasRightLayout.addListener(() { - hasRightLayout.value = controller.hasRightLayout.value; - }); - controller.rightLayoutOpened.addListener(() { - if (controller.rightLayoutOpened.value) { - rightLayoutMenuController.forward(); - } else { - rightLayoutMenuController.reverse(); + body: ValueListenableBuilder( + valueListenable: getIt().listenEndpointChanges, + builder: (context, value, _) { + return Dashboard( + tbContext, + key: UniqueKey(), + activeByDefault: false, + titleCallback: (title) { + dashboardTitleValue.value = title; + }, + controllerCallback: (controller) { + _dashboardController = controller; + if (widget._controller != null) { + widget._controller!._setDashboardController(controller); + controller.hasRightLayout.addListener(() { + hasRightLayout.value = controller.hasRightLayout.value; + }); + controller.rightLayoutOpened.addListener(() { + if (controller.rightLayoutOpened.value) { + rightLayoutMenuController.forward(); + } else { + rightLayoutMenuController.reverse(); + } + }); } - }); - } + }, + ); }, ), ); diff --git a/lib/utils/services/endpoint/endpoint_service.dart b/lib/utils/services/endpoint/endpoint_service.dart index 27c8ca93..8629af92 100644 --- a/lib/utils/services/endpoint/endpoint_service.dart +++ b/lib/utils/services/endpoint/endpoint_service.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_app/constants/database_keys.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; @@ -7,11 +8,14 @@ class EndpointService implements IEndpointService { EndpointService({required this.databaseService}); final ILocalDatabaseService databaseService; - String? _cachedEndpoint; + final _cachedEndpoint = ValueNotifier(null); + + @override + ValueListenable get listenEndpointChanges => _cachedEndpoint; @override Future setEndpoint(String endpoint) async { - _cachedEndpoint = endpoint; + _cachedEndpoint.value = endpoint; await databaseService.setItem( DatabaseKeys.thingsBoardApiEndpointKey, @@ -21,21 +25,24 @@ class EndpointService implements IEndpointService { @override Future getEndpoint() async { - _cachedEndpoint ??= await databaseService.getItem( + _cachedEndpoint.value ??= await databaseService.getItem( DatabaseKeys.thingsBoardApiEndpointKey, ); - return _cachedEndpoint ?? ThingsboardAppConstants.thingsBoardApiEndpoint; + return _cachedEndpoint.value ?? + ThingsboardAppConstants.thingsBoardApiEndpoint; } @override Future isCustomEndpoint() async { - _cachedEndpoint ??= await getEndpoint(); - return _cachedEndpoint != ThingsboardAppConstants.thingsBoardApiEndpoint; + _cachedEndpoint.value ??= await getEndpoint(); + return _cachedEndpoint.value != + ThingsboardAppConstants.thingsBoardApiEndpoint; } @override String getCachedEndpoint() { - return _cachedEndpoint ?? ThingsboardAppConstants.thingsBoardApiEndpoint; + return _cachedEndpoint.value ?? + ThingsboardAppConstants.thingsBoardApiEndpoint; } } diff --git a/lib/utils/services/endpoint/i_endpoint_service.dart b/lib/utils/services/endpoint/i_endpoint_service.dart index 6892c71f..007dac4b 100644 --- a/lib/utils/services/endpoint/i_endpoint_service.dart +++ b/lib/utils/services/endpoint/i_endpoint_service.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + /// This service provides information about the current active endpoint. /// Since we have a feature that allows for changing endpoints, there is some /// logic associated with the active endpoint, such as dashboard loading and OAuth2A. @@ -11,4 +13,6 @@ abstract interface class IEndpointService { /// At times, we need to retrieve the endpoint synchronously. /// We might consider using Hive in the future. String getCachedEndpoint(); + + ValueListenable get listenEndpointChanges; } diff --git a/test/ core/noauth/switch_endpoint_test.dart b/test/ core/noauth/switch_endpoint_test.dart index fe44294f..c63fa112 100644 --- a/test/ core/noauth/switch_endpoint_test.dart +++ b/test/ core/noauth/switch_endpoint_test.dart @@ -6,6 +6,7 @@ import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; @@ -15,17 +16,23 @@ void main() { late final TbContext tbContext; late final ThingsboardClient tbClient; late final IFirebaseService firebaseService; + late final IEndpointService endpointService; setUpAll(() { tbContext = MockTbContext(); tbClient = MockTbClient(); firebaseService = MockFirebaseService(); + endpointService = MockEndpointService(); when(() => tbContext.tbClient).thenReturn(tbClient); when(() => firebaseService.removeApp()).thenAnswer((_) => Future.value()); + when(() => endpointService.setEndpoint(any())).thenAnswer( + (_) => Future.value(), + ); getIt.registerLazySingleton(() => TbLogger()); getIt.registerLazySingleton(() => firebaseService); + getIt.registerLazySingleton(() => endpointService); NoAuthDi.init(tbContext: tbContext); }); diff --git a/test/mocks.dart b/test/mocks.dart index d2a4065e..7ad22af9 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -1,5 +1,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/utils/services/endpoint/endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/firebase_service.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; @@ -8,3 +9,5 @@ class MockTbContext extends Mock implements TbContext {} class MockTbClient extends Mock implements ThingsboardClient {} class MockFirebaseService extends Mock implements FirebaseService {} + +class MockEndpointService extends Mock implements EndpointService {} From 593df860d4f169764cbd85475bda932958b98c59 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 6 May 2024 11:29:08 +0300 Subject: [PATCH 12/61] Removed dev mode for Universal Links ios. --- ios/Runner/Runner.entitlements | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 4c5e8c56..4dde98e3 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -6,7 +6,7 @@ development com.apple.developer.associated-domains - applinks:qr.thingsboard.fun?mode=developer + applinks:qr.thingsboard.fun From e5182c4c8013b6062e0aad0b06bdc7d0e375c81e Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 9 May 2024 13:09:43 +0300 Subject: [PATCH 13/61] Resolved the UI issue for iOS in the switch endpoint view. --- .../presentation/view/switch_endpoint_noauth_view.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart index 8d102a4b..43ef8fea 100644 --- a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart +++ b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart @@ -37,10 +37,9 @@ class _SwitchEndpointNoAuthViewState extends State { parameters: widget.arguments, ), ), - child: SafeArea( - bottom: false, - child: Scaffold( - body: BlocConsumer( + child: Scaffold( + body: SafeArea( + child: BlocConsumer( listener: (context, state) { if (state is NoAuthErrorState) { widget.tbContext.showErrorNotification(state.message); From 4b131af2cea3a5eebf98fa042464e4a593f75d9a Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 13 May 2024 13:36:22 +0300 Subject: [PATCH 14/61] QR-code - Scan QR Button in the Login Window. (#84) * Changed login with qr code UI. * Added new qr code icons. * QR Code UI fix. --- assets/images/qr-demo-thingsboard.svg | 91 ----------------- assets/images/qr_code_scanner.svg | 10 ++ assets/images/qr_code_scanner2.svg | 11 +++ lib/constants/assets_path.dart | 3 +- lib/core/auth/login/login_page.dart | 134 ++++++++++---------------- 5 files changed, 74 insertions(+), 175 deletions(-) delete mode 100644 assets/images/qr-demo-thingsboard.svg create mode 100644 assets/images/qr_code_scanner.svg create mode 100644 assets/images/qr_code_scanner2.svg diff --git a/assets/images/qr-demo-thingsboard.svg b/assets/images/qr-demo-thingsboard.svg deleted file mode 100644 index 08b58dcd..00000000 --- a/assets/images/qr-demo-thingsboard.svg +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/qr_code_scanner.svg b/assets/images/qr_code_scanner.svg new file mode 100644 index 00000000..ec73e771 --- /dev/null +++ b/assets/images/qr_code_scanner.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/qr_code_scanner2.svg b/assets/images/qr_code_scanner2.svg new file mode 100644 index 00000000..6c3e4b3f --- /dev/null +++ b/assets/images/qr_code_scanner2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/constants/assets_path.dart b/lib/constants/assets_path.dart index 973953c6..564a2f6e 100644 --- a/lib/constants/assets_path.dart +++ b/lib/constants/assets_path.dart @@ -13,6 +13,7 @@ abstract class ThingsboardImage { 'github-logo': 'assets/images/github-logo.svg', 'facebook-logo': 'assets/images/facebook-logo.svg', 'apple-logo': 'assets/images/apple-logo.svg', - 'qr-code-logo': 'assets/images/qr-demo-thingsboard.svg' + 'qr-code-logo': 'assets/images/qr_code_scanner.svg', + 'qr-code': 'assets/images/qr_code_scanner2.svg' }; } diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 65d3bf77..0a8a79b3 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -106,6 +106,8 @@ class _LoginPageState extends TbPageState { ), ), Row( + mainAxisAlignment: + MainAxisAlignment.center, children: [ OutlinedButton( style: _oauth2IconButtonStyle, @@ -131,10 +133,22 @@ class _LoginPageState extends TbPageState { ); } }, - child: SvgPicture.asset( - ThingsboardImage - .oauth2Logos['qr-code-logo']!, - height: 24, + child: Row( + children: [ + SvgPicture.asset( + ThingsboardImage.oauth2Logos[ + 'qr-code-logo']!, + height: 24, + ), + const SizedBox(width: 8), + Text( + 'Scan QR code', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + ), + ), + ], ), ) ], @@ -281,81 +295,35 @@ class _LoginPageState extends TbPageState { } Widget _buildOAuth2Buttons(List clients) { - if (clients.length == 1 || clients.length > 6) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ...clients - .asMap() - .map( - (index, client) => MapEntry( - index, - _buildOAuth2Button( - client, - 'Login with ${client.name}', - false, - index == clients.length - 1, - ), - ), - ) - .values - .toList(), - OutlinedButton( - style: _oauth2IconButtonStyle, - onPressed: () async { - try { - final barcode = await tbContext.navigateTo( - '/qrCodeScan', - transition: TransitionType.nativeModal, - ); - - if (barcode != null && barcode.code != null) { - tbContext.navigateByAppLink( - barcode.code, - ); - } else {} - } catch (e) { - log.error( - 'Login with qr code error', - e, - ); - } - }, - child: SvgPicture.asset( - ThingsboardImage.oauth2Logos['qr-code-logo']!, - height: 24, - ), - ), - ], - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: const Center( - child: Text('LOGIN WITH'), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: const Center( + child: Text('LOGIN WITH'), ), - Row( - children: [ - ...clients - .asMap() - .map( - (index, client) => MapEntry( - index, - _buildOAuth2Button( - client, - clients.length == 2 ? client.name : null, - true, - index == clients.length - 1, - ), + ), + Row( + children: [ + ...clients + .asMap() + .map( + (index, client) => MapEntry( + index, + _buildOAuth2Button( + client, + clients.length == 2 ? client.name : null, + true, + index == clients.length - 1, ), - ) - .values - .toList(), - OutlinedButton( + ), + ) + .values + .toList(), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( style: _oauth2IconButtonStyle, onPressed: () async { try { @@ -377,15 +345,15 @@ class _LoginPageState extends TbPageState { } }, child: SvgPicture.asset( - ThingsboardImage.oauth2Logos['qr-code-logo']!, + ThingsboardImage.oauth2Logos['qr-code']!, height: 24, ), ), - ], - ) - ], - ); - } + ), + ], + ) + ], + ); } Widget _buildOAuth2Button( From 7b4454d9e3a2fae28d28984b0655c68db232fad4 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 15 May 2024 15:26:23 +0300 Subject: [PATCH 15/61] Scan QR Button in the Login Window. (#86) * Changed login with qr code UI. * Added new qr code icons. * QR Code UI fix. * Changed login with qr code UI. * QR code images * QR Code UI fix. * Fix OAuth2 redirect issue. --- lib/core/context/tb_context.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index 9e153b03..f5f55d2e 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -299,6 +299,7 @@ class TbContext implements PopEntry { oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients( pkgName: packageName, platform: _oauth2PlatformType, + requestConfig: RequestConfig(followRedirect: false), ); } From 0cd9c7c85f7fccb4f9d1788b1d14da0242175582 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 21 May 2024 17:00:31 +0300 Subject: [PATCH 16/61] Fixed an issue with auth by qr code. (#88) --- .../presentation/view/switch_endpoint_noauth_view.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart index 43ef8fea..808888fb 100644 --- a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart +++ b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart @@ -6,13 +6,14 @@ import 'package:thingsboard_app/core/auth/noauth/di/noauth_di.dart'; import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/locator.dart'; -class SwitchEndpointNoAuthView extends StatefulWidget { +class SwitchEndpointNoAuthView extends TbPageWidget { SwitchEndpointNoAuthView({ required this.tbContext, required this.arguments, - }); + }) : super(tbContext); final Map? arguments; final TbContext tbContext; @@ -21,7 +22,8 @@ class SwitchEndpointNoAuthView extends StatefulWidget { State createState() => _SwitchEndpointNoAuthViewState(); } -class _SwitchEndpointNoAuthViewState extends State { +class _SwitchEndpointNoAuthViewState + extends TbPageState { @override Widget build(BuildContext context) { if (getIt().isClosed) { From 4048912c20343d8acb516ffc216530ebb70bce48 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 22 May 2024 16:59:46 +0300 Subject: [PATCH 17/61] Fixed qr code issues. --- .../remote/i_noauth_remote_datasource.dart | 2 + .../remote/noauth_remote_datasource.dart | 5 +++ .../data/repository/noauth_repository.dart | 5 +++ .../repository/i_noauth_repository.dart | 2 + .../usecases/switch_endpoint_usecase.dart | 43 +++++++++++++++---- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart index 80c39920..a22b3383 100644 --- a/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart +++ b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart @@ -16,4 +16,6 @@ abstract interface class INoAuthRemoteDatasource { required VoidCallback onDone, required ErrorCallback onError, }); + + bool isAuthenticated(); } diff --git a/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart index 712fa35d..4d9f9aa3 100644 --- a/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart +++ b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart @@ -66,4 +66,9 @@ class NoAuthRemoteDatasource implements INoAuthRemoteDatasource { onError: onError, ); } + + @override + bool isAuthenticated() { + return tbContext.isAuthenticated; + } } diff --git a/lib/core/auth/noauth/data/repository/noauth_repository.dart b/lib/core/auth/noauth/data/repository/noauth_repository.dart index 625cb937..3e4bc990 100644 --- a/lib/core/auth/noauth/data/repository/noauth_repository.dart +++ b/lib/core/auth/noauth/data/repository/noauth_repository.dart @@ -45,4 +45,9 @@ class NoAuthRepository implements INoAuthRepository { onError: onError, ); } + + @override + bool isAuthenticated() { + return remoteDatasource.isAuthenticated(); + } } diff --git a/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart index 43039ab4..03db7f26 100644 --- a/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart +++ b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart @@ -16,4 +16,6 @@ abstract interface class INoAuthRepository { required VoidCallback onDone, required ErrorCallback onError, }); + + bool isAuthenticated(); } diff --git a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart index a3707d65..a0910bc7 100644 --- a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart +++ b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/firebase_options.dart'; import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; @@ -41,24 +42,48 @@ class SwitchEndpointUseCase extends UseCase { final uri = params['uri']!; final host = params['host'] ?? uri.origin; final key = params['secret']!; + final currentEndpoint = await getIt().getEndpoint(); + final isTheSameHost = + Uri.parse(host).host.compareTo(Uri.parse(currentEndpoint).host) == 0; try { _progressSteamCtrl.add('Getting data from your host $host'); final loginData = await repository.getJwtToken(host: host, key: key); - _progressSteamCtrl.add('Logout you ...'); - await repository.logout( - requestConfig: RequestConfig(ignoreErrors: true), - notifyUser: false, - ); + if (repository.isAuthenticated()) { + _progressSteamCtrl.add('Logout you ...'); + await repository.logout( + requestConfig: RequestConfig(ignoreErrors: true), + notifyUser: false, + ); + } + + if (isTheSameHost) { + _progressSteamCtrl.add('Logging you into the host $host'); + } else { + _progressSteamCtrl.add('Switching you to the new host $host'); + } - _progressSteamCtrl.add('Switching you to the new host $host'); await repository.setUserFromJwtToken(loginData); - logger.debug('SwitchEndpointUseCase:deleteFB App'); - await getIt().removeApp(); + if (!isTheSameHost) { + logger.debug('SwitchEndpointUseCase:deleteFB App'); + await getIt() + ..removeApp() + ..removeApp(name: currentEndpoint); + await getIt().setEndpoint(host); + + // If we revert to the original host configured in the app_constants + if (!await getIt().isCustomEndpoint()) { + await getIt().initializeApp( + name: host, + options: DefaultFirebaseOptions.currentPlatform, + ); + } + } - await getIt().setEndpoint(host); + // A re-initialization is required if we set 'notifyUser' to true for + // 'setUserFromJwtToken'. This code will be executed twice. await repository.reInit( endpoint: host, onDone: params.onDone, From 7b908c8a78cf66009de20dca795fe1756cf8e6ea Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 22 May 2024 17:11:32 +0300 Subject: [PATCH 18/61] FirebaseService: added a name parameter to initializeApp. --- lib/utils/services/firebase/firebase_service.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utils/services/firebase/firebase_service.dart b/lib/utils/services/firebase/firebase_service.dart index 09dcd01b..884e76ba 100644 --- a/lib/utils/services/firebase/firebase_service.dart +++ b/lib/utils/services/firebase/firebase_service.dart @@ -21,6 +21,8 @@ class FirebaseService implements IFirebaseService { String name = defaultFirebaseAppName, FirebaseOptions? options, }) async { + logger.debug('FirebaseService::initializeApp(name: $name)'); + try { if (await endpointService.isCustomEndpoint()) { throw UnimplementedError( @@ -29,7 +31,7 @@ class FirebaseService implements IFirebaseService { ); } - final app = await Firebase.initializeApp(options: options); + final app = await Firebase.initializeApp(options: options, name: name); _apps.add(name); return app; From 6abd1e593541e0f33674f2b5b0035cec7405dcb3 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 24 May 2024 10:49:47 +0300 Subject: [PATCH 19/61] Fixed crash issue on IOS when switch back to the original endpoint. --- .../auth/noauth/domain/usecases/switch_endpoint_usecase.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart index a0910bc7..1641e4a1 100644 --- a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart +++ b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart @@ -76,7 +76,6 @@ class SwitchEndpointUseCase extends UseCase { // If we revert to the original host configured in the app_constants if (!await getIt().isCustomEndpoint()) { await getIt().initializeApp( - name: host, options: DefaultFirebaseOptions.currentPlatform, ); } From 3d1538b4869006b53bd71eb389afae178a03ca64 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 4 Jun 2024 11:26:21 +0300 Subject: [PATCH 20/61] Added dart linker support. (#87) * web supports message. * Fixed linter issues. --- analysis_options.yaml | 28 + lib/config/routes/router.dart | 15 +- lib/config/themes/tb_theme.dart | 58 +- lib/constants/assets_path.dart | 14 +- lib/core/auth/auth_routes.dart | 21 +- lib/core/auth/login/login_page.dart | 555 ++++++++------- .../auth/login/login_page_background.dart | 12 +- .../login/reset_password_request_page.dart | 151 ++-- .../login/two_factor_authentication_page.dart | 671 ++++++++++-------- .../widgets/endpoint_name_widget.dart | 6 +- .../widgets/noauth_loading_widget.dart | 2 +- lib/core/auth/oauth2/tb_oauth2_client.dart | 31 +- lib/core/auth/web/tb_web_auth.dart | 14 +- lib/core/context/tb_context.dart | 204 +++--- lib/core/context/tb_context_widget.dart | 7 +- lib/core/entity/entities_base.dart | 220 +++--- lib/core/entity/entities_grid.dart | 4 +- lib/core/entity/entities_list.dart | 48 +- lib/core/entity/entities_list_widget.dart | 256 ++++--- lib/core/entity/entity_details_page.dart | 240 ++++--- lib/core/entity/entity_grid_card.dart | 70 +- lib/core/entity/entity_list_card.dart | 78 +- lib/core/init/init_app.dart | 4 +- lib/core/init/init_routes.dart | 9 +- lib/generated/intl/messages_all.dart | 6 +- lib/generated/intl/messages_en.dart | 48 +- lib/generated/intl/messages_zh.dart | 6 +- lib/main.dart | 13 +- lib/modules/alarm/alarm_routes.dart | 19 +- lib/modules/alarm/alarms_base.dart | 379 +++++----- lib/modules/alarm/alarms_list.dart | 8 +- lib/modules/alarm/alarms_page.dart | 32 +- lib/modules/asset/asset_details_page.dart | 58 +- lib/modules/asset/asset_routes.dart | 20 +- lib/modules/asset/assets_base.dart | 179 +++-- lib/modules/asset/assets_list.dart | 9 +- lib/modules/asset/assets_list_widget.dart | 8 +- lib/modules/asset/assets_page.dart | 35 +- .../audit_log/audit_log_details_page.dart | 153 ++-- lib/modules/audit_log/audit_logs_base.dart | 279 ++++---- lib/modules/audit_log/audit_logs_list.dart | 8 +- lib/modules/audit_log/audit_logs_page.dart | 31 +- lib/modules/audit_log/audit_logs_routes.dart | 13 +- .../customer/customer_details_page.dart | 16 +- lib/modules/customer/customer_routes.dart | 25 +- lib/modules/customer/customers_list.dart | 8 +- lib/modules/customer/customers_page.dart | 31 +- lib/modules/dashboard/dashboard.dart | 156 ++-- lib/modules/dashboard/dashboard_page.dart | 57 +- lib/modules/dashboard/dashboard_routes.dart | 49 +- lib/modules/dashboard/dashboards_base.dart | 225 +++--- lib/modules/dashboard/dashboards_grid.dart | 15 +- lib/modules/dashboard/dashboards_list.dart | 8 +- .../dashboard/dashboards_list_widget.dart | 8 +- lib/modules/dashboard/dashboards_page.dart | 14 +- .../dashboard/fullscreen_dashboard_page.dart | 78 +- .../dashboard/main_dashboard_page.dart | 38 +- lib/modules/device/device_details_page.dart | 12 +- lib/modules/device/device_profiles_base.dart | 552 +++++++------- lib/modules/device/device_profiles_grid.dart | 6 +- lib/modules/device/device_routes.dart | 58 +- lib/modules/device/devices_base.dart | 570 ++++++++------- lib/modules/device/devices_list.dart | 9 +- lib/modules/device/devices_list_page.dart | 109 +-- lib/modules/device/devices_list_widget.dart | 8 +- lib/modules/device/devices_main_page.dart | 18 +- lib/modules/device/devices_page.dart | 17 +- lib/modules/home/home_page.dart | 78 +- lib/modules/home/home_routes.dart | 9 +- lib/modules/main/main_page.dart | 105 +-- lib/modules/more/more_page.dart | 307 ++++---- .../notification/notification_page.dart | 8 +- .../widgets/filter_segmented_button.dart | 5 +- .../no_notifications_found_widget.dart | 2 +- .../widgets/notification_icon.dart | 15 +- .../widgets/notification_list.dart | 15 +- .../widgets/notification_slidable_widget.dart | 37 +- .../widgets/notification_widget.dart | 5 +- lib/modules/profile/change_password_page.dart | 297 ++++---- lib/modules/profile/profile_page.dart | 222 +++--- lib/modules/profile/profile_routes.dart | 13 +- lib/modules/tenant/tenant_details_page.dart | 16 +- lib/modules/tenant/tenant_routes.dart | 21 +- lib/modules/tenant/tenants_list.dart | 8 +- lib/modules/tenant/tenants_page.dart | 31 +- lib/modules/tenant/tenants_widget.dart | 4 +- lib/modules/url/url_page.dart | 2 +- lib/utils/services/_tb_secure_storage.dart | 2 +- lib/utils/services/device_profile_cache.dart | 19 +- lib/utils/services/entity_query_api.dart | 75 +- lib/utils/services/notification_service.dart | 20 +- lib/utils/services/widget_action_handler.dart | 80 ++- lib/utils/ui/qr_code_scanner.dart | 117 +-- lib/utils/ui_utils_routes.dart | 11 +- lib/utils/utils.dart | 283 +++++--- lib/widgets/tb_app_bar.dart | 139 ++-- lib/widgets/tb_progress_indicator.dart | 43 +- lib/widgets/two_page_view.dart | 39 +- lib/widgets/two_value_listenable_builder.dart | 6 +- pubspec.lock | 386 +++++++++- pubspec.yaml | 2 + test/ core/noauth/switch_endpoint_test.dart | 7 +- test/widget_test.dart | 18 +- 103 files changed, 5008 insertions(+), 3548 deletions(-) create mode 100644 analysis_options.yaml diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..69c1ffcd --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + require_trailing_commas: true + prefer_single_quotes: true + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/config/routes/router.dart b/lib/config/routes/router.dart index 4c09604c..536f46c2 100644 --- a/lib/config/routes/router.dart +++ b/lib/config/routes/router.dart @@ -23,13 +23,14 @@ class ThingsboardAppRouter { ThingsboardAppRouter() { router.notFoundHandler = Handler( - handlerFunc: (BuildContext? context, Map> params) { - var settings = context!.settings; - return Scaffold( - appBar: AppBar(title: Text('Not Found')), - body: Center(child: Text('Route not defined: ${settings!.name}')), - ); - }); + handlerFunc: (BuildContext? context, Map> params) { + var settings = context!.settings; + return Scaffold( + appBar: AppBar(title: const Text('Not Found')), + body: Center(child: Text('Route not defined: ${settings!.name}')), + ); + }, + ); InitRoutes(_tbContext).registerRoutes(); AuthRoutes(_tbContext).registerRoutes(); diff --git a/lib/config/themes/tb_theme.dart b/lib/config/themes/tb_theme.dart index aa985595..5760c1f2 100644 --- a/lib/config/themes/tb_theme.dart +++ b/lib/config/themes/tb_theme.dart @@ -46,39 +46,39 @@ const tbDarkMatIndigo = MaterialColor( final ThemeData theme = ThemeData(primarySwatch: tbMatIndigo); ThemeData tbTheme = ThemeData( - useMaterial3: false, - primarySwatch: tbMatIndigo, - colorScheme: theme.colorScheme - .copyWith(primary: tbMatIndigo, secondary: Colors.deepOrange), - scaffoldBackgroundColor: Color(0xFFFAFAFA), - textTheme: tbTypography.black, - primaryTextTheme: tbTypography.black, - typography: tbTypography, - appBarTheme: AppBarTheme( - backgroundColor: Colors.white, - foregroundColor: _tbTextColor, - /* titleTextStyle: TextStyle( - color: _tbTextColor - ), - toolbarTextStyle: TextStyle( - color: _tbTextColor - ), */ - iconTheme: IconThemeData(color: _tbTextColor)), - bottomNavigationBarTheme: BottomNavigationBarThemeData( - backgroundColor: Colors.white, - selectedItemColor: _tbPrimaryColor, - unselectedItemColor: _tbPrimaryColor.withAlpha((255 * 0.38).ceil()), - showSelectedLabels: true, - showUnselectedLabels: true), - pageTransitionsTheme: PageTransitionsTheme(builders: { + useMaterial3: false, + primarySwatch: tbMatIndigo, + colorScheme: theme.colorScheme + .copyWith(primary: tbMatIndigo, secondary: Colors.deepOrange), + scaffoldBackgroundColor: const Color(0xFFFAFAFA), + textTheme: tbTypography.black, + primaryTextTheme: tbTypography.black, + typography: tbTypography, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: _tbTextColor, + iconTheme: IconThemeData(color: _tbTextColor), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: Colors.white, + selectedItemColor: _tbPrimaryColor, + unselectedItemColor: _tbPrimaryColor.withAlpha((255 * 0.38).ceil()), + showSelectedLabels: true, + showUnselectedLabels: true, + ), + pageTransitionsTheme: const PageTransitionsTheme( + builders: { TargetPlatform.iOS: FadeOpenPageTransitionsBuilder(), TargetPlatform.android: FadeOpenPageTransitionsBuilder(), - })); + }, + ), +); final ThemeData darkTheme = ThemeData(primarySwatch: tbDarkMatIndigo, brightness: Brightness.dark); ThemeData tbDarkTheme = ThemeData( - primarySwatch: tbDarkMatIndigo, - colorScheme: darkTheme.colorScheme.copyWith(secondary: Colors.deepOrange), - brightness: Brightness.dark); + primarySwatch: tbDarkMatIndigo, + colorScheme: darkTheme.colorScheme.copyWith(secondary: Colors.deepOrange), + brightness: Brightness.dark, +); diff --git a/lib/constants/assets_path.dart b/lib/constants/assets_path.dart index 564a2f6e..ed4e1ba9 100644 --- a/lib/constants/assets_path.dart +++ b/lib/constants/assets_path.dart @@ -1,11 +1,11 @@ abstract class ThingsboardImage { - static final thingsBoardWithTitle = + static const thingsBoardWithTitle = 'assets/images/thingsboard_with_title.svg'; - static final thingsboard = 'assets/images/thingsboard.svg'; - static final thingsboardOuter = 'assets/images/thingsboard_outer.svg'; - static final thingsboardCenter = 'assets/images/thingsboard_center.svg'; - static final dashboardPlaceholder = 'assets/images/dashboard-placeholder.svg'; - static final deviceProfilePlaceholder = + static const thingsboard = 'assets/images/thingsboard.svg'; + static const thingsboardOuter = 'assets/images/thingsboard_outer.svg'; + static const thingsboardCenter = 'assets/images/thingsboard_center.svg'; + static const dashboardPlaceholder = 'assets/images/dashboard-placeholder.svg'; + static const deviceProfilePlaceholder = 'assets/images/device-profile-placeholder.svg'; static final oauth2Logos = { @@ -14,6 +14,6 @@ abstract class ThingsboardImage { 'facebook-logo': 'assets/images/facebook-logo.svg', 'apple-logo': 'assets/images/apple-logo.svg', 'qr-code-logo': 'assets/images/qr_code_scanner.svg', - 'qr-code': 'assets/images/qr_code_scanner2.svg' + 'qr-code': 'assets/images/qr_code_scanner2.svg', }; } diff --git a/lib/core/auth/auth_routes.dart b/lib/core/auth/auth_routes.dart index 527eb967..dea26280 100644 --- a/lib/core/auth/auth_routes.dart +++ b/lib/core/auth/auth_routes.dart @@ -10,19 +10,22 @@ import 'login/login_page.dart'; class AuthRoutes extends TbRoutes { late var loginHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return LoginPage(tbContext); - }); + handlerFunc: (BuildContext? context, Map params) { + return LoginPage(tbContext); + }, + ); late var resetPasswordRequestHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return ResetPasswordRequestPage(tbContext); - }); + handlerFunc: (BuildContext? context, Map params) { + return ResetPasswordRequestPage(tbContext); + }, + ); late var twoFactorAuthenticationHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return TwoFactorAuthenticationPage(tbContext); - }); + handlerFunc: (BuildContext? context, Map params) { + return TwoFactorAuthenticationPage(tbContext); + }, + ); AuthRoutes(TbContext tbContext) : super(tbContext); diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 0a8a79b3..2076977b 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -17,20 +17,23 @@ import 'package:thingsboard_client/thingsboard_client.dart'; import 'login_page_background.dart'; class LoginPage extends TbPageWidget { - LoginPage(TbContext tbContext) : super(tbContext); + LoginPage(TbContext tbContext, {super.key}) : super(tbContext); @override - _LoginPageState createState() => _LoginPageState(); + State createState() => _LoginPageState(); } class _LoginPageState extends TbPageState { final ButtonStyle _oauth2ButtonWithTextStyle = OutlinedButton.styleFrom( - padding: EdgeInsets.all(16), - alignment: Alignment.centerLeft, - foregroundColor: Colors.black87); + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + foregroundColor: Colors.black87, + ); final ButtonStyle _oauth2IconButtonStyle = OutlinedButton.styleFrom( - padding: EdgeInsets.all(16), alignment: Alignment.center); + padding: const EdgeInsets.all(16), + alignment: Alignment.center, + ); final _isLoginNotifier = ValueNotifier(false); final _showPasswordNotifier = ValueNotifier(false); @@ -47,251 +50,276 @@ class _LoginPageState extends TbPageState { } } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, - body: Stack(children: [ - LoginPageBackground(), - Positioned.fill(child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - padding: EdgeInsets.fromLTRB(24, 71, 24, 24), + backgroundColor: Colors.white, + body: Stack( + children: [ + const LoginPageBackground(), + Positioned.fill( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 71, 24, 24), child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - (71 + 24)), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + constraints: BoxConstraints( + minHeight: constraints.maxHeight - (71 + 24), + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( children: [ - Row(children: [ - SvgPicture.asset( - ThingsboardImage.thingsBoardWithTitle, - height: 25, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, - BlendMode.srcIn), - semanticsLabel: - '${S.of(context).logoDefaultValue}') - ]), - SizedBox(height: 32), - Row(children: [ - Text('${S.of(context).loginNotification}', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 28, - height: 36 / 28)) - ]), - SizedBox(height: 48), - if (tbContext.hasOAuthClients) - _buildOAuth2Buttons( - tbContext.oauth2ClientInfos!, + SvgPicture.asset( + ThingsboardImage.thingsBoardWithTitle, + height: 25, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.srcIn, ), - Visibility( - visible: !tbContext.hasOAuthClients, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Container( - padding: const EdgeInsets.symmetric( - vertical: 16), - child: const Center( - child: Text('LOGIN WITH'), - ), - ), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - OutlinedButton( - style: _oauth2IconButtonStyle, - onPressed: () async { - try { - final barcode = - await tbContext.navigateTo( - '/qrCodeScan', - transition: - TransitionType.nativeModal, - ); - - if (barcode != null && - barcode.code != null) { - tbContext.navigateByAppLink( - barcode.code, - ); - } else {} - } catch (e) { - log.error( - 'Login with qr code error', - e, - ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - ThingsboardImage.oauth2Logos[ - 'qr-code-logo']!, - height: 24, - ), - const SizedBox(width: 8), - Text( - 'Scan QR code', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ) - ], - ) - ], + semanticsLabel: S.of(context).logoDefaultValue, + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Text( + S.of(context).loginNotification, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 28, + height: 36 / 28, ), ), - Padding( - padding: EdgeInsets.only(top: 10, bottom: 16), - child: Row( + ], + ), + const SizedBox(height: 48), + if (tbContext.hasOAuthClients) + _buildOAuth2Buttons( + tbContext.oauth2ClientInfos!, + ), + Visibility( + visible: !tbContext.hasOAuthClients, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + child: const Center( + child: Text('LOGIN WITH'), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Flexible(child: Divider()), - Padding( - padding: - EdgeInsets.symmetric(horizontal: 16), - child: Text('${S.of(context).OR}'), + OutlinedButton( + style: _oauth2IconButtonStyle, + onPressed: () async { + try { + final barcode = + await tbContext.navigateTo( + '/qrCodeScan', + transition: + TransitionType.nativeModal, + ); + + if (barcode != null && + barcode.code != null) { + tbContext.navigateByAppLink( + barcode.code, + ); + } else {} + } catch (e) { + log.error( + 'Login with qr code error', + e, + ); + } + }, + child: Row( + children: [ + SvgPicture.asset( + ThingsboardImage + .oauth2Logos['qr-code-logo']!, + height: 24, + ), + const SizedBox(width: 8), + const Text( + 'Scan QR code', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + ), + ), + ], + ), ), - Flexible(child: Divider()) ], ), - ), - FormBuilder( - key: _loginFormKey, - autovalidateMode: AutovalidateMode.disabled, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - FormBuilderTextField( - name: 'username', - keyboardType: - TextInputType.emailAddress, - validator: - FormBuilderValidators.compose([ - FormBuilderValidators.required( - errorText: - '${S.of(context).emailRequireText}'), - FormBuilderValidators.email( - errorText: - '${S.of(context).emailInvalidText}') - ]), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: - '${S.of(context).email}'), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 16, + ), + child: Row( + children: [ + const Flexible(child: Divider()), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Text(S.of(context).OR), + ), + const Flexible(child: Divider()), + ], + ), + ), + FormBuilder( + key: _loginFormKey, + autovalidateMode: AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormBuilderTextField( + name: 'username', + keyboardType: TextInputType.emailAddress, + validator: FormBuilderValidators.compose( + [ + FormBuilderValidators.required( + errorText: + S.of(context).emailRequireText, + ), + FormBuilderValidators.email( + errorText: + S.of(context).emailInvalidText, ), - SizedBox(height: 28), - ValueListenableBuilder( - valueListenable: - _showPasswordNotifier, - builder: (BuildContext context, - bool showPassword, child) { - return FormBuilderTextField( - name: 'password', - obscureText: !showPassword, - validator: FormBuilderValidators - .compose([ - FormBuilderValidators.required( - errorText: - '${S.of(context).passwordRequireText}') - ]), - decoration: InputDecoration( - suffixIcon: IconButton( - icon: Icon(showPassword - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - _showPasswordNotifier - .value = - !_showPasswordNotifier - .value; - }, - ), - border: OutlineInputBorder(), - labelText: - '${S.of(context).password}'), - ); - }) ], - )), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - _forgotPassword(); - }, - child: Text( - '${S.of(context).passwordForgotText}', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .primary, - letterSpacing: 1, - fontSize: 12, - height: 16 / 12), - ), - ) - ], - ), - Spacer(), - ElevatedButton( - child: Text('${S.of(context).login}'), - style: ElevatedButton.styleFrom( - padding: - EdgeInsets.symmetric(vertical: 16)), + ), + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: S.of(context).email, + ), + ), + const SizedBox(height: 28), + ValueListenableBuilder( + valueListenable: _showPasswordNotifier, + builder: ( + BuildContext context, + bool showPassword, + child, + ) { + return FormBuilderTextField( + name: 'password', + obscureText: !showPassword, + validator: FormBuilderValidators.compose( + [ + FormBuilderValidators.required( + errorText: S + .of(context) + .passwordRequireText, + ), + ], + ), + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + _showPasswordNotifier.value = + !_showPasswordNotifier.value; + }, + ), + border: const OutlineInputBorder(), + labelText: S.of(context).password, + ), + ); + }, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( onPressed: () { - _login(); + _forgotPassword(); }, - ), - SizedBox(height: 48) - ]), - ))); - }, - )), - ValueListenableBuilder( - valueListenable: _isLoginNotifier, - builder: (BuildContext context, bool loading, child) { - if (loading) { - var data = MediaQuery.of(context); - var bottomPadding = data.padding.top; - bottomPadding += kToolbarHeight; - return SizedBox.expand( - child: ClipRect( - child: BackdropFilter( - filter: - ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - decoration: new BoxDecoration( + child: Text( + S.of(context).passwordForgotText, + style: TextStyle( color: - Colors.grey.shade200.withOpacity(0.2)), - child: Container( - padding: - EdgeInsets.only(bottom: bottomPadding), - alignment: Alignment.center, - child: TbProgressIndicator(size: 50.0), + Theme.of(context).colorScheme.primary, + letterSpacing: 1, + fontSize: 12, + height: 16 / 12, + ), ), - )))); - } else { - return SizedBox.shrink(); - } - }) - ])); + ), + ], + ), + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: () { + _login(); + }, + child: Text(S.of(context).login), + ), + const SizedBox(height: 48), + ], + ), + ), + ), + ); + }, + ), + ), + ValueListenableBuilder( + valueListenable: _isLoginNotifier, + builder: (BuildContext context, bool loading, child) { + if (loading) { + var data = MediaQuery.of(context); + var bottomPadding = data.padding.top; + bottomPadding += kToolbarHeight; + return SizedBox.expand( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + decoration: BoxDecoration( + color: Colors.grey.shade200.withOpacity(0.2), + ), + child: Container( + padding: EdgeInsets.only(bottom: bottomPadding), + alignment: Alignment.center, + child: const TbProgressIndicator(size: 50.0), + ), + ), + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], + ), + ); } Widget _buildOAuth2Buttons(List clients) { @@ -351,18 +379,24 @@ class _LoginPageState extends TbPageState { ), ), ], - ) + ), ], ); } Widget _buildOAuth2Button( - OAuth2ClientInfo client, String? text, bool expand, bool isLast) { + OAuth2ClientInfo client, + String? text, + bool expand, + bool isLast, + ) { Widget? icon; if (client.icon != null) { if (ThingsboardImage.oauth2Logos.containsKey(client.icon)) { - icon = SvgPicture.asset(ThingsboardImage.oauth2Logos[client.icon]!, - height: 24); + icon = SvgPicture.asset( + ThingsboardImage.oauth2Logos[client.icon]!, + height: 24, + ); } else { String strIcon = client.icon!; if (strIcon.startsWith('mdi:')) { @@ -375,36 +409,40 @@ class _LoginPageState extends TbPageState { } } } - if (icon == null) { - icon = Icon(Icons.login, size: 24, color: Theme.of(context).primaryColor); - } + icon ??= Icon(Icons.login, size: 24, color: Theme.of(context).primaryColor); Widget button; bool iconOnly = text == null; if (iconOnly) { button = OutlinedButton( - style: _oauth2IconButtonStyle, - onPressed: () => _oauth2ButtonPressed(client), - child: icon); + style: _oauth2IconButtonStyle, + onPressed: () => _oauth2ButtonPressed(client), + child: icon, + ); } else { button = OutlinedButton( - style: _oauth2ButtonWithTextStyle, - onPressed: () => _oauth2ButtonPressed(client), - child: Stack(children: [ + style: _oauth2ButtonWithTextStyle, + onPressed: () => _oauth2ButtonPressed(client), + child: Stack( + children: [ Align(alignment: Alignment.centerLeft, child: icon), - Container( + SizedBox( height: 24, child: Align( - alignment: Alignment.center, - child: Text(text, textAlign: TextAlign.center)), - ) - ])); + alignment: Alignment.center, + child: Text(text, textAlign: TextAlign.center), + ), + ), + ], + ), + ); } if (expand) { return Expanded( - child: Padding( - padding: EdgeInsets.only(right: isLast ? 0 : 8), - child: button, - )); + child: Padding( + padding: EdgeInsets.only(right: isLast ? 0 : 8), + child: button, + ), + ); } else { return Padding( padding: EdgeInsets.only(bottom: isLast ? 0 : 8), @@ -419,7 +457,10 @@ class _LoginPageState extends TbPageState { final result = await tbContext.oauth2Client.authenticate(client.url); if (result.success) { await tbClient.setUserFromJwtToken( - result.accessToken, result.refreshToken, true); + result.accessToken, + result.refreshToken, + true, + ); } else { _isLoginNotifier.value = false; showErrorNotification(result.error!); @@ -441,7 +482,7 @@ class _LoginPageState extends TbPageState { await tbClient.login(LoginRequest(username, password)); } catch (e) { _isLoginNotifier.value = false; - if (!(e is ThingsboardError) || + if (e is! ThingsboardError || e.errorCode == ThingsBoardErrorCode.general) { await tbContext.onFatalError(e); } diff --git a/lib/core/auth/login/login_page_background.dart b/lib/core/auth/login/login_page_background.dart index 0d79be51..d9572edd 100644 --- a/lib/core/auth/login/login_page_background.dart +++ b/lib/core/auth/login/login_page_background.dart @@ -1,13 +1,17 @@ import 'package:flutter/material.dart'; class LoginPageBackground extends StatelessWidget { + const LoginPageBackground({super.key}); + @override Widget build(BuildContext context) { return SizedBox.expand( - child: CustomPaint( - painter: - _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor), - )); + child: CustomPaint( + painter: _LoginPageBackgroundPainter( + color: Theme.of(context).primaryColor, + ), + ), + ); } } diff --git a/lib/core/auth/login/reset_password_request_page.dart b/lib/core/auth/login/reset_password_request_page.dart index e6e70a48..14db7fbf 100644 --- a/lib/core/auth/login/reset_password_request_page.dart +++ b/lib/core/auth/login/reset_password_request_page.dart @@ -9,11 +9,10 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; class ResetPasswordRequestPage extends TbPageWidget { - ResetPasswordRequestPage(TbContext tbContext) : super(tbContext); + ResetPasswordRequestPage(TbContext tbContext, {super.key}) : super(tbContext); @override - _ResetPasswordRequestPageState createState() => - _ResetPasswordRequestPageState(); + State createState() => _ResetPasswordRequestPageState(); } class _ResetPasswordRequestPageState @@ -25,77 +24,98 @@ class _ResetPasswordRequestPageState @override Widget build(BuildContext context) { return Scaffold( - body: Stack(children: [ - LoginPageBackground(), - SizedBox.expand( - child: Scaffold( + body: Stack( + children: [ + const LoginPageBackground(), + SizedBox.expand( + child: Scaffold( backgroundColor: Colors.transparent, appBar: TbAppBar( tbContext, - title: Text('${S.of(context).passwordReset}'), + title: Text(S.of(context).passwordReset), ), - body: Stack(children: [ - SizedBox.expand( + body: Stack( + children: [ + SizedBox.expand( child: Padding( - padding: EdgeInsets.all(24), - child: FormBuilder( - key: _resetPasswordFormKey, - autovalidateMode: AutovalidateMode.disabled, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 16), - Text( - '${S.of(context).passwordResetText}', - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 14, - height: 24 / 14), + padding: const EdgeInsets.all(24), + child: FormBuilder( + key: _resetPasswordFormKey, + autovalidateMode: AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + Text( + S.of(context).passwordResetText, + textAlign: TextAlign.center, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 14, + height: 24 / 14, + ), + ), + const SizedBox(height: 61), + FormBuilderTextField( + name: 'email', + autofocus: true, + validator: FormBuilderValidators.compose( + [ + FormBuilderValidators.required( + errorText: S.of(context).emailRequireText, ), - SizedBox(height: 61), - FormBuilderTextField( - name: 'email', - autofocus: true, - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required( - errorText: - '${S.of(context).emailRequireText}'), - FormBuilderValidators.email( - errorText: - '${S.of(context).emailInvalidText}') - ]), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: '${S.of(context).email} *'), + FormBuilderValidators.email( + errorText: S.of(context).emailInvalidText, ), - Spacer(), - ElevatedButton( - child: Text( - '${S.of(context).requestPasswordReset}'), - style: ElevatedButton.styleFrom( - padding: - EdgeInsets.symmetric(vertical: 16)), - onPressed: () { - _requestPasswordReset(); - }, - ) - ])))), - ValueListenableBuilder( + ], + ), + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: '${S.of(context).email} *', + ), + ), + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: () { + _requestPasswordReset(); + }, + child: Text( + S.of(context).requestPasswordReset, + ), + ), + ], + ), + ), + ), + ), + ValueListenableBuilder( valueListenable: _isLoadingNotifier, builder: (BuildContext context, bool loading, child) { if (loading) { return SizedBox.expand( - child: Container( - color: Color(0x99FFFFFF), - child: Center(child: TbProgressIndicator(size: 50.0)), - )); + child: Container( + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator(size: 50.0), + ), + ), + ); } else { - return SizedBox.shrink(); + return const SizedBox.shrink(); } - }) - ]))) - ])); + }, + ), + ], + ), + ), + ), + ], + ), + ); } void _requestPasswordReset() async { @@ -105,11 +125,14 @@ class _ResetPasswordRequestPageState String email = formValue['email']; _isLoadingNotifier.value = true; try { - await Future.delayed(Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); await tbClient.sendResetPasswordLink(email); _isLoadingNotifier.value = false; - showSuccessNotification( - '${S.of(context).passwordResetLinkSuccessfullySentNotification}'); + if (mounted) { + showSuccessNotification( + S.of(context).passwordResetLinkSuccessfullySentNotification, + ); + } } catch (e) { _isLoadingNotifier.value = false; } diff --git a/lib/core/auth/login/two_factor_authentication_page.dart b/lib/core/auth/login/two_factor_authentication_page.dart index d15d23e1..14e43555 100644 --- a/lib/core/auth/login/two_factor_authentication_page.dart +++ b/lib/core/auth/login/two_factor_authentication_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; @@ -7,13 +8,14 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart import 'package:thingsboard_app/core/auth/login/login_page_background.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_app_bar.dart'; import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -import 'package:collection/collection.dart'; typedef ProviderDescFunction = String Function( - BuildContext context, String? contact); + BuildContext context, + String? contact, +); typedef TextFunction = String Function(BuildContext context); class TwoFactorAuthProviderLoginData { @@ -21,84 +23,94 @@ class TwoFactorAuthProviderLoginData { ProviderDescFunction descFunction; TextFunction placeholderFunction; String icon; - TwoFactorAuthProviderLoginData( - {required this.nameFunction, - required this.descFunction, - required this.placeholderFunction, - required this.icon}); + + TwoFactorAuthProviderLoginData({ + required this.nameFunction, + required this.descFunction, + required this.placeholderFunction, + required this.icon, + }); } -final Map - twoFactorAuthProvidersLoginData = { +final twoFactorAuthProvidersLoginData = + { TwoFaProviderType.TOTP: TwoFactorAuthProviderLoginData( - nameFunction: (context) => S.of(context).mfaProviderTopt, - descFunction: (context, contact) => S.of(context).totpAuthDescription, - placeholderFunction: (context) => S.of(context).toptAuthPlaceholder, - icon: 'cellphone-key'), + nameFunction: (context) => S.of(context).mfaProviderTopt, + descFunction: (context, contact) => S.of(context).totpAuthDescription, + placeholderFunction: (context) => S.of(context).toptAuthPlaceholder, + icon: 'cellphone-key', + ), TwoFaProviderType.SMS: TwoFactorAuthProviderLoginData( - nameFunction: (context) => S.of(context).mfaProviderSms, - descFunction: (context, contact) => - S.of(context).smsAuthDescription(contact ?? ''), - placeholderFunction: (context) => S.of(context).smsAuthPlaceholder, - icon: 'message-reply-text-outline'), + nameFunction: (context) => S.of(context).mfaProviderSms, + descFunction: (context, contact) => + S.of(context).smsAuthDescription(contact ?? ''), + placeholderFunction: (context) => S.of(context).smsAuthPlaceholder, + icon: 'message-reply-text-outline', + ), TwoFaProviderType.EMAIL: TwoFactorAuthProviderLoginData( - nameFunction: (context) => S.of(context).mfaProviderEmail, - descFunction: (context, contact) => - S.of(context).emailAuthDescription(contact ?? ''), - placeholderFunction: (context) => S.of(context).emailAuthPlaceholder, - icon: 'email-outline'), + nameFunction: (context) => S.of(context).mfaProviderEmail, + descFunction: (context, contact) => + S.of(context).emailAuthDescription(contact ?? ''), + placeholderFunction: (context) => S.of(context).emailAuthPlaceholder, + icon: 'email-outline', + ), TwoFaProviderType.BACKUP_CODE: TwoFactorAuthProviderLoginData( - nameFunction: (context) => S.of(context).mfaProviderBackupCode, - descFunction: (context, contact) => - S.of(context).backupCodeAuthDescription, - placeholderFunction: (context) => S.of(context).backupCodeAuthPlaceholder, - icon: 'lock-outline') + nameFunction: (context) => S.of(context).mfaProviderBackupCode, + descFunction: (context, contact) => S.of(context).backupCodeAuthDescription, + placeholderFunction: (context) => S.of(context).backupCodeAuthPlaceholder, + icon: 'lock-outline', + ), }; class TwoFactorAuthenticationPage extends TbPageWidget { - TwoFactorAuthenticationPage(TbContext tbContext) : super(tbContext); + TwoFactorAuthenticationPage(TbContext tbContext, {super.key}) + : super(tbContext); @override - _TwoFactorAuthenticationPageState createState() => - _TwoFactorAuthenticationPageState(); + State createState() => _TwoFactorAuthenticationPageState(); } class _TwoFactorAuthenticationPageState extends TbPageState { final _twoFactorAuthFormKey = GlobalKey(); - ValueNotifier _selectedProvider = - ValueNotifier(null); + final _selectedProvider = ValueNotifier(null); TwoFaProviderType? _prevProvider; int? _minVerificationPeriod; - List _allowProviders = []; - ValueNotifier _disableSendButton = ValueNotifier(false); - ValueNotifier _showResendAction = ValueNotifier(false); - ValueNotifier _hideResendButton = ValueNotifier(true); + final _allowProviders = []; + final _disableSendButton = ValueNotifier(false); + final _showResendAction = ValueNotifier(false); + final _hideResendButton = ValueNotifier(true); Timer? _timer; Timer? _tooManyRequestsTimer; - ValueNotifier _countDownTime = ValueNotifier(0); + final _countDownTime = ValueNotifier(0); @override void initState() { super.initState(); var providersInfo = tbContext.twoFactorAuthProviders; - TwoFaProviderType.values.forEach((provider) { - var providerConfig = - providersInfo!.firstWhereOrNull((config) => config.type == provider); + + for (final provider in TwoFaProviderType.values) { + final providerConfig = providersInfo!.firstWhereOrNull( + (config) => config.type == provider, + ); + if (providerConfig != null) { if (providerConfig.isDefault) { _minVerificationPeriod = providerConfig.minVerificationCodeSendPeriod ?? 30; _selectedProvider.value = providerConfig.type; } + _allowProviders.add(providerConfig.type); } - }); - if (this._selectedProvider.value != TwoFaProviderType.TOTP) { + } + + if (_selectedProvider.value != TwoFaProviderType.TOTP) { _sendCode(); _showResendAction.value = true; } - _timer = Timer.periodic(Duration(seconds: 1), (timer) { + + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { _updatedTime(); }); } @@ -118,227 +130,304 @@ class _TwoFactorAuthenticationPageState Widget build(BuildContext context) { // ignore: deprecated_member_use return WillPopScope( - onWillPop: () async { - return await _goBack(); - }, - child: Scaffold( - backgroundColor: Colors.white, - resizeToAvoidBottomInset: false, - body: Stack(children: [ - LoginPageBackground(), - SizedBox.expand( - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: TbAppBar( - tbContext, - title: Text('${S.of(context).verifyYourIdentity}'), - ), - body: Stack(children: [ + onWillPop: () async { + return await _goBack(); + }, + child: Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + const LoginPageBackground(), + SizedBox.expand( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: TbAppBar( + tbContext, + title: Text(S.of(context).verifyYourIdentity), + ), + body: Stack( + children: [ SizedBox.expand( - child: Padding( - padding: EdgeInsets.all(24), - child: ValueListenableBuilder( - valueListenable: _selectedProvider, - builder: (context, providerType, _widget) { - if (providerType == null) { - var children = [ - Padding( - padding: EdgeInsets.only(bottom: 16), + child: Padding( + padding: const EdgeInsets.all(24), + child: ValueListenableBuilder( + valueListenable: _selectedProvider, + builder: (context, providerType, child) { + if (providerType == null) { + final children = [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + S.of(context).selectWayToVerify, + style: const TextStyle( + color: Colors.black87, + fontSize: 16, + height: 24 / 16, + ), + ), + ), + ]; + + for (final type in _allowProviders) { + var providerData = + twoFactorAuthProvidersLoginData[type]!; + Widget? icon; + var iconData = MdiIcons.fromString( + providerData.icon, + ); + if (iconData != null) { + icon = Icon( + iconData, + size: 24, + color: Theme.of(context).primaryColor, + ); + } else { + icon = Icon( + Icons.login, + size: 24, + color: Theme.of(context).primaryColor, + ); + } + children.add( + Container( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + ), + onPressed: () async => + await _selectProvider(type), + icon: icon, + label: Text( + providerData.nameFunction(context), + ), + ), + ), + ); + } + + return ListView( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + children: children, + ); + } else { + final providerConfig = tbContext + .twoFactorAuthProviders + ?.firstWhereOrNull( + (config) => config.type == providerType, + ); + if (providerConfig == null) { + return const SizedBox.shrink(); + } + + final providerDescription = + twoFactorAuthProvidersLoginData[providerType]! + .descFunction; + + return FormBuilder( + key: _twoFactorAuthFormKey, + autovalidateMode: AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + Text( + providerDescription( + context, + providerConfig.contact, + ), + textAlign: TextAlign.start, + style: const TextStyle( + color: Color(0xFF7F7F7F), + fontSize: 14, + height: 24 / 14, + ), + ), + const SizedBox(height: 16), + _buildVerificationCodeField( + context, + providerType, + ), + const Spacer(), + ValueListenableBuilder( + valueListenable: _disableSendButton, + builder: ( + context, + disableSendButton, + child, + ) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + onPressed: disableSendButton + ? null + : () => _sendVerificationCode( + context, + ), child: Text( - '${S.of(context).selectWayToVerify}', - style: TextStyle( - color: Colors.black87, - fontSize: 16, - height: 24 / 16))) - ]; - _allowProviders.forEach((type) { - var providerData = - twoFactorAuthProvidersLoginData[ - type]!; - Widget? icon; - var iconData = MdiIcons.fromString( - providerData.icon); - if (iconData != null) { - icon = Icon(iconData, - size: 24, - color: - Theme.of(context).primaryColor); - } else { - icon = Icon(Icons.login, - size: 24, - color: - Theme.of(context).primaryColor); - } - children.add(Container( - padding: - EdgeInsets.symmetric(vertical: 8), - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: EdgeInsets.all(16), - alignment: - Alignment.centerLeft), - onPressed: () async => - await _selectProvider(type), - icon: icon, - label: Text(providerData - .nameFunction(context))))); - }); - return ListView( - padding: - EdgeInsets.symmetric(vertical: 8), - children: children, - ); - } else { - var providerConfig = tbContext - .twoFactorAuthProviders - ?.firstWhereOrNull((config) => - config.type == providerType); - if (providerConfig == null) { - return SizedBox.shrink(); - } - var providerDescription = - twoFactorAuthProvidersLoginData[ - providerType]! - .descFunction; - return FormBuilder( - key: _twoFactorAuthFormKey, - autovalidateMode: - AutovalidateMode.disabled, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 16), - Text( - providerDescription(context, - providerConfig.contact), - textAlign: TextAlign.start, - style: TextStyle( - color: Color(0xFF7F7F7F), - fontSize: 14, - height: 24 / 14), + S.of(context).continueText, + ), + ); + }, + ), + const SizedBox(height: 16), + SizedBox( + height: 49, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + ValueListenableBuilder( + valueListenable: _showResendAction, + builder: ( + context, + showResendActionValue, + child, + ) { + if (showResendActionValue) { + return Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .stretch, + children: [ + ValueListenableBuilder< + int>( + valueListenable: + _countDownTime, + builder: ( + context, + countDown, + child, + ) { + if (countDown > 0) { + return Padding( + padding: + const EdgeInsets + .symmetric( + vertical: 12, + ), + child: Text( + S + .of(context) + .resendCodeWait( + countDown, + ), + textAlign: + TextAlign + .center, + style: + const TextStyle( + color: Color( + 0xFF7F7F7F, + ), + fontSize: 12, + height: + 24 / 12, + ), + ), + ); + } else { + return const SizedBox + .shrink(); + } + }, + ), + ValueListenableBuilder< + bool>( + valueListenable: + _hideResendButton, + builder: ( + context, + hideResendButton, + child, + ) { + if (!hideResendButton) { + return TextButton( + style: + ElevatedButton + .styleFrom( + padding: + const EdgeInsets + .symmetric( + vertical: 16, + ), + ), + onPressed: () { + _sendCode(); + }, + child: Text( + S + .of(context) + .resendCode, + ), + ); + } else { + return const SizedBox + .shrink(); + } + }, + ), + ], + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + if (_allowProviders.length > 1) + Expanded( + child: TextButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets + .symmetric( + vertical: 16, + ), + ), + onPressed: () async { + await _selectProvider( + null, + ); + }, + child: Text( + S.of(context).tryAnotherWay, + ), ), - SizedBox(height: 16), - _buildVerificationCodeField( - context, providerType), - Spacer(), - ValueListenableBuilder( - valueListenable: - _disableSendButton, - builder: (context, - disableSendButton, - _widget) { - return ElevatedButton( - child: Text( - '${S.of(context).continueText}'), - style: ElevatedButton - .styleFrom( - padding: EdgeInsets - .symmetric( - vertical: - 16)), - onPressed: disableSendButton - ? null - : () => - _sendVerificationCode( - context)); - }), - SizedBox(height: 16), - SizedBox( - height: 49, - child: Row( - mainAxisSize: - MainAxisSize.max, - children: [ - ValueListenableBuilder< - bool>( - valueListenable: - _showResendAction, - builder: (context, - showResendActionValue, - _widget) { - if (showResendActionValue) { - return Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - ValueListenableBuilder< - int>( - valueListenable: - _countDownTime, - builder: (context, - countDown, - _widget) { - if (countDown > - 0) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: Text( - S.of(context).resendCodeWait(countDown), - textAlign: TextAlign.center, - style: TextStyle(color: Color(0xFF7F7F7F), fontSize: 12, height: 24 / 12), - ), - ); - } else { - return SizedBox.shrink(); - } - }), - ValueListenableBuilder< - bool>( - valueListenable: - _hideResendButton, - builder: (context, - hideResendButton, - _widget) { - if (!hideResendButton) { - return TextButton( - child: Text('${S.of(context).resendCode}'), - style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)), - onPressed: () { - _sendCode(); - }, - ); - } else { - return SizedBox.shrink(); - } - }) - ])); - } else { - return SizedBox - .shrink(); - } - }), - if (_allowProviders - .length > - 1) - Expanded( - child: TextButton( - child: Text( - '${S.of(context).tryAnotherWay}'), - style: ElevatedButton.styleFrom( - padding: EdgeInsets - .symmetric( - vertical: - 16)), - onPressed: - () async { - await _selectProvider( - null); - }, - )) - ])) - ])); - } - }))) - ]), + ), + ], + ), + ), + ], + ), + ); + } + }, + ), + ), + ), + ], ), - ) - ]))); + ), + ), + ], + ), + ), + ); } FormBuilderTextField _buildVerificationCodeField( - BuildContext context, TwoFaProviderType providerType) { + BuildContext context, + TwoFaProviderType providerType, + ) { int maxLengthInput = 6; TextInputType keyboardType = TextInputType.number; String pattern = '[0-9]*'; @@ -349,26 +438,33 @@ class _TwoFactorAuthenticationPageState keyboardType = TextInputType.text; } - List> validators = [ + final validators = >[ FormBuilderValidators.required( - errorText: '${S.of(context).verificationCodeInvalid}'), - FormBuilderValidators.equalLength(maxLengthInput, - errorText: '${S.of(context).verificationCodeInvalid}'), - FormBuilderValidators.match(pattern, - errorText: '${S.of(context).verificationCodeInvalid}') + errorText: S.of(context).verificationCodeInvalid, + ), + FormBuilderValidators.equalLength( + maxLengthInput, + errorText: S.of(context).verificationCodeInvalid, + ), + FormBuilderValidators.match( + pattern, + errorText: S.of(context).verificationCodeInvalid, + ), ]; - var providerFormData = twoFactorAuthProvidersLoginData[providerType]!; + final providerFormData = twoFactorAuthProvidersLoginData[providerType]!; return FormBuilderTextField( - name: 'verificationCode', - autofocus: true, - maxLength: maxLengthInput, - keyboardType: keyboardType, - validator: FormBuilderValidators.compose(validators), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: providerFormData.placeholderFunction(context))); + name: 'verificationCode', + autofocus: true, + maxLength: maxLengthInput, + keyboardType: keyboardType, + validator: FormBuilderValidators.compose(validators), + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: providerFormData.placeholderFunction(context), + ), + ); } Future _sendVerificationCode(BuildContext context) async { @@ -378,21 +474,27 @@ class _TwoFactorAuthenticationPageState String verificationCode = formValue['verificationCode']; try { await tbClient.checkTwoFaVerificationCode( - _selectedProvider.value!, verificationCode, - requestConfig: RequestConfig(ignoreErrors: true)); + _selectedProvider.value!, + verificationCode, + requestConfig: RequestConfig(ignoreErrors: true), + ); } catch (e) { if (e is ThingsboardError) { if (e.status == 400) { - _twoFactorAuthFormKey.currentState!.fields['verificationCode']! - .invalidate(S.of(context).verificationCodeIncorrect); + if (context.mounted) { + _twoFactorAuthFormKey.currentState!.fields['verificationCode']! + .invalidate(S.of(context).verificationCodeIncorrect); + } } else if (e.status == 429) { - _twoFactorAuthFormKey.currentState!.fields['verificationCode']! - .invalidate(S.of(context).verificationCodeManyRequest); + if (context.mounted) { + _twoFactorAuthFormKey.currentState!.fields['verificationCode']! + .invalidate(S.of(context).verificationCodeManyRequest); + } _disableSendButton.value = true; if (_tooManyRequestsTimer != null) { _tooManyRequestsTimer!.cancel(); } - _tooManyRequestsTimer = Timer(Duration(seconds: 5), () { + _tooManyRequestsTimer = Timer(const Duration(seconds: 5), () { _twoFactorAuthFormKey.currentState!.fields['verificationCode']! .validate(); _disableSendButton.value = false; @@ -431,9 +533,10 @@ class _TwoFactorAuthenticationPageState try { await tbContext.tbClient .getTwoFactorAuthService() - .requestTwoFaVerificationCode(_selectedProvider.value!, - requestConfig: RequestConfig(ignoreErrors: true)); - } catch (e) { + .requestTwoFaVerificationCode( + _selectedProvider.value!, + requestConfig: RequestConfig(ignoreErrors: true), + ); } finally { _countDownTime.value = _minVerificationPeriod!; } diff --git a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart index 72364335..9c90f4e3 100644 --- a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart +++ b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class EndpointNameWidget extends StatelessWidget { - const EndpointNameWidget({required this.endpoint}); + const EndpointNameWidget({required this.endpoint, super.key}); final String endpoint; @@ -11,7 +11,7 @@ class EndpointNameWidget extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), border: Border.all( - color: Color(0xFF305680), + color: const Color(0xFF305680), ), ), padding: const EdgeInsets.all(5), @@ -21,7 +21,7 @@ class EndpointNameWidget extends StatelessWidget { style: Theme.of(context) .textTheme .labelSmall - ?.copyWith(color: Color(0xFF305680)), + ?.copyWith(color: const Color(0xFF305680)), ), ), ); diff --git a/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart b/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart index 4ee5d30d..65b5a6b8 100644 --- a/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart +++ b/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; class NoAuthLoadingWidget extends StatelessWidget { - const NoAuthLoadingWidget(); + const NoAuthLoadingWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/core/auth/oauth2/tb_oauth2_client.dart b/lib/core/auth/oauth2/tb_oauth2_client.dart index d9435251..b6766b8b 100644 --- a/lib/core/auth/oauth2/tb_oauth2_client.dart +++ b/lib/core/auth/oauth2/tb_oauth2_client.dart @@ -27,10 +27,10 @@ class TbOAuth2Client { final TbContext _tbContext; final AppSecretProvider _appSecretProvider; - TbOAuth2Client( - {required TbContext tbContext, - required AppSecretProvider appSecretProvider}) - : _tbContext = tbContext, + TbOAuth2Client({ + required TbContext tbContext, + required AppSecretProvider appSecretProvider, + }) : _tbContext = tbContext, _appSecretProvider = appSecretProvider; Future authenticate(String oauth2Url) async { @@ -39,13 +39,16 @@ class TbOAuth2Client { final jwt = JWT( { 'callbackUrlScheme': - ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme + ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, }, issuer: pkgName, ); final key = SecretKey(appSecret); - final appToken = jwt.sign(key, - algorithm: _HMACBase64Algorithm.HS512, expiresIn: Duration(minutes: 2)); + final appToken = jwt.sign( + key, + algorithm: _HMACBase64Algorithm.hs512, + expiresIn: const Duration(minutes: 2), + ); var url = Uri.parse(await getIt().getEndpoint() + oauth2Url); final params = Map.from(url.queryParameters); @@ -53,10 +56,11 @@ class TbOAuth2Client { params['appToken'] = appToken; url = url.replace(queryParameters: params); final result = await TbWebAuth.authenticate( - url: url.toString(), - callbackUrlScheme: - ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, - saveHistory: false); + url: url.toString(), + callbackUrlScheme: + ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, + saveHistory: false, + ); final resultUri = Uri.parse(result); final error = resultUri.queryParameters['error']; if (error != null) { @@ -68,14 +72,15 @@ class TbOAuth2Client { return TbOAuth2AuthenticateResult.success(accessToken, refreshToken); } else { return TbOAuth2AuthenticateResult.failed( - 'No authentication credentials in response.'); + 'No authentication credentials in response.', + ); } } } } class _HMACBase64Algorithm extends JWTAlgorithm { - static const HS512 = _HMACBase64Algorithm('HS512'); + static const hs512 = _HMACBase64Algorithm('HS512'); final String _name; diff --git a/lib/core/auth/web/tb_web_auth.dart b/lib/core/auth/web/tb_web_auth.dart index 6013f3b9..580f20b7 100644 --- a/lib/core/auth/web/tb_web_auth.dart +++ b/lib/core/auth/web/tb_web_auth.dart @@ -17,19 +17,21 @@ class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver { } class TbWebAuth { - static const MethodChannel _channel = const MethodChannel('tb_web_auth'); + static const MethodChannel _channel = MethodChannel('tb_web_auth'); static final _OnAppLifecycleResumeObserver _resumedObserver = _OnAppLifecycleResumeObserver(() { _cleanUpDanglingCalls(); }); - static Future authenticate( - {required String url, - required String callbackUrlScheme, - bool? saveHistory}) async { + static Future authenticate({ + required String url, + required String callbackUrlScheme, + bool? saveHistory, + }) async { WidgetsBinding.instance.removeObserver( - _resumedObserver); // safety measure so we never add this observer twice + _resumedObserver, + ); // safety measure so we never add this observer twice WidgetsBinding.instance.addObserver(_resumedObserver); return await _channel.invokeMethod('authenticate', { 'url': url, diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index f5f55d2e..d4e8b1ad 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -24,15 +24,21 @@ import 'package:universal_platform/universal_platform.dart'; enum NotificationType { info, warn, success, error } -typedef OpenDashboardCallback = void Function(String dashboardId, - {String? dashboardTitle, String? state, bool? hideToolbar}); +typedef OpenDashboardCallback = void Function( + String dashboardId, { + String? dashboardTitle, + String? state, + bool? hideToolbar, +}); abstract class TbMainDashboardHolder { - Future navigateToDashboard(String dashboardId, - {String? dashboardTitle, - String? state, - bool? hideToolbar, - bool animate = true}); + Future navigateToDashboard( + String dashboardId, { + String? dashboardTitle, + String? state, + bool? hideToolbar, + bool animate = true, + }); Future openMain({bool animate}); @@ -59,7 +65,7 @@ class TbContext implements PopEntry { HomeDashboardInfo? homeDashboard; final _isLoadingNotifier = ValueNotifier(false); final _log = TbLogger(); - late final _widgetActionHandler; + late final WidgetActionHandler _widgetActionHandler; late final AndroidDeviceInfo? _androidInfo; late final IosDeviceInfo? _iosInfo; late final String packageName; @@ -68,8 +74,10 @@ class TbContext implements PopEntry { StreamSubscription? _appLinkStreamSubscription; late bool _handleRootState; + @override final ValueNotifier canPopNotifier = ValueNotifier(false); + @override PopInvokedCallback get onPopInvoked => onPopInvokedImpl; GlobalKey messengerKey = @@ -185,7 +193,7 @@ class TbContext implements PopEntry { var message = e is ThingsboardError ? (e.message ?? 'Unknown error.') : 'Unknown error.'; - message = 'Fatal application error occured:\n' + message + '.'; + message = 'Fatal application error occured:\n$message.'; await alert(title: 'Fatal error', message: message, ok: 'Close'); logout(); } @@ -211,23 +219,26 @@ class TbContext implements PopEntry { showNotification(message, NotificationType.success, duration: duration); } - void showNotification(String message, NotificationType type, - {Duration? duration}) { + void showNotification( + String message, + NotificationType type, { + Duration? duration, + }) { duration ??= const Duration(days: 1); Color backgroundColor; - var textColor = Color(0xFFFFFFFF); + var textColor = const Color(0xFFFFFFFF); switch (type) { case NotificationType.info: - backgroundColor = Color(0xFF323232); + backgroundColor = const Color(0xFF323232); break; case NotificationType.warn: - backgroundColor = Color(0xFFdc6d1b); + backgroundColor = const Color(0xFFdc6d1b); break; case NotificationType.success: - backgroundColor = Color(0xFF008000); + backgroundColor = const Color(0xFF008000); break; case NotificationType.error: - backgroundColor = Color(0xFF800000); + backgroundColor = const Color(0xFF800000); break; } final snackBar = SnackBar( @@ -267,7 +278,8 @@ class TbContext implements PopEntry { Future onUserLoaded({VoidCallback? onDone}) async { try { log.debug( - 'TbContext.onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}'); + 'TbContext.onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}', + ); isUserLoaded = true; if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { log.debug('authUser: ${tbClient.getAuthUser()}'); @@ -337,7 +349,7 @@ class TbContext implements PopEntry { replace: true, clearStack: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), ); } } @@ -351,13 +363,14 @@ class TbContext implements PopEntry { log.error('TbContext:getInitialUri() exception $e'); } - if (_appLinkStreamSubscription == null) { - _appLinkStreamSubscription = linkStream.listen((link) { + _appLinkStreamSubscription ??= linkStream.listen( + (link) { navigateByAppLink(link); - }, onError: (err) { + }, + onError: (err) { log.error('linkStream.listen $err'); - }); - } + }, + ); } } @@ -406,7 +419,8 @@ class TbContext implements PopEntry { Future updateRouteState() async { log.debug( - 'TbContext:updateRouteState() ${currentState != null && currentState!.mounted}'); + 'TbContext:updateRouteState() ${currentState != null && currentState!.mounted}', + ); if (currentState != null && currentState!.mounted) { if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { final defaultDashboardId = _defaultDashboardId(); @@ -432,7 +446,7 @@ class TbContext implements PopEntry { '/home', replace: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), ); } } else { @@ -441,7 +455,7 @@ class TbContext implements PopEntry { replace: true, clearStack: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), ); } } @@ -544,21 +558,25 @@ class TbContext implements PopEntry { } } - Future navigateToDashboard(String dashboardId, - {String? dashboardTitle, - String? state, - bool? hideToolbar, - bool animate = true}) async { - await _mainDashboardHolder?.navigateToDashboard(dashboardId, - dashboardTitle: dashboardTitle, - state: state, - hideToolbar: hideToolbar, - animate: animate); + Future navigateToDashboard( + String dashboardId, { + String? dashboardTitle, + String? state, + bool? hideToolbar, + bool animate = true, + }) async { + await _mainDashboardHolder?.navigateToDashboard( + dashboardId, + dashboardTitle: dashboardTitle, + state: state, + hideToolbar: hideToolbar, + animate: animate, + ); } Future showFullScreenDialog(Widget dialog) { return Navigator.of(currentState!.context).push( - new MaterialPageRoute( + MaterialPageRoute( builder: (BuildContext context) { return dialog; }, @@ -619,35 +637,43 @@ class TbContext implements PopEntry { return false; } - Future alert( - {required String title, required String message, String ok = 'Ok'}) { + Future alert({ + required String title, + required String message, + String ok = 'Ok', + }) { return showDialog( - context: currentState!.context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(message), - actions: [ - TextButton(onPressed: () => pop(null, context), child: Text(ok)) - ], - )); - } - - Future confirm( - {required String title, - required String message, - String cancel = 'Cancel', - String ok = 'Ok'}) { + context: currentState!.context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton(onPressed: () => pop(null, context), child: Text(ok)), + ], + ), + ); + } + + Future confirm({ + required String title, + required String message, + String cancel = 'Cancel', + String ok = 'Ok', + }) { return showDialog( - context: currentState!.context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(message), - actions: [ - TextButton( - onPressed: () => pop(false, context), child: Text(cancel)), - TextButton(onPressed: () => pop(true, context), child: Text(ok)) - ], - )); + context: currentState!.context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => pop(false, context), + child: Text(cancel), + ), + TextButton(onPressed: () => pop(true, context), child: Text(ok)), + ], + ), + ); } } @@ -698,8 +724,11 @@ mixin HasTbContext { await _tbContext.init(); } - Future navigateTo(String path, - {bool replace = false, bool clearStack = false}) => + Future navigateTo( + String path, { + bool replace = false, + bool clearStack = false, + }) => _tbContext.navigateTo(path, replace: replace, clearStack: clearStack); void pop([T? result, BuildContext? context]) => @@ -708,24 +737,33 @@ mixin HasTbContext { Future maybePop([T? result]) => _tbContext.maybePop(result); - Future navigateToDashboard(String dashboardId, - {String? dashboardTitle, - String? state, - bool? hideToolbar, - bool animate = true}) => - _tbContext.navigateToDashboard(dashboardId, - dashboardTitle: dashboardTitle, - state: state, - hideToolbar: hideToolbar, - animate: animate); - - Future confirm( - {required String title, - required String message, - String cancel = 'Cancel', - String ok = 'Ok'}) => + Future navigateToDashboard( + String dashboardId, { + String? dashboardTitle, + String? state, + bool? hideToolbar, + bool animate = true, + }) => + _tbContext.navigateToDashboard( + dashboardId, + dashboardTitle: dashboardTitle, + state: state, + hideToolbar: hideToolbar, + animate: animate, + ); + + Future confirm({ + required String title, + required String message, + String cancel = 'Cancel', + String ok = 'Ok', + }) => _tbContext.confirm( - title: title, message: message, cancel: cancel, ok: ok); + title: title, + message: message, + cancel: cancel, + ok: ok, + ); void hideNotification() => _tbContext.hideNotification(); diff --git a/lib/core/context/tb_context_widget.dart b/lib/core/context/tb_context_widget.dart index 5155f5b7..1a0dc3c8 100644 --- a/lib/core/context/tb_context_widget.dart +++ b/lib/core/context/tb_context_widget.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; abstract class RefreshableWidget extends Widget { + const RefreshableWidget({super.key}); + refresh(); } @@ -84,10 +86,11 @@ abstract class TbPageState extends TbContextState class TextContextWidget extends TbContextWidget { final String text; - TextContextWidget(TbContext tbContext, this.text) : super(tbContext); + TextContextWidget(TbContext tbContext, this.text, {super.key}) + : super(tbContext); @override - _TextContextWidgetState createState() => _TextContextWidgetState(); + State createState() => _TextContextWidgetState(); } class _TextContextWidgetState extends TbContextState { diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart index 489871f2..83441cf3 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -9,7 +9,7 @@ import 'package:thingsboard_app/generated/l10n.dart'; import 'package:thingsboard_app/utils/utils.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -const Map entityTypeTranslations = { +const entityTypeTranslations = { EntityType.TENANT: 'Tenant', EntityType.TENANT_PROFILE: 'Tenant profile', EntityType.CUSTOMER: 'Customer', @@ -27,15 +27,18 @@ const Map entityTypeTranslations = { EntityType.WIDGET_TYPE: 'Widget type', EntityType.API_USAGE_STATE: 'Api Usage State', EntityType.TB_RESOURCE: 'Resource', - EntityType.OTA_PACKAGE: 'OTA package' + EntityType.OTA_PACKAGE: 'OTA package', }; typedef EntityTapFunction = Function(T entity); typedef EntityCardWidgetBuilder = Widget Function( - BuildContext context, T entity); + BuildContext context, + T entity, +); class EntityCardSettings { bool dropShadow; + EntityCardSettings({this.dropShadow = true}); } @@ -55,15 +58,15 @@ mixin EntitiesBase on HasTbContext { Key? getKey(T entity) => null; Widget buildEntityListCard(BuildContext context, T entity) { - return Text('${S.of(context).notImplemented}'); + return Text(S.of(context).notImplemented); } Widget buildEntityListWidgetCard(BuildContext context, T entity) { - return Text('${S.of(context).notImplemented}'); + return Text(S.of(context).notImplemented); } Widget buildEntityGridCard(BuildContext context, T entity) { - return Text('${S.of(context).notImplemented}'); + return Text(S.of(context).notImplemented); } double? gridChildAspectRatio() => null; @@ -80,61 +83,77 @@ mixin ContactBasedBase on EntitiesBase { Widget buildEntityListCard(BuildContext context, T contact) { var address = Utils.contactToShortAddress(contact); return Container( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: Row( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text('${contact.getName()}', - style: TextStyle( - color: Color(0xFF282828), - fontSize: 14, - fontWeight: FontWeight.w500, - height: 20 / 14))), - Text( - entityDateFormat.format( - DateTime.fromMillisecondsSinceEpoch( - contact.createdTime!)), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)) - ]), - SizedBox(height: 4), - if (contact.email != null) - Text(contact.email!, - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)), - if (contact.email == null) SizedBox(height: 16), - if (address != null) SizedBox(height: 4), - if (address != null) - Text(address, - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)), - ], - )), - SizedBox(width: 16), - Icon(Icons.chevron_right, color: Color(0xFFACACAC)), - SizedBox(width: 8) + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text( + contact.getName(), + style: const TextStyle( + color: Color(0xFF282828), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 20 / 14, + ), + ), + ), + Text( + entityDateFormat.format( + DateTime.fromMillisecondsSinceEpoch( + contact.createdTime!, + ), + ), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 4), + if (contact.email != null) + Text( + contact.email!, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + if (contact.email == null) const SizedBox(height: 16), + if (address != null) const SizedBox(height: 4), + if (address != null) + Text( + address, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + const Icon(Icons.chevron_right, color: Color(0xFFACACAC)), + const SizedBox(width: 8), ], ), ); @@ -155,8 +174,17 @@ class PageKeyValue

{ class PageLinkController extends PageKeyController { PageLinkController({int pageSize = 20, String? searchText}) - : super(PageLink( - pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC))); + : super( + PageLink( + pageSize, + 0, + searchText, + SortOrder( + 'createdTime', + Direction.DESC, + ), + ), + ); @override PageLink nextPageKey(PageLink pageKey) => pageKey.nextPageLink(); @@ -170,8 +198,14 @@ class PageLinkController extends PageKeyController { class TimePageLinkController extends PageKeyController { TimePageLinkController({int pageSize = 20, String? searchText}) - : super(TimePageLink( - pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC))); + : super( + TimePageLink( + pageSize, + 0, + searchText, + SortOrder('createdTime', Direction.DESC), + ), + ); @override TimePageLink nextPageKey(TimePageLink pageKey) => pageKey.nextPageLink(); @@ -188,15 +222,23 @@ abstract class BaseEntitiesWidget extends TbContextWidget final bool searchMode; final PageKeyController

pageKeyController; - BaseEntitiesWidget(TbContext tbContext, this.pageKeyController, - {this.searchMode = false}) - : super(tbContext); + BaseEntitiesWidget( + TbContext tbContext, + this.pageKeyController, { + super.key, + this.searchMode = false, + }) : super(tbContext); @override Widget? buildHeading(BuildContext context) => searchMode - ? Text('Search results', + ? const Text( + 'Search results', style: TextStyle( - color: Color(0xFFAFAFAF), fontSize: 16, height: 24 / 16)) + color: Color(0xFFAFAFAF), + fontSize: 16, + height: 24 / 16, + ), + ) : null; } @@ -243,14 +285,13 @@ abstract class BaseEntitiesState } Future _refresh() { - if (_refreshCompleter == null) { - _refreshCompleter = Completer(); - } + _refreshCompleter ??= Completer(); if (_dataLoading) { _scheduleRefresh = true; } else { _refreshPagingController(); } + return _refreshCompleter!.future; } @@ -305,40 +346,47 @@ abstract class BaseEntitiesState @override Widget build(BuildContext context) { return RefreshIndicator( - onRefresh: () => Future.wait([widget.onRefresh(), _refresh()]), - child: pagedViewBuilder(context)); + onRefresh: () => Future.wait([widget.onRefresh(), _refresh()]), + child: pagedViewBuilder(context), + ); } Widget pagedViewBuilder(BuildContext context); Widget firstPageProgressIndicatorBuilder(BuildContext context) { - return Stack(children: [ - Positioned( - top: 20, - left: 0, - right: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [RefreshProgressIndicator()], + return const Stack( + children: [ + Positioned( + top: 20, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RefreshProgressIndicator(), + ], + ), ), - ) - ]); + ], + ); } Widget newPageProgressIndicatorBuilder(BuildContext context) { - return Padding( - padding: const EdgeInsets.only( + return const Padding( + padding: EdgeInsets.only( top: 16, bottom: 16, ), - child: Center(child: RefreshProgressIndicator()), + child: Center( + child: RefreshProgressIndicator(), + ), ); } Widget noItemsFoundIndicatorBuilder(BuildContext context) { return FirstPageExceptionIndicator( title: widget.noItemsFoundText, - message: '${S.of(context).listIsEmptyText}', + message: S.of(context).listIsEmptyText, onTryAgain: widget.searchMode ? null : () => pagingController.refresh(), ); } @@ -393,8 +441,8 @@ class FirstPageExceptionIndicator extends StatelessWidget { color: Colors.white, ), label: Text( - '${S.of(context).tryAgain}', - style: TextStyle( + S.of(context).tryAgain, + style: const TextStyle( fontSize: 16, color: Colors.white, ), diff --git a/lib/core/entity/entities_grid.dart b/lib/core/entity/entities_grid.dart index d0808e99..0494e8b2 100644 --- a/lib/core/entity/entities_grid.dart +++ b/lib/core/entity/entities_grid.dart @@ -7,7 +7,7 @@ import 'entity_grid_card.dart'; mixin EntitiesGridStateBase on StatefulWidget { @override - _EntitiesGridState createState() => _EntitiesGridState(); + State createState() => _EntitiesGridState(); } class _EntitiesGridState extends BaseEntitiesState { @@ -30,7 +30,7 @@ class _EntitiesGridState extends BaseEntitiesState { slivers.add( SliverPadding( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), sliver: PagedSliverGrid( showNewPageProgressIndicatorAsGridChild: false, showNewPageErrorIndicatorAsGridChild: false, diff --git a/lib/core/entity/entities_list.dart b/lib/core/entity/entities_list.dart index 0590716a..91267e94 100644 --- a/lib/core/entity/entities_list.dart +++ b/lib/core/entity/entities_list.dart @@ -7,7 +7,7 @@ import 'entity_list_card.dart'; mixin EntitiesListStateBase on StatefulWidget { @override - _EntitiesListState createState() => _EntitiesListState(); + State createState() => _EntitiesListState(); } class _EntitiesListState extends BaseEntitiesState { @@ -18,27 +18,35 @@ class _EntitiesListState extends BaseEntitiesState { var heading = widget.buildHeading(context); List slivers = []; if (heading != null) { - slivers.add(SliverPadding( - padding: EdgeInsets.fromLTRB(16, 16, 16, 0), - sliver: SliverToBoxAdapter(child: heading))); + slivers.add( + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + sliver: SliverToBoxAdapter(child: heading), + ), + ); } - slivers.add(SliverPadding( - padding: EdgeInsets.all(16), + slivers.add( + SliverPadding( + padding: const EdgeInsets.all(16), sliver: PagedSliverList.separated( - pagingController: pagingController, - separatorBuilder: (context, index) => SizedBox(height: 8), - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => EntityListCard( - item, - key: widget.getKey(item), - entityCardWidgetBuilder: widget.buildEntityListCard, - onEntityTap: widget.onEntityTap, - ), - firstPageProgressIndicatorBuilder: - firstPageProgressIndicatorBuilder, - newPageProgressIndicatorBuilder: - newPageProgressIndicatorBuilder, - noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder)))); + pagingController: pagingController, + separatorBuilder: (context, index) => const SizedBox(height: 8), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => EntityListCard( + item, + key: widget.getKey(item), + entityCardWidgetBuilder: widget.buildEntityListCard, + onEntityTap: widget.onEntityTap, + ), + firstPageProgressIndicatorBuilder: + firstPageProgressIndicatorBuilder, + newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder, + noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder, + ), + ), + ), + ); + return CustomScrollView(slivers: slivers); } } diff --git a/lib/core/entity/entities_list_widget.dart b/lib/core/entity/entities_list_widget.dart index 272f1f3a..29153f30 100644 --- a/lib/core/entity/entities_list_widget.dart +++ b/lib/core/entity/entities_list_widget.dart @@ -10,15 +10,17 @@ import 'package:thingsboard_client/thingsboard_client.dart'; import 'entity_list_card.dart'; class EntitiesListWidgetController { - final List<_EntitiesListWidgetState> states = []; + final states = <_EntitiesListWidgetState>[]; void _registerEntitiesWidgetState( - _EntitiesListWidgetState entitiesListWidgetState) { + _EntitiesListWidgetState entitiesListWidgetState, + ) { states.add(entitiesListWidgetState); } void _unregisterEntitiesWidgetState( - _EntitiesListWidgetState entitiesListWidgetState) { + _EntitiesListWidgetState entitiesListWidgetState, + ) { states.remove(entitiesListWidgetState); } @@ -33,9 +35,11 @@ class EntitiesListWidgetController { abstract class EntitiesListPageLinkWidget extends EntitiesListWidget { - EntitiesListPageLinkWidget(TbContext tbContext, - {EntitiesListWidgetController? controller}) - : super(tbContext, controller: controller); + EntitiesListPageLinkWidget( + TbContext tbContext, { + EntitiesListWidgetController? controller, + super.key, + }) : super(tbContext, controller: controller); @override PageKeyController createPageKeyController() => @@ -46,14 +50,15 @@ abstract class EntitiesListWidget extends TbContextWidget with EntitiesBase { final EntitiesListWidgetController? _controller; - EntitiesListWidget(TbContext tbContext, - {EntitiesListWidgetController? controller}) - : _controller = controller, + EntitiesListWidget( + TbContext tbContext, { + super.key, + EntitiesListWidgetController? controller, + }) : _controller = controller, super(tbContext); @override - _EntitiesListWidgetState createState() => - _EntitiesListWidgetState(_controller); + State createState() => _EntitiesListWidgetState(); PageKeyController

createPageKeyController(); @@ -62,31 +67,23 @@ abstract class EntitiesListWidget extends TbContextWidget class _EntitiesListWidgetState extends TbContextState> { - final EntitiesListWidgetController? _controller; - late final PageKeyController

_pageKeyController; final StreamController?> _entitiesStreamController = StreamController.broadcast(); - _EntitiesListWidgetState(EntitiesListWidgetController? controller) - : _controller = controller; - @override void initState() { super.initState(); _pageKeyController = widget.createPageKeyController(); - if (_controller != null) { - _controller._registerEntitiesWidgetState(this); - } + widget._controller?._registerEntitiesWidgetState(this); + _refresh(); } @override void dispose() { - if (_controller != null) { - _controller._unregisterEntitiesWidgetState(this); - } + widget._controller?._unregisterEntitiesWidgetState(this); _pageKeyController.dispose(); _entitiesStreamController.close(); super.dispose(); @@ -102,117 +99,142 @@ class _EntitiesListWidgetState @override Widget build(BuildContext context) { return Container( - height: 120, - margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: Card( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( + height: 120, + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(25), + blurRadius: 10.0, + offset: const Offset(0, 4), + ), + BoxShadow( + color: Colors.black.withAlpha(18), + blurRadius: 30.0, + offset: const Offset(0, 10), + ), + ], + ), + child: Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Container( + height: 24, + margin: const EdgeInsets.only(bottom: 8), + child: Row( children: [ - Container( - height: 24, - margin: const EdgeInsets.only(bottom: 8), - child: Row( - children: [ - StreamBuilder?>( - stream: _entitiesStreamController.stream, - builder: (context, snapshot) { - var title = widget.title; - if (snapshot.hasData) { - var data = snapshot.data; - title += ' (${data!.totalElements})'; - } - return Text(title, - style: TextStyle( - color: Color(0xFF282828), - fontSize: 16, - fontWeight: FontWeight.normal, - height: 1.5)); - }, + StreamBuilder?>( + stream: _entitiesStreamController.stream, + builder: (context, snapshot) { + var title = widget.title; + if (snapshot.hasData) { + var data = snapshot.data; + title += ' (${data!.totalElements})'; + } + return Text( + title, + style: const TextStyle( + color: Color(0xFF282828), + fontSize: 16, + fontWeight: FontWeight.normal, + height: 1.5, ), - Spacer(), - TextButton( - onPressed: () { - widget.onViewAll(); - }, - style: TextButton.styleFrom( - padding: EdgeInsets.zero), - child: Text('View all')) - ], + ); + }, + ), + const Spacer(), + TextButton( + onPressed: () { + widget.onViewAll(); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, ), + child: const Text('View all'), ), - Container( - height: 64, - child: StreamBuilder?>( - stream: _entitiesStreamController.stream, - builder: (context, snapshot) { - if (snapshot.hasData) { - var data = snapshot.data!; - if (data.data.isEmpty) { - return _buildNoEntitiesFound(); //return Text('Loaded'); - } else { - return _buildEntitiesView(context, data.data); - } - } else { - return Center( - child: RefreshProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Theme.of(tbContext.currentState!.context) - .colorScheme - .primary), - )); - } - }), - ) ], - ))), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(25), - blurRadius: 10.0, - offset: Offset(0, 4)), - BoxShadow( - color: Colors.black.withAlpha(18), - blurRadius: 30.0, - offset: Offset(0, 10)), - ], - )); + ), + ), + SizedBox( + height: 64, + child: StreamBuilder?>( + stream: _entitiesStreamController.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final data = snapshot.data!; + if (data.data.isEmpty) { + return _buildNoEntitiesFound(); //return Text('Loaded'); + } else { + return _buildEntitiesView(context, data.data); + } + } else { + return Center( + child: RefreshProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Theme.of(tbContext.currentState!.context) + .colorScheme + .primary, + ), + ), + ); + } + }, + ), + ), + ], + ), + ), + ), + ); } Widget _buildNoEntitiesFound() { return Container( decoration: BoxDecoration( - border: Border.all( - color: Color(0xFFDEDEDE), style: BorderStyle.solid, width: 1), - borderRadius: BorderRadius.circular(4)), + border: Border.all( + color: const Color(0xFFDEDEDE), + style: BorderStyle.solid, + width: 1, + ), + borderRadius: BorderRadius.circular(4), + ), child: Center( - child: Text(widget.noItemsFoundText, - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 14, - )), + child: Text( + widget.noItemsFoundText, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 14, + ), + ), ), ); } Widget _buildEntitiesView(BuildContext context, List entities) { return FadingEdgeScrollView.fromScrollView( - gradientFractionOnStart: 0.2, - gradientFractionOnEnd: 0.2, - child: ListView( - scrollDirection: Axis.horizontal, - controller: ScrollController(), - children: entities - .map((entity) => EntityListCard(entity, - entityCardWidgetBuilder: widget.buildEntityListWidgetCard, - onEntityTap: widget.onEntityTap, - listWidgetCard: true)) - .toList())); + gradientFractionOnStart: 0.2, + gradientFractionOnEnd: 0.2, + child: ListView( + scrollDirection: Axis.horizontal, + controller: ScrollController(), + children: entities + .map( + (entity) => EntityListCard( + entity, + entityCardWidgetBuilder: widget.buildEntityListWidgetCard, + onEntityTap: widget.onEntityTap, + listWidgetCard: true, + ), + ) + .toList(), + ), + ); } } diff --git a/lib/core/entity/entity_details_page.dart b/lib/core/entity/entity_details_page.dart index a8711755..ab08d71a 100644 --- a/lib/core/entity/entity_details_page.dart +++ b/lib/core/entity/entity_details_page.dart @@ -7,10 +7,10 @@ import 'package:thingsboard_client/thingsboard_client.dart'; abstract class EntityDetailsPage extends TbPageWidget { final labelTextStyle = - TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14); + const TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14); final valueTextStyle = - TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14); + const TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14); final String _defaultTitle; final String _entityId; @@ -19,23 +19,25 @@ abstract class EntityDetailsPage extends TbPageWidget { final bool _hideAppBar; final double? _appBarElevation; - EntityDetailsPage(TbContext tbContext, - {required String defaultTitle, - required String entityId, - String? subTitle, - bool showLoadingIndicator = true, - bool hideAppBar = false, - double? appBarElevation}) - : this._defaultTitle = defaultTitle, - this._entityId = entityId, - this._subTitle = subTitle, - this._showLoadingIndicator = showLoadingIndicator, - this._hideAppBar = hideAppBar, - this._appBarElevation = appBarElevation, + EntityDetailsPage( + TbContext tbContext, { + required String defaultTitle, + required String entityId, + String? subTitle, + bool showLoadingIndicator = true, + bool hideAppBar = false, + double? appBarElevation, + super.key, + }) : _defaultTitle = defaultTitle, + _entityId = entityId, + _subTitle = subTitle, + _showLoadingIndicator = showLoadingIndicator, + _hideAppBar = hideAppBar, + _appBarElevation = appBarElevation, super(tbContext); @override - _EntityDetailsPageState createState() => _EntityDetailsPageState(); + State createState() => _EntityDetailsPageState(); Future fetchEntity(String id); @@ -80,32 +82,39 @@ class _EntityDetailsPageState elevation: widget._appBarElevation, title: ValueListenableBuilder( valueListenable: titleValue, - builder: (context, title, _widget) { + builder: (context, title, child) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text(title, - style: widget._subTitle != null - ? Theme.of(context) - .primaryTextTheme - .titleLarge! - .copyWith(fontSize: 16) - : null)), - if (widget._subTitle != null) - Text(widget._subTitle!, - style: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .titleLarge! - .color! - .withAlpha((0.38 * 255).ceil()), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)) - ]); + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text( + title, + style: widget._subTitle != null + ? Theme.of(context) + .primaryTextTheme + .titleLarge! + .copyWith(fontSize: 16) + : null, + ), + ), + if (widget._subTitle != null) + Text( + widget._subTitle!, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .titleLarge! + .color! + .withAlpha((0.38 * 255).ceil()), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + ], + ); }, ), ), @@ -117,13 +126,16 @@ class _EntityDetailsPageState if (entity != null) { return widget.buildEntityDetails(context, entity); } else { - return Center(child: Text('Requested entity does not exists.')); + return const Center( + child: Text('Requested entity does not exists.'), + ); } } else { - return Center( - child: TbProgressIndicator( - size: 50.0, - )); + return const Center( + child: TbProgressIndicator( + size: 50.0, + ), + ); } }, ), @@ -133,75 +145,83 @@ class _EntityDetailsPageState abstract class ContactBasedDetailsPage extends EntityDetailsPage { - ContactBasedDetailsPage(TbContext tbContext, - {required String defaultTitle, - required String entityId, - String? subTitle, - bool showLoadingIndicator = true, - bool hideAppBar = false, - double? appBarElevation}) - : super(tbContext, - defaultTitle: defaultTitle, - entityId: entityId, - subTitle: subTitle, - showLoadingIndicator: showLoadingIndicator, - hideAppBar: hideAppBar, - appBarElevation: appBarElevation); + ContactBasedDetailsPage( + TbContext tbContext, { + required String defaultTitle, + required String entityId, + String? subTitle, + bool showLoadingIndicator = true, + bool hideAppBar = false, + double? appBarElevation, + super.key, + }) : super( + tbContext, + defaultTitle: defaultTitle, + entityId: entityId, + subTitle: subTitle, + showLoadingIndicator: showLoadingIndicator, + hideAppBar: hideAppBar, + appBarElevation: appBarElevation, + ); @override - Widget buildEntityDetails(BuildContext context, T contact) { + Widget buildEntityDetails(BuildContext context, T entity) { return Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text('Title', style: labelTextStyle), + Text(entity.getName(), style: valueTextStyle), + const SizedBox(height: 16), + Text('Country', style: labelTextStyle), + Text(entity.country ?? '', style: valueTextStyle), + const SizedBox(height: 16), + Row( mainAxisSize: MainAxisSize.max, children: [ - Text('Title', style: labelTextStyle), - Text(contact.getName(), style: valueTextStyle), - SizedBox(height: 16), - Text('Country', style: labelTextStyle), - Text(contact.country ?? '', style: valueTextStyle), - SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text('City', style: labelTextStyle), - Text(contact.city ?? '', style: valueTextStyle), - ], - )), - Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text('State / Province', style: labelTextStyle), - Text(contact.state ?? '', style: valueTextStyle), - ], - )), - ], + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text('City', style: labelTextStyle), + Text(entity.city ?? '', style: valueTextStyle), + ], + ), + ), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text('State / Province', style: labelTextStyle), + Text(entity.state ?? '', style: valueTextStyle), + ], + ), ), - SizedBox(height: 16), - Text('Zip / Postal Code', style: labelTextStyle), - Text(contact.zip ?? '', style: valueTextStyle), - SizedBox(height: 16), - Text('Address', style: labelTextStyle), - Text(contact.address ?? '', style: valueTextStyle), - SizedBox(height: 16), - Text('Address 2', style: labelTextStyle), - Text(contact.address2 ?? '', style: valueTextStyle), - SizedBox(height: 16), - Text('Phone', style: labelTextStyle), - Text(contact.phone ?? '', style: valueTextStyle), - SizedBox(height: 16), - Text('Email', style: labelTextStyle), - Text(contact.email ?? '', style: valueTextStyle), - ])); + ], + ), + const SizedBox(height: 16), + Text('Zip / Postal Code', style: labelTextStyle), + Text(entity.zip ?? '', style: valueTextStyle), + const SizedBox(height: 16), + Text('Address', style: labelTextStyle), + Text(entity.address ?? '', style: valueTextStyle), + const SizedBox(height: 16), + Text('Address 2', style: labelTextStyle), + Text(entity.address2 ?? '', style: valueTextStyle), + const SizedBox(height: 16), + Text('Phone', style: labelTextStyle), + Text(entity.phone ?? '', style: valueTextStyle), + const SizedBox(height: 16), + Text('Email', style: labelTextStyle), + Text(entity.email ?? '', style: valueTextStyle), + ], + ), + ); } } diff --git a/lib/core/entity/entity_grid_card.dart b/lib/core/entity/entity_grid_card.dart index 7f6ba748..33a77889 100644 --- a/lib/core/entity/entity_grid_card.dart +++ b/lib/core/entity/entity_grid_card.dart @@ -8,44 +8,48 @@ class EntityGridCard extends StatelessWidget { final EntityCardWidgetBuilder _entityCardWidgetBuilder; final EntityCardSettings _settings; - EntityGridCard(T entity, - {Key? key, - EntityTapFunction? onEntityTap, - required EntityCardWidgetBuilder entityCardWidgetBuilder, - required EntityCardSettings settings}) - : this._entity = entity, - this._onEntityTap = onEntityTap, - this._entityCardWidgetBuilder = entityCardWidgetBuilder, - this._settings = settings, + const EntityGridCard( + T entity, { + Key? key, + EntityTapFunction? onEntityTap, + required EntityCardWidgetBuilder entityCardWidgetBuilder, + required EntityCardSettings settings, + }) : _entity = entity, + _onEntityTap = onEntityTap, + _entityCardWidgetBuilder = entityCardWidgetBuilder, + _settings = settings, super(key: key); @override Widget build(BuildContext context) { return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - child: Card( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - elevation: 0, - child: _entityCardWidgetBuilder(context, _entity)), - decoration: _settings.dropShadow - ? BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha((255 * 0.05).ceil()), - blurRadius: 6.0, - offset: Offset(0, 4)) - ], - ) - : null, + behavior: HitTestBehavior.opaque, + child: Container( + decoration: _settings.dropShadow + ? BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha((255 * 0.05).ceil()), + blurRadius: 6.0, + offset: const Offset(0, 4), + ), + ], + ) + : null, + child: Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + elevation: 0, + child: _entityCardWidgetBuilder(context, _entity), ), - onTap: () { - if (_onEntityTap != null) { - _onEntityTap(_entity); - } - }); + ), + onTap: () { + if (_onEntityTap != null) { + _onEntityTap(_entity); + } + }, + ); } } diff --git a/lib/core/entity/entity_list_card.dart b/lib/core/entity/entity_list_card.dart index 9ea3a408..999b64df 100644 --- a/lib/core/entity/entity_list_card.dart +++ b/lib/core/entity/entity_list_card.dart @@ -8,50 +8,52 @@ class EntityListCard extends StatelessWidget { final EntityTapFunction? _onEntityTap; final EntityCardWidgetBuilder _entityCardWidgetBuilder; - EntityListCard(T entity, - {Key? key, - EntityTapFunction? onEntityTap, - required EntityCardWidgetBuilder entityCardWidgetBuilder, - bool listWidgetCard = false}) - : this._entity = entity, - this._onEntityTap = onEntityTap, - this._entityCardWidgetBuilder = entityCardWidgetBuilder, - this._listWidgetCard = listWidgetCard, - super(key: key); + const EntityListCard( + T entity, { + EntityTapFunction? onEntityTap, + required EntityCardWidgetBuilder entityCardWidgetBuilder, + bool listWidgetCard = false, + super.key, + }) : _entity = entity, + _onEntityTap = onEntityTap, + _entityCardWidgetBuilder = entityCardWidgetBuilder, + _listWidgetCard = listWidgetCard; @override Widget build(BuildContext context) { return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - margin: _listWidgetCard ? EdgeInsets.only(right: 8) : EdgeInsets.zero, - child: Card( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( + behavior: HitTestBehavior.opaque, + child: Container( + margin: + _listWidgetCard ? const EdgeInsets.only(right: 8) : EdgeInsets.zero, + decoration: _listWidgetCard + ? BoxDecoration( + border: Border.all( + color: const Color(0xFFDEDEDE), + style: BorderStyle.solid, + width: 1, + ), borderRadius: BorderRadius.circular(4), + ) + : BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha((255 * 0.05).ceil()), + blurRadius: 6.0, + offset: const Offset(0, 4), + ), + ], ), - elevation: 0, - child: _entityCardWidgetBuilder(context, _entity)), - decoration: _listWidgetCard - ? BoxDecoration( - border: Border.all( - color: Color(0xFFDEDEDE), - style: BorderStyle.solid, - width: 1), - borderRadius: BorderRadius.circular(4)) - : BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha((255 * 0.05).ceil()), - blurRadius: 6.0, - offset: Offset(0, 4)), - ], - ), + child: Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + elevation: 0, + child: _entityCardWidgetBuilder(context, _entity), ), - onTap: () { - if (_onEntityTap != null) { - _onEntityTap(_entity); - } - }); + ), + onTap: () => _onEntityTap?.call(_entity), + ); } } diff --git a/lib/core/init/init_app.dart b/lib/core/init/init_app.dart index 890f19df..33a820d9 100644 --- a/lib/core/init/init_app.dart +++ b/lib/core/init/init_app.dart @@ -8,7 +8,7 @@ class ThingsboardInitApp extends TbPageWidget { : super(tbContext, key: key); @override - _ThingsboardInitAppState createState() => _ThingsboardInitAppState(); + State createState() => _ThingsboardInitAppState(); } class _ThingsboardInitAppState extends TbPageState { @@ -23,7 +23,7 @@ class _ThingsboardInitAppState extends TbPageState { return Container( alignment: Alignment.center, color: Colors.white, - child: TbProgressIndicator(size: 50.0), + child: const TbProgressIndicator(size: 50.0), ); } } diff --git a/lib/core/init/init_routes.dart b/lib/core/init/init_routes.dart index 698e53c1..9e640a3c 100644 --- a/lib/core/init/init_routes.dart +++ b/lib/core/init/init_routes.dart @@ -8,14 +8,15 @@ import 'init_app.dart'; class InitRoutes extends TbRoutes { late var initHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return ThingsboardInitApp(tbContext); - }); + handlerFunc: (BuildContext? context, Map params) { + return ThingsboardInitApp(tbContext); + }, + ); InitRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/", handler: initHandler); + router.define('/', handler: initHandler); } } diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart index 32161b6f..ad8c0bc3 100644 --- a/lib/generated/intl/messages_all.dart +++ b/lib/generated/intl/messages_all.dart @@ -39,8 +39,10 @@ MessageLookupByLibrary? _findExact(String localeName) { /// User programs should call this before using [localeName] for messages. Future initializeMessages(String localeName) { var availableLocale = Intl.verifiedLocale( - localeName, (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null); + localeName, + (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null, + ); if (availableLocale == null) { return new SynchronousFuture(false); } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 62ba8666..614fccce 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -30,6 +30,7 @@ class MessageLookup extends MessageLookupByLibrary { "A security code has been sent to your phone at ${contact}."; final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { "No": MessageLookupByLibrary.simpleMessage("No"), "OR": MessageLookupByLibrary.simpleMessage("OR"), @@ -39,11 +40,13 @@ class MessageLookup extends MessageLookupByLibrary { "address": MessageLookupByLibrary.simpleMessage("Address"), "address2": MessageLookupByLibrary.simpleMessage("Address 2"), "alarmAcknowledgeText": MessageLookupByLibrary.simpleMessage( - "Are you sure you want to acknowledge Alarm?"), + "Are you sure you want to acknowledge Alarm?", + ), "alarmAcknowledgeTitle": MessageLookupByLibrary.simpleMessage("Acknowledge Alarm"), "alarmClearText": MessageLookupByLibrary.simpleMessage( - "Are you sure you want to clear Alarm?"), + "Are you sure you want to clear Alarm?", + ), "alarmClearTitle": MessageLookupByLibrary.simpleMessage("Clear Alarm"), "alarms": MessageLookupByLibrary.simpleMessage("Alarms"), "allDevices": MessageLookupByLibrary.simpleMessage("All devices"), @@ -56,7 +59,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Audit log details"), "auditLogs": MessageLookupByLibrary.simpleMessage("Audit Logs"), "backupCodeAuthDescription": MessageLookupByLibrary.simpleMessage( - "Please enter one of your backup codes."), + "Please enter one of your backup codes.", + ), "backupCodeAuthPlaceholder": MessageLookupByLibrary.simpleMessage("Backup code"), "changePassword": @@ -67,7 +71,8 @@ class MessageLookup extends MessageLookupByLibrary { "currentPassword": MessageLookupByLibrary.simpleMessage("currentPassword"), "currentPasswordRequireText": MessageLookupByLibrary.simpleMessage( - "Current password is required."), + "Current password is required.", + ), "currentPasswordStar": MessageLookupByLibrary.simpleMessage("Current password *"), "customer": MessageLookupByLibrary.simpleMessage("Customer"), @@ -93,7 +98,8 @@ class MessageLookup extends MessageLookupByLibrary { "lastName": MessageLookupByLibrary.simpleMessage("lastName"), "lastNameUpper": MessageLookupByLibrary.simpleMessage("Last Name"), "listIsEmptyText": MessageLookupByLibrary.simpleMessage( - "The list is currently empty."), + "The list is currently empty.", + ), "login": MessageLookupByLibrary.simpleMessage("Log In"), "loginNotification": MessageLookupByLibrary.simpleMessage("Login to your account"), @@ -110,7 +116,8 @@ class MessageLookup extends MessageLookupByLibrary { "newPassword": MessageLookupByLibrary.simpleMessage("newPassword"), "newPassword2": MessageLookupByLibrary.simpleMessage("newPassword2"), "newPassword2RequireText": MessageLookupByLibrary.simpleMessage( - "New password again is required."), + "New password again is required.", + ), "newPassword2Star": MessageLookupByLibrary.simpleMessage("New password again *"), "newPasswordRequireText": @@ -121,7 +128,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Not implemented!"), "password": MessageLookupByLibrary.simpleMessage("Password"), "passwordErrorNotification": MessageLookupByLibrary.simpleMessage( - "Entered passwords must be same!"), + "Entered passwords must be same!", + ), "passwordForgotText": MessageLookupByLibrary.simpleMessage("Forgot Password?"), "passwordRequireText": @@ -129,15 +137,19 @@ class MessageLookup extends MessageLookupByLibrary { "passwordReset": MessageLookupByLibrary.simpleMessage("Reset password"), "passwordResetLinkSuccessfullySentNotification": MessageLookupByLibrary.simpleMessage( - "Password reset link was successfully sent!"), + "Password reset link was successfully sent!", + ), "passwordResetText": MessageLookupByLibrary.simpleMessage( - "Enter the email associated with your account and we\'ll send an email with password reset link"), + "Enter the email associated with your account and we\'ll send an email with password reset link", + ), "passwordSuccessNotification": MessageLookupByLibrary.simpleMessage( - "Password successfully changed"), + "Password successfully changed", + ), "phone": MessageLookupByLibrary.simpleMessage("Phone"), "postalCode": MessageLookupByLibrary.simpleMessage("Zip / Postal Code"), "profileSuccessNotification": MessageLookupByLibrary.simpleMessage( - "Profile successfully updated"), + "Profile successfully updated", + ), "requestPasswordReset": MessageLookupByLibrary.simpleMessage("Request password reset"), "resendCode": MessageLookupByLibrary.simpleMessage("Resend code"), @@ -155,19 +167,23 @@ class MessageLookup extends MessageLookupByLibrary { "title": MessageLookupByLibrary.simpleMessage("Title"), "toptAuthPlaceholder": MessageLookupByLibrary.simpleMessage("Code"), "totpAuthDescription": MessageLookupByLibrary.simpleMessage( - "Please enter the security code from your authenticator app."), + "Please enter the security code from your authenticator app.", + ), "tryAgain": MessageLookupByLibrary.simpleMessage("Try Again"), "tryAnotherWay": MessageLookupByLibrary.simpleMessage("Try another way"), "type": MessageLookupByLibrary.simpleMessage("Type"), "username": MessageLookupByLibrary.simpleMessage("username"), "verificationCodeIncorrect": MessageLookupByLibrary.simpleMessage( - "Verification code is incorrect"), + "Verification code is incorrect", + ), "verificationCodeInvalid": MessageLookupByLibrary.simpleMessage( - "Invalid verification code format"), + "Invalid verification code format", + ), "verificationCodeManyRequest": MessageLookupByLibrary.simpleMessage( - "Too many requests check verification code"), + "Too many requests check verification code", + ), "verifyYourIdentity": - MessageLookupByLibrary.simpleMessage("Verify your identity") + MessageLookupByLibrary.simpleMessage("Verify your identity"), }; } diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index d022ffa9..541ce257 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -21,6 +21,7 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh'; final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { "No": MessageLookupByLibrary.simpleMessage("否"), "OR": MessageLookupByLibrary.simpleMessage("或"), @@ -88,7 +89,8 @@ class MessageLookup extends MessageLookupByLibrary { "passwordResetLinkSuccessfullySentNotification": MessageLookupByLibrary.simpleMessage("密码重置链接已发送"), "passwordResetText": MessageLookupByLibrary.simpleMessage( - "输入和账号关联的Email,我们将发送一个密码重置链接到的Email"), + "输入和账号关联的Email,我们将发送一个密码重置链接到的Email", + ), "passwordSuccessNotification": MessageLookupByLibrary.simpleMessage("密码修改成功"), "phone": MessageLookupByLibrary.simpleMessage("电话"), @@ -102,6 +104,6 @@ class MessageLookup extends MessageLookupByLibrary { "title": MessageLookupByLibrary.simpleMessage("标题"), "tryAgain": MessageLookupByLibrary.simpleMessage("再试一次"), "type": MessageLookupByLibrary.simpleMessage("类型"), - "username": MessageLookupByLibrary.simpleMessage("用户名") + "username": MessageLookupByLibrary.simpleMessage("用户名"), }; } diff --git a/lib/main.dart b/lib/main.dart index 0824e0a2..0bb0782a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,11 +49,11 @@ void main() async { log('main::getInitialUri() exception $e', error: e); } - runApp(ThingsboardApp()); + runApp(const ThingsboardApp()); } class ThingsboardApp extends StatefulWidget { - ThingsboardApp({Key? key}) : super(key: key); + const ThingsboardApp({Key? key}) : super(key: key); @override ThingsboardAppState createState() => ThingsboardAppState(); @@ -126,6 +126,7 @@ class ThingsboardAppState extends State return _closeDashboard(animate: animate); } + @override bool isDashboardOpen() { return _mainPageViewController.index == 1; } @@ -167,7 +168,7 @@ class ThingsboardAppState extends State @override Widget build(BuildContext context) { SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( + const SystemUiOverlayStyle( systemNavigationBarColor: Colors.white, statusBarColor: Colors.white, systemNavigationBarIconBrightness: Brightness.light, @@ -176,7 +177,7 @@ class ThingsboardAppState extends State return MaterialApp( debugShowCheckedModeBanner: false, - localizationsDelegates: [ + localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -192,7 +193,7 @@ class ThingsboardAppState extends State key: mainAppKey, scaffoldMessengerKey: getIt().tbContext.messengerKey, - localizationsDelegates: [ + localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -212,7 +213,7 @@ class ThingsboardAppState extends State debugShowCheckedModeBanner: false, key: dashboardKey, // scaffoldMessengerKey: appRouter.tbContext.messengerKey, - localizationsDelegates: [ + localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, diff --git a/lib/modules/alarm/alarm_routes.dart b/lib/modules/alarm/alarm_routes.dart index f2851b27..a4b6cc6f 100644 --- a/lib/modules/alarm/alarm_routes.dart +++ b/lib/modules/alarm/alarm_routes.dart @@ -7,19 +7,20 @@ import 'package:thingsboard_app/modules/main/main_page.dart'; class AlarmRoutes extends TbRoutes { late var alarmsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - var searchMode = params['search']?.first == 'true'; - if (searchMode) { - return AlarmsPage(tbContext, searchMode: true); - } else { - return MainPage(tbContext, path: '/alarms'); - } - }); + handlerFunc: (BuildContext? context, Map params) { + var searchMode = params['search']?.first == 'true'; + if (searchMode) { + return AlarmsPage(tbContext, searchMode: true); + } else { + return MainPage(tbContext, path: '/alarms'); + } + }, + ); AlarmRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/alarms", handler: alarmsHandler); + router.define('/alarms', handler: alarmsHandler); } } diff --git a/lib/modules/alarm/alarms_base.dart b/lib/modules/alarm/alarms_base.dart index ea452cf4..7c099fc7 100644 --- a/lib/modules/alarm/alarms_base.dart +++ b/lib/modules/alarm/alarms_base.dart @@ -47,14 +47,20 @@ mixin AlarmsBase on EntitiesBase { void onEntityTap(AlarmInfo alarm) { String? dashboardId = alarm.details?['dashboardId']; if (dashboardId != null) { - var state = Utils.createDashboardEntityState(alarm.originator, - entityName: alarm.originatorName); - navigateToDashboard(dashboardId, - dashboardTitle: alarm.originatorName, state: state); + var state = Utils.createDashboardEntityState( + alarm.originator, + entityName: alarm.originatorName, + ); + navigateToDashboard( + dashboardId, + dashboardTitle: alarm.originatorName, + state: state, + ); } else { if (tbClient.isTenantAdmin()) { showWarnNotification( - 'Mobile dashboard should be configured in device profile alarm rules!'); + 'Mobile dashboard should be configured in device profile alarm rules!', + ); } } } @@ -71,10 +77,17 @@ mixin AlarmsBase on EntitiesBase { class AlarmQueryController extends PageKeyController { AlarmQueryController({int pageSize = 20, String? searchText}) - : super(AlarmQuery( - TimePageLink(pageSize, 0, searchText, - SortOrder('createdTime', Direction.DESC)), - fetchOriginator: true)); + : super( + AlarmQuery( + TimePageLink( + pageSize, + 0, + searchText, + SortOrder('createdTime', Direction.DESC), + ), + fetchOriginator: true, + ), + ); @override AlarmQuery nextPageKey(AlarmQuery pageKey) { @@ -92,189 +105,219 @@ class AlarmQueryController extends PageKeyController { class AlarmCard extends TbContextWidget { final AlarmInfo alarm; - AlarmCard(TbContext tbContext, {required this.alarm}) : super(tbContext); + AlarmCard(TbContext tbContext, {required this.alarm, super.key}) + : super(tbContext); @override - _AlarmCardState createState() => _AlarmCardState(alarm); + State createState() => _AlarmCardState(); } class _AlarmCardState extends TbContextState { bool loading = false; - AlarmInfo alarm; + late AlarmInfo alarm; final entityDateFormat = DateFormat('yyyy-MM-dd'); - _AlarmCardState(this.alarm) : super(); - @override void initState() { + alarm = widget.alarm; super.initState(); } @override void didUpdateWidget(AlarmCard oldWidget) { super.didUpdateWidget(oldWidget); - this.loading = false; - this.alarm = widget.alarm; + loading = false; } @override Widget build(BuildContext context) { - if (this.loading) { + if (loading) { return Container( - height: 134, - alignment: Alignment.center, - child: RefreshProgressIndicator()); + height: 134, + alignment: Alignment.center, + child: const RefreshProgressIndicator(), + ); } else { bool hasDashboard = alarm.details?['dashboardId'] != null; return Stack( children: [ Positioned.fill( + child: Container( + alignment: Alignment.centerLeft, 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( + width: 4, + decoration: BoxDecoration( + color: alarmSeverityColors[alarm.severity]!, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(width: 4), + Flexible( fit: FlexFit.tight, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(width: 16), - Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 12), - Row( - mainAxisSize: MainAxisSize.max, - 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)) - ]), - SizedBox(height: 4), - Row( - 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))), - Text( - alarmSeverityTranslations[ - alarm.severity]!, - style: TextStyle( - color: alarmSeverityColors[ - alarm.severity]!, - fontWeight: FontWeight.w500, - fontSize: 12, - height: 16 / 12)) - ]), - SizedBox(height: 12) - ], - )), - SizedBox(width: 16), - if (hasDashboard) - Icon(Icons.chevron_right, - color: Color(0xFFACACAC)), - if (hasDashboard) SizedBox(width: 16), - ]), - Divider(height: 1), - SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(width: 16), - Flexible( - fit: FlexFit.tight, - child: Text( - alarmStatusTranslations[alarm.status]!, - style: TextStyle( - color: Color(0xFF282828), - fontWeight: FontWeight.normal, - fontSize: 14, - height: 20 / 14))), - SizedBox(height: 32), - Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 16), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, 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, context))), - 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, context))) - ]) + const SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: AutoSizeText( + alarm.type, + maxLines: 2, + minFontSize: 8, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + Text( + entityDateFormat.format( + DateTime.fromMillisecondsSinceEpoch( + alarm.createdTime!, + ), + ), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 12, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: Text( + alarm.originatorName != null + ? alarm.originatorName! + : '', + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 12, + height: 16 / 12, + ), + ), + ), + Text( + alarmSeverityTranslations[alarm.severity]!, + style: TextStyle( + color: + alarmSeverityColors[alarm.severity]!, + fontWeight: FontWeight.w500, + fontSize: 12, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 12), ], ), - SizedBox(width: 8) - ], - ), - SizedBox(height: 8) - ])) - ]) + ), + const SizedBox(width: 16), + if (hasDashboard) + const Icon( + Icons.chevron_right, + color: Color(0xFFACACAC), + ), + if (hasDashboard) const SizedBox(width: 16), + ], + ), + const Divider(height: 1), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 16), + Flexible( + fit: FlexFit.tight, + child: Text( + alarmStatusTranslations[alarm.status]!, + style: const TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.normal, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + const SizedBox(height: 32), + Row( + children: [ + if ([ + AlarmStatus.CLEARED_UNACK, + AlarmStatus.ACTIVE_UNACK, + ].contains(alarm.status)) + CircleAvatar( + radius: 16, + backgroundColor: const Color(0xffF0F4F9), + child: IconButton( + icon: const Icon(Icons.done, size: 18), + padding: const EdgeInsets.all(7.0), + onPressed: () => _ackAlarm(alarm, context), + ), + ), + if ([ + AlarmStatus.ACTIVE_UNACK, + AlarmStatus.ACTIVE_ACK, + ].contains(alarm.status)) + Row( + children: [ + const SizedBox(width: 4), + CircleAvatar( + radius: 16, + backgroundColor: const Color(0xffF0F4F9), + child: IconButton( + icon: const Icon(Icons.clear, size: 18), + padding: const EdgeInsets.all(7.0), + onPressed: () => + _clearAlarm(alarm, context), + ), + ), + ], + ), + ], + ), + const SizedBox(width: 8), + ], + ), + const SizedBox(height: 8), + ], + ), + ), + ], + ), ], ); } @@ -282,10 +325,11 @@ class _AlarmCardState extends TbContextState { _clearAlarm(AlarmInfo alarm, BuildContext context) async { var res = await confirm( - title: '${S.of(context).alarmClearTitle}', - message: '${S.of(context).alarmClearText}', - cancel: '${S.of(context).No}', - ok: '${S.of(context).Yes}'); + title: S.of(context).alarmClearTitle, + message: S.of(context).alarmClearText, + cancel: S.of(context).No, + ok: S.of(context).Yes, + ); if (res != null && res) { setState(() { loading = true; @@ -302,10 +346,11 @@ class _AlarmCardState extends TbContextState { _ackAlarm(AlarmInfo alarm, BuildContext context) async { var res = await confirm( - title: '${S.of(context).alarmAcknowledgeTitle}', - message: '${S.of(context).alarmAcknowledgeText}', - cancel: '${S.of(context).No}', - ok: '${S.of(context).Yes}'); + title: S.of(context).alarmAcknowledgeTitle, + message: S.of(context).alarmAcknowledgeText, + cancel: S.of(context).No, + ok: S.of(context).Yes, + ); if (res != null && res) { setState(() { loading = true; diff --git a/lib/modules/alarm/alarms_list.dart b/lib/modules/alarm/alarms_list.dart index a28947db..74c27091 100644 --- a/lib/modules/alarm/alarms_list.dart +++ b/lib/modules/alarm/alarms_list.dart @@ -8,7 +8,9 @@ import 'alarms_base.dart'; class AlarmsList extends BaseEntitiesWidget with AlarmsBase, EntitiesListStateBase { AlarmsList( - TbContext tbContext, PageKeyController pageKeyController, - {searchMode = false}) - : super(tbContext, pageKeyController, searchMode: searchMode); + TbContext tbContext, + PageKeyController pageKeyController, { + super.key, + searchMode = false, + }) : super(tbContext, pageKeyController, searchMode: searchMode); } diff --git a/lib/modules/alarm/alarms_page.dart b/lib/modules/alarm/alarms_page.dart index ca0193c2..bec2dec3 100644 --- a/lib/modules/alarm/alarms_page.dart +++ b/lib/modules/alarm/alarms_page.dart @@ -9,10 +9,11 @@ import 'alarms_list.dart'; class AlarmsPage extends TbContextWidget { final bool searchMode; - AlarmsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext); + AlarmsPage(TbContext tbContext, {this.searchMode = false, super.key}) + : super(tbContext); @override - _AlarmsPageState createState() => _AlarmsPageState(); + State createState() => _AlarmsPageState(); } class _AlarmsPageState extends TbContextState @@ -27,8 +28,11 @@ class _AlarmsPageState extends TbContextState @override Widget build(BuildContext context) { super.build(context); - var alarmsList = AlarmsList(tbContext, _alarmQueryController, - searchMode: widget.searchMode); + var alarmsList = AlarmsList( + tbContext, + _alarmQueryController, + searchMode: widget.searchMode, + ); PreferredSizeWidget appBar; if (widget.searchMode) { appBar = TbAppSearchBar( @@ -37,14 +41,18 @@ class _AlarmsPageState extends TbContextState _alarmQueryController.onSearchText(searchText), ); } else { - appBar = TbAppBar(tbContext, title: Text(alarmsList.title), actions: [ - IconButton( - icon: Icon(Icons.search), - onPressed: () { - navigateTo('/alarms?search=true'); - }, - ) - ]); + appBar = TbAppBar( + tbContext, + title: Text(alarmsList.title), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + navigateTo('/alarms?search=true'); + }, + ), + ], + ); } return Scaffold(appBar: appBar, body: alarmsList); } diff --git a/lib/modules/asset/asset_details_page.dart b/lib/modules/asset/asset_details_page.dart index eca8f78f..98f3c3f6 100644 --- a/lib/modules/asset/asset_details_page.dart +++ b/lib/modules/asset/asset_details_page.dart @@ -6,37 +6,43 @@ import 'package:thingsboard_app/generated/l10n.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class AssetDetailsPage extends EntityDetailsPage { - AssetDetailsPage(TbContext tbContext, String assetId) - : super(tbContext, - entityId: assetId, - defaultTitle: 'Asset', - subTitle: 'Asset details'); + AssetDetailsPage(TbContext tbContext, String assetId, {super.key}) + : super( + tbContext, + entityId: assetId, + defaultTitle: 'Asset', + subTitle: 'Asset details', + ); @override - Future fetchEntity(String assetId) { - return tbClient.getAssetService().getAssetInfo(assetId); + Future fetchEntity(String id) { + return tbClient.getAssetService().getAssetInfo(id); } @override - Widget buildEntityDetails(BuildContext context, AssetInfo asset) { + Widget buildEntityDetails(BuildContext context, AssetInfo entity) { return Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text('${S.of(context).assetName}', style: labelTextStyle), - Text(asset.name, style: valueTextStyle), - SizedBox(height: 16), - Text('${S.of(context).type}', style: labelTextStyle), - Text(asset.type, style: valueTextStyle), - SizedBox(height: 16), - Text('${S.of(context).label}', style: labelTextStyle), - Text(asset.label ?? '', style: valueTextStyle), - SizedBox(height: 16), - Text('${S.of(context).assignedToCustomer}', - style: labelTextStyle), - Text(asset.customerTitle ?? '', style: valueTextStyle), - ])); + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text(S.of(context).assetName, style: labelTextStyle), + Text(entity.name, style: valueTextStyle), + const SizedBox(height: 16), + Text(S.of(context).type, style: labelTextStyle), + Text(entity.type, style: valueTextStyle), + const SizedBox(height: 16), + Text(S.of(context).label, style: labelTextStyle), + Text(entity.label ?? '', style: valueTextStyle), + const SizedBox(height: 16), + Text( + S.of(context).assignedToCustomer, + style: labelTextStyle, + ), + Text(entity.customerTitle ?? '', style: valueTextStyle), + ], + ), + ); } } diff --git a/lib/modules/asset/asset_routes.dart b/lib/modules/asset/asset_routes.dart index 6a527431..66abb3e0 100644 --- a/lib/modules/asset/asset_routes.dart +++ b/lib/modules/asset/asset_routes.dart @@ -8,21 +8,23 @@ import 'asset_details_page.dart'; class AssetRoutes extends TbRoutes { late var assetsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - var searchMode = params['search']?.first == 'true'; - return AssetsPage(tbContext, searchMode: searchMode); - }); + handlerFunc: (BuildContext? context, Map params) { + var searchMode = params['search']?.first == 'true'; + return AssetsPage(tbContext, searchMode: searchMode); + }, + ); late var assetDetailsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return AssetDetailsPage(tbContext, params["id"][0]); - }); + handlerFunc: (BuildContext? context, Map params) { + return AssetDetailsPage(tbContext, params['id'][0]); + }, + ); AssetRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/assets", handler: assetsHandler); - router.define("/asset/:id", handler: assetDetailsHandler); + router.define('/assets', handler: assetsHandler); + router.define('/asset/:id', handler: assetDetailsHandler); } } diff --git a/lib/modules/asset/assets_base.dart b/lib/modules/asset/assets_base.dart index ddf6e388..b8d6fc3e 100644 --- a/lib/modules/asset/assets_base.dart +++ b/lib/modules/asset/assets_base.dart @@ -41,93 +41,126 @@ mixin AssetsBase on EntitiesBase { } Widget _buildCard(context, AssetInfo asset) { - return Row(mainAxisSize: MainAxisSize.max, children: [ - Flexible( + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( fit: FlexFit.tight, child: Container( - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 0), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 0), child: Row( mainAxisSize: MainAxisSize.max, children: [ - SizedBox(width: 16), + const SizedBox(width: 16), Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text('${asset.name}', - style: TextStyle( - color: Color(0xFF282828), - fontSize: 14, - fontWeight: FontWeight.w500, - height: 20 / 14))), + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text( + asset.name, + style: const TextStyle( + color: Color(0xFF282828), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 20 / 14, + ), ), - Text( - entityDateFormat.format( - DateTime.fromMillisecondsSinceEpoch( - asset.createdTime!)), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)) - ]), - SizedBox(height: 4), - Text('${asset.type}', - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33)) - ], - )), - SizedBox(width: 16), - Icon(Icons.chevron_right, color: Color(0xFFACACAC)), - SizedBox(width: 16) + ), + ), + Text( + entityDateFormat.format( + DateTime.fromMillisecondsSinceEpoch( + asset.createdTime!, + ), + ), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + asset.type, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 1.33, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + const Icon(Icons.chevron_right, color: Color(0xFFACACAC)), + const SizedBox(width: 16), ], ), - )) - ]); + ), + ), + ], + ); } Widget _buildListWidgetCard(BuildContext context, AssetInfo asset) { - return Row(mainAxisSize: MainAxisSize.min, children: [ - Flexible( + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( fit: FlexFit.loose, child: Container( - padding: EdgeInsets.symmetric(vertical: 9, horizontal: 16), - child: Row(mainAxisSize: MainAxisSize.min, children: [ + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Flexible( - fit: FlexFit.loose, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text('${asset.name}', - style: TextStyle( - color: Color(0xFF282828), - fontSize: 14, - fontWeight: FontWeight.w500, - height: 1.7))), - Text('${asset.type}', - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33)) - ], - )) - ]))) - ]); + fit: FlexFit.loose, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text( + asset.name, + style: const TextStyle( + color: Color(0xFF282828), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.7, + ), + ), + ), + Text( + asset.type, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 1.33, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); } } diff --git a/lib/modules/asset/assets_list.dart b/lib/modules/asset/assets_list.dart index 9f080022..08b0425c 100644 --- a/lib/modules/asset/assets_list.dart +++ b/lib/modules/asset/assets_list.dart @@ -7,7 +7,10 @@ import 'assets_base.dart'; class AssetsList extends BaseEntitiesWidget with AssetsBase, EntitiesListStateBase { - AssetsList(TbContext tbContext, PageKeyController pageKeyController, - {searchMode = false}) - : super(tbContext, pageKeyController, searchMode: searchMode); + AssetsList( + TbContext tbContext, + PageKeyController pageKeyController, { + super.key, + searchMode = false, + }) : super(tbContext, pageKeyController, searchMode: searchMode); } diff --git a/lib/modules/asset/assets_list_widget.dart b/lib/modules/asset/assets_list_widget.dart index d02f95a0..2a6c43a2 100644 --- a/lib/modules/asset/assets_list_widget.dart +++ b/lib/modules/asset/assets_list_widget.dart @@ -5,9 +5,11 @@ import 'package:thingsboard_client/thingsboard_client.dart'; class AssetsListWidget extends EntitiesListPageLinkWidget with AssetsBase { - AssetsListWidget(TbContext tbContext, - {EntitiesListWidgetController? controller}) - : super(tbContext, controller: controller); + AssetsListWidget( + TbContext tbContext, { + super.key, + EntitiesListWidgetController? controller, + }) : super(tbContext, controller: controller); @override void onViewAll() { diff --git a/lib/modules/asset/assets_page.dart b/lib/modules/asset/assets_page.dart index b6911301..f6fde92b 100644 --- a/lib/modules/asset/assets_page.dart +++ b/lib/modules/asset/assets_page.dart @@ -9,10 +9,14 @@ import 'assets_list.dart'; class AssetsPage extends TbPageWidget { final bool searchMode; - AssetsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext); + AssetsPage( + TbContext tbContext, { + this.searchMode = false, + super.key, + }) : super(tbContext); @override - _AssetsPageState createState() => _AssetsPageState(); + State createState() => _AssetsPageState(); } class _AssetsPageState extends TbPageState { @@ -20,8 +24,11 @@ class _AssetsPageState extends TbPageState { @override Widget build(BuildContext context) { - var assetsList = AssetsList(tbContext, _pageLinkController, - searchMode: widget.searchMode); + final assetsList = AssetsList( + tbContext, + _pageLinkController, + searchMode: widget.searchMode, + ); PreferredSizeWidget appBar; if (widget.searchMode) { appBar = TbAppSearchBar( @@ -29,14 +36,18 @@ class _AssetsPageState extends TbPageState { onSearch: (searchText) => _pageLinkController.onSearchText(searchText), ); } else { - appBar = TbAppBar(tbContext, title: Text(assetsList.title), actions: [ - IconButton( - icon: Icon(Icons.search), - onPressed: () { - navigateTo('/assets?search=true'); - }, - ) - ]); + appBar = TbAppBar( + tbContext, + title: Text(assetsList.title), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + navigateTo('/assets?search=true'); + }, + ), + ], + ); } return Scaffold(appBar: appBar, body: assetsList); } diff --git a/lib/modules/audit_log/audit_log_details_page.dart b/lib/modules/audit_log/audit_log_details_page.dart index acaaca71..90d3a5f5 100644 --- a/lib/modules/audit_log/audit_log_details_page.dart +++ b/lib/modules/audit_log/audit_log_details_page.dart @@ -12,71 +12,93 @@ import 'package:thingsboard_client/thingsboard_client.dart'; class AuditLogDetailsPage extends TbContextWidget { final AuditLog auditLog; - AuditLogDetailsPage(TbContext tbContext, this.auditLog) : super(tbContext); + AuditLogDetailsPage(TbContext tbContext, this.auditLog, {super.key}) + : super(tbContext); @override - _AuditLogDetailsPageState createState() => _AuditLogDetailsPageState(); + State createState() => _AuditLogDetailsPageState(); } class _AuditLogDetailsPageState extends TbContextState { final labelTextStyle = - TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14); + const TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14); final valueTextStyle = - TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14); + const TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14); - final JsonEncoder encoder = new JsonEncoder.withIndent(' '); + final encoder = const JsonEncoder.withIndent(' '); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - appBar: TbAppBar(tbContext, - title: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + appBar: TbAppBar( + tbContext, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ if (widget.auditLog.entityName != null) - Text(widget.auditLog.entityName!, - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20 / 16)), - Text('${S.of(context).auditLogDetails}', - style: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .titleLarge! - .color! - .withAlpha((0.38 * 255).ceil()), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)) - ])), + Text( + widget.auditLog.entityName!, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20 / 16, + ), + ), + Text( + S.of(context).auditLogDetails, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .titleLarge! + .color! + .withAlpha((0.38 * 255).ceil()), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + ], + ), + ), body: Padding( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text('${S.of(context).entityType}', style: labelTextStyle), - Text(entityTypeTranslations[widget.auditLog.entityId.entityType]!, - style: valueTextStyle), - SizedBox(height: 16), - Text('${S.of(context).type}', style: labelTextStyle), - Text(actionTypeTranslations[widget.auditLog.actionType]!, - style: valueTextStyle), - SizedBox(height: 16), + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text(S.of(context).entityType, style: labelTextStyle), + Text( + entityTypeTranslations[widget.auditLog.entityId.entityType]!, + style: valueTextStyle, + ), + const SizedBox(height: 16), + Text(S.of(context).type, style: labelTextStyle), + Text( + actionTypeTranslations[widget.auditLog.actionType]!, + style: valueTextStyle, + ), + const SizedBox(height: 16), + Flexible( + fit: FlexFit.loose, + child: buildBorderedText( + S.of(context).actionData, + encoder.convert(widget.auditLog.actionData), + ), + ), + if (widget.auditLog.actionStatus == ActionStatus.FAILURE) + const SizedBox(height: 16), + if (widget.auditLog.actionStatus == ActionStatus.FAILURE) Flexible( - fit: FlexFit.loose, - child: buildBorderedText('${S.of(context).actionData}', - encoder.convert(widget.auditLog.actionData))), - if (widget.auditLog.actionStatus == ActionStatus.FAILURE) - SizedBox(height: 16), - if (widget.auditLog.actionStatus == ActionStatus.FAILURE) - Flexible( - fit: FlexFit.loose, - child: buildBorderedText('${S.of(context).failureDetails}', - widget.auditLog.actionFailureDetails!)) - ]), + fit: FlexFit.loose, + child: buildBorderedText( + S.of(context).failureDetails, + widget.auditLog.actionFailureDetails!, + ), + ), + ], + ), ), ); } @@ -86,33 +108,40 @@ class _AuditLogDetailsPageState extends TbContextState { children: [ Container( width: double.infinity, - padding: EdgeInsets.fromLTRB(16, 18, 48, 18), - margin: EdgeInsets.only(top: 6), + padding: const EdgeInsets.fromLTRB(16, 18, 48, 18), + margin: const EdgeInsets.only(top: 6), decoration: BoxDecoration( - border: Border.all(color: Color(0xFFDEDEDE), width: 1), + border: Border.all(color: const Color(0xFFDEDEDE), width: 1), borderRadius: BorderRadius.circular(4), shape: BoxShape.rectangle, ), child: SingleChildScrollView( child: Text( content, - style: TextStyle( - color: Color(0xFF282828), fontSize: 14, height: 20 / 14), + style: const TextStyle( + color: Color(0xFF282828), + fontSize: 14, + height: 20 / 14, + ), ), ), ), Positioned( - left: 16, - top: 0, - child: Container( - padding: EdgeInsets.only(left: 4, right: 4), - color: Colors.white, - child: Text( - title, - style: TextStyle( - color: Color(0xFF757575), fontSize: 12, height: 14 / 12), + left: 16, + top: 0, + child: Container( + padding: const EdgeInsets.only(left: 4, right: 4), + color: Colors.white, + child: Text( + title, + style: const TextStyle( + color: Color(0xFF757575), + fontSize: 12, + height: 14 / 12, ), - )), + ), + ), + ), ], ); } diff --git a/lib/modules/audit_log/audit_logs_base.dart b/lib/modules/audit_log/audit_logs_base.dart index 7455ae4d..aa668208 100644 --- a/lib/modules/audit_log/audit_logs_base.dart +++ b/lib/modules/audit_log/audit_logs_base.dart @@ -43,12 +43,12 @@ const Map actionTypeTranslations = { ActionType.ADDED_COMMENT: 'Added Comment', ActionType.UPDATED_COMMENT: 'Updated Comment', ActionType.DELETED_COMMENT: 'Deleted Comment', - ActionType.SMS_SENT: 'SMS Sent' + ActionType.SMS_SENT: 'SMS Sent', }; const Map actionStatusTranslations = { ActionStatus.SUCCESS: 'Success', - ActionStatus.FAILURE: 'Failure' + ActionStatus.FAILURE: 'Failure', }; mixin AuditLogsBase on EntitiesBase { @@ -79,21 +79,16 @@ mixin AuditLogsBase on EntitiesBase { class AuditLogCard extends TbContextWidget { final AuditLog auditLog; - AuditLogCard(TbContext tbContext, {required this.auditLog}) + AuditLogCard(TbContext tbContext, {super.key, required this.auditLog}) : super(tbContext); @override - _AuditLogCardState createState() => _AuditLogCardState(); + State createState() => _AuditLogCardState(); } class _AuditLogCardState extends TbContextState { final entityDateFormat = DateFormat('yyyy-MM-dd'); - @override - void initState() { - super.initState(); - } - @override void didUpdateWidget(AuditLogCard oldWidget) { super.didUpdateWidget(oldWidget); @@ -104,138 +99,156 @@ class _AuditLogCardState extends TbContextState { return Stack( children: [ Positioned.fill( + child: Container( + alignment: Alignment.centerLeft, child: Container( - alignment: Alignment.centerLeft, - child: Container( - width: 4, - decoration: BoxDecoration( - color: - widget.auditLog.actionStatus == ActionStatus.SUCCESS - ? Color(0xFF008A00) - : Color(0xFFFF0000), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4), - bottomLeft: Radius.circular(4))), - ))), - Row(mainAxisSize: MainAxisSize.max, children: [ - SizedBox(width: 4), - Flexible( + width: 4, + decoration: BoxDecoration( + color: widget.auditLog.actionStatus == ActionStatus.SUCCESS + ? const Color(0xFF008A00) + : const Color(0xFFFF0000), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(width: 4), + Flexible( fit: FlexFit.tight, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(width: 16), - Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 12), - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Flexible( - fit: FlexFit.tight, - child: AutoSizeText( - widget.auditLog.entityName ?? - '', - 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( - widget.auditLog - .createdTime!)), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontWeight: FontWeight.normal, - fontSize: 12, - height: 16 / 12)) - ]), - SizedBox(height: 4), - Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Flexible( - fit: FlexFit.tight, - child: Text( - entityTypeTranslations[widget - .auditLog - .entityId - .entityType]!, - style: TextStyle( - color: Color(0xFFAFAFAF), - fontWeight: - FontWeight.normal, - fontSize: 12, - height: 16 / 12))), - Text( - actionStatusTranslations[ - widget.auditLog.actionStatus]!, - style: TextStyle( - color: widget.auditLog - .actionStatus == - ActionStatus.SUCCESS - ? Color(0xFF008A00) - : Color(0xFFFF0000), - fontWeight: FontWeight.w500, - fontSize: 12, - height: 16 / 12)) - ]), - SizedBox(height: 12) - ], - )), - SizedBox(width: 16) - ]), - SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(width: 16), - Flexible( - fit: FlexFit.tight, - child: Text( - actionTypeTranslations[ - widget.auditLog.actionType]!, - style: TextStyle( - color: Color(0xFF282828), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 16), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: AutoSizeText( + widget.auditLog.entityName ?? '', + maxLines: 2, + minFontSize: 8, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + Text( + entityDateFormat.format( + DateTime.fromMillisecondsSinceEpoch( + widget.auditLog.createdTime!, + ), + ), + style: const TextStyle( + color: Color(0xFFAFAFAF), fontWeight: FontWeight.normal, - fontSize: 14, - height: 20 / 14))), - SizedBox(height: 32), - CircleAvatar( - radius: 16, - backgroundColor: Color(0xffF0F4F9), - child: IconButton( - icon: Icon(Icons.code, size: 18), - padding: EdgeInsets.all(7.0), - onPressed: () => - _auditLogDetails(widget.auditLog))), - SizedBox(width: 8) - ], - ), - SizedBox(height: 8) - ])) - ]) + fontSize: 12, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: Text( + entityTypeTranslations[ + widget.auditLog.entityId.entityType]!, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 12, + height: 16 / 12, + ), + ), + ), + Text( + actionStatusTranslations[ + widget.auditLog.actionStatus]!, + style: TextStyle( + color: widget.auditLog.actionStatus == + ActionStatus.SUCCESS + ? const Color(0xFF008A00) + : const Color(0xFFFF0000), + fontWeight: FontWeight.w500, + fontSize: 12, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 12), + ], + ), + ), + const SizedBox(width: 16), + ], + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 16), + Flexible( + fit: FlexFit.tight, + child: Text( + actionTypeTranslations[widget.auditLog.actionType]!, + style: const TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.normal, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + const SizedBox(height: 32), + CircleAvatar( + radius: 16, + backgroundColor: const Color(0xffF0F4F9), + child: IconButton( + icon: const Icon(Icons.code, size: 18), + padding: const EdgeInsets.all(7.0), + onPressed: () => _auditLogDetails(widget.auditLog), + ), + ), + const SizedBox(width: 8), + ], + ), + const SizedBox(height: 8), + ], + ), + ), + ], + ), ], ); } _auditLogDetails(AuditLog auditLog) { - tbContext - .showFullScreenDialog(new AuditLogDetailsPage(tbContext, auditLog)); + tbContext.showFullScreenDialog(AuditLogDetailsPage(tbContext, auditLog)); } } diff --git a/lib/modules/audit_log/audit_logs_list.dart b/lib/modules/audit_log/audit_logs_list.dart index c79abf63..c15a5a1f 100644 --- a/lib/modules/audit_log/audit_logs_list.dart +++ b/lib/modules/audit_log/audit_logs_list.dart @@ -7,7 +7,9 @@ import 'package:thingsboard_client/thingsboard_client.dart'; class AuditLogsList extends BaseEntitiesWidget with AuditLogsBase, EntitiesListStateBase { AuditLogsList( - TbContext tbContext, PageKeyController pageKeyController, - {searchMode = false}) - : super(tbContext, pageKeyController, searchMode: searchMode); + TbContext tbContext, + PageKeyController pageKeyController, { + searchMode = false, + super.key, + }) : super(tbContext, pageKeyController, searchMode: searchMode); } diff --git a/lib/modules/audit_log/audit_logs_page.dart b/lib/modules/audit_log/audit_logs_page.dart index e415557d..b007afc4 100644 --- a/lib/modules/audit_log/audit_logs_page.dart +++ b/lib/modules/audit_log/audit_logs_page.dart @@ -8,11 +8,11 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class AuditLogsPage extends TbPageWidget { final bool searchMode; - AuditLogsPage(TbContext tbContext, {this.searchMode = false}) + AuditLogsPage(TbContext tbContext, {this.searchMode = false, super.key}) : super(tbContext); @override - _AuditLogsPageState createState() => _AuditLogsPageState(); + State createState() => _AuditLogsPageState(); } class _AuditLogsPageState extends TbPageState { @@ -21,8 +21,11 @@ class _AuditLogsPageState extends TbPageState { @override Widget build(BuildContext context) { - var auditLogsList = AuditLogsList(tbContext, _timePageLinkController, - searchMode: widget.searchMode); + var auditLogsList = AuditLogsList( + tbContext, + _timePageLinkController, + searchMode: widget.searchMode, + ); PreferredSizeWidget appBar; if (widget.searchMode) { appBar = TbAppSearchBar( @@ -31,14 +34,18 @@ class _AuditLogsPageState extends TbPageState { _timePageLinkController.onSearchText(searchText), ); } else { - appBar = TbAppBar(tbContext, title: Text(auditLogsList.title), actions: [ - IconButton( - icon: Icon(Icons.search), - onPressed: () { - navigateTo('/auditLogs?search=true'); - }, - ) - ]); + appBar = TbAppBar( + tbContext, + title: Text(auditLogsList.title), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + navigateTo('/auditLogs?search=true'); + }, + ), + ], + ); } return Scaffold(appBar: appBar, body: auditLogsList); } diff --git a/lib/modules/audit_log/audit_logs_routes.dart b/lib/modules/audit_log/audit_logs_routes.dart index f753c44d..781414bf 100644 --- a/lib/modules/audit_log/audit_logs_routes.dart +++ b/lib/modules/audit_log/audit_logs_routes.dart @@ -5,16 +5,17 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/modules/audit_log/audit_logs_page.dart'; class AuditLogsRoutes extends TbRoutes { - late var auditLogsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - var searchMode = params['search']?.first == 'true'; - return AuditLogsPage(tbContext, searchMode: searchMode); - }); + late final auditLogsHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + var searchMode = params['search']?.first == 'true'; + return AuditLogsPage(tbContext, searchMode: searchMode); + }, + ); AuditLogsRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/auditLogs", handler: auditLogsHandler); + router.define('/auditLogs', handler: auditLogsHandler); } } diff --git a/lib/modules/customer/customer_details_page.dart b/lib/modules/customer/customer_details_page.dart index 07d0572c..c84652f7 100644 --- a/lib/modules/customer/customer_details_page.dart +++ b/lib/modules/customer/customer_details_page.dart @@ -3,14 +3,16 @@ import 'package:thingsboard_app/core/entity/entity_details_page.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class CustomerDetailsPage extends ContactBasedDetailsPage { - CustomerDetailsPage(TbContext tbContext, String customerId) - : super(tbContext, - entityId: customerId, - defaultTitle: 'Customer', - subTitle: 'Customer details'); + CustomerDetailsPage(TbContext tbContext, String customerId, {super.key}) + : super( + tbContext, + entityId: customerId, + defaultTitle: 'Customer', + subTitle: 'Customer details', + ); @override - Future fetchEntity(String customerId) { - return tbClient.getCustomerService().getCustomer(customerId); + Future fetchEntity(String id) { + return tbClient.getCustomerService().getCustomer(id); } } diff --git a/lib/modules/customer/customer_routes.dart b/lib/modules/customer/customer_routes.dart index 25311ec9..052c662d 100644 --- a/lib/modules/customer/customer_routes.dart +++ b/lib/modules/customer/customer_routes.dart @@ -2,26 +2,29 @@ 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 'customer_details_page.dart'; import 'customers_page.dart'; class CustomerRoutes extends TbRoutes { - late var customersHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - var searchMode = params['search']?.first == 'true'; - return CustomersPage(tbContext, searchMode: searchMode); - }); + late final customersHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + var searchMode = params['search']?.first == 'true'; + return CustomersPage(tbContext, searchMode: searchMode); + }, + ); - late var customerDetailsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return CustomerDetailsPage(tbContext, params["id"][0]); - }); + late final customerDetailsHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return CustomerDetailsPage(tbContext, params['id'][0]); + }, + ); CustomerRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/customers", handler: customersHandler); - router.define("/customer/:id", handler: customerDetailsHandler); + router.define('/customers', handler: customersHandler); + router.define('/customer/:id', handler: customerDetailsHandler); } } diff --git a/lib/modules/customer/customers_list.dart b/lib/modules/customer/customers_list.dart index 836a420b..607d0cb4 100644 --- a/lib/modules/customer/customers_list.dart +++ b/lib/modules/customer/customers_list.dart @@ -8,7 +8,9 @@ import 'customers_base.dart'; class CustomersList extends BaseEntitiesWidget with CustomersBase, ContactBasedBase, EntitiesListStateBase { CustomersList( - TbContext tbContext, PageKeyController pageKeyController, - {searchMode = false}) - : super(tbContext, pageKeyController, searchMode: searchMode); + TbContext tbContext, + PageKeyController pageKeyController, { + super.key, + searchMode = false, + }) : super(tbContext, pageKeyController, searchMode: searchMode); } diff --git a/lib/modules/customer/customers_page.dart b/lib/modules/customer/customers_page.dart index 15e0ebdc..5c256e28 100644 --- a/lib/modules/customer/customers_page.dart +++ b/lib/modules/customer/customers_page.dart @@ -8,11 +8,11 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class CustomersPage extends TbPageWidget { final bool searchMode; - CustomersPage(TbContext tbContext, {this.searchMode = false}) + CustomersPage(TbContext tbContext, {this.searchMode = false, super.key}) : super(tbContext); @override - _CustomersPageState createState() => _CustomersPageState(); + State createState() => _CustomersPageState(); } class _CustomersPageState extends TbPageState { @@ -20,8 +20,11 @@ class _CustomersPageState extends TbPageState { @override Widget build(BuildContext context) { - var customersList = CustomersList(tbContext, _pageLinkController, - searchMode: widget.searchMode); + var customersList = CustomersList( + tbContext, + _pageLinkController, + searchMode: widget.searchMode, + ); PreferredSizeWidget appBar; if (widget.searchMode) { appBar = TbAppSearchBar( @@ -29,14 +32,18 @@ class _CustomersPageState extends TbPageState { onSearch: (searchText) => _pageLinkController.onSearchText(searchText), ); } else { - appBar = TbAppBar(tbContext, title: Text(customersList.title), actions: [ - IconButton( - icon: Icon(Icons.search), - onPressed: () { - navigateTo('/customers?search=true'); - }, - ) - ]); + appBar = TbAppBar( + tbContext, + title: Text(customersList.title), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + navigateTo('/customers?search=true'); + }, + ), + ], + ); } return Scaffold(appBar: appBar, body: customersList); } diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index 44ed9f71..f0ccb8cb 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -22,10 +22,18 @@ class DashboardController { DashboardController(this.dashboardState); - Future openDashboard(String dashboardId, - {String? state, bool? hideToolbar, bool fullscreen = false}) async { - return await dashboardState._openDashboard(dashboardId, - state: state, hideToolbar: hideToolbar, fullscreen: fullscreen); + Future openDashboard( + String dashboardId, { + String? state, + bool? hideToolbar, + bool fullscreen = false, + }) async { + return await dashboardState._openDashboard( + dashboardId, + state: state, + hideToolbar: hideToolbar, + fullscreen: fullscreen, + ); } Future goBack() async { @@ -36,12 +44,12 @@ class DashboardController { canGoBack.value = await canGoBackFuture; } - onHasRightLayout(bool _hasRightLayout) { - hasRightLayout.value = _hasRightLayout; + onHasRightLayout(bool hasRightLayout) { + this.hasRightLayout.value = hasRightLayout; } - onRightLayoutOpened(bool _rightLayoutOpened) { - rightLayoutOpened.value = _rightLayoutOpened; + onRightLayoutOpened(bool rightLayoutOpened) { + this.rightLayoutOpened.value = rightLayoutOpened; } Future toggleRightLayout() async { @@ -66,7 +74,8 @@ class DashboardController { typedef DashboardTitleCallback = void Function(String title); typedef DashboardControllerCallback = void Function( - DashboardController controller); + DashboardController controller, +); class Dashboard extends TbContextWidget { final bool? _home; @@ -74,20 +83,21 @@ class Dashboard extends TbContextWidget { final DashboardTitleCallback? _titleCallback; final DashboardControllerCallback? _controllerCallback; - Dashboard(TbContext tbContext, - {Key? key, - bool? home, - bool activeByDefault = true, - DashboardTitleCallback? titleCallback, - DashboardControllerCallback? controllerCallback}) - : this._home = home, - this._activeByDefault = activeByDefault, - this._titleCallback = titleCallback, - this._controllerCallback = controllerCallback, + Dashboard( + TbContext tbContext, { + Key? key, + bool? home, + bool activeByDefault = true, + DashboardTitleCallback? titleCallback, + DashboardControllerCallback? controllerCallback, + }) : _home = home, + _activeByDefault = activeByDefault, + _titleCallback = titleCallback, + _controllerCallback = controllerCallback, super(tbContext, key: key); @override - _DashboardState createState() => _DashboardState(); + State createState() => _DashboardState(); } class _DashboardState extends TbContextState { @@ -141,8 +151,9 @@ class _DashboardState extends TbContextState { if (tbContext.isAuthenticated) { if (!readyState.value) { _initialUrl = Uri.parse( - await getIt().getEndpoint() + - '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}', + '${await getIt().getEndpoint()}' + '?accessToken=${tbClient.getJwtToken()!}' + '&refreshToken=${tbClient.getRefreshToken()!}', ); readyState.value = true; @@ -151,14 +162,15 @@ class _DashboardState extends TbContextState { 'type': 'reloadUserMessage', 'data': { 'accessToken': tbClient.getJwtToken()!, - 'refreshToken': tbClient.getRefreshToken()! - } + 'refreshToken': tbClient.getRefreshToken()!, + }, }; if (!UniversalPlatform.isWeb) { var controller = await _controller.future; await controller.postWebMessage( - message: WebMessage(data: jsonEncode(windowMessage)), - targetOrigin: Uri.parse('*')); + message: WebMessage(data: jsonEncode(windowMessage)), + targetOrigin: Uri.parse('*'), + ); } } } @@ -200,8 +212,12 @@ class _DashboardState extends TbContextState { } } - Future _openDashboard(String dashboardId, - {String? state, bool? hideToolbar, bool fullscreen = false}) async { + Future _openDashboard( + String dashboardId, { + String? state, + bool? hideToolbar, + bool fullscreen = false, + }) async { dashboardLoading.value = true; InAppWebViewController? controller; if (!UniversalPlatform.isWeb) { @@ -209,7 +225,7 @@ class _DashboardState extends TbContextState { } var windowMessage = { 'type': 'openDashboardMessage', - 'data': {'dashboardId': dashboardId} + 'data': {'dashboardId': dashboardId}, }; if (state != null) { windowMessage['data']['state'] = state; @@ -232,13 +248,15 @@ class _DashboardState extends TbContextState { var windowMessage = {'type': 'toggleDashboardLayout'}; var webMessage = WebMessage(data: jsonEncode(windowMessage)); await controller.postWebMessage( - message: webMessage, targetOrigin: Uri.parse('*')); + message: webMessage, + targetOrigin: Uri.parse('*'), + ); } Future tryLocalNavigation(String? path) async { - log.debug("path: $path"); + log.debug('path: $path'); if (path != null && path != '/home') { - final parts = path.split("/"); + final parts = path.split('/'); if ([ 'profile', 'devices', @@ -293,25 +311,26 @@ class _DashboardState extends TbContextState { valueListenable: readyState, builder: (BuildContext context, bool ready, child) { if (!ready) { - return SizedBox.shrink(); + return const SizedBox.shrink(); } else { return Stack( children: [ UniversalPlatform.isWeb - ? Center(child: Text('Not implemented!')) + ? const Center(child: Text('Not implemented!')) : InAppWebView( key: webViewKey, initialUrlRequest: URLRequest(url: _initialUrl), initialOptions: options, onWebViewCreated: (webViewController) { - log.debug("onWebViewCreated"); + log.debug('onWebViewCreated'); webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardLoadedHandler", + handlerName: 'tbMobileDashboardLoadedHandler', callback: (args) async { bool hasRightLayout = args[0]; bool rightLayoutOpened = args[1]; log.debug( - "Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened"); + 'Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened', + ); _dashboardController .onHasRightLayout(hasRightLayout); _dashboardController @@ -320,20 +339,22 @@ class _DashboardState extends TbContextState { }, ); webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardLayoutHandler", + handlerName: 'tbMobileDashboardLayoutHandler', callback: (args) async { bool rightLayoutOpened = args[0]; log.debug( - "Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened"); + 'Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened', + ); _dashboardController .onRightLayoutOpened(rightLayoutOpened); }, ); webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardStateNameHandler", + handlerName: 'tbMobileDashboardStateNameHandler', callback: (args) async { log.debug( - "Invoked tbMobileDashboardStateNameHandler: $args"); + 'Invoked tbMobileDashboardStateNameHandler: $args', + ); if (args.isNotEmpty && args[0] is String) { if (widget._titleCallback != null) { widget._titleCallback!(args[0]); @@ -342,10 +363,10 @@ class _DashboardState extends TbContextState { }, ); webViewController.addJavaScriptHandler( - handlerName: "tbMobileNavigationHandler", + handlerName: 'tbMobileNavigationHandler', callback: (args) async { log.debug( - "Invoked tbMobileNavigationHandler: $args", + 'Invoked tbMobileNavigationHandler: $args', ); if (args.isNotEmpty) { late String path; @@ -361,22 +382,24 @@ class _DashboardState extends TbContextState { params = args[1]; } - log.debug("path: $path"); - log.debug("params: $params"); + log.debug('path: $path'); + log.debug('params: $params'); try { await tryLocalNavigation(path); } on UnimplementedError catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - _buildWarnSnackBar(e.message!), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + _buildWarnSnackBar(e.message!), + ); + } } } }, ); webViewController.addJavaScriptHandler( - handlerName: "tbMobileHandler", + handlerName: 'tbMobileHandler', callback: (args) async { - log.debug("Invoked tbMobileHandler: $args"); + log.debug('Invoked tbMobileHandler: $args'); return await widgetActionHandler .handleWidgetMobileAction( args, @@ -399,16 +422,18 @@ class _DashboardState extends TbContextState { IOSWKNavigationType.LINK_ACTIVATED) { if (uriString.startsWith(endpoint)) { var target = uriString.substring(endpoint.length); - if (!target.startsWith("?accessToken")) { - if (target.startsWith("/")) { + if (!target.startsWith('?accessToken')) { + if (target.startsWith('/')) { target = target.substring(1); } try { await tryLocalNavigation(target); } on UnimplementedError catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - _buildWarnSnackBar(e.message!), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + _buildWarnSnackBar(e.message!), + ); + } } return NavigationActionPolicy.CANCEL; } @@ -431,7 +456,8 @@ class _DashboardState extends TbContextState { }, onConsoleMessage: (controller, consoleMessage) { log.debug( - '[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}'); + '[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}', + ); }, onLoadStart: (controller, url) async { log.debug('onLoadStart: $url'); @@ -446,10 +472,12 @@ class _DashboardState extends TbContextState { androidOnPermissionRequest: (controller, origin, resources) async { log.debug( - 'androidOnPermissionRequest origin: $origin, resources: $resources'); + 'androidOnPermissionRequest origin: $origin, resources: $resources', + ); return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT); + resources: resources, + action: PermissionRequestResponseAction.GRANT, + ); }, ), if (!UniversalPlatform.isWeb) @@ -458,7 +486,7 @@ class _DashboardState extends TbContextState { secondValueListenable: dashboardActive, builder: (context, loading, active, child) { if (!loading && active) { - return SizedBox.shrink(); + return const SizedBox.shrink(); } else { var data = MediaQuery.of(context); var bottomPadding = data.padding.top; @@ -469,11 +497,11 @@ class _DashboardState extends TbContextState { padding: EdgeInsets.only(bottom: bottomPadding), alignment: Alignment.center, color: Colors.white, - child: TbProgressIndicator(size: 50.0), + child: const TbProgressIndicator(size: 50.0), ); } }, - ) + ), ], ); } @@ -485,10 +513,10 @@ class _DashboardState extends TbContextState { SnackBar _buildWarnSnackBar(String message) { return SnackBar( duration: const Duration(seconds: 10), - backgroundColor: Color(0xFFdc6d1b), + backgroundColor: const Color(0xFFdc6d1b), content: Text( message, - style: TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), ), action: SnackBarAction( label: 'Close', diff --git a/lib/modules/dashboard/dashboard_page.dart b/lib/modules/dashboard/dashboard_page.dart index b3707360..470f29c9 100644 --- a/lib/modules/dashboard/dashboard_page.dart +++ b/lib/modules/dashboard/dashboard_page.dart @@ -5,24 +5,23 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class DashboardPage extends TbPageWidget { final String? _dashboardTitle; + // final String? _dashboardId; // final String? _state; // final bool? _fullscreen; - DashboardPage(TbContext tbContext, - {String? dashboardId, - bool? fullscreen, - String? dashboardTitle, - String? state}) - : - // _dashboardId = dashboardId, - // _fullscreen = fullscreen, - _dashboardTitle = dashboardTitle, - // _state = state, + DashboardPage( + TbContext tbContext, { + String? dashboardId, + bool? fullscreen, + String? dashboardTitle, + String? state, + super.key, + }) : _dashboardTitle = dashboardTitle, super(tbContext); @override - _DashboardPageState createState() => _DashboardPageState(); + State createState() => _DashboardPageState(); } class _DashboardPageState extends TbPageState { @@ -37,26 +36,22 @@ class _DashboardPageState extends TbPageState { @override Widget build(BuildContext context) { return Scaffold( - appBar: TbAppBar( - tbContext, - showLoadingIndicator: false, - elevation: 0, - title: ValueListenableBuilder( - valueListenable: dashboardTitleValue, - builder: (context, title, widget) { - return FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text(title)); - }, - ), + appBar: TbAppBar( + tbContext, + showLoadingIndicator: false, + elevation: 0, + title: ValueListenableBuilder( + valueListenable: dashboardTitleValue, + builder: (context, title, widget) { + return FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text(title), + ); + }, ), - body: Text( - 'Deprecated') //Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state, - //fullscreen: widget._fullscreen, titleCallback: (title) { - //dashboardTitleValue.value = title; - //} - //), - ); + ), + body: const Text('Deprecated'), + ); } } diff --git a/lib/modules/dashboard/dashboard_routes.dart b/lib/modules/dashboard/dashboard_routes.dart index 71e5a9b6..90215284 100644 --- a/lib/modules/dashboard/dashboard_routes.dart +++ b/lib/modules/dashboard/dashboard_routes.dart @@ -8,35 +8,42 @@ import 'package:thingsboard_app/modules/dashboard/fullscreen_dashboard_page.dart import 'dashboard_page.dart'; class DashboardRoutes extends TbRoutes { - late var dashboardsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return DashboardsPage(tbContext); - }); + late final dashboardsHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return DashboardsPage(tbContext); + }, + ); - late var dashboardDetailsHandler = Handler( - handlerFunc: (BuildContext? context, Map> params) { - var fullscreen = params['fullscreen']?.first == 'true'; - var dashboardTitle = params['title']?.first; - var state = params['state']?.first; - return DashboardPage(tbContext, - dashboardId: params["id"]![0], + late final dashboardDetailsHandler = Handler( + handlerFunc: (BuildContext? context, Map> params) { + var fullscreen = params['fullscreen']?.first == 'true'; + var dashboardTitle = params['title']?.first; + var state = params['state']?.first; + return DashboardPage( + tbContext, + dashboardId: params['id']![0], fullscreen: fullscreen, dashboardTitle: dashboardTitle, - state: state); - }); + state: state, + ); + }, + ); - late var fullscreenDashboardHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return FullscreenDashboardPage(tbContext, params["id"]![0]); - }); + late final fullscreenDashboardHandler = Handler( + handlerFunc: (BuildContext? context, Map 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); + router.define('/dashboards', handler: dashboardsHandler); + router.define('/dashboard/:id', handler: dashboardDetailsHandler); + router.define( + '/fullscreenDashboard/:id', + handler: fullscreenDashboardHandler, + ); } } diff --git a/lib/modules/dashboard/dashboards_base.dart b/lib/modules/dashboard/dashboards_base.dart index 3328bc5f..fbc19730 100644 --- a/lib/modules/dashboard/dashboards_base.dart +++ b/lib/modules/dashboard/dashboards_base.dart @@ -23,8 +23,10 @@ mixin DashboardsBase on EntitiesBase { .getTenantDashboards(pageLink, mobile: true); } else { return tbClient.getDashboardService().getCustomerDashboards( - tbClient.getAuthUser()!.customerId!, pageLink, - mobile: true); + tbClient.getAuthUser()!.customerId!, + pageLink, + mobile: true, + ); } } @@ -42,7 +44,9 @@ mixin DashboardsBase on EntitiesBase { @override Widget buildEntityListWidgetCard( - BuildContext context, DashboardInfo dashboard) { + BuildContext context, + DashboardInfo dashboard, + ) { return _buildEntityListCard(context, dashboard, true); } @@ -56,61 +60,80 @@ mixin DashboardsBase on EntitiesBase { } Widget _buildEntityListCard( - BuildContext context, DashboardInfo dashboard, bool listWidgetCard) { + BuildContext context, + DashboardInfo dashboard, + bool listWidgetCard, + ) { return Row( - mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max, - children: [ - Flexible( - fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, - child: Container( - padding: EdgeInsets.symmetric( - vertical: listWidgetCard ? 9 : 10, horizontal: 16), - child: Row( - mainAxisSize: - listWidgetCard ? MainAxisSize.min : MainAxisSize.max, - children: [ - Flexible( - fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text('${dashboard.title}', - style: TextStyle( - color: Color(0xFF282828), - fontSize: 14, - fontWeight: FontWeight.w500, - height: 1.7))), - Text('${_dashboardDetailsText(dashboard)}', - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33)) - ], - )), - (!listWidgetCard - ? Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - entityDateFormat.format( - DateTime.fromMillisecondsSinceEpoch( - dashboard.createdTime!)), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33)) - ], - ) - : Container()) - ], + mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max, + children: [ + Flexible( + fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, + child: Container( + padding: EdgeInsets.symmetric( + vertical: listWidgetCard ? 9 : 10, + horizontal: 16, + ), + child: Row( + mainAxisSize: + listWidgetCard ? MainAxisSize.min : MainAxisSize.max, + children: [ + Flexible( + fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text( + dashboard.title, + style: const TextStyle( + color: Color(0xFF282828), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.7, + ), + ), + ), + Text( + _dashboardDetailsText(dashboard), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 1.33, + ), + ), + ], + ), ), - )) - ]); + (!listWidgetCard + ? Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + entityDateFormat.format( + DateTime.fromMillisecondsSinceEpoch( + dashboard.createdTime!, + ), + ), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 1.33, + ), + ), + ], + ) + : Container()), + ], + ), + ), + ), + ], + ); } String _dashboardDetailsText(DashboardInfo dashboard) { @@ -132,21 +155,16 @@ mixin DashboardsBase on EntitiesBase { class DashboardGridCard extends TbContextWidget { final DashboardInfo dashboard; - DashboardGridCard(TbContext tbContext, {required this.dashboard}) + DashboardGridCard(TbContext tbContext, {required this.dashboard, super.key}) : super(tbContext); @override - _DashboardGridCardState createState() => _DashboardGridCardState(); + State createState() => _DashboardGridCardState(); } class _DashboardGridCardState extends TbContextState { _DashboardGridCardState() : super(); - @override - void initState() { - super.initState(); - } - @override void didUpdateWidget(DashboardGridCard oldWidget) { super.didUpdateWidget(oldWidget); @@ -160,42 +178,55 @@ class _DashboardGridCardState extends TbContextState { image = Utils.imageFromTbImage(context, tbClient, widget.dashboard.image!); } else { - image = SvgPicture.asset(ThingsboardImage.dashboardPlaceholder, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.overlay), - semanticsLabel: 'Dashboard'); + image = SvgPicture.asset( + ThingsboardImage.dashboardPlaceholder, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.overlay, + ), + semanticsLabel: 'Dashboard', + ); } return ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Column( - children: [ - Expanded( - child: Stack(children: [ - SizedBox.expand( + borderRadius: BorderRadius.circular(4), + child: Column( + children: [ + Expanded( + child: Stack( + children: [ + SizedBox.expand( child: FittedBox( - clipBehavior: Clip.hardEdge, - fit: BoxFit.cover, - child: image)) - ])), - 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), - ))), - ) - ], - )); + clipBehavior: Clip.hardEdge, + fit: BoxFit.cover, + child: image, + ), + ), + ], + ), + ), + const Divider(height: 1), + SizedBox( + height: 44, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Center( + child: AutoSizeText( + widget.dashboard.title, + textAlign: TextAlign.center, + maxLines: 1, + minFontSize: 12, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + ), + ), + ], + ), + ); } } diff --git a/lib/modules/dashboard/dashboards_grid.dart b/lib/modules/dashboard/dashboards_grid.dart index fa255d64..f8d3b8f4 100644 --- a/lib/modules/dashboard/dashboards_grid.dart +++ b/lib/modules/dashboard/dashboards_grid.dart @@ -8,10 +8,10 @@ import 'package:thingsboard_client/thingsboard_client.dart'; import 'dashboards_base.dart'; class DashboardsGridWidget extends TbContextWidget { - DashboardsGridWidget(TbContext tbContext) : super(tbContext); + DashboardsGridWidget(TbContext tbContext, {super.key}) : super(tbContext); @override - _DashboardsGridWidgetState createState() => _DashboardsGridWidgetState(); + State createState() => _DashboardsGridWidgetState(); } class _DashboardsGridWidgetState extends TbContextState { @@ -19,7 +19,10 @@ class _DashboardsGridWidgetState extends TbContextState { @override Widget build(BuildContext context) { - return DashboardsGrid(tbContext, _pageLinkController); + return DashboardsGrid( + tbContext, + _pageLinkController, + ); } @override @@ -32,6 +35,8 @@ class _DashboardsGridWidgetState extends TbContextState { class DashboardsGrid extends BaseEntitiesWidget with DashboardsBase, EntitiesGridStateBase { DashboardsGrid( - TbContext tbContext, PageKeyController pageKeyController) - : super(tbContext, pageKeyController); + TbContext tbContext, + PageKeyController pageKeyController, { + super.key, + }) : super(tbContext, pageKeyController); } diff --git a/lib/modules/dashboard/dashboards_list.dart b/lib/modules/dashboard/dashboards_list.dart index 159ff0c0..6874f9a4 100644 --- a/lib/modules/dashboard/dashboards_list.dart +++ b/lib/modules/dashboard/dashboards_list.dart @@ -1,6 +1,6 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:thingsboard_app/core/entity/entities_list.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/core/entity/entities_list.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; import 'dashboards_base.dart'; @@ -8,6 +8,8 @@ import 'dashboards_base.dart'; class DashboardsList extends BaseEntitiesWidget with DashboardsBase, EntitiesListStateBase { DashboardsList( - TbContext tbContext, PageKeyController pageKeyController) - : super(tbContext, pageKeyController); + TbContext tbContext, + PageKeyController pageKeyController, { + super.key, + }) : super(tbContext, pageKeyController); } diff --git a/lib/modules/dashboard/dashboards_list_widget.dart b/lib/modules/dashboard/dashboards_list_widget.dart index a62f59fc..3cfd49ce 100644 --- a/lib/modules/dashboard/dashboards_list_widget.dart +++ b/lib/modules/dashboard/dashboards_list_widget.dart @@ -5,9 +5,11 @@ import 'package:thingsboard_client/thingsboard_client.dart'; class DashboardsListWidget extends EntitiesListPageLinkWidget with DashboardsBase { - DashboardsListWidget(TbContext tbContext, - {EntitiesListWidgetController? controller}) - : super(tbContext, controller: controller); + DashboardsListWidget( + TbContext tbContext, { + super.key, + EntitiesListWidgetController? controller, + }) : super(tbContext, controller: controller); @override void onViewAll() { diff --git a/lib/modules/dashboard/dashboards_page.dart b/lib/modules/dashboard/dashboards_page.dart index 9a1a5e6b..867b3db4 100644 --- a/lib/modules/dashboard/dashboards_page.dart +++ b/lib/modules/dashboard/dashboards_page.dart @@ -6,22 +6,18 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'dashboards_grid.dart'; class DashboardsPage extends TbPageWidget { - DashboardsPage(TbContext tbContext) : super(tbContext); + DashboardsPage(TbContext tbContext, {super.key}) : super(tbContext); @override - _DashboardsPageState createState() => _DashboardsPageState(); + State createState() => _DashboardsPageState(); } class _DashboardsPageState extends TbPageState { @override Widget build(BuildContext context) { return Scaffold( - appBar: TbAppBar(tbContext, title: Text('Dashboards')), - body: DashboardsGridWidget(tbContext)); - } - - @override - void dispose() { - super.dispose(); + appBar: TbAppBar(tbContext, title: const Text('Dashboards')), + body: DashboardsGridWidget(tbContext), + ); } } diff --git a/lib/modules/dashboard/fullscreen_dashboard_page.dart b/lib/modules/dashboard/fullscreen_dashboard_page.dart index 8bb0455d..daca6b4e 100644 --- a/lib/modules/dashboard/fullscreen_dashboard_page.dart +++ b/lib/modules/dashboard/fullscreen_dashboard_page.dart @@ -10,14 +10,16 @@ class FullscreenDashboardPage extends TbPageWidget { final String fullscreenDashboardId; final String? _dashboardTitle; - FullscreenDashboardPage(TbContext tbContext, this.fullscreenDashboardId, - {String? dashboardTitle}) - : _dashboardTitle = dashboardTitle, + FullscreenDashboardPage( + TbContext tbContext, + this.fullscreenDashboardId, { + String? dashboardTitle, + super.key, + }) : _dashboardTitle = dashboardTitle, super(tbContext); @override - _FullscreenDashboardPageState createState() => - _FullscreenDashboardPageState(); + State createState() => _FullscreenDashboardPageState(); } class _FullscreenDashboardPageState @@ -31,11 +33,6 @@ class _FullscreenDashboardPageState dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard'); } - @override - void dispose() { - super.dispose(); - } - _onCanGoBack(bool canGoBack) { showBackValue.value = canGoBack; } @@ -44,34 +41,41 @@ class _FullscreenDashboardPageState Widget build(BuildContext context) { return Scaffold( appBar: PreferredSize( - preferredSize: Size.fromHeight(kToolbarHeight), + preferredSize: const Size.fromHeight(kToolbarHeight), child: ValueListenableBuilder( - valueListenable: showBackValue, - builder: (context, canGoBack, widget) { - return TbAppBar(tbContext, - leading: canGoBack - ? BackButton(onPressed: () { - maybePop(); - }) - : null, - showLoadingIndicator: false, - elevation: 1, - shadowColor: Colors.transparent, - title: ValueListenableBuilder( - 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')) - ]); - }), + valueListenable: showBackValue, + builder: (context, canGoBack, widget) { + return TbAppBar( + tbContext, + leading: canGoBack + ? BackButton( + onPressed: () { + maybePop(); + }, + ) + : null, + showLoadingIndicator: false, + elevation: 1, + shadowColor: Colors.transparent, + title: ValueListenableBuilder( + valueListenable: dashboardTitleValue, + builder: (context, title, widget) { + return FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text(title), + ); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () => navigateTo('/profile?fullscreen=true'), + ), + ], + ); + }, + ), ), body: ValueListenableBuilder( valueListenable: getIt().listenEndpointChanges, diff --git a/lib/modules/dashboard/main_dashboard_page.dart b/lib/modules/dashboard/main_dashboard_page.dart index 5468c1b5..a6e94624 100644 --- a/lib/modules/dashboard/main_dashboard_page.dart +++ b/lib/modules/dashboard/main_dashboard_page.dart @@ -26,13 +26,20 @@ class MainDashboardPageController { } } - Future openDashboard(String dashboardId, - {String? dashboardTitle, String? state, bool? hideToolbar}) async { + Future openDashboard( + String dashboardId, { + String? dashboardTitle, + String? state, + bool? hideToolbar, + }) async { if (dashboardTitle != null) { _mainDashboardPageState?._updateTitle(dashboardTitle); } - await _dashboardController?.openDashboard(dashboardId, - state: state, hideToolbar: hideToolbar); + await _dashboardController?.openDashboard( + dashboardId, + state: state, + hideToolbar: hideToolbar, + ); } Future activateDashboard() async { @@ -48,14 +55,17 @@ class MainDashboardPage extends TbContextWidget { final String? _dashboardTitle; final MainDashboardPageController? _controller; - MainDashboardPage(TbContext tbContext, - {MainDashboardPageController? controller, String? dashboardTitle}) - : _controller = controller, + MainDashboardPage( + TbContext tbContext, { + MainDashboardPageController? controller, + String? dashboardTitle, + super.key, + }) : _controller = controller, _dashboardTitle = dashboardTitle, super(tbContext); @override - _MainDashboardPageState createState() => _MainDashboardPageState(); + State createState() => _MainDashboardPageState(); } class _MainDashboardPageState extends TbContextState @@ -71,10 +81,12 @@ class _MainDashboardPageState extends TbContextState super.initState(); rightLayoutMenuController = AnimationController( vsync: this, - duration: Duration(milliseconds: 200), + duration: const Duration(milliseconds: 200), ); rightLayoutMenuAnimation = CurvedAnimation( - curve: Curves.linear, parent: rightLayoutMenuController); + curve: Curves.linear, + parent: rightLayoutMenuController, + ); if (widget._controller != null) { widget._controller!._setMainDashboardPageState(this); } @@ -113,8 +125,8 @@ class _MainDashboardPageState extends TbContextState actions: [ ValueListenableBuilder( valueListenable: hasRightLayout, - builder: (context, _hasRightLayout, widget) { - if (_hasRightLayout) { + builder: (context, hasRightLayout, widget) { + if (hasRightLayout) { return IconButton( onPressed: () => _dashboardController?.toggleRightLayout(), icon: AnimatedIcon( @@ -126,7 +138,7 @@ class _MainDashboardPageState extends TbContextState return const SizedBox.shrink(); } }, - ) + ), ], ), body: ValueListenableBuilder( diff --git a/lib/modules/device/device_details_page.dart b/lib/modules/device/device_details_page.dart index c2c96555..ab18edb0 100644 --- a/lib/modules/device/device_details_page.dart +++ b/lib/modules/device/device_details_page.dart @@ -4,19 +4,19 @@ import 'package:thingsboard_app/core/entity/entity_details_page.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class DeviceDetailsPage extends EntityDetailsPage { - DeviceDetailsPage(TbContext tbContext, String deviceId) + DeviceDetailsPage(TbContext tbContext, String deviceId, {super.key}) : super(tbContext, entityId: deviceId, defaultTitle: 'Device'); @override - Future fetchEntity(String deviceId) { - return tbClient.getDeviceService().getDeviceInfo(deviceId); + Future fetchEntity(String id) { + return tbClient.getDeviceService().getDeviceInfo(id); } @override - Widget buildEntityDetails(BuildContext context, DeviceInfo device) { + Widget buildEntityDetails(BuildContext context, DeviceInfo entity) { return ListTile( - title: Text('${device.name}'), - subtitle: Text('${device.type}'), + title: Text(entity.name), + subtitle: Text(entity.type), ); } } diff --git a/lib/modules/device/device_profiles_base.dart b/lib/modules/device/device_profiles_base.dart index b7b2850a..29e87148 100644 --- a/lib/modules/device/device_profiles_base.dart +++ b/lib/modules/device/device_profiles_base.dart @@ -48,7 +48,9 @@ mixin DeviceProfilesBase on EntitiesBase { @override Widget buildEntityGridCard( - BuildContext context, DeviceProfileInfo deviceProfile) { + BuildContext context, + DeviceProfileInfo deviceProfile, + ) { return DeviceProfileCard(tbContext, deviceProfile); } @@ -65,11 +67,11 @@ class RefreshDeviceCounts { class AllDevicesCard extends TbContextWidget { final RefreshDeviceCounts refreshDeviceCounts; - AllDevicesCard(TbContext tbContext, this.refreshDeviceCounts) + AllDevicesCard(TbContext tbContext, this.refreshDeviceCounts, {super.key}) : super(tbContext); @override - _AllDevicesCardState createState() => _AllDevicesCardState(); + State createState() => _AllDevicesCardState(); } class _AllDevicesCardState extends TbContextState { @@ -109,7 +111,7 @@ class _AllDevicesCardState extends TbContextState { Future> countsFuture = Future.wait([activeDevicesCount, inactiveDevicesCount]); countsFuture.then((counts) { - if (this.mounted) { + if (mounted) { _activeDevicesCount.add(counts[0]); _inactiveDevicesCount.add(counts[1]); } @@ -120,143 +122,165 @@ class _AllDevicesCardState extends TbContextState { @override Widget build(BuildContext context) { return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - child: Card( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), + behavior: HitTestBehavior.opaque, + child: Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha((255 * 0.05).ceil()), + blurRadius: 6.0, + offset: const Offset(0, 4), + ), + ], + ), + child: Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + elevation: 0, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 15), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).allDevices, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, + ), + ), + const Icon(Icons.arrow_forward, size: 18), + ], + ), ), - elevation: 0, - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(16, 12, 16, 15), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('${S.of(context).allDevices}', - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14)), - Icon(Icons.arrow_forward, size: 18) - ], - )), - Divider(height: 1), - Padding( - padding: EdgeInsets.all(0), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - fit: FlexFit.tight, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - height: 40, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + fit: FlexFit.tight, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + child: StreamBuilder( + stream: _activeDevicesCount.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + var deviceCount = snapshot.data!; + return _buildDeviceCount( + context, + true, + deviceCount, + ); + } else { + return Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Theme.of( + tbContext.currentState!.context, + ).colorScheme.primary, + ), + strokeWidth: 2.5, ), - child: StreamBuilder( - stream: _activeDevicesCount.stream, - builder: (context, snapshot) { - if (snapshot.hasData) { - 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'); - }), + ), + ); + } + }, + ), + ), + onTap: () { + navigateTo('/deviceList?active=true'); + }, + ), + ), + // SizedBox(width: 4), + const SizedBox( + width: 1, + height: 40, + child: VerticalDivider(width: 1), + ), + Flexible( + fit: FlexFit.tight, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), ), - // SizedBox(width: 4), - Container( - width: 1, - height: 40, - child: VerticalDivider(width: 1)), - Flexible( - fit: FlexFit.tight, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - height: 40, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), + child: StreamBuilder( + stream: _inactiveDevicesCount.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + var deviceCount = snapshot.data!; + return _buildDeviceCount( + context, + false, + deviceCount, + ); + } else { + return Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Theme.of( + tbContext.currentState!.context, + ).colorScheme.primary, + ), + strokeWidth: 2.5, ), - child: StreamBuilder( - stream: _inactiveDevicesCount.stream, - builder: (context, snapshot) { - if (snapshot.hasData) { - 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'); - }), - ) - ], - )) - ], - )), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha((255 * 0.05).ceil()), - blurRadius: 6.0, - offset: Offset(0, 4)) + ), + ); + } + }, + ), + ), + onTap: () { + navigateTo('/deviceList?active=false'); + }, + ), + ), + ], + ), + ), ], ), ), - onTap: () { - navigateTo('/deviceList'); - }); + ), + onTap: () { + navigateTo('/deviceList'); + }, + ); } } class DeviceProfileCard extends TbContextWidget { final DeviceProfileInfo deviceProfile; - DeviceProfileCard(TbContext tbContext, this.deviceProfile) : super(tbContext); + DeviceProfileCard(TbContext tbContext, this.deviceProfile, {super.key}) + : super(tbContext); @override - _DeviceProfileCardState createState() => _DeviceProfileCardState(); + State createState() => _DeviceProfileCardState(); } class _DeviceProfileCardState extends TbContextState { @@ -276,10 +300,16 @@ class _DeviceProfileCardState extends TbContextState { } _countDevices() { - activeDevicesCount = EntityQueryApi.countDevices(tbClient, - deviceType: widget.deviceProfile.name, active: true); - inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, - deviceType: widget.deviceProfile.name, active: false); + activeDevicesCount = EntityQueryApi.countDevices( + tbClient, + deviceType: widget.deviceProfile.name, + active: true, + ); + inactiveDevicesCount = EntityQueryApi.countDevices( + tbClient, + deviceType: widget.deviceProfile.name, + active: false, + ); } @override @@ -294,109 +324,139 @@ class _DeviceProfileCardState extends TbContextState { imageFit = BoxFit.contain; padding = 8; } else { - image = SvgPicture.asset(ThingsboardImage.deviceProfilePlaceholder, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.overlay), - semanticsLabel: 'Device profile'); + image = SvgPicture.asset( + ThingsboardImage.deviceProfilePlaceholder, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.overlay, + ), + semanticsLabel: 'Device profile', + ); imageFit = BoxFit.cover; padding = 0; } return ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Column(children: [ + borderRadius: BorderRadius.circular(4), + child: Column( + children: [ Expanded( - child: Stack(children: [ - SizedBox.expand( - child: Padding( + child: Stack( + children: [ + SizedBox.expand( + child: Padding( padding: EdgeInsets.all(padding), child: FittedBox( - clipBehavior: Clip.hardEdge, - fit: imageFit, - child: image))) - ])), - 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( - 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)))); - } - }, + clipBehavior: Clip.hardEdge, + fit: imageFit, + child: image, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 44, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Center( + child: AutoSizeText( + entity.name, + textAlign: TextAlign.center, + maxLines: 1, + minFontSize: 12, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, + ), + ), ), - onTap: () { - navigateTo('/deviceList?active=true&deviceType=${entity.name}'); - }), - Divider(height: 1), + ), + ), + const Divider(height: 1), GestureDetector( - behavior: HitTestBehavior.opaque, - child: FutureBuilder( - 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}'); - }) - ])); + behavior: HitTestBehavior.opaque, + child: FutureBuilder( + future: activeDevicesCount, + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, true, deviceCount); + } else { + return SizedBox( + height: 40, + child: Center( + child: SizedBox( + 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}'); + }, + ), + const Divider(height: 1), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: FutureBuilder( + future: inactiveDevicesCount, + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, false, deviceCount); + } else { + return SizedBox( + height: 40, + child: Center( + child: SizedBox( + 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) { - Color color = active ? Color(0xFF008A00) : Color(0xFFAFAFAF); + Color color = active ? const Color(0xFF008A00) : const Color(0xFFAFAFAF); return Padding( - padding: EdgeInsets.all(12), + padding: const EdgeInsets.all(12), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -409,31 +469,34 @@ Widget _buildDeviceCount(BuildContext context, bool active, int count) { Icon(Icons.devices_other, size: 16, color: color), if (!active) CustomPaint( - size: Size.square(16), + size: const Size.square(16), painter: StrikeThroughPainter(color: color, offset: 2), - ) + ), ], ), - SizedBox(width: 8.67), + const SizedBox(width: 8.67), Text( - active - ? '${S.of(context).active}' - : '${S.of(context).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)) + active ? S.of(context).active : S.of(context).inactive, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 16 / 12, + color: color, + ), + ), + const SizedBox(width: 8.67), + Text( + count.toString(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 16 / 12, + color: color, + ), + ), ], ), - Icon(Icons.chevron_right, size: 16, color: Color(0xFFACACAC)) + const Icon(Icons.chevron_right, size: 16, color: Color(0xFFACACAC)), ], ), ); @@ -449,10 +512,17 @@ class StrikeThroughPainter extends CustomPainter { void paint(Canvas canvas, Size size) { final paint = Paint()..color = color; paint.strokeWidth = 1.5; - canvas.drawLine(Offset(offset, offset), - Offset(size.width - offset, size.height - offset), paint); + canvas.drawLine( + Offset(offset, offset), + Offset(size.width - offset, size.height - offset), + paint, + ); paint.color = Colors.white; - canvas.drawLine(Offset(2, 0), Offset(size.width + 2, size.height), paint); + canvas.drawLine( + const Offset(2, 0), + Offset(size.width + 2, size.height), + paint, + ); } @override diff --git a/lib/modules/device/device_profiles_grid.dart b/lib/modules/device/device_profiles_grid.dart index 29538393..812f4f89 100644 --- a/lib/modules/device/device_profiles_grid.dart +++ b/lib/modules/device/device_profiles_grid.dart @@ -8,6 +8,8 @@ import 'device_profiles_base.dart'; class DeviceProfilesGrid extends BaseEntitiesWidget with DeviceProfilesBase, EntitiesGridStateBase { DeviceProfilesGrid( - TbContext tbContext, PageKeyController pageKeyController) - : super(tbContext, pageKeyController); + TbContext tbContext, + PageKeyController pageKeyController, { + super.key, + }) : super(tbContext, pageKeyController); } diff --git a/lib/modules/device/device_routes.dart b/lib/modules/device/device_routes.dart index 2495e60e..3101c1ca 100644 --- a/lib/modules/device/device_routes.dart +++ b/lib/modules/device/device_routes.dart @@ -9,38 +9,46 @@ import 'device_details_page.dart'; import 'devices_list_page.dart'; class DeviceRoutes extends TbRoutes { - late var devicesHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return MainPage(tbContext, path: '/devices'); - }); + late final devicesHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return MainPage(tbContext, path: '/devices'); + }, + ); - late var devicesPageHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return DevicesPage(tbContext); - }); + late final devicesPageHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return DevicesPage(tbContext); + }, + ); - late var deviceListHandler = Handler( - handlerFunc: (BuildContext? context, Map 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 DevicesListPage(tbContext, - searchMode: searchMode, deviceType: deviceType, active: active); - }); + late final deviceListHandler = Handler( + handlerFunc: (BuildContext? context, Map 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 DevicesListPage( + tbContext, + searchMode: searchMode, + deviceType: deviceType, + active: active, + ); + }, + ); - late var deviceDetailsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return DeviceDetailsPage(tbContext, params["id"][0]); - }); + late final deviceDetailsHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return DeviceDetailsPage(tbContext, params['id'][0]); + }, + ); DeviceRoutes(TbContext tbContext) : super(tbContext); @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); + router.define('/devices', handler: devicesHandler); + router.define('/devicesPage', handler: devicesPageHandler); + router.define('/deviceList', handler: deviceListHandler); + router.define('/device/:id', handler: deviceDetailsHandler); } } diff --git a/lib/modules/device/devices_base.dart b/lib/modules/device/devices_base.dart index 57893e76..aa0c6c35 100644 --- a/lib/modules/device/devices_base.dart +++ b/lib/modules/device/devices_base.dart @@ -28,18 +28,27 @@ mixin DevicesBase on EntitiesBase { @override void onEntityTap(EntityData device) async { var profile = await DeviceProfileCache.getDeviceProfileInfo( - tbClient, device.field('type')!, device.entityId.id!); + tbClient, + device.field('type')!, + device.entityId.id!, + ); if (profile.defaultDashboardId != null) { var dashboardId = profile.defaultDashboardId!.id!; - var state = Utils.createDashboardEntityState(device.entityId, - entityName: device.field('name')!, - entityLabel: device.field('label')!); - navigateToDashboard(dashboardId, - dashboardTitle: device.field('name'), state: state); + var state = Utils.createDashboardEntityState( + device.entityId, + entityName: device.field('name')!, + entityLabel: device.field('label')!, + ); + navigateToDashboard( + dashboardId, + dashboardTitle: device.field('name'), + state: state, + ); } else { if (tbClient.isTenantAdmin()) { showWarnNotification( - 'Mobile dashboard should be configured in device profile!'); + 'Mobile dashboard should be configured in device profile!', + ); } } } @@ -62,26 +71,36 @@ mixin DevicesBase on EntitiesBase { bool displayCardImage(bool listWidgetCard) => listWidgetCard; Widget _buildEntityListCard( - BuildContext context, EntityData device, bool listWidgetCard) { - return DeviceCard(tbContext, - device: device, - listWidgetCard: listWidgetCard, - displayImage: displayCardImage(listWidgetCard)); + BuildContext context, + EntityData device, + bool listWidgetCard, + ) { + return DeviceCard( + tbContext, + device: device, + listWidgetCard: listWidgetCard, + displayImage: displayCardImage(listWidgetCard), + ); } } class DeviceQueryController extends PageKeyController { - DeviceQueryController( - {int pageSize = 20, String? searchText, String? deviceType, bool? active}) - : super(EntityQueryApi.createDefaultDeviceQuery( + DeviceQueryController({ + int pageSize = 20, + String? searchText, + String? deviceType, + bool? active, + }) : super( + EntityQueryApi.createDefaultDeviceQuery( pageSize: pageSize, searchText: searchText, deviceType: deviceType, - active: active)); + active: active, + ), + ); @override - EntityDataQuery nextPageKey(EntityDataQuery deviceQuery) => - deviceQuery.next(); + EntityDataQuery nextPageKey(EntityDataQuery pageKey) => pageKey.next(); onSearchText(String searchText) { value.pageKey.pageLink.page = 0; @@ -95,14 +114,16 @@ class DeviceCard extends TbContextWidget { final bool listWidgetCard; final bool displayImage; - DeviceCard(TbContext tbContext, - {required this.device, - this.listWidgetCard = false, - this.displayImage = false}) - : super(tbContext); + DeviceCard( + TbContext tbContext, { + super.key, + required this.device, + this.listWidgetCard = false, + this.displayImage = false, + }) : super(tbContext); @override - _DeviceCardState createState() => _DeviceCardState(); + State createState() => _DeviceCardState(); } class _DeviceCardState extends TbContextState { @@ -115,7 +136,10 @@ class _DeviceCardState extends TbContextState { super.initState(); if (widget.displayImage || !widget.listWidgetCard) { deviceProfileFuture = DeviceProfileCache.getDeviceProfileInfo( - tbClient, widget.device.field('type')!, widget.device.entityId.id!); + tbClient, + widget.device.field('type')!, + widget.device.entityId.id!, + ); } } @@ -127,7 +151,10 @@ class _DeviceCardState extends TbContextState { var device = widget.device; if (oldDevice.field('type')! != device.field('type')!) { deviceProfileFuture = DeviceProfileCache.getDeviceProfileInfo( - tbClient, widget.device.field('type')!, widget.device.entityId.id!); + tbClient, + widget.device.field('type')!, + widget.device.entityId.id!, + ); } } } @@ -142,21 +169,26 @@ class _DeviceCardState extends TbContextState { } Widget buildCard(BuildContext context) { - return Stack(children: [ - Positioned.fill( + return Stack( + children: [ + Positioned.fill( child: Container( - alignment: Alignment.centerLeft, - child: Container( - width: 4, - decoration: BoxDecoration( - color: widget.device.attribute('active') == 'true' - ? Color(0xFF008A00) - : Color(0xFFAFAFAF), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4), - bottomLeft: Radius.circular(4))), - ))), - FutureBuilder( + alignment: Alignment.centerLeft, + child: Container( + width: 4, + decoration: BoxDecoration( + color: widget.device.attribute('active') == 'true' + ? const Color(0xFF008A00) + : const Color(0xFFAFAFAF), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + ), + ), + ), + FutureBuilder( future: deviceProfileFuture, builder: (context, snapshot) { if (snapshot.hasData && @@ -171,229 +203,285 @@ class _DeviceCardState extends TbContextState { imageFit = BoxFit.contain; } else { image = SvgPicture.asset( - ThingsboardImage.deviceProfilePlaceholder, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.overlay), - semanticsLabel: 'Device'); + ThingsboardImage.deviceProfilePlaceholder, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.overlay, + ), + semanticsLabel: 'Device', + ); imageFit = BoxFit.cover; } return Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(width: 20), - Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 20), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: 12), - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, + if (widget.displayImage) + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + child: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + child: Stack( + children: [ + Positioned.fill( + child: FittedBox( + fit: imageFit, + child: image, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 12), + Flexible( + fit: FlexFit.tight, + child: Column( children: [ - if (widget.displayImage) - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(4))), - child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(4)), - child: Stack( - children: [ - Positioned.fill( - child: FittedBox( - fit: imageFit, - child: image, - )) - ], - ))), - SizedBox(width: 12), - Flexible( - fit: FlexFit.tight, - child: Column(children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Flexible( - fit: FlexFit.tight, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: - Alignment.centerLeft, - child: Text( - '${widget.device.field('name')!}', - style: TextStyle( - color: Color( - 0xFF282828), - fontSize: 14, - fontWeight: - FontWeight - .w500, - height: - 20 / 14)))), - SizedBox(width: 12), - Text( - entityDateFormat.format(DateTime - .fromMillisecondsSinceEpoch( - widget.device - .createdTime!)), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: - FontWeight.normal, - height: 16 / 12)) - ]), - SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - '${widget.device.field('type')!}', - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: - FontWeight.normal, - height: 16 / 12)), - Text( - widget.device.attribute( - 'active') == - 'true' - ? '${S.of(context).active}' - : '${S.of(context).inactive}', - style: TextStyle( - color: widget.device - .attribute( - 'active') == - 'true' - ? Color(0xFF008A00) - : Color(0xFFAFAFAF), - fontSize: 12, - height: 16 / 12, - fontWeight: FontWeight.normal, - )) - ], - ) - ])), - SizedBox(width: 16), - if (hasDashboard) - Icon(Icons.chevron_right, - color: Color(0xFFACACAC)), - if (hasDashboard) SizedBox(width: 16), - ]), - SizedBox(height: 12) + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + fit: FlexFit.tight, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + widget.device.field('name')!, + style: const TextStyle( + color: Color( + 0xFF282828, + ), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 20 / 14, + ), + ), + ), + ), + const SizedBox(width: 12), + Text( + entityDateFormat.format( + DateTime.fromMillisecondsSinceEpoch( + widget.device.createdTime!, + ), + ), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.device.field('type')!, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + Text( + widget.device.attribute( + 'active', + ) == + 'true' + ? S.of(context).active + : S.of(context).inactive, + style: TextStyle( + color: widget.device.attribute( + 'active', + ) == + 'true' + ? const Color(0xFF008A00) + : const Color(0xFFAFAFAF), + fontSize: 12, + height: 16 / 12, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(width: 16), + if (hasDashboard) + const Icon( + Icons.chevron_right, + color: Color(0xFFACACAC), + ), + if (hasDashboard) const SizedBox(width: 16), ], - )) - ]); + ), + const SizedBox(height: 12), + ], + ), + ), + ], + ); } else { - return Container( - height: 64, - child: Center( - child: RefreshProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Theme.of(tbContext.currentState!.context) - .colorScheme - .primary)))); + return SizedBox( + height: 64, + child: Center( + child: RefreshProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Theme.of(tbContext.currentState!.context) + .colorScheme + .primary, + ), + ), + ), + ); } - }) - ]); + }, + ), + ], + ); } Widget buildListWidgetCard(BuildContext context) { - return Row(mainAxisSize: MainAxisSize.min, children: [ - if (widget.displayImage) - Container( - width: 58, - height: 58, - decoration: BoxDecoration( + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.displayImage) + Container( + width: 58, + height: 58, + decoration: const BoxDecoration( // color: Color(0xFFEEEEEE), - borderRadius: BorderRadius.horizontal(left: Radius.circular(4))), - child: FutureBuilder( - future: deviceProfileFuture, - builder: (context, snapshot) { - if (snapshot.hasData && - snapshot.connectionState == ConnectionState.done) { - var profile = snapshot.data!; - Widget image; - BoxFit imageFit; - if (profile.image != null) { - image = - Utils.imageFromTbImage(context, tbClient, profile.image!); - imageFit = BoxFit.contain; - } else { - image = SvgPicture.asset( + borderRadius: BorderRadius.horizontal(left: Radius.circular(4)), + ), + child: FutureBuilder( + future: deviceProfileFuture, + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + var profile = snapshot.data!; + Widget image; + BoxFit imageFit; + if (profile.image != null) { + image = Utils.imageFromTbImage( + context, + tbClient, + profile.image!, + ); + imageFit = BoxFit.contain; + } else { + image = SvgPicture.asset( ThingsboardImage.deviceProfilePlaceholder, colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.overlay), - semanticsLabel: 'Device'); - imageFit = BoxFit.cover; - } - return ClipRRect( + Theme.of(context).primaryColor, + BlendMode.overlay, + ), + semanticsLabel: 'Device', + ); + imageFit = BoxFit.cover; + } + return ClipRRect( borderRadius: - BorderRadius.horizontal(left: Radius.circular(4)), + const BorderRadius.horizontal(left: Radius.circular(4)), child: Stack( children: [ Positioned.fill( - child: FittedBox( - fit: imageFit, - child: image, - )) + child: FittedBox( + fit: imageFit, + child: image, + ), + ), ], - )); - } else { - return Center( + ), + ); + } else { + return Center( child: RefreshProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Theme.of(tbContext.currentState!.context) - .colorScheme - .primary))); - } - }, + valueColor: AlwaysStoppedAnimation( + Theme.of(tbContext.currentState!.context) + .colorScheme + .primary, + ), + ), + ); + } + }, + ), ), - ), - Flexible( + Flexible( fit: FlexFit.loose, child: Container( - padding: EdgeInsets.symmetric(vertical: 9, horizontal: 16), - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text('${widget.device.field('name')!}', - style: TextStyle( - color: Color(0xFF282828), - fontSize: 14, - fontWeight: FontWeight.w500, - height: 20 / 14))) - ]), - SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('${widget.device.field('type')!}', - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)), - ]) - ], - ))) - ]); + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 16), + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text( + widget.device.field('name')!, + style: const TextStyle( + color: Color(0xFF282828), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 20 / 14, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.device.field('type')!, + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); } } diff --git a/lib/modules/device/devices_list.dart b/lib/modules/device/devices_list.dart index 089d7cf0..e56a2ec7 100644 --- a/lib/modules/device/devices_list.dart +++ b/lib/modules/device/devices_list.dart @@ -9,9 +9,12 @@ class DevicesList extends BaseEntitiesWidget final bool displayDeviceImage; DevicesList( - TbContext tbContext, PageKeyController pageKeyController, - {searchMode = false, this.displayDeviceImage = false}) - : super(tbContext, pageKeyController, searchMode: searchMode); + TbContext tbContext, + PageKeyController pageKeyController, { + super.key, + searchMode = false, + this.displayDeviceImage = false, + }) : super(tbContext, pageKeyController, searchMode: searchMode); @override bool displayCardImage(bool listWidgetCard) => displayDeviceImage; diff --git a/lib/modules/device/devices_list_page.dart b/lib/modules/device/devices_list_page.dart index 27a3a3b7..e124b413 100644 --- a/lib/modules/device/devices_list_page.dart +++ b/lib/modules/device/devices_list_page.dart @@ -11,12 +11,16 @@ class DevicesListPage extends TbPageWidget { final bool? active; final bool searchMode; - DevicesListPage(TbContext tbContext, - {this.deviceType, this.active, this.searchMode = false}) - : super(tbContext); + DevicesListPage( + TbContext tbContext, { + this.deviceType, + this.active, + this.searchMode = false, + super.key, + }) : super(tbContext); @override - _DevicesListPageState createState() => _DevicesListPageState(); + State createState() => _DevicesListPageState(); } class _DevicesListPageState extends TbPageState { @@ -26,14 +30,19 @@ class _DevicesListPageState extends TbPageState { void initState() { super.initState(); _deviceQueryController = DeviceQueryController( - deviceType: widget.deviceType, active: widget.active); + deviceType: widget.deviceType, + active: widget.active, + ); } @override Widget build(BuildContext context) { - var devicesList = DevicesList(tbContext, _deviceQueryController, - searchMode: widget.searchMode, - displayDeviceImage: widget.deviceType == null); + var devicesList = DevicesList( + tbContext, + _deviceQueryController, + searchMode: widget.searchMode, + displayDeviceImage: widget.deviceType == null, + ); PreferredSizeWidget appBar; if (widget.searchMode) { appBar = TbAppSearchBar( @@ -44,49 +53,61 @@ class _DevicesListPageState extends TbPageState { } else { String titleText = widget.deviceType != null ? widget.deviceType! - : '${S.of(context).allDevices}'; + : S.of(context).allDevices; String? subTitleText; if (widget.active != null) { subTitleText = widget.active == true - ? '${S.of(context).active}' - : '${S.of(context).inactive}'; + ? S.of(context).active + : S.of(context).inactive; } - Column title = - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(titleText, + 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, + 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 - .titleLarge! - .color! - .withAlpha((0.38 * 255).ceil()), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12)) - ]); + color: Theme.of(context) + .primaryTextTheme + .titleLarge! + .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 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('&')}'); - }, - ) - ]); + appBar = TbAppBar( + tbContext, + title: title, + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + List 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); } diff --git a/lib/modules/device/devices_list_widget.dart b/lib/modules/device/devices_list_widget.dart index 035fe309..def3238a 100644 --- a/lib/modules/device/devices_list_widget.dart +++ b/lib/modules/device/devices_list_widget.dart @@ -6,9 +6,11 @@ import 'package:thingsboard_client/thingsboard_client.dart'; class DevicesListWidget extends EntitiesListWidget with DevicesBase { - DevicesListWidget(TbContext tbContext, - {EntitiesListWidgetController? controller}) - : super(tbContext, controller: controller); + DevicesListWidget( + TbContext tbContext, { + super.key, + EntitiesListWidgetController? controller, + }) : super(tbContext, controller: controller); @override void onViewAll() { diff --git a/lib/modules/device/devices_main_page.dart b/lib/modules/device/devices_main_page.dart index e6c4e807..dd15bd89 100644 --- a/lib/modules/device/devices_main_page.dart +++ b/lib/modules/device/devices_main_page.dart @@ -6,10 +6,10 @@ import 'package:thingsboard_app/modules/device/device_profiles_grid.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class DevicesMainPage extends TbContextWidget { - DevicesMainPage(TbContext tbContext) : super(tbContext); + DevicesMainPage(TbContext tbContext, {super.key}) : super(tbContext); @override - _DevicesMainPageState createState() => _DevicesMainPageState(); + State createState() => _DevicesMainPageState(); } class _DevicesMainPageState extends TbContextState @@ -24,10 +24,18 @@ class _DevicesMainPageState extends TbContextState @override Widget build(BuildContext context) { super.build(context); - var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController); + final deviceProfilesList = DeviceProfilesGrid( + tbContext, + _pageLinkController, + ); + return Scaffold( - appBar: TbAppBar(tbContext, title: Text(deviceProfilesList.title)), - body: deviceProfilesList); + appBar: TbAppBar( + tbContext, + title: Text(deviceProfilesList.title), + ), + body: deviceProfilesList, + ); } @override diff --git a/lib/modules/device/devices_page.dart b/lib/modules/device/devices_page.dart index bb7e295d..df707fd7 100644 --- a/lib/modules/device/devices_page.dart +++ b/lib/modules/device/devices_page.dart @@ -6,10 +6,10 @@ import 'package:thingsboard_app/modules/device/device_profiles_grid.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class DevicesPage extends TbPageWidget { - DevicesPage(TbContext tbContext) : super(tbContext); + DevicesPage(TbContext tbContext, {super.key}) : super(tbContext); @override - _DevicesPageState createState() => _DevicesPageState(); + State createState() => _DevicesPageState(); } class _DevicesPageState extends TbPageState { @@ -17,10 +17,17 @@ class _DevicesPageState extends TbPageState { @override Widget build(BuildContext context) { - var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController); + final deviceProfilesList = DeviceProfilesGrid( + tbContext, + _pageLinkController, + ); return Scaffold( - appBar: TbAppBar(tbContext, title: Text(deviceProfilesList.title)), - body: deviceProfilesList); + appBar: TbAppBar( + tbContext, + title: Text(deviceProfilesList.title), + ), + body: deviceProfilesList, + ); } @override diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index bc567d4a..5d0a4a4a 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -4,36 +4,26 @@ 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/modules/dashboard/dashboard.dart' - as dashboardUi; + as dashboard_ui; import 'package:thingsboard_app/modules/dashboard/dashboards_grid.dart'; import 'package:thingsboard_app/modules/tenant/tenants_widget.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class HomePage extends TbContextWidget { - HomePage(TbContext tbContext) : super(tbContext); + HomePage(TbContext tbContext, {super.key}) : super(tbContext); @override - _HomePageState createState() => _HomePageState(); + State createState() => _HomePageState(); } class _HomePageState extends TbContextState with AutomaticKeepAliveClientMixin { - @override - void initState() { - super.initState(); - } - @override bool get wantKeepAlive { return true; } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { super.build(context); @@ -44,34 +34,44 @@ class _HomePageState extends TbContextState tbContext, elevation: dashboardState ? 0 : 8, title: Center( - child: Container( - height: 24, - child: SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn), - semanticsLabel: 'ThingsBoard Logo'))), + child: SizedBox( + height: 24, + child: SvgPicture.asset( + ThingsboardImage.thingsBoardWithTitle, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.srcIn, + ), + semanticsLabel: 'ThingsBoard Logo', + ), + ), + ), actions: [ if (tbClient.isSystemAdmin()) IconButton( - icon: Icon(Icons.search), + icon: const Icon(Icons.search), onPressed: () { navigateTo('/tenants?search=true'); }, - ) + ), ], ), - body: Builder(builder: (context) { - if (dashboardState) { - return _buildDashboardHome(context, homeDashboard); - } else { - return _buildDefaultHome(context); - } - }), + body: Builder( + builder: (context) { + if (dashboardState) { + return _buildDashboardHome(context, homeDashboard); + } else { + return _buildDefaultHome(context); + } + }, + ), ); } Widget _buildDashboardHome( - BuildContext context, HomeDashboardInfo dashboard) { + BuildContext context, + HomeDashboardInfo dashboard, + ) { return HomeDashboard(tbContext, dashboard); } @@ -91,19 +91,25 @@ class _HomePageState extends TbContextState class HomeDashboard extends TbContextWidget { final HomeDashboardInfo dashboard; - HomeDashboard(TbContext tbContext, this.dashboard) : super(tbContext); + HomeDashboard(TbContext tbContext, this.dashboard, {super.key}) + : super(tbContext); @override - _HomeDashboardState createState() => _HomeDashboardState(); + State createState() => _HomeDashboardState(); } class _HomeDashboardState extends TbContextState { @override Widget build(BuildContext context) { - return dashboardUi.Dashboard(tbContext, home: true, - controllerCallback: (controller) { - controller.openDashboard(widget.dashboard.dashboardId!.id!, - hideToolbar: widget.dashboard.hideDashboardToolbar); - }); + return dashboard_ui.Dashboard( + tbContext, + home: true, + controllerCallback: (controller) { + controller.openDashboard( + widget.dashboard.dashboardId!.id!, + hideToolbar: widget.dashboard.hideDashboardToolbar, + ); + }, + ); } } diff --git a/lib/modules/home/home_routes.dart b/lib/modules/home/home_routes.dart index 6dc2cb3d..98f6f963 100644 --- a/lib/modules/home/home_routes.dart +++ b/lib/modules/home/home_routes.dart @@ -6,14 +6,15 @@ import 'package:thingsboard_app/modules/main/main_page.dart'; class HomeRoutes extends TbRoutes { late var homeHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return MainPage(tbContext, path: '/home'); - }); + handlerFunc: (BuildContext? context, Map params) { + return MainPage(tbContext, path: '/home'); + }, + ); HomeRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/home", handler: homeHandler); + router.define('/home', handler: homeHandler); } } diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart index 03999d80..1a0d2665 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -14,11 +14,12 @@ class TbMainNavigationItem { final Icon icon; final String path; - TbMainNavigationItem( - {required this.page, - required this.title, - required this.icon, - required this.path}); + TbMainNavigationItem({ + required this.page, + required this.title, + required this.icon, + required this.path, + }); static Map> mainPageStateMap = { Authority.SYS_ADMIN: Set.unmodifiable(['/home', '/more']), @@ -41,10 +42,11 @@ class TbMainNavigationItem { if (tbContext.isAuthenticated) { List items = [ TbMainNavigationItem( - page: HomePage(tbContext), - title: 'Home', - icon: Icon(Icons.home), - path: '/home') + page: HomePage(tbContext), + title: 'Home', + icon: const Icon(Icons.home), + path: '/home', + ), ]; switch (tbContext.tbClient.getAuthUser()!.authority) { case Authority.SYS_ADMIN: @@ -53,15 +55,17 @@ class TbMainNavigationItem { case Authority.CUSTOMER_USER: items.addAll([ TbMainNavigationItem( - page: AlarmsPage(tbContext), - title: 'Alarms', - icon: Icon(Icons.notifications), - path: '/alarms'), + page: AlarmsPage(tbContext), + title: 'Alarms', + icon: const Icon(Icons.notifications), + path: '/alarms', + ), TbMainNavigationItem( - page: DevicesMainPage(tbContext), - title: 'Devices', - icon: Icon(Icons.devices_other), - path: '/devices') + page: DevicesMainPage(tbContext), + title: 'Devices', + icon: const Icon(Icons.devices_other), + path: '/devices', + ), ]); break; case Authority.REFRESH_TOKEN: @@ -71,11 +75,14 @@ class TbMainNavigationItem { case Authority.PRE_VERIFICATION_TOKEN: break; } - items.add(TbMainNavigationItem( + items.add( + TbMainNavigationItem( page: MorePage(tbContext), title: 'More', - icon: Icon(Icons.menu), - path: '/more')); + icon: const Icon(Icons.menu), + path: '/more', + ), + ); return items; } else { return []; @@ -83,20 +90,22 @@ class TbMainNavigationItem { } static void changeItemsTitleIntl( - List items, BuildContext context) { + List items, + BuildContext context, + ) { for (var item in items) { switch (item.path) { case '/home': - item.title = '${S.of(context).home}'; + item.title = S.of(context).home; break; case '/alarms': - item.title = '${S.of(context).alarms}'; + item.title = S.of(context).alarms; break; case '/devices': - item.title = '${S.of(context).devices}'; + item.title = S.of(context).devices; break; case '/more': - item.title = '${S.of(context).more}'; + item.title = S.of(context).more; break; } } @@ -106,12 +115,12 @@ class TbMainNavigationItem { class MainPage extends TbPageWidget { final String _path; - MainPage(TbContext tbContext, {required String path}) + MainPage(TbContext tbContext, {required String path, super.key}) : _path = path, super(tbContext); @override - _MainPageState createState() => _MainPageState(); + State createState() => _MainPageState(); } class _MainPageState extends TbPageState @@ -142,7 +151,7 @@ class _MainPageState extends TbPageState _onTabAnimation() { var value = _tabController.animation!.value; - var targetIndex; + int targetIndex; if (value >= _tabController.previousIndex) { targetIndex = value.round(); } else { @@ -165,24 +174,30 @@ class _MainPageState extends TbPageState TbMainNavigationItem.changeItemsTitleIntl(_tabItems, context); // ignore: deprecated_member_use return Scaffold( - body: TabBarView( - physics: tbContext.homeDashboard != null - ? NeverScrollableScrollPhysics() - : null, - controller: _tabController, - children: _tabItems.map((item) => item.page).toList(), + body: TabBarView( + physics: tbContext.homeDashboard != null + ? const NeverScrollableScrollPhysics() + : null, + controller: _tabController, + children: _tabItems.map((item) => item.page).toList(), + ), + bottomNavigationBar: ValueListenableBuilder( + 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(), ), - bottomNavigationBar: ValueListenableBuilder( - 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()), - )); + ), + ); } int _indexFromPath(String path) { diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart index b924746c..5e293948 100644 --- a/lib/modules/more/more_page.dart +++ b/lib/modules/more/more_page.dart @@ -11,10 +11,10 @@ import 'package:thingsboard_app/utils/services/notification_service.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class MorePage extends TbContextWidget { - MorePage(TbContext tbContext) : super(tbContext); + MorePage(TbContext tbContext, {super.key}) : super(tbContext); @override - _MorePageState createState() => _MorePageState(); + State createState() => _MorePageState(); } class _MorePageState extends TbContextState @@ -22,93 +22,113 @@ class _MorePageState extends TbContextState @override Widget build(BuildContext context) { return SafeArea( - child: Scaffold( - backgroundColor: Colors.white, - body: Container( - padding: EdgeInsets.fromLTRB(16, 40, 16, 20), - child: Column( + child: Scaffold( + backgroundColor: Colors.white, + body: Container( + padding: const EdgeInsets.fromLTRB(16, 40, 16, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.account_circle, - size: 48, color: Color(0xFFAFAFAF)), - Spacer(), - IconButton( - icon: Icon(Icons.settings, color: Color(0xFFAFAFAF)), - onPressed: () async { - await navigateTo('/profile'); - setState(() {}); - }) - ], + const Icon( + Icons.account_circle, + size: 48, + color: Color(0xFFAFAFAF), ), - SizedBox(height: 22), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - _getUserDisplayName(), - style: TextStyle( - color: Color(0xFF282828), + const Spacer(), + IconButton( + icon: const Icon(Icons.settings, color: Color(0xFFAFAFAF)), + onPressed: () async { + await navigateTo('/profile'); + setState(() {}); + }, + ), + ], + ), + const SizedBox(height: 22), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + _getUserDisplayName(), + style: const TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 20, + height: 23 / 20, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: EndpointNameWidget( + endpoint: getIt().getCachedEndpoint(), + ), + ), + ], + ), + const SizedBox(height: 2), + Text( + _getAuthorityName(context), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 14, + height: 16 / 14, + ), + ), + const SizedBox(height: 24), + const Divider(color: Color(0xFFEDEDED)), + const SizedBox(height: 8), + buildMoreMenuItems(context), + const SizedBox(height: 8), + const Divider(color: Color(0xFFEDEDED)), + const SizedBox(height: 8), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 18, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + const Icon( + Icons.logout, + color: Color(0xFFE04B2F), + ), + const SizedBox(width: 34), + Text( + S.of(context).logout, + style: const TextStyle( + color: Color(0xFFE04B2F), + fontStyle: FontStyle.normal, fontWeight: FontWeight.w500, - fontSize: 20, - height: 23 / 20, + fontSize: 14, + height: 20 / 14, ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 10), - child: EndpointNameWidget( - endpoint: - getIt().getCachedEndpoint(), - ), - ), - ], + ], + ), ), - SizedBox(height: 2), - Text(_getAuthorityName(context), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontWeight: FontWeight.normal, - fontSize: 14, - height: 16 / 14)), - SizedBox(height: 24), - Divider(color: Color(0xFFEDEDED)), - SizedBox(height: 8), - buildMoreMenuItems(context), - SizedBox(height: 8), - Divider(color: Color(0xFFEDEDED)), - SizedBox(height: 8), - GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - height: 48, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: 0, horizontal: 18), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Icon(Icons.logout, - color: Color(0xFFE04B2F)), - SizedBox(width: 34), - Text('${S.of(context).logout}', - style: TextStyle( - color: Color(0xFFE04B2F), - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14)) - ]))), - onTap: () { - tbContext.logout( - requestConfig: RequestConfig(ignoreErrors: true)); - }) - ], + ), + onTap: () { + tbContext.logout( + requestConfig: RequestConfig(ignoreErrors: true), + ); + }, ), - ))); + ], + ), + ), + ), + ); } @override @@ -136,46 +156,55 @@ class _MorePageState extends TbContextState List items = MoreMenuItem.getItems(tbContext, context).map((menuItem) { return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - height: 48, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 0, horizontal: 18), - child: Row(mainAxisSize: MainAxisSize.max, children: [ - Icon( - menuItem.icon, - color: !menuItem.disabled - ? Color(0xFF282828) - : Colors.grey.withOpacity(0.5), - ), - Visibility( - visible: menuItem.showAdditionalIcon, - child: menuItem.additionalIcon ?? const SizedBox.shrink(), - ), - SizedBox(width: menuItem.showAdditionalIcon ? 15 : 34), - Text(menuItem.title, - style: TextStyle( - color: !menuItem.disabled - ? Color(0xFF282828) - : Colors.grey.withOpacity(0.5), - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14)) - ]))), - onTap: () { - if (!menuItem.disabled) { - navigateTo(menuItem.path); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - menuItem.disabledReasonMessage ?? 'The item is disabled', + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 18), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon( + menuItem.icon, + color: !menuItem.disabled + ? const Color(0xFF282828) + : Colors.grey.withOpacity(0.5), + ), + Visibility( + visible: menuItem.showAdditionalIcon, + child: menuItem.additionalIcon ?? const SizedBox.shrink(), + ), + SizedBox(width: menuItem.showAdditionalIcon ? 15 : 34), + Text( + menuItem.title, + style: TextStyle( + color: !menuItem.disabled + ? const Color(0xFF282828) + : Colors.grey.withOpacity(0.5), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, ), ), - ); - } - }); + ], + ), + ), + ), + onTap: () { + if (!menuItem.disabled) { + navigateTo(menuItem.path); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + menuItem.disabledReasonMessage ?? 'The item is disabled', + ), + ), + ); + } + }, + ); }).toList(); return Column(children: items); } @@ -209,13 +238,13 @@ class _MorePageState extends TbContextState var authority = user.authority; switch (authority) { case Authority.SYS_ADMIN: - name = '${S.of(context).systemAdministrator}'; + name = S.of(context).systemAdministrator; break; case Authority.TENANT_ADMIN: - name = '${S.of(context).tenantAdministrator}'; + name = S.of(context).tenantAdministrator; break; case Authority.CUSTOMER_USER: - name = '${S.of(context).customer}'; + name = S.of(context).customer; break; default: break; @@ -245,7 +274,9 @@ class MoreMenuItem { }); static List getItems( - TbContext tbContext, BuildContext context) { + TbContext tbContext, + BuildContext context, + ) { if (tbContext.isAuthenticated) { List items = []; switch (tbContext.tbClient.getAuthUser()!.authority) { @@ -267,17 +298,20 @@ class MoreMenuItem { case Authority.TENANT_ADMIN: items.addAll([ MoreMenuItem( - title: '${S.of(context).customers}', - icon: Icons.supervisor_account, - path: '/customers'), + title: S.of(context).customers, + icon: Icons.supervisor_account, + path: '/customers', + ), MoreMenuItem( - title: '${S.of(context).assets}', - icon: Icons.domain, - path: '/assets'), + title: S.of(context).assets, + icon: Icons.domain, + path: '/assets', + ), MoreMenuItem( - title: '${S.of(context).auditLogs}', - icon: Icons.track_changes, - path: '/auditLogs'), + title: S.of(context).auditLogs, + icon: Icons.track_changes, + path: '/auditLogs', + ), MoreMenuItem( title: 'Notifications', icon: Icons.notifications_active, @@ -293,9 +327,10 @@ class MoreMenuItem { case Authority.CUSTOMER_USER: items.addAll([ MoreMenuItem( - title: '${S.of(context).assets}', - icon: Icons.domain, - path: '/assets'), + title: S.of(context).assets, + icon: Icons.domain, + path: '/assets', + ), MoreMenuItem( title: 'Notifications', icon: Icons.notifications_active, @@ -345,7 +380,7 @@ class MoreMenuItem { child: Text( '${snapshot.data! > 99 ? '99+' : snapshot.data}', textAlign: TextAlign.center, - style: TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), ), ), ); diff --git a/lib/modules/notification/notification_page.dart b/lib/modules/notification/notification_page.dart index 1fc6cc04..04f6430f 100644 --- a/lib/modules/notification/notification_page.dart +++ b/lib/modules/notification/notification_page.dart @@ -15,7 +15,7 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; enum NotificationsFilter { all, unread } class NotificationPage extends TbPageWidget { - NotificationPage(TbContext tbContext) : super(tbContext); + NotificationPage(TbContext tbContext, {super.key}) : super(tbContext); @override State createState() => _NotificationPageState(); @@ -44,7 +44,7 @@ class _NotificationPageState extends TbPageState { '/home', replace: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), ); } }, @@ -55,7 +55,7 @@ class _NotificationPageState extends TbPageState { title: const Text('Notifications'), actions: [ TextButton( - child: Text('Mark all as read'), + child: const Text('Mark all as read'), onPressed: () async { await notificationRepository.markAllAsRead(); @@ -98,7 +98,7 @@ class _NotificationPageState extends TbPageState { ); }); }, - segments: [ + segments: const [ FilterSegments( label: 'Unread', value: NotificationsFilter.unread, diff --git a/lib/modules/notification/widgets/filter_segmented_button.dart b/lib/modules/notification/widgets/filter_segmented_button.dart index a9b116eb..26af7975 100644 --- a/lib/modules/notification/widgets/filter_segmented_button.dart +++ b/lib/modules/notification/widgets/filter_segmented_button.dart @@ -5,13 +5,14 @@ class FilterSegmentedButton extends StatelessWidget { required this.segments, required this.selected, required this.onSelectionChanged, + super.key, }); final List segments; final T selected; final void Function(T) onSelectionChanged; - final selectedTextStyle = TextStyle( + final selectedTextStyle = const TextStyle( color: Colors.white, fontWeight: FontWeight.w500, ); @@ -40,7 +41,7 @@ class FilterSegmentedButton extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), color: segments[index].value == selected - ? Color(0xFF305680) + ? const Color(0xFF305680) : null, ), child: Center( diff --git a/lib/modules/notification/widgets/no_notifications_found_widget.dart b/lib/modules/notification/widgets/no_notifications_found_widget.dart index a5b2f13e..e24e1328 100644 --- a/lib/modules/notification/widgets/no_notifications_found_widget.dart +++ b/lib/modules/notification/widgets/no_notifications_found_widget.dart @@ -5,7 +5,7 @@ class NoNotificationsFoundWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( + return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/modules/notification/widgets/notification_icon.dart b/lib/modules/notification/widgets/notification_icon.dart index eb9d9867..b4f4d32c 100644 --- a/lib/modules/notification/widgets/notification_icon.dart +++ b/lib/modules/notification/widgets/notification_icon.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class NotificationIcon extends StatelessWidget { - const NotificationIcon({required this.notification}); + const NotificationIcon({super.key, required this.notification}); final PushNotification notification; @@ -18,9 +16,9 @@ class NotificationIcon extends StatelessWidget { Color _toColor(String? data) { if (data != null) { - var hexColor = data.replaceAll("#", ""); + var hexColor = data.replaceAll('#', ''); if (hexColor.length == 6) { - hexColor = "FF" + hexColor; + hexColor = 'FF$hexColor'; } if (hexColor.length == 8) { @@ -43,11 +41,6 @@ class NotificationIcon extends StatelessWidget { MdiIcons.fromString(imageData.split('mdi:').last), color: _toColor(data['color']), ); - - return SvgPicture.network( - '${ThingsboardAppConstants.thingsBoardApiEndpoint}/assets/mdi/${imageData.split('mdi:').last}.svg', - color: _toColor(data['color']), - ); } return Icon( @@ -58,7 +51,7 @@ class NotificationIcon extends StatelessWidget { ); } - return Icon(Icons.notifications, color: Colors.black54); + return const Icon(Icons.notifications, color: Colors.black54); } } diff --git a/lib/modules/notification/widgets/notification_list.dart b/lib/modules/notification/widgets/notification_list.dart index 64c61d94..aef9860a 100644 --- a/lib/modules/notification/widgets/notification_list.dart +++ b/lib/modules/notification/widgets/notification_list.dart @@ -8,12 +8,13 @@ import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class NotificationsList extends StatelessWidget { - NotificationsList({ + const NotificationsList({ required this.pagingController, required this.thingsboardClient, required this.onClearNotification, required this.onReadNotification, required this.tbContext, + super.key, }); final ThingsboardClient thingsboardClient; @@ -29,18 +30,18 @@ class NotificationsList extends StatelessWidget { builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) { return NotificationSlidableWidget( + notification: item as PushNotification, + onReadNotification: onReadNotification, + onClearNotification: onClearNotification, + tbContext: tbContext, + thingsboardClient: thingsboardClient, child: NotificationWidget( - notification: item as PushNotification, + notification: item, thingsboardClient: thingsboardClient, onClearNotification: onClearNotification, onReadNotification: onReadNotification, tbContext: tbContext, ), - notification: item, - onReadNotification: onReadNotification, - onClearNotification: onClearNotification, - tbContext: tbContext, - thingsboardClient: thingsboardClient, ); }, firstPageProgressIndicatorBuilder: (_) => SizedBox.expand( diff --git a/lib/modules/notification/widgets/notification_slidable_widget.dart b/lib/modules/notification/widgets/notification_slidable_widget.dart index 03117b26..57055196 100644 --- a/lib/modules/notification/widgets/notification_slidable_widget.dart +++ b/lib/modules/notification/widgets/notification_slidable_widget.dart @@ -12,6 +12,7 @@ class NotificationSlidableWidget extends StatefulWidget { required this.onClearNotification, required this.onReadNotification, required this.tbContext, + super.key, }); final Widget child; @@ -34,13 +35,12 @@ class _NotificationSlidableWidget extends State { return Container( height: 134, alignment: Alignment.center, - child: RefreshProgressIndicator(), + child: const RefreshProgressIndicator(), ); } return Slidable( key: ValueKey(widget.notification.id!.id), - child: widget.child, startActionPane: widget.notification.status == PushNotificationStatus.READ ? null : ActionPane( @@ -51,7 +51,7 @@ class _NotificationSlidableWidget extends State { onPressed: (context) => widget.onReadNotification( widget.notification.id!.id!, ), - backgroundColor: Color(0xFF198038), + backgroundColor: const Color(0xFF198038), foregroundColor: Colors.white, icon: Icons.check_circle_outline, label: 'Mark as read', @@ -72,19 +72,20 @@ class _NotificationSlidableWidget extends State { widget.notification.status == PushNotificationStatus.READ, ); }, - backgroundColor: Color(0xFFD12730).withOpacity(0.94), + backgroundColor: const Color(0xFFD12730).withOpacity(0.94), foregroundColor: Colors.white, icon: Icons.delete, label: 'Delete', borderRadius: _buildAlarmRelatedButtons(widget.notification).isEmpty ? BorderRadius.circular(8) - : BorderRadius.only( + : const BorderRadius.only( topRight: Radius.circular(8), bottomRight: Radius.circular(8), ), ), ], ), + child: widget.child, ); } @@ -102,12 +103,12 @@ class _NotificationSlidableWidget extends State { items.add( SlidableAction( onPressed: (context) => _ackAlarm(id, context), - backgroundColor: Color(0xFF198038), + backgroundColor: const Color(0xFF198038), foregroundColor: Colors.white, icon: Icons.done, label: 'Acknowledge', padding: const EdgeInsets.symmetric(horizontal: 4), - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), ), @@ -120,12 +121,12 @@ class _NotificationSlidableWidget extends State { items.add( SlidableAction( onPressed: (context) => _clearAlarm(id, context), - backgroundColor: Color(0xFF757575), + backgroundColor: const Color(0xFF757575), foregroundColor: Colors.white, icon: Icons.clear, label: 'Clear', borderRadius: items.isEmpty - ? BorderRadius.only( + ? const BorderRadius.only( topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), ) @@ -141,10 +142,11 @@ class _NotificationSlidableWidget extends State { void _ackAlarm(String alarmId, BuildContext context) async { final res = await widget.tbContext.confirm( - title: '${S.of(context).alarmAcknowledgeTitle}', - message: '${S.of(context).alarmAcknowledgeText}', - cancel: '${S.of(context).No}', - ok: '${S.of(context).Yes}'); + title: S.of(context).alarmAcknowledgeTitle, + message: S.of(context).alarmAcknowledgeText, + cancel: S.of(context).No, + ok: S.of(context).Yes, + ); if (res != null && res) { setState(() { @@ -163,10 +165,11 @@ class _NotificationSlidableWidget extends State { void _clearAlarm(String alarmId, BuildContext context) async { final res = await widget.tbContext.confirm( - title: '${S.of(context).alarmClearTitle}', - message: '${S.of(context).alarmClearText}', - cancel: '${S.of(context).No}', - ok: '${S.of(context).Yes}'); + title: S.of(context).alarmClearTitle, + message: S.of(context).alarmClearText, + cancel: S.of(context).No, + ok: S.of(context).Yes, + ); if (res != null && res) { setState(() { loading = true; diff --git a/lib/modules/notification/widgets/notification_widget.dart b/lib/modules/notification/widgets/notification_widget.dart index 7025b281..bb098c96 100644 --- a/lib/modules/notification/widgets/notification_widget.dart +++ b/lib/modules/notification/widgets/notification_widget.dart @@ -14,6 +14,7 @@ class NotificationWidget extends StatelessWidget { required this.onClearNotification, required this.onReadNotification, required this.tbContext, + super.key, }); final PushNotification notification; @@ -72,7 +73,7 @@ class NotificationWidget extends StatelessWidget { padding: const EdgeInsets.only(left: 7), child: Text( notification.subject, - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), @@ -123,7 +124,7 @@ class NotificationWidget extends StatelessWidget { Visibility( visible: notification.status == PushNotificationStatus.READ, - child: SizedBox( + child: const SizedBox( width: 30, height: 50, ), diff --git a/lib/modules/profile/change_password_page.dart b/lib/modules/profile/change_password_page.dart index cba6f882..c1bd7fbb 100644 --- a/lib/modules/profile/change_password_page.dart +++ b/lib/modules/profile/change_password_page.dart @@ -8,10 +8,10 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; class ChangePasswordPage extends TbContextWidget { - ChangePasswordPage(TbContext tbContext) : super(tbContext); + ChangePasswordPage(TbContext tbContext, {super.key}) : super(tbContext); @override - _ChangePasswordPageState createState() => _ChangePasswordPageState(); + State createState() => _ChangePasswordPageState(); } class _ChangePasswordPageState extends TbContextState { @@ -26,138 +26,165 @@ class _ChangePasswordPageState extends TbContextState { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, - appBar: TbAppBar( - tbContext, - title: Text('${S.of(context).changePassword}'), - ), - body: Stack( - children: [ - SizedBox.expand( - child: Padding( - padding: EdgeInsets.all(16), - child: SingleChildScrollView( - child: FormBuilder( - key: _changePasswordFormKey, - autovalidateMode: AutovalidateMode.disabled, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 16), - ValueListenableBuilder( - valueListenable: _showCurrentPasswordNotifier, - builder: (BuildContext context, bool showPassword, - child) { - return FormBuilderTextField( - name: 'currentPassword', - obscureText: !showPassword, - autofocus: true, - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required( - errorText: - '${S.of(context).currentPasswordRequireText}') - ]), - decoration: InputDecoration( - suffixIcon: IconButton( - icon: Icon(showPassword - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - _showCurrentPasswordNotifier.value = - !_showCurrentPasswordNotifier - .value; - }, - ), - border: OutlineInputBorder(), - labelText: - '${S.of(context).currentPasswordStar}'), - ); - }), - SizedBox(height: 24), - ValueListenableBuilder( - valueListenable: _showNewPasswordNotifier, - builder: (BuildContext context, bool showPassword, - child) { - return FormBuilderTextField( - name: 'newPassword', - obscureText: !showPassword, - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required( - errorText: - '${S.of(context).newPasswordRequireText}') - ]), - decoration: InputDecoration( - suffixIcon: IconButton( - icon: Icon(showPassword - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - _showNewPasswordNotifier.value = - !_showNewPasswordNotifier.value; - }, - ), - border: OutlineInputBorder(), - labelText: - '${S.of(context).newPasswordStar}'), - ); - }), - SizedBox(height: 24), - ValueListenableBuilder( - valueListenable: _showNewPassword2Notifier, - builder: (BuildContext context, bool showPassword, - child) { - return FormBuilderTextField( - name: 'newPassword2', - obscureText: !showPassword, - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required( - errorText: - '${S.of(context).newPassword2RequireText}') - ]), - decoration: InputDecoration( - suffixIcon: IconButton( - icon: Icon(showPassword - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - _showNewPassword2Notifier.value = - !_showNewPassword2Notifier.value; - }, - ), - border: OutlineInputBorder(), - labelText: - '${S.of(context).newPassword2Star}'), - ); - }), - SizedBox(height: 24), - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: EdgeInsets.all(16), - alignment: Alignment.centerLeft), - onPressed: () { - _changePassword(); - }, - child: Center( - child: - Text('${S.of(context).changePassword}'))) - ]), - ))), + backgroundColor: Colors.white, + appBar: TbAppBar( + tbContext, + title: Text(S.of(context).changePassword), + ), + body: Stack( + children: [ + SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: FormBuilder( + key: _changePasswordFormKey, + autovalidateMode: AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: _showCurrentPasswordNotifier, + builder: ( + BuildContext context, + bool showPassword, + child, + ) { + return FormBuilderTextField( + name: 'currentPassword', + obscureText: !showPassword, + autofocus: true, + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required( + errorText: + S.of(context).currentPasswordRequireText, + ), + ]), + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + _showCurrentPasswordNotifier.value = + !_showCurrentPasswordNotifier.value; + }, + ), + border: const OutlineInputBorder(), + labelText: S.of(context).currentPasswordStar, + ), + ); + }, + ), + const SizedBox(height: 24), + ValueListenableBuilder( + valueListenable: _showNewPasswordNotifier, + builder: ( + BuildContext context, + bool showPassword, + child, + ) { + return FormBuilderTextField( + name: 'newPassword', + obscureText: !showPassword, + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required( + errorText: S.of(context).newPasswordRequireText, + ), + ]), + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + _showNewPasswordNotifier.value = + !_showNewPasswordNotifier.value; + }, + ), + border: const OutlineInputBorder(), + labelText: S.of(context).newPasswordStar, + ), + ); + }, + ), + const SizedBox(height: 24), + ValueListenableBuilder( + valueListenable: _showNewPassword2Notifier, + builder: ( + BuildContext context, + bool showPassword, + child, + ) { + return FormBuilderTextField( + name: 'newPassword2', + obscureText: !showPassword, + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required( + errorText: + S.of(context).newPassword2RequireText, + ), + ]), + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + _showNewPassword2Notifier.value = + !_showNewPassword2Notifier.value; + }, + ), + border: const OutlineInputBorder(), + labelText: S.of(context).newPassword2Star, + ), + ); + }, + ), + const SizedBox(height: 24), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + ), + onPressed: () { + _changePassword(); + }, + child: Center( + child: Text(S.of(context).changePassword), + ), + ), + ], + ), + ), + ), ), - ValueListenableBuilder( - valueListenable: _isLoadingNotifier, - builder: (BuildContext context, bool loading, child) { - if (loading) { - return SizedBox.expand( - child: Container( - color: Color(0x99FFFFFF), - child: Center(child: TbProgressIndicator(size: 50.0)), - )); - } else { - return SizedBox.shrink(); - } - }) - ], - )); + ), + ValueListenableBuilder( + valueListenable: _isLoadingNotifier, + builder: (BuildContext context, bool loading, child) { + if (loading) { + return SizedBox.expand( + child: Container( + color: const Color(0x99FFFFFF), + child: const Center(child: TbProgressIndicator(size: 50.0)), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], + ), + ); } Future _changePassword() async { @@ -168,11 +195,11 @@ class _ChangePasswordPageState extends TbContextState { String newPassword = formValue['newPassword']; String newPassword2 = formValue['newPassword2']; if (newPassword != newPassword2) { - showErrorNotification('${S.of(context).passwordErrorNotification}'); + showErrorNotification(S.of(context).passwordErrorNotification); } else { _isLoadingNotifier.value = true; try { - await Future.delayed(Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); await tbClient.changePassword(currentPassword, newPassword); pop(true); } catch (e) { diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart index 01baddbc..d493d0f5 100644 --- a/lib/modules/profile/profile_page.dart +++ b/lib/modules/profile/profile_page.dart @@ -1,24 +1,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/generated/l10n.dart'; import 'package:thingsboard_app/modules/profile/change_password_page.dart'; 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 { final bool _fullscreen; - ProfilePage(TbContext tbContext, {bool fullscreen = false}) - : _fullscreen = fullscreen, + ProfilePage( + TbContext tbContext, { + bool fullscreen = false, + super.key, + }) : _fullscreen = fullscreen, super(tbContext); @override - _ProfilePageState createState() => _ProfilePageState(); + State createState() => _ProfilePageState(); } class _ProfilePageState extends TbPageState { @@ -34,102 +36,110 @@ class _ProfilePageState extends TbPageState { _loadUser(); } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, - appBar: TbAppBar( - tbContext, - title: const Text('Profile'), - actions: [ + backgroundColor: Colors.white, + appBar: TbAppBar( + tbContext, + title: const Text('Profile'), + actions: [ + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + _saveProfile(); + }, + ), + if (widget._fullscreen) IconButton( - icon: Icon(Icons.check), - onPressed: () { - _saveProfile(); - }), - if (widget._fullscreen) - IconButton( - icon: Icon(Icons.logout), - onPressed: () { - tbContext.logout(); - }) - ], - ), - body: Stack( - children: [ - SizedBox.expand( - child: Padding( - padding: EdgeInsets.all(16), - child: SingleChildScrollView( - child: FormBuilder( - key: _profileFormKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 16), - FormBuilderTextField( - name: 'email', - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required( - errorText: - '${S.of(context).emailRequireText}'), - FormBuilderValidators.email( - errorText: - '${S.of(context).emailInvalidText}') - ]), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: '${S.of(context).emailStar}'), - ), - SizedBox(height: 24), - FormBuilderTextField( - name: 'firstName', - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: '${S.of(context).firstNameUpper}'), + icon: const Icon(Icons.logout), + onPressed: () { + tbContext.logout(); + }, + ), + ], + ), + body: Stack( + children: [ + SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: FormBuilder( + key: _profileFormKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + FormBuilderTextField( + name: 'email', + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required( + errorText: S.of(context).emailRequireText, ), - SizedBox(height: 24), - FormBuilderTextField( - name: 'lastName', - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: '${S.of(context).lastNameUpper}'), + FormBuilderValidators.email( + errorText: S.of(context).emailInvalidText, ), - SizedBox(height: 24), - OutlinedButton( - style: OutlinedButton.styleFrom( - padding: EdgeInsets.all(16), - alignment: Alignment.centerLeft), - onPressed: () { - _changePassword(); - }, - child: Center( - child: - Text('${S.of(context).changePassword}'))) ]), - ))), + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: S.of(context).emailStar, + ), + ), + const SizedBox(height: 24), + FormBuilderTextField( + name: 'firstName', + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: S.of(context).firstNameUpper, + ), + ), + const SizedBox(height: 24), + FormBuilderTextField( + name: 'lastName', + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: S.of(context).lastNameUpper, + ), + ), + const SizedBox(height: 24), + OutlinedButton( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + ), + onPressed: () { + _changePassword(); + }, + child: Center( + child: Text(S.of(context).changePassword), + ), + ), + ], + ), + ), + ), ), - ValueListenableBuilder( - valueListenable: _isLoadingNotifier, - builder: (BuildContext context, bool loading, child) { - if (loading) { - return SizedBox.expand( - child: Container( - color: Color(0x99FFFFFF), - child: Center(child: TbProgressIndicator(size: 50.0)), - )); - } else { - return SizedBox.shrink(); - } - }) - ], - )); + ), + ValueListenableBuilder( + valueListenable: _isLoadingNotifier, + builder: (BuildContext context, bool loading, child) { + if (loading) { + return SizedBox.expand( + child: Container( + color: const Color(0x99FFFFFF), + child: const Center(child: TbProgressIndicator(size: 50.0)), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], + ), + ); } Future _loadUser() async { @@ -143,7 +153,7 @@ class _ProfilePageState extends TbPageState { _profileFormKey.currentState?.patchValue({ 'email': _currentUser!.email, 'firstName': _currentUser!.firstName ?? '', - 'lastName': _currentUser!.lastName ?? '' + 'lastName': _currentUser!.lastName ?? '', }); } @@ -159,22 +169,28 @@ class _ProfilePageState extends TbPageState { _currentUser = await tbClient.getUserService().saveUser(_currentUser!); tbContext.userDetails = _currentUser; _setUser(); - await Future.delayed(Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); _isLoadingNotifier.value = false; - showSuccessNotification('${S.of(context).profileSuccessNotification}', - duration: Duration(milliseconds: 1500)); - showSuccessNotification('${S.of(context).profileSuccessNotification}', - duration: Duration(milliseconds: 1500)); + showSuccessNotification( + S.of(context).profileSuccessNotification, + duration: const Duration(milliseconds: 1500), + ); + showSuccessNotification( + S.of(context).profileSuccessNotification, + duration: const Duration(milliseconds: 1500), + ); } } } _changePassword() async { var res = await tbContext - .showFullScreenDialog(new ChangePasswordPage(tbContext)); + .showFullScreenDialog(ChangePasswordPage(tbContext)); if (res == true) { - showSuccessNotification('${S.of(context).passwordSuccessNotification}', - duration: Duration(milliseconds: 1500)); + showSuccessNotification( + S.of(context).passwordSuccessNotification, + duration: const Duration(milliseconds: 1500), + ); } } } diff --git a/lib/modules/profile/profile_routes.dart b/lib/modules/profile/profile_routes.dart index 1d48089c..8088ea15 100644 --- a/lib/modules/profile/profile_routes.dart +++ b/lib/modules/profile/profile_routes.dart @@ -6,16 +6,17 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'profile_page.dart'; class ProfileRoutes extends TbRoutes { - late var profileHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - var fullscreen = params['fullscreen']?.first == 'true'; - return ProfilePage(tbContext, fullscreen: fullscreen); - }); + late final profileHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + var fullscreen = params['fullscreen']?.first == 'true'; + return ProfilePage(tbContext, fullscreen: fullscreen); + }, + ); ProfileRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/profile", handler: profileHandler); + router.define('/profile', handler: profileHandler); } } diff --git a/lib/modules/tenant/tenant_details_page.dart b/lib/modules/tenant/tenant_details_page.dart index 31e0d668..dc7b3f47 100644 --- a/lib/modules/tenant/tenant_details_page.dart +++ b/lib/modules/tenant/tenant_details_page.dart @@ -3,14 +3,16 @@ import 'package:thingsboard_app/core/entity/entity_details_page.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class TenantDetailsPage extends ContactBasedDetailsPage { - TenantDetailsPage(TbContext tbContext, String tenantId) - : super(tbContext, - entityId: tenantId, - defaultTitle: 'Tenant', - subTitle: 'Tenant details'); + TenantDetailsPage(TbContext tbContext, String tenantId, {super.key}) + : super( + tbContext, + entityId: tenantId, + defaultTitle: 'Tenant', + subTitle: 'Tenant details', + ); @override - Future fetchEntity(String tenantId) { - return tbClient.getTenantService().getTenant(tenantId); + Future fetchEntity(String id) { + return tbClient.getTenantService().getTenant(id); } } diff --git a/lib/modules/tenant/tenant_routes.dart b/lib/modules/tenant/tenant_routes.dart index 365667d1..74411178 100644 --- a/lib/modules/tenant/tenant_routes.dart +++ b/lib/modules/tenant/tenant_routes.dart @@ -2,26 +2,29 @@ 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 'tenant_details_page.dart'; import 'tenants_page.dart'; class TenantRoutes extends TbRoutes { late var tenantsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - var searchMode = params['search']?.first == 'true'; - return TenantsPage(tbContext, searchMode: searchMode); - }); + handlerFunc: (BuildContext? context, Map params) { + var searchMode = params['search']?.first == 'true'; + return TenantsPage(tbContext, searchMode: searchMode); + }, + ); late var tenantDetailsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return TenantDetailsPage(tbContext, params["id"][0]); - }); + handlerFunc: (BuildContext? context, Map params) { + return TenantDetailsPage(tbContext, params['id'][0]); + }, + ); TenantRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/tenants", handler: tenantsHandler); - router.define("/tenant/:id", handler: tenantDetailsHandler); + router.define('/tenants', handler: tenantsHandler); + router.define('/tenant/:id', handler: tenantDetailsHandler); } } diff --git a/lib/modules/tenant/tenants_list.dart b/lib/modules/tenant/tenants_list.dart index 543d33b9..b91425b0 100644 --- a/lib/modules/tenant/tenants_list.dart +++ b/lib/modules/tenant/tenants_list.dart @@ -8,7 +8,9 @@ import 'tenants_base.dart'; class TenantsList extends BaseEntitiesWidget with TenantsBase, ContactBasedBase, EntitiesListStateBase { TenantsList( - TbContext tbContext, PageKeyController pageKeyController, - {searchMode = false}) - : super(tbContext, pageKeyController, searchMode: searchMode); + TbContext tbContext, + PageKeyController pageKeyController, { + searchMode = false, + super.key, + }) : super(tbContext, pageKeyController, searchMode: searchMode); } diff --git a/lib/modules/tenant/tenants_page.dart b/lib/modules/tenant/tenants_page.dart index 77e7b932..6e09ab56 100644 --- a/lib/modules/tenant/tenants_page.dart +++ b/lib/modules/tenant/tenants_page.dart @@ -9,11 +9,11 @@ import 'tenants_list.dart'; class TenantsPage extends TbPageWidget { final bool searchMode; - TenantsPage(TbContext tbContext, {this.searchMode = false}) + TenantsPage(TbContext tbContext, {this.searchMode = false, super.key}) : super(tbContext); @override - _TenantsPageState createState() => _TenantsPageState(); + State createState() => _TenantsPageState(); } class _TenantsPageState extends TbPageState { @@ -21,8 +21,11 @@ class _TenantsPageState extends TbPageState { @override Widget build(BuildContext context) { - var tenantsList = TenantsList(tbContext, _pageLinkController, - searchMode: widget.searchMode); + var tenantsList = TenantsList( + tbContext, + _pageLinkController, + searchMode: widget.searchMode, + ); PreferredSizeWidget appBar; if (widget.searchMode) { appBar = TbAppSearchBar( @@ -30,14 +33,18 @@ class _TenantsPageState extends TbPageState { onSearch: (searchText) => _pageLinkController.onSearchText(searchText), ); } else { - appBar = TbAppBar(tbContext, title: Text(tenantsList.title), actions: [ - IconButton( - icon: Icon(Icons.search), - onPressed: () { - navigateTo('/tenants?search=true'); - }, - ) - ]); + appBar = TbAppBar( + tbContext, + title: Text(tenantsList.title), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + navigateTo('/tenants?search=true'); + }, + ), + ], + ); } return Scaffold(appBar: appBar, body: tenantsList); } diff --git a/lib/modules/tenant/tenants_widget.dart b/lib/modules/tenant/tenants_widget.dart index 7f5f66e6..b78966d5 100644 --- a/lib/modules/tenant/tenants_widget.dart +++ b/lib/modules/tenant/tenants_widget.dart @@ -6,10 +6,10 @@ import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'tenants_list.dart'; class TenantsWidget extends TbContextWidget { - TenantsWidget(TbContext tbContext) : super(tbContext); + TenantsWidget(TbContext tbContext, {super.key}) : super(tbContext); @override - _TenantsWidgetState createState() => _TenantsWidgetState(); + State createState() => _TenantsWidgetState(); } class _TenantsWidgetState extends TbContextState { diff --git a/lib/modules/url/url_page.dart b/lib/modules/url/url_page.dart index 9b00c71a..3e48ecb5 100644 --- a/lib/modules/url/url_page.dart +++ b/lib/modules/url/url_page.dart @@ -26,7 +26,7 @@ class UrlPage extends StatelessWidget { onPressed: () { launchUrlString(url, mode: LaunchMode.externalApplication); }, - icon: Icon(Icons.open_in_browser), + icon: const Icon(Icons.open_in_browser), ), ], ), diff --git a/lib/utils/services/_tb_secure_storage.dart b/lib/utils/services/_tb_secure_storage.dart index caa315c7..e88a88ce 100644 --- a/lib/utils/services/_tb_secure_storage.dart +++ b/lib/utils/services/_tb_secure_storage.dart @@ -4,7 +4,7 @@ import 'package:thingsboard_client/thingsboard_client.dart'; TbStorage createAppStorage() => TbSecureStorage(); class TbSecureStorage implements TbStorage { - final flutterStorage = FlutterSecureStorage(); + final flutterStorage = const FlutterSecureStorage(); @override Future deleteItem(String key) async { diff --git a/lib/utils/services/device_profile_cache.dart b/lib/utils/services/device_profile_cache.dart index ce188a38..49594876 100644 --- a/lib/utils/services/device_profile_cache.dart +++ b/lib/utils/services/device_profile_cache.dart @@ -1,10 +1,13 @@ import 'package:thingsboard_client/thingsboard_client.dart'; abstract class DeviceProfileCache { - static final _cache = Map(); + static final _cache = {}; static Future getDeviceProfileInfo( - ThingsboardClient tbClient, String name, String deviceId) async { + ThingsboardClient tbClient, + String name, + String deviceId, + ) async { var deviceProfile = _cache[name]; if (deviceProfile == null) { var device = await tbClient.getDeviceService().getDevice(deviceId); @@ -17,13 +20,17 @@ abstract class DeviceProfileCache { } static Future> getDeviceProfileInfos( - ThingsboardClient tbClient, PageLink pageLink) async { - var deviceProfileInfos = await tbClient + ThingsboardClient tbClient, + PageLink pageLink, + ) async { + final deviceProfileInfos = await tbClient .getDeviceProfileService() .getDeviceProfileInfos(pageLink); - deviceProfileInfos.data.forEach((deviceProfile) { + + for (final deviceProfile in deviceProfileInfos.data) { _cache[deviceProfile.name] = deviceProfile; - }); + } + return deviceProfileInfos; } } diff --git a/lib/utils/services/entity_query_api.dart b/lib/utils/services/entity_query_api.dart index eb66b754..b6690909 100644 --- a/lib/utils/services/entity_query_api.dart +++ b/lib/utils/services/entity_query_api.dart @@ -2,32 +2,39 @@ import 'package:thingsboard_client/thingsboard_client.dart'; abstract class EntityQueryApi { static final activeDeviceKeyFilter = KeyFilter( - key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), - valueType: EntityKeyValueType.BOOLEAN, - predicate: BooleanFilterPredicate( - operation: BooleanOperation.EQUAL, - value: FilterPredicateValue(true))); + key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), + valueType: EntityKeyValueType.BOOLEAN, + predicate: BooleanFilterPredicate( + operation: BooleanOperation.EQUAL, + value: FilterPredicateValue(true), + ), + ); static final inactiveDeviceKeyFilter = KeyFilter( - key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), - valueType: EntityKeyValueType.BOOLEAN, - predicate: BooleanFilterPredicate( - operation: BooleanOperation.EQUAL, - value: FilterPredicateValue(false))); + key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), + valueType: EntityKeyValueType.BOOLEAN, + predicate: BooleanFilterPredicate( + operation: BooleanOperation.EQUAL, + value: FilterPredicateValue(false), + ), + ); static final defaultDeviceFields = [ EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'name'), EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'type'), EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'label'), - EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'createdTime') + EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'createdTime'), ]; static final defaultDeviceAttributes = [ - EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active') + EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), ]; - static Future countDevices(ThingsboardClient tbClient, - {String? deviceType, bool? active}) { + static Future countDevices( + ThingsboardClient tbClient, { + String? deviceType, + bool? active, + }) { EntityFilter deviceFilter; if (deviceType != null) { deviceFilter = @@ -39,7 +46,7 @@ abstract class EntityQueryApi { EntityCountQuery(entityFilter: deviceFilter); if (active != null) { deviceCountQuery.keyFilters = [ - active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter + active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter, ]; } return tbClient @@ -47,11 +54,12 @@ abstract class EntityQueryApi { .countEntitiesByQuery(deviceCountQuery); } - static EntityDataQuery createDefaultDeviceQuery( - {int pageSize = 20, - String? searchText, - String? deviceType, - bool? active}) { + static EntityDataQuery createDefaultDeviceQuery({ + int pageSize = 20, + String? searchText, + String? deviceType, + bool? active, + }) { EntityFilter deviceFilter; List? keyFilters; if (deviceType != null) { @@ -64,16 +72,21 @@ abstract class EntityQueryApi { keyFilters = [active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter]; } return EntityDataQuery( - entityFilter: deviceFilter, - keyFilters: keyFilters, - entityFields: defaultDeviceFields, - latestValues: defaultDeviceAttributes, - pageLink: EntityDataPageLink( - pageSize: pageSize, - textSearch: searchText, - sortOrder: EntityDataSortOrder( - key: EntityKey( - type: EntityKeyType.ENTITY_FIELD, key: 'createdTime'), - direction: EntityDataSortOrderDirection.DESC))); + entityFilter: deviceFilter, + keyFilters: keyFilters, + entityFields: defaultDeviceFields, + latestValues: defaultDeviceAttributes, + pageLink: EntityDataPageLink( + pageSize: pageSize, + textSearch: searchText, + sortOrder: EntityDataSortOrder( + key: EntityKey( + type: EntityKeyType.ENTITY_FIELD, + key: 'createdTime', + ), + direction: EntityDataSortOrderDirection.DESC, + ), + ), + ); } } diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 6ea62644..8475168d 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -63,7 +63,8 @@ class NotificationService { final settings = await _requestPermission(); _log.debug( - 'Notification authorizationStatus: ${settings.authorizationStatus}'); + 'Notification authorizationStatus: ${settings.authorizationStatus}', + ); if (settings.authorizationStatus == AuthorizationStatus.authorized || settings.authorizationStatus == AuthorizationStatus.provisional) { await _getAndSaveToken(); @@ -148,7 +149,7 @@ class NotificationService { }, ); - final androidPlatformChannelSpecifics = AndroidNotificationDetails( + const androidPlatformChannelSpecifics = AndroidNotificationDetails( 'general', 'General notifications', importance: Importance.max, @@ -159,7 +160,7 @@ class NotificationService { const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); - _notificationDetails = NotificationDetails( + _notificationDetails = const NotificationDetails( android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics, ); @@ -203,7 +204,7 @@ class NotificationService { if (mobileInfo != null) { int timeAfterCreatedToken = DateTime.now().millisecondsSinceEpoch - mobileInfo.fcmTokenTimestamp; - if (timeAfterCreatedToken > Duration(days: 30).inMilliseconds) { + if (timeAfterCreatedToken > const Duration(days: 30).inMilliseconds) { fcmToken = await _resetToken(fcmToken); if (fcmToken != null) { await _saveToken(fcmToken); @@ -217,7 +218,9 @@ class NotificationService { Future _saveToken(String token) async { await _tbClient.getUserService().saveMobileSession( - token, MobileSessionInfo(DateTime.now().millisecondsSinceEpoch)); + token, + MobileSessionInfo(DateTime.now().millisecondsSinceEpoch), + ); } void showNotification(RemoteMessage message) async { @@ -239,7 +242,7 @@ class NotificationService { void _subscribeOnForegroundMessage() { _foregroundMessageSubscription = FirebaseMessaging.onMessage.listen((message) { - _log.debug('Message:' + message.toString()); + _log.debug('Message:$message'); if (message.sentTime == null) { final map = message.toMap(); map['sentTime'] = DateTime.now().millisecondsSinceEpoch; @@ -259,14 +262,15 @@ class NotificationService { case 'DASHBOARD': final dashboardId = data['dashboardId'] ?? data['onClick.dashboardId']; - var entityId; + EntityId? entityId; if ((data['stateEntityId'] ?? data['onClick.stateEntityId']) != null && (data['stateEntityType'] ?? data['onClick.stateEntityType']) != null) { entityId = EntityId.fromTypeAndUuid( entityTypeFromString( - data['stateEntityType'] ?? data['onClick.stateEntityType']), + data['stateEntityType'] ?? data['onClick.stateEntityType'], + ), data['stateEntityId'] ?? data['onClick.stateEntityId'], ); } diff --git a/lib/utils/services/widget_action_handler.dart b/lib/utils/services/widget_action_handler.dart index 6b9c91b0..97dce383 100644 --- a/lib/utils/services/widget_action_handler.dart +++ b/lib/utils/services/widget_action_handler.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:fluro/fluro.dart'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -64,6 +65,7 @@ class MobileActionResult { class _LaunchResult extends MobileActionResult { bool launched; + _LaunchResult(this.launched); @override @@ -76,6 +78,7 @@ class _LaunchResult extends MobileActionResult { class _ImageResult extends MobileActionResult { String imageUrl; + _ImageResult(this.imageUrl); @override @@ -89,6 +92,7 @@ class _ImageResult extends MobileActionResult { class _QrCodeResult extends MobileActionResult { String code; String format; + _QrCodeResult(this.code, this.format); @override @@ -103,6 +107,7 @@ class _QrCodeResult extends MobileActionResult { class _LocationResult extends MobileActionResult { num latitude; num longitude; + _LocationResult(this.latitude, this.longitude); @override @@ -128,8 +133,9 @@ enum WidgetMobileActionType { WidgetMobileActionType widgetMobileActionTypeFromString(String value) { return WidgetMobileActionType.values.firstWhere( - (e) => e.toString().split('.')[1].toUpperCase() == value.toUpperCase(), - orElse: () => WidgetMobileActionType.unknown); + (e) => e.toString().split('.')[1].toUpperCase() == value.toUpperCase(), + orElse: () => WidgetMobileActionType.unknown, + ); } class WidgetActionHandler with HasTbContext { @@ -138,13 +144,17 @@ class WidgetActionHandler with HasTbContext { } Future> handleWidgetMobileAction( - List args, InAppWebViewController controller) async { + List args, + InAppWebViewController controller, + ) async { var result = await _handleWidgetMobileAction(args, controller); return result.toJson(); } Future _handleWidgetMobileAction( - List args, InAppWebViewController controller) async { + List args, + InAppWebViewController controller, + ) async { if (args.isNotEmpty && args[0] is String) { var actionType = widgetMobileActionTypeFromString(args[0]); switch (actionType) { @@ -166,11 +176,13 @@ class WidgetActionHandler with HasTbContext { return await _takeScreenshot(controller); case WidgetMobileActionType.unknown: return WidgetMobileActionResult.errorResult( - 'Unknown actionType: ${args[0]}'); + 'Unknown actionType: ${args[0]}', + ); } } else { return WidgetMobileActionResult.errorResult( - 'actionType is not provided.'); + 'actionType is not provided.', + ); } } @@ -186,10 +198,12 @@ class WidgetActionHandler with HasTbContext { String imageUrl = UriData.fromBytes(imageBytes, mimeType: mimeType).toString(); return WidgetMobileActionResult.successResult( - MobileActionResult.image(imageUrl)); + MobileActionResult.image(imageUrl), + ); } else { return WidgetMobileActionResult.errorResult( - 'Unknown picture mime type'); + 'Unknown picture mime type', + ); } } else { return WidgetMobileActionResult.emptyResult(); @@ -200,7 +214,9 @@ class WidgetActionHandler with HasTbContext { } Future _launchMap( - List args, bool directionElseLocation) async { + List args, + bool directionElseLocation, + ) async { try { num? lat; num? lon; @@ -209,7 +225,8 @@ class WidgetActionHandler with HasTbContext { lon = args[2]; } else { return WidgetMobileActionResult.errorResult( - 'Missing target latitude or longitude arguments!'); + 'Missing target latitude or longitude arguments!', + ); } var url = 'https://www.google.com/maps/'; url += directionElseLocation @@ -223,11 +240,17 @@ class WidgetActionHandler with HasTbContext { Future _scanQrCode() async { try { - Barcode? barcode = await tbContext.navigateTo('/qrCodeScan', - transition: TransitionType.nativeModal); + Barcode? barcode = await tbContext.navigateTo( + '/qrCodeScan', + transition: TransitionType.nativeModal, + ); if (barcode != null && barcode.code != null) { - return WidgetMobileActionResult.successResult(MobileActionResult.qrCode( - barcode.code!, barcode.format.toString())); + return WidgetMobileActionResult.successResult( + MobileActionResult.qrCode( + barcode.code!, + barcode.format.toString(), + ), + ); } else { return WidgetMobileActionResult.emptyResult(); } @@ -238,15 +261,17 @@ class WidgetActionHandler with HasTbContext { Future _makePhoneCall(List args) async { try { - var phoneNumber; + dynamic phoneNumber; if (args.length > 1 && args[1] != null) { phoneNumber = args[1]; } else { return WidgetMobileActionResult.errorResult( - 'Missing or invalid phone number!'); + 'Missing or invalid phone number!', + ); } return WidgetMobileActionResult.successResult( - await _tryLaunch('tel://$phoneNumber')); + await _tryLaunch('tel://$phoneNumber'), + ); } catch (e) { return _handleError(e); } @@ -259,38 +284,45 @@ class WidgetActionHandler with HasTbContext { serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { return WidgetMobileActionResult.errorResult( - 'Location services are disabled.'); + 'Location services are disabled.', + ); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { return WidgetMobileActionResult.errorResult( - 'Location permissions are denied.'); + 'Location permissions are denied.', + ); } } if (permission == LocationPermission.deniedForever) { return WidgetMobileActionResult.errorResult( - 'Location permissions are permanently denied, we cannot request permissions.'); + 'Location permissions are permanently denied, we cannot request permissions.', + ); } var position = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.high); + desiredAccuracy: LocationAccuracy.high, + ); return WidgetMobileActionResult.successResult( - MobileActionResult.location(position.latitude, position.longitude)); + MobileActionResult.location(position.latitude, position.longitude), + ); } catch (e) { return _handleError(e); } } Future _takeScreenshot( - InAppWebViewController controller) async { + InAppWebViewController controller, + ) async { try { List? imageBytes = await controller.takeScreenshot(); if (imageBytes != null) { String imageUrl = UriData.fromBytes(imageBytes, mimeType: 'image/png').toString(); return WidgetMobileActionResult.successResult( - MobileActionResult.image(imageUrl)); + MobileActionResult.image(imageUrl), + ); } else { return WidgetMobileActionResult.emptyResult(); } diff --git a/lib/utils/ui/qr_code_scanner.dart b/lib/utils/ui/qr_code_scanner.dart index 779fcc2f..2cc55950 100644 --- a/lib/utils/ui/qr_code_scanner.dart +++ b/lib/utils/ui/qr_code_scanner.dart @@ -1,5 +1,5 @@ -import 'dart:io'; import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; @@ -7,10 +7,10 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; class QrCodeScannerPage extends TbPageWidget { - QrCodeScannerPage(TbContext tbContext) : super(tbContext); + QrCodeScannerPage(TbContext tbContext, {super.key}) : super(tbContext); @override - _QrCodeScannerPageState createState() => _QrCodeScannerPageState(); + State createState() => _QrCodeScannerPageState(); } class _QrCodeScannerPageState extends TbPageState { @@ -28,11 +28,6 @@ class _QrCodeScannerPageState extends TbPageState { } } - @override - void initState() { - super.initState(); - } - @override void dispose() { controller?.dispose(); @@ -45,57 +40,68 @@ class _QrCodeScannerPageState extends TbPageState { @override Widget build(BuildContext context) { return Scaffold( - body: Stack( - children: [ - _buildQrView(context), - Positioned( + body: Stack( + children: [ + _buildQrView(context), + const Positioned( bottom: 0, left: 0, right: 0, height: kToolbarHeight, child: Center( - child: Text('Scan a code', - style: TextStyle(color: Colors.white, fontSize: 20)))), - Positioned( - child: AppBar( - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - iconTheme: IconThemeData(color: Colors.white), - elevation: 0, - actions: [ - IconButton( - icon: FutureBuilder( + child: Text( + 'Scan a code', + style: TextStyle(color: Colors.white, fontSize: 20), + ), + ), + ), + Positioned( + child: AppBar( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.white), + elevation: 0, + actions: [ + IconButton( + icon: FutureBuilder( future: controller?.getFlashStatus(), builder: (context, snapshot) { - return Icon(snapshot.data == false - ? Icons.flash_on - : Icons.flash_off); - }), - onPressed: () async { - await controller?.toggleFlash(); - setState(() {}); - }, - tooltip: 'Toggle flash', - ), - IconButton( - icon: FutureBuilder( + return Icon( + snapshot.data == false + ? Icons.flash_on + : Icons.flash_off, + ); + }, + ), + onPressed: () async { + await controller?.toggleFlash(); + setState(() {}); + }, + tooltip: 'Toggle flash', + ), + IconButton( + icon: FutureBuilder( future: controller?.getCameraInfo(), builder: (context, snapshot) { - return Icon(snapshot.data == CameraFacing.front - ? Icons.camera_rear - : Icons.camera_front); - }), - onPressed: () async { - await controller?.flipCamera(); - setState(() {}); - }, - tooltip: 'Toggle camera', - ), - ], + return Icon( + snapshot.data == CameraFacing.front + ? Icons.camera_rear + : Icons.camera_front, + ); + }, + ), + onPressed: () async { + await controller?.flipCamera(); + setState(() {}); + }, + tooltip: 'Toggle camera', + ), + ], + ), ), - ) - ], - )); + ], + ), + ); } Widget _buildQrView(BuildContext context) { @@ -110,11 +116,12 @@ class _QrCodeScannerPageState extends TbPageState { key: qrKey, onQRViewCreated: _onQRViewCreated, overlay: QrScannerOverlayShape( - borderColor: Colors.red, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: scanArea), + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea, + ), ); } @@ -127,7 +134,7 @@ class _QrCodeScannerPageState extends TbPageState { pop(scanData); }); } else { - simulatedQrTimer = Timer(Duration(seconds: 3), () { + simulatedQrTimer = Timer(const Duration(seconds: 3), () { pop(Barcode('test code', BarcodeFormat.qrcode, null)); }); } diff --git a/lib/utils/ui_utils_routes.dart b/lib/utils/ui_utils_routes.dart index 308e1f0c..7d789d1a 100644 --- a/lib/utils/ui_utils_routes.dart +++ b/lib/utils/ui_utils_routes.dart @@ -5,15 +5,16 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/utils/ui/qr_code_scanner.dart'; class UiUtilsRoutes extends TbRoutes { - late var qrCodeScannerHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return QrCodeScannerPage(tbContext); - }); + late final qrCodeScannerHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return QrCodeScannerPage(tbContext); + }, + ); UiUtilsRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { - router.define("/qrCodeScan", handler: qrCodeScannerHandler); + router.define('/qrCodeScan', handler: qrCodeScannerHandler); } } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 9e4b3679..88a4ee76 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -9,19 +9,22 @@ import 'package:thingsboard_client/thingsboard_client.dart'; abstract class Utils { static const _tbImagePrefix = 'tb-image;'; static const _imageBase64UrlPrefix = 'data:image/'; - static final _imagesUrlRegexp = - RegExp('\/api\/images\/(tenant|system)\/(.*)'); + static final _imagesUrlRegexp = RegExp('/api/images/(tenant|system)/(.*)'); static final _noImageDataUri = UriData.parse( - 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==') - .contentAsBytes(); + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + ).contentAsBytes(); static const _authScheme = 'Bearer '; static const _authHeaderName = 'X-Authorization'; - static String createDashboardEntityState(EntityId? entityId, - {String? entityName, String? entityLabel, String? stateId}) { + static String createDashboardEntityState( + EntityId? entityId, { + String? entityName, + String? entityLabel, + String? stateId, + }) { var stateObj = [ - {'params': {}} + {'params': {}}, ]; if (entityId != null) { stateObj[0]['params']['entityId'] = entityId.toJson(); @@ -64,30 +67,37 @@ abstract class Utils { } static Widget imageFromTbImage( - BuildContext context, ThingsboardClient tbClient, String? imageUrl, - {Color? color, - double? width, - double? height, - String? semanticLabel, - Widget Function(BuildContext)? onError}) { + BuildContext context, + ThingsboardClient tbClient, + String? imageUrl, { + Color? color, + double? width, + double? height, + String? semanticLabel, + Widget Function(BuildContext)? onError, + }) { if (imageUrl == null || imageUrl.isEmpty) { - return _onErrorImage(context, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError); + return _onErrorImage( + context, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ); } else { imageUrl = _removeTbImagePrefix(imageUrl); if (_isImageResourceUrl(imageUrl)) { var jwtToken = tbClient.getJwtToken(); if (jwtToken == null) { - return _onErrorImage(context, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError); + return _onErrorImage( + context, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ); } var parts = imageUrl.split('/'); var key = parts[parts.length - 1]; @@ -96,105 +106,141 @@ abstract class Utils { var imageLink = getIt().getCachedEndpoint() + encodedUrl; - return _networkImage(context, imageLink, - headers: {_authHeaderName: _authScheme + jwtToken}, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError); + return _networkImage( + context, + imageLink, + headers: {_authHeaderName: _authScheme + jwtToken}, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ); } else if (_isBase64DataImageUrl(imageUrl)) { - return _imageFromBase64(context, imageUrl, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError); + return _imageFromBase64( + context, + imageUrl, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ); } else if (_isValidUrl(imageUrl)) { - return _networkImage(context, imageUrl, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError); + return _networkImage( + context, + imageUrl, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ); } else { - return _onErrorImage(context, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError); + return _onErrorImage( + context, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ); } } } - static Widget _networkImage(BuildContext context, String imageUrl, - {Map? headers, - Color? color, - double? width, - double? height, - String? semanticLabel, - Widget Function(BuildContext)? onError}) { - return Image.network(imageUrl, + static Widget _networkImage( + BuildContext context, + String imageUrl, { + Map? headers, + Color? color, + double? width, + double? height, + String? semanticLabel, + Widget Function(BuildContext)? onError, + }) { + return Image.network( + imageUrl, + headers: headers, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + errorBuilder: (context, error, stackTrace) => _svgImageFromUrl( + context, + imageUrl, headers: headers, - color: color, width: width, height: height, semanticLabel: semanticLabel, - errorBuilder: (context, error, stackTrace) => _svgImageFromUrl( - context, imageUrl, - headers: headers, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError)); + onError: onError, + ), + ); } - static Widget _imageFromBase64(BuildContext context, String base64, - {Color? color, - double? width, - double? height, - String? semanticLabel, - Widget Function(BuildContext)? onError}) { + static Widget _imageFromBase64( + BuildContext context, + String base64, { + Color? color, + double? width, + double? height, + String? semanticLabel, + Widget Function(BuildContext)? onError, + }) { var uriData = UriData.parse(base64); if (uriData.mimeType == 'image/svg+xml') { - return _svgImageFromUrl(context, base64, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError); + return _svgImageFromUrl( + context, + base64, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ); } else { - return Image.memory(uriData.contentAsBytes(), + return Image.memory( + uriData.contentAsBytes(), + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + errorBuilder: (context, error, stackTrace) => _onErrorImage( + context, color: color, width: width, height: height, semanticLabel: semanticLabel, - errorBuilder: (context, error, stackTrace) => _onErrorImage(context, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError)); + onError: onError, + ), + ); } } - static Widget _svgImageFromUrl(BuildContext context, String imageUrl, - {Map? headers, - Color? color, - double? width, - double? height, - String? semanticLabel, - Widget Function(BuildContext)? onError}) { + static Widget _svgImageFromUrl( + BuildContext context, + String imageUrl, { + Map? headers, + Color? color, + double? width, + double? height, + String? semanticLabel, + Widget Function(BuildContext)? onError, + }) { Widget image = ScalableImageWidget.fromSISource( - si: ScalableImageSource.fromSvgHttpUrl(Uri.parse(imageUrl), - httpHeaders: headers), - onError: (context) => _onErrorImage(context, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel, - onError: onError)); + si: ScalableImageSource.fromSvgHttpUrl( + Uri.parse(imageUrl), + httpHeaders: headers, + ), + onError: (context) => _onErrorImage( + context, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: onError, + ), + ); if (color != null) { var colorFilter = ColorFilter.mode(color, BlendMode.srcIn); image = ColorFiltered( @@ -212,28 +258,37 @@ abstract class Utils { return image; } - static Widget _onErrorImage(BuildContext context, - {Color? color, - double? width, - double? height, - String? semanticLabel, - Widget Function(BuildContext)? onError}) { + static Widget _onErrorImage( + BuildContext context, { + Color? color, + double? width, + double? height, + String? semanticLabel, + Widget Function(BuildContext)? onError, + }) { return onError != null ? onError(context) : _emptyImage( color: color, width: width, height: height, - semanticLabel: semanticLabel); + semanticLabel: semanticLabel, + ); } - static Widget _emptyImage( - {Color? color, double? width, double? height, String? semanticLabel}) { - return Image.memory(_noImageDataUri, - color: color, - width: width, - height: height, - semanticLabel: semanticLabel); + static Widget _emptyImage({ + Color? color, + double? width, + double? height, + String? semanticLabel, + }) { + return Image.memory( + _noImageDataUri, + color: color, + width: width, + height: height, + semanticLabel: semanticLabel, + ); } static String _removeTbImagePrefix(String url) { diff --git a/lib/widgets/tb_app_bar.dart b/lib/widgets/tb_app_bar.dart index b6460fb1..1e9adeac 100644 --- a/lib/widgets/tb_app_bar.dart +++ b/lib/widgets/tb_app_bar.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:stream_transform/stream_transform.dart'; import 'package:flutter/material.dart'; +import 'package:stream_transform/stream_transform.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; @@ -16,46 +16,41 @@ class TbAppBar extends TbContextWidget implements PreferredSizeWidget { @override final Size preferredSize; - TbAppBar(TbContext tbContext, - {this.leading, - this.title, - this.actions, - this.elevation = 8, - this.shadowColor, - this.showLoadingIndicator = false}) - : preferredSize = + TbAppBar( + TbContext tbContext, { + super.key, + this.leading, + this.title, + this.actions, + this.elevation = 8, + this.shadowColor, + this.showLoadingIndicator = false, + }) : preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)), super(tbContext); @override - _TbAppBarState createState() => _TbAppBarState(); + State createState() => _TbAppBarState(); } class _TbAppBarState extends TbContextState { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { List children = []; children.add(buildDefaultBar()); if (widget.showLoadingIndicator) { - children.add(ValueListenableBuilder( + children.add( + ValueListenableBuilder( valueListenable: loadingNotifier, builder: (context, bool loading, child) { if (loading) { - return LinearProgressIndicator(); + return const LinearProgressIndicator(); } else { return Container(height: 4); } - })); + }, + ), + ); } return Column( children: children, @@ -68,7 +63,7 @@ class _TbAppBarState extends TbContextState { title: widget.title, actions: widget.actions, elevation: widget.elevation ?? 8, - shadowColor: widget.shadowColor ?? Color(0xFFFFFFFF).withAlpha(150), + shadowColor: widget.shadowColor ?? const Color(0xFFFFFFFF).withAlpha(150), ); } } @@ -83,22 +78,24 @@ class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget { @override final Size preferredSize; - TbAppSearchBar(TbContext tbContext, - {this.elevation = 8, - this.shadowColor, - this.showLoadingIndicator = false, - this.searchHint, - this.onSearch}) - : preferredSize = + TbAppSearchBar( + TbContext tbContext, { + super.key, + this.elevation = 8, + this.shadowColor, + this.showLoadingIndicator = false, + this.searchHint, + this.onSearch, + }) : preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)), super(tbContext); @override - _TbAppSearchBarState createState() => _TbAppSearchBarState(); + State createState() => _TbAppSearchBarState(); } class _TbAppSearchBarState extends TbContextState { - final TextEditingController _filter = new TextEditingController(); + final TextEditingController _filter = TextEditingController(); final _textUpdates = StreamController(); @override @@ -126,15 +123,18 @@ class _TbAppSearchBarState extends TbContextState { List children = []; children.add(buildSearchBar()); if (widget.showLoadingIndicator) { - children.add(ValueListenableBuilder( + children.add( + ValueListenableBuilder( valueListenable: loadingNotifier, builder: (context, bool loading, child) { if (loading) { - return LinearProgressIndicator(); + return const LinearProgressIndicator(); } else { return Container(height: 4); } - })); + }, + ), + ); } return Column( children: children, @@ -143,37 +143,40 @@ class _TbAppSearchBarState extends TbContextState { AppBar buildSearchBar() { return AppBar( - centerTitle: true, - elevation: widget.elevation ?? 8, - shadowColor: widget.shadowColor ?? Color(0xFFFFFFFF).withAlpha(150), - title: TextField( - controller: _filter, - autofocus: true, - // cursorColor: Colors.white, - decoration: new InputDecoration( - border: InputBorder.none, - hintStyle: TextStyle( - color: Color(0xFF282828).withAlpha((255 * 0.38).ceil()), - ), - contentPadding: - EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15), - hintText: widget.searchHint ?? 'Search', - )), - actions: [ - ValueListenableBuilder( - valueListenable: _filter, - builder: (context, value, child) { - if (_filter.text.isNotEmpty) { - return IconButton( - icon: Icon(Icons.clear), - onPressed: () { - _filter.text = ''; - }, - ); - } else { - return Container(); - } - }) - ]); + centerTitle: true, + elevation: widget.elevation ?? 8, + shadowColor: widget.shadowColor ?? const Color(0xFFFFFFFF).withAlpha(150), + title: TextField( + controller: _filter, + autofocus: true, + // cursorColor: Colors.white, + decoration: InputDecoration( + border: InputBorder.none, + hintStyle: TextStyle( + color: const Color(0xFF282828).withAlpha((255 * 0.38).ceil()), + ), + contentPadding: + const EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15), + hintText: widget.searchHint ?? 'Search', + ), + ), + actions: [ + ValueListenableBuilder( + valueListenable: _filter, + builder: (context, value, child) { + if (_filter.text.isNotEmpty) { + return IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _filter.text = ''; + }, + ); + } else { + return Container(); + } + }, + ), + ], + ); } } diff --git a/lib/widgets/tb_progress_indicator.dart b/lib/widgets/tb_progress_indicator.dart index 99ecaef7..ee00f384 100644 --- a/lib/widgets/tb_progress_indicator.dart +++ b/lib/widgets/tb_progress_indicator.dart @@ -22,7 +22,7 @@ class TbProgressIndicator extends ProgressIndicator { ); @override - _TbProgressIndicatorState createState() => _TbProgressIndicatorState(); + State createState() => _TbProgressIndicatorState(); Color _getValueColor(BuildContext context) => valueColor?.value ?? Theme.of(context).primaryColor; @@ -37,10 +37,11 @@ class _TbProgressIndicatorState extends State void initState() { super.initState(); _controller = AnimationController( - duration: const Duration(milliseconds: 1500), - vsync: this, - upperBound: 1, - animationBehavior: AnimationBehavior.preserve); + duration: const Duration(milliseconds: 1500), + vsync: this, + upperBound: 1, + animationBehavior: AnimationBehavior.preserve, + ); _rotation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); _controller.repeat(); } @@ -61,23 +62,33 @@ class _TbProgressIndicatorState extends State Widget build(BuildContext context) { return Stack( children: [ - SvgPicture.asset(ThingsboardImage.thingsboardCenter, + SvgPicture.asset( + ThingsboardImage.thingsboardCenter, + height: widget.size, + width: widget.size, + colorFilter: ColorFilter.mode( + widget._getValueColor(context), + BlendMode.srcIn, + ), + ), + AnimatedBuilder( + animation: _rotation, + child: SvgPicture.asset( + ThingsboardImage.thingsboardOuter, height: widget.size, width: widget.size, colorFilter: ColorFilter.mode( - widget._getValueColor(context), BlendMode.srcIn)), - AnimatedBuilder( - animation: _rotation, - child: SvgPicture.asset(ThingsboardImage.thingsboardOuter, - height: widget.size, - width: widget.size, - colorFilter: ColorFilter.mode( - widget._getValueColor(context), BlendMode.srcIn)), + widget._getValueColor(context), + BlendMode.srcIn, + ), + ), builder: (BuildContext context, Widget? child) { return Transform.rotate( - angle: _rotation.value * pi * 2, child: child); + angle: _rotation.value * pi * 2, + child: child, + ); }, - ) + ), ], ); } diff --git a/lib/widgets/two_page_view.dart b/lib/widgets/two_page_view.dart index ef44a4e8..249e0809 100644 --- a/lib/widgets/two_page_view.dart +++ b/lib/widgets/two_page_view.dart @@ -31,16 +31,16 @@ class TwoPageView extends StatefulWidget { final Duration duration; final TwoPageViewController? controller; - const TwoPageView( - {Key? key, - required this.first, - required this.second, - this.controller, - this.duration = const Duration(milliseconds: 250)}) - : super(key: key); + const TwoPageView({ + Key? key, + required this.first, + required this.second, + this.controller, + this.duration = const Duration(milliseconds: 250), + }) : super(key: key); @override - _TwoPageViewState createState() => _TwoPageViewState(); + State createState() => _TwoPageViewState(); } class _TwoPageViewState extends State { @@ -64,8 +64,11 @@ class _TwoPageViewState extends State { _reverse = true; }); } - await _pageController.animateToPage(_selectedIndex, - duration: widget.duration, curve: Curves.fastOutSlowIn); + await _pageController.animateToPage( + _selectedIndex, + duration: widget.duration, + curve: Curves.fastOutSlowIn, + ); return true; } return false; @@ -74,8 +77,11 @@ class _TwoPageViewState extends State { Future _close(int index, {bool animate = true}) async { if (_selectedIndex == index) { _selectedIndex = index == 1 ? 0 : 1; - await _pageController.animateToPage(_selectedIndex, - duration: widget.duration, curve: Curves.fastOutSlowIn); + await _pageController.animateToPage( + _selectedIndex, + duration: widget.duration, + curve: Curves.fastOutSlowIn, + ); if (index == 0) { setState(() { _reverse = false; @@ -86,22 +92,17 @@ class _TwoPageViewState extends State { return false; } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { return PreloadPageView( - children: _pages, - physics: NeverScrollableScrollPhysics(), + physics: const NeverScrollableScrollPhysics(), reverse: _reverse, onPageChanged: (int position) { _selectedIndex = position; }, preloadPagesCount: 2, controller: _pageController, + children: _pages, ); } } diff --git a/lib/widgets/two_value_listenable_builder.dart b/lib/widgets/two_value_listenable_builder.dart index 2d81ffb0..1495b929 100644 --- a/lib/widgets/two_value_listenable_builder.dart +++ b/lib/widgets/two_value_listenable_builder.dart @@ -2,13 +2,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class TwoValueListenableBuilder extends StatelessWidget { - TwoValueListenableBuilder({ - Key? key, + const TwoValueListenableBuilder({ required this.firstValueListenable, required this.secondValueListenable, required this.builder, this.child, - }) : super(key: key); + super.key, + }); final ValueListenable firstValueListenable; final ValueListenable secondValueListenable; diff --git a/pubspec.lock b/pubspec.lock index 4150c3b8..cd78df05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" _flutterfire_internals: dependency: transitive description: @@ -17,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" archive: dependency: transitive description: @@ -49,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -98,7 +130,7 @@ packages: source: hosted version: "1.1.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a @@ -113,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + url: "https://pub.dev" + source: hosted + version: "1.8.0" cross_file: dependency: transitive description: @@ -133,10 +173,10 @@ packages: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "0.17.3" cupertino_icons: dependency: "direct main" description: @@ -177,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: transitive description: @@ -193,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fading_edge_scrollview: dependency: "direct main" description: @@ -326,6 +382,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_app_badger: + dependency: "direct main" + description: + name: flutter_app_badger + sha256: "64d4a279bab862ed28850431b9b446b9820aaae0bf363322d51077419f930fa8" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + url: "https://pub.dev" + source: hosted + version: "8.1.5" flutter_form_builder: dependency: "direct main" description: @@ -334,6 +406,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "5717c89f126c24e310ef36770a7f4446cba81cb27abd0ce8292d098f45682efc" + url: "https://pub.dev" + source: hosted + version: "3.0.0-alpha.5" flutter_inappwebview: dependency: "direct main" description: @@ -350,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" flutter_local_notifications: dependency: "direct main" description: @@ -435,6 +523,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + url: "https://pub.dev" + source: hosted + version: "3.1.0" flutter_speed_dial: dependency: "direct main" description: @@ -477,6 +573,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" geolocator: dependency: "direct main" description: @@ -525,6 +629,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" html: dependency: transitive description: @@ -541,6 +661,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -637,6 +765,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" jovial_misc: dependency: transitive description: @@ -677,6 +813,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" logger: dependency: "direct main" description: @@ -685,22 +853,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2+1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" material_design_icons_flutter: dependency: "direct main" description: @@ -713,10 +889,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: "direct main" description: @@ -725,6 +901,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + numerus: + dependency: transitive + description: + name: numerus + sha256: "0087ef729d63b96cb347a9c44b9c592f21cecb3605b415bbd18710aef80ce5cb" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -745,10 +961,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -837,6 +1053,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" preload_page_view: dependency: "direct main" description: @@ -845,6 +1069,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" qr_code_scanner: dependency: "direct main" description: @@ -853,6 +1093,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -866,6 +1138,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.12" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -922,6 +1210,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + url: "https://pub.dev" + source: hosted + version: "1.24.9" test_api: dependency: transitive description: @@ -930,14 +1226,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" thingsboard_client: dependency: "direct main" description: name: thingsboard_client - sha256: addae7f2ae7c14234f97c64313f4f7a1f98765624cda04935dd5cce329d364bf + sha256: f8573d3203e9a201dd70322a72b013e098fa6b5fd03ae82ef077f2831b0141f8 url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.1.1" + timeago: + dependency: "direct main" + description: + name: timeago + sha256: d3204eb4c788214883380253da7f23485320a58c11d145babc82ad16bf4e7764 + url: "https://pub.dev" + source: hosted + version: "3.6.1" timezone: dependency: transitive description: @@ -954,6 +1266,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uni_links: + dependency: "direct main" + description: + name: uni_links + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + uni_links_platform_interface: + dependency: transitive + description: + name: uni_links_platform_interface + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + uni_links_web: + dependency: transitive + description: + name: uni_links_web + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" + source: hosted + version: "0.1.0" universal_html: dependency: "direct main" description: @@ -1082,6 +1418,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: @@ -1098,6 +1450,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a7d76a30..0aaf1628 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: get_it: ^7.6.7 equatable: ^2.0.5 uni_links: ^0.5.1 + collection: ^1.18.0 dev_dependencies: flutter_test: @@ -63,6 +64,7 @@ dev_dependencies: flutter_launcher_icons: ^0.13.1 mocktail: ^1.0.3 bloc_test: ^9.1.7 + flutter_lints: ^2.0.0 flutter: uses-material-design: true diff --git a/test/ core/noauth/switch_endpoint_test.dart b/test/ core/noauth/switch_endpoint_test.dart index c63fa112..73f39233 100644 --- a/test/ core/noauth/switch_endpoint_test.dart +++ b/test/ core/noauth/switch_endpoint_test.dart @@ -59,7 +59,7 @@ void main() { (e) => e.message, 'error message', 'An empty request data received.', - ) + ), ], ); @@ -134,7 +134,8 @@ void main() { ), ).thenAnswer( (invocation) { - final onError = invocation.namedArguments[Symbol('onError')]; + final onError = + invocation.namedArguments[const Symbol('onError')]; onError( ThingsboardError(message: 'TBClient re-init error message'), ); @@ -212,7 +213,7 @@ void main() { ), ).thenAnswer( (invocation) { - final onDone = invocation.namedArguments[Symbol('onDone')]; + final onDone = invocation.namedArguments[const Symbol('onDone')]; onDone(); return Future.value(); diff --git a/test/widget_test.dart b/test/widget_test.dart index fab161e3..b0591923 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -7,20 +7,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:thingsboard_app/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(ThingsboardApp()); + await tester.pumpWidget(const ThingsboardApp()); // Verify that our counter starts at 0. - expect(find.byWidgetPredicate((widget) { - if (widget is MaterialApp) { - return widget.title == 'ThingsBoard'; - } - return false; - }), findsOneWidget); + expect( + find.byWidgetPredicate((widget) { + if (widget is MaterialApp) { + return widget.title == 'ThingsBoard'; + } + return false; + }), + findsOneWidget, + ); }); } From e780d392ba57c4b688611a43e60dfd5f87cc3319 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 7 Jun 2024 15:14:03 +0300 Subject: [PATCH 21/61] The FlutterSecureStorage changed to Hive to fix an issue when an app was deleted data persisted. --- lib/locator.dart | 9 ++++-- lib/main.dart | 4 ++- lib/utils/services/_tb_secure_storage.dart | 32 +++++++++++++++++++--- pubspec.yaml | 5 ++++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/lib/locator.dart b/lib/locator.dart index 6b679151..47a7071a 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,18 +1,21 @@ import 'package:get_it/get_it.dart'; import 'package:thingsboard_app/config/routes/router.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/utils/services/_tb_secure_storage.dart'; import 'package:thingsboard_app/utils/services/endpoint/endpoint_service.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; import 'package:thingsboard_app/utils/services/local_database/local_database_service.dart'; -import 'package:thingsboard_app/utils/services/tb_app_storage.dart'; import 'utils/services/firebase/firebase_service.dart'; final getIt = GetIt.instance; -void setUpRootDependencies() { +Future setUpRootDependencies() async { + final secureStorage = createAppStorage() as TbSecureStorage; + await secureStorage.init(); + getIt ..registerSingleton( ThingsboardAppRouter(), @@ -22,7 +25,7 @@ void setUpRootDependencies() { ) ..registerLazySingleton( () => LocalDatabaseService( - storage: createAppStorage(), + storage: secureStorage, logger: getIt(), ), ) diff --git a/lib/main.dart b/lib/main.dart index 0bb0782a..8712ea51 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:thingsboard_app/config/routes/router.dart'; import 'package:thingsboard_app/constants/database_keys.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; @@ -23,8 +24,9 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); // await FlutterDownloader.initialize(); // await Permission.storage.request(); + await Hive.initFlutter(); - setUpRootDependencies(); + await setUpRootDependencies(); if (UniversalPlatform.isAndroid) { await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true); } diff --git a/lib/utils/services/_tb_secure_storage.dart b/lib/utils/services/_tb_secure_storage.dart index e88a88ce..d2201a02 100644 --- a/lib/utils/services/_tb_secure_storage.dart +++ b/lib/utils/services/_tb_secure_storage.dart @@ -1,23 +1,47 @@ +import 'dart:convert'; + import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; TbStorage createAppStorage() => TbSecureStorage(); class TbSecureStorage implements TbStorage { - final flutterStorage = const FlutterSecureStorage(); + late Box encryptedBox; + + Future init() async { + const secureStorage = FlutterSecureStorage(); + // if key not exists return null + final encryptionKeyString = await secureStorage.read(key: 'key'); + if (encryptionKeyString == null) { + final key = Hive.generateSecureKey(); + await secureStorage.write( + key: 'key', + value: base64UrlEncode(key), + ); + } + + final key = await secureStorage.read(key: 'key'); + final encryptionKeyUint8List = base64Url.decode(key!); + + encryptedBox = await Hive.openBox( + 'securedStorage', + encryptionCipher: HiveAesCipher(encryptionKeyUint8List), + ); + } @override Future deleteItem(String key) async { - return await flutterStorage.delete(key: key); + return await encryptedBox.delete(key); } @override Future getItem(String key) async { - return await flutterStorage.read(key: key); + return await encryptedBox.get(key); } @override Future setItem(String key, String value) async { - return await flutterStorage.write(key: key, value: value); + return await encryptedBox.put(key, value); } } diff --git a/pubspec.yaml b/pubspec.yaml index 0aaf1628..cb11b327 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,9 @@ dependencies: equatable: ^2.0.5 uni_links: ^0.5.1 collection: ^1.18.0 + html: ^0.15.4 + hive: ^2.2.3 + hive_flutter: ^1.1.0 dev_dependencies: flutter_test: @@ -65,6 +68,8 @@ dev_dependencies: mocktail: ^1.0.3 bloc_test: ^9.1.7 flutter_lints: ^2.0.0 + hive_generator: ^2.0.1 + build_runner: ^2.4.9 flutter: uses-material-design: true From f4bf9892e0b5e90b86aa32c19ca638a3366a3b03 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 11 Jun 2024 12:25:35 +0300 Subject: [PATCH 22/61] Fixed an issue with Notification service initialization when 2FA enabled. --- lib/core/context/tb_context.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index d4e8b1ad..c7fafd3a 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -326,7 +326,7 @@ class TbContext implements PopEntry { await updateRouteState(); } - if (tbClient.getAuthUser()?.userId != null) { + if (isAuthenticated) { if (getIt().apps.isNotEmpty) { await NotificationService().init(tbClient, log, this); } From e826436ccf941ac2a89b1fe8a3b751c8327cf049 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 11 Jun 2024 16:57:16 +0300 Subject: [PATCH 23/61] Fixed an issue app crashing when click on notification after using qr code. --- lib/modules/url/url_page.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/modules/url/url_page.dart b/lib/modules/url/url_page.dart index 3e48ecb5..3b746232 100644 --- a/lib/modules/url/url_page.dart +++ b/lib/modules/url/url_page.dart @@ -1,20 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.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_app_bar.dart'; import 'package:universal_platform/universal_platform.dart'; import 'package:url_launcher/url_launcher_string.dart'; -class UrlPage extends StatelessWidget { - const UrlPage({ +class UrlPage extends TbPageWidget { + UrlPage({ required this.url, - required this.tbContext, + required TbContext tbContext, super.key, - }); + }) : super(tbContext); final String url; - final TbContext tbContext; + @override + State createState() => _UrlPageState(); +} + +class _UrlPageState extends TbPageState { @override Widget build(BuildContext context) { return Scaffold( @@ -24,7 +29,7 @@ class UrlPage extends StatelessWidget { actions: [ IconButton( onPressed: () { - launchUrlString(url, mode: LaunchMode.externalApplication); + launchUrlString(widget.url, mode: LaunchMode.externalApplication); }, icon: const Icon(Icons.open_in_browser), ), @@ -34,7 +39,7 @@ class UrlPage extends StatelessWidget { ? const Center(child: Text('Not implemented!')) : InAppWebView( initialUrlRequest: URLRequest( - url: Uri.parse(url), + url: Uri.parse(widget.url), ), androidOnPermissionRequest: (controller, origin, resources) async { From 3711c7843cb8f221f48af6fcbab8f99ec4936cdb Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 13 Jun 2024 11:06:47 +0300 Subject: [PATCH 24/61] Version bumped to 1.2.0 --- ios/Runner/Runner.entitlements | 2 +- pubspec.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 4dde98e3..e01669d6 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -6,7 +6,7 @@ development com.apple.developer.associated-domains - applinks:qr.thingsboard.fun + applinks:demo.thingsboard.io diff --git a/pubspec.yaml b/pubspec.yaml index cb11b327..27466cbf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter ThingsBoard Mobile Application publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.1.1 +version: 1.2.0 environment: sdk: ">=3.2.0 <4.0.0" @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - thingsboard_client: ^1.1.1 + thingsboard_client: ^1.2.0 intl: ^0.18.1 flutter_secure_storage: ^9.0.0 flutter_speed_dial: ^7.0.0 From 6f0944abb0b631e1e7f6fa1edf05735b80eb60cd Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 17 Jun 2024 11:59:32 +0300 Subject: [PATCH 25/61] Hotfix dashboard never load after login to the same host. --- android/app/src/main/AndroidManifest.xml | 2 ++ .../remote/i_noauth_remote_datasource.dart | 4 ++++ .../remote/noauth_remote_datasource.dart | 10 ++++++++ .../data/repository/noauth_repository.dart | 10 ++++++++ .../repository/i_noauth_repository.dart | 4 ++++ .../usecases/switch_endpoint_usecase.dart | 10 +++++++- lib/utils/services/_tb_secure_storage.dart | 2 +- .../services/endpoint/endpoint_service.dart | 23 ++++++++++--------- 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b43c30eb..176199e6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -25,6 +25,8 @@ { _progressSteamCtrl.add('Getting data from your host $host'); final loginData = await repository.getJwtToken(host: host, key: key); + final authUserFromJwt = repository.getAuthUserFromJwt(loginData.token); + final currentlyAuthUser = + repository.getCurrentlyAuthenticatedUserOrNull(); + if (authUserFromJwt.userId == currentlyAuthUser?.userId) { + params.onDone(); + return; + } + if (repository.isAuthenticated()) { _progressSteamCtrl.add('Logout you ...'); await repository.logout( @@ -65,13 +73,13 @@ class SwitchEndpointUseCase extends UseCase { } await repository.setUserFromJwtToken(loginData); + await getIt().setEndpoint(host); if (!isTheSameHost) { logger.debug('SwitchEndpointUseCase:deleteFB App'); await getIt() ..removeApp() ..removeApp(name: currentEndpoint); - await getIt().setEndpoint(host); // If we revert to the original host configured in the app_constants if (!await getIt().isCustomEndpoint()) { diff --git a/lib/utils/services/_tb_secure_storage.dart b/lib/utils/services/_tb_secure_storage.dart index d2201a02..53e1c45f 100644 --- a/lib/utils/services/_tb_secure_storage.dart +++ b/lib/utils/services/_tb_secure_storage.dart @@ -10,7 +10,7 @@ class TbSecureStorage implements TbStorage { late Box encryptedBox; Future init() async { - const secureStorage = FlutterSecureStorage(); + final secureStorage = FlutterSecureStorage(); // if key not exists return null final encryptionKeyString = await secureStorage.read(key: 'key'); if (encryptionKeyString == null) { diff --git a/lib/utils/services/endpoint/endpoint_service.dart b/lib/utils/services/endpoint/endpoint_service.dart index 8629af92..f201c2d5 100644 --- a/lib/utils/services/endpoint/endpoint_service.dart +++ b/lib/utils/services/endpoint/endpoint_service.dart @@ -8,14 +8,18 @@ class EndpointService implements IEndpointService { EndpointService({required this.databaseService}); final ILocalDatabaseService databaseService; - final _cachedEndpoint = ValueNotifier(null); + String? _cachedEndpoint; + final _notifierValue = ValueNotifier(UniqueKey().toString()); @override - ValueListenable get listenEndpointChanges => _cachedEndpoint; + ValueListenable get listenEndpointChanges => _notifierValue; @override Future setEndpoint(String endpoint) async { - _cachedEndpoint.value = endpoint; + print('setEndpoint'); + + _cachedEndpoint = endpoint; + _notifierValue.value = UniqueKey().toString(); await databaseService.setItem( DatabaseKeys.thingsBoardApiEndpointKey, @@ -25,24 +29,21 @@ class EndpointService implements IEndpointService { @override Future getEndpoint() async { - _cachedEndpoint.value ??= await databaseService.getItem( + _cachedEndpoint ??= await databaseService.getItem( DatabaseKeys.thingsBoardApiEndpointKey, ); - return _cachedEndpoint.value ?? - ThingsboardAppConstants.thingsBoardApiEndpoint; + return _cachedEndpoint ?? ThingsboardAppConstants.thingsBoardApiEndpoint; } @override Future isCustomEndpoint() async { - _cachedEndpoint.value ??= await getEndpoint(); - return _cachedEndpoint.value != - ThingsboardAppConstants.thingsBoardApiEndpoint; + _cachedEndpoint ??= await getEndpoint(); + return _cachedEndpoint != ThingsboardAppConstants.thingsBoardApiEndpoint; } @override String getCachedEndpoint() { - return _cachedEndpoint.value ?? - ThingsboardAppConstants.thingsBoardApiEndpoint; + return _cachedEndpoint ?? ThingsboardAppConstants.thingsBoardApiEndpoint; } } From 61bb12d7dc153e38edcae3792b59bd03e24dc351 Mon Sep 17 00:00:00 2001 From: Salem Aljebaly <56629923+salemaljebaly@users.noreply.github.com> Date: Tue, 9 Jul 2024 11:28:47 +0200 Subject: [PATCH 26/61] Support flutter 3.22.X (#97) * fix: update thingsboard_client to fix unsupported platform * chore: update flutter packages * chore: upadte iOS platflrom to 13 * refactor: migration from flutter_inappwebview 5 replace Uri with WebUri --------- Co-authored-by: Yevhen Beshkarov --- ios/Podfile | 2 +- .../widgets/endpoint_name_widget.dart | 3 ++- lib/modules/url/url_page.dart | 2 +- pubspec.yaml | 22 +++++++++---------- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 279576f3..10f3c9b4 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart index 9c90f4e3..e922ae9b 100644 --- a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart +++ b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; class EndpointNameWidget extends StatelessWidget { const EndpointNameWidget({required this.endpoint, super.key}); @@ -17,7 +18,7 @@ class EndpointNameWidget extends StatelessWidget { padding: const EdgeInsets.all(5), child: Center( child: Text( - Uri.parse(endpoint).host, + WebUri(endpoint).host, style: Theme.of(context) .textTheme .labelSmall diff --git a/lib/modules/url/url_page.dart b/lib/modules/url/url_page.dart index 3b746232..bcb7d6de 100644 --- a/lib/modules/url/url_page.dart +++ b/lib/modules/url/url_page.dart @@ -39,7 +39,7 @@ class _UrlPageState extends TbPageState { ? const Center(child: Text('Not implemented!')) : InAppWebView( initialUrlRequest: URLRequest( - url: Uri.parse(widget.url), + url: WebUri(widget.url.toString()), ), androidOnPermissionRequest: (controller, origin, resources) async { diff --git a/pubspec.yaml b/pubspec.yaml index 27466cbf..f6346785 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,8 +11,8 @@ environment: dependencies: flutter: sdk: flutter - thingsboard_client: ^1.2.0 - intl: ^0.18.1 + thingsboard_client: ^1.2.1 + intl: ^0.19.0 flutter_secure_storage: ^9.0.0 flutter_speed_dial: ^7.0.0 cupertino_icons: ^1.0.6 @@ -23,7 +23,7 @@ dependencies: infinite_scroll_pagination: ^4.0.0 fading_edge_scrollview: ^4.0.0 stream_transform: ^2.1.0 - flutter_inappwebview: ^5.8.0 + flutter_inappwebview: ^6.0.0 # flutter_downloader: ^1.6.0 # permission_handler: ^8.0.0+2 # path_provider: ^2.0.2 @@ -32,23 +32,23 @@ dependencies: mime: ^1.0.4 logger: ^2.0.2+1 qr_code_scanner: ^1.0.1 - device_info_plus: ^9.1.1 - geolocator: ^10.1.0 + device_info_plus: ^10.1.0 + geolocator: ^12.0.0 material_design_icons_flutter: ^7.0.7296 - package_info_plus: ^5.0.1 + package_info_plus: ^8.0.0 dart_jsonwebtoken: ^2.12.1 crypto: ^3.0.3 flutter_form_builder: ^9.1.1 - form_builder_validators: ^9.1.0 - flutter_html: 3.0.0-alpha.5 + form_builder_validators: ^10.0.1 + flutter_html: 3.0.0-beta.2 universal_html: ^2.2.4 universal_platform: ^1.0.0+1 preload_page_view: ^0.2.0 flutter_localizations: sdk: flutter - firebase_core: ^2.24.2 - firebase_messaging: ^14.7.10 - flutter_local_notifications: ^16.3.0 + firebase_core: ^3.1.0 + firebase_messaging: ^15.0.1 + flutter_local_notifications: ^17.1.2 flutter_app_badger: ^1.5.0 timeago: ^3.6.1 flutter_slidable: ^3.0.1 From 3d9b78099c94b0f469486dc5dda4b697059ef4fd Mon Sep 17 00:00:00 2001 From: Salem Aljebaly <56629923+salemaljebaly@users.noreply.github.com> Date: Tue, 9 Jul 2024 11:32:09 +0200 Subject: [PATCH 27/61] Add Arabic locale (#96) * Add Arabic locale * Added a new line. --------- Co-authored-by: Yevhen Beshkarov --- lib/generated/intl/messages_all.dart | 4 + lib/generated/intl/messages_ar.dart | 129 +++++++++++++++++++++++++++ lib/generated/l10n.dart | 1 + lib/l10n/intl_ar.arb | 112 +++++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 lib/generated/intl/messages_ar.dart create mode 100644 lib/l10n/intl_ar.arb diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart index ad8c0bc3..3838d266 100644 --- a/lib/generated/intl/messages_all.dart +++ b/lib/generated/intl/messages_all.dart @@ -18,11 +18,13 @@ import 'package:intl/src/intl_helpers.dart'; import 'messages_en.dart' as messages_en; import 'messages_zh.dart' as messages_zh; +import 'messages_ar.dart' as messages_ar; typedef Future LibraryLoader(); Map _deferredLibraries = { 'en': () => new SynchronousFuture(null), 'zh': () => new SynchronousFuture(null), + 'ar': () => new SynchronousFuture(null), }; MessageLookupByLibrary? _findExact(String localeName) { @@ -31,6 +33,8 @@ MessageLookupByLibrary? _findExact(String localeName) { return messages_en.messages; case 'zh': return messages_zh.messages; + case 'ar': + return messages_ar.messages; default: return null; } diff --git a/lib/generated/intl/messages_ar.dart b/lib/generated/intl/messages_ar.dart new file mode 100644 index 00000000..1845c26f --- /dev/null +++ b/lib/generated/intl/messages_ar.dart @@ -0,0 +1,129 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a ar locale. All the +// messages from the main program should be duplicated here with the same +// function name. +// @dart=2.12 +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = MessageLookup(); + +typedef String? MessageIfAbsent( + String? messageStr, List? args); + +class MessageLookup extends MessageLookupByLibrary { + @override + String get localeName => 'ar'; + + static m0(contact) => "تم إرسال رمز أمني إلى بريدك الإلكتروني على العنوان ${contact}."; + + static m1(time) => "إعادة إرسال الرمز في {time, plural, =1{ثانية واحدة} other{${time} ثواني}}"; + + static m2(contact) => "تم إرسال رمز أمني إلى هاتفك على الرقم ${contact}."; + + @override + final Map messages = _notInlinedMessages(_notInlinedMessages); + + static Map _notInlinedMessages(_) => { + 'No': MessageLookupByLibrary.simpleMessage('لا'), + 'OR': MessageLookupByLibrary.simpleMessage('أو'), + 'Yes': MessageLookupByLibrary.simpleMessage('نعم'), + 'actionData': MessageLookupByLibrary.simpleMessage('بيانات الإجراء'), + 'active': MessageLookupByLibrary.simpleMessage('نشط'), + 'address': MessageLookupByLibrary.simpleMessage('العنوان'), + 'address2': MessageLookupByLibrary.simpleMessage('العنوان 2'), + 'alarmAcknowledgeText': MessageLookupByLibrary.simpleMessage('هل أنت متأكد أنك تريد الإقرار بالتنبيه؟'), + 'alarmAcknowledgeTitle': MessageLookupByLibrary.simpleMessage('إقرار التنبيه'), + 'alarmClearText': MessageLookupByLibrary.simpleMessage('هل أنت متأكد أنك تريد مسح التنبيه؟'), + 'alarmClearTitle': MessageLookupByLibrary.simpleMessage('مسح التنبيه'), + 'alarms': MessageLookupByLibrary.simpleMessage('التنبيهات'), + 'allDevices': MessageLookupByLibrary.simpleMessage('جميع الأجهزة'), + 'appTitle': MessageLookupByLibrary.simpleMessage('ThingsBoard'), + 'assetName': MessageLookupByLibrary.simpleMessage('اسم الأصل'), + 'assets': MessageLookupByLibrary.simpleMessage('الأصول'), + 'assignedToCustomer': MessageLookupByLibrary.simpleMessage('معين للعميل'), + 'auditLogDetails': MessageLookupByLibrary.simpleMessage('تفاصيل سجل التدقيق'), + 'auditLogs': MessageLookupByLibrary.simpleMessage('سجلات التدقيق'), + 'backupCodeAuthDescription': MessageLookupByLibrary.simpleMessage('يرجى إدخال أحد الرموز الاحتياطية الخاصة بك.'), + 'backupCodeAuthPlaceholder': MessageLookupByLibrary.simpleMessage('الرمز الاحتياطي'), + 'changePassword': MessageLookupByLibrary.simpleMessage('تغيير كلمة المرور'), + 'city': MessageLookupByLibrary.simpleMessage('المدينة'), + 'continueText': MessageLookupByLibrary.simpleMessage('استمرار'), + 'country': MessageLookupByLibrary.simpleMessage('البلد'), + 'currentPassword': MessageLookupByLibrary.simpleMessage('كلمة المرور الحالية'), + 'currentPasswordRequireText': MessageLookupByLibrary.simpleMessage('كلمة المرور الحالية مطلوبة.'), + 'currentPasswordStar': MessageLookupByLibrary.simpleMessage('كلمة المرور الحالية *'), + 'customer': MessageLookupByLibrary.simpleMessage('العميل'), + 'customers': MessageLookupByLibrary.simpleMessage('العملاء'), + 'devices': MessageLookupByLibrary.simpleMessage('الأجهزة'), + 'email': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني'), + 'emailAuthDescription': m0, + 'emailAuthPlaceholder': MessageLookupByLibrary.simpleMessage('رمز البريد الإلكتروني'), + 'emailInvalidText': MessageLookupByLibrary.simpleMessage('صيغة البريد الإلكتروني غير صحيحة.'), + 'emailRequireText': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني مطلوب.'), + 'emailStar': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني *'), + 'entityType': MessageLookupByLibrary.simpleMessage('نوع الكيان'), + 'failureDetails': MessageLookupByLibrary.simpleMessage('تفاصيل الفشل'), + 'firstName': MessageLookupByLibrary.simpleMessage('الاسم الأول'), + 'firstNameUpper': MessageLookupByLibrary.simpleMessage('الاسم الأول'), + 'home': MessageLookupByLibrary.simpleMessage('الرئيسية'), + 'inactive': MessageLookupByLibrary.simpleMessage('غير نشط'), + 'label': MessageLookupByLibrary.simpleMessage('التسمية'), + 'lastName': MessageLookupByLibrary.simpleMessage('الاسم الأخير'), + 'lastNameUpper': MessageLookupByLibrary.simpleMessage('الاسم الأخير'), + 'listIsEmptyText': MessageLookupByLibrary.simpleMessage('القائمة فارغة حالياً.'), + 'login': MessageLookupByLibrary.simpleMessage('تسجيل دخول'), + 'loginNotification': MessageLookupByLibrary.simpleMessage('تسجيل الدخول إلى حسابك'), + 'logoDefaultValue': MessageLookupByLibrary.simpleMessage('شعار ثينغز بورد'), + 'logout': MessageLookupByLibrary.simpleMessage('تسجيل خروج'), + 'mfaProviderBackupCode': MessageLookupByLibrary.simpleMessage('الرمز الاحتياطي'), + 'mfaProviderEmail': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني'), + 'mfaProviderSms': MessageLookupByLibrary.simpleMessage('SMS'), + 'mfaProviderTopt': MessageLookupByLibrary.simpleMessage('تطبيق المصادقة'), + 'more': MessageLookupByLibrary.simpleMessage('المزيد'), + 'newPassword': MessageLookupByLibrary.simpleMessage('كلمة المرور الجديدة'), + 'newPassword2': MessageLookupByLibrary.simpleMessage('تأكيد كلمة المرور الجديدة'), + 'newPassword2RequireText': MessageLookupByLibrary.simpleMessage('تأكيد كلمة المرور الجديدة مطلوب.'), + 'newPassword2Star': MessageLookupByLibrary.simpleMessage('تأكيد كلمة المرور الجديدة *'), + 'newPasswordRequireText': MessageLookupByLibrary.simpleMessage('كلمة المرور الجديدة مطلوبة.'), + 'newPasswordStar': MessageLookupByLibrary.simpleMessage('كلمة المرور الجديدة *'), + 'notImplemented': MessageLookupByLibrary.simpleMessage('لم يتم التنفيذ!'), + 'password': MessageLookupByLibrary.simpleMessage('كلمة المرور'), + 'passwordErrorNotification': MessageLookupByLibrary.simpleMessage('كلمات المرور المدخلة يجب أن تكون متطابقة!'), + 'passwordForgotText': MessageLookupByLibrary.simpleMessage('هل نسيت كلمة المرور؟'), + 'passwordRequireText': MessageLookupByLibrary.simpleMessage('كلمة المرور مطلوبة.'), + 'passwordReset': MessageLookupByLibrary.simpleMessage('إعادة تعيين كلمة المرور'), + 'passwordResetLinkSuccessfullySentNotification': MessageLookupByLibrary.simpleMessage('تم إرسال رابط إعادة تعيين كلمة المرور بنجاح!'), + 'passwordResetText': MessageLookupByLibrary.simpleMessage('أدخل البريد الإلكتروني المرتبط بحسابك وسنرسل بريدًا إلكترونيًا يحتوي على رابط لإعادة تعيين كلمة المرور'), + 'passwordSuccessNotification': MessageLookupByLibrary.simpleMessage('تم تغيير كلمة المرور بنجاح'), + 'phone': MessageLookupByLibrary.simpleMessage('الهاتف'), + 'postalCode': MessageLookupByLibrary.simpleMessage('الرمز البريدي'), + 'profileSuccessNotification': MessageLookupByLibrary.simpleMessage('تم تحديث الملف الشخصي بنجاح'), + 'requestPasswordReset': MessageLookupByLibrary.simpleMessage('طلب إعادة تعيين كلمة المرور'), + 'resendCode': MessageLookupByLibrary.simpleMessage('إعادة إرسال الرمز'), + 'resendCodeWait': m1, + 'selectWayToVerify': MessageLookupByLibrary.simpleMessage('اختر طريقة للتحقق'), + 'smsAuthDescription': m2, + 'smsAuthPlaceholder': MessageLookupByLibrary.simpleMessage('رمز SMS'), + 'stateOrProvince': MessageLookupByLibrary.simpleMessage('الولاية / المقاطعة'), + 'systemAdministrator': MessageLookupByLibrary.simpleMessage('مسؤول النظام'), + 'tenantAdministrator': MessageLookupByLibrary.simpleMessage('مسؤول المستأجر'), + 'title': MessageLookupByLibrary.simpleMessage('العنوان'), + 'toptAuthPlaceholder': MessageLookupByLibrary.simpleMessage('الرمز'), + 'totpAuthDescription': MessageLookupByLibrary.simpleMessage('يرجى إدخال الرمز الأمني من تطبيق المصادقة الخاص بك.'), + 'tryAgain': MessageLookupByLibrary.simpleMessage('حاول مرة أخرى'), + 'tryAnotherWay': MessageLookupByLibrary.simpleMessage('حاول بطريقة أخرى'), + 'type': MessageLookupByLibrary.simpleMessage('النوع'), + 'username': MessageLookupByLibrary.simpleMessage('اسم المستخدم'), + 'verificationCodeIncorrect': MessageLookupByLibrary.simpleMessage('الرمز غير صحيح'), + 'verificationCodeInvalid': MessageLookupByLibrary.simpleMessage('صيغة الرمز غير صالحة'), + 'verificationCodeManyRequest': MessageLookupByLibrary.simpleMessage('طلبات كثيرة للتحقق من الرمز'), + 'verifyYourIdentity': MessageLookupByLibrary.simpleMessage('تحقق من هويتك') + }; +} diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 7965265c..ed3a6508 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -998,6 +998,7 @@ class AppLocalizationDelegate extends LocalizationsDelegate { return const [ Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'zh'), + Locale.fromSubtags(languageCode: 'ar'), ]; } diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb new file mode 100644 index 00000000..18c5af07 --- /dev/null +++ b/lib/l10n/intl_ar.arb @@ -0,0 +1,112 @@ +{ + "appTitle": "ThingsBoard", + + "home": "الرئيسية", + "alarms": "التنبيهات", + "devices": "الأجهزة", + "more": "المزيد", + + "customers": "العملاء", + "assets": "الأصول", + "auditLogs": "سجلات التدقيق", + "logout": "تسجيل خروج", + "login": "تسجيل دخول", + + "logoDefaultValue": "شعار ثينغز بورد", + "loginNotification": "تسجيل الدخول إلى حسابك", + "email": "البريد الإلكتروني", + "emailRequireText": "البريد الإلكتروني مطلوب.", + "emailInvalidText": "صيغة البريد الإلكتروني غير صحيحة.", + "username": "اسم المستخدم", + "password": "كلمة المرور", + "passwordRequireText": "كلمة المرور مطلوبة.", + "passwordForgotText": "هل نسيت كلمة المرور؟", + "passwordReset": "إعادة تعيين كلمة المرور", + "passwordResetText": "أدخل البريد الإلكتروني المرتبط بحسابك وسنرسل بريدًا إلكترونيًا يحتوي على رابط لإعادة تعيين كلمة المرور", + "requestPasswordReset": "طلب إعادة تعيين كلمة المرور", + "passwordResetLinkSuccessfullySentNotification": "تم إرسال رابط إعادة تعيين كلمة المرور بنجاح!", + + "OR": "أو", + "No": "لا", + "Yes": "نعم", + + "title": "العنوان", + "country": "البلد", + "city": "المدينة", + "stateOrProvince": "الولاية / المقاطعة", + "postalCode": "الرمز البريدي", + "address": "العنوان", + "address2": "العنوان 2", + "phone": "الهاتف", + + "alarmClearTitle": "مسح التنبيه", + "alarmClearText": "هل أنت متأكد أنك تريد مسح التنبيه؟", + + "alarmAcknowledgeTitle": "إقرار التنبيه", + "alarmAcknowledgeText": "هل أنت متأكد أنك تريد الإقرار بالتنبيه؟", + + "assetName": "اسم الأصل", + "type": "النوع", + "label": "التسمية", + "assignedToCustomer": "معين للعميل", + + "auditLogDetails": "تفاصيل سجل التدقيق", + "entityType": "نوع الكيان", + "actionData": "بيانات الإجراء", + "failureDetails": "تفاصيل الفشل", + + "allDevices": "جميع الأجهزة", + "active": "نشط", + "inactive": "غير نشط", + + "systemAdministrator": "مسؤول النظام", + "tenantAdministrator": "مسؤول المستأجر", + "customer": "العميل", + + "changePassword": "تغيير كلمة المرور", + "currentPassword": "كلمة المرور الحالية", + "currentPasswordRequireText": "كلمة المرور الحالية مطلوبة.", + "currentPasswordStar": "كلمة المرور الحالية *", + "newPassword": "كلمة المرور الجديدة", + "newPasswordRequireText": "كلمة المرور الجديدة مطلوبة.", + "newPasswordStar": "كلمة المرور الجديدة *", + "newPassword2": "تأكيد كلمة المرور الجديدة", + "newPassword2RequireText": "تأكيد كلمة المرور الجديدة مطلوب.", + "newPassword2Star": "تأكيد كلمة المرور الجديدة *", + "passwordErrorNotification": "كلمات المرور المدخلة يجب أن تكون متطابقة!", + + "emailStar": "البريد الإلكتروني *", + "firstName": "الاسم الأول", + "firstNameUpper": "الاسم الأول", + "lastName": "الاسم الأخير", + "lastNameUpper": "الاسم الأخير", + "profileSuccessNotification": "تم تحديث الملف الشخصي بنجاح", + "passwordSuccessNotification": "تم تغيير كلمة المرور بنجاح", + + "notImplemented": "لم يتم التنفيذ!", + + "listIsEmptyText": "القائمة فارغة حالياً.", + "tryAgain": "حاول مرة أخرى", + + "verifyYourIdentity": "تحقق من هويتك", + "continueText": "استمرار", + "resendCode": "إعادة إرسال الرمز", + "resendCodeWait": "إعادة إرسال الرمز في {time, plural, =1{ثانية واحدة} other{{time} ثواني}}", + "totpAuthDescription": "يرجى إدخال الرمز الأمني من تطبيق المصادقة الخاص بك.", + "smsAuthDescription": "تم إرسال رمز أمني إلى هاتفك على الرقم {contact}.", + "emailAuthDescription": "تم إرسال رمز أمني إلى بريدك الإلكتروني على العنوان {contact}.", + "backupCodeAuthDescription": "يرجى إدخال أحد الرموز الاحتياطية الخاصة بك.", + "verificationCodeInvalid": "صيغة الرمز غير صالحة", + "toptAuthPlaceholder": "الرمز", + "smsAuthPlaceholder": "رمز SMS", + "emailAuthPlaceholder": "رمز البريد الإلكتروني", + "backupCodeAuthPlaceholder": "الرمز الاحتياطي", + "verificationCodeIncorrect": "الرمز غير صحيح", + "verificationCodeManyRequest": "طلبات كثيرة للتحقق من الرمز", + "tryAnotherWay": "حاول بطريقة أخرى", + "selectWayToVerify": "اختر طريقة للتحقق", + "mfaProviderTopt": "تطبيق المصادقة", + "mfaProviderSms": "SMS", + "mfaProviderEmail": "البريد الإلكتروني", + "mfaProviderBackupCode": "الرمز الاحتياطي" +} From 913bd56f8cfd11b00de005a2030131076699cf7a Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 9 Jul 2024 12:39:06 +0300 Subject: [PATCH 28/61] Fix missed after merge conflicts WebUri for the dashboard. --- lib/modules/dashboard/dashboard.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index f0ccb8cb..81489de4 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -131,7 +131,7 @@ class _DashboardState extends TbContextState { ), ); - late Uri _initialUrl; + late WebUri _initialUrl; @override void initState() { @@ -150,7 +150,7 @@ class _DashboardState extends TbContextState { void _onAuthenticated() async { if (tbContext.isAuthenticated) { if (!readyState.value) { - _initialUrl = Uri.parse( + _initialUrl = WebUri( '${await getIt().getEndpoint()}' '?accessToken=${tbClient.getJwtToken()!}' '&refreshToken=${tbClient.getRefreshToken()!}', @@ -169,7 +169,7 @@ class _DashboardState extends TbContextState { var controller = await _controller.future; await controller.postWebMessage( message: WebMessage(data: jsonEncode(windowMessage)), - targetOrigin: Uri.parse('*'), + targetOrigin: WebUri('*'), ); } } @@ -239,7 +239,7 @@ class _DashboardState extends TbContextState { var webMessage = WebMessage(data: jsonEncode(windowMessage)); if (!UniversalPlatform.isWeb) { await controller! - .postWebMessage(message: webMessage, targetOrigin: Uri.parse('*')); + .postWebMessage(message: webMessage, targetOrigin: WebUri('*')); } } @@ -249,7 +249,7 @@ class _DashboardState extends TbContextState { var webMessage = WebMessage(data: jsonEncode(windowMessage)); await controller.postWebMessage( message: webMessage, - targetOrigin: Uri.parse('*'), + targetOrigin: WebUri('*'), ); } From 227abd45ac6eda243be04e46d742e1cf14e828d7 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 9 Jul 2024 13:01:38 +0300 Subject: [PATCH 29/61] compileSdkVersion changed to 34. --- android/app/build.gradle | 2 +- ios/Podfile.lock | 118 ++++++++++++++++++++++----------------- 2 files changed, 67 insertions(+), 53 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b37341bc..9d27ab8e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index beb53492..23516760 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,44 +1,46 @@ PODS: - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (10.18.0): - - FirebaseCore (= 10.18.0) - - Firebase/Messaging (10.18.0): + - Firebase/CoreOnly (10.27.0): + - FirebaseCore (= 10.27.0) + - Firebase/Messaging (10.27.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.18.0) - - firebase_core (2.24.2): - - Firebase/CoreOnly (= 10.18.0) + - FirebaseMessaging (~> 10.27.0) + - firebase_core (3.1.1): + - Firebase/CoreOnly (= 10.27.0) - Flutter - - firebase_messaging (14.7.10): - - Firebase/Messaging (= 10.18.0) + - firebase_messaging (15.0.2): + - Firebase/Messaging (= 10.27.0) - firebase_core - Flutter - - FirebaseCore (10.18.0): + - FirebaseCore (10.27.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.20.0): + - FirebaseCoreInternal (10.28.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.20.0): + - FirebaseInstallations (10.28.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.18.0): + - FirebaseMessaging (10.27.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.2) + - GoogleDataTransport (~> 9.3) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Reachability (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - Flutter (1.0.0) - - flutter_inappwebview (0.0.1): + - flutter_app_badger (1.3.0): - Flutter - - flutter_inappwebview/Core (= 0.0.1) + - flutter_inappwebview_ios (0.0.1): + - Flutter + - flutter_inappwebview_ios/Core (= 0.0.1) - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): + - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) - flutter_local_notifications (0.0.1): @@ -47,42 +49,50 @@ PODS: - Flutter - geolocator_apple (1.2.0): - Flutter - - GoogleDataTransport (9.3.0): + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.12.0): + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.12.0): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.12.0)" - - GoogleUtilities/Reachability (7.12.0): + - "GoogleUtilities/NSData+zlib (7.13.3)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - image_picker_ios (0.0.1): - Flutter - MTBBarcodeScanner (5.0.11) - - nanopb (2.30909.1): - - nanopb/decode (= 2.30909.1) - - nanopb/encode (= 2.30909.1) - - nanopb/decode (2.30909.1) - - nanopb/encode (2.30909.1) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - PromisesObjC (2.3.1) + - PromisesObjC (2.4.0) - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner @@ -94,7 +104,8 @@ DEPENDENCIES: - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_app_badger (from `.symlinks/plugins/flutter_app_badger/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) @@ -127,8 +138,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_app_badger: + :path: ".symlinks/plugins/flutter_app_badger/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage: @@ -147,31 +160,32 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 - firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5 - firebase_messaging: 90e8a6db84b6e1e876cebce4f30f01dc495e7014 - FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f - FirebaseCoreInternal: efeeb171ac02d623bdaefe121539939821e10811 - FirebaseInstallations: 558b1da7d65afeb996fd5c814332f013234ece4e - FirebaseMessaging: 9bc34a98d2e0237e1b121915120d4d48ddcf301e + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + Firebase: 26b040b20866a55f55eb3611b9fcf3ae64816b86 + firebase_core: f8d0424c45e0f1e596811085fc12c638d628457c + firebase_messaging: 8b29edaf5adfd3b52b5bfa5af8128c44164670c6 + FirebaseCore: a2b95ae4ce7c83ceecfbbbe3b6f1cddc7415a808 + FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698 + FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e + FirebaseMessaging: 585984d0a1df120617eb10b44cad8968b859815e Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf - flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_app_badger: b87fc231847b03b92ce1412aa351842e7e97932f + flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 - GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe - GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb - nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b -PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 +PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 From 21371a98b2ebb41262f41e405adc956b8e12bc80 Mon Sep 17 00:00:00 2001 From: Makson Lee Date: Fri, 12 Jul 2024 02:29:43 +0800 Subject: [PATCH 30/61] Add zh-TW locale (#68) --- l10n.yaml | 6 + lib/core/auth/login/login_page.dart | 4 +- .../login/reset_password_request_page.dart | 2 +- .../login/two_factor_authentication_page.dart | 2 +- lib/core/entity/entities_base.dart | 2 +- lib/generated/intl/messages_all.dart | 73 -- lib/generated/intl/messages_en.dart | 189 --- lib/generated/intl/messages_zh.dart | 109 -- lib/generated/l10n.dart | 1020 ----------------- lib/l10n/intl_en.arb | 8 +- lib/l10n/intl_zh.arb | 8 +- lib/l10n/intl_zh_CN.arb | 90 ++ lib/l10n/intl_zh_TW.arb | 90 ++ lib/main.dart | 8 +- lib/modules/alarm/alarms_base.dart | 10 +- lib/modules/asset/asset_details_page.dart | 2 +- .../audit_log/audit_log_details_page.dart | 2 +- lib/modules/device/device_profiles_base.dart | 2 +- lib/modules/device/devices_base.dart | 2 +- lib/modules/device/devices_list_page.dart | 2 +- lib/modules/main/main_page.dart | 2 +- lib/modules/more/more_page.dart | 2 +- .../widgets/notification_slidable_widget.dart | 11 +- lib/modules/profile/change_password_page.dart | 2 +- lib/modules/profile/profile_page.dart | 2 +- pubspec.yaml | 1 + 26 files changed, 224 insertions(+), 1427 deletions(-) create mode 100644 l10n.yaml delete mode 100644 lib/generated/intl/messages_all.dart delete mode 100644 lib/generated/intl/messages_en.dart delete mode 100644 lib/generated/intl/messages_zh.dart delete mode 100644 lib/generated/l10n.dart create mode 100644 lib/l10n/intl_zh_CN.arb create mode 100644 lib/l10n/intl_zh_TW.arb diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 00000000..f1f39b37 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,6 @@ +arb-dir: lib/l10n +template-arb-file: intl_en.arb +output-localization-file: messages.dart +output-class: S +preferred-supported-locales: [ en ] +nullable-getter: false diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 2076977b..46b59685 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -10,7 +10,7 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.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/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; @@ -176,7 +176,7 @@ class _LoginPageState extends TbPageState { padding: const EdgeInsets.symmetric( horizontal: 16, ), - child: Text(S.of(context).OR), + child: Text(S.of(context).or), ), const Flexible(child: Divider()), ], diff --git a/lib/core/auth/login/reset_password_request_page.dart b/lib/core/auth/login/reset_password_request_page.dart index 14db7fbf..07d790e6 100644 --- a/lib/core/auth/login/reset_password_request_page.dart +++ b/lib/core/auth/login/reset_password_request_page.dart @@ -4,7 +4,7 @@ import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:thingsboard_app/core/auth/login/login_page_background.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; diff --git a/lib/core/auth/login/two_factor_authentication_page.dart b/lib/core/auth/login/two_factor_authentication_page.dart index 14e43555..d65bb250 100644 --- a/lib/core/auth/login/two_factor_authentication_page.dart +++ b/lib/core/auth/login/two_factor_authentication_page.dart @@ -8,7 +8,7 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart import 'package:thingsboard_app/core/auth/login/login_page_background.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart index 83441cf3..58b3666f 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -5,7 +5,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:intl/intl.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/utils/utils.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart deleted file mode 100644 index 3838d266..00000000 --- a/lib/generated/intl/messages_all.dart +++ /dev/null @@ -1,73 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that looks up messages for specific locales by -// delegating to the appropriate library. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:implementation_imports, file_names, unnecessary_new -// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering -// ignore_for_file:argument_type_not_assignable, invalid_assignment -// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases -// ignore_for_file:comment_references - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; -import 'package:intl/src/intl_helpers.dart'; - -import 'messages_en.dart' as messages_en; -import 'messages_zh.dart' as messages_zh; -import 'messages_ar.dart' as messages_ar; - -typedef Future LibraryLoader(); -Map _deferredLibraries = { - 'en': () => new SynchronousFuture(null), - 'zh': () => new SynchronousFuture(null), - 'ar': () => new SynchronousFuture(null), -}; - -MessageLookupByLibrary? _findExact(String localeName) { - switch (localeName) { - case 'en': - return messages_en.messages; - case 'zh': - return messages_zh.messages; - case 'ar': - return messages_ar.messages; - default: - return null; - } -} - -/// User programs should call this before using [localeName] for messages. -Future initializeMessages(String localeName) { - var availableLocale = Intl.verifiedLocale( - localeName, - (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null, - ); - if (availableLocale == null) { - return new SynchronousFuture(false); - } - var lib = _deferredLibraries[availableLocale]; - lib == null ? new SynchronousFuture(false) : lib(); - initializeInternalMessageLookup(() => new CompositeMessageLookup()); - messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); - return new SynchronousFuture(true); -} - -bool _messagesExistFor(String locale) { - try { - return _findExact(locale) != null; - } catch (e) { - return false; - } -} - -MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { - var actualLocale = - Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); - if (actualLocale == null) return null; - return _findExact(actualLocale); -} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart deleted file mode 100644 index 614fccce..00000000 --- a/lib/generated/intl/messages_en.dart +++ /dev/null @@ -1,189 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that provides messages for a en locale. All the -// messages from the main program should be duplicated here with the same -// function name. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new -// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering -// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; - -final messages = new MessageLookup(); - -typedef String MessageIfAbsent(String messageStr, List args); - -class MessageLookup extends MessageLookupByLibrary { - String get localeName => 'en'; - - static String m0(contact) => - "A security code has been sent to your email address at ${contact}."; - - static String m1(time) => - "Resend code in ${Intl.plural(time, one: '1 second', other: '${time} seconds')}"; - - static String m2(contact) => - "A security code has been sent to your phone at ${contact}."; - - final messages = _notInlinedMessages(_notInlinedMessages); - - static Map _notInlinedMessages(_) => { - "No": MessageLookupByLibrary.simpleMessage("No"), - "OR": MessageLookupByLibrary.simpleMessage("OR"), - "Yes": MessageLookupByLibrary.simpleMessage("Yes"), - "actionData": MessageLookupByLibrary.simpleMessage("Action data"), - "active": MessageLookupByLibrary.simpleMessage("Active"), - "address": MessageLookupByLibrary.simpleMessage("Address"), - "address2": MessageLookupByLibrary.simpleMessage("Address 2"), - "alarmAcknowledgeText": MessageLookupByLibrary.simpleMessage( - "Are you sure you want to acknowledge Alarm?", - ), - "alarmAcknowledgeTitle": - MessageLookupByLibrary.simpleMessage("Acknowledge Alarm"), - "alarmClearText": MessageLookupByLibrary.simpleMessage( - "Are you sure you want to clear Alarm?", - ), - "alarmClearTitle": MessageLookupByLibrary.simpleMessage("Clear Alarm"), - "alarms": MessageLookupByLibrary.simpleMessage("Alarms"), - "allDevices": MessageLookupByLibrary.simpleMessage("All devices"), - "appTitle": MessageLookupByLibrary.simpleMessage("ThingsBoard"), - "assetName": MessageLookupByLibrary.simpleMessage("Asset name"), - "assets": MessageLookupByLibrary.simpleMessage("Assets"), - "assignedToCustomer": - MessageLookupByLibrary.simpleMessage("Assigned to customer"), - "auditLogDetails": - MessageLookupByLibrary.simpleMessage("Audit log details"), - "auditLogs": MessageLookupByLibrary.simpleMessage("Audit Logs"), - "backupCodeAuthDescription": MessageLookupByLibrary.simpleMessage( - "Please enter one of your backup codes.", - ), - "backupCodeAuthPlaceholder": - MessageLookupByLibrary.simpleMessage("Backup code"), - "changePassword": - MessageLookupByLibrary.simpleMessage("Change Password"), - "city": MessageLookupByLibrary.simpleMessage("City"), - "continueText": MessageLookupByLibrary.simpleMessage("Continue"), - "country": MessageLookupByLibrary.simpleMessage("Country"), - "currentPassword": - MessageLookupByLibrary.simpleMessage("currentPassword"), - "currentPasswordRequireText": MessageLookupByLibrary.simpleMessage( - "Current password is required.", - ), - "currentPasswordStar": - MessageLookupByLibrary.simpleMessage("Current password *"), - "customer": MessageLookupByLibrary.simpleMessage("Customer"), - "customers": MessageLookupByLibrary.simpleMessage("Customers"), - "devices": MessageLookupByLibrary.simpleMessage("Devices"), - "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailAuthDescription": m0, - "emailAuthPlaceholder": - MessageLookupByLibrary.simpleMessage("Email code"), - "emailInvalidText": - MessageLookupByLibrary.simpleMessage("Invalid email format."), - "emailRequireText": - MessageLookupByLibrary.simpleMessage("Email is required."), - "emailStar": MessageLookupByLibrary.simpleMessage("Email *"), - "entityType": MessageLookupByLibrary.simpleMessage("Entity Type"), - "failureDetails": - MessageLookupByLibrary.simpleMessage("Failure details"), - "firstName": MessageLookupByLibrary.simpleMessage("firstName"), - "firstNameUpper": MessageLookupByLibrary.simpleMessage("First Name"), - "home": MessageLookupByLibrary.simpleMessage("Home"), - "inactive": MessageLookupByLibrary.simpleMessage("Inactive"), - "label": MessageLookupByLibrary.simpleMessage("Label"), - "lastName": MessageLookupByLibrary.simpleMessage("lastName"), - "lastNameUpper": MessageLookupByLibrary.simpleMessage("Last Name"), - "listIsEmptyText": MessageLookupByLibrary.simpleMessage( - "The list is currently empty.", - ), - "login": MessageLookupByLibrary.simpleMessage("Log In"), - "loginNotification": - MessageLookupByLibrary.simpleMessage("Login to your account"), - "logoDefaultValue": - MessageLookupByLibrary.simpleMessage("ThingsBoard Logo"), - "logout": MessageLookupByLibrary.simpleMessage("Log Out"), - "mfaProviderBackupCode": - MessageLookupByLibrary.simpleMessage("Backup code"), - "mfaProviderEmail": MessageLookupByLibrary.simpleMessage("Email"), - "mfaProviderSms": MessageLookupByLibrary.simpleMessage("SMS"), - "mfaProviderTopt": - MessageLookupByLibrary.simpleMessage("Authenticator app"), - "more": MessageLookupByLibrary.simpleMessage("More"), - "newPassword": MessageLookupByLibrary.simpleMessage("newPassword"), - "newPassword2": MessageLookupByLibrary.simpleMessage("newPassword2"), - "newPassword2RequireText": MessageLookupByLibrary.simpleMessage( - "New password again is required.", - ), - "newPassword2Star": - MessageLookupByLibrary.simpleMessage("New password again *"), - "newPasswordRequireText": - MessageLookupByLibrary.simpleMessage("New password is required."), - "newPasswordStar": - MessageLookupByLibrary.simpleMessage("New password *"), - "notImplemented": - MessageLookupByLibrary.simpleMessage("Not implemented!"), - "password": MessageLookupByLibrary.simpleMessage("Password"), - "passwordErrorNotification": MessageLookupByLibrary.simpleMessage( - "Entered passwords must be same!", - ), - "passwordForgotText": - MessageLookupByLibrary.simpleMessage("Forgot Password?"), - "passwordRequireText": - MessageLookupByLibrary.simpleMessage("Password is required."), - "passwordReset": MessageLookupByLibrary.simpleMessage("Reset password"), - "passwordResetLinkSuccessfullySentNotification": - MessageLookupByLibrary.simpleMessage( - "Password reset link was successfully sent!", - ), - "passwordResetText": MessageLookupByLibrary.simpleMessage( - "Enter the email associated with your account and we\'ll send an email with password reset link", - ), - "passwordSuccessNotification": MessageLookupByLibrary.simpleMessage( - "Password successfully changed", - ), - "phone": MessageLookupByLibrary.simpleMessage("Phone"), - "postalCode": MessageLookupByLibrary.simpleMessage("Zip / Postal Code"), - "profileSuccessNotification": MessageLookupByLibrary.simpleMessage( - "Profile successfully updated", - ), - "requestPasswordReset": - MessageLookupByLibrary.simpleMessage("Request password reset"), - "resendCode": MessageLookupByLibrary.simpleMessage("Resend code"), - "resendCodeWait": m1, - "selectWayToVerify": - MessageLookupByLibrary.simpleMessage("Select a way to verify"), - "smsAuthDescription": m2, - "smsAuthPlaceholder": MessageLookupByLibrary.simpleMessage("SMS code"), - "stateOrProvince": - MessageLookupByLibrary.simpleMessage("State / Province"), - "systemAdministrator": - MessageLookupByLibrary.simpleMessage("System Administrator"), - "tenantAdministrator": - MessageLookupByLibrary.simpleMessage("Tenant Administrator"), - "title": MessageLookupByLibrary.simpleMessage("Title"), - "toptAuthPlaceholder": MessageLookupByLibrary.simpleMessage("Code"), - "totpAuthDescription": MessageLookupByLibrary.simpleMessage( - "Please enter the security code from your authenticator app.", - ), - "tryAgain": MessageLookupByLibrary.simpleMessage("Try Again"), - "tryAnotherWay": - MessageLookupByLibrary.simpleMessage("Try another way"), - "type": MessageLookupByLibrary.simpleMessage("Type"), - "username": MessageLookupByLibrary.simpleMessage("username"), - "verificationCodeIncorrect": MessageLookupByLibrary.simpleMessage( - "Verification code is incorrect", - ), - "verificationCodeInvalid": MessageLookupByLibrary.simpleMessage( - "Invalid verification code format", - ), - "verificationCodeManyRequest": MessageLookupByLibrary.simpleMessage( - "Too many requests check verification code", - ), - "verifyYourIdentity": - MessageLookupByLibrary.simpleMessage("Verify your identity"), - }; -} diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart deleted file mode 100644 index 541ce257..00000000 --- a/lib/generated/intl/messages_zh.dart +++ /dev/null @@ -1,109 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that provides messages for a zh locale. All the -// messages from the main program should be duplicated here with the same -// function name. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new -// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering -// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; - -final messages = new MessageLookup(); - -typedef String MessageIfAbsent(String messageStr, List args); - -class MessageLookup extends MessageLookupByLibrary { - String get localeName => 'zh'; - - final messages = _notInlinedMessages(_notInlinedMessages); - - static Map _notInlinedMessages(_) => { - "No": MessageLookupByLibrary.simpleMessage("否"), - "OR": MessageLookupByLibrary.simpleMessage("或"), - "Yes": MessageLookupByLibrary.simpleMessage("是"), - "actionData": MessageLookupByLibrary.simpleMessage("动作数据"), - "active": MessageLookupByLibrary.simpleMessage("激活"), - "address": MessageLookupByLibrary.simpleMessage("地址"), - "address2": MessageLookupByLibrary.simpleMessage("地址 2"), - "alarmAcknowledgeText": - MessageLookupByLibrary.simpleMessage("你确定要确认告警吗?"), - "alarmAcknowledgeTitle": MessageLookupByLibrary.simpleMessage("确认告警"), - "alarmClearText": MessageLookupByLibrary.simpleMessage("你确定要清除告警吗?"), - "alarmClearTitle": MessageLookupByLibrary.simpleMessage("清除告警"), - "alarms": MessageLookupByLibrary.simpleMessage("告警"), - "allDevices": MessageLookupByLibrary.simpleMessage("所有设备"), - "appTitle": MessageLookupByLibrary.simpleMessage("Thingsboard"), - "assetName": MessageLookupByLibrary.simpleMessage("资产名"), - "assignedToCustomer": MessageLookupByLibrary.simpleMessage("分配给客户"), - "auditLogDetails": MessageLookupByLibrary.simpleMessage("审计日志详情"), - "auditLogs": MessageLookupByLibrary.simpleMessage("审计报告"), - "changePassword": MessageLookupByLibrary.simpleMessage("修改密码"), - "city": MessageLookupByLibrary.simpleMessage("城市"), - "country": MessageLookupByLibrary.simpleMessage("国家"), - "currentPassword": MessageLookupByLibrary.simpleMessage("当前密码"), - "currentPasswordRequireText": - MessageLookupByLibrary.simpleMessage("输入当前密码"), - "currentPasswordStar": MessageLookupByLibrary.simpleMessage("当前密码 *"), - "customer": MessageLookupByLibrary.simpleMessage("客户"), - "customers": MessageLookupByLibrary.simpleMessage("客户"), - "devices": MessageLookupByLibrary.simpleMessage("设备"), - "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailInvalidText": MessageLookupByLibrary.simpleMessage("Email格式错误"), - "emailRequireText": MessageLookupByLibrary.simpleMessage("输入Email"), - "emailStar": MessageLookupByLibrary.simpleMessage("Email *"), - "entityType": MessageLookupByLibrary.simpleMessage("实体类型"), - "failureDetails": MessageLookupByLibrary.simpleMessage("失败详情"), - "firstName": MessageLookupByLibrary.simpleMessage("名"), - "firstNameUpper": MessageLookupByLibrary.simpleMessage("名"), - "home": MessageLookupByLibrary.simpleMessage("主页"), - "inactive": MessageLookupByLibrary.simpleMessage("失活"), - "label": MessageLookupByLibrary.simpleMessage("标签"), - "lastName": MessageLookupByLibrary.simpleMessage("姓"), - "lastNameUpper": MessageLookupByLibrary.simpleMessage("姓"), - "listIsEmptyText": MessageLookupByLibrary.simpleMessage("列表当前为空"), - "login": MessageLookupByLibrary.simpleMessage("登录"), - "loginNotification": MessageLookupByLibrary.simpleMessage("登录你的账号"), - "logoDefaultValue": - MessageLookupByLibrary.simpleMessage("Thingsboard Logo"), - "logout": MessageLookupByLibrary.simpleMessage("登出"), - "more": MessageLookupByLibrary.simpleMessage("更多"), - "newPassword": MessageLookupByLibrary.simpleMessage("新密码"), - "newPassword2": MessageLookupByLibrary.simpleMessage("新密码2"), - "newPassword2RequireText": - MessageLookupByLibrary.simpleMessage("再次输入新密码"), - "newPassword2Star": MessageLookupByLibrary.simpleMessage("再次输入新密码 *"), - "newPasswordRequireText": MessageLookupByLibrary.simpleMessage("输入新密码"), - "newPasswordStar": MessageLookupByLibrary.simpleMessage("新密码 *"), - "notImplemented": MessageLookupByLibrary.simpleMessage("未实现!"), - "password": MessageLookupByLibrary.simpleMessage("密码"), - "passwordErrorNotification": - MessageLookupByLibrary.simpleMessage("输入的密码必须相同"), - "passwordForgotText": MessageLookupByLibrary.simpleMessage("忘记密码?"), - "passwordRequireText": MessageLookupByLibrary.simpleMessage("输入密码"), - "passwordReset": MessageLookupByLibrary.simpleMessage("重置密码"), - "passwordResetLinkSuccessfullySentNotification": - MessageLookupByLibrary.simpleMessage("密码重置链接已发送"), - "passwordResetText": MessageLookupByLibrary.simpleMessage( - "输入和账号关联的Email,我们将发送一个密码重置链接到的Email", - ), - "passwordSuccessNotification": - MessageLookupByLibrary.simpleMessage("密码修改成功"), - "phone": MessageLookupByLibrary.simpleMessage("电话"), - "postalCode": MessageLookupByLibrary.simpleMessage("邮编"), - "profileSuccessNotification": - MessageLookupByLibrary.simpleMessage("配置更新成功"), - "requestPasswordReset": MessageLookupByLibrary.simpleMessage("要求重置密码"), - "stateOrProvince": MessageLookupByLibrary.simpleMessage("州 / 省"), - "systemAdministrator": MessageLookupByLibrary.simpleMessage("系统管理员"), - "tenantAdministrator": MessageLookupByLibrary.simpleMessage("租户管理员"), - "title": MessageLookupByLibrary.simpleMessage("标题"), - "tryAgain": MessageLookupByLibrary.simpleMessage("再试一次"), - "type": MessageLookupByLibrary.simpleMessage("类型"), - "username": MessageLookupByLibrary.simpleMessage("用户名"), - }; -} diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart deleted file mode 100644 index ed3a6508..00000000 --- a/lib/generated/l10n.dart +++ /dev/null @@ -1,1020 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'intl/messages_all.dart'; - -// ************************************************************************** -// Generator: Flutter Intl IDE plugin -// Made by Localizely -// ************************************************************************** - -// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars -// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each -// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes - -class S { - S(); - - static S? _current; - - static S get current { - assert(_current != null, - 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.'); - return _current!; - } - - static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); - - static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); - final localeName = Intl.canonicalizedLocale(name); - return initializeMessages(localeName).then((_) { - Intl.defaultLocale = localeName; - final instance = S(); - S._current = instance; - - return instance; - }); - } - - static S of(BuildContext context) { - final instance = S.maybeOf(context); - assert(instance != null, - 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?'); - return instance!; - } - - static S? maybeOf(BuildContext context) { - return Localizations.of(context, S); - } - - /// `ThingsBoard` - String get appTitle { - return Intl.message( - 'ThingsBoard', - name: 'appTitle', - desc: '', - args: [], - ); - } - - /// `Home` - String get home { - return Intl.message( - 'Home', - name: 'home', - desc: '', - args: [], - ); - } - - /// `Alarms` - String get alarms { - return Intl.message( - 'Alarms', - name: 'alarms', - desc: '', - args: [], - ); - } - - /// `Devices` - String get devices { - return Intl.message( - 'Devices', - name: 'devices', - desc: '', - args: [], - ); - } - - /// `More` - String get more { - return Intl.message( - 'More', - name: 'more', - desc: '', - args: [], - ); - } - - /// `Customers` - String get customers { - return Intl.message( - 'Customers', - name: 'customers', - desc: '', - args: [], - ); - } - - /// `Assets` - String get assets { - return Intl.message( - 'Assets', - name: 'assets', - desc: '', - args: [], - ); - } - - /// `Audit Logs` - String get auditLogs { - return Intl.message( - 'Audit Logs', - name: 'auditLogs', - desc: '', - args: [], - ); - } - - /// `Log Out` - String get logout { - return Intl.message( - 'Log Out', - name: 'logout', - desc: '', - args: [], - ); - } - - /// `Log In` - String get login { - return Intl.message( - 'Log In', - name: 'login', - desc: '', - args: [], - ); - } - - /// `ThingsBoard Logo` - String get logoDefaultValue { - return Intl.message( - 'ThingsBoard Logo', - name: 'logoDefaultValue', - desc: '', - args: [], - ); - } - - /// `Login to your account` - String get loginNotification { - return Intl.message( - 'Login to your account', - name: 'loginNotification', - desc: '', - args: [], - ); - } - - /// `Email` - String get email { - return Intl.message( - 'Email', - name: 'email', - desc: '', - args: [], - ); - } - - /// `Email is required.` - String get emailRequireText { - return Intl.message( - 'Email is required.', - name: 'emailRequireText', - desc: '', - args: [], - ); - } - - /// `Invalid email format.` - String get emailInvalidText { - return Intl.message( - 'Invalid email format.', - name: 'emailInvalidText', - desc: '', - args: [], - ); - } - - /// `username` - String get username { - return Intl.message( - 'username', - name: 'username', - desc: '', - args: [], - ); - } - - /// `Password` - String get password { - return Intl.message( - 'Password', - name: 'password', - desc: '', - args: [], - ); - } - - /// `Password is required.` - String get passwordRequireText { - return Intl.message( - 'Password is required.', - name: 'passwordRequireText', - desc: '', - args: [], - ); - } - - /// `Forgot Password?` - String get passwordForgotText { - return Intl.message( - 'Forgot Password?', - name: 'passwordForgotText', - desc: '', - args: [], - ); - } - - /// `Reset password` - String get passwordReset { - return Intl.message( - 'Reset password', - name: 'passwordReset', - desc: '', - args: [], - ); - } - - /// `Enter the email associated with your account and we'll send an email with password reset link` - String get passwordResetText { - return Intl.message( - 'Enter the email associated with your account and we\'ll send an email with password reset link', - name: 'passwordResetText', - desc: '', - args: [], - ); - } - - /// `Request password reset` - String get requestPasswordReset { - return Intl.message( - 'Request password reset', - name: 'requestPasswordReset', - desc: '', - args: [], - ); - } - - /// `Password reset link was successfully sent!` - String get passwordResetLinkSuccessfullySentNotification { - return Intl.message( - 'Password reset link was successfully sent!', - name: 'passwordResetLinkSuccessfullySentNotification', - desc: '', - args: [], - ); - } - - /// `OR` - String get OR { - return Intl.message( - 'OR', - name: 'OR', - desc: '', - args: [], - ); - } - - /// `No` - String get No { - return Intl.message( - 'No', - name: 'No', - desc: '', - args: [], - ); - } - - /// `Yes` - String get Yes { - return Intl.message( - 'Yes', - name: 'Yes', - desc: '', - args: [], - ); - } - - /// `Title` - String get title { - return Intl.message( - 'Title', - name: 'title', - desc: '', - args: [], - ); - } - - /// `Country` - String get country { - return Intl.message( - 'Country', - name: 'country', - desc: '', - args: [], - ); - } - - /// `City` - String get city { - return Intl.message( - 'City', - name: 'city', - desc: '', - args: [], - ); - } - - /// `State / Province` - String get stateOrProvince { - return Intl.message( - 'State / Province', - name: 'stateOrProvince', - desc: '', - args: [], - ); - } - - /// `Zip / Postal Code` - String get postalCode { - return Intl.message( - 'Zip / Postal Code', - name: 'postalCode', - desc: '', - args: [], - ); - } - - /// `Address` - String get address { - return Intl.message( - 'Address', - name: 'address', - desc: '', - args: [], - ); - } - - /// `Address 2` - String get address2 { - return Intl.message( - 'Address 2', - name: 'address2', - desc: '', - args: [], - ); - } - - /// `Phone` - String get phone { - return Intl.message( - 'Phone', - name: 'phone', - desc: '', - args: [], - ); - } - - /// `Clear Alarm` - String get alarmClearTitle { - return Intl.message( - 'Clear Alarm', - name: 'alarmClearTitle', - desc: '', - args: [], - ); - } - - /// `Are you sure you want to clear Alarm?` - String get alarmClearText { - return Intl.message( - 'Are you sure you want to clear Alarm?', - name: 'alarmClearText', - desc: '', - args: [], - ); - } - - /// `Acknowledge Alarm` - String get alarmAcknowledgeTitle { - return Intl.message( - 'Acknowledge Alarm', - name: 'alarmAcknowledgeTitle', - desc: '', - args: [], - ); - } - - /// `Are you sure you want to acknowledge Alarm?` - String get alarmAcknowledgeText { - return Intl.message( - 'Are you sure you want to acknowledge Alarm?', - name: 'alarmAcknowledgeText', - desc: '', - args: [], - ); - } - - /// `Asset name` - String get assetName { - return Intl.message( - 'Asset name', - name: 'assetName', - desc: '', - args: [], - ); - } - - /// `Type` - String get type { - return Intl.message( - 'Type', - name: 'type', - desc: '', - args: [], - ); - } - - /// `Label` - String get label { - return Intl.message( - 'Label', - name: 'label', - desc: '', - args: [], - ); - } - - /// `Assigned to customer` - String get assignedToCustomer { - return Intl.message( - 'Assigned to customer', - name: 'assignedToCustomer', - desc: '', - args: [], - ); - } - - /// `Audit log details` - String get auditLogDetails { - return Intl.message( - 'Audit log details', - name: 'auditLogDetails', - desc: '', - args: [], - ); - } - - /// `Entity Type` - String get entityType { - return Intl.message( - 'Entity Type', - name: 'entityType', - desc: '', - args: [], - ); - } - - /// `Action data` - String get actionData { - return Intl.message( - 'Action data', - name: 'actionData', - desc: '', - args: [], - ); - } - - /// `Failure details` - String get failureDetails { - return Intl.message( - 'Failure details', - name: 'failureDetails', - desc: '', - args: [], - ); - } - - /// `All devices` - String get allDevices { - return Intl.message( - 'All devices', - name: 'allDevices', - desc: '', - args: [], - ); - } - - /// `Active` - String get active { - return Intl.message( - 'Active', - name: 'active', - desc: '', - args: [], - ); - } - - /// `Inactive` - String get inactive { - return Intl.message( - 'Inactive', - name: 'inactive', - desc: '', - args: [], - ); - } - - /// `System Administrator` - String get systemAdministrator { - return Intl.message( - 'System Administrator', - name: 'systemAdministrator', - desc: '', - args: [], - ); - } - - /// `Tenant Administrator` - String get tenantAdministrator { - return Intl.message( - 'Tenant Administrator', - name: 'tenantAdministrator', - desc: '', - args: [], - ); - } - - /// `Customer` - String get customer { - return Intl.message( - 'Customer', - name: 'customer', - desc: '', - args: [], - ); - } - - /// `Change Password` - String get changePassword { - return Intl.message( - 'Change Password', - name: 'changePassword', - desc: '', - args: [], - ); - } - - /// `currentPassword` - String get currentPassword { - return Intl.message( - 'currentPassword', - name: 'currentPassword', - desc: '', - args: [], - ); - } - - /// `Current password is required.` - String get currentPasswordRequireText { - return Intl.message( - 'Current password is required.', - name: 'currentPasswordRequireText', - desc: '', - args: [], - ); - } - - /// `Current password *` - String get currentPasswordStar { - return Intl.message( - 'Current password *', - name: 'currentPasswordStar', - desc: '', - args: [], - ); - } - - /// `newPassword` - String get newPassword { - return Intl.message( - 'newPassword', - name: 'newPassword', - desc: '', - args: [], - ); - } - - /// `New password is required.` - String get newPasswordRequireText { - return Intl.message( - 'New password is required.', - name: 'newPasswordRequireText', - desc: '', - args: [], - ); - } - - /// `New password *` - String get newPasswordStar { - return Intl.message( - 'New password *', - name: 'newPasswordStar', - desc: '', - args: [], - ); - } - - /// `newPassword2` - String get newPassword2 { - return Intl.message( - 'newPassword2', - name: 'newPassword2', - desc: '', - args: [], - ); - } - - /// `New password again is required.` - String get newPassword2RequireText { - return Intl.message( - 'New password again is required.', - name: 'newPassword2RequireText', - desc: '', - args: [], - ); - } - - /// `New password again *` - String get newPassword2Star { - return Intl.message( - 'New password again *', - name: 'newPassword2Star', - desc: '', - args: [], - ); - } - - /// `Entered passwords must be same!` - String get passwordErrorNotification { - return Intl.message( - 'Entered passwords must be same!', - name: 'passwordErrorNotification', - desc: '', - args: [], - ); - } - - /// `Email *` - String get emailStar { - return Intl.message( - 'Email *', - name: 'emailStar', - desc: '', - args: [], - ); - } - - /// `firstName` - String get firstName { - return Intl.message( - 'firstName', - name: 'firstName', - desc: '', - args: [], - ); - } - - /// `First Name` - String get firstNameUpper { - return Intl.message( - 'First Name', - name: 'firstNameUpper', - desc: '', - args: [], - ); - } - - /// `lastName` - String get lastName { - return Intl.message( - 'lastName', - name: 'lastName', - desc: '', - args: [], - ); - } - - /// `Last Name` - String get lastNameUpper { - return Intl.message( - 'Last Name', - name: 'lastNameUpper', - desc: '', - args: [], - ); - } - - /// `Profile successfully updated` - String get profileSuccessNotification { - return Intl.message( - 'Profile successfully updated', - name: 'profileSuccessNotification', - desc: '', - args: [], - ); - } - - /// `Password successfully changed` - String get passwordSuccessNotification { - return Intl.message( - 'Password successfully changed', - name: 'passwordSuccessNotification', - desc: '', - args: [], - ); - } - - /// `Not implemented!` - String get notImplemented { - return Intl.message( - 'Not implemented!', - name: 'notImplemented', - desc: '', - args: [], - ); - } - - /// `The list is currently empty.` - String get listIsEmptyText { - return Intl.message( - 'The list is currently empty.', - name: 'listIsEmptyText', - desc: '', - args: [], - ); - } - - /// `Try Again` - String get tryAgain { - return Intl.message( - 'Try Again', - name: 'tryAgain', - desc: '', - args: [], - ); - } - - /// `Verify your identity` - String get verifyYourIdentity { - return Intl.message( - 'Verify your identity', - name: 'verifyYourIdentity', - desc: '', - args: [], - ); - } - - /// `Continue` - String get continueText { - return Intl.message( - 'Continue', - name: 'continueText', - desc: '', - args: [], - ); - } - - /// `Resend code` - String get resendCode { - return Intl.message( - 'Resend code', - name: 'resendCode', - desc: '', - args: [], - ); - } - - /// `Resend code in {time,plural, =1{1 second}other{{time} seconds}}` - String resendCodeWait(num time) { - return Intl.message( - 'Resend code in ${Intl.plural(time, one: '1 second', other: '$time seconds')}', - name: 'resendCodeWait', - desc: '', - args: [time], - ); - } - - /// `Please enter the security code from your authenticator app.` - String get totpAuthDescription { - return Intl.message( - 'Please enter the security code from your authenticator app.', - name: 'totpAuthDescription', - desc: '', - args: [], - ); - } - - /// `A security code has been sent to your phone at {contact}.` - String smsAuthDescription(Object contact) { - return Intl.message( - 'A security code has been sent to your phone at $contact.', - name: 'smsAuthDescription', - desc: '', - args: [contact], - ); - } - - /// `A security code has been sent to your email address at {contact}.` - String emailAuthDescription(Object contact) { - return Intl.message( - 'A security code has been sent to your email address at $contact.', - name: 'emailAuthDescription', - desc: '', - args: [contact], - ); - } - - /// `Please enter one of your backup codes.` - String get backupCodeAuthDescription { - return Intl.message( - 'Please enter one of your backup codes.', - name: 'backupCodeAuthDescription', - desc: '', - args: [], - ); - } - - /// `Invalid verification code format` - String get verificationCodeInvalid { - return Intl.message( - 'Invalid verification code format', - name: 'verificationCodeInvalid', - desc: '', - args: [], - ); - } - - /// `Code` - String get toptAuthPlaceholder { - return Intl.message( - 'Code', - name: 'toptAuthPlaceholder', - desc: '', - args: [], - ); - } - - /// `SMS code` - String get smsAuthPlaceholder { - return Intl.message( - 'SMS code', - name: 'smsAuthPlaceholder', - desc: '', - args: [], - ); - } - - /// `Email code` - String get emailAuthPlaceholder { - return Intl.message( - 'Email code', - name: 'emailAuthPlaceholder', - desc: '', - args: [], - ); - } - - /// `Backup code` - String get backupCodeAuthPlaceholder { - return Intl.message( - 'Backup code', - name: 'backupCodeAuthPlaceholder', - desc: '', - args: [], - ); - } - - /// `Verification code is incorrect` - String get verificationCodeIncorrect { - return Intl.message( - 'Verification code is incorrect', - name: 'verificationCodeIncorrect', - desc: '', - args: [], - ); - } - - /// `Too many requests check verification code` - String get verificationCodeManyRequest { - return Intl.message( - 'Too many requests check verification code', - name: 'verificationCodeManyRequest', - desc: '', - args: [], - ); - } - - /// `Try another way` - String get tryAnotherWay { - return Intl.message( - 'Try another way', - name: 'tryAnotherWay', - desc: '', - args: [], - ); - } - - /// `Select a way to verify` - String get selectWayToVerify { - return Intl.message( - 'Select a way to verify', - name: 'selectWayToVerify', - desc: '', - args: [], - ); - } - - /// `Authenticator app` - String get mfaProviderTopt { - return Intl.message( - 'Authenticator app', - name: 'mfaProviderTopt', - desc: '', - args: [], - ); - } - - /// `SMS` - String get mfaProviderSms { - return Intl.message( - 'SMS', - name: 'mfaProviderSms', - desc: '', - args: [], - ); - } - - /// `Email` - String get mfaProviderEmail { - return Intl.message( - 'Email', - name: 'mfaProviderEmail', - desc: '', - args: [], - ); - } - - /// `Backup code` - String get mfaProviderBackupCode { - return Intl.message( - 'Backup code', - name: 'mfaProviderBackupCode', - desc: '', - args: [], - ); - } -} - -class AppLocalizationDelegate extends LocalizationsDelegate { - const AppLocalizationDelegate(); - - List get supportedLocales { - return const [ - Locale.fromSubtags(languageCode: 'en'), - Locale.fromSubtags(languageCode: 'zh'), - Locale.fromSubtags(languageCode: 'ar'), - ]; - } - - @override - bool isSupported(Locale locale) => _isSupported(locale); - @override - Future load(Locale locale) => S.load(locale); - @override - bool shouldReload(AppLocalizationDelegate old) => false; - - bool _isSupported(Locale locale) { - for (var supportedLocale in supportedLocales) { - if (supportedLocale.languageCode == locale.languageCode) { - return true; - } - } - return false; - } -} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index feb56043..6846ac36 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -26,9 +26,9 @@ "requestPasswordReset": "Request password reset", "passwordResetLinkSuccessfullySentNotification": "Password reset link was successfully sent!", - "OR": "OR", - "No": "No", - "Yes": "Yes", + "or": "Or", + "no": "No", + "yes": "Yes", "title": "Title", "country": "Country", @@ -109,4 +109,4 @@ "mfaProviderSms": "SMS", "mfaProviderEmail": "Email", "mfaProviderBackupCode": "Backup code" -} \ No newline at end of file +} diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index b338e84c..aa791575 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -26,9 +26,9 @@ "requestPasswordReset": "要求重置密码", "passwordResetLinkSuccessfullySentNotification": "密码重置链接已发送", - "OR": "或", - "No": "否", - "Yes": "是", + "or": "或", + "no": "否", + "yes": "是", "title": "标题", "country": "国家", @@ -87,4 +87,4 @@ "listIsEmptyText": "列表当前为空", "tryAgain": "再试一次" -} \ No newline at end of file +} diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb new file mode 100644 index 00000000..aa791575 --- /dev/null +++ b/lib/l10n/intl_zh_CN.arb @@ -0,0 +1,90 @@ +{ + "appTitle": "Thingsboard", + + "home": "主页", + "alarms": "告警", + "devices": "设备", + "more": "更多", + + "customers": "客户", + "asserts": "资产", + "auditLogs": "审计报告", + "logout": "登出", + "login": "登录", + + "logoDefaultValue": "Thingsboard Logo", + "loginNotification": "登录你的账号", + "email": "Email", + "emailRequireText": "输入Email", + "emailInvalidText": "Email格式错误", + "username": "用户名", + "password": "密码", + "passwordRequireText": "输入密码", + "passwordForgotText": "忘记密码?", + "passwordReset": "重置密码", + "passwordResetText": "输入和账号关联的Email,我们将发送一个密码重置链接到的Email", + "requestPasswordReset": "要求重置密码", + "passwordResetLinkSuccessfullySentNotification": "密码重置链接已发送", + + "or": "或", + "no": "否", + "yes": "是", + + "title": "标题", + "country": "国家", + "city": "城市", + "stateOrProvince": "州 / 省", + "postalCode": "邮编", + "address": "地址", + "address2": "地址 2", + "phone": "电话", + + "alarmClearTitle": "清除告警", + "alarmClearText": "你确定要清除告警吗?", + + "alarmAcknowledgeTitle": "确认告警", + "alarmAcknowledgeText": "你确定要确认告警吗?", + + "assetName": "资产名", + "type": "类型", + "label": "标签", + "assignedToCustomer": "分配给客户", + + "auditLogDetails": "审计日志详情", + "entityType": "实体类型", + "actionData": "动作数据", + "failureDetails": "失败详情", + + "allDevices": "所有设备", + "active": "激活", + "inactive": "失活", + + "systemAdministrator": "系统管理员", + "tenantAdministrator": "租户管理员", + "customer": "客户", + + "changePassword": "修改密码", + "currentPassword": "当前密码", + "currentPasswordRequireText": "输入当前密码", + "currentPasswordStar": "当前密码 *", + "newPassword": "新密码", + "newPasswordRequireText": "输入新密码", + "newPasswordStar": "新密码 *", + "newPassword2": "新密码2", + "newPassword2RequireText": "再次输入新密码", + "newPassword2Star": "再次输入新密码 *", + "passwordErrorNotification": "输入的密码必须相同", + + "emailStar": "Email *", + "firstName": "名", + "firstNameUpper": "名", + "lastName": "姓", + "lastNameUpper": "姓", + "profileSuccessNotification": "配置更新成功", + "passwordSuccessNotification": "密码修改成功", + + "notImplemented": "未实现!", + + "listIsEmptyText": "列表当前为空", + "tryAgain": "再试一次" +} diff --git a/lib/l10n/intl_zh_TW.arb b/lib/l10n/intl_zh_TW.arb new file mode 100644 index 00000000..726b154c --- /dev/null +++ b/lib/l10n/intl_zh_TW.arb @@ -0,0 +1,90 @@ +{ + "appTitle": "Thingsboard", + + "home": "首頁", + "alarms": "警報", + "devices": "設備", + "more": "更多", + + "customers": "客戶", + "assets": "資產", + "auditLogs": "審計日誌", + "logout": "登出", + "login": "登入", + + "logoDefaultValue": "Thingsboard Logo", + "loginNotification": "登入你的帳號", + "email": "電子郵件", + "emailRequireText": "輸入電子郵件", + "emailInvalidText": "電子郵件格式錯誤", + "username": "使用者名稱", + "password": "密碼", + "passwordRequireText": "輸入密碼", + "passwordForgotText": "忘記密碼?", + "passwordReset": "重設密碼", + "passwordResetText": "輸入和帳號關聯的電子郵件,我們將發送一個包含密碼重設連結的電子郵件", + "requestPasswordReset": "要求重設密碼", + "passwordResetLinkSuccessfullySentNotification": "密碼重設連結已發送", + + "or": "或", + "no": "否", + "yes": "是", + + "title": "標題", + "country": "國家", + "city": "城市", + "stateOrProvince": "州 / 省", + "postalCode": "郵遞區號", + "address": "地址", + "address2": "地址 2", + "phone": "電話", + + "alarmClearTitle": "清除警報", + "alarmClearText": "你確定要清除警報嗎?", + + "alarmAcknowledgeTitle": "確認警報", + "alarmAcknowledgeText": "你確定要確認警報嗎?", + + "assetName": "資產名稱", + "type": "類型", + "label": "標籤", + "assignedToCustomer": "分派給客戶", + + "auditLogDetails": "審計日誌詳情", + "entityType": "實體類型", + "actionData": "動作數據", + "failureDetails": "失敗詳情", + + "allDevices": "所有設備", + "active": "active", + "inactive": "inactive", + + "systemAdministrator": "系統管理員", + "tenantAdministrator": "租戶管理員", + "customer": "客戶", + + "changePassword": "修改密碼", + "currentPassword": "目前密碼", + "currentPasswordRequireText": "輸入新密碼", + "currentPasswordStar": "目前密碼 *", + "newPassword": "新密碼", + "newPasswordRequireText": "輸入新密碼", + "newPasswordStar": "新密碼 *", + "newPassword2": "新密碼2", + "newPassword2RequireText": "再次輸入新密碼", + "newPassword2Star": "再次輸入新密碼 *", + "passwordErrorNotification": "輸入的密碼必須相同", + + "emailStar": "電子郵件 *", + "firstName": "名", + "firstNameUpper": "名", + "lastName": "姓", + "lastNameUpper": "姓", + "profileSuccessNotification": "配置更新成功", + "passwordSuccessNotification": "密碼修改成功", + + "notImplemented": "未實現!", + + "listIsEmptyText": "列表當前為空", + "tryAgain": "再試一次" +} diff --git a/lib/main.dart b/lib/main.dart index 8712ea51..2453dee7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,7 @@ import 'package:uni_links/uni_links.dart'; import 'package:universal_platform/universal_platform.dart'; import 'config/themes/tb_theme.dart'; -import 'generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -185,7 +185,7 @@ class ThingsboardAppState extends State GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: S.delegate.supportedLocales, + supportedLocales: S.supportedLocales, onGenerateTitle: (BuildContext context) => S.of(context).appTitle, themeMode: ThemeMode.light, home: TwoPageView( @@ -201,7 +201,7 @@ class ThingsboardAppState extends State GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: S.delegate.supportedLocales, + supportedLocales: S.supportedLocales, onGenerateTitle: (BuildContext context) => S.of(context).appTitle, theme: tbTheme, themeMode: ThemeMode.light, @@ -221,7 +221,7 @@ class ThingsboardAppState extends State GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: S.delegate.supportedLocales, + supportedLocales: S.supportedLocales, onGenerateTitle: (BuildContext context) => S.of(context).appTitle, theme: tbTheme, themeMode: ThemeMode.light, diff --git a/lib/modules/alarm/alarms_base.dart b/lib/modules/alarm/alarms_base.dart index 7c099fc7..ca66197b 100644 --- a/lib/modules/alarm/alarms_base.dart +++ b/lib/modules/alarm/alarms_base.dart @@ -4,7 +4,7 @@ import 'package:intl/intl.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/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/utils/utils.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; @@ -327,8 +327,8 @@ class _AlarmCardState extends TbContextState { var res = await confirm( title: S.of(context).alarmClearTitle, message: S.of(context).alarmClearText, - cancel: S.of(context).No, - ok: S.of(context).Yes, + cancel: S.of(context).no, + ok: S.of(context).yes, ); if (res != null && res) { setState(() { @@ -348,8 +348,8 @@ class _AlarmCardState extends TbContextState { var res = await confirm( title: S.of(context).alarmAcknowledgeTitle, message: S.of(context).alarmAcknowledgeText, - cancel: S.of(context).No, - ok: S.of(context).Yes, + cancel: S.of(context).no, + ok: S.of(context).yes, ); if (res != null && res) { setState(() { diff --git a/lib/modules/asset/asset_details_page.dart b/lib/modules/asset/asset_details_page.dart index 98f3c3f6..3e858446 100644 --- a/lib/modules/asset/asset_details_page.dart +++ b/lib/modules/asset/asset_details_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entity_details_page.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class AssetDetailsPage extends EntityDetailsPage { diff --git a/lib/modules/audit_log/audit_log_details_page.dart b/lib/modules/audit_log/audit_log_details_page.dart index 90d3a5f5..75a2d9c7 100644 --- a/lib/modules/audit_log/audit_log_details_page.dart +++ b/lib/modules/audit_log/audit_log_details_page.dart @@ -4,7 +4,7 @@ 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/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/modules/audit_log/audit_logs_base.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; diff --git a/lib/modules/device/device_profiles_base.dart b/lib/modules/device/device_profiles_base.dart index 29e87148..51044291 100644 --- a/lib/modules/device/device_profiles_base.dart +++ b/lib/modules/device/device_profiles_base.dart @@ -7,7 +7,7 @@ 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'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.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/utils/utils.dart'; diff --git a/lib/modules/device/devices_base.dart b/lib/modules/device/devices_base.dart index aa0c6c35..cde9decc 100644 --- a/lib/modules/device/devices_base.dart +++ b/lib/modules/device/devices_base.dart @@ -7,7 +7,7 @@ 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'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.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/utils/utils.dart'; diff --git a/lib/modules/device/devices_list_page.dart b/lib/modules/device/devices_list_page.dart index e124b413..3dbe686f 100644 --- a/lib/modules/device/devices_list_page.dart +++ b/lib/modules/device/devices_list_page.dart @@ -1,7 +1,7 @@ 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/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.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'; diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart index 1a0d2665..fe58edf8 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -1,7 +1,7 @@ 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/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.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/home/home_page.dart'; diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart index 5e293948..621cdb16 100644 --- a/lib/modules/more/more_page.dart +++ b/lib/modules/more/more_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; diff --git a/lib/modules/notification/widgets/notification_slidable_widget.dart b/lib/modules/notification/widgets/notification_slidable_widget.dart index 57055196..26e8ab91 100644 --- a/lib/modules/notification/widgets/notification_slidable_widget.dart +++ b/lib/modules/notification/widgets/notification_slidable_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class NotificationSlidableWidget extends StatefulWidget { @@ -144,8 +144,8 @@ class _NotificationSlidableWidget extends State { final res = await widget.tbContext.confirm( title: S.of(context).alarmAcknowledgeTitle, message: S.of(context).alarmAcknowledgeText, - cancel: S.of(context).No, - ok: S.of(context).Yes, + cancel: S.of(context).no, + ok: S.of(context).yes, ); if (res != null && res) { @@ -167,9 +167,10 @@ class _NotificationSlidableWidget extends State { final res = await widget.tbContext.confirm( title: S.of(context).alarmClearTitle, message: S.of(context).alarmClearText, - cancel: S.of(context).No, - ok: S.of(context).Yes, + cancel: S.of(context).no, + ok: S.of(context).yes, ); + if (res != null && res) { setState(() { loading = true; diff --git a/lib/modules/profile/change_password_page.dart b/lib/modules/profile/change_password_page.dart index c1bd7fbb..4b353a97 100644 --- a/lib/modules/profile/change_password_page.dart +++ b/lib/modules/profile/change_password_page.dart @@ -3,7 +3,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart index d493d0f5..d01030a5 100644 --- a/lib/modules/profile/profile_page.dart +++ b/lib/modules/profile/profile_page.dart @@ -3,7 +3,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/modules/profile/change_password_page.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index f6346785..de5b16b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,7 @@ flutter: uses-material-design: true assets: - assets/images/ + generate: true # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. From 8b512278a1d6b8384ee1640c0427359986422870 Mon Sep 17 00:00:00 2001 From: Makson Lee Date: Wed, 31 Jul 2024 20:49:58 +0800 Subject: [PATCH 31/61] Remove unnecessary generated file (#99) --- lib/generated/intl/messages_ar.dart | 129 ---------------------------- 1 file changed, 129 deletions(-) delete mode 100644 lib/generated/intl/messages_ar.dart diff --git a/lib/generated/intl/messages_ar.dart b/lib/generated/intl/messages_ar.dart deleted file mode 100644 index 1845c26f..00000000 --- a/lib/generated/intl/messages_ar.dart +++ /dev/null @@ -1,129 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that provides messages for a ar locale. All the -// messages from the main program should be duplicated here with the same -// function name. -// @dart=2.12 -// Ignore issues from commonly used lints in this file. -// ignore_for_file:unnecessary_brace_in_string_interps -// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering -// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; - -final messages = MessageLookup(); - -typedef String? MessageIfAbsent( - String? messageStr, List? args); - -class MessageLookup extends MessageLookupByLibrary { - @override - String get localeName => 'ar'; - - static m0(contact) => "تم إرسال رمز أمني إلى بريدك الإلكتروني على العنوان ${contact}."; - - static m1(time) => "إعادة إرسال الرمز في {time, plural, =1{ثانية واحدة} other{${time} ثواني}}"; - - static m2(contact) => "تم إرسال رمز أمني إلى هاتفك على الرقم ${contact}."; - - @override - final Map messages = _notInlinedMessages(_notInlinedMessages); - - static Map _notInlinedMessages(_) => { - 'No': MessageLookupByLibrary.simpleMessage('لا'), - 'OR': MessageLookupByLibrary.simpleMessage('أو'), - 'Yes': MessageLookupByLibrary.simpleMessage('نعم'), - 'actionData': MessageLookupByLibrary.simpleMessage('بيانات الإجراء'), - 'active': MessageLookupByLibrary.simpleMessage('نشط'), - 'address': MessageLookupByLibrary.simpleMessage('العنوان'), - 'address2': MessageLookupByLibrary.simpleMessage('العنوان 2'), - 'alarmAcknowledgeText': MessageLookupByLibrary.simpleMessage('هل أنت متأكد أنك تريد الإقرار بالتنبيه؟'), - 'alarmAcknowledgeTitle': MessageLookupByLibrary.simpleMessage('إقرار التنبيه'), - 'alarmClearText': MessageLookupByLibrary.simpleMessage('هل أنت متأكد أنك تريد مسح التنبيه؟'), - 'alarmClearTitle': MessageLookupByLibrary.simpleMessage('مسح التنبيه'), - 'alarms': MessageLookupByLibrary.simpleMessage('التنبيهات'), - 'allDevices': MessageLookupByLibrary.simpleMessage('جميع الأجهزة'), - 'appTitle': MessageLookupByLibrary.simpleMessage('ThingsBoard'), - 'assetName': MessageLookupByLibrary.simpleMessage('اسم الأصل'), - 'assets': MessageLookupByLibrary.simpleMessage('الأصول'), - 'assignedToCustomer': MessageLookupByLibrary.simpleMessage('معين للعميل'), - 'auditLogDetails': MessageLookupByLibrary.simpleMessage('تفاصيل سجل التدقيق'), - 'auditLogs': MessageLookupByLibrary.simpleMessage('سجلات التدقيق'), - 'backupCodeAuthDescription': MessageLookupByLibrary.simpleMessage('يرجى إدخال أحد الرموز الاحتياطية الخاصة بك.'), - 'backupCodeAuthPlaceholder': MessageLookupByLibrary.simpleMessage('الرمز الاحتياطي'), - 'changePassword': MessageLookupByLibrary.simpleMessage('تغيير كلمة المرور'), - 'city': MessageLookupByLibrary.simpleMessage('المدينة'), - 'continueText': MessageLookupByLibrary.simpleMessage('استمرار'), - 'country': MessageLookupByLibrary.simpleMessage('البلد'), - 'currentPassword': MessageLookupByLibrary.simpleMessage('كلمة المرور الحالية'), - 'currentPasswordRequireText': MessageLookupByLibrary.simpleMessage('كلمة المرور الحالية مطلوبة.'), - 'currentPasswordStar': MessageLookupByLibrary.simpleMessage('كلمة المرور الحالية *'), - 'customer': MessageLookupByLibrary.simpleMessage('العميل'), - 'customers': MessageLookupByLibrary.simpleMessage('العملاء'), - 'devices': MessageLookupByLibrary.simpleMessage('الأجهزة'), - 'email': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني'), - 'emailAuthDescription': m0, - 'emailAuthPlaceholder': MessageLookupByLibrary.simpleMessage('رمز البريد الإلكتروني'), - 'emailInvalidText': MessageLookupByLibrary.simpleMessage('صيغة البريد الإلكتروني غير صحيحة.'), - 'emailRequireText': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني مطلوب.'), - 'emailStar': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني *'), - 'entityType': MessageLookupByLibrary.simpleMessage('نوع الكيان'), - 'failureDetails': MessageLookupByLibrary.simpleMessage('تفاصيل الفشل'), - 'firstName': MessageLookupByLibrary.simpleMessage('الاسم الأول'), - 'firstNameUpper': MessageLookupByLibrary.simpleMessage('الاسم الأول'), - 'home': MessageLookupByLibrary.simpleMessage('الرئيسية'), - 'inactive': MessageLookupByLibrary.simpleMessage('غير نشط'), - 'label': MessageLookupByLibrary.simpleMessage('التسمية'), - 'lastName': MessageLookupByLibrary.simpleMessage('الاسم الأخير'), - 'lastNameUpper': MessageLookupByLibrary.simpleMessage('الاسم الأخير'), - 'listIsEmptyText': MessageLookupByLibrary.simpleMessage('القائمة فارغة حالياً.'), - 'login': MessageLookupByLibrary.simpleMessage('تسجيل دخول'), - 'loginNotification': MessageLookupByLibrary.simpleMessage('تسجيل الدخول إلى حسابك'), - 'logoDefaultValue': MessageLookupByLibrary.simpleMessage('شعار ثينغز بورد'), - 'logout': MessageLookupByLibrary.simpleMessage('تسجيل خروج'), - 'mfaProviderBackupCode': MessageLookupByLibrary.simpleMessage('الرمز الاحتياطي'), - 'mfaProviderEmail': MessageLookupByLibrary.simpleMessage('البريد الإلكتروني'), - 'mfaProviderSms': MessageLookupByLibrary.simpleMessage('SMS'), - 'mfaProviderTopt': MessageLookupByLibrary.simpleMessage('تطبيق المصادقة'), - 'more': MessageLookupByLibrary.simpleMessage('المزيد'), - 'newPassword': MessageLookupByLibrary.simpleMessage('كلمة المرور الجديدة'), - 'newPassword2': MessageLookupByLibrary.simpleMessage('تأكيد كلمة المرور الجديدة'), - 'newPassword2RequireText': MessageLookupByLibrary.simpleMessage('تأكيد كلمة المرور الجديدة مطلوب.'), - 'newPassword2Star': MessageLookupByLibrary.simpleMessage('تأكيد كلمة المرور الجديدة *'), - 'newPasswordRequireText': MessageLookupByLibrary.simpleMessage('كلمة المرور الجديدة مطلوبة.'), - 'newPasswordStar': MessageLookupByLibrary.simpleMessage('كلمة المرور الجديدة *'), - 'notImplemented': MessageLookupByLibrary.simpleMessage('لم يتم التنفيذ!'), - 'password': MessageLookupByLibrary.simpleMessage('كلمة المرور'), - 'passwordErrorNotification': MessageLookupByLibrary.simpleMessage('كلمات المرور المدخلة يجب أن تكون متطابقة!'), - 'passwordForgotText': MessageLookupByLibrary.simpleMessage('هل نسيت كلمة المرور؟'), - 'passwordRequireText': MessageLookupByLibrary.simpleMessage('كلمة المرور مطلوبة.'), - 'passwordReset': MessageLookupByLibrary.simpleMessage('إعادة تعيين كلمة المرور'), - 'passwordResetLinkSuccessfullySentNotification': MessageLookupByLibrary.simpleMessage('تم إرسال رابط إعادة تعيين كلمة المرور بنجاح!'), - 'passwordResetText': MessageLookupByLibrary.simpleMessage('أدخل البريد الإلكتروني المرتبط بحسابك وسنرسل بريدًا إلكترونيًا يحتوي على رابط لإعادة تعيين كلمة المرور'), - 'passwordSuccessNotification': MessageLookupByLibrary.simpleMessage('تم تغيير كلمة المرور بنجاح'), - 'phone': MessageLookupByLibrary.simpleMessage('الهاتف'), - 'postalCode': MessageLookupByLibrary.simpleMessage('الرمز البريدي'), - 'profileSuccessNotification': MessageLookupByLibrary.simpleMessage('تم تحديث الملف الشخصي بنجاح'), - 'requestPasswordReset': MessageLookupByLibrary.simpleMessage('طلب إعادة تعيين كلمة المرور'), - 'resendCode': MessageLookupByLibrary.simpleMessage('إعادة إرسال الرمز'), - 'resendCodeWait': m1, - 'selectWayToVerify': MessageLookupByLibrary.simpleMessage('اختر طريقة للتحقق'), - 'smsAuthDescription': m2, - 'smsAuthPlaceholder': MessageLookupByLibrary.simpleMessage('رمز SMS'), - 'stateOrProvince': MessageLookupByLibrary.simpleMessage('الولاية / المقاطعة'), - 'systemAdministrator': MessageLookupByLibrary.simpleMessage('مسؤول النظام'), - 'tenantAdministrator': MessageLookupByLibrary.simpleMessage('مسؤول المستأجر'), - 'title': MessageLookupByLibrary.simpleMessage('العنوان'), - 'toptAuthPlaceholder': MessageLookupByLibrary.simpleMessage('الرمز'), - 'totpAuthDescription': MessageLookupByLibrary.simpleMessage('يرجى إدخال الرمز الأمني من تطبيق المصادقة الخاص بك.'), - 'tryAgain': MessageLookupByLibrary.simpleMessage('حاول مرة أخرى'), - 'tryAnotherWay': MessageLookupByLibrary.simpleMessage('حاول بطريقة أخرى'), - 'type': MessageLookupByLibrary.simpleMessage('النوع'), - 'username': MessageLookupByLibrary.simpleMessage('اسم المستخدم'), - 'verificationCodeIncorrect': MessageLookupByLibrary.simpleMessage('الرمز غير صحيح'), - 'verificationCodeInvalid': MessageLookupByLibrary.simpleMessage('صيغة الرمز غير صالحة'), - 'verificationCodeManyRequest': MessageLookupByLibrary.simpleMessage('طلبات كثيرة للتحقق من الرمز'), - 'verifyYourIdentity': MessageLookupByLibrary.simpleMessage('تحقق من هويتك') - }; -} From 06dff3fdacd273f968bdfe48431aa67e5754d838 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 31 Jul 2024 16:03:42 +0300 Subject: [PATCH 32/61] Implemented Alarm filters (#101) * WIP. * WIP * WIP. * Alarm filters. * Fixed frequently encounter merge conflicts due to the different names of the Dart client. * When a user click on another bottom navigation tab alarm filters must be closed. * CE alarm filters color. --- lib/core/auth/login/login_page.dart | 4 +- .../login/two_factor_authentication_page.dart | 4 +- .../remote/i_noauth_remote_datasource.dart | 2 +- .../remote/noauth_remote_datasource.dart | 2 +- .../data/repository/noauth_repository.dart | 2 +- .../repository/i_noauth_repository.dart | 2 +- .../usecases/switch_endpoint_usecase.dart | 2 +- lib/core/context/tb_context.dart | 6 +- lib/core/entity/entities_base.dart | 69 +--- lib/core/entity/entities_list_widget.dart | 2 +- lib/core/entity/entity_details_page.dart | 2 +- lib/main.dart | 2 +- lib/modules/alarm/alarm_routes.dart | 26 +- lib/modules/alarm/alarms_base.dart | 344 +----------------- lib/modules/alarm/alarms_list.dart | 82 ++++- lib/modules/alarm/alarms_page.dart | 65 ---- .../alarm_types/alarm_types_datasource.dart | 13 + .../alarm_types/i_alarm_types_datasource.dart | 5 + .../datasource/alarms/alarms_datasource.dart | 13 + .../alarms/i_alarms_datasource.dart | 5 + .../assignee/assignee_datasource.dart | 13 + .../assignee/i_assignee_datasource.dart | 5 + .../alarm_types/alarm_types_repository.dart | 14 + .../repository/alarms/alarms_repository.dart | 14 + .../assignee/assignee_repository.dart | 14 + lib/modules/alarm/di/alarm_types_di.dart | 64 ++++ lib/modules/alarm/di/alarms_di.dart | 74 ++++ lib/modules/alarm/di/assignee_di.dart | 67 ++++ .../domain/entities/alarm_filters_entity.dart | 33 ++ .../domain/entities/assignee_entity.dart | 28 ++ .../domain/entities/filter_data_entity.dart | 11 + .../alarm_types_pagination_repository.dart | 18 + .../alarm_types/alarm_types_query_ctrl.dart | 39 ++ .../alarms/alarms_pagination_repository.dart | 18 + .../pagination/alarms/alarms_query_ctrl.dart | 48 +++ .../assignee_pagination_repository.dart | 19 + .../assignee/assignee_query_ctrl.dart | 38 ++ .../alarm_types/i_alarm_types_repository.dart | 5 + .../alarms/i_alarms_repository.dart | 5 + .../assignee/i_assigne_repository.dart | 5 + .../fetch_alarm_types_usecase.dart | 15 + .../usecases/alarms/fetch_alarms_usecase.dart | 15 + .../assignee/fetch_assignee_usecase.dart | 23 ++ .../bloc/alarm_types/alarm_types_bloc.dart | 62 ++++ .../bloc/alarm_types/alarm_types_event.dart | 34 ++ .../bloc/alarm_types/alarm_types_state.dart | 28 ++ .../presentation/bloc/alarm_types/bloc.dart | 3 + .../alarm/presentation/bloc/alarms_bloc.dart | 50 +++ .../presentation/bloc/alarms_events.dart | 35 ++ .../presentation/bloc/alarms_states.dart | 16 + .../bloc/assignee/assignee_bloc.dart | 63 ++++ .../bloc/assignee/assignee_event.dart | 38 ++ .../bloc/assignee/assignee_state.dart | 26 ++ .../presentation/bloc/assignee/bloc.dart | 3 + lib/modules/alarm/presentation/bloc/bloc.dart | 3 + .../presentation/view/alarms_filter_page.dart | 244 +++++++++++++ .../alarm/presentation/view/alarms_page.dart | 126 +++++++ .../presentation/view/alarms_search_page.dart | 43 +++ .../widgets/alarm_control_filters_button.dart | 84 +++++ .../widgets/alarm_filter_widget.dart | 45 +++ .../alarm_types/alarm_types_widget.dart | 156 ++++++++ .../alarm_types/types_list_widget.dart | 88 +++++ .../presentation/widgets/alarms_card.dart | 273 ++++++++++++++ .../assignee/alarm_assignee_widget.dart | 99 +++++ .../assignee/assignee_list_widget.dart | 189 ++++++++++ .../assignee/user_info_avatar_widget.dart | 34 ++ .../widgets/assignee/user_info_widget.dart | 65 ++++ .../widgets/filter_toggle_block_widget.dart | 85 +++++ lib/modules/asset/asset_details_page.dart | 4 +- lib/modules/asset/assets_base.dart | 2 +- lib/modules/asset/assets_list.dart | 2 +- lib/modules/asset/assets_list_widget.dart | 2 +- .../audit_log/audit_log_details_page.dart | 4 +- lib/modules/audit_log/audit_logs_base.dart | 2 +- lib/modules/audit_log/audit_logs_list.dart | 2 +- .../customer/customer_details_page.dart | 2 +- lib/modules/customer/customers_base.dart | 2 +- lib/modules/customer/customers_list.dart | 2 +- lib/modules/dashboard/dashboards_base.dart | 3 +- lib/modules/dashboard/dashboards_grid.dart | 2 +- lib/modules/dashboard/dashboards_list.dart | 2 +- .../dashboard/dashboards_list_widget.dart | 2 +- lib/modules/device/device_details_page.dart | 2 +- lib/modules/device/device_profiles_base.dart | 4 +- lib/modules/device/device_profiles_grid.dart | 2 +- lib/modules/device/devices_base.dart | 4 +- lib/modules/device/devices_list.dart | 2 +- lib/modules/device/devices_list_widget.dart | 2 +- lib/modules/home/home_page.dart | 8 +- lib/modules/main/main_navigation_item.dart | 114 ++++++ lib/modules/main/main_page.dart | 139 +------ lib/modules/more/more_page.dart | 4 +- .../controllers/notification_query_ctrl.dart | 2 +- .../notification_pagination_repository.dart | 8 +- .../repository/notification_repository.dart | 2 +- .../service/notifications_local_service.dart | 2 +- .../widgets/notification_icon.dart | 2 +- .../widgets/notification_list.dart | 9 +- .../widgets/notification_slidable_widget.dart | 4 +- .../widgets/notification_widget.dart | 2 +- lib/modules/profile/profile_page.dart | 4 +- lib/modules/tenant/tenant_details_page.dart | 2 +- lib/modules/tenant/tenants_base.dart | 2 +- lib/modules/tenant/tenants_list.dart | 2 +- lib/thingsboard_client.dart | 9 + lib/utils/services/_tb_app_storage.dart | 2 +- lib/utils/services/_tb_secure_storage.dart | 2 +- lib/utils/services/_tb_web_local_storage.dart | 2 +- lib/utils/services/device_profile_cache.dart | 2 +- lib/utils/services/entity_query_api.dart | 2 +- .../i_local_database_service.dart | 2 +- .../local_database_service.dart | 2 +- lib/utils/services/notification_service.dart | 2 +- lib/utils/services/pagination_repository.dart | 69 ++++ lib/utils/string_utils.dart | 7 + lib/utils/ui/back_button_widget.dart | 19 + lib/utils/ui/pagination_list_widget.dart | 43 +++ .../first_page_exception_widget.dart | 66 ++++ .../first_page_progress_builder.dart | 24 ++ .../new_page_progress_builder.dart | 18 + lib/utils/ui/ui_utils.dart | 16 + lib/utils/utils.dart | 2 +- lib/widgets/tb_app_bar.dart | 4 +- lib/widgets/two_page_view.dart | 14 +- pubspec.yaml | 1 + test/ core/noauth/switch_endpoint_test.dart | 2 +- test/mocks.dart | 2 +- 127 files changed, 3102 insertions(+), 694 deletions(-) delete mode 100644 lib/modules/alarm/alarms_page.dart create mode 100644 lib/modules/alarm/data/datasource/alarm_types/alarm_types_datasource.dart create mode 100644 lib/modules/alarm/data/datasource/alarm_types/i_alarm_types_datasource.dart create mode 100644 lib/modules/alarm/data/datasource/alarms/alarms_datasource.dart create mode 100644 lib/modules/alarm/data/datasource/alarms/i_alarms_datasource.dart create mode 100644 lib/modules/alarm/data/datasource/assignee/assignee_datasource.dart create mode 100644 lib/modules/alarm/data/datasource/assignee/i_assignee_datasource.dart create mode 100644 lib/modules/alarm/data/repository/alarm_types/alarm_types_repository.dart create mode 100644 lib/modules/alarm/data/repository/alarms/alarms_repository.dart create mode 100644 lib/modules/alarm/data/repository/assignee/assignee_repository.dart create mode 100644 lib/modules/alarm/di/alarm_types_di.dart create mode 100644 lib/modules/alarm/di/alarms_di.dart create mode 100644 lib/modules/alarm/di/assignee_di.dart create mode 100644 lib/modules/alarm/domain/entities/alarm_filters_entity.dart create mode 100644 lib/modules/alarm/domain/entities/assignee_entity.dart create mode 100644 lib/modules/alarm/domain/entities/filter_data_entity.dart create mode 100644 lib/modules/alarm/domain/pagination/alarm_types/alarm_types_pagination_repository.dart create mode 100644 lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart create mode 100644 lib/modules/alarm/domain/pagination/alarms/alarms_pagination_repository.dart create mode 100644 lib/modules/alarm/domain/pagination/alarms/alarms_query_ctrl.dart create mode 100644 lib/modules/alarm/domain/pagination/assignee/assignee_pagination_repository.dart create mode 100644 lib/modules/alarm/domain/pagination/assignee/assignee_query_ctrl.dart create mode 100644 lib/modules/alarm/domain/repository/alarm_types/i_alarm_types_repository.dart create mode 100644 lib/modules/alarm/domain/repository/alarms/i_alarms_repository.dart create mode 100644 lib/modules/alarm/domain/repository/assignee/i_assigne_repository.dart create mode 100644 lib/modules/alarm/domain/usecases/alarm_types/fetch_alarm_types_usecase.dart create mode 100644 lib/modules/alarm/domain/usecases/alarms/fetch_alarms_usecase.dart create mode 100644 lib/modules/alarm/domain/usecases/assignee/fetch_assignee_usecase.dart create mode 100644 lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart create mode 100644 lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart create mode 100644 lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_state.dart create mode 100644 lib/modules/alarm/presentation/bloc/alarm_types/bloc.dart create mode 100644 lib/modules/alarm/presentation/bloc/alarms_bloc.dart create mode 100644 lib/modules/alarm/presentation/bloc/alarms_events.dart create mode 100644 lib/modules/alarm/presentation/bloc/alarms_states.dart create mode 100644 lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart create mode 100644 lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart create mode 100644 lib/modules/alarm/presentation/bloc/assignee/assignee_state.dart create mode 100644 lib/modules/alarm/presentation/bloc/assignee/bloc.dart create mode 100644 lib/modules/alarm/presentation/bloc/bloc.dart create mode 100644 lib/modules/alarm/presentation/view/alarms_filter_page.dart create mode 100644 lib/modules/alarm/presentation/view/alarms_page.dart create mode 100644 lib/modules/alarm/presentation/view/alarms_search_page.dart create mode 100644 lib/modules/alarm/presentation/widgets/alarm_control_filters_button.dart create mode 100644 lib/modules/alarm/presentation/widgets/alarm_filter_widget.dart create mode 100644 lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart create mode 100644 lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart create mode 100644 lib/modules/alarm/presentation/widgets/alarms_card.dart create mode 100644 lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart create mode 100644 lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart create mode 100644 lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart create mode 100644 lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart create mode 100644 lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart create mode 100644 lib/modules/main/main_navigation_item.dart create mode 100644 lib/thingsboard_client.dart create mode 100644 lib/utils/services/pagination_repository.dart create mode 100644 lib/utils/string_utils.dart create mode 100644 lib/utils/ui/back_button_widget.dart create mode 100644 lib/utils/ui/pagination_list_widget.dart create mode 100644 lib/utils/ui/pagination_widgets/first_page_exception_widget.dart create mode 100644 lib/utils/ui/pagination_widgets/first_page_progress_builder.dart create mode 100644 lib/utils/ui/pagination_widgets/new_page_progress_builder.dart create mode 100644 lib/utils/ui/ui_utils.dart diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 46b59685..8914fbe9 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -4,15 +4,15 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.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:flutter_gen/gen_l10n/messages.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; import 'login_page_background.dart'; diff --git a/lib/core/auth/login/two_factor_authentication_page.dart b/lib/core/auth/login/two_factor_authentication_page.dart index d65bb250..19e99583 100644 --- a/lib/core/auth/login/two_factor_authentication_page.dart +++ b/lib/core/auth/login/two_factor_authentication_page.dart @@ -3,14 +3,14 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:thingsboard_app/core/auth/login/login_page_background.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; typedef ProviderDescFunction = String Function( BuildContext context, diff --git a/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart index 19089928..580a287b 100644 --- a/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart +++ b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; abstract interface class INoAuthRemoteDatasource { Future getJwtToken({ diff --git a/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart index 31dfe50a..6b7882ca 100644 --- a/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart +++ b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NoAuthRemoteDatasource implements INoAuthRemoteDatasource { const NoAuthRemoteDatasource({ diff --git a/lib/core/auth/noauth/data/repository/noauth_repository.dart b/lib/core/auth/noauth/data/repository/noauth_repository.dart index 5315108c..6b1b7b6a 100644 --- a/lib/core/auth/noauth/data/repository/noauth_repository.dart +++ b/lib/core/auth/noauth/data/repository/noauth_repository.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NoAuthRepository implements INoAuthRepository { const NoAuthRepository({required this.remoteDatasource}); diff --git a/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart index b7fb16c3..c19fe1a1 100644 --- a/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart +++ b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; abstract interface class INoAuthRepository { Future getJwtToken({ diff --git a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart index faf0eb61..30239230 100644 --- a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart +++ b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart @@ -5,10 +5,10 @@ import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repo import 'package:thingsboard_app/core/logger/tb_logger.dart'; import 'package:thingsboard_app/firebase_options.dart'; import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_app/utils/usecase.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; final class SwitchEndpointParams { const SwitchEndpointParams({ diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index c7fafd3a..81a18a45 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -12,13 +12,13 @@ import 'package:thingsboard_app/core/auth/oauth2/tb_oauth2_client.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; import 'package:thingsboard_app/locator.dart'; -import 'package:thingsboard_app/modules/main/main_page.dart'; +import 'package:thingsboard_app/modules/main/main_navigation_item.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; import 'package:thingsboard_app/utils/services/notification_service.dart'; import 'package:thingsboard_app/utils/services/widget_action_handler.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; import 'package:uni_links/uni_links.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -105,6 +105,8 @@ class TbContext implements PopEntry { WidgetActionHandler get widgetActionHandler => _widgetActionHandler; + final bottomNavigationTabChangedStream = StreamController.broadcast(); + Future init() async { assert(() { if (_initialized) { diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart index 58b3666f..55120204 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:intl/intl.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/first_page_exception_widget.dart'; import 'package:thingsboard_app/utils/utils.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; const entityTypeTranslations = { EntityType.TENANT: 'Tenant', @@ -391,67 +392,3 @@ abstract class BaseEntitiesState ); } } - -class FirstPageExceptionIndicator extends StatelessWidget { - const FirstPageExceptionIndicator({ - required this.title, - this.message, - this.onTryAgain, - Key? key, - }) : super(key: key); - - final String title; - final String? message; - final VoidCallback? onTryAgain; - - @override - Widget build(BuildContext context) { - final message = this.message; - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), - child: Column( - children: [ - Text( - title, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - if (message != null) - const SizedBox( - height: 16, - ), - if (message != null) - Text( - message, - textAlign: TextAlign.center, - ), - if (onTryAgain != null) - const SizedBox( - height: 48, - ), - if (onTryAgain != null) - SizedBox( - height: 50, - width: double.infinity, - child: ElevatedButton.icon( - onPressed: onTryAgain, - icon: const Icon( - Icons.refresh, - color: Colors.white, - ), - label: Text( - S.of(context).tryAgain, - style: const TextStyle( - fontSize: 16, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/core/entity/entities_list_widget.dart b/lib/core/entity/entities_list_widget.dart index 29153f30..f9ac224e 100644 --- a/lib/core/entity/entities_list_widget.dart +++ b/lib/core/entity/entities_list_widget.dart @@ -5,7 +5,7 @@ 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_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'entity_list_card.dart'; diff --git a/lib/core/entity/entity_details_page.dart b/lib/core/entity/entity_details_page.dart index ab08d71a..e38732ee 100644 --- a/lib/core/entity/entity_details_page.dart +++ b/lib/core/entity/entity_details_page.dart @@ -1,9 +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/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; abstract class EntityDetailsPage extends TbPageWidget { final labelTextStyle = diff --git a/lib/main.dart b/lib/main.dart index 2453dee7..1436b1a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -18,7 +19,6 @@ import 'package:uni_links/uni_links.dart'; import 'package:universal_platform/universal_platform.dart'; import 'config/themes/tb_theme.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/modules/alarm/alarm_routes.dart b/lib/modules/alarm/alarm_routes.dart index a4b6cc6f..b43e8b4c 100644 --- a/lib/modules/alarm/alarm_routes.dart +++ b/lib/modules/alarm/alarm_routes.dart @@ -1,24 +1,28 @@ 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/alarm/alarms_page.dart'; -import 'package:thingsboard_app/modules/main/main_page.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/view/alarms_page.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/view/alarms_search_page.dart'; class AlarmRoutes extends TbRoutes { - late var alarmsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - var searchMode = params['search']?.first == 'true'; - if (searchMode) { - return AlarmsPage(tbContext, searchMode: true); + AlarmRoutes(TbContext tbContext) : super(tbContext); + + late final alarmsHandler = Handler( + handlerFunc: (context, params) { + final searchMode = params['search']?.first == 'true'; + if (!searchMode) { + return AlarmsPage( + tbContext, + searchMode: params['search']?.first == 'true', + ); } else { - return MainPage(tbContext, path: '/alarms'); + return AlarmsSearchPage( + tbContext: tbContext, + ); } }, ); - AlarmRoutes(TbContext tbContext) : super(tbContext); - @override void doRegisterRoutes(router) { router.define('/alarms', handler: alarmsHandler); diff --git a/lib/modules/alarm/alarms_base.dart b/lib/modules/alarm/alarms_base.dart index ca66197b..e9d0e300 100644 --- a/lib/modules/alarm/alarms_base.dart +++ b/lib/modules/alarm/alarms_base.dart @@ -1,12 +1,5 @@ -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.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:flutter_gen/gen_l10n/messages.dart'; -import 'package:thingsboard_app/utils/utils.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; const Map alarmSeverityColors = { AlarmSeverity.CRITICAL: Color(0xFFFF0000), @@ -30,338 +23,3 @@ const Map alarmStatusTranslations = { AlarmStatus.CLEARED_ACK: 'Cleared Acknowledged', AlarmStatus.CLEARED_UNACK: 'Cleared Unacknowledged', }; - -mixin AlarmsBase on EntitiesBase { - @override - String get title => 'Alarms'; - - @override - String get noItemsFoundText => 'No alarms found'; - - @override - Future> fetchEntities(AlarmQuery query) { - return tbClient.getAlarmService().getAllAlarms(query); - } - - @override - void onEntityTap(AlarmInfo alarm) { - String? dashboardId = alarm.details?['dashboardId']; - if (dashboardId != null) { - var state = Utils.createDashboardEntityState( - alarm.originator, - entityName: alarm.originatorName, - ); - navigateToDashboard( - dashboardId, - dashboardTitle: alarm.originatorName, - state: state, - ); - } else { - if (tbClient.isTenantAdmin()) { - showWarnNotification( - 'Mobile dashboard should be configured in device profile alarm rules!', - ); - } - } - } - - @override - Widget buildEntityListCard(BuildContext context, AlarmInfo alarm) { - return _buildEntityListCard(context, alarm); - } - - Widget _buildEntityListCard(BuildContext context, AlarmInfo alarm) { - return AlarmCard(tbContext, alarm: alarm); - } -} - -class AlarmQueryController extends PageKeyController { - AlarmQueryController({int pageSize = 20, String? searchText}) - : super( - AlarmQuery( - TimePageLink( - pageSize, - 0, - searchText, - SortOrder('createdTime', Direction.DESC), - ), - fetchOriginator: true, - ), - ); - - @override - AlarmQuery nextPageKey(AlarmQuery pageKey) { - return AlarmQuery(pageKey.pageLink.nextPageLink()); - } - - onSearchText(String searchText) { - var query = value.pageKey; - query.pageLink.page = 0; - query.pageLink.textSearch = searchText; - notifyListeners(); - } -} - -class AlarmCard extends TbContextWidget { - final AlarmInfo alarm; - - AlarmCard(TbContext tbContext, {required this.alarm, super.key}) - : super(tbContext); - - @override - State createState() => _AlarmCardState(); -} - -class _AlarmCardState extends TbContextState { - bool loading = false; - late AlarmInfo alarm; - - final entityDateFormat = DateFormat('yyyy-MM-dd'); - - @override - void initState() { - alarm = widget.alarm; - super.initState(); - } - - @override - void didUpdateWidget(AlarmCard oldWidget) { - super.didUpdateWidget(oldWidget); - loading = false; - } - - @override - Widget build(BuildContext context) { - if (loading) { - return Container( - height: 134, - alignment: Alignment.center, - child: const RefreshProgressIndicator(), - ); - } else { - bool hasDashboard = alarm.details?['dashboardId'] != null; - return Stack( - children: [ - Positioned.fill( - child: Container( - alignment: Alignment.centerLeft, - child: Container( - width: 4, - decoration: BoxDecoration( - color: alarmSeverityColors[alarm.severity]!, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(4), - bottomLeft: Radius.circular(4), - ), - ), - ), - ), - ), - Row( - mainAxisSize: MainAxisSize.max, - children: [ - const SizedBox(width: 4), - Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(width: 16), - Flexible( - fit: FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - fit: FlexFit.tight, - child: AutoSizeText( - alarm.type, - maxLines: 2, - minFontSize: 8, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Color(0xFF282828), - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14, - ), - ), - ), - Text( - entityDateFormat.format( - DateTime.fromMillisecondsSinceEpoch( - alarm.createdTime!, - ), - ), - style: const TextStyle( - color: Color(0xFFAFAFAF), - fontWeight: FontWeight.normal, - fontSize: 12, - height: 16 / 12, - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - fit: FlexFit.tight, - child: Text( - alarm.originatorName != null - ? alarm.originatorName! - : '', - style: const TextStyle( - color: Color(0xFFAFAFAF), - fontWeight: FontWeight.normal, - fontSize: 12, - height: 16 / 12, - ), - ), - ), - Text( - alarmSeverityTranslations[alarm.severity]!, - style: TextStyle( - color: - alarmSeverityColors[alarm.severity]!, - fontWeight: FontWeight.w500, - fontSize: 12, - height: 16 / 12, - ), - ), - ], - ), - const SizedBox(height: 12), - ], - ), - ), - const SizedBox(width: 16), - if (hasDashboard) - const Icon( - Icons.chevron_right, - color: Color(0xFFACACAC), - ), - if (hasDashboard) const SizedBox(width: 16), - ], - ), - const Divider(height: 1), - const SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(width: 16), - Flexible( - fit: FlexFit.tight, - child: Text( - alarmStatusTranslations[alarm.status]!, - style: const TextStyle( - color: Color(0xFF282828), - fontWeight: FontWeight.normal, - fontSize: 14, - height: 20 / 14, - ), - ), - ), - const SizedBox(height: 32), - Row( - children: [ - if ([ - AlarmStatus.CLEARED_UNACK, - AlarmStatus.ACTIVE_UNACK, - ].contains(alarm.status)) - CircleAvatar( - radius: 16, - backgroundColor: const Color(0xffF0F4F9), - child: IconButton( - icon: const Icon(Icons.done, size: 18), - padding: const EdgeInsets.all(7.0), - onPressed: () => _ackAlarm(alarm, context), - ), - ), - if ([ - AlarmStatus.ACTIVE_UNACK, - AlarmStatus.ACTIVE_ACK, - ].contains(alarm.status)) - Row( - children: [ - const SizedBox(width: 4), - CircleAvatar( - radius: 16, - backgroundColor: const Color(0xffF0F4F9), - child: IconButton( - icon: const Icon(Icons.clear, size: 18), - padding: const EdgeInsets.all(7.0), - onPressed: () => - _clearAlarm(alarm, context), - ), - ), - ], - ), - ], - ), - const SizedBox(width: 8), - ], - ), - const SizedBox(height: 8), - ], - ), - ), - ], - ), - ], - ); - } - } - - _clearAlarm(AlarmInfo alarm, BuildContext context) async { - var res = await confirm( - title: S.of(context).alarmClearTitle, - message: S.of(context).alarmClearText, - cancel: S.of(context).no, - ok: S.of(context).yes, - ); - if (res != null && res) { - setState(() { - loading = true; - }); - await tbClient.getAlarmService().clearAlarm(alarm.id!.id!); - var newAlarm = - await tbClient.getAlarmService().getAlarmInfo(alarm.id!.id!); - setState(() { - loading = false; - this.alarm = newAlarm!; - }); - } - } - - _ackAlarm(AlarmInfo alarm, BuildContext context) async { - var res = await confirm( - title: S.of(context).alarmAcknowledgeTitle, - message: S.of(context).alarmAcknowledgeText, - cancel: S.of(context).no, - ok: S.of(context).yes, - ); - if (res != null && res) { - setState(() { - loading = true; - }); - await tbClient.getAlarmService().ackAlarm(alarm.id!.id!); - var newAlarm = - await tbClient.getAlarmService().getAlarmInfo(alarm.id!.id!); - setState(() { - loading = false; - this.alarm = newAlarm!; - }); - } - } -} diff --git a/lib/modules/alarm/alarms_list.dart b/lib/modules/alarm/alarms_list.dart index 74c27091..634639a6 100644 --- a/lib/modules/alarm/alarms_list.dart +++ b/lib/modules/alarm/alarms_list.dart @@ -1,16 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:thingsboard_app/core/entity/entities_base.dart'; -import 'package:thingsboard_app/core/entity/entities_list.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/core/entity/entity_list_card.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_events.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarms_card.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/ui/pagination_list_widget.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/first_page_exception_widget.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/first_page_progress_builder.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/new_page_progress_builder.dart'; +import 'package:thingsboard_app/utils/utils.dart'; -import 'alarms_base.dart'; +class AlarmsList extends StatelessWidget { + const AlarmsList({required this.tbContext, super.key}); -class AlarmsList extends BaseEntitiesWidget - with AlarmsBase, EntitiesListStateBase { - AlarmsList( - TbContext tbContext, - PageKeyController pageKeyController, { - super.key, - searchMode = false, - }) : super(tbContext, pageKeyController, searchMode: searchMode); + final TbContext tbContext; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async => + getIt().add(const AlarmsRefreshPageEvent()), + child: PaginationListWidget( + pagingController: + getIt().paginationRepository.pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, alarm, index) { + return EntityListCard( + alarm, + entityCardWidgetBuilder: (context, alarm) { + return AlarmCard( + tbContext, + alarm: alarm, + ); + }, + onEntityTap: (alarm) { + String? dashboardId = alarm.details?['dashboardId']; + if (dashboardId != null) { + final state = Utils.createDashboardEntityState( + alarm.originator, + entityName: alarm.originatorName); + tbContext.navigateToDashboard( + dashboardId, + dashboardTitle: alarm.originatorName, + state: state, + ); + } else { + if (tbContext.tbClient.isTenantAdmin()) { + tbContext.showWarnNotification( + 'Mobile dashboard should be configured in device profile alarm rules!', + ); + } + } + }, + ); + }, + firstPageProgressIndicatorBuilder: (_) => + const FirstPageProgressBuilder(), + newPageProgressIndicatorBuilder: (_) => + const NewPageProgressBuilder(), + noItemsFoundIndicatorBuilder: (context) => + FirstPageExceptionIndicator( + title: 'No alarms found', + message: S.of(context).listIsEmptyText, + ), + ), + ), + ); + } } diff --git a/lib/modules/alarm/alarms_page.dart b/lib/modules/alarm/alarms_page.dart deleted file mode 100644 index bec2dec3..00000000 --- a/lib/modules/alarm/alarms_page.dart +++ /dev/null @@ -1,65 +0,0 @@ -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/alarm/alarms_base.dart'; -import 'package:thingsboard_app/widgets/tb_app_bar.dart'; - -import 'alarms_list.dart'; - -class AlarmsPage extends TbContextWidget { - final bool searchMode; - - AlarmsPage(TbContext tbContext, {this.searchMode = false, super.key}) - : super(tbContext); - - @override - State createState() => _AlarmsPageState(); -} - -class _AlarmsPageState extends TbContextState - with AutomaticKeepAliveClientMixin { - 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) { - appBar = TbAppSearchBar( - tbContext, - onSearch: (searchText) => - _alarmQueryController.onSearchText(searchText), - ); - } else { - appBar = TbAppBar( - tbContext, - title: Text(alarmsList.title), - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () { - navigateTo('/alarms?search=true'); - }, - ), - ], - ); - } - return Scaffold(appBar: appBar, body: alarmsList); - } - - @override - void dispose() { - _alarmQueryController.dispose(); - super.dispose(); - } -} diff --git a/lib/modules/alarm/data/datasource/alarm_types/alarm_types_datasource.dart b/lib/modules/alarm/data/datasource/alarm_types/alarm_types_datasource.dart new file mode 100644 index 00000000..ca5b428d --- /dev/null +++ b/lib/modules/alarm/data/datasource/alarm_types/alarm_types_datasource.dart @@ -0,0 +1,13 @@ +import 'package:thingsboard_app/modules/alarm/data/datasource/alarm_types/i_alarm_types_datasource.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmTypesDatasource implements IAlarmTypesDatasource { + const AlarmTypesDatasource({required this.tbClient}); + + final ThingsboardClient tbClient; + + @override + Future> fetchAlarmTypes(PageLink pageKey) { + return tbClient.getAlarmService().getAlarmTypes(pageKey); + } +} diff --git a/lib/modules/alarm/data/datasource/alarm_types/i_alarm_types_datasource.dart b/lib/modules/alarm/data/datasource/alarm_types/i_alarm_types_datasource.dart new file mode 100644 index 00000000..031821f5 --- /dev/null +++ b/lib/modules/alarm/data/datasource/alarm_types/i_alarm_types_datasource.dart @@ -0,0 +1,5 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract interface class IAlarmTypesDatasource { + Future> fetchAlarmTypes(PageLink pageKey); +} diff --git a/lib/modules/alarm/data/datasource/alarms/alarms_datasource.dart b/lib/modules/alarm/data/datasource/alarms/alarms_datasource.dart new file mode 100644 index 00000000..ed745817 --- /dev/null +++ b/lib/modules/alarm/data/datasource/alarms/alarms_datasource.dart @@ -0,0 +1,13 @@ +import 'package:thingsboard_app/modules/alarm/data/datasource/alarms/i_alarms_datasource.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmsDatasource implements IAlarmsDatasource { + const AlarmsDatasource({required this.thingsboardClient}); + + final ThingsboardClient thingsboardClient; + + @override + Future> fetchAlarms(AlarmQueryV2 query) { + return thingsboardClient.getAlarmService().getAllAlarmsV2(query); + } +} diff --git a/lib/modules/alarm/data/datasource/alarms/i_alarms_datasource.dart b/lib/modules/alarm/data/datasource/alarms/i_alarms_datasource.dart new file mode 100644 index 00000000..f15c838d --- /dev/null +++ b/lib/modules/alarm/data/datasource/alarms/i_alarms_datasource.dart @@ -0,0 +1,5 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract interface class IAlarmsDatasource { + Future> fetchAlarms(AlarmQueryV2 query); +} diff --git a/lib/modules/alarm/data/datasource/assignee/assignee_datasource.dart b/lib/modules/alarm/data/datasource/assignee/assignee_datasource.dart new file mode 100644 index 00000000..03e098bf --- /dev/null +++ b/lib/modules/alarm/data/datasource/assignee/assignee_datasource.dart @@ -0,0 +1,13 @@ +import 'package:thingsboard_app/modules/alarm/data/datasource/assignee/i_assignee_datasource.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AssigneeDatasource implements IAssigneeDatasource { + const AssigneeDatasource({required this.tbClient}); + + final ThingsboardClient tbClient; + + @override + Future> fetchAssignee(PageLink pageKey) async { + return tbClient.getUserService().getUsersInfo(pageKey); + } +} diff --git a/lib/modules/alarm/data/datasource/assignee/i_assignee_datasource.dart b/lib/modules/alarm/data/datasource/assignee/i_assignee_datasource.dart new file mode 100644 index 00000000..14ba0520 --- /dev/null +++ b/lib/modules/alarm/data/datasource/assignee/i_assignee_datasource.dart @@ -0,0 +1,5 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract interface class IAssigneeDatasource { + Future> fetchAssignee(PageLink pageKey); +} diff --git a/lib/modules/alarm/data/repository/alarm_types/alarm_types_repository.dart b/lib/modules/alarm/data/repository/alarm_types/alarm_types_repository.dart new file mode 100644 index 00000000..b00d89c5 --- /dev/null +++ b/lib/modules/alarm/data/repository/alarm_types/alarm_types_repository.dart @@ -0,0 +1,14 @@ +import 'package:thingsboard_app/modules/alarm/data/datasource/alarm_types/i_alarm_types_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/domain/repository/alarm_types/i_alarm_types_repository.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmTypesRepository implements IAlarmTypesRepository { + const AlarmTypesRepository({required this.datasource}); + + final IAlarmTypesDatasource datasource; + + @override + Future> fetchAlarmTypes(PageLink pageKey) async { + return datasource.fetchAlarmTypes(pageKey); + } +} diff --git a/lib/modules/alarm/data/repository/alarms/alarms_repository.dart b/lib/modules/alarm/data/repository/alarms/alarms_repository.dart new file mode 100644 index 00000000..e6e4175b --- /dev/null +++ b/lib/modules/alarm/data/repository/alarms/alarms_repository.dart @@ -0,0 +1,14 @@ +import 'package:thingsboard_app/modules/alarm/data/datasource/alarms/i_alarms_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/domain/repository/alarms/i_alarms_repository.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmsRepository implements IAlarmsRepository { + const AlarmsRepository({required this.datasource}); + + final IAlarmsDatasource datasource; + + @override + Future> fetchAlarms(AlarmQueryV2 query) { + return datasource.fetchAlarms(query); + } +} diff --git a/lib/modules/alarm/data/repository/assignee/assignee_repository.dart b/lib/modules/alarm/data/repository/assignee/assignee_repository.dart new file mode 100644 index 00000000..ffa33451 --- /dev/null +++ b/lib/modules/alarm/data/repository/assignee/assignee_repository.dart @@ -0,0 +1,14 @@ +import 'package:thingsboard_app/modules/alarm/data/datasource/assignee/i_assignee_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/domain/repository/assignee/i_assigne_repository.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AssigneeRepository implements IAssigneeRepository { + const AssigneeRepository({required this.datasource}); + + final IAssigneeDatasource datasource; + + @override + Future> fetchAssignee(PageLink pageKey) async { + return datasource.fetchAssignee(pageKey); + } +} diff --git a/lib/modules/alarm/di/alarm_types_di.dart b/lib/modules/alarm/di/alarm_types_di.dart new file mode 100644 index 00000000..084ed4c3 --- /dev/null +++ b/lib/modules/alarm/di/alarm_types_di.dart @@ -0,0 +1,64 @@ +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/data/datasource/alarm_types/alarm_types_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/data/datasource/alarm_types/i_alarm_types_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/data/repository/alarm_types/alarm_types_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/alarm_types/alarm_types_pagination_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart'; +import 'package:thingsboard_app/modules/alarm/domain/repository/alarm_types/i_alarm_types_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/usecases/alarm_types/fetch_alarm_types_usecase.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +class AlarmTypesDi { + static const _scopeName = 'AlarmTypesDi'; + + static void init(ThingsboardClient tbClient) { + getIt.pushNewScope( + scopeName: _scopeName, + init: (locator) { + locator.registerFactory( + () => AlarmTypesDatasource( + tbClient: tbClient, + ), + ); + + locator.registerFactory( + () => AlarmTypesRepository( + datasource: locator(), + ), + ); + + locator.registerFactory( + () => AlarmTypesQueryCtrl(), + ); + + locator.registerFactory( + () => FetchAlarmTypesUseCase( + repository: locator(), + ), + ); + + locator.registerFactory>( + () => AlarmTypesPaginationRepository( + alarmTypesQueryCtrl: locator(), + onFetchPageData: locator(), + ), + ); + + locator.registerLazySingleton( + () => AlarmTypesBloc( + paginationRepository: locator(), + fetchAlarmTypesUseCase: locator(), + ), + ); + }, + ); + } + + static void dispose() { + getIt>().dispose(); + getIt().close(); + getIt.dropScope(_scopeName); + } +} diff --git a/lib/modules/alarm/di/alarms_di.dart b/lib/modules/alarm/di/alarms_di.dart new file mode 100644 index 00000000..eaffe2a0 --- /dev/null +++ b/lib/modules/alarm/di/alarms_di.dart @@ -0,0 +1,74 @@ +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/data/datasource/alarms/alarms_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/data/datasource/alarms/i_alarms_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/data/repository/alarms/alarms_repository.dart'; +import 'package:thingsboard_app/modules/alarm/di/alarm_types_di.dart'; +import 'package:thingsboard_app/modules/alarm/di/assignee_di.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/alarms/alarms_pagination_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/alarms/alarms_query_ctrl.dart'; +import 'package:thingsboard_app/modules/alarm/domain/repository/alarms/i_alarms_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/usecases/alarms/fetch_alarms_usecase.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_bloc.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +class AlarmsDi { + static void init( + final String scopeName, { + required ThingsboardClient tbClient, + }) { + getIt.pushNewScope( + scopeName: scopeName, + init: (locator) { + locator.registerFactory( + () => AlarmsDatasource( + thingsboardClient: tbClient, + ), + ); + + locator.registerFactory( + () => AlarmsRepository( + datasource: locator(), + ), + ); + + locator.registerLazySingleton( + () => AlarmQueryController(), + ); + + locator.registerFactory( + () => FetchAlarmsUseCase( + repository: locator(), + ), + ); + + locator.registerLazySingleton< + PaginationRepository>( + () => AlarmsPaginationRepository( + queryController: locator(), + onFetchData: locator(), + ), + ); + + locator.registerLazySingleton( + () => AlarmBloc( + paginationRepository: locator(), + fetchAlarmsUseCase: locator(), + queryController: locator(), + ), + ); + + AlarmTypesDi.init(tbClient); + AssigneeDi.inti(tbClient); + }, + ); + } + + static void dispose(final String scopeName) { + AlarmTypesDi.dispose(); + AssigneeDi.dispose(); + getIt>().dispose(); + getIt().close(); + getIt.dropScope(scopeName); + } +} diff --git a/lib/modules/alarm/di/assignee_di.dart b/lib/modules/alarm/di/assignee_di.dart new file mode 100644 index 00000000..10ed45b9 --- /dev/null +++ b/lib/modules/alarm/di/assignee_di.dart @@ -0,0 +1,67 @@ +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/data/datasource/assignee/assignee_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/data/datasource/assignee/i_assignee_datasource.dart'; +import 'package:thingsboard_app/modules/alarm/data/repository/assignee/assignee_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/assignee_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/assignee/assignee_pagination_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/assignee/assignee_query_ctrl.dart'; +import 'package:thingsboard_app/modules/alarm/domain/repository/assignee/i_assigne_repository.dart'; +import 'package:thingsboard_app/modules/alarm/domain/usecases/assignee/fetch_assignee_usecase.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +class AssigneeDi { + static const _scopeName = 'AssigneeDi'; + + static void inti(ThingsboardClient tbClient) { + getIt.pushNewScope( + scopeName: _scopeName, + init: (locator) { + locator.registerFactory( + () => AssigneeDatasource( + tbClient: tbClient, + ), + ); + + locator.registerFactory( + () => AssigneeRepository( + datasource: locator(), + ), + ); + + locator.registerLazySingleton( + () => AssigneeQueryCtrl(), + ); + + locator.registerFactory( + () => FetchAssigneeUseCase( + repository: locator(), + ), + ); + + locator.registerLazySingleton< + PaginationRepository>( + () => AssigneePaginationRepository( + assigneeQueryCtrl: locator(), + onFetchPageData: locator(), + ), + ); + + locator.registerLazySingleton( + () => AssigneeBloc( + paginationRepository: locator(), + fetchAssigneeUseCase: locator(), + queryCtrl: locator(), + ), + ); + }, + ); + } + + static void dispose() { + getIt>().dispose(); + getIt().close(); + getIt.dropScope(_scopeName); + } +} diff --git a/lib/modules/alarm/domain/entities/alarm_filters_entity.dart b/lib/modules/alarm/domain/entities/alarm_filters_entity.dart new file mode 100644 index 00000000..f4f7e99e --- /dev/null +++ b/lib/modules/alarm/domain/entities/alarm_filters_entity.dart @@ -0,0 +1,33 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmFiltersEntity { + AlarmFiltersEntity({ + this.typeList, + this.statusList = const [AlarmSearchStatus.ANY], + this.severityList, + this.assigneeId, + }); + + final List? typeList; + final List? statusList; + final List? severityList; + final UserId? assigneeId; + + factory AlarmFiltersEntity.defaultFilters() { + return AlarmFiltersEntity(); + } + + factory AlarmFiltersEntity.fromUiFilters({ + required List typeList, + required List status, + required List severity, + required String? userId, + }) { + return AlarmFiltersEntity( + typeList: typeList.isNotEmpty ? typeList : null, + statusList: status.isNotEmpty ? status : null, + severityList: severity.isNotEmpty ? severity : null, + assigneeId: userId != null ? UserId(userId) : null, + ); + } +} diff --git a/lib/modules/alarm/domain/entities/assignee_entity.dart b/lib/modules/alarm/domain/entities/assignee_entity.dart new file mode 100644 index 00000000..51593e9f --- /dev/null +++ b/lib/modules/alarm/domain/entities/assignee_entity.dart @@ -0,0 +1,28 @@ +import 'package:equatable/equatable.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AssigneeEntity extends Equatable { + const AssigneeEntity({ + required this.userInfo, + required this.shortName, + required this.displayName, + }); + + final UserInfo userInfo; + final String shortName; + final String displayName; + + factory AssigneeEntity.fromUserInfo(UserInfo info) { + final name = '${info.firstName ?? ''} ${info.lastName ?? ''}'; + final displayName = name.length > 1 ? name : info.email; + + return AssigneeEntity( + userInfo: info, + displayName: displayName, + shortName: displayName.split(' ').map((e) => e[0]).join('').toUpperCase(), + ); + } + + @override + List get props => [userInfo, shortName, displayName]; +} diff --git a/lib/modules/alarm/domain/entities/filter_data_entity.dart b/lib/modules/alarm/domain/entities/filter_data_entity.dart new file mode 100644 index 00000000..21222330 --- /dev/null +++ b/lib/modules/alarm/domain/entities/filter_data_entity.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; + +class FilterDataEntity extends Equatable { + const FilterDataEntity({required this.label, required this.data}); + + final String label; + final T data; + + @override + List get props => [label, data]; +} diff --git a/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_pagination_repository.dart b/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_pagination_repository.dart new file mode 100644 index 00000000..4691eee9 --- /dev/null +++ b/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_pagination_repository.dart @@ -0,0 +1,18 @@ +import 'package:thingsboard_app/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +final class AlarmTypesPaginationRepository + extends PaginationRepository { + AlarmTypesPaginationRepository({ + required AlarmTypesQueryCtrl alarmTypesQueryCtrl, + required this.onFetchPageData, + }) : super(pageKeyController: alarmTypesQueryCtrl); + + final Future> Function(PageLink) onFetchPageData; + + @override + Future> fetchPageData(PageLink pageKey) async { + return onFetchPageData(pageKey); + } +} diff --git a/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart b/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart new file mode 100644 index 00000000..6ae87cce --- /dev/null +++ b/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart @@ -0,0 +1,39 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmTypesQueryCtrl extends PageKeyController { + AlarmTypesQueryCtrl({ + int pageSize = 20, + String? searchText, + SortOrder? sortOrder, + }) : super( + PageLink( + pageSize, + 0, + searchText, + sortOrder, + ), + ); + + @override + PageLink nextPageKey(PageLink pageKey) { + return pageKey.nextPageLink(); + + // return PushNotificationQuery( + // pageKey.pageLink.nextPageLink(), + // unreadOnly: value.pageKey.unreadOnly, + // ); + } + + // void onSearchText(String searchText) { + // final query = value.pageKey; + // query.page = 0; + // query.textSearch = searchText; + // + // notifyListeners(); + // } + + void refresh() { + notifyListeners(); + } +} diff --git a/lib/modules/alarm/domain/pagination/alarms/alarms_pagination_repository.dart b/lib/modules/alarm/domain/pagination/alarms/alarms_pagination_repository.dart new file mode 100644 index 00000000..86ec1939 --- /dev/null +++ b/lib/modules/alarm/domain/pagination/alarms/alarms_pagination_repository.dart @@ -0,0 +1,18 @@ +import 'package:thingsboard_app/modules/alarm/domain/pagination/alarms/alarms_query_ctrl.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +final class AlarmsPaginationRepository + extends PaginationRepository { + AlarmsPaginationRepository({ + required AlarmQueryController queryController, + required this.onFetchData, + }) : super(pageKeyController: queryController); + + final Future> Function(AlarmQueryV2 query) onFetchData; + + @override + Future> fetchPageData(AlarmQueryV2 pageKey) { + return onFetchData(pageKey); + } +} diff --git a/lib/modules/alarm/domain/pagination/alarms/alarms_query_ctrl.dart b/lib/modules/alarm/domain/pagination/alarms/alarms_query_ctrl.dart new file mode 100644 index 00000000..2f11b67c --- /dev/null +++ b/lib/modules/alarm/domain/pagination/alarms/alarms_query_ctrl.dart @@ -0,0 +1,48 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/alarm_filters_entity.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmQueryController extends PageKeyController { + AlarmQueryController({int pageSize = 20, String? searchText}) + : super( + AlarmQueryV2( + TimePageLink( + pageSize, + 0, + searchText, + SortOrder('createdTime', Direction.DESC), + ), + statusList: [AlarmSearchStatus.ACTIVE], + ), + ); + + @override + AlarmQueryV2 nextPageKey(AlarmQueryV2 pageKey) { + return AlarmQueryV2( + pageKey.pageLink.nextPageLink(), + typeList: pageKey.typeList, + statusList: pageKey.statusList, + severityList: pageKey.severityList, + assigneeId: pageKey.assigneeId, + ); + } + + void onSearchText(String? searchText) { + final query = value.pageKey; + query.pageLink.page = 0; + query.pageLink.textSearch = searchText; + + notifyListeners(); + } + + void onFiltersUpdated(AlarmFiltersEntity filters) { + final query = value.pageKey; + query.pageLink.page = 0; + query.assigneeId = filters.assigneeId; + query.severityList = filters.severityList; + query.statusList = filters.statusList; + query.typeList = filters.typeList; + + notifyListeners(); + } +} diff --git a/lib/modules/alarm/domain/pagination/assignee/assignee_pagination_repository.dart b/lib/modules/alarm/domain/pagination/assignee/assignee_pagination_repository.dart new file mode 100644 index 00000000..6294ee01 --- /dev/null +++ b/lib/modules/alarm/domain/pagination/assignee/assignee_pagination_repository.dart @@ -0,0 +1,19 @@ +import 'package:thingsboard_app/modules/alarm/domain/entities/assignee_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/assignee/assignee_query_ctrl.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +final class AssigneePaginationRepository + extends PaginationRepository { + AssigneePaginationRepository({ + required AssigneeQueryCtrl assigneeQueryCtrl, + required this.onFetchPageData, + }) : super(pageKeyController: assigneeQueryCtrl); + + final Future> Function(PageLink) onFetchPageData; + + @override + Future> fetchPageData(PageLink pageKey) async { + return onFetchPageData(pageKey); + } +} diff --git a/lib/modules/alarm/domain/pagination/assignee/assignee_query_ctrl.dart b/lib/modules/alarm/domain/pagination/assignee/assignee_query_ctrl.dart new file mode 100644 index 00000000..c9cbebc4 --- /dev/null +++ b/lib/modules/alarm/domain/pagination/assignee/assignee_query_ctrl.dart @@ -0,0 +1,38 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AssigneeQueryCtrl extends PageKeyController { + AssigneeQueryCtrl({ + int pageSize = 50, + String? searchText, + SortOrder? sortOrder, + }) : super( + PageLink( + pageSize, + 0, + searchText, + sortOrder ?? + SortOrder( + 'email', + Direction.ASC, + ), + ), + ); + + @override + PageLink nextPageKey(PageLink pageKey) { + return pageKey.nextPageLink(); + } + + void onSearchText(String? searchText) { + final query = value.pageKey; + query.page = 0; + query.textSearch = searchText; + + notifyListeners(); + } + + void refresh() { + notifyListeners(); + } +} diff --git a/lib/modules/alarm/domain/repository/alarm_types/i_alarm_types_repository.dart b/lib/modules/alarm/domain/repository/alarm_types/i_alarm_types_repository.dart new file mode 100644 index 00000000..abca99b7 --- /dev/null +++ b/lib/modules/alarm/domain/repository/alarm_types/i_alarm_types_repository.dart @@ -0,0 +1,5 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract interface class IAlarmTypesRepository { + Future> fetchAlarmTypes(PageLink pageKey); +} diff --git a/lib/modules/alarm/domain/repository/alarms/i_alarms_repository.dart b/lib/modules/alarm/domain/repository/alarms/i_alarms_repository.dart new file mode 100644 index 00000000..05e4ef94 --- /dev/null +++ b/lib/modules/alarm/domain/repository/alarms/i_alarms_repository.dart @@ -0,0 +1,5 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract interface class IAlarmsRepository { + Future> fetchAlarms(AlarmQueryV2 query); +} diff --git a/lib/modules/alarm/domain/repository/assignee/i_assigne_repository.dart b/lib/modules/alarm/domain/repository/assignee/i_assigne_repository.dart new file mode 100644 index 00000000..7f9f7f8a --- /dev/null +++ b/lib/modules/alarm/domain/repository/assignee/i_assigne_repository.dart @@ -0,0 +1,5 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract interface class IAssigneeRepository { + Future> fetchAssignee(PageLink pageKey); +} diff --git a/lib/modules/alarm/domain/usecases/alarm_types/fetch_alarm_types_usecase.dart b/lib/modules/alarm/domain/usecases/alarm_types/fetch_alarm_types_usecase.dart new file mode 100644 index 00000000..1d56b730 --- /dev/null +++ b/lib/modules/alarm/domain/usecases/alarm_types/fetch_alarm_types_usecase.dart @@ -0,0 +1,15 @@ +import 'package:thingsboard_app/modules/alarm/domain/repository/alarm_types/i_alarm_types_repository.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/usecase.dart'; + +class FetchAlarmTypesUseCase + extends UseCase>, PageLink> { + const FetchAlarmTypesUseCase({required this.repository}); + + final IAlarmTypesRepository repository; + + @override + Future> call(PageLink params) async { + return repository.fetchAlarmTypes(params); + } +} diff --git a/lib/modules/alarm/domain/usecases/alarms/fetch_alarms_usecase.dart b/lib/modules/alarm/domain/usecases/alarms/fetch_alarms_usecase.dart new file mode 100644 index 00000000..2065c93f --- /dev/null +++ b/lib/modules/alarm/domain/usecases/alarms/fetch_alarms_usecase.dart @@ -0,0 +1,15 @@ +import 'package:thingsboard_app/modules/alarm/domain/repository/alarms/i_alarms_repository.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/usecase.dart'; + +class FetchAlarmsUseCase + extends UseCase>, AlarmQueryV2> { + const FetchAlarmsUseCase({required this.repository}); + + final IAlarmsRepository repository; + + @override + Future> call(AlarmQueryV2 params) { + return repository.fetchAlarms(params); + } +} diff --git a/lib/modules/alarm/domain/usecases/assignee/fetch_assignee_usecase.dart b/lib/modules/alarm/domain/usecases/assignee/fetch_assignee_usecase.dart new file mode 100644 index 00000000..a031b049 --- /dev/null +++ b/lib/modules/alarm/domain/usecases/assignee/fetch_assignee_usecase.dart @@ -0,0 +1,23 @@ +import 'package:thingsboard_app/modules/alarm/domain/entities/assignee_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/repository/assignee/i_assigne_repository.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/usecase.dart'; + +class FetchAssigneeUseCase + extends UseCase>, PageLink> { + const FetchAssigneeUseCase({required this.repository}); + + final IAssigneeRepository repository; + + @override + Future> call(PageLink params) async { + final pageData = await repository.fetchAssignee(params); + + return PageData( + pageData.data.map((info) => AssigneeEntity.fromUserInfo(info)).toList(), + pageData.totalPages, + pageData.totalElements, + pageData.hasNext, + ); + } +} diff --git a/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart new file mode 100644 index 00000000..75768e71 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart @@ -0,0 +1,62 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/domain/usecases/alarm_types/fetch_alarm_types_usecase.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/bloc.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +class AlarmTypesBloc extends Bloc { + AlarmTypesBloc({ + required this.paginationRepository, + required this.fetchAlarmTypesUseCase, + }) : super(const AlarmTypesSelectionEmptyState()) { + on(_onEvent); + } + + final PaginationRepository paginationRepository; + final FetchAlarmTypesUseCase fetchAlarmTypesUseCase; + + final selectedTypes = {}; + + Future _onEvent( + AlarmTypesEvent event, + Emitter emit, + ) async { + switch (event) { + case AlarmTypesSelectedEvent(): + selectedTypes.add(event.type); + emit( + AlarmTypeSelectedState( + selectedTypes: selectedTypes, + allowToAddMore: selectedTypes.length < + (paginationRepository.pagingController.itemList?.length ?? 0), + ), + ); + break; + + case AlarmTypesRemoveSelectedEvent(): + selectedTypes.remove(event.type); + if (selectedTypes.isNotEmpty) { + emit( + AlarmTypeSelectedState( + selectedTypes: selectedTypes, + allowToAddMore: selectedTypes.length < + (paginationRepository.pagingController.itemList?.length ?? 0), + ), + ); + } else { + emit(const AlarmTypesSelectionEmptyState()); + } + + break; + case AlarmTypesResetEvent(): + selectedTypes.clear(); + emit(const AlarmTypesSelectionEmptyState()); + + break; + case AlarmTypesRefreshEvent(): + paginationRepository.refresh(); + + break; + } + } +} diff --git a/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart new file mode 100644 index 00000000..d821c5d2 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +sealed class AlarmTypesEvent extends Equatable { + const AlarmTypesEvent(); + + @override + List get props => []; +} + +final class AlarmTypesSelectedEvent extends AlarmTypesEvent { + const AlarmTypesSelectedEvent({required this.type}); + + final String type; + + @override + List get props => [type]; +} + +final class AlarmTypesRemoveSelectedEvent extends AlarmTypesEvent { + const AlarmTypesRemoveSelectedEvent({required this.type}); + + final String type; + + @override + List get props => [type]; +} + +final class AlarmTypesResetEvent extends AlarmTypesEvent { + const AlarmTypesResetEvent(); +} + +final class AlarmTypesRefreshEvent extends AlarmTypesEvent { + const AlarmTypesRefreshEvent(); +} diff --git a/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_state.dart b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_state.dart new file mode 100644 index 00000000..ecb97c8b --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_state.dart @@ -0,0 +1,28 @@ +import 'package:equatable/equatable.dart'; + +sealed class AlarmTypesState extends Equatable { + const AlarmTypesState(); + + @override + List get props => []; +} + +final class AlarmTypeSelectedState extends AlarmTypesState { + const AlarmTypeSelectedState({ + required this.selectedTypes, + required this.allowToAddMore, + }); + + final Set selectedTypes; + final bool allowToAddMore; + + @override + List get props => [double.nan]; +} + +final class AlarmTypesSelectionEmptyState extends AlarmTypesState { + const AlarmTypesSelectionEmptyState(); + + @override + List get props => []; +} diff --git a/lib/modules/alarm/presentation/bloc/alarm_types/bloc.dart b/lib/modules/alarm/presentation/bloc/alarm_types/bloc.dart new file mode 100644 index 00000000..daa204d1 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarm_types/bloc.dart @@ -0,0 +1,3 @@ +export 'alarm_types_bloc.dart'; +export 'alarm_types_event.dart'; +export 'alarm_types_state.dart'; diff --git a/lib/modules/alarm/presentation/bloc/alarms_bloc.dart b/lib/modules/alarm/presentation/bloc/alarms_bloc.dart new file mode 100644 index 00000000..e6316bde --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarms_bloc.dart @@ -0,0 +1,50 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/alarm_filters_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/alarms/alarms_query_ctrl.dart'; +import 'package:thingsboard_app/modules/alarm/domain/usecases/alarms/fetch_alarms_usecase.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_events.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_states.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +class AlarmBloc extends Bloc { + AlarmBloc({ + required this.paginationRepository, + required this.fetchAlarmsUseCase, + required this.queryController, + }) : super(const AlarmsFiltersNotActivatedState()) { + on(_onEvent); + } + + final PaginationRepository paginationRepository; + final FetchAlarmsUseCase fetchAlarmsUseCase; + final AlarmQueryController queryController; + bool isFiltersActive = false; + + Future _onEvent(AlarmEvent event, Emitter emit) async { + switch (event) { + case AlarmFiltersResetEvent(): + isFiltersActive = false; + + emit(const AlarmsFiltersNotActivatedState()); + queryController.onFiltersUpdated(AlarmFiltersEntity.defaultFilters()); + + break; + + case AlarmFiltersUpdateEvent(): + queryController.onFiltersUpdated(event.filtersEntity); + isFiltersActive = true; + + emit(const AlarmsFilterActivatedState()); + break; + + case AlarmSearchTextChanged(): + queryController.onSearchText(event.searchText); + break; + + case AlarmsRefreshPageEvent(): + paginationRepository.refresh(); + break; + } + } +} diff --git a/lib/modules/alarm/presentation/bloc/alarms_events.dart b/lib/modules/alarm/presentation/bloc/alarms_events.dart new file mode 100644 index 00000000..4bf04700 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarms_events.dart @@ -0,0 +1,35 @@ +import 'package:equatable/equatable.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/alarm_filters_entity.dart'; + +sealed class AlarmEvent extends Equatable { + const AlarmEvent(); + + @override + List get props => []; +} + +final class AlarmFiltersResetEvent extends AlarmEvent { + const AlarmFiltersResetEvent(); +} + +final class AlarmFiltersUpdateEvent extends AlarmEvent { + const AlarmFiltersUpdateEvent({required this.filtersEntity}); + + final AlarmFiltersEntity filtersEntity; + + @override + List get props => [filtersEntity]; +} + +final class AlarmSearchTextChanged extends AlarmEvent { + const AlarmSearchTextChanged({required this.searchText}); + + final String? searchText; + + @override + List get props => [searchText]; +} + +final class AlarmsRefreshPageEvent extends AlarmEvent { + const AlarmsRefreshPageEvent(); +} diff --git a/lib/modules/alarm/presentation/bloc/alarms_states.dart b/lib/modules/alarm/presentation/bloc/alarms_states.dart new file mode 100644 index 00000000..ff09423b --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarms_states.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +sealed class AlarmsState extends Equatable { + const AlarmsState(); + + @override + List get props => []; +} + +final class AlarmsFiltersNotActivatedState extends AlarmsState { + const AlarmsFiltersNotActivatedState(); +} + +final class AlarmsFilterActivatedState extends AlarmsState { + const AlarmsFilterActivatedState(); +} diff --git a/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart b/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart new file mode 100644 index 00000000..2af5eab7 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart @@ -0,0 +1,63 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/assignee_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/pagination/assignee/assignee_query_ctrl.dart'; +import 'package:thingsboard_app/modules/alarm/domain/usecases/assignee/fetch_assignee_usecase.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_event.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_state.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +class AssigneeBloc extends Bloc { + AssigneeBloc({ + required this.paginationRepository, + required this.fetchAssigneeUseCase, + required this.queryCtrl, + }) : super(const AssigneeEmptyState()) { + on(_onEvent); + } + + final PaginationRepository paginationRepository; + final FetchAssigneeUseCase fetchAssigneeUseCase; + final AssigneeQueryCtrl queryCtrl; + + String? selectedUserId; + + Future _onEvent( + AssigneeEvent event, + Emitter emit, + ) async { + switch (event) { + case AssigneeSelectedEvent(): + selectedUserId = event.userId; + queryCtrl.onSearchText(null); + + final assignee = + paginationRepository.pagingController.itemList?.firstWhere( + (assignee) => assignee.userInfo.id.id == event.userId, + ); + + if (assignee != null) { + if (event.selfAssignment) { + emit(const AssigneeSelfAssignmentState()); + } else { + emit(AssigneeSelectedState(assignee: assignee)); + } + } + + break; + case AssigneeSearchEvent(): + queryCtrl.onSearchText(event.searchText); + break; + + case AssigneeResetEvent(): + selectedUserId = null; + emit(const AssigneeEmptyState()); + + break; + case AssigneeRefreshEvent(): + paginationRepository.refresh(); + + break; + } + } +} diff --git a/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart b/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart new file mode 100644 index 00000000..ba64e386 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; + +sealed class AssigneeEvent extends Equatable { + const AssigneeEvent(); + + @override + List get props => []; +} + +final class AssigneeSelectedEvent extends AssigneeEvent { + const AssigneeSelectedEvent({ + required this.userId, + this.selfAssignment = false, + }); + + final String userId; + final bool selfAssignment; + + @override + List get props => [userId, selfAssignment]; +} + +final class AssigneeSearchEvent extends AssigneeEvent { + const AssigneeSearchEvent({required this.searchText}); + + final String searchText; + + @override + List get props => [searchText]; +} + +final class AssigneeResetEvent extends AssigneeEvent { + const AssigneeResetEvent(); +} + +final class AssigneeRefreshEvent extends AssigneeEvent { + const AssigneeRefreshEvent(); +} diff --git a/lib/modules/alarm/presentation/bloc/assignee/assignee_state.dart b/lib/modules/alarm/presentation/bloc/assignee/assignee_state.dart new file mode 100644 index 00000000..3fb52b9f --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_state.dart @@ -0,0 +1,26 @@ +import 'package:equatable/equatable.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/assignee_entity.dart'; + +sealed class AssigneeState extends Equatable { + const AssigneeState(); + + @override + List get props => []; +} + +final class AssigneeEmptyState extends AssigneeState { + const AssigneeEmptyState(); +} + +final class AssigneeSelectedState extends AssigneeState { + const AssigneeSelectedState({required this.assignee}); + + final AssigneeEntity assignee; + + @override + List get props => [assignee]; +} + +final class AssigneeSelfAssignmentState extends AssigneeState { + const AssigneeSelfAssignmentState(); +} diff --git a/lib/modules/alarm/presentation/bloc/assignee/bloc.dart b/lib/modules/alarm/presentation/bloc/assignee/bloc.dart new file mode 100644 index 00000000..1f284839 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/assignee/bloc.dart @@ -0,0 +1,3 @@ +export 'assignee_bloc.dart'; +export 'assignee_event.dart'; +export 'assignee_state.dart'; diff --git a/lib/modules/alarm/presentation/bloc/bloc.dart b/lib/modules/alarm/presentation/bloc/bloc.dart new file mode 100644 index 00000000..3ee67dd5 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'alarms_bloc.dart'; +export 'alarms_events.dart'; +export 'alarms_states.dart'; diff --git a/lib/modules/alarm/presentation/view/alarms_filter_page.dart b/lib/modules/alarm/presentation/view/alarms_filter_page.dart new file mode 100644 index 00000000..1f2c478a --- /dev/null +++ b/lib/modules/alarm/presentation/view/alarms_filter_page.dart @@ -0,0 +1,244 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:preload_page_view/preload_page_view.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/alarm_filters_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/filter_data_entity.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_event.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_control_filters_button.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class AlarmsFilterPage extends TbContextWidget { + AlarmsFilterPage( + this.tbContext, { + required this.pageController, + super.key, + }) : super(tbContext); + + @override + final TbContext tbContext; + final PreloadPageController pageController; + + @override + State createState() => _AlarmsFilterPageState(); +} + +class _AlarmsFilterPageState extends TbContextState { + final alarmStatus = const [ + FilterDataEntity(label: 'Active', data: AlarmSearchStatus.ACTIVE), + FilterDataEntity(label: 'Cleared', data: AlarmSearchStatus.CLEARED), + FilterDataEntity(label: 'Acknowledged', data: AlarmSearchStatus.ACK), + FilterDataEntity(label: 'Unacknowledged', data: AlarmSearchStatus.UNACK), + ]; + + final alarmSeverity = const [ + FilterDataEntity(label: 'Critical', data: AlarmSeverity.CRITICAL), + FilterDataEntity(label: 'Major', data: AlarmSeverity.MAJOR), + FilterDataEntity(label: 'Minor', data: AlarmSeverity.MINOR), + FilterDataEntity(label: 'Warning', data: AlarmSeverity.WARNING), + FilterDataEntity(label: 'Indeterminate', data: AlarmSeverity.INDETERMINATE), + ]; + + final alarmStatusSelected = []; + final alarmSeveritySelected = []; + + bool filtersChanged = false; + + late final StreamSubscription listenNavigationChanges; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: getIt()), + BlocProvider.value(value: getIt()), + ], + child: Scaffold( + appBar: TbAppBar( + tbContext, + title: const Text('Filters'), + leading: BackButton( + onPressed: () { + widget.pageController.animateToPage( + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + }, + ), + ), + body: RefreshIndicator( + onRefresh: () async { + getIt().add(const AlarmTypesRefreshEvent()); + getIt().add(const AssigneeRefreshEvent()); + }, + child: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FilterToggleBlockWidget( + key: ValueKey(alarmStatusSelected.length), + label: 'Alarm status list', + items: alarmStatus, + selected: alarmStatusSelected.toSet(), + onSelectedChanged: (values) { + alarmStatusSelected + ..clear() + ..addAll(values.cast()); + + setState(() { + filtersChanged = true; + }); + }, + labelAtIndex: (index) => alarmStatus[index].label, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: FilterToggleBlockWidget( + key: ValueKey(alarmSeveritySelected.length), + label: 'Alarm severity list', + items: alarmSeverity, + selected: alarmSeveritySelected.toSet(), + onSelectedChanged: (values) { + alarmSeveritySelected + ..clear() + ..addAll(values.cast()); + + setState(() { + filtersChanged = true; + }); + }, + labelAtIndex: (index) => alarmSeverity[index].label, + ), + ), + AlarmTypesWidget( + tbContext: tbContext, + onChanged: () { + setState(() { + filtersChanged = true; + }); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: AlarmAssigneeFilter( + tbContext: tbContext, + onChanged: () { + setState(() { + filtersChanged = true; + }); + }, + ), + ), + ], + ), + AlarmControlFiltersButton( + onResetTap: filtersChanged + ? () { + setState(() { + alarmStatusSelected + ..clear() + ..add(alarmStatus.first); + alarmSeveritySelected.clear(); + filtersChanged = false; + }); + + getIt().add( + const AlarmTypesResetEvent(), + ); + getIt().add( + const AssigneeResetEvent(), + ); + getIt().add( + const AlarmFiltersResetEvent(), + ); + } + : null, + onCancelTap: () { + widget.pageController.animateToPage( + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + }, + onUpdateTap: filtersChanged + ? () { + final filters = AlarmFiltersEntity.fromUiFilters( + typeList: getIt() + .selectedTypes + .toList(), + status: alarmStatusSelected + .map((e) => e.data) + .toList() + .cast(), + severity: alarmSeveritySelected + .map((e) => e.data) + .toList() + .cast(), + userId: getIt().selectedUserId, + ); + + getIt().add( + AlarmFiltersUpdateEvent( + filtersEntity: filters, + ), + ); + + widget.pageController.animateToPage( + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + } + : null, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + @override + void initState() { + alarmStatusSelected.add(alarmStatus.first); + listenNavigationChanges = widget + .tbContext.bottomNavigationTabChangedStream.stream + .listen((tabIndex) { + widget.pageController.animateToPage( + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + }); + + super.initState(); + } + + @override + void dispose() { + listenNavigationChanges.cancel(); + + super.dispose(); + } +} diff --git a/lib/modules/alarm/presentation/view/alarms_page.dart b/lib/modules/alarm/presentation/view/alarms_page.dart new file mode 100644 index 00000000..225c2fb1 --- /dev/null +++ b/lib/modules/alarm/presentation/view/alarms_page.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:preload_page_view/preload_page_view.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/alarms_list.dart'; +import 'package:thingsboard_app/modules/alarm/di/alarms_di.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_states.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/view/alarms_filter_page.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class AlarmsPage extends TbContextWidget { + AlarmsPage( + TbContext tbContext, { + this.searchMode = false, + super.key, + }) : super(tbContext); + + final bool searchMode; + + @override + State createState() => _AlarmsPageState(); +} + +class _AlarmsPageState extends TbContextState + with AutomaticKeepAliveClientMixin { + final _preloadPageCtrl = PreloadPageController(); + final diScopeKey = UniqueKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + + return BlocProvider.value( + value: getIt(), + child: PreloadPageView.builder( + itemCount: 2, + itemBuilder: (context, index) { + switch (index) { + case 0: + return Scaffold( + appBar: TbAppBar( + tbContext, + title: const Text('Alarms'), + actions: [ + Stack( + children: [ + IconButton( + icon: const Icon(Icons.filter_list), + onPressed: () { + _preloadPageCtrl.animateToPage( + 1, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + }, + ), + BlocBuilder( + builder: (context, state) { + if (state is AlarmsFilterActivatedState) { + return Positioned( + right: 13, + top: 13, + child: Container( + height: 8, + width: 8, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 1, + ), + borderRadius: BorderRadius.circular(16), + color: const Color(0xff305680), + ), + ), + ); + } + + return const SizedBox.shrink(); + }, + ), + ], + ), + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + navigateTo('/alarms?search=true'); + }, + ), + ], + ), + body: AlarmsList(tbContext: tbContext), + ); + + case 1: + return AlarmsFilterPage( + tbContext, + pageController: _preloadPageCtrl, + ); + } + + return const SizedBox.shrink(); + }, + controller: _preloadPageCtrl, + physics: const NeverScrollableScrollPhysics(), + ), + ); + } + + @override + void initState() { + AlarmsDi.init(diScopeKey.toString(), tbClient: widget.tbContext.tbClient); + super.initState(); + } + + @override + void dispose() { + AlarmsDi.dispose(diScopeKey.toString()); + super.dispose(); + } +} diff --git a/lib/modules/alarm/presentation/view/alarms_search_page.dart b/lib/modules/alarm/presentation/view/alarms_search_page.dart new file mode 100644 index 00000000..fb65bcad --- /dev/null +++ b/lib/modules/alarm/presentation/view/alarms_search_page.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/alarms_list.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/utils/ui/back_button_widget.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class AlarmsSearchPage extends StatelessWidget { + const AlarmsSearchPage({ + required this.tbContext, + super.key, + }); + + final TbContext tbContext; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TbAppSearchBar( + tbContext, + onSearch: (searchText) => getIt().add( + AlarmSearchTextChanged( + searchText: searchText, + ), + ), + leading: BackButtonWidget( + onPressed: () { + getIt().add( + const AlarmSearchTextChanged( + searchText: null, + ), + ); + + getIt().router.pop(context); + }, + ), + ), + body: AlarmsList(tbContext: tbContext), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/alarm_control_filters_button.dart b/lib/modules/alarm/presentation/widgets/alarm_control_filters_button.dart new file mode 100644 index 00000000..3f67018a --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/alarm_control_filters_button.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +class AlarmControlFiltersButton extends StatelessWidget { + const AlarmControlFiltersButton({ + required this.onResetTap, + required this.onCancelTap, + required this.onUpdateTap, + super.key, + }); + + final VoidCallback? onResetTap; + final VoidCallback onCancelTap; + final VoidCallback? onUpdateTap; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + TextButton( + onPressed: onResetTap, + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + child: const Text( + 'Reset', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + const Spacer(), + TextButton( + onPressed: onCancelTap, + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: onUpdateTap, + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + // Change your radius here + borderRadius: BorderRadius.circular(4), + ), + ), + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + child: const Text( + 'Update', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/alarm_filter_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_filter_widget.dart new file mode 100644 index 00000000..58596d53 --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/alarm_filter_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class AlarmFilterWidget extends StatelessWidget { + const AlarmFilterWidget({ + required this.filterTitle, + required this.child, + super.key, + }); + + final String filterTitle; + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black.withOpacity(0.12), + ), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.only( + top: 12, + bottom: 12, + left: 16, + right: 8, + ), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + filterTitle, + style: TextStyle( + fontSize: 16, + color: Colors.black.withOpacity(0.76), + ), + ), + const SizedBox(height: 12), + child, + ], + ), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart new file mode 100644 index 00000000..96e826bb --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_filter_widget.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart'; +import 'package:thingsboard_app/utils/ui/ui_utils.dart'; + +class AlarmTypesWidget extends StatelessWidget { + const AlarmTypesWidget({ + required this.tbContext, + required this.onChanged, + super.key, + }); + + final TbContext tbContext; + final VoidCallback onChanged; + + @override + Widget build(BuildContext context) { + return AlarmFilterWidget( + filterTitle: 'Alarm type list', + child: Container( + constraints: const BoxConstraints(minHeight: 38), + decoration: BoxDecoration( + border: Border.all( + color: Colors.black.withOpacity(0.12), + ), + borderRadius: BorderRadius.circular(4), + ), + width: double.infinity, + child: BlocBuilder( + builder: (context, state) { + switch (state) { + case AlarmTypesSelectionEmptyState(): + return InkWell( + onTap: () { + UiUtils.showModalBottomSheet( + context: context, + builder: (context) => AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 500), + child: TypesListWidget( + tbContext: tbContext, + onChanged: onChanged, + ), + ), + ); + }, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + 'Any type', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.black.withOpacity(0.38), + ), + ), + ), + ], + ), + ); + case AlarmTypeSelectedState(): + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Wrap( + direction: Axis.horizontal, + spacing: 12, + runSpacing: 12, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...List.generate( + state.selectedTypes.length, + (index) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.black.withOpacity(0.04), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + state.selectedTypes.elementAt(index), + style: TextStyle( + color: Colors.black.withOpacity(0.87), + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + const SizedBox(width: 4), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + getIt().add( + AlarmTypesRemoveSelectedEvent( + type: + state.selectedTypes.elementAt(index), + ), + ); + }, + child: Icon( + Icons.close, + color: Colors.black.withOpacity(0.54), + ), + ), + ], + ), + ), + ), + Visibility( + visible: state.allowToAddMore, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + UiUtils.showModalBottomSheet( + context: context, + builder: (context) => TypesListWidget( + tbContext: tbContext, + onChanged: onChanged, + ), + ); + }, + child: Text( + '+ Alarm type', + style: TextStyle( + fontSize: 14, + color: Colors.black.withOpacity(0.38), + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ], + ), + ); + } + }, + ), + ), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart new file mode 100644 index 00000000..80ea4ebe --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/bloc.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; + +class TypesListWidget extends StatelessWidget { + const TypesListWidget({ + required this.tbContext, + required this.onChanged, + super.key, + }); + + final TbContext tbContext; + final VoidCallback onChanged; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + minHeight: MediaQuery.of(context).size.height * 0.3, + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ), + Flexible( + child: PagedListView.separated( + pagingController: getIt() + .paginationRepository + .pagingController, + shrinkWrap: true, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pop(); + getIt().add( + AlarmTypesSelectedEvent(type: item.type), + ); + onChanged(); + }, + child: Row( + children: [ + Text(item.type), + ], + ), + ); + }, + firstPageProgressIndicatorBuilder: (_) { + return Container( + height: 200, + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator(size: 50.0), + ), + ); + }, + ), + separatorBuilder: (_, __) => const Divider( + thickness: 1, + height: 24, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/alarms_card.dart b/lib/modules/alarm/presentation/widgets/alarms_card.dart new file mode 100644 index 00000000..efd3fe8a --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/alarms_card.dart @@ -0,0 +1,273 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:intl/intl.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_base.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmCard extends TbContextWidget { + final AlarmInfo alarm; + + AlarmCard(TbContext tbContext, {super.key, required this.alarm}) + : super(tbContext); + + @override + State createState() => _AlarmCardState(); +} + +class _AlarmCardState extends TbContextState { + bool loading = false; + late AlarmInfo alarm; + + final entityDateFormat = DateFormat('yyyy-MM-dd'); + + @override + void initState() { + alarm = widget.alarm; + super.initState(); + } + + @override + void didUpdateWidget(AlarmCard oldWidget) { + super.didUpdateWidget(oldWidget); + loading = false; + alarm = widget.alarm; + } + + @override + Widget build(BuildContext context) { + if (loading) { + return Container( + height: 134, + alignment: Alignment.center, + child: const RefreshProgressIndicator(), + ); + } else { + bool hasDashboard = alarm.details?['dashboardId'] != null; + return Stack( + children: [ + Positioned.fill( + child: Container( + alignment: Alignment.centerLeft, + child: Container( + width: 4, + decoration: BoxDecoration( + color: alarmSeverityColors[alarm.severity]!, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(width: 4), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 16), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: AutoSizeText( + alarm.type, + maxLines: 2, + minFontSize: 8, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + Text( + entityDateFormat.format( + DateTime.fromMillisecondsSinceEpoch( + alarm.createdTime!, + ), + ), + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 12, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: Text( + alarm.originatorName != null + ? alarm.originatorName! + : '', + style: const TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 12, + height: 16 / 12, + ), + ), + ), + Text( + alarmSeverityTranslations[alarm.severity]!, + style: TextStyle( + color: + alarmSeverityColors[alarm.severity]!, + fontWeight: FontWeight.w500, + fontSize: 12, + height: 16 / 12, + ), + ), + ], + ), + const SizedBox(height: 12), + ], + ), + ), + const SizedBox(width: 16), + if (hasDashboard) + const Icon( + Icons.chevron_right, + color: Color(0xFFACACAC), + ), + if (hasDashboard) const SizedBox(width: 16), + ], + ), + const Divider(height: 1), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 16), + Flexible( + fit: FlexFit.tight, + child: Text( + alarmStatusTranslations[alarm.status]!, + style: const TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.normal, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + const SizedBox(height: 32), + Row( + children: [ + if ([ + AlarmStatus.CLEARED_UNACK, + AlarmStatus.ACTIVE_UNACK, + ].contains(alarm.status)) + CircleAvatar( + radius: 16, + backgroundColor: const Color(0xffF0F4F9), + child: IconButton( + icon: const Icon(Icons.done, size: 18), + padding: const EdgeInsets.all(7.0), + onPressed: () => _ackAlarm(alarm, context), + ), + ), + if ([ + AlarmStatus.ACTIVE_UNACK, + AlarmStatus.ACTIVE_ACK, + ].contains(alarm.status)) + Row( + children: [ + const SizedBox(width: 4), + CircleAvatar( + radius: 16, + backgroundColor: const Color(0xffF0F4F9), + child: IconButton( + icon: const Icon(Icons.clear, size: 18), + padding: const EdgeInsets.all(7.0), + onPressed: () => + _clearAlarm(alarm, context), + ), + ), + ], + ), + ], + ), + const SizedBox(width: 8), + ], + ), + const SizedBox(height: 8), + ], + ), + ), + ], + ), + ], + ); + } + } + + _clearAlarm(AlarmInfo alarm, BuildContext context) async { + var res = await confirm( + title: S.of(context).alarmClearTitle, + message: S.of(context).alarmClearText, + cancel: S.of(context).no, + ok: S.of(context).yes, + ); + if (res != null && res) { + setState(() { + loading = true; + }); + await tbClient.getAlarmService().clearAlarm(alarm.id!.id!); + var newAlarm = + await tbClient.getAlarmService().getAlarmInfo(alarm.id!.id!); + setState(() { + loading = false; + this.alarm = newAlarm!; + }); + } + } + + _ackAlarm(AlarmInfo alarm, BuildContext context) async { + var res = await confirm( + title: S.of(context).alarmAcknowledgeTitle, + message: S.of(context).alarmAcknowledgeText, + cancel: S.of(context).no, + ok: S.of(context).yes, + ); + if (res != null && res) { + setState(() { + loading = true; + }); + await tbClient.getAlarmService().ackAlarm(alarm.id!.id!); + var newAlarm = + await tbClient.getAlarmService().getAlarmInfo(alarm.id!.id!); + setState(() { + loading = false; + this.alarm = newAlarm!; + }); + } + } +} diff --git a/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart new file mode 100644 index 00000000..3f0cc44f --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_filter_widget.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/user_info_widget.dart'; +import 'package:thingsboard_app/utils/ui/ui_utils.dart'; + +class AlarmAssigneeFilter extends StatelessWidget { + const AlarmAssigneeFilter({ + required this.tbContext, + required this.onChanged, + super.key, + }); + + final TbContext tbContext; + final VoidCallback onChanged; + + @override + Widget build(BuildContext context) { + return AlarmFilterWidget( + filterTitle: 'Assignee', + child: InkWell( + onTap: () { + UiUtils.showModalBottomSheet( + context: context, + builder: (context) => AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 500), + child: AssigneeListWidget( + tbContext: tbContext, + onChanged: onChanged, + ), + ), + ); + }, + child: Container( + height: 38, + decoration: BoxDecoration( + border: Border.all( + color: Colors.black.withOpacity(0.12), + ), + borderRadius: BorderRadius.circular(4), + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: BlocBuilder( + builder: (context, state) { + switch (state) { + case AssigneeEmptyState(): + return Row( + children: [ + Icon( + Icons.account_circle, + color: Colors.black.withOpacity(0.38), + ), + const SizedBox(width: 8), + Text( + 'All', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.black.withOpacity(0.38), + ), + ), + ], + ); + case AssigneeSelectedState(): + return UserInfoWidget( + id: state.assignee.userInfo.id.id ?? '', + avatar: UserInfoAvatarWidget( + shortName: state.assignee.shortName, + color: HSLColor.fromAHSL( + 1, + state.assignee.displayName.hashCode % 360, + 40 / 100, + 60 / 100, + ).toColor(), + ), + name: state.assignee.displayName, + ); + case AssigneeSelfAssignmentState(): + return UserInfoWidget( + avatar: Icon( + Icons.account_circle, + color: Colors.black.withOpacity(0.38), + ), + name: 'Assigned to me', + id: '', + ); + } + }, + ), + ), + ), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart new file mode 100644 index 00000000..cfc8f580 --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/assignee_entity.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/user_info_widget.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/string_utils.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; + +class AssigneeListWidget extends StatelessWidget { + const AssigneeListWidget({ + required this.tbContext, + required this.onChanged, + super.key, + }); + + final TbContext tbContext; + final VoidCallback onChanged; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + minHeight: MediaQuery.of(context).size.height * 0.3, + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ), + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black.withOpacity(0.12), + ), + borderRadius: BorderRadius.circular(4), + ), + margin: const EdgeInsets.only(left: 12, right: 12, bottom: 8), + width: double.infinity, + child: Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + border: InputBorder.none, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.38), + fontWeight: FontWeight.w400, + fontSize: 14, + ), + hintText: 'Search users', + contentPadding: const EdgeInsets.fromLTRB(16, 8, 8, 8), + isDense: true, + ), + onChanged: (text) { + getIt().add( + AssigneeSearchEvent(searchText: text), + ); + }, + ), + ), + Icon(Icons.search, color: Colors.black.withOpacity(0.54)), + const SizedBox(width: 8), + ], + ), + ), + Flexible( + child: PagedListView.separated( + pagingController: + getIt().paginationRepository.pagingController, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shrinkWrap: true, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + final state = getIt().state; + Widget? userInfoWidget; + + if (state is AssigneeSelectedState) { + final selectedId = state.assignee.userInfo.id.id; + if (selectedId == item.userInfo.id.id) { + userInfoWidget = const SizedBox.shrink(); + } + } + + return Column( + children: [ + Visibility( + visible: index == 0 && + state is! AssigneeSelfAssignmentState, + child: Column( + children: [ + UserInfoWidget( + avatar: Icon( + Icons.account_circle, + color: Colors.black.withOpacity(0.38), + ), + name: 'Assigned to me', + onUserTap: (id) { + Navigator.of(context).pop(); + getIt().add( + AssigneeSelectedEvent( + userId: id, + selfAssignment: true, + ), + ); + + onChanged(); + }, + id: tbContext.tbClient.getAuthUser()!.userId!, + ), + const Divider(thickness: 1, height: 24), + ], + ), + ), + userInfoWidget ?? + UserInfoWidget( + avatar: UserInfoAvatarWidget( + shortName: item.shortName, + color: HSLColor.fromAHSL( + 1, + item.displayName.hashCode % 360, + 40 / 100, + 60 / 100, + ).toColor(), + ), + name: item.displayName, + email: item.userInfo.email, + showEmail: !item.displayName.isValidEmail(), + onUserTap: (id) { + Navigator.of(context).pop(); + getIt().add( + AssigneeSelectedEvent(userId: id), + ); + + onChanged(); + }, + id: item.userInfo.id.id!, + ), + ], + ); + }, + firstPageProgressIndicatorBuilder: (_) { + return Container( + height: 200, + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator(size: 50.0), + ), + ); + }, + ), + separatorBuilder: (_, index) { + final state = getIt().state; + + if (state is AssigneeSelectedState) { + final selectedId = state.assignee.userInfo.id.id; + final userId = getIt() + .paginationRepository + .pagingController + .itemList?[index]; + if (selectedId == userId?.userInfo.id.id) { + return const SizedBox.shrink(); + } + } + + return const Divider(thickness: 1, height: 24); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart new file mode 100644 index 00000000..6e9d4e9d --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class UserInfoAvatarWidget extends StatelessWidget { + const UserInfoAvatarWidget({ + required this.shortName, + required this.color, + super.key, + }) : assert(shortName.length <= 2); + + final String shortName; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: color, + ), + height: 24, + width: 24, + child: Center( + child: Text( + shortName, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 11, + ), + ), + ), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart new file mode 100644 index 00000000..ce9a87e0 --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class UserInfoWidget extends StatelessWidget { + const UserInfoWidget({ + required this.id, + required this.avatar, + required this.name, + this.email = '', + this.showEmail = false, + this.onUserTap, + super.key, + }); + + final String id; + final Widget avatar; + final String name; + final String email; + final bool showEmail; + final Function(String)? onUserTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + // To disable the gesture detection tap handler, and allow the parent tap handler to process the event. + onTap: onUserTap != null ? () => onUserTap!.call(id) : null, + child: Row( + children: [ + avatar, + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + Visibility( + visible: showEmail, + child: Text( + email, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: Colors.black.withOpacity(0.38), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart b/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart new file mode 100644 index 00000000..59382f0e --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_filter_widget.dart'; + +class FilterToggleBlockWidget extends StatefulWidget { + const FilterToggleBlockWidget({ + required this.label, + required this.items, + required this.onSelectedChanged, + required this.labelAtIndex, + this.selected = const {}, + this.multiselect = true, + super.key, + }); + + final String label; + final List items; + final ValueChanged> onSelectedChanged; + final String Function(int) labelAtIndex; + final Set selected; + final bool multiselect; + + @override + State createState() => _FilterToggleBlockWidgetState(); +} + +class _FilterToggleBlockWidgetState extends State { + final selected = {}; + + @override + void initState() { + selected.addAll(widget.selected); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlarmFilterWidget( + filterTitle: widget.label, + child: Wrap( + spacing: 12, + runSpacing: 12, + children: List.generate( + widget.items.length, + (index) => FilledButton.icon( + onPressed: () { + setState(() { + final element = widget.items[index]; + + if (selected.contains(element)) { + selected.remove(element); + } else { + selected.add(element); + } + }); + + widget.onSelectedChanged(selected.toList()); + }, + label: Text( + widget.labelAtIndex(index), + style: !selected.contains(widget.items[index]) + ? TextStyle( + color: Colors.black.withOpacity(0.38), + fontWeight: FontWeight.w400, + fontSize: 13, + ) + : const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 13, + ), + ), + icon: !selected.contains(widget.items[index]) + ? const SizedBox.shrink() + : const Icon(Icons.check), + style: FilledButton.styleFrom( + backgroundColor: !selected.contains(widget.items[index]) + ? Colors.black.withOpacity(0.06) + : null, + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/asset/asset_details_page.dart b/lib/modules/asset/asset_details_page.dart index 3e858446..3567486c 100644 --- a/lib/modules/asset/asset_details_page.dart +++ b/lib/modules/asset/asset_details_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entity_details_page.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class AssetDetailsPage extends EntityDetailsPage { AssetDetailsPage(TbContext tbContext, String assetId, {super.key}) diff --git a/lib/modules/asset/assets_base.dart b/lib/modules/asset/assets_base.dart index b8d6fc3e..d0132733 100644 --- a/lib/modules/asset/assets_base.dart +++ b/lib/modules/asset/assets_base.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; mixin AssetsBase on EntitiesBase { @override diff --git a/lib/modules/asset/assets_list.dart b/lib/modules/asset/assets_list.dart index 08b0425c..a61bdd7c 100644 --- a/lib/modules/asset/assets_list.dart +++ b/lib/modules/asset/assets_list.dart @@ -1,7 +1,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'assets_base.dart'; diff --git a/lib/modules/asset/assets_list_widget.dart b/lib/modules/asset/assets_list_widget.dart index 2a6c43a2..30faec71 100644 --- a/lib/modules/asset/assets_list_widget.dart +++ b/lib/modules/asset/assets_list_widget.dart @@ -1,7 +1,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_list_widget.dart'; import 'package:thingsboard_app/modules/asset/assets_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class AssetsListWidget extends EntitiesListPageLinkWidget with AssetsBase { diff --git a/lib/modules/audit_log/audit_log_details_page.dart b/lib/modules/audit_log/audit_log_details_page.dart index 75a2d9c7..20e6be48 100644 --- a/lib/modules/audit_log/audit_log_details_page.dart +++ b/lib/modules/audit_log/audit_log_details_page.dart @@ -1,13 +1,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.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:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/modules/audit_log/audit_logs_base.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class AuditLogDetailsPage extends TbContextWidget { final AuditLog auditLog; diff --git a/lib/modules/audit_log/audit_logs_base.dart b/lib/modules/audit_log/audit_logs_base.dart index aa668208..bee0be86 100644 --- a/lib/modules/audit_log/audit_logs_base.dart +++ b/lib/modules/audit_log/audit_logs_base.dart @@ -5,7 +5,7 @@ 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/modules/audit_log/audit_log_details_page.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; const Map actionTypeTranslations = { ActionType.ADDED: 'Added', diff --git a/lib/modules/audit_log/audit_logs_list.dart b/lib/modules/audit_log/audit_logs_list.dart index c15a5a1f..e3a9c25d 100644 --- a/lib/modules/audit_log/audit_logs_list.dart +++ b/lib/modules/audit_log/audit_logs_list.dart @@ -2,7 +2,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list.dart'; import 'package:thingsboard_app/modules/audit_log/audit_logs_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class AuditLogsList extends BaseEntitiesWidget with AuditLogsBase, EntitiesListStateBase { diff --git a/lib/modules/customer/customer_details_page.dart b/lib/modules/customer/customer_details_page.dart index c84652f7..c8755759 100644 --- a/lib/modules/customer/customer_details_page.dart +++ b/lib/modules/customer/customer_details_page.dart @@ -1,6 +1,6 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entity_details_page.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class CustomerDetailsPage extends ContactBasedDetailsPage { CustomerDetailsPage(TbContext tbContext, String customerId, {super.key}) diff --git a/lib/modules/customer/customers_base.dart b/lib/modules/customer/customers_base.dart index 33699204..3233476f 100644 --- a/lib/modules/customer/customers_base.dart +++ b/lib/modules/customer/customers_base.dart @@ -1,5 +1,5 @@ import 'package:thingsboard_app/core/entity/entities_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; mixin CustomersBase on EntitiesBase { @override diff --git a/lib/modules/customer/customers_list.dart b/lib/modules/customer/customers_list.dart index 607d0cb4..df3566ef 100644 --- a/lib/modules/customer/customers_list.dart +++ b/lib/modules/customer/customers_list.dart @@ -1,7 +1,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'customers_base.dart'; diff --git a/lib/modules/dashboard/dashboards_base.dart b/lib/modules/dashboard/dashboards_base.dart index fbc19730..16d3e3f5 100644 --- a/lib/modules/dashboard/dashboards_base.dart +++ b/lib/modules/dashboard/dashboards_base.dart @@ -5,8 +5,8 @@ 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'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/utils.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; mixin DashboardsBase on EntitiesBase { @override @@ -187,6 +187,7 @@ class _DashboardGridCardState extends TbContextState { semanticsLabel: 'Dashboard', ); } + return ClipRRect( borderRadius: BorderRadius.circular(4), child: Column( diff --git a/lib/modules/dashboard/dashboards_grid.dart b/lib/modules/dashboard/dashboards_grid.dart index f8d3b8f4..fcf081a4 100644 --- a/lib/modules/dashboard/dashboards_grid.dart +++ b/lib/modules/dashboard/dashboards_grid.dart @@ -3,7 +3,7 @@ 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/core/entity/entities_grid.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'dashboards_base.dart'; diff --git a/lib/modules/dashboard/dashboards_list.dart b/lib/modules/dashboard/dashboards_list.dart index 6874f9a4..04c0dc16 100644 --- a/lib/modules/dashboard/dashboards_list.dart +++ b/lib/modules/dashboard/dashboards_list.dart @@ -1,7 +1,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'dashboards_base.dart'; diff --git a/lib/modules/dashboard/dashboards_list_widget.dart b/lib/modules/dashboard/dashboards_list_widget.dart index 3cfd49ce..8ad0bf26 100644 --- a/lib/modules/dashboard/dashboards_list_widget.dart +++ b/lib/modules/dashboard/dashboards_list_widget.dart @@ -1,7 +1,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_list_widget.dart'; import 'package:thingsboard_app/modules/dashboard/dashboards_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class DashboardsListWidget extends EntitiesListPageLinkWidget with DashboardsBase { diff --git a/lib/modules/device/device_details_page.dart b/lib/modules/device/device_details_page.dart index ab18edb0..4e7f88cf 100644 --- a/lib/modules/device/device_details_page.dart +++ b/lib/modules/device/device_details_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entity_details_page.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class DeviceDetailsPage extends EntityDetailsPage { DeviceDetailsPage(TbContext tbContext, String deviceId, {super.key}) diff --git a/lib/modules/device/device_profiles_base.dart b/lib/modules/device/device_profiles_base.dart index 51044291..8ab4eb05 100644 --- a/lib/modules/device/device_profiles_base.dart +++ b/lib/modules/device/device_profiles_base.dart @@ -2,16 +2,16 @@ import 'dart:async'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.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_base.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:thingsboard_app/thingsboard_client.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/utils/utils.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; mixin DeviceProfilesBase on EntitiesBase { final RefreshDeviceCounts refreshDeviceCounts = RefreshDeviceCounts(); diff --git a/lib/modules/device/device_profiles_grid.dart b/lib/modules/device/device_profiles_grid.dart index 812f4f89..e39f919d 100644 --- a/lib/modules/device/device_profiles_grid.dart +++ b/lib/modules/device/device_profiles_grid.dart @@ -1,7 +1,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_grid.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'device_profiles_base.dart'; diff --git a/lib/modules/device/devices_base.dart b/lib/modules/device/devices_base.dart index cde9decc..47b3a135 100644 --- a/lib/modules/device/devices_base.dart +++ b/lib/modules/device/devices_base.dart @@ -1,17 +1,17 @@ import 'dart:core'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:flutter_svg/flutter_svg.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'; -import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:thingsboard_app/thingsboard_client.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/utils/utils.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; mixin DevicesBase on EntitiesBase { @override diff --git a/lib/modules/device/devices_list.dart b/lib/modules/device/devices_list.dart index e56a2ec7..dc233e2c 100644 --- a/lib/modules/device/devices_list.dart +++ b/lib/modules/device/devices_list.dart @@ -2,7 +2,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list.dart'; import 'package:thingsboard_app/modules/device/devices_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class DevicesList extends BaseEntitiesWidget with DevicesBase, EntitiesListStateBase { diff --git a/lib/modules/device/devices_list_widget.dart b/lib/modules/device/devices_list_widget.dart index def3238a..400093c3 100644 --- a/lib/modules/device/devices_list_widget.dart +++ b/lib/modules/device/devices_list_widget.dart @@ -2,7 +2,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list_widget.dart'; import 'package:thingsboard_app/modules/device/devices_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class DevicesListWidget extends EntitiesListWidget with DevicesBase { diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index 5d0a4a4a..ecee32d0 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -7,8 +7,8 @@ import 'package:thingsboard_app/modules/dashboard/dashboard.dart' as dashboard_ui; import 'package:thingsboard_app/modules/dashboard/dashboards_grid.dart'; import 'package:thingsboard_app/modules/tenant/tenants_widget.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class HomePage extends TbContextWidget { HomePage(TbContext tbContext, {super.key}) : super(tbContext); @@ -27,8 +27,10 @@ class _HomePageState extends TbContextState @override Widget build(BuildContext context) { super.build(context); - var homeDashboard = tbContext.homeDashboard; - var dashboardState = homeDashboard != null; + + final homeDashboard = tbContext.homeDashboard; + final dashboardState = homeDashboard != null; + return Scaffold( appBar: TbAppBar( tbContext, diff --git a/lib/modules/main/main_navigation_item.dart b/lib/modules/main/main_navigation_item.dart new file mode 100644 index 00000000..2c01a5cd --- /dev/null +++ b/lib/modules/main/main_navigation_item.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/view/alarms_page.dart'; +import 'package:thingsboard_app/modules/device/devices_main_page.dart'; +import 'package:thingsboard_app/modules/home/home_page.dart'; +import 'package:thingsboard_app/modules/more/more_page.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class TbMainNavigationItem { + final Widget page; + String title; + final Icon icon; + final String path; + + TbMainNavigationItem({ + required this.page, + required this.title, + required this.icon, + required this.path, + }); + + static const mainPageStateMap = >{ + Authority.SYS_ADMIN: {'/home', '/more'}, + Authority.TENANT_ADMIN: {'/home', '/alarms', '/devices', '/more'}, + Authority.CUSTOMER_USER: {'/home', '/alarms', '/devices', '/more'}, + }; + + static bool isMainPageState(TbContext tbContext, String path) { + if (tbContext.isAuthenticated) { + return mainPageStateMap[tbContext.tbClient.getAuthUser()!.authority]! + .contains(path); + } else { + return false; + } + } + + static List getItems(TbContext tbContext) { + if (tbContext.isAuthenticated) { + final items = [ + TbMainNavigationItem( + page: HomePage(tbContext), + title: 'Home', + icon: const Icon(Icons.home), + path: '/home', + ), + ]; + + switch (tbContext.tbClient.getAuthUser()!.authority) { + case Authority.SYS_ADMIN: + break; + case Authority.TENANT_ADMIN: + case Authority.CUSTOMER_USER: + items.addAll( + [ + TbMainNavigationItem( + page: AlarmsPage(tbContext), + title: 'Alarms', + icon: const Icon(Icons.notifications), + path: '/alarms', + ), + TbMainNavigationItem( + page: DevicesMainPage(tbContext), + title: 'Devices', + icon: const Icon(Icons.devices_other), + path: '/devices', + ), + ], + ); + break; + case Authority.REFRESH_TOKEN: + break; + case Authority.ANONYMOUS: + break; + case Authority.PRE_VERIFICATION_TOKEN: + break; + } + + items.add( + TbMainNavigationItem( + page: MorePage(tbContext), + title: 'More', + icon: const Icon(Icons.menu), + path: '/more', + ), + ); + return items; + } else { + return []; + } + } + + static void changeItemsTitleIntl( + List items, + BuildContext context, + ) { + for (final item in items) { + switch (item.path) { + case '/home': + item.title = S.of(context).home; + break; + case '/alarms': + item.title = S.of(context).alarms; + break; + case '/devices': + item.title = S.of(context).devices; + break; + case '/more': + item.title = S.of(context).more; + break; + } + } + } +} diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart index fe58edf8..55480c7c 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -1,124 +1,18 @@ 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:flutter_gen/gen_l10n/messages.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/home/home_page.dart'; -import 'package:thingsboard_app/modules/more/more_page.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; - -class TbMainNavigationItem { - final Widget page; - String title; - final Icon icon; - final String path; - - TbMainNavigationItem({ - required this.page, - required this.title, - required this.icon, - required this.path, - }); - - static Map> mainPageStateMap = { - Authority.SYS_ADMIN: Set.unmodifiable(['/home', '/more']), - Authority.TENANT_ADMIN: - Set.unmodifiable(['/home', '/alarms', '/devices', '/more']), - Authority.CUSTOMER_USER: - Set.unmodifiable(['/home', '/alarms', '/devices', '/more']), - }; - - static bool isMainPageState(TbContext tbContext, String path) { - if (tbContext.isAuthenticated) { - return mainPageStateMap[tbContext.tbClient.getAuthUser()!.authority]! - .contains(path); - } else { - return false; - } - } - - static List getItems(TbContext tbContext) { - if (tbContext.isAuthenticated) { - List items = [ - TbMainNavigationItem( - page: HomePage(tbContext), - title: 'Home', - icon: const Icon(Icons.home), - path: '/home', - ), - ]; - switch (tbContext.tbClient.getAuthUser()!.authority) { - case Authority.SYS_ADMIN: - break; - case Authority.TENANT_ADMIN: - case Authority.CUSTOMER_USER: - items.addAll([ - TbMainNavigationItem( - page: AlarmsPage(tbContext), - title: 'Alarms', - icon: const Icon(Icons.notifications), - path: '/alarms', - ), - TbMainNavigationItem( - page: DevicesMainPage(tbContext), - title: 'Devices', - icon: const Icon(Icons.devices_other), - path: '/devices', - ), - ]); - break; - case Authority.REFRESH_TOKEN: - break; - case Authority.ANONYMOUS: - break; - case Authority.PRE_VERIFICATION_TOKEN: - break; - } - items.add( - TbMainNavigationItem( - page: MorePage(tbContext), - title: 'More', - icon: const Icon(Icons.menu), - path: '/more', - ), - ); - return items; - } else { - return []; - } - } - - static void changeItemsTitleIntl( - List items, - BuildContext context, - ) { - for (var item in items) { - switch (item.path) { - case '/home': - item.title = S.of(context).home; - break; - case '/alarms': - item.title = S.of(context).alarms; - break; - case '/devices': - item.title = S.of(context).devices; - break; - case '/more': - item.title = S.of(context).more; - break; - } - } - } -} +import 'package:thingsboard_app/modules/main/main_navigation_item.dart'; class MainPage extends TbPageWidget { - final String _path; - - MainPage(TbContext tbContext, {required String path, super.key}) - : _path = path, + MainPage( + TbContext tbContext, { + super.key, + required String path, + }) : _path = path, super(tbContext); + final String _path; + @override State createState() => _MainPageState(); } @@ -172,12 +66,10 @@ class _MainPageState extends TbPageState @override Widget build(BuildContext context) { TbMainNavigationItem.changeItemsTitleIntl(_tabItems, context); - // ignore: deprecated_member_use + return Scaffold( body: TabBarView( - physics: tbContext.homeDashboard != null - ? const NeverScrollableScrollPhysics() - : null, + physics: const NeverScrollableScrollPhysics(), controller: _tabController, children: _tabItems.map((item) => item.page).toList(), ), @@ -186,7 +78,7 @@ class _MainPageState extends TbPageState builder: (context, index, child) => BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: index, - onTap: (int index) => _setIndex(index) /*_currentIndex = index*/, + onTap: (int index) => _setIndex(index), items: _tabItems .map( (item) => BottomNavigationBarItem( @@ -220,8 +112,11 @@ class _MainPageState extends TbPageState return _tabController.index == 0; } - _setIndex(int index) { - hideNotification(); - _tabController.index = index; + void _setIndex(int index) { + if (_tabController.index != index) { + hideNotification(); + _tabController.index = index; + tbContext.bottomNavigationTabChangedStream.add(index); + } } } diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart index 621cdb16..ca975656 100644 --- a/lib/modules/more/more_page.dart +++ b/lib/modules/more/more_page.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_app/utils/services/notification_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class MorePage extends TbContextWidget { MorePage(TbContext tbContext, {super.key}) : super(tbContext); diff --git a/lib/modules/notification/controllers/notification_query_ctrl.dart b/lib/modules/notification/controllers/notification_query_ctrl.dart index 3b3c7384..70d29e34 100644 --- a/lib/modules/notification/controllers/notification_query_ctrl.dart +++ b/lib/modules/notification/controllers/notification_query_ctrl.dart @@ -1,5 +1,5 @@ import 'package:thingsboard_app/core/entity/entities_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NotificationQueryCtrl extends PageKeyController { NotificationQueryCtrl({int pageSize = 20, String? searchText}) diff --git a/lib/modules/notification/repository/notification_pagination_repository.dart b/lib/modules/notification/repository/notification_pagination_repository.dart index 6bf60f07..1964f0f4 100644 --- a/lib/modules/notification/repository/notification_pagination_repository.dart +++ b/lib/modules/notification/repository/notification_pagination_repository.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:thingsboard_app/modules/notification/controllers/notification_query_ctrl.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NotificationPaginationRepository { NotificationPaginationRepository({ @@ -12,10 +12,12 @@ class NotificationPaginationRepository { final NotificationQueryCtrl notificationQueryPageCtrl; final ThingsboardClient tbClient; - late final PagingController pagingController; + late final PagingController + pagingController; void init() { - pagingController = PagingController( + pagingController = + PagingController( firstPageKey: notificationQueryPageCtrl.value.pageKey, ); diff --git a/lib/modules/notification/repository/notification_repository.dart b/lib/modules/notification/repository/notification_repository.dart index 5b016258..e8495157 100644 --- a/lib/modules/notification/repository/notification_repository.dart +++ b/lib/modules/notification/repository/notification_repository.dart @@ -2,7 +2,7 @@ import 'package:thingsboard_app/modules/notification/controllers/notification_qu import 'package:thingsboard_app/modules/notification/repository/i_notification_query_repository.dart'; import 'package:thingsboard_app/modules/notification/service/i_notifications_local_service.dart'; import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NotificationRepository implements INotificationQueryRepository { NotificationRepository({ diff --git a/lib/modules/notification/service/notifications_local_service.dart b/lib/modules/notification/service/notifications_local_service.dart index 36ee09c5..111e4ee4 100644 --- a/lib/modules/notification/service/notifications_local_service.dart +++ b/lib/modules/notification/service/notifications_local_service.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/notification/service/i_notifications_local_service.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; final class NotificationsLocalService implements INotificationsLocalService { NotificationsLocalService() : storage = getIt(); diff --git a/lib/modules/notification/widgets/notification_icon.dart b/lib/modules/notification/widgets/notification_icon.dart index b4f4d32c..496543e5 100644 --- a/lib/modules/notification/widgets/notification_icon.dart +++ b/lib/modules/notification/widgets/notification_icon.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NotificationIcon extends StatelessWidget { const NotificationIcon({super.key, required this.notification}); diff --git a/lib/modules/notification/widgets/notification_list.dart b/lib/modules/notification/widgets/notification_list.dart index aef9860a..4ad5d803 100644 --- a/lib/modules/notification/widgets/notification_list.dart +++ b/lib/modules/notification/widgets/notification_list.dart @@ -4,8 +4,8 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/modules/notification/widgets/no_notifications_found_widget.dart'; import 'package:thingsboard_app/modules/notification/widgets/notification_slidable_widget.dart'; import 'package:thingsboard_app/modules/notification/widgets/notification_widget.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class NotificationsList extends StatelessWidget { const NotificationsList({ @@ -21,16 +21,17 @@ class NotificationsList extends StatelessWidget { final Function(String id, bool read) onClearNotification; final ValueChanged onReadNotification; final TbContext tbContext; - final PagingController pagingController; + final PagingController + pagingController; @override Widget build(BuildContext context) { - return PagedListView.separated( + return PagedListView.separated( pagingController: pagingController, builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) { return NotificationSlidableWidget( - notification: item as PushNotification, + notification: item, onReadNotification: onReadNotification, onClearNotification: onClearNotification, tbContext: tbContext, diff --git a/lib/modules/notification/widgets/notification_slidable_widget.dart b/lib/modules/notification/widgets/notification_slidable_widget.dart index 26e8ab91..3e11b035 100644 --- a/lib/modules/notification/widgets/notification_slidable_widget.dart +++ b/lib/modules/notification/widgets/notification_slidable_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NotificationSlidableWidget extends StatefulWidget { const NotificationSlidableWidget({ diff --git a/lib/modules/notification/widgets/notification_widget.dart b/lib/modules/notification/widgets/notification_widget.dart index bb098c96..447727c9 100644 --- a/lib/modules/notification/widgets/notification_widget.dart +++ b/lib/modules/notification/widgets/notification_widget.dart @@ -3,8 +3,8 @@ import 'package:flutter_html/flutter_html.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/modules/alarm/alarms_base.dart'; import 'package:thingsboard_app/modules/notification/widgets/notification_icon.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/notification_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; import 'package:timeago/timeago.dart' as timeago; class NotificationWidget extends StatelessWidget { diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart index d01030a5..ce532b81 100644 --- a/lib/modules/profile/profile_page.dart +++ b/lib/modules/profile/profile_page.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:thingsboard_app/modules/profile/change_password_page.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class ProfilePage extends TbPageWidget { final bool _fullscreen; diff --git a/lib/modules/tenant/tenant_details_page.dart b/lib/modules/tenant/tenant_details_page.dart index dc7b3f47..3e865c29 100644 --- a/lib/modules/tenant/tenant_details_page.dart +++ b/lib/modules/tenant/tenant_details_page.dart @@ -1,6 +1,6 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entity_details_page.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class TenantDetailsPage extends ContactBasedDetailsPage { TenantDetailsPage(TbContext tbContext, String tenantId, {super.key}) diff --git a/lib/modules/tenant/tenants_base.dart b/lib/modules/tenant/tenants_base.dart index a61dabe0..ce369291 100644 --- a/lib/modules/tenant/tenants_base.dart +++ b/lib/modules/tenant/tenants_base.dart @@ -1,5 +1,5 @@ import 'package:thingsboard_app/core/entity/entities_base.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; mixin TenantsBase on EntitiesBase { @override diff --git a/lib/modules/tenant/tenants_list.dart b/lib/modules/tenant/tenants_list.dart index b91425b0..3a62a440 100644 --- a/lib/modules/tenant/tenants_list.dart +++ b/lib/modules/tenant/tenants_list.dart @@ -1,7 +1,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'tenants_base.dart'; diff --git a/lib/thingsboard_client.dart b/lib/thingsboard_client.dart new file mode 100644 index 00000000..baab71a0 --- /dev/null +++ b/lib/thingsboard_client.dart @@ -0,0 +1,9 @@ +/// Since the CE and PE versions are mergeable, we frequently encounter merge +/// conflicts due to the different names of the Dart client. +/// The purpose of this file is to resolve these conflicts. +/// +/// By exporting the TB Client here, we ensure a consistent name for the client +/// throughout the project. This file will change rarely, +/// thus minimizing merge conflicts. + +export 'package:thingsboard_client/thingsboard_client.dart'; diff --git a/lib/utils/services/_tb_app_storage.dart b/lib/utils/services/_tb_app_storage.dart index 199bd02f..5228ce48 100644 --- a/lib/utils/services/_tb_app_storage.dart +++ b/lib/utils/services/_tb_app_storage.dart @@ -1,3 +1,3 @@ -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; TbStorage createAppStorage() => throw UnsupportedError(''); diff --git a/lib/utils/services/_tb_secure_storage.dart b/lib/utils/services/_tb_secure_storage.dart index 53e1c45f..9186e07b 100644 --- a/lib/utils/services/_tb_secure_storage.dart +++ b/lib/utils/services/_tb_secure_storage.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; TbStorage createAppStorage() => TbSecureStorage(); diff --git a/lib/utils/services/_tb_web_local_storage.dart b/lib/utils/services/_tb_web_local_storage.dart index 5d1647ee..16394e89 100644 --- a/lib/utils/services/_tb_web_local_storage.dart +++ b/lib/utils/services/_tb_web_local_storage.dart @@ -1,4 +1,4 @@ -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:universal_html/html.dart' as html; TbStorage createAppStorage() => TbWebLocalStorage(); diff --git a/lib/utils/services/device_profile_cache.dart b/lib/utils/services/device_profile_cache.dart index 49594876..d30417c8 100644 --- a/lib/utils/services/device_profile_cache.dart +++ b/lib/utils/services/device_profile_cache.dart @@ -1,4 +1,4 @@ -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; abstract class DeviceProfileCache { static final _cache = {}; diff --git a/lib/utils/services/entity_query_api.dart b/lib/utils/services/entity_query_api.dart index b6690909..d443147c 100644 --- a/lib/utils/services/entity_query_api.dart +++ b/lib/utils/services/entity_query_api.dart @@ -1,4 +1,4 @@ -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; abstract class EntityQueryApi { static final activeDeviceKeyFilter = KeyFilter( diff --git a/lib/utils/services/local_database/i_local_database_service.dart b/lib/utils/services/local_database/i_local_database_service.dart index e3d324c4..88788e25 100644 --- a/lib/utils/services/local_database/i_local_database_service.dart +++ b/lib/utils/services/local_database/i_local_database_service.dart @@ -1,4 +1,4 @@ -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; /// The aim of this service is to consolidate operations with /// the local database provider into one centralized location. diff --git a/lib/utils/services/local_database/local_database_service.dart b/lib/utils/services/local_database/local_database_service.dart index ee57cb86..701aa4f2 100644 --- a/lib/utils/services/local_database/local_database_service.dart +++ b/lib/utils/services/local_database/local_database_service.dart @@ -1,6 +1,6 @@ import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class LocalDatabaseService implements ILocalDatabaseService { const LocalDatabaseService({ diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart index 8475168d..5bb65a4b 100644 --- a/lib/utils/services/notification_service.dart +++ b/lib/utils/services/notification_service.dart @@ -8,8 +8,8 @@ import 'package:thingsboard_app/core/logger/tb_logger.dart'; import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/notification/service/i_notifications_local_service.dart'; import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/utils.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class NotificationService { static final NotificationService _instance = NotificationService._(); diff --git a/lib/utils/services/pagination_repository.dart b/lib/utils/services/pagination_repository.dart new file mode 100644 index 00000000..aaf99d92 --- /dev/null +++ b/lib/utils/services/pagination_repository.dart @@ -0,0 +1,69 @@ +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract base class PaginationRepository { + PaginationRepository({ + required this.pageKeyController, + }) { + init(); + } + + late final PagingController pagingController; + final PageKeyController pageKeyController; + + void init() { + pagingController = PagingController( + firstPageKey: pageKeyController.value.pageKey, + ); + + pageKeyController.addListener(_didChangePageKeyValue); + pagingController.addPageRequestListener((pageKey) { + _fetchPage(pageKey); + }); + } + + void dispose() { + pageKeyController.removeListener(_didChangePageKeyValue); + pagingController.dispose(); + } + + void refresh() { + _fetchPage(pagingController.firstPageKey, refresh: true); + } + + Future> fetchPageData(T pageKey); + + Future _fetchPage( + T pageKey, { + bool refresh = false, + }) async { + try { + final pageData = await fetchPageData(pageKey); + + final isLastPage = !pageData.hasNext; + if (refresh) { + var state = pagingController.value; + if (state.itemList != null) { + state.itemList!.clear(); + } + } + if (isLastPage) { + pagingController.appendLastPage(pageData.data); + } else { + final nextPageKey = pageKeyController.nextPageKey(pageKey); + pagingController.appendPage(pageData.data, nextPageKey); + } + } catch (error) { + pagingController.error = error; + } + } + + void _didChangePageKeyValue() { + _refreshPagingController(); + } + + void _refreshPagingController() { + _fetchPage(pageKeyController.value.pageKey, refresh: true); + } +} diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart new file mode 100644 index 00000000..07c228d8 --- /dev/null +++ b/lib/utils/string_utils.dart @@ -0,0 +1,7 @@ +extension EmailValidator on String { + bool isValidEmail() { + return RegExp( + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', + ).hasMatch(this); + } +} diff --git a/lib/utils/ui/back_button_widget.dart b/lib/utils/ui/back_button_widget.dart new file mode 100644 index 00000000..a9470b48 --- /dev/null +++ b/lib/utils/ui/back_button_widget.dart @@ -0,0 +1,19 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class BackButtonWidget extends StatelessWidget { + const BackButtonWidget({this.onPressed, super.key}); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: onPressed, + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back, + ), + ); + } +} diff --git a/lib/utils/ui/pagination_list_widget.dart b/lib/utils/ui/pagination_list_widget.dart new file mode 100644 index 00000000..e1cb19de --- /dev/null +++ b/lib/utils/ui/pagination_list_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +class PaginationListWidget extends StatelessWidget { + const PaginationListWidget({ + required this.pagingController, + required this.builderDelegate, + this.separatorWidgetBuilder, + this.heading, + super.key, + }); + + final Widget? heading; + final PagingController pagingController; + final PagedChildBuilderDelegate builderDelegate; + final IndexedWidgetBuilder? separatorWidgetBuilder; + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverVisibility( + visible: heading != null, + sliver: SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + sliver: SliverToBoxAdapter(child: heading), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: PagedSliverList.separated( + pagingController: pagingController, + builderDelegate: builderDelegate, + separatorBuilder: + separatorWidgetBuilder ?? (_, __) => const SizedBox(height: 8), + ), + ) + ], + ); + } +} diff --git a/lib/utils/ui/pagination_widgets/first_page_exception_widget.dart b/lib/utils/ui/pagination_widgets/first_page_exception_widget.dart new file mode 100644 index 00000000..abd894bc --- /dev/null +++ b/lib/utils/ui/pagination_widgets/first_page_exception_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; + +class FirstPageExceptionIndicator extends StatelessWidget { + const FirstPageExceptionIndicator({ + required this.title, + this.message, + this.onTryAgain, + Key? key, + }) : super(key: key); + + final String title; + final String? message; + final VoidCallback? onTryAgain; + + @override + Widget build(BuildContext context) { + final message = this.message; + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), + child: Column( + children: [ + Text( + title, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + if (message != null) + const SizedBox( + height: 16, + ), + if (message != null) + Text( + message, + textAlign: TextAlign.center, + ), + if (onTryAgain != null) + const SizedBox( + height: 48, + ), + if (onTryAgain != null) + SizedBox( + height: 50, + width: double.infinity, + child: ElevatedButton.icon( + onPressed: onTryAgain, + icon: const Icon( + Icons.refresh, + color: Colors.white, + ), + label: Text( + S.of(context).tryAgain, + style: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/utils/ui/pagination_widgets/first_page_progress_builder.dart b/lib/utils/ui/pagination_widgets/first_page_progress_builder.dart new file mode 100644 index 00000000..a0ffc784 --- /dev/null +++ b/lib/utils/ui/pagination_widgets/first_page_progress_builder.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class FirstPageProgressBuilder extends StatelessWidget { + const FirstPageProgressBuilder({super.key}); + + @override + Widget build(BuildContext context) { + return const Stack( + children: [ + Positioned( + top: 20, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RefreshProgressIndicator(), + ], + ), + ), + ], + ); + } +} diff --git a/lib/utils/ui/pagination_widgets/new_page_progress_builder.dart b/lib/utils/ui/pagination_widgets/new_page_progress_builder.dart new file mode 100644 index 00000000..c8ce8958 --- /dev/null +++ b/lib/utils/ui/pagination_widgets/new_page_progress_builder.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class NewPageProgressBuilder extends StatelessWidget { + const NewPageProgressBuilder({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.only( + top: 16, + bottom: 16, + ), + child: Center( + child: RefreshProgressIndicator(), + ), + ); + } +} diff --git a/lib/utils/ui/ui_utils.dart b/lib/utils/ui/ui_utils.dart new file mode 100644 index 00000000..7478fe0d --- /dev/null +++ b/lib/utils/ui/ui_utils.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +abstract class UiUtils { + static Future showModalBottomSheet({ + required BuildContext context, + required WidgetBuilder builder, + Color barrierColor = Colors.black54, + }) async { + return showBarModalBottomSheet( + context: context, + builder: builder, + barrierColor: barrierColor, + ); + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 88a4ee76..a67385ae 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -3,8 +3,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:jovial_svg/jovial_svg.dart'; import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; abstract class Utils { static const _tbImagePrefix = 'tb-image;'; diff --git a/lib/widgets/tb_app_bar.dart b/lib/widgets/tb_app_bar.dart index 1e9adeac..7a69de00 100644 --- a/lib/widgets/tb_app_bar.dart +++ b/lib/widgets/tb_app_bar.dart @@ -74,6 +74,7 @@ class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget { final bool showLoadingIndicator; final String? searchHint; final void Function(String searchText)? onSearch; + final Widget? leading; @override final Size preferredSize; @@ -86,6 +87,7 @@ class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget { this.showLoadingIndicator = false, this.searchHint, this.onSearch, + this.leading, }) : preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)), super(tbContext); @@ -149,7 +151,6 @@ class _TbAppSearchBarState extends TbContextState { title: TextField( controller: _filter, autofocus: true, - // cursorColor: Colors.white, decoration: InputDecoration( border: InputBorder.none, hintStyle: TextStyle( @@ -160,6 +161,7 @@ class _TbAppSearchBarState extends TbContextState { hintText: widget.searchHint ?? 'Search', ), ), + leading: widget.leading, actions: [ ValueListenableBuilder( valueListenable: _filter, diff --git a/lib/widgets/two_page_view.dart b/lib/widgets/two_page_view.dart index 249e0809..edc760f5 100644 --- a/lib/widgets/two_page_view.dart +++ b/lib/widgets/two_page_view.dart @@ -47,7 +47,7 @@ class _TwoPageViewState extends State { late List _pages; bool _reverse = false; int _selectedIndex = 0; - final PreloadPageController _pageController = PreloadPageController(); + final _pageController = PreloadPageController(); @override void initState() { @@ -56,6 +56,12 @@ class _TwoPageViewState extends State { super.initState(); } + @override + void didUpdateWidget(TwoPageView oldWidget) { + super.didUpdateWidget(oldWidget); + _pages = [widget.first, widget.second]; + } + Future _open(int index, {bool animate = true}) async { if (_selectedIndex != index) { _selectedIndex = index; @@ -92,6 +98,12 @@ class _TwoPageViewState extends State { return false; } + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return PreloadPageView( diff --git a/pubspec.yaml b/pubspec.yaml index de5b16b8..910ac15f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: html: ^0.15.4 hive: ^2.2.3 hive_flutter: ^1.1.0 + modal_bottom_sheet: ^3.0.0 dev_dependencies: flutter_test: diff --git a/test/ core/noauth/switch_endpoint_test.dart b/test/ core/noauth/switch_endpoint_test.dart index 73f39233..72512b53 100644 --- a/test/ core/noauth/switch_endpoint_test.dart +++ b/test/ core/noauth/switch_endpoint_test.dart @@ -6,9 +6,9 @@ import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; import '../../mocks.dart'; diff --git a/test/mocks.dart b/test/mocks.dart index 7ad22af9..638018e3 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -1,8 +1,8 @@ import 'package:mocktail/mocktail.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/endpoint/endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/firebase_service.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class MockTbContext extends Mock implements TbContext {} From 88316625156f18588d6d4087ed683406c6e1c190 Mon Sep 17 00:00:00 2001 From: Makson Lee Date: Thu, 1 Aug 2024 20:22:25 +0800 Subject: [PATCH 33/61] Remove flutter_inappwebview deprecated things (#104) --- lib/main.dart | 2 +- lib/modules/dashboard/dashboard.dart | 47 +++++++++++++--------------- lib/modules/url/url_page.dart | 9 +++--- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1436b1a4..46bc55f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,7 @@ void main() async { await setUpRootDependencies(); if (UniversalPlatform.isAndroid) { - await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true); + await InAppWebViewController.setWebContentsDebuggingEnabled(true); } try { diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index 81489de4..3c8f76b2 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -112,23 +112,19 @@ class _DashboardState extends TbContextState { late final DashboardController _dashboardController; - InAppWebViewGroupOptions options = InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( - useShouldOverrideUrlLoading: true, - mediaPlaybackRequiresUserGesture: false, - javaScriptEnabled: true, - cacheEnabled: true, - supportZoom: false, - // useOnDownloadStart: true - ), - android: AndroidInAppWebViewOptions( - useHybridComposition: true, - thirdPartyCookiesEnabled: true, - ), - ios: IOSInAppWebViewOptions( - allowsInlineMediaPlayback: true, - allowsBackForwardNavigationGestures: false, - ), + InAppWebViewSettings settings = InAppWebViewSettings( + useShouldOverrideUrlLoading: true, + mediaPlaybackRequiresUserGesture: false, + javaScriptEnabled: true, + cacheEnabled: true, + supportZoom: false, + // useOnDownloadStart: true + + useHybridComposition: true, + thirdPartyCookiesEnabled: true, + + allowsInlineMediaPlayback: true, + allowsBackForwardNavigationGestures: false, ); late WebUri _initialUrl; @@ -320,7 +316,7 @@ class _DashboardState extends TbContextState { : InAppWebView( key: webViewKey, initialUrlRequest: URLRequest(url: _initialUrl), - initialOptions: options, + initialSettings: settings, onWebViewCreated: (webViewController) { log.debug('onWebViewCreated'); webViewController.addJavaScriptHandler( @@ -418,8 +414,8 @@ class _DashboardState extends TbContextState { log.debug('shouldOverrideUrlLoading $uriString'); if (Platform.isAndroid || Platform.isIOS && - navigationAction.iosWKNavigationType == - IOSWKNavigationType.LINK_ACTIVATED) { + navigationAction.navigationType == + NavigationType.LINK_ACTIVATED) { if (uriString.startsWith(endpoint)) { var target = uriString.substring(endpoint.length); if (!target.startsWith('?accessToken')) { @@ -469,14 +465,13 @@ class _DashboardState extends TbContextState { _controller.complete(controller); } }, - androidOnPermissionRequest: - (controller, origin, resources) async { + onPermissionRequest: (controller, request) async { log.debug( - 'androidOnPermissionRequest origin: $origin, resources: $resources', + 'androidOnPermissionRequest origin: ${request.origin}, resources: ${request.resources}', ); - return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT, + return PermissionResponse( + resources: request.resources, + action: PermissionResponseAction.GRANT, ); }, ), diff --git a/lib/modules/url/url_page.dart b/lib/modules/url/url_page.dart index bcb7d6de..a215e2bc 100644 --- a/lib/modules/url/url_page.dart +++ b/lib/modules/url/url_page.dart @@ -41,11 +41,10 @@ class _UrlPageState extends TbPageState { initialUrlRequest: URLRequest( url: WebUri(widget.url.toString()), ), - androidOnPermissionRequest: - (controller, origin, resources) async { - return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT, + onPermissionRequest: (controller, request) async { + return PermissionResponse( + resources: request.resources, + action: PermissionResponseAction.GRANT, ); }, ), From eccb51284a2e6ac10c0dd8f82cc7c796771cbaf2 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 7 Aug 2024 11:14:26 +0300 Subject: [PATCH 34/61] Dashboard refactoring. Fixed an issue with loading dashboards on IOS. --- .fvmrc | 3 + .gitignore | 3 + ios/Podfile | 4 + .../usecases/switch_endpoint_usecase.dart | 2 +- .../view/switch_endpoint_noauth_view.dart | 3 +- .../widgets/endpoint_name_widget.dart | 3 +- lib/core/context/tb_context.dart | 98 +--- lib/locator.dart | 13 + lib/main.dart | 168 +----- lib/modules/alarm/alarms_list.dart | 8 +- .../alarm_types/alarm_types_query_ctrl.dart | 13 - .../presentation/bloc/alarms_events.dart | 3 + lib/modules/dashboard/dashboard.dart | 525 ------------------ lib/modules/dashboard/dashboard_page.dart | 57 -- lib/modules/dashboard/dashboard_routes.dart | 51 +- lib/modules/dashboard/dashboards_base.dart | 233 -------- lib/modules/dashboard/dashboards_grid.dart | 42 -- lib/modules/dashboard/dashboards_list.dart | 15 - .../dashboard/dashboards_list_widget.dart | 18 - lib/modules/dashboard/dashboards_page.dart | 23 - lib/modules/dashboard/di/dashboards_di.dart | 37 ++ .../domain/entites/dashboard_arguments.dart | 21 + .../dashboards_pagination_repository.dart | 18 + .../pagination/dashboards_query_ctrl.dart | 11 + .../usecases/fetch_dashboards_usecase.dart | 25 + .../dashboard/main_dashboard_page.dart | 146 ++--- .../controller/dashboard_controller.dart | 122 ++++ .../controller/dashboard_page_controller.dart | 46 ++ .../presentation/view/dashboards_page.dart | 58 ++ .../view}/fullscreen_dashboard_page.dart | 5 +- .../view/home_dashboard_page.dart | 37 ++ .../view/single_dashboard_view.dart | 125 +++++ .../widgets/dashboard_grid_card.dart | 88 +++ .../widgets/dashboard_widget.dart | 339 +++++++++++ .../widgets/dashboards_appbar.dart | 51 ++ .../presentation/widgets/dashboards_grid.dart | 69 +++ lib/modules/home/home_page.dart | 82 +-- .../communication/communication_service.dart | 19 + lib/utils/services/communication/events.dart | 18 + .../i_communication_service.dart | 7 + .../services/endpoint/endpoint_service.dart | 2 - lib/utils/services/user/i_user_service.dart | 1 + lib/utils/services/user/user_service.dart | 10 + .../pagination_grid_widget.dart | 50 ++ pubspec.yaml | 1 + 45 files changed, 1306 insertions(+), 1367 deletions(-) create mode 100644 .fvmrc delete mode 100644 lib/modules/dashboard/dashboard.dart delete mode 100644 lib/modules/dashboard/dashboard_page.dart delete mode 100644 lib/modules/dashboard/dashboards_base.dart delete mode 100644 lib/modules/dashboard/dashboards_grid.dart delete mode 100644 lib/modules/dashboard/dashboards_list.dart delete mode 100644 lib/modules/dashboard/dashboards_list_widget.dart delete mode 100644 lib/modules/dashboard/dashboards_page.dart create mode 100644 lib/modules/dashboard/di/dashboards_di.dart create mode 100644 lib/modules/dashboard/domain/entites/dashboard_arguments.dart create mode 100644 lib/modules/dashboard/domain/pagination/dashboards_pagination_repository.dart create mode 100644 lib/modules/dashboard/domain/pagination/dashboards_query_ctrl.dart create mode 100644 lib/modules/dashboard/domain/usecases/fetch_dashboards_usecase.dart create mode 100644 lib/modules/dashboard/presentation/controller/dashboard_controller.dart create mode 100644 lib/modules/dashboard/presentation/controller/dashboard_page_controller.dart create mode 100644 lib/modules/dashboard/presentation/view/dashboards_page.dart rename lib/modules/dashboard/{ => presentation/view}/fullscreen_dashboard_page.dart (95%) create mode 100644 lib/modules/dashboard/presentation/view/home_dashboard_page.dart create mode 100644 lib/modules/dashboard/presentation/view/single_dashboard_view.dart create mode 100644 lib/modules/dashboard/presentation/widgets/dashboard_grid_card.dart create mode 100644 lib/modules/dashboard/presentation/widgets/dashboard_widget.dart create mode 100644 lib/modules/dashboard/presentation/widgets/dashboards_appbar.dart create mode 100644 lib/modules/dashboard/presentation/widgets/dashboards_grid.dart create mode 100644 lib/utils/services/communication/communication_service.dart create mode 100644 lib/utils/services/communication/events.dart create mode 100644 lib/utils/services/communication/i_communication_service.dart create mode 100644 lib/utils/services/user/i_user_service.dart create mode 100644 lib/utils/services/user/user_service.dart create mode 100644 lib/utils/ui/pagination_widgets/pagination_grid_widget.dart diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..8f59eb58 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.22.2" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0fa6b675..fbeacdd1 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index 10f3c9b4..85d910fc 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -37,5 +37,9 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '5.0' # required by simple_permission + config.build_settings['ENABLE_BITCODE'] = 'NO' + end end end diff --git a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart index 30239230..0a2dc553 100644 --- a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart +++ b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart @@ -77,7 +77,7 @@ class SwitchEndpointUseCase extends UseCase { if (!isTheSameHost) { logger.debug('SwitchEndpointUseCase:deleteFB App'); - await getIt() + getIt() ..removeApp() ..removeApp(name: currentEndpoint); diff --git a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart index 808888fb..ad74168d 100644 --- a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart +++ b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart @@ -11,6 +11,7 @@ import 'package:thingsboard_app/locator.dart'; class SwitchEndpointNoAuthView extends TbPageWidget { SwitchEndpointNoAuthView({ + super.key, required this.tbContext, required this.arguments, }) : super(tbContext); @@ -109,7 +110,7 @@ class _SwitchEndpointNoAuthViewState child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.error, color: Colors.red, size: 50, diff --git a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart index e922ae9b..9c90f4e3 100644 --- a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart +++ b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; class EndpointNameWidget extends StatelessWidget { const EndpointNameWidget({required this.endpoint, super.key}); @@ -18,7 +17,7 @@ class EndpointNameWidget extends StatelessWidget { padding: const EdgeInsets.all(5), child: Center( child: Text( - WebUri(endpoint).host, + Uri.parse(endpoint).host, style: Theme.of(context) .textTheme .labelSmall diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index 81a18a45..cafca5d9 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -12,6 +12,7 @@ import 'package:thingsboard_app/core/auth/oauth2/tb_oauth2_client.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/dashboard/domain/entites/dashboard_arguments.dart'; import 'package:thingsboard_app/modules/main/main_navigation_item.dart'; import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; @@ -24,35 +25,6 @@ import 'package:universal_platform/universal_platform.dart'; enum NotificationType { info, warn, success, error } -typedef OpenDashboardCallback = void Function( - String dashboardId, { - String? dashboardTitle, - String? state, - bool? hideToolbar, -}); - -abstract class TbMainDashboardHolder { - Future navigateToDashboard( - String dashboardId, { - String? dashboardTitle, - String? state, - bool? hideToolbar, - bool animate = true, - }); - - Future openMain({bool animate}); - - Future closeMain({bool animate}); - - Future openDashboard({bool animate}); - - Future closeDashboard({bool animate}); - - bool isDashboardOpen(); - - Future dashboardGoBack(); -} - class TbContext implements PopEntry { static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); bool _initialized = false; @@ -69,7 +41,6 @@ class TbContext implements PopEntry { late final AndroidDeviceInfo? _androidInfo; late final IosDeviceInfo? _iosInfo; late final String packageName; - TbMainDashboardHolder? _mainDashboardHolder; bool _closeMainFirst = false; StreamSubscription? _appLinkStreamSubscription; late bool _handleRootState; @@ -187,10 +158,6 @@ class TbContext implements PopEntry { _initialized = true; } - void setMainDashboardHolder(TbMainDashboardHolder holder) { - _mainDashboardHolder = holder; - } - Future onFatalError(e) async { var message = e is ThingsboardError ? (e.message ?? 'Unknown error.') @@ -522,11 +489,7 @@ class TbContext implements PopEntry { }) async { if (currentState != null) { hideNotification(); - bool isOpenedDashboard = - _mainDashboardHolder?.isDashboardOpen() == true && closeDashboard; - if (isOpenedDashboard) { - _mainDashboardHolder?.openMain(); - } + if (currentState is TbMainState) { var mainState = currentState as TbMainState; if (mainState.canNavigate(path) && !replace) { @@ -538,7 +501,7 @@ class TbContext implements PopEntry { replace = true; clearStack = true; } - if (transition != TransitionType.nativeModal && isOpenedDashboard) { + if (transition != TransitionType.nativeModal) { transition = TransitionType.none; } else if (transition == null) { if (replace) { @@ -547,7 +510,7 @@ class TbContext implements PopEntry { transition = TransitionType.native; } } - _closeMainFirst = isOpenedDashboard; + return await router.navigateTo( currentState!.context, path, @@ -567,12 +530,18 @@ class TbContext implements PopEntry { bool? hideToolbar, bool animate = true, }) async { - await _mainDashboardHolder?.navigateToDashboard( - dashboardId, - dashboardTitle: dashboardTitle, - state: state, - hideToolbar: hideToolbar, - animate: animate, + router.navigateTo( + currentState!.context, + '/dashboard', + routeSettings: RouteSettings( + arguments: DashboardArgumentsEntity( + dashboardId, + title: dashboardTitle, + state: state, + hideToolbar: hideToolbar, + animate: animate, + ), + ), ); } @@ -588,7 +557,6 @@ class TbContext implements PopEntry { } void pop([T? result, BuildContext? context]) async { - await closeMainIfNeeded(); var targetContext = context ?? currentState?.context; if (targetContext != null) { router.pop(targetContext, result); @@ -603,40 +571,18 @@ class TbContext implements PopEntry { } } - Future willPop() async { - if (await closeMainIfNeeded()) { - return true; - } - if (_mainDashboardHolder != null) { - return await _mainDashboardHolder!.dashboardGoBack(); - } - return true; - } - void onPopInvokedImpl(bool didPop) async { if (didPop) { return; } - if (await willPop()) { - if (await currentState!.willPop()) { - var navigator = Navigator.of(currentState!.context); - if (navigator.canPop()) { - navigator.pop(); - } else { - SystemNavigator.pop(); - } - } - } - } - - Future closeMainIfNeeded() async { - if (currentState != null) { - if (currentState!.closeMainFirst && _mainDashboardHolder != null) { - await _mainDashboardHolder!.closeMain(); - return true; + if (await currentState!.willPop()) { + var navigator = Navigator.of(currentState!.context); + if (navigator.canPop()) { + navigator.pop(); + } else { + SystemNavigator.pop(); } } - return false; } Future alert({ diff --git a/lib/locator.dart b/lib/locator.dart index 47a7071a..92d39090 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,12 +1,17 @@ +import 'package:event_bus/event_bus.dart'; import 'package:get_it/get_it.dart'; import 'package:thingsboard_app/config/routes/router.dart'; import 'package:thingsboard_app/core/logger/tb_logger.dart'; import 'package:thingsboard_app/utils/services/_tb_secure_storage.dart'; +import 'package:thingsboard_app/utils/services/communication/communication_service.dart'; +import 'package:thingsboard_app/utils/services/communication/i_communication_service.dart'; import 'package:thingsboard_app/utils/services/endpoint/endpoint_service.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; import 'package:thingsboard_app/utils/services/local_database/local_database_service.dart'; +import 'package:thingsboard_app/utils/services/user/i_user_service.dart'; +import 'package:thingsboard_app/utils/services/user/user_service.dart'; import 'utils/services/firebase/firebase_service.dart'; @@ -39,5 +44,13 @@ Future setUpRootDependencies() async { logger: getIt(), endpointService: getIt(), ), + ) + ..registerLazySingleton( + () => CommunicationService( + EventBus(), + ), + ) + ..registerSingleton( + UserService(), ); } diff --git a/lib/main.dart b/lib/main.dart index 46bc55f3..3f88a0f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,13 +8,10 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:thingsboard_app/config/routes/router.dart'; import 'package:thingsboard_app/constants/database_keys.dart'; -import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/firebase_options.dart'; import 'package:thingsboard_app/locator.dart'; -import 'package:thingsboard_app/modules/dashboard/main_dashboard_page.dart'; import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; -import 'package:thingsboard_app/widgets/two_page_view.dart'; import 'package:uni_links/uni_links.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -54,119 +51,9 @@ void main() async { runApp(const ThingsboardApp()); } -class ThingsboardApp extends StatefulWidget { +class ThingsboardApp extends StatelessWidget { const ThingsboardApp({Key? key}) : super(key: key); - @override - ThingsboardAppState createState() => ThingsboardAppState(); -} - -class ThingsboardAppState extends State - with TickerProviderStateMixin - implements TbMainDashboardHolder { - final _mainPageViewController = TwoPageViewController(); - final _mainDashboardPageController = MainDashboardPageController(); - - final mainAppKey = GlobalKey(); - final dashboardKey = GlobalKey(); - - @override - void initState() { - super.initState(); - getIt().tbContext.setMainDashboardHolder(this); - } - - @override - Future navigateToDashboard( - String dashboardId, { - String? dashboardTitle, - String? state, - bool? hideToolbar, - bool animate = true, - }) async { - await _mainDashboardPageController.openDashboard( - dashboardId, - dashboardTitle: dashboardTitle, - state: state, - hideToolbar: hideToolbar, - ); - - _openDashboard(animate: animate); - } - - @override - Future dashboardGoBack() async { - if (_mainPageViewController.index == 1) { - final canGoBack = await _mainDashboardPageController.dashboardGoBack(); - if (canGoBack) { - closeDashboard(); - } - - return false; - } - - return true; - } - - @override - Future openMain({bool animate = true}) async { - return _openMain(animate: animate); - } - - @override - Future closeMain({bool animate = true}) async { - return _closeMain(animate: animate); - } - - @override - Future openDashboard({bool animate = true}) async { - return _openDashboard(animate: animate); - } - - @override - Future closeDashboard({bool animate = true}) { - return _closeDashboard(animate: animate); - } - - @override - bool isDashboardOpen() { - return _mainPageViewController.index == 1; - } - - Future _openMain({bool animate = true}) async { - final res = await _mainPageViewController.open(0, animate: animate); - if (res) { - await _mainDashboardPageController.deactivateDashboard(); - } - - return res; - } - - Future _closeMain({bool animate = true}) async { - if (!isDashboardOpen()) { - await _mainDashboardPageController.activateDashboard(); - } - - return _mainPageViewController.close(0, animate: animate); - } - - Future _openDashboard({bool animate = true}) async { - if (!isDashboardOpen()) { - _mainDashboardPageController.activateDashboard(); - } - - return _mainPageViewController.open(1, animate: animate); - } - - Future _closeDashboard({bool animate = true}) async { - final res = await _mainPageViewController.close(1, animate: animate); - if (res) { - _mainDashboardPageController.deactivateDashboard(); - } - - return res; - } - @override Widget build(BuildContext context) { SystemChrome.setSystemUIOverlayStyle( @@ -178,7 +65,8 @@ class ThingsboardAppState extends State ); return MaterialApp( - debugShowCheckedModeBanner: false, + scaffoldMessengerKey: + getIt().tbContext.messengerKey, localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, @@ -188,50 +76,12 @@ class ThingsboardAppState extends State supportedLocales: S.supportedLocales, onGenerateTitle: (BuildContext context) => S.of(context).appTitle, themeMode: ThemeMode.light, - home: TwoPageView( - controller: _mainPageViewController, - first: MaterialApp( - debugShowCheckedModeBanner: false, - key: mainAppKey, - scaffoldMessengerKey: - getIt().tbContext.messengerKey, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.supportedLocales, - onGenerateTitle: (BuildContext context) => S.of(context).appTitle, - theme: tbTheme, - themeMode: ThemeMode.light, - darkTheme: tbDarkTheme, - onGenerateRoute: getIt().router.generator, - navigatorObservers: [ - getIt().tbContext.routeObserver, - ], - ), - second: MaterialApp( - debugShowCheckedModeBanner: false, - key: dashboardKey, - // scaffoldMessengerKey: appRouter.tbContext.messengerKey, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.supportedLocales, - onGenerateTitle: (BuildContext context) => S.of(context).appTitle, - theme: tbTheme, - themeMode: ThemeMode.light, - darkTheme: tbDarkTheme, - home: MainDashboardPage( - getIt().tbContext, - controller: _mainDashboardPageController, - ), - ), - ), + theme: tbTheme, + darkTheme: tbDarkTheme, + onGenerateRoute: getIt().router.generator, + navigatorObservers: [ + getIt().tbContext.routeObserver, + ], ); } } diff --git a/lib/modules/alarm/alarms_list.dart b/lib/modules/alarm/alarms_list.dart index 634639a6..7a952f57 100644 --- a/lib/modules/alarm/alarms_list.dart +++ b/lib/modules/alarm/alarms_list.dart @@ -22,8 +22,9 @@ class AlarmsList extends StatelessWidget { @override Widget build(BuildContext context) { return RefreshIndicator( - onRefresh: () async => - getIt().add(const AlarmsRefreshPageEvent()), + onRefresh: () async => getIt().add( + const AlarmsRefreshPageEvent(), + ), child: PaginationListWidget( pagingController: getIt().paginationRepository.pagingController, @@ -66,6 +67,9 @@ class AlarmsList extends StatelessWidget { FirstPageExceptionIndicator( title: 'No alarms found', message: S.of(context).listIsEmptyText, + onTryAgain: () => getIt().add( + const AlarmsRefreshPageEvent(), + ), ), ), ), diff --git a/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart b/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart index 6ae87cce..28f73ffe 100644 --- a/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart +++ b/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart @@ -18,21 +18,8 @@ class AlarmTypesQueryCtrl extends PageKeyController { @override PageLink nextPageKey(PageLink pageKey) { return pageKey.nextPageLink(); - - // return PushNotificationQuery( - // pageKey.pageLink.nextPageLink(), - // unreadOnly: value.pageKey.unreadOnly, - // ); } - // void onSearchText(String searchText) { - // final query = value.pageKey; - // query.page = 0; - // query.textSearch = searchText; - // - // notifyListeners(); - // } - void refresh() { notifyListeners(); } diff --git a/lib/modules/alarm/presentation/bloc/alarms_events.dart b/lib/modules/alarm/presentation/bloc/alarms_events.dart index 4bf04700..817a51ae 100644 --- a/lib/modules/alarm/presentation/bloc/alarms_events.dart +++ b/lib/modules/alarm/presentation/bloc/alarms_events.dart @@ -32,4 +32,7 @@ final class AlarmSearchTextChanged extends AlarmEvent { final class AlarmsRefreshPageEvent extends AlarmEvent { const AlarmsRefreshPageEvent(); + + @override + List get props => [double.nan]; } diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart deleted file mode 100644 index 3c8f76b2..00000000 --- a/lib/modules/dashboard/dashboard.dart +++ /dev/null @@ -1,525 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_app/locator.dart'; -import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; -import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; -import 'package:thingsboard_app/widgets/two_value_listenable_builder.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -class DashboardController { - final ValueNotifier canGoBack = ValueNotifier(false); - final ValueNotifier hasRightLayout = ValueNotifier(false); - final ValueNotifier rightLayoutOpened = ValueNotifier(false); - - final _DashboardState dashboardState; - - DashboardController(this.dashboardState); - - Future openDashboard( - String dashboardId, { - String? state, - bool? hideToolbar, - bool fullscreen = false, - }) async { - return await dashboardState._openDashboard( - dashboardId, - state: state, - hideToolbar: hideToolbar, - fullscreen: fullscreen, - ); - } - - Future goBack() async { - return dashboardState._goBack(); - } - - onHistoryUpdated(Future canGoBackFuture) async { - canGoBack.value = await canGoBackFuture; - } - - onHasRightLayout(bool hasRightLayout) { - this.hasRightLayout.value = hasRightLayout; - } - - onRightLayoutOpened(bool rightLayoutOpened) { - this.rightLayoutOpened.value = rightLayoutOpened; - } - - Future toggleRightLayout() async { - await dashboardState._toggleRightLayout(); - } - - Future activateDashboard() async { - await dashboardState._activateDashboard(); - } - - Future deactivateDashboard() async { - await dashboardState._deactivateDashboard(); - } - - dispose() { - canGoBack.dispose(); - hasRightLayout.dispose(); - rightLayoutOpened.dispose(); - } -} - -typedef DashboardTitleCallback = void Function(String title); - -typedef DashboardControllerCallback = void Function( - DashboardController controller, -); - -class Dashboard extends TbContextWidget { - final bool? _home; - final bool _activeByDefault; - final DashboardTitleCallback? _titleCallback; - final DashboardControllerCallback? _controllerCallback; - - Dashboard( - TbContext tbContext, { - Key? key, - bool? home, - bool activeByDefault = true, - DashboardTitleCallback? titleCallback, - DashboardControllerCallback? controllerCallback, - }) : _home = home, - _activeByDefault = activeByDefault, - _titleCallback = titleCallback, - _controllerCallback = controllerCallback, - super(tbContext, key: key); - - @override - State createState() => _DashboardState(); -} - -class _DashboardState extends TbContextState { - final _controller = Completer(); - - bool webViewLoading = true; - final dashboardLoading = ValueNotifier(true); - final dashboardActive = ValueNotifier(true); - final readyState = ValueNotifier(false); - - final webViewKey = GlobalKey(); - - late final DashboardController _dashboardController; - - InAppWebViewSettings settings = InAppWebViewSettings( - useShouldOverrideUrlLoading: true, - mediaPlaybackRequiresUserGesture: false, - javaScriptEnabled: true, - cacheEnabled: true, - supportZoom: false, - // useOnDownloadStart: true - - useHybridComposition: true, - thirdPartyCookiesEnabled: true, - - allowsInlineMediaPlayback: true, - allowsBackForwardNavigationGestures: false, - ); - - late WebUri _initialUrl; - - @override - void initState() { - super.initState(); - dashboardActive.value = widget._activeByDefault; - _dashboardController = DashboardController(this); - if (widget._controllerCallback != null) { - widget._controllerCallback!(_dashboardController); - } - tbContext.isAuthenticatedListenable.addListener(_onAuthenticated); - if (tbContext.isAuthenticated) { - _onAuthenticated(); - } - } - - void _onAuthenticated() async { - if (tbContext.isAuthenticated) { - if (!readyState.value) { - _initialUrl = WebUri( - '${await getIt().getEndpoint()}' - '?accessToken=${tbClient.getJwtToken()!}' - '&refreshToken=${tbClient.getRefreshToken()!}', - ); - - readyState.value = true; - } else { - var windowMessage = { - 'type': 'reloadUserMessage', - 'data': { - 'accessToken': tbClient.getJwtToken()!, - 'refreshToken': tbClient.getRefreshToken()!, - }, - }; - if (!UniversalPlatform.isWeb) { - var controller = await _controller.future; - await controller.postWebMessage( - message: WebMessage(data: jsonEncode(windowMessage)), - targetOrigin: WebUri('*'), - ); - } - } - } - } - - Future _goBack() async { - if (!UniversalPlatform.isWeb) { - if (_dashboardController.rightLayoutOpened.value) { - await _toggleRightLayout(); - return false; - } - var controller = await _controller.future; - if (await controller.canGoBack()) { - await controller.goBack(); - return false; - } - } - return true; - } - - @override - void dispose() { - tbContext.isAuthenticatedListenable.removeListener(_onAuthenticated); - readyState.dispose(); - dashboardLoading.dispose(); - _dashboardController.dispose(); - super.dispose(); - } - - Future _activateDashboard() async { - if (!dashboardActive.value) { - dashboardActive.value = true; - } - } - - Future _deactivateDashboard() async { - if (dashboardActive.value) { - dashboardActive.value = false; - } - } - - Future _openDashboard( - String dashboardId, { - String? state, - bool? hideToolbar, - bool fullscreen = false, - }) async { - dashboardLoading.value = true; - InAppWebViewController? controller; - if (!UniversalPlatform.isWeb) { - controller = await _controller.future; - } - var windowMessage = { - 'type': 'openDashboardMessage', - 'data': {'dashboardId': dashboardId}, - }; - if (state != null) { - windowMessage['data']['state'] = state; - } - if (widget._home == true) { - windowMessage['data']['embedded'] = true; - } - if (hideToolbar == true) { - windowMessage['data']['hideToolbar'] = true; - } - var webMessage = WebMessage(data: jsonEncode(windowMessage)); - if (!UniversalPlatform.isWeb) { - await controller! - .postWebMessage(message: webMessage, targetOrigin: WebUri('*')); - } - } - - Future _toggleRightLayout() async { - var controller = await _controller.future; - var windowMessage = {'type': 'toggleDashboardLayout'}; - var webMessage = WebMessage(data: jsonEncode(windowMessage)); - await controller.postWebMessage( - message: webMessage, - targetOrigin: WebUri('*'), - ); - } - - Future tryLocalNavigation(String? path) async { - log.debug('path: $path'); - if (path != null && path != '/home') { - final parts = path.split('/'); - if ([ - 'profile', - 'devices', - 'assets', - 'dashboards', - 'dashboard', - 'customers', - 'auditLogs', - 'deviceGroups', - 'assetGroups', - 'customerGroups', - 'dashboardGroups', - 'alarms', - ].contains(parts[1])) { - var firstPart = parts[1]; - if (firstPart.endsWith('Groups')) { - firstPart = firstPart.replaceFirst('Groups', 's'); - } - - if ((firstPart == 'dashboard' || firstPart == 'dashboards') && - parts.length > 1) { - final dashboardId = parts[1]; - await navigateToDashboard(dashboardId); - } else if (firstPart != 'dashboard') { - var targetPath = '/$firstPart'; - if (firstPart == 'devices' && widget._home != true) { - targetPath = '/devicesPage'; - } - - await navigateTo(targetPath); - } - } else { - throw UnimplementedError('The path $path is currently not supported.'); - } - } - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - if (widget._home == true && !tbContext.isHomePage()) { - return true; - } - if (readyState.value) { - return await _goBack(); - } else { - return true; - } - }, - child: ValueListenableBuilder( - valueListenable: readyState, - builder: (BuildContext context, bool ready, child) { - if (!ready) { - return const SizedBox.shrink(); - } else { - return Stack( - children: [ - UniversalPlatform.isWeb - ? const Center(child: Text('Not implemented!')) - : InAppWebView( - key: webViewKey, - initialUrlRequest: URLRequest(url: _initialUrl), - initialSettings: settings, - onWebViewCreated: (webViewController) { - log.debug('onWebViewCreated'); - webViewController.addJavaScriptHandler( - handlerName: 'tbMobileDashboardLoadedHandler', - callback: (args) async { - bool hasRightLayout = args[0]; - bool rightLayoutOpened = args[1]; - log.debug( - 'Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened', - ); - _dashboardController - .onHasRightLayout(hasRightLayout); - _dashboardController - .onRightLayoutOpened(rightLayoutOpened); - dashboardLoading.value = false; - }, - ); - webViewController.addJavaScriptHandler( - handlerName: 'tbMobileDashboardLayoutHandler', - callback: (args) async { - bool rightLayoutOpened = args[0]; - log.debug( - 'Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened', - ); - _dashboardController - .onRightLayoutOpened(rightLayoutOpened); - }, - ); - webViewController.addJavaScriptHandler( - handlerName: 'tbMobileDashboardStateNameHandler', - callback: (args) async { - log.debug( - 'Invoked tbMobileDashboardStateNameHandler: $args', - ); - if (args.isNotEmpty && args[0] is String) { - if (widget._titleCallback != null) { - widget._titleCallback!(args[0]); - } - } - }, - ); - webViewController.addJavaScriptHandler( - handlerName: 'tbMobileNavigationHandler', - callback: (args) async { - log.debug( - 'Invoked tbMobileNavigationHandler: $args', - ); - if (args.isNotEmpty) { - late String path; - - if (args.first.contains('.')) { - path = '/${args.first.split('.').last}'; - } else { - path = '/${args.first}'; - } - - Map? params; - if (args.length > 1) { - params = args[1]; - } - - log.debug('path: $path'); - log.debug('params: $params'); - try { - await tryLocalNavigation(path); - } on UnimplementedError catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - _buildWarnSnackBar(e.message!), - ); - } - } - } - }, - ); - webViewController.addJavaScriptHandler( - handlerName: 'tbMobileHandler', - callback: (args) async { - log.debug('Invoked tbMobileHandler: $args'); - return await widgetActionHandler - .handleWidgetMobileAction( - args, - webViewController, - ); - }, - ); - }, - shouldOverrideUrlLoading: - (controller, navigationAction) async { - final uri = navigationAction.request.url!; - final uriString = uri.toString(); - final endpoint = - await getIt().getEndpoint(); - - log.debug('shouldOverrideUrlLoading $uriString'); - if (Platform.isAndroid || - Platform.isIOS && - navigationAction.navigationType == - NavigationType.LINK_ACTIVATED) { - if (uriString.startsWith(endpoint)) { - var target = uriString.substring(endpoint.length); - if (!target.startsWith('?accessToken')) { - if (target.startsWith('/')) { - target = target.substring(1); - } - try { - await tryLocalNavigation(target); - } on UnimplementedError catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - _buildWarnSnackBar(e.message!), - ); - } - } - return NavigationActionPolicy.CANCEL; - } - } else if (await canLaunchUrlString(uriString)) { - await launchUrlString( - uriString, - ); - return NavigationActionPolicy.CANCEL; - } - } - return Platform.isIOS - ? NavigationActionPolicy.ALLOW - : NavigationActionPolicy.CANCEL; - }, - onUpdateVisitedHistory: - (controller, url, androidIsReload) async { - log.debug('onUpdateVisitedHistory: $url'); - _dashboardController - .onHistoryUpdated(controller.canGoBack()); - }, - onConsoleMessage: (controller, consoleMessage) { - log.debug( - '[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}', - ); - }, - onLoadStart: (controller, url) async { - log.debug('onLoadStart: $url'); - }, - onLoadStop: (controller, url) async { - log.debug('onLoadStop: $url'); - if (webViewLoading) { - webViewLoading = false; - _controller.complete(controller); - } - }, - onPermissionRequest: (controller, request) async { - log.debug( - 'androidOnPermissionRequest origin: ${request.origin}, resources: ${request.resources}', - ); - return PermissionResponse( - resources: request.resources, - action: PermissionResponseAction.GRANT, - ); - }, - ), - if (!UniversalPlatform.isWeb) - TwoValueListenableBuilder( - firstValueListenable: dashboardLoading, - secondValueListenable: dashboardActive, - builder: (context, loading, active, child) { - if (!loading && active) { - return const SizedBox.shrink(); - } else { - var data = MediaQuery.of(context); - 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: const TbProgressIndicator(size: 50.0), - ); - } - }, - ), - ], - ); - } - }, - ), - ); - } - - SnackBar _buildWarnSnackBar(String message) { - return SnackBar( - duration: const Duration(seconds: 10), - backgroundColor: const Color(0xFFdc6d1b), - content: Text( - message, - style: const TextStyle(color: Colors.white), - ), - action: SnackBarAction( - label: 'Close', - textColor: Colors.white, - onPressed: () { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - }, - ), - ); - } -} diff --git a/lib/modules/dashboard/dashboard_page.dart b/lib/modules/dashboard/dashboard_page.dart deleted file mode 100644 index 470f29c9..00000000 --- a/lib/modules/dashboard/dashboard_page.dart +++ /dev/null @@ -1,57 +0,0 @@ -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/widgets/tb_app_bar.dart'; - -class DashboardPage extends TbPageWidget { - final String? _dashboardTitle; - - // final String? _dashboardId; - // final String? _state; - // final bool? _fullscreen; - - DashboardPage( - TbContext tbContext, { - String? dashboardId, - bool? fullscreen, - String? dashboardTitle, - String? state, - super.key, - }) : _dashboardTitle = dashboardTitle, - super(tbContext); - - @override - State createState() => _DashboardPageState(); -} - -class _DashboardPageState extends TbPageState { - late ValueNotifier dashboardTitleValue; - - @override - void initState() { - super.initState(); - dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard'); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: TbAppBar( - tbContext, - showLoadingIndicator: false, - elevation: 0, - title: ValueListenableBuilder( - valueListenable: dashboardTitleValue, - builder: (context, title, widget) { - return FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text(title), - ); - }, - ), - ), - body: const Text('Deprecated'), - ); - } -} diff --git a/lib/modules/dashboard/dashboard_routes.dart b/lib/modules/dashboard/dashboard_routes.dart index 90215284..a67e2a13 100644 --- a/lib/modules/dashboard/dashboard_routes.dart +++ b/lib/modules/dashboard/dashboard_routes.dart @@ -1,36 +1,34 @@ 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/dashboard/dashboards_page.dart'; -import 'package:thingsboard_app/modules/dashboard/fullscreen_dashboard_page.dart'; - -import 'dashboard_page.dart'; +import 'package:thingsboard_app/modules/dashboard/domain/entites/dashboard_arguments.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/view/dashboards_page.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/view/single_dashboard_view.dart'; class DashboardRoutes extends TbRoutes { late final dashboardsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { + handlerFunc: (context, params) { return DashboardsPage(tbContext); }, ); - late final dashboardDetailsHandler = Handler( - handlerFunc: (BuildContext? context, Map> params) { - var fullscreen = params['fullscreen']?.first == 'true'; - var dashboardTitle = params['title']?.first; - var state = params['state']?.first; - return DashboardPage( + late final dashboardHandler = Handler( + handlerFunc: (context, params) { + final args = context?.settings?.arguments as DashboardArgumentsEntity; + + return SingleDashboardView( tbContext, - dashboardId: params['id']![0], - fullscreen: fullscreen, - dashboardTitle: dashboardTitle, - state: state, + id: args.id, + title: args.title, + state: args.state, + hideToolbar: args.hideToolbar, ); }, ); late final fullscreenDashboardHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { + handlerFunc: (context, params) { return FullscreenDashboardPage(tbContext, params['id']![0]); }, ); @@ -39,11 +37,18 @@ class DashboardRoutes extends TbRoutes { @override void doRegisterRoutes(router) { - router.define('/dashboards', handler: dashboardsHandler); - router.define('/dashboard/:id', handler: dashboardDetailsHandler); - router.define( - '/fullscreenDashboard/:id', - handler: fullscreenDashboardHandler, - ); + router + ..define( + '/dashboards', + handler: dashboardsHandler, + ) + ..define( + '/dashboard', + handler: dashboardHandler, + ) + ..define( + '/fullscreenDashboard/:id', + handler: fullscreenDashboardHandler, + ); } } diff --git a/lib/modules/dashboard/dashboards_base.dart b/lib/modules/dashboard/dashboards_base.dart deleted file mode 100644 index 16d3e3f5..00000000 --- a/lib/modules/dashboard/dashboards_base.dart +++ /dev/null @@ -1,233 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.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_base.dart'; -import 'package:thingsboard_app/thingsboard_client.dart'; -import 'package:thingsboard_app/utils/utils.dart'; - -mixin DashboardsBase on EntitiesBase { - @override - String get title => 'Dashboards'; - - @override - String get noItemsFoundText => 'No dashboards found'; - - @override - Future> fetchEntities(PageLink pageLink) { - if (tbClient.isTenantAdmin()) { - return tbClient - .getDashboardService() - .getTenantDashboards(pageLink, mobile: true); - } else { - return tbClient.getDashboardService().getCustomerDashboards( - tbClient.getAuthUser()!.customerId!, - pageLink, - mobile: true, - ); - } - } - - @override - void onEntityTap(DashboardInfo dashboard) { - navigateToDashboard(dashboard.id!.id!, dashboardTitle: dashboard.title); - // navigateTo('/fullscreenDashboard/${dashboard.id!.id}?title=${dashboard.title}'); - // navigateTo('/dashboard/${dashboard.id!.id}?title=${dashboard.title}'); - } - - @override - Widget buildEntityListCard(BuildContext context, DashboardInfo dashboard) { - return _buildEntityListCard(context, dashboard, false); - } - - @override - Widget buildEntityListWidgetCard( - BuildContext context, - DashboardInfo dashboard, - ) { - return _buildEntityListCard(context, dashboard, true); - } - - @override - EntityCardSettings entityGridCardSettings(DashboardInfo dashboard) => - EntityCardSettings(dropShadow: true); //dashboard.image != null); - - @override - Widget buildEntityGridCard(BuildContext context, DashboardInfo dashboard) { - return DashboardGridCard(tbContext, dashboard: dashboard); - } - - Widget _buildEntityListCard( - BuildContext context, - DashboardInfo dashboard, - bool listWidgetCard, - ) { - return Row( - mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max, - children: [ - Flexible( - fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, - child: Container( - padding: EdgeInsets.symmetric( - vertical: listWidgetCard ? 9 : 10, - horizontal: 16, - ), - child: Row( - mainAxisSize: - listWidgetCard ? MainAxisSize.min : MainAxisSize.max, - children: [ - Flexible( - fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text( - dashboard.title, - style: const TextStyle( - color: Color(0xFF282828), - fontSize: 14, - fontWeight: FontWeight.w500, - height: 1.7, - ), - ), - ), - Text( - _dashboardDetailsText(dashboard), - style: const TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33, - ), - ), - ], - ), - ), - (!listWidgetCard - ? Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - entityDateFormat.format( - DateTime.fromMillisecondsSinceEpoch( - dashboard.createdTime!, - ), - ), - style: const TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33, - ), - ), - ], - ) - : Container()), - ], - ), - ), - ), - ], - ); - } - - String _dashboardDetailsText(DashboardInfo dashboard) { - if (tbClient.isTenantAdmin()) { - if (_isPublicDashboard(dashboard)) { - return 'Public'; - } else { - return dashboard.assignedCustomers.map((e) => e.title).join(', '); - } - } - return ''; - } - - bool _isPublicDashboard(DashboardInfo dashboard) { - return dashboard.assignedCustomers.any((element) => element.isPublic); - } -} - -class DashboardGridCard extends TbContextWidget { - final DashboardInfo dashboard; - - DashboardGridCard(TbContext tbContext, {required this.dashboard, super.key}) - : super(tbContext); - - @override - State createState() => _DashboardGridCardState(); -} - -class _DashboardGridCardState extends TbContextState { - _DashboardGridCardState() : super(); - - @override - void didUpdateWidget(DashboardGridCard oldWidget) { - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - var hasImage = widget.dashboard.image != null; - Widget image; - if (hasImage) { - image = - Utils.imageFromTbImage(context, tbClient, widget.dashboard.image!); - } else { - image = SvgPicture.asset( - ThingsboardImage.dashboardPlaceholder, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, - BlendMode.overlay, - ), - semanticsLabel: 'Dashboard', - ); - } - - return ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Column( - children: [ - Expanded( - child: Stack( - children: [ - SizedBox.expand( - child: FittedBox( - clipBehavior: Clip.hardEdge, - fit: BoxFit.cover, - child: image, - ), - ), - ], - ), - ), - const Divider(height: 1), - SizedBox( - height: 44, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Center( - child: AutoSizeText( - widget.dashboard.title, - textAlign: TextAlign.center, - maxLines: 1, - minFontSize: 12, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14, - ), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/modules/dashboard/dashboards_grid.dart b/lib/modules/dashboard/dashboards_grid.dart deleted file mode 100644 index fcf081a4..00000000 --- a/lib/modules/dashboard/dashboards_grid.dart +++ /dev/null @@ -1,42 +0,0 @@ -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/core/entity/entities_base.dart'; -import 'package:thingsboard_app/core/entity/entities_grid.dart'; -import 'package:thingsboard_app/thingsboard_client.dart'; - -import 'dashboards_base.dart'; - -class DashboardsGridWidget extends TbContextWidget { - DashboardsGridWidget(TbContext tbContext, {super.key}) : super(tbContext); - - @override - State createState() => _DashboardsGridWidgetState(); -} - -class _DashboardsGridWidgetState extends TbContextState { - final PageLinkController _pageLinkController = PageLinkController(); - - @override - Widget build(BuildContext context) { - return DashboardsGrid( - tbContext, - _pageLinkController, - ); - } - - @override - void dispose() { - _pageLinkController.dispose(); - super.dispose(); - } -} - -class DashboardsGrid extends BaseEntitiesWidget - with DashboardsBase, EntitiesGridStateBase { - DashboardsGrid( - TbContext tbContext, - PageKeyController pageKeyController, { - super.key, - }) : super(tbContext, pageKeyController); -} diff --git a/lib/modules/dashboard/dashboards_list.dart b/lib/modules/dashboard/dashboards_list.dart deleted file mode 100644 index 04c0dc16..00000000 --- a/lib/modules/dashboard/dashboards_list.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:thingsboard_app/core/entity/entities_base.dart'; -import 'package:thingsboard_app/core/entity/entities_list.dart'; -import 'package:thingsboard_app/thingsboard_client.dart'; - -import 'dashboards_base.dart'; - -class DashboardsList extends BaseEntitiesWidget - with DashboardsBase, EntitiesListStateBase { - DashboardsList( - TbContext tbContext, - PageKeyController pageKeyController, { - super.key, - }) : super(tbContext, pageKeyController); -} diff --git a/lib/modules/dashboard/dashboards_list_widget.dart b/lib/modules/dashboard/dashboards_list_widget.dart deleted file mode 100644 index 8ad0bf26..00000000 --- a/lib/modules/dashboard/dashboards_list_widget.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:thingsboard_app/core/context/tb_context.dart'; -import 'package:thingsboard_app/core/entity/entities_list_widget.dart'; -import 'package:thingsboard_app/modules/dashboard/dashboards_base.dart'; -import 'package:thingsboard_app/thingsboard_client.dart'; - -class DashboardsListWidget extends EntitiesListPageLinkWidget - with DashboardsBase { - DashboardsListWidget( - TbContext tbContext, { - super.key, - EntitiesListWidgetController? controller, - }) : super(tbContext, controller: controller); - - @override - void onViewAll() { - navigateTo('/dashboards'); - } -} diff --git a/lib/modules/dashboard/dashboards_page.dart b/lib/modules/dashboard/dashboards_page.dart deleted file mode 100644 index 867b3db4..00000000 --- a/lib/modules/dashboard/dashboards_page.dart +++ /dev/null @@ -1,23 +0,0 @@ -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/widgets/tb_app_bar.dart'; - -import 'dashboards_grid.dart'; - -class DashboardsPage extends TbPageWidget { - DashboardsPage(TbContext tbContext, {super.key}) : super(tbContext); - - @override - State createState() => _DashboardsPageState(); -} - -class _DashboardsPageState extends TbPageState { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: TbAppBar(tbContext, title: const Text('Dashboards')), - body: DashboardsGridWidget(tbContext), - ); - } -} diff --git a/lib/modules/dashboard/di/dashboards_di.dart b/lib/modules/dashboard/di/dashboards_di.dart new file mode 100644 index 00000000..11ee343a --- /dev/null +++ b/lib/modules/dashboard/di/dashboards_di.dart @@ -0,0 +1,37 @@ +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/dashboard/domain/pagination/dashboards_pagination_repository.dart'; +import 'package:thingsboard_app/modules/dashboard/domain/pagination/dashboards_query_ctrl.dart'; +import 'package:thingsboard_app/modules/dashboard/domain/usecases/fetch_dashboards_usecase.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +abstract class DashboardsDi { + static void init( + String key, { + required ThingsboardClient tbClient, + }) { + getIt.pushNewScope( + scopeName: key, + init: (locator) { + locator.registerLazySingleton( + () => DashboardsQueryCtrl(), + ); + + locator.registerFactory( + () => FetchDashboardsUseCase(tbClient), + ); + + locator.registerLazySingleton( + () => DashboardsPaginationRepository( + queryController: locator(), + onFetchData: locator(), + ), + ); + }, + ); + } + + static void dispose(String scopeName) { + getIt().dispose(); + getIt.dropScope(scopeName); + } +} diff --git a/lib/modules/dashboard/domain/entites/dashboard_arguments.dart b/lib/modules/dashboard/domain/entites/dashboard_arguments.dart new file mode 100644 index 00000000..be2a3c54 --- /dev/null +++ b/lib/modules/dashboard/domain/entites/dashboard_arguments.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +/// This class represents the arguments a user can pass to open a dashboard +class DashboardArgumentsEntity extends Equatable { + const DashboardArgumentsEntity( + this.id, { + this.title, + this.state, + this.hideToolbar, + this.animate = true, + }); + + final String id; + final String? title; + final String? state; + final bool? hideToolbar; + final bool animate; + + @override + List get props => [id, title, state, hideToolbar, animate]; +} diff --git a/lib/modules/dashboard/domain/pagination/dashboards_pagination_repository.dart b/lib/modules/dashboard/domain/pagination/dashboards_pagination_repository.dart new file mode 100644 index 00000000..b5813284 --- /dev/null +++ b/lib/modules/dashboard/domain/pagination/dashboards_pagination_repository.dart @@ -0,0 +1,18 @@ +import 'package:thingsboard_app/modules/dashboard/domain/pagination/dashboards_query_ctrl.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/services/pagination_repository.dart'; + +final class DashboardsPaginationRepository + extends PaginationRepository { + DashboardsPaginationRepository({ + required DashboardsQueryCtrl queryController, + required this.onFetchData, + }) : super(pageKeyController: queryController); + + final Future> Function(PageLink query) onFetchData; + + @override + Future> fetchPageData(PageLink pageKey) { + return onFetchData(pageKey); + } +} diff --git a/lib/modules/dashboard/domain/pagination/dashboards_query_ctrl.dart b/lib/modules/dashboard/domain/pagination/dashboards_query_ctrl.dart new file mode 100644 index 00000000..75bab4d5 --- /dev/null +++ b/lib/modules/dashboard/domain/pagination/dashboards_query_ctrl.dart @@ -0,0 +1,11 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class DashboardsQueryCtrl extends PageKeyController { + DashboardsQueryCtrl({int pageSize = 20}) : super(PageLink(20)); + + @override + PageLink nextPageKey(PageLink pageKey) { + return pageKey.nextPageLink(); + } +} diff --git a/lib/modules/dashboard/domain/usecases/fetch_dashboards_usecase.dart b/lib/modules/dashboard/domain/usecases/fetch_dashboards_usecase.dart new file mode 100644 index 00000000..d14c6e65 --- /dev/null +++ b/lib/modules/dashboard/domain/usecases/fetch_dashboards_usecase.dart @@ -0,0 +1,25 @@ +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/usecase.dart'; + +class FetchDashboardsUseCase + extends UseCase>, PageLink> { + const FetchDashboardsUseCase(this.tbClient); + + final ThingsboardClient tbClient; + + @override + Future> call(PageLink params) { + if (tbClient.isTenantAdmin()) { + return tbClient.getDashboardService().getTenantDashboards( + params, + mobile: true, + ); + } else { + return tbClient.getDashboardService().getCustomerDashboards( + tbClient.getAuthUser()!.customerId!, + params, + mobile: true, + ); + } + } +} diff --git a/lib/modules/dashboard/main_dashboard_page.dart b/lib/modules/dashboard/main_dashboard_page.dart index a6e94624..9b5bdc3e 100644 --- a/lib/modules/dashboard/main_dashboard_page.dart +++ b/lib/modules/dashboard/main_dashboard_page.dart @@ -2,67 +2,20 @@ 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/locator.dart'; -import 'package:thingsboard_app/modules/dashboard/dashboard.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/controller/dashboard_controller.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/controller/dashboard_page_controller.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/widgets/dashboard_widget.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.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 dashboardGoBack() { - if (_dashboardController != null) { - return _dashboardController!.goBack(); - } else { - return Future.value(true); - } - } - - Future openDashboard( - String dashboardId, { - String? dashboardTitle, - String? state, - bool? hideToolbar, - }) async { - if (dashboardTitle != null) { - _mainDashboardPageState?._updateTitle(dashboardTitle); - } - await _dashboardController?.openDashboard( - dashboardId, - state: state, - hideToolbar: hideToolbar, - ); - } - - Future activateDashboard() async { - await _dashboardController?.activateDashboard(); - } - - Future deactivateDashboard() async { - await _dashboardController?.deactivateDashboard(); - } -} - class MainDashboardPage extends TbContextWidget { - final String? _dashboardTitle; - final MainDashboardPageController? _controller; - MainDashboardPage( TbContext tbContext, { - MainDashboardPageController? controller, - String? dashboardTitle, + this.controller, super.key, - }) : _controller = controller, - _dashboardTitle = dashboardTitle, - super(tbContext); + }) : super(tbContext); + + final DashboardPageController? controller; @override State createState() => _MainDashboardPageState(); @@ -70,38 +23,13 @@ class MainDashboardPage extends TbContextWidget { class _MainDashboardPageState extends TbContextState with TickerProviderStateMixin { - late ValueNotifier dashboardTitleValue; - final ValueNotifier hasRightLayout = ValueNotifier(false); - DashboardController? _dashboardController; + final dashboardTitleValue = ValueNotifier('Dashboard'); + final hasRightLayout = ValueNotifier(false); + late final Animation rightLayoutMenuAnimation; late final AnimationController rightLayoutMenuController; - @override - void initState() { - super.initState(); - rightLayoutMenuController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - ); - rightLayoutMenuAnimation = CurvedAnimation( - curve: Curves.linear, - parent: rightLayoutMenuController, - ); - if (widget._controller != null) { - widget._controller!._setMainDashboardPageState(this); - } - dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard'); - } - - @override - void dispose() { - rightLayoutMenuController.dispose(); - super.dispose(); - } - - _updateTitle(String newTitle) { - dashboardTitleValue.value = newTitle; - } + DashboardController? _dashboardController; @override Widget build(BuildContext context) { @@ -144,32 +72,52 @@ class _MainDashboardPageState extends TbContextState body: ValueListenableBuilder( valueListenable: getIt().listenEndpointChanges, builder: (context, value, _) { - return Dashboard( + return DashboardWidget( tbContext, - key: UniqueKey(), - activeByDefault: false, titleCallback: (title) { dashboardTitleValue.value = title; }, + pageController: widget.controller, controllerCallback: (controller) { _dashboardController = controller; - if (widget._controller != null) { - widget._controller!._setDashboardController(controller); - controller.hasRightLayout.addListener(() { - hasRightLayout.value = controller.hasRightLayout.value; - }); - controller.rightLayoutOpened.addListener(() { - if (controller.rightLayoutOpened.value) { - rightLayoutMenuController.forward(); - } else { - rightLayoutMenuController.reverse(); - } - }); - } + widget.controller?.setDashboardController(controller); + + controller.hasRightLayout.addListener(() { + hasRightLayout.value = controller.hasRightLayout.value; + }); + controller.rightLayoutOpened.addListener(() { + if (controller.rightLayoutOpened.value) { + rightLayoutMenuController.forward(); + } else { + rightLayoutMenuController.reverse(); + } + }); }, ); }, ), ); } + + @override + void initState() { + super.initState(); + rightLayoutMenuController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + + rightLayoutMenuAnimation = CurvedAnimation( + curve: Curves.linear, + parent: rightLayoutMenuController, + ); + + widget.controller?.setDashboardTitleNotifier(dashboardTitleValue); + } + + @override + void dispose() { + rightLayoutMenuController.dispose(); + super.dispose(); + } } diff --git a/lib/modules/dashboard/presentation/controller/dashboard_controller.dart b/lib/modules/dashboard/presentation/controller/dashboard_controller.dart new file mode 100644 index 00000000..971061d5 --- /dev/null +++ b/lib/modules/dashboard/presentation/controller/dashboard_controller.dart @@ -0,0 +1,122 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; + +typedef DashboardTitleCallback = void Function(String title); + +typedef DashboardControllerCallback = void Function( + DashboardController controller, +); + +class DashboardController { + DashboardController(this.tbContext); + + final canGoBack = ValueNotifier(false); + final hasRightLayout = ValueNotifier(false); + final rightLayoutOpened = ValueNotifier(false); + final TbContext tbContext; + + InAppWebViewController? controller; + + void setWebViewController(InAppWebViewController ctrl) { + controller = ctrl; + } + + Future openDashboard( + String dashboardId, { + String? state, + bool? hideToolbar, + bool fullscreen = false, + bool home = false, + }) async { + final windowMessage = { + 'type': 'openDashboardMessage', + 'data': {'dashboardId': dashboardId}, + }; + if (state != null) { + windowMessage['data']['state'] = state; + } + if (home) { + windowMessage['data']['embedded'] = true; + } + if (hideToolbar == true) { + windowMessage['data']['hideToolbar'] = true; + } + + await controller?.postWebMessage( + message: WebMessage(data: jsonEncode(windowMessage)), + targetOrigin: WebUri('*'), + ); + } + + void onHistoryUpdated(Future canGoBackFuture) async { + canGoBack.value = await canGoBackFuture; + } + + void onHasRightLayout(bool hasRightLayout) { + this.hasRightLayout.value = hasRightLayout; + } + + void onRightLayoutOpened(bool rightLayoutOpened) { + this.rightLayoutOpened.value = rightLayoutOpened; + } + + Future toggleRightLayout() async { + final windowMessage = {'type': 'toggleDashboardLayout'}; + final webMessage = WebMessage(data: jsonEncode(windowMessage)); + await controller?.postWebMessage( + message: webMessage, + targetOrigin: WebUri('*'), + ); + } + + Future tryLocalNavigation(String? path, {required bool? home}) async { + tbContext.log.debug('tryLocalNavigation($path)'); + + if (path != null && path != '/home') { + final parts = path.split('/'); + if ([ + 'profile', + 'devices', + 'assets', + 'dashboards', + 'dashboard', + 'customers', + 'auditLogs', + 'deviceGroups', + 'assetGroups', + 'customerGroups', + 'dashboardGroups', + 'alarms', + ].contains(parts[1])) { + var firstPart = parts[1]; + if (firstPart.endsWith('Groups')) { + firstPart = firstPart.replaceFirst('Groups', 's'); + } + + if ((firstPart == 'dashboard' || firstPart == 'dashboards') && + parts.length > 1) { + final dashboardId = parts[1]; + await tbContext.navigateToDashboard(dashboardId); + } else if (firstPart != 'dashboard') { + var targetPath = '/$firstPart'; + if (firstPart == 'devices' && home == true) { + targetPath = '/devicesPage'; + } + + await tbContext.navigateTo(targetPath); + } + } else { + throw UnimplementedError('The path $path is currently not supported.'); + } + } + } + + void dispose() { + canGoBack.dispose(); + hasRightLayout.dispose(); + rightLayoutOpened.dispose(); + } +} diff --git a/lib/modules/dashboard/presentation/controller/dashboard_page_controller.dart b/lib/modules/dashboard/presentation/controller/dashboard_page_controller.dart new file mode 100644 index 00000000..7523c385 --- /dev/null +++ b/lib/modules/dashboard/presentation/controller/dashboard_page_controller.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/controller/dashboard_controller.dart'; +import 'package:thingsboard_app/widgets/two_page_view.dart'; + +class DashboardPageController { + DashboardPageController({required this.pageCtrl}); + + final dashboardController = Completer(); + late ValueNotifier dashboardTitleValue; + final TwoPageViewController pageCtrl; + + void setDashboardController(DashboardController controller) { + dashboardController.complete(controller); + } + + void setDashboardTitleNotifier(ValueNotifier notifier) { + dashboardTitleValue = notifier; + } + + Future openDashboard( + String dashboardId, { + String? title, + String? state, + bool? hideToolbar, + }) async { + if (title != null) { + dashboardTitleValue.value = title; + } + + dashboardController.future.then((controller) { + controller.openDashboard( + dashboardId, + state: state, + hideToolbar: hideToolbar, + ); + }); + + return pageCtrl.open(1, animate: true); + } + + Future closeDashboard() async { + return pageCtrl.close(1, animate: true); + } +} diff --git a/lib/modules/dashboard/presentation/view/dashboards_page.dart b/lib/modules/dashboard/presentation/view/dashboards_page.dart new file mode 100644 index 00000000..37f14f20 --- /dev/null +++ b/lib/modules/dashboard/presentation/view/dashboards_page.dart @@ -0,0 +1,58 @@ +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/dashboard/di/dashboards_di.dart'; +import 'package:thingsboard_app/modules/dashboard/main_dashboard_page.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/controller/dashboard_page_controller.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/widgets/dashboards_appbar.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/widgets/dashboards_grid.dart'; +import 'package:thingsboard_app/widgets/two_page_view.dart'; + +class DashboardsPage extends TbContextWidget { + DashboardsPage( + TbContext tbContext, { + super.key, + }) : super(tbContext); + + @override + State createState() => _DashboardsPageState(); +} + +class _DashboardsPageState extends TbContextState { + final pageViewCtrl = TwoPageViewController(); + late final DashboardPageController dashboardPageCtrl; + + late final String diKey; + + @override + Widget build(BuildContext context) { + return TwoPageView( + controller: pageViewCtrl, + first: DashboardsAppbar( + tbContext: tbContext, + body: DashboardsGridWidget( + tbContext: tbContext, + dashboardPageCtrl: dashboardPageCtrl, + ), + ), + second: MainDashboardPage( + tbContext, + controller: dashboardPageCtrl, + ), + ); + } + + @override + void initState() { + diKey = UniqueKey().toString(); + DashboardsDi.init(diKey, tbClient: widget.tbClient); + dashboardPageCtrl = DashboardPageController(pageCtrl: pageViewCtrl); + super.initState(); + } + + @override + void dispose() { + DashboardsDi.dispose(diKey); + super.dispose(); + } +} diff --git a/lib/modules/dashboard/fullscreen_dashboard_page.dart b/lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart similarity index 95% rename from lib/modules/dashboard/fullscreen_dashboard_page.dart rename to lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart index daca6b4e..0b10a643 100644 --- a/lib/modules/dashboard/fullscreen_dashboard_page.dart +++ b/lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart @@ -2,7 +2,7 @@ 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/locator.dart'; -import 'package:thingsboard_app/modules/dashboard/dashboard.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/widgets/dashboard_widget.dart'; import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; @@ -79,9 +79,8 @@ class _FullscreenDashboardPageState ), body: ValueListenableBuilder( valueListenable: getIt().listenEndpointChanges, - builder: (context, _, __) => Dashboard( + builder: (context, _, __) => DashboardWidget( tbContext, - key: UniqueKey(), titleCallback: (title) { dashboardTitleValue.value = title; }, diff --git a/lib/modules/dashboard/presentation/view/home_dashboard_page.dart b/lib/modules/dashboard/presentation/view/home_dashboard_page.dart new file mode 100644 index 00000000..b224413f --- /dev/null +++ b/lib/modules/dashboard/presentation/view/home_dashboard_page.dart @@ -0,0 +1,37 @@ +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/dashboard/presentation/widgets/dashboard_widget.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/widgets/dashboards_appbar.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class HomeDashboardPage extends TbContextWidget { + final HomeDashboardInfo dashboard; + + HomeDashboardPage(TbContext tbContext, this.dashboard, {super.key}) + : super(tbContext); + + @override + State createState() => _HomeDashboardState(); +} + +class _HomeDashboardState extends TbContextState { + @override + Widget build(BuildContext context) { + return DashboardsAppbar( + tbContext: tbContext, + dashboardState: true, + body: DashboardWidget( + tbContext, + home: true, + controllerCallback: (controller) { + controller.openDashboard( + widget.dashboard.dashboardId!.id!, + hideToolbar: widget.dashboard.hideDashboardToolbar, + home: true, + ); + }, + ), + ); + } +} diff --git a/lib/modules/dashboard/presentation/view/single_dashboard_view.dart b/lib/modules/dashboard/presentation/view/single_dashboard_view.dart new file mode 100644 index 00000000..7d3e8ab8 --- /dev/null +++ b/lib/modules/dashboard/presentation/view/single_dashboard_view.dart @@ -0,0 +1,125 @@ +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/dashboard/presentation/controller/dashboard_controller.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/widgets/dashboard_widget.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class SingleDashboardView extends TbContextWidget { + SingleDashboardView( + TbContext tbContext, { + required this.id, + this.title, + this.state, + this.hideToolbar, + super.key, + }) : super(tbContext); + + final String id; + final String? title; + final String? state; + final bool? hideToolbar; + + @override + State createState() => _SingleDashboardViewState(); +} + +class _SingleDashboardViewState extends TbContextState + with TickerProviderStateMixin { + final dashboardTitleValue = ValueNotifier('Dashboard'); + final hasRightLayout = ValueNotifier(false); + + late final Animation rightLayoutMenuAnimation; + late final AnimationController rightLayoutMenuController; + + DashboardController? _dashboardController; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TbAppBar( + tbContext, + leading: BackButton(onPressed: maybePop), + showLoadingIndicator: false, + elevation: 1, + shadowColor: Colors.transparent, + title: ValueListenableBuilder( + valueListenable: dashboardTitleValue, + builder: (context, title, widget) { + return FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text(title), + ); + }, + ), + actions: [ + ValueListenableBuilder( + valueListenable: hasRightLayout, + builder: (context, hasRightLayout, widget) { + if (hasRightLayout) { + return IconButton( + onPressed: () => _dashboardController?.toggleRightLayout(), + icon: AnimatedIcon( + progress: rightLayoutMenuAnimation, + icon: AnimatedIcons.menu_close, + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], + ), + body: DashboardWidget( + tbContext, + titleCallback: (title) { + dashboardTitleValue.value = widget.title ?? title; + }, + controllerCallback: (controller) { + controller.hasRightLayout.addListener(() { + hasRightLayout.value = controller.hasRightLayout.value; + }); + controller.rightLayoutOpened.addListener(() { + if (controller.rightLayoutOpened.value) { + rightLayoutMenuController.forward(); + } else { + rightLayoutMenuController.reverse(); + } + }); + + controller.openDashboard( + widget.id, + state: widget.state, + hideToolbar: widget.hideToolbar, + ); + }, + ), + ); + } + + @override + void initState() { + super.initState(); + rightLayoutMenuController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + + rightLayoutMenuAnimation = CurvedAnimation( + curve: Curves.linear, + parent: rightLayoutMenuController, + ); + + if (widget.title != null) { + dashboardTitleValue.value = widget.title!; + } + } + + @override + void dispose() { + rightLayoutMenuController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_grid_card.dart b/lib/modules/dashboard/presentation/widgets/dashboard_grid_card.dart new file mode 100644 index 00000000..dc645d98 --- /dev/null +++ b/lib/modules/dashboard/presentation/widgets/dashboard_grid_card.dart @@ -0,0 +1,88 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.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/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/utils.dart'; + +class DashboardGridCard extends TbContextWidget { + final DashboardInfo dashboard; + + DashboardGridCard(TbContext tbContext, {super.key, required this.dashboard}) + : super(tbContext); + + @override + State createState() => _DashboardGridCardState(); +} + +class _DashboardGridCardState extends TbContextState { + _DashboardGridCardState() : super(); + + @override + void didUpdateWidget(DashboardGridCard oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + var hasImage = widget.dashboard.image != null; + Widget image; + if (hasImage) { + image = + Utils.imageFromTbImage(context, tbClient, widget.dashboard.image!); + } else { + image = SvgPicture.asset( + ThingsboardImage.dashboardPlaceholder, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.overlay, + ), + semanticsLabel: 'Dashboard', + ); + } + + return ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Column( + children: [ + Expanded( + child: Stack( + children: [ + SizedBox.expand( + child: FittedBox( + clipBehavior: Clip.hardEdge, + fit: BoxFit.cover, + child: image, + ), + ), + ], + ), + ), + const Divider(height: 1), + SizedBox( + height: 44, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Center( + child: AutoSizeText( + widget.dashboard.title, + textAlign: TextAlign.center, + maxLines: 1, + minFontSize: 12, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart new file mode 100644 index 00000000..2a57ddf5 --- /dev/null +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -0,0 +1,339 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/controller/dashboard_controller.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/controller/dashboard_page_controller.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; +import 'package:universal_platform/universal_platform.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class DashboardWidget extends StatefulWidget { + const DashboardWidget( + this.tbContext, { + required this.controllerCallback, + this.pageController, + this.home, + this.titleCallback, + super.key, + }); + + final TbContext tbContext; + final DashboardControllerCallback controllerCallback; + final DashboardPageController? pageController; + final bool? home; + final DashboardTitleCallback? titleCallback; + + @override + State createState() => _DashboardState(); +} + +class _DashboardState extends State { + bool webViewLoading = true; + final dashboardLoading = ValueNotifier(true); + + late final DashboardController dashboardController; + late final TbLogger log; + late WebUri _initialUrl; + + final settings = InAppWebViewSettings( + isInspectable: kDebugMode, + useShouldOverrideUrlLoading: true, + mediaPlaybackRequiresUserGesture: false, + javaScriptEnabled: true, + cacheEnabled: true, + supportZoom: false, + clearCache: true, + useHybridComposition: true, + thirdPartyCookiesEnabled: true, + allowsInlineMediaPlayback: true, + allowsBackForwardNavigationGestures: false, + ); + + final options = InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + useShouldOverrideUrlLoading: true, + mediaPlaybackRequiresUserGesture: false, + javaScriptEnabled: true, + cacheEnabled: true, + supportZoom: false, + clearCache: true, + // useOnDownloadStart: true + ), + android: AndroidInAppWebViewOptions( + useHybridComposition: true, + thirdPartyCookiesEnabled: true, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: true, + allowsBackForwardNavigationGestures: false, + ), + ); + + @override + Widget build(BuildContext context) { + if (UniversalPlatform.isWeb) { + return const Center(child: Text('Not implemented!')); + } + + return WillPopScope( + onWillPop: () async { + if (dashboardController.rightLayoutOpened.value) { + await dashboardController.toggleRightLayout(); + } + + final controller = dashboardController.controller; + if (await controller?.canGoBack() == true) { + await controller?.goBack(); + } else { + widget.pageController?.closeDashboard().then( + (_) => dashboardLoading.value = true, + ); + return true; + } + + return true; + }, + child: Stack( + children: [ + InAppWebView( + initialUrlRequest: URLRequest(url: _initialUrl), + initialSettings: settings, + onWebViewCreated: (webViewController) { + log.debug('onWebViewCreated'); + webViewController.addJavaScriptHandler( + handlerName: 'tbMobileReadyHandler', + callback: (_) async { + log.debug('Invoked tbMobileReadyHandler'); + + dashboardController.setWebViewController(webViewController); + widget.controllerCallback(dashboardController); + }, + ); + webViewController.addJavaScriptHandler( + handlerName: 'tbMobileDashboardLoadedHandler', + callback: (args) async { + bool hasRightLayout = args[0]; + bool rightLayoutOpened = args[1]; + log.debug( + 'Invoked tbMobileDashboardLoadedHandler: ' + 'hasRightLayout: $hasRightLayout, ' + 'rightLayoutOpened: $rightLayoutOpened', + ); + dashboardController.onHasRightLayout(hasRightLayout); + dashboardController.onRightLayoutOpened(rightLayoutOpened); + dashboardLoading.value = false; + }, + ); + webViewController.addJavaScriptHandler( + handlerName: 'tbMobileDashboardLayoutHandler', + callback: (args) async { + bool rightLayoutOpened = args[0]; + log.debug( + 'Invoked tbMobileDashboardLayoutHandler: ' + 'rightLayoutOpened: $rightLayoutOpened', + ); + dashboardController.onRightLayoutOpened(rightLayoutOpened); + }, + ); + webViewController.addJavaScriptHandler( + handlerName: 'tbMobileDashboardStateNameHandler', + callback: (args) async { + log.debug( + 'Invoked tbMobileDashboardStateNameHandler: $args', + ); + if (args.isNotEmpty && args[0] is String) { + if (widget.titleCallback != null) { + widget.titleCallback!(args[0]); + } + } + }, + ); + webViewController.addJavaScriptHandler( + handlerName: 'tbMobileNavigationHandler', + callback: (args) async { + log.debug( + 'Invoked tbMobileNavigationHandler: $args', + ); + if (args.isNotEmpty) { + late String path; + + if (args.first.contains('.')) { + path = '/${args.first.split('.').last}'; + } else { + path = '/${args.first}'; + } + + Map? params; + if (args.length > 1) { + params = args[1]; + } + + log.debug('path: $path'); + log.debug('params: $params'); + try { + await dashboardController.tryLocalNavigation( + path, + home: widget.home, + ); + } on UnimplementedError catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + _buildWarnSnackBar(e.message!), + ); + } + } + } + }, + ); + webViewController.addJavaScriptHandler( + handlerName: 'tbMobileHandler', + callback: (args) async { + log.debug('Invoked tbMobileHandler: $args'); + return await widget.tbContext.widgetActionHandler + .handleWidgetMobileAction( + args, + webViewController, + ); + }, + ); + }, + shouldOverrideUrlLoading: (controller, navigationAction) async { + final uri = navigationAction.request.url!; + final uriString = uri.toString(); + final endpoint = await getIt().getEndpoint(); + + log.debug('shouldOverrideUrlLoading $uriString'); + if (Platform.isAndroid || + Platform.isIOS && + navigationAction.iosWKNavigationType == + IOSWKNavigationType.LINK_ACTIVATED) { + if (uriString.startsWith(endpoint)) { + var target = uriString.substring(endpoint.length); + if (!target.startsWith('?accessToken')) { + if (target.startsWith('/')) { + target = target.substring(1); + } + try { + await dashboardController.tryLocalNavigation( + target, + home: widget.home, + ); + } on UnimplementedError catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + _buildWarnSnackBar(e.message!), + ); + } + } + return NavigationActionPolicy.CANCEL; + } + } else if (await canLaunchUrlString(uriString)) { + await launchUrlString(uriString); + return NavigationActionPolicy.CANCEL; + } + } + + return Platform.isIOS + ? NavigationActionPolicy.ALLOW + : NavigationActionPolicy.CANCEL; + }, + onUpdateVisitedHistory: (controller, url, androidIsReload) async { + log.debug('onUpdateVisitedHistory: $url'); + dashboardController.onHistoryUpdated(controller.canGoBack()); + }, + onConsoleMessage: (controller, consoleMessage) { + log.debug( + '[JavaScript console] ${consoleMessage.messageLevel}: ' + '${consoleMessage.message}', + ); + }, + onLoadStart: (controller, url) async { + log.debug('onLoadStart: $url'); + }, + onLoadStop: (controller, url) async { + log.debug('onLoadStop: $url'); + if (webViewLoading) { + webViewLoading = false; + } + }, + androidOnPermissionRequest: (controller, origin, resources) async { + log.debug( + 'androidOnPermissionRequest origin: $origin, resources: $resources', + ); + return PermissionRequestResponse( + resources: resources, + action: PermissionRequestResponseAction.GRANT, + ); + }, + ), + ValueListenableBuilder( + valueListenable: dashboardLoading, + builder: (context, loading, child) { + if (!loading) { + return const SizedBox.shrink(); + } else { + final data = MediaQuery.of(context); + 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: const TbProgressIndicator(size: 50.0), + ); + } + }, + ), + ], + ), + ); + } + + @override + void initState() { + super.initState(); + + _initialUrl = WebUri( + '${getIt().getCachedEndpoint()}' + '?accessToken=${widget.tbContext.tbClient.getJwtToken()!}' + '&refreshToken=${widget.tbContext.tbClient.getRefreshToken()!}', + ); + + dashboardController = DashboardController(widget.tbContext); + log = widget.tbContext.log; + } + + @override + void dispose() { + dashboardLoading.dispose(); + dashboardController.dispose(); + super.dispose(); + } + + SnackBar _buildWarnSnackBar(String message) { + return SnackBar( + duration: const Duration(seconds: 10), + backgroundColor: const Color(0xFFdc6d1b), + content: Text( + message, + style: const TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Close', + textColor: Colors.white, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + ); + } +} diff --git a/lib/modules/dashboard/presentation/widgets/dashboards_appbar.dart b/lib/modules/dashboard/presentation/widgets/dashboards_appbar.dart new file mode 100644 index 00000000..0dd77a8d --- /dev/null +++ b/lib/modules/dashboard/presentation/widgets/dashboards_appbar.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.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/widgets/tb_app_bar.dart'; + +class DashboardsAppbar extends StatelessWidget { + const DashboardsAppbar({ + required this.tbContext, + required this.body, + this.dashboardState = false, + super.key, + }); + + final TbContext tbContext; + final Widget body; + final bool dashboardState; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TbAppBar( + tbContext, + elevation: dashboardState ? 0 : 8, + title: Center( + child: SizedBox( + height: 24, + child: SvgPicture.asset( + ThingsboardImage.thingsBoardWithTitle, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.srcIn, + ), + semanticsLabel: 'ThingsBoard Logo', + ), + ), + ), + actions: [ + if (tbContext.tbClient.isSystemAdmin()) + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + tbContext.navigateTo('/tenants?search=true'); + }, + ), + ], + ), + body: body, + ); + } +} diff --git a/lib/modules/dashboard/presentation/widgets/dashboards_grid.dart b/lib/modules/dashboard/presentation/widgets/dashboards_grid.dart new file mode 100644 index 00000000..618098c8 --- /dev/null +++ b/lib/modules/dashboard/presentation/widgets/dashboards_grid.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/core/entity/entity_grid_card.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/dashboard/domain/pagination/dashboards_pagination_repository.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/controller/dashboard_page_controller.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/widgets/dashboard_grid_card.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/first_page_exception_widget.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/first_page_progress_builder.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/new_page_progress_builder.dart'; +import 'package:thingsboard_app/utils/ui/pagination_widgets/pagination_grid_widget.dart'; + +class DashboardsGridWidget extends StatelessWidget { + const DashboardsGridWidget({ + required this.tbContext, + required this.dashboardPageCtrl, + super.key, + }); + + final TbContext tbContext; + final DashboardPageController dashboardPageCtrl; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + getIt().pagingController.refresh(); + }, + child: PaginationGridWidget( + pagingController: + getIt().pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => EntityGridCard( + item, + entityCardWidgetBuilder: (_, dashboard) => DashboardGridCard( + tbContext, + dashboard: dashboard, + ), + onEntityTap: (dashboard) { + dashboardPageCtrl.openDashboard( + dashboard.id!.id!, + title: dashboard.title, + ); + }, + settings: EntityCardSettings(dropShadow: true), + ), + firstPageProgressIndicatorBuilder: (_) => + const FirstPageProgressBuilder(), + newPageProgressIndicatorBuilder: (_) => + const NewPageProgressBuilder(), + noItemsFoundIndicatorBuilder: (context) => + FirstPageExceptionIndicator( + title: 'No dashboards found', + message: S.of(context).listIsEmptyText, + onTryAgain: () { + getIt() + .pagingController + .refresh(); + }, + ), + ), + ), + ); + } +} diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index ecee32d0..51fa4465 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -1,14 +1,10 @@ import 'package:flutter/material.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/modules/dashboard/dashboard.dart' - as dashboard_ui; -import 'package:thingsboard_app/modules/dashboard/dashboards_grid.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/view/dashboards_page.dart'; +import 'package:thingsboard_app/modules/dashboard/presentation/view/home_dashboard_page.dart'; import 'package:thingsboard_app/modules/tenant/tenants_widget.dart'; import 'package:thingsboard_app/thingsboard_client.dart'; -import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class HomePage extends TbContextWidget { HomePage(TbContext tbContext, {super.key}) : super(tbContext); @@ -29,59 +25,25 @@ class _HomePageState extends TbContextState super.build(context); final homeDashboard = tbContext.homeDashboard; - final dashboardState = homeDashboard != null; - - return Scaffold( - appBar: TbAppBar( - tbContext, - elevation: dashboardState ? 0 : 8, - title: Center( - child: SizedBox( - height: 24, - child: SvgPicture.asset( - ThingsboardImage.thingsBoardWithTitle, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, - BlendMode.srcIn, - ), - semanticsLabel: 'ThingsBoard Logo', - ), - ), - ), - actions: [ - if (tbClient.isSystemAdmin()) - IconButton( - icon: const Icon(Icons.search), - onPressed: () { - navigateTo('/tenants?search=true'); - }, - ), - ], - ), - body: Builder( - builder: (context) { - if (dashboardState) { - return _buildDashboardHome(context, homeDashboard); - } else { - return _buildDefaultHome(context); - } - }, - ), - ); + if (homeDashboard != null) { + return _buildDashboardHome(context, homeDashboard); + } else { + return _buildDefaultHome(context); + } } Widget _buildDashboardHome( BuildContext context, HomeDashboardInfo dashboard, ) { - return HomeDashboard(tbContext, dashboard); + return HomeDashboardPage(tbContext, dashboard); } Widget _buildDefaultHome(BuildContext context) { if (tbClient.isSystemAdmin()) { return _buildSysAdminHome(context); } else { - return DashboardsGridWidget(tbContext); + return DashboardsPage(tbContext); } } @@ -89,29 +51,3 @@ class _HomePageState extends TbContextState return TenantsWidget(tbContext); } } - -class HomeDashboard extends TbContextWidget { - final HomeDashboardInfo dashboard; - - HomeDashboard(TbContext tbContext, this.dashboard, {super.key}) - : super(tbContext); - - @override - State createState() => _HomeDashboardState(); -} - -class _HomeDashboardState extends TbContextState { - @override - Widget build(BuildContext context) { - return dashboard_ui.Dashboard( - tbContext, - home: true, - controllerCallback: (controller) { - controller.openDashboard( - widget.dashboard.dashboardId!.id!, - hideToolbar: widget.dashboard.hideDashboardToolbar, - ); - }, - ); - } -} diff --git a/lib/utils/services/communication/communication_service.dart b/lib/utils/services/communication/communication_service.dart new file mode 100644 index 00000000..a21b0a81 --- /dev/null +++ b/lib/utils/services/communication/communication_service.dart @@ -0,0 +1,19 @@ +import 'package:event_bus/event_bus.dart'; +import 'package:thingsboard_app/utils/services/communication/events.dart'; +import 'package:thingsboard_app/utils/services/communication/i_communication_service.dart'; + +class CommunicationService implements ICommunicationService { + const CommunicationService(EventBus eventBus) : _eventBus = eventBus; + + final EventBus _eventBus; + + @override + void fire(CommunicationEvent event) { + _eventBus.fire(event); + } + + @override + Stream on() { + return _eventBus.on(); + } +} diff --git a/lib/utils/services/communication/events.dart b/lib/utils/services/communication/events.dart new file mode 100644 index 00000000..b7d966a4 --- /dev/null +++ b/lib/utils/services/communication/events.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +base class CommunicationEvent extends Equatable { + const CommunicationEvent(); + + @override + List get props => []; +} + +final class UserLoggedInEvent extends CommunicationEvent { + const UserLoggedInEvent(this.user); + + final User? user; + + @override + List get props => [user]; +} diff --git a/lib/utils/services/communication/i_communication_service.dart b/lib/utils/services/communication/i_communication_service.dart new file mode 100644 index 00000000..50acabd7 --- /dev/null +++ b/lib/utils/services/communication/i_communication_service.dart @@ -0,0 +1,7 @@ +import 'package:thingsboard_app/utils/services/communication/events.dart'; + +abstract interface class ICommunicationService { + Stream on(); + + void fire(CommunicationEvent event); +} diff --git a/lib/utils/services/endpoint/endpoint_service.dart b/lib/utils/services/endpoint/endpoint_service.dart index f201c2d5..b5512d81 100644 --- a/lib/utils/services/endpoint/endpoint_service.dart +++ b/lib/utils/services/endpoint/endpoint_service.dart @@ -16,8 +16,6 @@ class EndpointService implements IEndpointService { @override Future setEndpoint(String endpoint) async { - print('setEndpoint'); - _cachedEndpoint = endpoint; _notifierValue.value = UniqueKey().toString(); diff --git a/lib/utils/services/user/i_user_service.dart b/lib/utils/services/user/i_user_service.dart new file mode 100644 index 00000000..04fa554e --- /dev/null +++ b/lib/utils/services/user/i_user_service.dart @@ -0,0 +1 @@ +abstract interface class IUserService {} diff --git a/lib/utils/services/user/user_service.dart b/lib/utils/services/user/user_service.dart new file mode 100644 index 00000000..7085fc19 --- /dev/null +++ b/lib/utils/services/user/user_service.dart @@ -0,0 +1,10 @@ +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/communication/events.dart'; +import 'package:thingsboard_app/utils/services/communication/i_communication_service.dart'; +import 'package:thingsboard_app/utils/services/user/i_user_service.dart'; + +class UserService implements IUserService { + UserService() { + getIt().on().listen((user) {}); + } +} diff --git a/lib/utils/ui/pagination_widgets/pagination_grid_widget.dart b/lib/utils/ui/pagination_widgets/pagination_grid_widget.dart new file mode 100644 index 00000000..a571b3ec --- /dev/null +++ b/lib/utils/ui/pagination_widgets/pagination_grid_widget.dart @@ -0,0 +1,50 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +class PaginationGridWidget extends StatelessWidget { + const PaginationGridWidget({ + required this.pagingController, + required this.builderDelegate, + this.heading, + this.gridDelegate, + this.gridChildAspectRatio, + super.key, + }); + + final Widget? heading; + final PagingController pagingController; + final PagedChildBuilderDelegate builderDelegate; + final SliverGridDelegate? gridDelegate; + final double? gridChildAspectRatio; + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverVisibility( + visible: heading != null, + sliver: SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + sliver: SliverToBoxAdapter(child: heading), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: PagedSliverGrid( + pagingController: pagingController, + builderDelegate: builderDelegate, + gridDelegate: gridDelegate ?? + SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: gridChildAspectRatio ?? 156 / 150, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + crossAxisCount: 2, + ), + ), + ) + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 910ac15f..3b4b76e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 modal_bottom_sheet: ^3.0.0 + event_bus: ^2.0.0 dev_dependencies: flutter_test: From af3b742354caafd049908b28271f39b43654d66a Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 7 Aug 2024 11:50:15 +0300 Subject: [PATCH 35/61] Removed deprecated things for web view. --- ios/Podfile.lock | 46 +++++++++++-------- .../widgets/dashboard_widget.dart | 20 -------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 23516760..35007fd7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,30 +1,30 @@ PODS: - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (10.27.0): - - FirebaseCore (= 10.27.0) - - Firebase/Messaging (10.27.0): + - Firebase/CoreOnly (10.29.0): + - FirebaseCore (= 10.29.0) + - Firebase/Messaging (10.29.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.27.0) - - firebase_core (3.1.1): - - Firebase/CoreOnly (= 10.27.0) + - FirebaseMessaging (~> 10.29.0) + - firebase_core (3.3.0): + - Firebase/CoreOnly (= 10.29.0) - Flutter - - firebase_messaging (15.0.2): - - Firebase/Messaging (= 10.27.0) + - firebase_messaging (15.0.4): + - Firebase/Messaging (= 10.29.0) - firebase_core - Flutter - - FirebaseCore (10.27.0): + - FirebaseCore (10.29.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.28.0): + - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.28.0): + - FirebaseInstallations (10.29.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.27.0): + - FirebaseMessaging (10.29.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleDataTransport (~> 9.3) @@ -96,6 +96,8 @@ PODS: - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner + - uni_links (0.0.1): + - Flutter - url_launcher_ios (0.0.1): - Flutter @@ -113,6 +115,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) + - uni_links (from `.symlinks/plugins/uni_links/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -156,18 +159,20 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" qr_code_scanner: :path: ".symlinks/plugins/qr_code_scanner/ios" + uni_links: + :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d - Firebase: 26b040b20866a55f55eb3611b9fcf3ae64816b86 - firebase_core: f8d0424c45e0f1e596811085fc12c638d628457c - firebase_messaging: 8b29edaf5adfd3b52b5bfa5af8128c44164670c6 - FirebaseCore: a2b95ae4ce7c83ceecfbbbe3b6f1cddc7415a808 - FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698 - FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e - FirebaseMessaging: 585984d0a1df120617eb10b44cad8968b859815e + Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d + firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb + firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757 + FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_app_badger: b87fc231847b03b92ce1412aa351842e7e97932f flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 @@ -184,8 +189,9 @@ SPEC CHECKSUMS: path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b -PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b +PODFILE CHECKSUM: 8a4b2eabe1a99180e46baba5a37492de09c643c4 COCOAPODS: 1.15.2 diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart index 2a57ddf5..184d1b57 100644 --- a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -55,26 +55,6 @@ class _DashboardState extends State { allowsBackForwardNavigationGestures: false, ); - final options = InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( - useShouldOverrideUrlLoading: true, - mediaPlaybackRequiresUserGesture: false, - javaScriptEnabled: true, - cacheEnabled: true, - supportZoom: false, - clearCache: true, - // useOnDownloadStart: true - ), - android: AndroidInAppWebViewOptions( - useHybridComposition: true, - thirdPartyCookiesEnabled: true, - ), - ios: IOSInAppWebViewOptions( - allowsInlineMediaPlayback: true, - allowsBackForwardNavigationGestures: false, - ), - ); - @override Widget build(BuildContext context) { if (UniversalPlatform.isWeb) { From a93c9651d2ec93858137f15eed63f9a8aa0c23af Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 16 Aug 2024 11:37:58 +0300 Subject: [PATCH 36/61] Reduced size of tb_context.dart file by moving has_tb_context.dart mixin into separate file. --- .../view/switch_endpoint_noauth_view.dart | 5 +- lib/core/context/has_tb_context.dart | 107 ++++++++++++++++ lib/core/context/tb_context.dart | 117 +----------------- lib/modules/alarm/alarms_list.dart | 5 +- 4 files changed, 115 insertions(+), 119 deletions(-) create mode 100644 lib/core/context/has_tb_context.dart diff --git a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart index ad74168d..487aca20 100644 --- a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart +++ b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart @@ -11,13 +11,12 @@ import 'package:thingsboard_app/locator.dart'; class SwitchEndpointNoAuthView extends TbPageWidget { SwitchEndpointNoAuthView({ - super.key, - required this.tbContext, + required TbContext tbContext, required this.arguments, + super.key, }) : super(tbContext); final Map? arguments; - final TbContext tbContext; @override State createState() => _SwitchEndpointNoAuthViewState(); diff --git a/lib/core/context/has_tb_context.dart b/lib/core/context/has_tb_context.dart new file mode 100644 index 00000000..12c29793 --- /dev/null +++ b/lib/core/context/has_tb_context.dart @@ -0,0 +1,107 @@ +part of 'tb_context.dart'; + +mixin HasTbContext { + late final TbContext _tbContext; + + void setTbContext(TbContext tbContext) { + _tbContext = tbContext; + } + + void setupCurrentState(TbContextState currentState) { + if (_tbContext.currentState != null) { + // ignore: deprecated_member_use + ModalRoute.of(_tbContext.currentState!.context) + ?.unregisterPopEntry(_tbContext); + } + _tbContext.currentState = currentState; + if (_tbContext.currentState != null) { + // ignore: deprecated_member_use + ModalRoute.of(_tbContext.currentState!.context) + ?.registerPopEntry(_tbContext); + } + } + + void setupTbContext(TbContextState currentState) { + _tbContext = currentState.widget.tbContext; + } + + TbContext get tbContext => _tbContext; + + TbLogger get log => _tbContext.log; + + bool get isPhysicalDevice => _tbContext.isPhysicalDevice(); + + WidgetActionHandler get widgetActionHandler => _tbContext.widgetActionHandler; + + ValueNotifier get loadingNotifier => _tbContext._isLoadingNotifier; + + ThingsboardClient get tbClient => _tbContext.tbClient; + + Future initTbContext() async { + await _tbContext.init(); + } + + Future navigateTo( + String path, { + bool replace = false, + bool clearStack = false, + }) => + _tbContext.navigateTo(path, replace: replace, clearStack: clearStack); + + void pop([T? result, BuildContext? context]) => + _tbContext.pop(result, context); + + Future maybePop([T? result]) => + _tbContext.maybePop(result); + + Future navigateToDashboard( + String dashboardId, { + String? dashboardTitle, + String? state, + bool? hideToolbar, + bool animate = true, + }) => + _tbContext.navigateToDashboard( + dashboardId, + dashboardTitle: dashboardTitle, + state: state, + hideToolbar: hideToolbar, + animate: animate, + ); + + Future confirm({ + required String title, + required String message, + String cancel = 'Cancel', + String ok = 'Ok', + }) => + _tbContext.confirm( + title: title, + message: message, + cancel: cancel, + ok: ok, + ); + + void hideNotification() => _tbContext.hideNotification(); + + void showErrorNotification(String message, {Duration? duration}) => + _tbContext.showErrorNotification(message, duration: duration); + + void showInfoNotification(String message, {Duration? duration}) => + _tbContext.showInfoNotification(message, duration: duration); + + void showWarnNotification(String message, {Duration? duration}) => + _tbContext.showWarnNotification(message, duration: duration); + + void showSuccessNotification(String message, {Duration? duration}) => + _tbContext.showSuccessNotification(message, duration: duration); + + void subscribeRouteObserver(TbPageState pageState) { + _tbContext.routeObserver + .subscribe(pageState, ModalRoute.of(pageState.context) as PageRoute); + } + + void unsubscribeRouteObserver(TbPageState pageState) { + _tbContext.routeObserver.unsubscribe(pageState); + } +} diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index cafca5d9..c28c8f74 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -23,6 +23,8 @@ import 'package:thingsboard_app/utils/services/widget_action_handler.dart'; import 'package:uni_links/uni_links.dart'; import 'package:universal_platform/universal_platform.dart'; +part 'has_tb_context.dart'; + enum NotificationType { info, warn, success, error } class TbContext implements PopEntry { @@ -41,7 +43,6 @@ class TbContext implements PopEntry { late final AndroidDeviceInfo? _androidInfo; late final IosDeviceInfo? _iosInfo; late final String packageName; - bool _closeMainFirst = false; StreamSubscription? _appLinkStreamSubscription; late bool _handleRootState; @@ -162,7 +163,7 @@ class TbContext implements PopEntry { var message = e is ThingsboardError ? (e.message ?? 'Unknown error.') : 'Unknown error.'; - message = 'Fatal application error occured:\n$message.'; + message = 'Fatal application error occurred:\n$message.'; await alert(title: 'Fatal error', message: message, ok: 'Close'); logout(); } @@ -624,115 +625,3 @@ class TbContext implements PopEntry { ); } } - -mixin HasTbContext { - late final TbContext _tbContext; - - void setTbContext(TbContext tbContext) { - _tbContext = tbContext; - } - - void setupCurrentState(TbContextState currentState) { - if (_tbContext.currentState != null) { - // ignore: deprecated_member_use - ModalRoute.of(_tbContext.currentState!.context) - ?.unregisterPopEntry(_tbContext); - } - _tbContext.currentState = currentState; - if (_tbContext.currentState != null) { - // ignore: deprecated_member_use - ModalRoute.of(_tbContext.currentState!.context) - ?.registerPopEntry(_tbContext); - } - if (_tbContext._closeMainFirst) { - _tbContext._closeMainFirst = false; - if (_tbContext.currentState != null) { - _tbContext.currentState!.closeMainFirst = true; - } - } - } - - void setupTbContext(TbContextState currentState) { - _tbContext = currentState.widget.tbContext; - } - - TbContext get tbContext => _tbContext; - - TbLogger get log => _tbContext.log; - - bool get isPhysicalDevice => _tbContext.isPhysicalDevice(); - - WidgetActionHandler get widgetActionHandler => _tbContext.widgetActionHandler; - - ValueNotifier get loadingNotifier => _tbContext._isLoadingNotifier; - - ThingsboardClient get tbClient => _tbContext.tbClient; - - Future initTbContext() async { - await _tbContext.init(); - } - - Future navigateTo( - String path, { - bool replace = false, - bool clearStack = false, - }) => - _tbContext.navigateTo(path, replace: replace, clearStack: clearStack); - - void pop([T? result, BuildContext? context]) => - _tbContext.pop(result, context); - - Future maybePop([T? result]) => - _tbContext.maybePop(result); - - Future navigateToDashboard( - String dashboardId, { - String? dashboardTitle, - String? state, - bool? hideToolbar, - bool animate = true, - }) => - _tbContext.navigateToDashboard( - dashboardId, - dashboardTitle: dashboardTitle, - state: state, - hideToolbar: hideToolbar, - animate: animate, - ); - - Future confirm({ - required String title, - required String message, - String cancel = 'Cancel', - String ok = 'Ok', - }) => - _tbContext.confirm( - title: title, - message: message, - cancel: cancel, - ok: ok, - ); - - void hideNotification() => _tbContext.hideNotification(); - - void showErrorNotification(String message, {Duration? duration}) => - _tbContext.showErrorNotification(message, duration: duration); - - void showInfoNotification(String message, {Duration? duration}) => - _tbContext.showInfoNotification(message, duration: duration); - - void showWarnNotification(String message, {Duration? duration}) => - _tbContext.showWarnNotification(message, duration: duration); - - void showSuccessNotification(String message, {Duration? duration}) => - _tbContext.showSuccessNotification(message, duration: duration); - - void subscribeRouteObserver(TbPageState pageState) { - _tbContext.routeObserver - .subscribe(pageState, ModalRoute.of(pageState.context) as PageRoute); - } - - void unsubscribeRouteObserver(TbPageState pageState) { - _tbContext.routeObserver.unsubscribe(pageState); - } -} diff --git a/lib/modules/alarm/alarms_list.dart b/lib/modules/alarm/alarms_list.dart index 7a952f57..e8c67e9a 100644 --- a/lib/modules/alarm/alarms_list.dart +++ b/lib/modules/alarm/alarms_list.dart @@ -42,8 +42,9 @@ class AlarmsList extends StatelessWidget { String? dashboardId = alarm.details?['dashboardId']; if (dashboardId != null) { final state = Utils.createDashboardEntityState( - alarm.originator, - entityName: alarm.originatorName); + alarm.originator, + entityName: alarm.originatorName, + ); tbContext.navigateToDashboard( dashboardId, dashboardTitle: alarm.originatorName, From d160fe772bf3ab01835fd47cfdf889383d92afd4 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 19 Aug 2024 11:24:27 +0300 Subject: [PATCH 37/61] [Mobile] The keyboard overlaps the entire "Assignee" bottomsheet on the "Filter" page --- .../bloc/assignee/assignee_bloc.dart | 6 + .../bloc/assignee/assignee_event.dart | 4 + .../presentation/view/alarms_filter_page.dart | 72 ++--- .../alarm_types/alarm_types_widget.dart | 2 + .../assignee/alarm_assignee_widget.dart | 7 +- .../assignee/assignee_list_widget.dart | 273 +++++++++--------- lib/utils/ui/ui_utils.dart | 2 + 7 files changed, 196 insertions(+), 170 deletions(-) diff --git a/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart b/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart index 2af5eab7..39b5a5e0 100644 --- a/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart @@ -52,11 +52,17 @@ class AssigneeBloc extends Bloc { case AssigneeResetEvent(): selectedUserId = null; emit(const AssigneeEmptyState()); + queryCtrl.onSearchText(null); break; case AssigneeRefreshEvent(): paginationRepository.refresh(); + break; + + case AssigneeResetSearchTextEvent(): + queryCtrl.onSearchText(null); + break; } } diff --git a/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart b/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart index ba64e386..3e97a4bd 100644 --- a/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart @@ -29,6 +29,10 @@ final class AssigneeSearchEvent extends AssigneeEvent { List get props => [searchText]; } +final class AssigneeResetSearchTextEvent extends AssigneeEvent { + const AssigneeResetSearchTextEvent(); +} + final class AssigneeResetEvent extends AssigneeEvent { const AssigneeResetEvent(); } diff --git a/lib/modules/alarm/presentation/view/alarms_filter_page.dart b/lib/modules/alarm/presentation/view/alarms_filter_page.dart index 1f2c478a..9c238776 100644 --- a/lib/modules/alarm/presentation/view/alarms_filter_page.dart +++ b/lib/modules/alarm/presentation/view/alarms_filter_page.dart @@ -71,11 +71,7 @@ class _AlarmsFilterPageState extends TbContextState { title: const Text('Filters'), leading: BackButton( onPressed: () { - widget.pageController.animateToPage( - 0, - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOut, - ); + _onBackButtonClick(); }, ), ), @@ -150,33 +146,9 @@ class _AlarmsFilterPageState extends TbContextState { ], ), AlarmControlFiltersButton( - onResetTap: filtersChanged - ? () { - setState(() { - alarmStatusSelected - ..clear() - ..add(alarmStatus.first); - alarmSeveritySelected.clear(); - filtersChanged = false; - }); - - getIt().add( - const AlarmTypesResetEvent(), - ); - getIt().add( - const AssigneeResetEvent(), - ); - getIt().add( - const AlarmFiltersResetEvent(), - ); - } - : null, + onResetTap: filtersChanged ? _resetFilters : null, onCancelTap: () { - widget.pageController.animateToPage( - 0, - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOut, - ); + _onBackButtonClick(); }, onUpdateTap: filtersChanged ? () { @@ -225,11 +197,7 @@ class _AlarmsFilterPageState extends TbContextState { listenNavigationChanges = widget .tbContext.bottomNavigationTabChangedStream.stream .listen((tabIndex) { - widget.pageController.animateToPage( - 0, - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOut, - ); + _onBackButtonClick(); }); super.initState(); @@ -241,4 +209,36 @@ class _AlarmsFilterPageState extends TbContextState { super.dispose(); } + + void _resetFilters() { + setState(() { + alarmStatusSelected + ..clear() + ..add(alarmStatus.first); + alarmSeveritySelected.clear(); + filtersChanged = false; + }); + + getIt().add( + const AlarmTypesResetEvent(), + ); + getIt().add( + const AssigneeResetEvent(), + ); + getIt().add( + const AlarmFiltersResetEvent(), + ); + } + + void _onBackButtonClick() { + widget.pageController.animateToPage( + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + + if (filtersChanged) { + _resetFilters(); + } + } } diff --git a/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart index 96e826bb..0f7c391f 100644 --- a/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart +++ b/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart @@ -38,6 +38,7 @@ class AlarmTypesWidget extends StatelessWidget { onTap: () { UiUtils.showModalBottomSheet( context: context, + topControl: const SizedBox.shrink(), builder: (context) => AnimatedSize( curve: Curves.easeInOut, duration: const Duration(milliseconds: 500), @@ -128,6 +129,7 @@ class AlarmTypesWidget extends StatelessWidget { onTap: () { UiUtils.showModalBottomSheet( context: context, + topControl: const SizedBox.shrink(), builder: (context) => TypesListWidget( tbContext: tbContext, onChanged: onChanged, diff --git a/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart index 3f0cc44f..313a19c5 100644 --- a/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart +++ b/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/bloc.dart'; import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_filter_widget.dart'; import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart'; @@ -23,9 +24,10 @@ class AlarmAssigneeFilter extends StatelessWidget { return AlarmFilterWidget( filterTitle: 'Assignee', child: InkWell( - onTap: () { - UiUtils.showModalBottomSheet( + onTap: () async { + await UiUtils.showModalBottomSheet( context: context, + topControl: const SizedBox.shrink(), builder: (context) => AnimatedSize( curve: Curves.easeInOut, duration: const Duration(milliseconds: 500), @@ -35,6 +37,7 @@ class AlarmAssigneeFilter extends StatelessWidget { ), ), ); + getIt().add(const AssigneeResetSearchTextEvent()); }, child: Container( height: 38, diff --git a/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart index cfc8f580..9f32940c 100644 --- a/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart +++ b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart @@ -24,164 +24,173 @@ class AssigneeListWidget extends StatelessWidget { Widget build(BuildContext context) { return ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, - minHeight: MediaQuery.of(context).size.height * 0.3, + maxHeight: MediaQuery.of(context).size.height * 0.7 + + MediaQuery.of(context).viewInsets.bottom, + minHeight: MediaQuery.of(context).size.height * 0.3 + + MediaQuery.of(context).viewInsets.bottom, ), child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ), - Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.black.withOpacity(0.12), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), ), - borderRadius: BorderRadius.circular(4), ), - margin: const EdgeInsets.only(left: 12, right: 12, bottom: 8), - width: double.infinity, - child: Row( - children: [ - Expanded( - child: TextField( - decoration: InputDecoration( - border: InputBorder.none, - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.38), - fontWeight: FontWeight.w400, - fontSize: 14, + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black.withOpacity(0.12), + ), + borderRadius: BorderRadius.circular(4), + ), + margin: const EdgeInsets.only(left: 12, right: 12, bottom: 8), + width: double.infinity, + child: Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + border: InputBorder.none, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.38), + fontWeight: FontWeight.w400, + fontSize: 14, + ), + hintText: 'Search users', + contentPadding: + const EdgeInsets.fromLTRB(16, 8, 8, 8), + isDense: true, ), - hintText: 'Search users', - contentPadding: const EdgeInsets.fromLTRB(16, 8, 8, 8), - isDense: true, + onChanged: (text) { + getIt().add( + AssigneeSearchEvent(searchText: text), + ); + }, ), - onChanged: (text) { - getIt().add( - AssigneeSearchEvent(searchText: text), - ); - }, ), - ), - Icon(Icons.search, color: Colors.black.withOpacity(0.54)), - const SizedBox(width: 8), - ], + Icon(Icons.search, color: Colors.black.withOpacity(0.54)), + const SizedBox(width: 8), + ], + ), ), - ), - Flexible( - child: PagedListView.separated( - pagingController: - getIt().paginationRepository.pagingController, - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - shrinkWrap: true, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - final state = getIt().state; - Widget? userInfoWidget; + Flexible( + child: PagedListView.separated( + pagingController: getIt() + .paginationRepository + .pagingController, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shrinkWrap: true, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + final state = getIt().state; + Widget? userInfoWidget; - if (state is AssigneeSelectedState) { - final selectedId = state.assignee.userInfo.id.id; - if (selectedId == item.userInfo.id.id) { - userInfoWidget = const SizedBox.shrink(); + if (state is AssigneeSelectedState) { + final selectedId = state.assignee.userInfo.id.id; + if (selectedId == item.userInfo.id.id) { + userInfoWidget = const SizedBox.shrink(); + } } - } - return Column( - children: [ - Visibility( - visible: index == 0 && - state is! AssigneeSelfAssignmentState, - child: Column( - children: [ + return Column( + children: [ + Visibility( + visible: index == 0 && + state is! AssigneeSelfAssignmentState, + child: Column( + children: [ + UserInfoWidget( + avatar: Icon( + Icons.account_circle, + color: Colors.black.withOpacity(0.38), + ), + name: 'Assigned to me', + onUserTap: (id) { + Navigator.of(context).pop(); + getIt().add( + AssigneeSelectedEvent( + userId: id, + selfAssignment: true, + ), + ); + + onChanged(); + }, + id: tbContext.tbClient.getAuthUser()!.userId!, + ), + const Divider(thickness: 1, height: 24), + ], + ), + ), + userInfoWidget ?? UserInfoWidget( - avatar: Icon( - Icons.account_circle, - color: Colors.black.withOpacity(0.38), + avatar: UserInfoAvatarWidget( + shortName: item.shortName, + color: HSLColor.fromAHSL( + 1, + item.displayName.hashCode % 360, + 40 / 100, + 60 / 100, + ).toColor(), ), - name: 'Assigned to me', + name: item.displayName, + email: item.userInfo.email, + showEmail: !item.displayName.isValidEmail(), onUserTap: (id) { Navigator.of(context).pop(); getIt().add( - AssigneeSelectedEvent( - userId: id, - selfAssignment: true, - ), + AssigneeSelectedEvent(userId: id), ); onChanged(); }, - id: tbContext.tbClient.getAuthUser()!.userId!, + id: item.userInfo.id.id!, ), - const Divider(thickness: 1, height: 24), - ], - ), + ], + ); + }, + firstPageProgressIndicatorBuilder: (_) { + return Container( + height: 200, + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator(size: 50.0), ), - userInfoWidget ?? - UserInfoWidget( - avatar: UserInfoAvatarWidget( - shortName: item.shortName, - color: HSLColor.fromAHSL( - 1, - item.displayName.hashCode % 360, - 40 / 100, - 60 / 100, - ).toColor(), - ), - name: item.displayName, - email: item.userInfo.email, - showEmail: !item.displayName.isValidEmail(), - onUserTap: (id) { - Navigator.of(context).pop(); - getIt().add( - AssigneeSelectedEvent(userId: id), - ); - - onChanged(); - }, - id: item.userInfo.id.id!, - ), - ], - ); - }, - firstPageProgressIndicatorBuilder: (_) { - return Container( - height: 200, - color: const Color(0x99FFFFFF), - child: const Center( - child: TbProgressIndicator(size: 50.0), - ), - ); - }, - ), - separatorBuilder: (_, index) { - final state = getIt().state; + ); + }, + ), + separatorBuilder: (_, index) { + final state = getIt().state; - if (state is AssigneeSelectedState) { - final selectedId = state.assignee.userInfo.id.id; - final userId = getIt() - .paginationRepository - .pagingController - .itemList?[index]; - if (selectedId == userId?.userInfo.id.id) { - return const SizedBox.shrink(); + if (state is AssigneeSelectedState) { + final selectedId = state.assignee.userInfo.id.id; + final userId = getIt() + .paginationRepository + .pagingController + .itemList?[index]; + if (selectedId == userId?.userInfo.id.id) { + return const SizedBox.shrink(); + } } - } - return const Divider(thickness: 1, height: 24); - }, + return const Divider(thickness: 1, height: 24); + }, + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/utils/ui/ui_utils.dart b/lib/utils/ui/ui_utils.dart index 7478fe0d..ae1e932a 100644 --- a/lib/utils/ui/ui_utils.dart +++ b/lib/utils/ui/ui_utils.dart @@ -6,11 +6,13 @@ abstract class UiUtils { required BuildContext context, required WidgetBuilder builder, Color barrierColor = Colors.black54, + Widget? topControl, }) async { return showBarModalBottomSheet( context: context, builder: builder, barrierColor: barrierColor, + topControl: topControl, ); } } From 06d377a7af2400f4fcce5ecb77e6f8907e6d6ccb Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 19 Aug 2024 11:24:43 +0300 Subject: [PATCH 38/61] [Mobile app] The keyboard doesn't hide after click on OAuth providers. --- lib/core/auth/login/login_page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 8914fbe9..e87e8d03 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -120,6 +120,7 @@ class _LoginPageState extends TbPageState { OutlinedButton( style: _oauth2IconButtonStyle, onPressed: () async { + FocusScope.of(context).unfocus(); try { final barcode = await tbContext.navigateTo( @@ -354,6 +355,7 @@ class _LoginPageState extends TbPageState { child: OutlinedButton( style: _oauth2IconButtonStyle, onPressed: () async { + FocusScope.of(context).unfocus(); try { final barcode = await tbContext.navigateTo( '/qrCodeScan', @@ -452,6 +454,7 @@ class _LoginPageState extends TbPageState { } void _oauth2ButtonPressed(OAuth2ClientInfo client) async { + FocusScope.of(context).unfocus(); _isLoginNotifier.value = true; try { final result = await tbContext.oauth2Client.authenticate(client.url); From 2e3172d032e62742630a34d1b6cdd15cf3d72d0a Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 19 Aug 2024 14:52:56 +0300 Subject: [PATCH 39/61] [Mobile] The long names of alarm type cut off by the phone's borders. --- .../widgets/alarm_types/alarm_types_widget.dart | 14 ++++++++------ .../widgets/alarm_types/types_list_widget.dart | 4 +++- .../widgets/assignee/alarm_assignee_widget.dart | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart index 0f7c391f..acf0d13d 100644 --- a/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart +++ b/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart @@ -94,12 +94,14 @@ class AlarmTypesWidget extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - state.selectedTypes.elementAt(index), - style: TextStyle( - color: Colors.black.withOpacity(0.87), - fontWeight: FontWeight.w400, - fontSize: 14, + Flexible( + child: Text( + state.selectedTypes.elementAt(index), + style: TextStyle( + color: Colors.black.withOpacity(0.87), + fontWeight: FontWeight.w400, + fontSize: 14, + ), ), ), const SizedBox(width: 4), diff --git a/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart index 80ea4ebe..b5b24ff0 100644 --- a/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart +++ b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart @@ -59,7 +59,9 @@ class TypesListWidget extends StatelessWidget { }, child: Row( children: [ - Text(item.type), + Flexible( + child: Text(item.type), + ), ], ), ); diff --git a/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart index 313a19c5..5fb95b71 100644 --- a/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart +++ b/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart @@ -40,14 +40,14 @@ class AlarmAssigneeFilter extends StatelessWidget { getIt().add(const AssigneeResetSearchTextEvent()); }, child: Container( - height: 38, + constraints: const BoxConstraints(minHeight: 38), decoration: BoxDecoration( border: Border.all( color: Colors.black.withOpacity(0.12), ), borderRadius: BorderRadius.circular(4), ), - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: BlocBuilder( builder: (context, state) { switch (state) { From 92f3c5173444ee6ca9653a97f2570851cbf33a84 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 20 Aug 2024 11:38:03 +0300 Subject: [PATCH 40/61] Fixed the error is displayed instead of user with long name on the list of users on the "Filters" screen. --- lib/modules/alarm/domain/entities/assignee_entity.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/modules/alarm/domain/entities/assignee_entity.dart b/lib/modules/alarm/domain/entities/assignee_entity.dart index 51593e9f..4466bab2 100644 --- a/lib/modules/alarm/domain/entities/assignee_entity.dart +++ b/lib/modules/alarm/domain/entities/assignee_entity.dart @@ -15,11 +15,13 @@ class AssigneeEntity extends Equatable { factory AssigneeEntity.fromUserInfo(UserInfo info) { final name = '${info.firstName ?? ''} ${info.lastName ?? ''}'; final displayName = name.length > 1 ? name : info.email; + final shortName = + '${info.firstName?[0] ?? ''}${info.lastName?[0] ?? ''}'.toUpperCase(); return AssigneeEntity( userInfo: info, displayName: displayName, - shortName: displayName.split(' ').map((e) => e[0]).join('').toUpperCase(), + shortName: shortName, ); } From bad0c1ece26ddad9f97fd328a12a9a7c6a11c0f1 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 20 Aug 2024 15:44:48 +0300 Subject: [PATCH 41/61] Alarm filetes UI updated. --- .../alarm_types/types_list_widget.dart | 47 +++++++++++++------ .../assignee/assignee_list_widget.dart | 46 +++++++++++++----- .../assignee/user_info_avatar_widget.dart | 6 +-- .../widgets/assignee/user_info_widget.dart | 7 +-- .../widgets/filter_toggle_block_widget.dart | 3 +- 5 files changed, 74 insertions(+), 35 deletions(-) diff --git a/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart index b5b24ff0..7fe82f7d 100644 --- a/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart +++ b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart @@ -28,14 +28,29 @@ class TypesListWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), + Padding( + padding: const EdgeInsets.only(left: 16, top: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Assignee', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.close), + ), + ), + ], ), ), Flexible( @@ -45,7 +60,7 @@ class TypesListWidget extends StatelessWidget { .pagingController, shrinkWrap: true, padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) { return GestureDetector( @@ -60,7 +75,13 @@ class TypesListWidget extends StatelessWidget { child: Row( children: [ Flexible( - child: Text(item.type), + child: Text( + item.type, + style: const TextStyle( + fontSize: 16, + height: 1.5, + ), + ), ), ], ), @@ -76,10 +97,8 @@ class TypesListWidget extends StatelessWidget { ); }, ), - separatorBuilder: (_, __) => const Divider( - thickness: 1, - height: 24, - ), + separatorBuilder: (_, __) => + const Divider(thickness: 1, height: 32), ), ), ], diff --git a/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart index 9f32940c..ef0d899f 100644 --- a/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart +++ b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart @@ -37,14 +37,29 @@ class AssigneeListWidget extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), + Padding( + padding: const EdgeInsets.only(left: 16, top: 12, bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Alarm types', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.close), + ), + ), + ], ), ), Container( @@ -69,7 +84,7 @@ class AssigneeListWidget extends StatelessWidget { ), hintText: 'Search users', contentPadding: - const EdgeInsets.fromLTRB(16, 8, 8, 8), + const EdgeInsets.fromLTRB(16, 12, 16, 12), isDense: true, ), onChanged: (text) { @@ -79,8 +94,12 @@ class AssigneeListWidget extends StatelessWidget { }, ), ), - Icon(Icons.search, color: Colors.black.withOpacity(0.54)), - const SizedBox(width: 8), + Icon( + Icons.search, + color: Colors.black.withOpacity(0.54), + size: 24, + ), + const SizedBox(width: 16), ], ), ), @@ -90,7 +109,7 @@ class AssigneeListWidget extends StatelessWidget { .paginationRepository .pagingController, padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shrinkWrap: true, builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) { @@ -115,6 +134,7 @@ class AssigneeListWidget extends StatelessWidget { avatar: Icon( Icons.account_circle, color: Colors.black.withOpacity(0.38), + size: 32, ), name: 'Assigned to me', onUserTap: (id) { @@ -130,7 +150,7 @@ class AssigneeListWidget extends StatelessWidget { }, id: tbContext.tbClient.getAuthUser()!.userId!, ), - const Divider(thickness: 1, height: 24), + const Divider(thickness: 1, height: 32), ], ), ), diff --git a/lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart index 6e9d4e9d..6b022f17 100644 --- a/lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart +++ b/lib/modules/alarm/presentation/widgets/assignee/user_info_avatar_widget.dart @@ -17,15 +17,15 @@ class UserInfoAvatarWidget extends StatelessWidget { borderRadius: BorderRadius.circular(100), color: color, ), - height: 24, - width: 24, + height: 32, + width: 32, child: Center( child: Text( shortName, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, - fontSize: 11, + fontSize: 12, ), ), ), diff --git a/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart index ce9a87e0..e27da55e 100644 --- a/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart +++ b/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart @@ -39,16 +39,17 @@ class UserInfoWidget extends StatelessWidget { Text( name, style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, + fontSize: 16, + fontWeight: FontWeight.w500, ), ), + const SizedBox(height: 4), Visibility( visible: showEmail, child: Text( email, style: TextStyle( - fontSize: 11, + fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black.withOpacity(0.38), ), diff --git a/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart b/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart index 59382f0e..b70167e4 100644 --- a/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart +++ b/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart @@ -37,8 +37,7 @@ class _FilterToggleBlockWidgetState extends State { return AlarmFilterWidget( filterTitle: widget.label, child: Wrap( - spacing: 12, - runSpacing: 12, + spacing: 8, children: List.generate( widget.items.length, (index) => FilledButton.icon( From f4e1faa8b7cecace9ac37e9decb68c42a058bebf Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 21 Aug 2024 13:35:11 +0300 Subject: [PATCH 42/61] Fixed the infinite loader appears on the "Profile" screen after receiving "Invalid email address format...." error from server. --- lib/modules/profile/profile_page.dart | 33 +++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart index ce532b81..52ea848a 100644 --- a/lib/modules/profile/profile_page.dart +++ b/lib/modules/profile/profile_page.dart @@ -161,24 +161,29 @@ class _ProfilePageState extends TbPageState { if (_currentUser != null) { FocusScope.of(context).unfocus(); if (_profileFormKey.currentState?.saveAndValidate() ?? false) { - var formValue = _profileFormKey.currentState!.value; + final formValue = _profileFormKey.currentState!.value; _currentUser!.email = formValue['email']; _currentUser!.firstName = formValue['firstName']; _currentUser!.lastName = formValue['lastName']; _isLoadingNotifier.value = true; - _currentUser = await tbClient.getUserService().saveUser(_currentUser!); - tbContext.userDetails = _currentUser; - _setUser(); - await Future.delayed(const Duration(milliseconds: 300)); - _isLoadingNotifier.value = false; - showSuccessNotification( - S.of(context).profileSuccessNotification, - duration: const Duration(milliseconds: 1500), - ); - showSuccessNotification( - S.of(context).profileSuccessNotification, - duration: const Duration(milliseconds: 1500), - ); + try { + _currentUser = + await tbClient.getUserService().saveUser(_currentUser!); + tbContext.userDetails = _currentUser; + _setUser(); + await Future.delayed(const Duration(milliseconds: 300)); + _isLoadingNotifier.value = false; + showSuccessNotification( + S.of(context).profileSuccessNotification, + duration: const Duration(milliseconds: 1500), + ); + showSuccessNotification( + S.of(context).profileSuccessNotification, + duration: const Duration(milliseconds: 1500), + ); + } catch (_) { + _isLoadingNotifier.value = false; + } } } } From 5c7a76ab4eb9eb9ea5b9c3819ebdc7a9d7acbd0c Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 23 Aug 2024 10:14:54 +0300 Subject: [PATCH 43/61] =?UTF-8?q?Fixed=20wrong=20titles=20are=20displayed?= =?UTF-8?q?=20on=20the=20=E2=80=9CAssignee=E2=80=9C=20and=20=E2=80=9CAlarm?= =?UTF-8?q?=20type=20list=E2=80=9C=20bottom=20sheet.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/intl_en.arb | 41 ++++++++++++++++++- .../alarm/presentation/view/alarms_page.dart | 3 +- .../alarm_types/types_list_widget.dart | 7 ++-- .../assignee/assignee_list_widget.dart | 7 ++-- .../widgets/dashboard_widget.dart | 15 +++---- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6846ac36..424628e1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -108,5 +108,44 @@ "mfaProviderTopt": "Authenticator app", "mfaProviderSms": "SMS", "mfaProviderEmail": "Email", - "mfaProviderBackupCode": "Backup code" + "mfaProviderBackupCode": "Backup code", + + "newUserText": "New User?", + "createAccount": "Create Account", + + "emailVerification": "Email verification", + "emailVerificationSentText": "An email with verification details was sent to the specified email address ", + "emailVerificationInstructionsText": "Please follow instructions provided in the email in order to complete your sign up procedure. Note: if you haven't seen the email for a while, please check your 'spam' folder or try to resend email by clicking 'Resend' button.", + "resend": "Resend", + + "activatingAccount": "Activating account...", + "accountActivated": "Account successfully activated!", + "emailVerified": "Email verified", + "activatingAccountText": "Your account is currently activating.\nPlease wait...", + "accountActivatedText": "Congratulations!\nYour {appTitle} account has been activated.\nNow you can login to your {appTitle} space.", + + "privacyPolicy": "Privacy Policy", + "cancel": "Cancel", + "accept": "Accept", + + "termsOfUse": "Terms of Use", + + "firstNameStar": "First name *", + "firstNameRequireText": "First name is required.", + "lastNameStar": "Last name *", + "lastNameRequireText": "Last name is required.", + "createPasswordStar": "Create a password *", + "repeatPasswordStar": "Repeat your password *", + "imNotARobot": "I'm not a robot", + "signUp": "Sign up", + "alreadyHaveAnAccount": "Already have an account?", + "signIn": "Sign In", + "invalidPasswordLengthMessage": "Your password must be at least 6 characters long", + "confirmNotRobotMessage": "You must confirm that you are not a robot", + "acceptPrivacyPolicyMessage": "You must accept our Privacy Policy", + "acceptTermsOfUseMessage": "You must accept our Terms of Use", + "inactiveUserAlreadyExists": "Inactive user already exists", + "inactiveUserAlreadyExistsMessage": "There is already registered user with unverified email address.\nClick 'Resend' button if you wish to resend verification email.", + "assignee": "Assignee", + "alarmTypes": "Alarm types" } diff --git a/lib/modules/alarm/presentation/view/alarms_page.dart b/lib/modules/alarm/presentation/view/alarms_page.dart index 225c2fb1..2fbfff22 100644 --- a/lib/modules/alarm/presentation/view/alarms_page.dart +++ b/lib/modules/alarm/presentation/view/alarms_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:preload_page_view/preload_page_view.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; @@ -46,7 +47,7 @@ class _AlarmsPageState extends TbContextState return Scaffold( appBar: TbAppBar( tbContext, - title: const Text('Alarms'), + title: Text(S.of(context).alarms), actions: [ Stack( children: [ diff --git a/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart index 7fe82f7d..670ff588 100644 --- a/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart +++ b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/locator.dart'; @@ -33,9 +34,9 @@ class TypesListWidget extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Assignee', - style: TextStyle( + Text( + S.of(context).alarmTypes, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), diff --git a/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart index ef0d899f..26a65a27 100644 --- a/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart +++ b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/messages.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/locator.dart'; @@ -42,9 +43,9 @@ class AssigneeListWidget extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Alarm types', - style: TextStyle( + Text( + S.of(context).assignee, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart index 184d1b57..9f225f31 100644 --- a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -191,8 +191,8 @@ class _DashboardState extends State { log.debug('shouldOverrideUrlLoading $uriString'); if (Platform.isAndroid || Platform.isIOS && - navigationAction.iosWKNavigationType == - IOSWKNavigationType.LINK_ACTIVATED) { + navigationAction.navigationType == + NavigationType.LINK_ACTIVATED) { if (uriString.startsWith(endpoint)) { var target = uriString.substring(endpoint.length); if (!target.startsWith('?accessToken')) { @@ -242,13 +242,14 @@ class _DashboardState extends State { webViewLoading = false; } }, - androidOnPermissionRequest: (controller, origin, resources) async { + onPermissionRequest: (controller, request) async { log.debug( - 'androidOnPermissionRequest origin: $origin, resources: $resources', + 'onPermissionRequest resources: ${request.resources}', ); - return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT, + + return PermissionResponse( + action: PermissionResponseAction.GRANT, + resources: request.resources, ); }, ), From d726c1c2111be2c1762424b7d7b02b33488271cb Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 23 Aug 2024 11:42:38 +0300 Subject: [PATCH 44/61] Fixed the user list is empty on the filters screen when one of the users is without a first name or last name. --- .../alarm/domain/entities/assignee_entity.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/modules/alarm/domain/entities/assignee_entity.dart b/lib/modules/alarm/domain/entities/assignee_entity.dart index 4466bab2..4c5dcf8c 100644 --- a/lib/modules/alarm/domain/entities/assignee_entity.dart +++ b/lib/modules/alarm/domain/entities/assignee_entity.dart @@ -15,13 +15,23 @@ class AssigneeEntity extends Equatable { factory AssigneeEntity.fromUserInfo(UserInfo info) { final name = '${info.firstName ?? ''} ${info.lastName ?? ''}'; final displayName = name.length > 1 ? name : info.email; - final shortName = - '${info.firstName?[0] ?? ''}${info.lastName?[0] ?? ''}'.toUpperCase(); + + String shortName = ''; + if (info.firstName?.isNotEmpty == true) { + shortName = info.firstName?[0] ?? ''; + } + if (info.lastName?.isNotEmpty == true) { + shortName += info.lastName?[0] ?? ''; + } + + if (shortName.isEmpty) { + shortName = info.email[0]; + } return AssigneeEntity( userInfo: info, displayName: displayName, - shortName: shortName, + shortName: shortName.toUpperCase(), ); } From 8eee3b654b0233ff002af63f98b858e7e2989330 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 23 Aug 2024 12:39:51 +0300 Subject: [PATCH 45/61] Filed all alarm filters reset after closing the "Filters" screen by taping on the "Back"/"Cancel" button or switching between tabs. --- .../presentation/view/alarms_filter_page.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/modules/alarm/presentation/view/alarms_filter_page.dart b/lib/modules/alarm/presentation/view/alarms_filter_page.dart index 9c238776..5855f983 100644 --- a/lib/modules/alarm/presentation/view/alarms_filter_page.dart +++ b/lib/modules/alarm/presentation/view/alarms_filter_page.dart @@ -54,8 +54,17 @@ class _AlarmsFilterPageState extends TbContextState { final alarmStatusSelected = []; final alarmSeveritySelected = []; + /// This flag indicates that the user has made changes to the filters. + /// For example, selecting a status, assignee, or any other filter option. bool filtersChanged = false; + /// This flag is used to indicate that the user has changed and applied filters by clicking the Update button. + /// It helps determine whether to reset filter changes when the user clicks the back or cancel button. + /// Logic: + /// -- If the filters were changed but not applied, clicking the back or cancel button will discard the changes. + /// -- If the filters were changed and applied, the filters will be preserved as they are. + bool filtersChangesApplied = false; + late final StreamSubscription listenNavigationChanges; @override @@ -178,6 +187,10 @@ class _AlarmsFilterPageState extends TbContextState { duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, ); + + setState(() { + filtersChangesApplied = true; + }); } : null, ), @@ -217,6 +230,7 @@ class _AlarmsFilterPageState extends TbContextState { ..add(alarmStatus.first); alarmSeveritySelected.clear(); filtersChanged = false; + filtersChangesApplied = false; }); getIt().add( @@ -237,7 +251,7 @@ class _AlarmsFilterPageState extends TbContextState { curve: Curves.easeInOut, ); - if (filtersChanged) { + if (filtersChanged && !filtersChangesApplied) { _resetFilters(); } } From ae4e7b349af82ded25c6ab5d5d368ad44f8933be Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 23 Aug 2024 14:32:49 +0300 Subject: [PATCH 46/61] "Pull to refresh" doesn't work on the alarm filters screen. --- .../presentation/view/alarms_filter_page.dart | 1 + .../notification/notification_page.dart | 70 +++++++++---------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/lib/modules/alarm/presentation/view/alarms_filter_page.dart b/lib/modules/alarm/presentation/view/alarms_filter_page.dart index 5855f983..c1f7b2d3 100644 --- a/lib/modules/alarm/presentation/view/alarms_filter_page.dart +++ b/lib/modules/alarm/presentation/view/alarms_filter_page.dart @@ -91,6 +91,7 @@ class _AlarmsFilterPageState extends TbContextState { }, child: SafeArea( child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), child: Padding( padding: const EdgeInsets.all(16), child: Column( diff --git a/lib/modules/notification/notification_page.dart b/lib/modules/notification/notification_page.dart index 04f6430f..bd921905 100644 --- a/lib/modules/notification/notification_page.dart +++ b/lib/modules/notification/notification_page.dart @@ -29,44 +29,44 @@ class _NotificationPageState extends TbPageState { @override Widget build(BuildContext context) { - return RefreshIndicator( - onRefresh: () async => _refresh(), - child: Scaffold( - appBar: TbAppBar( - tbContext, - leading: IconButton( - onPressed: () { - final navigator = Navigator.of(tbContext.currentState!.context); - if (navigator.canPop()) { - tbContext.pop(); - } else { - tbContext.navigateTo( - '/home', - replace: true, - transition: TransitionType.fadeIn, - transitionDuration: const Duration(milliseconds: 750), - ); + return Scaffold( + appBar: TbAppBar( + tbContext, + leading: IconButton( + onPressed: () { + final navigator = Navigator.of(tbContext.currentState!.context); + if (navigator.canPop()) { + tbContext.pop(); + } else { + tbContext.navigateTo( + '/home', + replace: true, + transition: TransitionType.fadeIn, + transitionDuration: const Duration(milliseconds: 750), + ); + } + }, + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back, + ), + ), + title: const Text('Notifications'), + actions: [ + TextButton( + child: const Text('Mark all as read'), + onPressed: () async { + await notificationRepository.markAllAsRead(); + + if (mounted) { + notificationQueryCtrl.refresh(); } }, - icon: Icon( - Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back, - ), ), - title: const Text('Notifications'), - actions: [ - TextButton( - child: const Text('Mark all as read'), - onPressed: () async { - await notificationRepository.markAllAsRead(); - - if (mounted) { - notificationQueryCtrl.refresh(); - } - }, - ), - ], - ), - body: StreamBuilder( + ], + ), + body: RefreshIndicator( + onRefresh: () async => _refresh(), + child: StreamBuilder( stream: NotificationsLocalService.notificationsNumberStream.stream, builder: (context, snapshot) { if (snapshot.hasData) { From 8f7b3268c0e428e49dd5acdc4b56a2cf710c776a Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 29 Aug 2024 11:51:44 +0300 Subject: [PATCH 47/61] All alarm filters reset after closing the "Filters" screen by taping on the "Back"/"Cancel" button or switching between tabs. --- lib/modules/alarm/di/alarm_types_di.dart | 1 + lib/modules/alarm/di/alarms_di.dart | 6 + lib/modules/alarm/di/assignee_di.dart | 1 + .../domain/entities/alarm_filters_entity.dart | 20 ++- .../bloc/alarm_types/alarm_types_bloc.dart | 42 +++-- .../bloc/alarm_types/alarm_types_event.dart | 4 + .../bloc/assignee/assignee_bloc.dart | 18 +- .../bloc/assignee/assignee_event.dart | 4 + .../bloc/filters/alarm_filters_service.dart | 155 ++++++++++++++++++ .../filters/alarm_assignee_filter.dart | 34 ++++ .../filters/alarm_severity_filter.dart | 38 +++++ .../filters/filters/alarm_status_filter.dart | 42 +++++ .../filters/filters/alarm_type_filter.dart | 38 +++++ .../bloc/filters/filters/i_alarm_filter.dart | 7 + .../bloc/filters/i_alarm_filters_service.dart | 22 +++ .../presentation/view/alarms_filter_page.dart | 119 ++++++-------- 16 files changed, 461 insertions(+), 90 deletions(-) create mode 100644 lib/modules/alarm/presentation/bloc/filters/alarm_filters_service.dart create mode 100644 lib/modules/alarm/presentation/bloc/filters/filters/alarm_assignee_filter.dart create mode 100644 lib/modules/alarm/presentation/bloc/filters/filters/alarm_severity_filter.dart create mode 100644 lib/modules/alarm/presentation/bloc/filters/filters/alarm_status_filter.dart create mode 100644 lib/modules/alarm/presentation/bloc/filters/filters/alarm_type_filter.dart create mode 100644 lib/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart create mode 100644 lib/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart diff --git a/lib/modules/alarm/di/alarm_types_di.dart b/lib/modules/alarm/di/alarm_types_di.dart index 084ed4c3..dfadbf10 100644 --- a/lib/modules/alarm/di/alarm_types_di.dart +++ b/lib/modules/alarm/di/alarm_types_di.dart @@ -50,6 +50,7 @@ class AlarmTypesDi { () => AlarmTypesBloc( paginationRepository: locator(), fetchAlarmTypesUseCase: locator(), + filtersService: locator(), ), ); }, diff --git a/lib/modules/alarm/di/alarms_di.dart b/lib/modules/alarm/di/alarms_di.dart index eaffe2a0..cfb0a667 100644 --- a/lib/modules/alarm/di/alarms_di.dart +++ b/lib/modules/alarm/di/alarms_di.dart @@ -9,6 +9,8 @@ import 'package:thingsboard_app/modules/alarm/domain/pagination/alarms/alarms_qu import 'package:thingsboard_app/modules/alarm/domain/repository/alarms/i_alarms_repository.dart'; import 'package:thingsboard_app/modules/alarm/domain/usecases/alarms/fetch_alarms_usecase.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarms_bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/alarm_filters_service.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart'; import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/pagination_repository.dart'; @@ -50,6 +52,10 @@ class AlarmsDi { ), ); + locator.registerLazySingleton( + () => AlarmFiltersService(logger: locator()), + ); + locator.registerLazySingleton( () => AlarmBloc( paginationRepository: locator(), diff --git a/lib/modules/alarm/di/assignee_di.dart b/lib/modules/alarm/di/assignee_di.dart index 10ed45b9..c3cd9d64 100644 --- a/lib/modules/alarm/di/assignee_di.dart +++ b/lib/modules/alarm/di/assignee_di.dart @@ -53,6 +53,7 @@ class AssigneeDi { paginationRepository: locator(), fetchAssigneeUseCase: locator(), queryCtrl: locator(), + filtersService: locator(), ), ); }, diff --git a/lib/modules/alarm/domain/entities/alarm_filters_entity.dart b/lib/modules/alarm/domain/entities/alarm_filters_entity.dart index f4f7e99e..b66cb06c 100644 --- a/lib/modules/alarm/domain/entities/alarm_filters_entity.dart +++ b/lib/modules/alarm/domain/entities/alarm_filters_entity.dart @@ -1,20 +1,23 @@ +import 'package:equatable/equatable.dart'; import 'package:thingsboard_app/thingsboard_client.dart'; -class AlarmFiltersEntity { - AlarmFiltersEntity({ +class AlarmFiltersEntity extends Equatable { + const AlarmFiltersEntity({ this.typeList, - this.statusList = const [AlarmSearchStatus.ANY], + this.statusList = const [AlarmSearchStatus.ACTIVE], this.severityList, this.assigneeId, + this.selfAssignee, }); final List? typeList; final List? statusList; final List? severityList; final UserId? assigneeId; + final bool? selfAssignee; factory AlarmFiltersEntity.defaultFilters() { - return AlarmFiltersEntity(); + return const AlarmFiltersEntity(); } factory AlarmFiltersEntity.fromUiFilters({ @@ -30,4 +33,13 @@ class AlarmFiltersEntity { assigneeId: userId != null ? UserId(userId) : null, ); } + + @override + String toString() { + return 'AlarmFiltersEntity(typeList: $typeList, statusList: $statusList, ' + 'severityList: $severityList, assigneeId: $assigneeId)'; + } + + @override + List get props => [typeList, statusList, severityList, assigneeId]; } diff --git a/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart index 75768e71..31f7bc4c 100644 --- a/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart +++ b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart @@ -1,6 +1,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thingsboard_app/modules/alarm/domain/usecases/alarm_types/fetch_alarm_types_usecase.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart'; import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/pagination_repository.dart'; @@ -8,14 +9,14 @@ class AlarmTypesBloc extends Bloc { AlarmTypesBloc({ required this.paginationRepository, required this.fetchAlarmTypesUseCase, + required this.filtersService, }) : super(const AlarmTypesSelectionEmptyState()) { on(_onEvent); } final PaginationRepository paginationRepository; final FetchAlarmTypesUseCase fetchAlarmTypesUseCase; - - final selectedTypes = {}; + final IAlarmFiltersService filtersService; Future _onEvent( AlarmTypesEvent event, @@ -23,23 +24,29 @@ class AlarmTypesBloc extends Bloc { ) async { switch (event) { case AlarmTypesSelectedEvent(): - selectedTypes.add(event.type); + final types = filtersService.getSelectedFilter(Filters.type); + types.add(event.type); + filtersService.setSelectedFilter(Filters.type, data: types); + emit( AlarmTypeSelectedState( - selectedTypes: selectedTypes, - allowToAddMore: selectedTypes.length < + selectedTypes: types, + allowToAddMore: types.length < (paginationRepository.pagingController.itemList?.length ?? 0), ), ); break; case AlarmTypesRemoveSelectedEvent(): - selectedTypes.remove(event.type); - if (selectedTypes.isNotEmpty) { + final types = filtersService.getSelectedFilter(Filters.type); + types.remove(event.type); + filtersService.setSelectedFilter(Filters.type, data: types); + + if (types.isNotEmpty) { emit( AlarmTypeSelectedState( - selectedTypes: selectedTypes, - allowToAddMore: selectedTypes.length < + selectedTypes: types, + allowToAddMore: types.length < (paginationRepository.pagingController.itemList?.length ?? 0), ), ); @@ -49,13 +56,28 @@ class AlarmTypesBloc extends Bloc { break; case AlarmTypesResetEvent(): - selectedTypes.clear(); emit(const AlarmTypesSelectionEmptyState()); break; case AlarmTypesRefreshEvent(): paginationRepository.refresh(); + break; + case AlarmTypesResetUnCommittedChanges(): + final types = filtersService.getSelectedFilter(Filters.type); + + if (types.isNotEmpty) { + emit( + AlarmTypeSelectedState( + selectedTypes: types, + allowToAddMore: types.length < + (paginationRepository.pagingController.itemList?.length ?? 0), + ), + ); + } else { + emit(const AlarmTypesSelectionEmptyState()); + } + break; } } diff --git a/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart index d821c5d2..00019b48 100644 --- a/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart +++ b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart @@ -32,3 +32,7 @@ final class AlarmTypesResetEvent extends AlarmTypesEvent { final class AlarmTypesRefreshEvent extends AlarmTypesEvent { const AlarmTypesRefreshEvent(); } + +final class AlarmTypesResetUnCommittedChanges extends AlarmTypesEvent { + const AlarmTypesResetUnCommittedChanges(); +} diff --git a/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart b/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart index 39b5a5e0..6423e91b 100644 --- a/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart @@ -4,6 +4,7 @@ import 'package:thingsboard_app/modules/alarm/domain/pagination/assignee/assigne import 'package:thingsboard_app/modules/alarm/domain/usecases/assignee/fetch_assignee_usecase.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_event.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_state.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart'; import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/pagination_repository.dart'; @@ -12,6 +13,7 @@ class AssigneeBloc extends Bloc { required this.paginationRepository, required this.fetchAssigneeUseCase, required this.queryCtrl, + required this.filtersService, }) : super(const AssigneeEmptyState()) { on(_onEvent); } @@ -19,8 +21,7 @@ class AssigneeBloc extends Bloc { final PaginationRepository paginationRepository; final FetchAssigneeUseCase fetchAssigneeUseCase; final AssigneeQueryCtrl queryCtrl; - - String? selectedUserId; + final IAlarmFiltersService filtersService; Future _onEvent( AssigneeEvent event, @@ -28,7 +29,7 @@ class AssigneeBloc extends Bloc { ) async { switch (event) { case AssigneeSelectedEvent(): - selectedUserId = event.userId; + filtersService.setSelectedFilter(Filters.assignee, data: event.userId); queryCtrl.onSearchText(null); final assignee = @@ -50,7 +51,7 @@ class AssigneeBloc extends Bloc { break; case AssigneeResetEvent(): - selectedUserId = null; + filtersService.setSelectedFilter(Filters.assignee, data: null); emit(const AssigneeEmptyState()); queryCtrl.onSearchText(null); @@ -63,6 +64,15 @@ class AssigneeBloc extends Bloc { case AssigneeResetSearchTextEvent(): queryCtrl.onSearchText(null); + break; + case AssigneeResetUnCommittedChanges(): + final assignee = filtersService.getSelectedFilter(Filters.assignee); + if (assignee != null) { + add(AssigneeSelectedEvent(userId: assignee)); + } else { + emit(const AssigneeEmptyState()); + } + break; } } diff --git a/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart b/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart index 3e97a4bd..f9562256 100644 --- a/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart @@ -40,3 +40,7 @@ final class AssigneeResetEvent extends AssigneeEvent { final class AssigneeRefreshEvent extends AssigneeEvent { const AssigneeRefreshEvent(); } + +final class AssigneeResetUnCommittedChanges extends AssigneeEvent { + const AssigneeResetUnCommittedChanges(); +} diff --git a/lib/modules/alarm/presentation/bloc/filters/alarm_filters_service.dart b/lib/modules/alarm/presentation/bloc/filters/alarm_filters_service.dart new file mode 100644 index 00000000..f50ec907 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/filters/alarm_filters_service.dart @@ -0,0 +1,155 @@ +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/alarm_filters_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/filter_data_entity.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/alarm_assignee_filter.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/alarm_severity_filter.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/alarm_status_filter.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/alarm_type_filter.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmFiltersService implements IAlarmFiltersService { + AlarmFiltersService({required this.logger}) + : statusFilter = AlarmStatusFilter( + logger: logger, + initiallySelected: _alarmStatus.first, + ), + severityFilter = AlarmSeverityFilter( + logger: logger, + ), + typeFilter = AlarmTypeFilter( + logger: logger, + ), + assigneeFilter = AlarmAssigneeFilter( + logger: logger, + ); + + static const _alarmStatus = [ + FilterDataEntity(label: 'Active', data: AlarmSearchStatus.ACTIVE), + FilterDataEntity(label: 'Cleared', data: AlarmSearchStatus.CLEARED), + FilterDataEntity(label: 'Acknowledged', data: AlarmSearchStatus.ACK), + FilterDataEntity(label: 'Unacknowledged', data: AlarmSearchStatus.UNACK), + ]; + + static const _alarmSeverity = [ + FilterDataEntity(label: 'Critical', data: AlarmSeverity.CRITICAL), + FilterDataEntity(label: 'Major', data: AlarmSeverity.MAJOR), + FilterDataEntity(label: 'Minor', data: AlarmSeverity.MINOR), + FilterDataEntity(label: 'Warning', data: AlarmSeverity.WARNING), + FilterDataEntity(label: 'Indeterminate', data: AlarmSeverity.INDETERMINATE), + ]; + + @override + List get statuses => _alarmStatus; + + @override + List get severities => _alarmSeverity; + + AlarmFiltersEntity _activeFilters = AlarmFiltersEntity.defaultFilters(); + + final TbLogger logger; + late final IAlarmFilter statusFilter; + late final IAlarmFilter severityFilter; + late final IAlarmFilter typeFilter; + late final IAlarmFilter assigneeFilter; + + @override + AlarmFiltersEntity getCommittedFilters() { + logger.debug( + 'AlarmFiltersService::getCommittedFilterOrNull() -> $_activeFilters', + ); + + return _activeFilters; + } + + @override + void reset() { + logger.debug('AlarmFiltersService::reset()'); + _activeFilters = AlarmFiltersEntity.defaultFilters(); + + statusFilter.reset(); + severityFilter.reset(); + typeFilter.reset(); + assigneeFilter.reset(); + } + + @override + void commitChanges() { + logger.debug('AlarmFiltersService::commitChanges()'); + + final filters = AlarmFiltersEntity.fromUiFilters( + typeList: typeFilter.getSelectedFilterData().toList(), + status: statusFilter + .getSelectedFilterData() + .map((e) => e.data) + .toList() + .cast(), + severity: severityFilter + .getSelectedFilterData() + .map((e) => e.data) + .toList() + .cast(), + userId: assigneeFilter.getSelectedFilterData(), + ); + + _activeFilters = filters; + } + + @override + void resetUnCommittedChanges() { + logger.debug('AlarmFiltersService::resetUnCommittedChanges()'); + + final committedStatus = _activeFilters.statusList + ?.map((e) => statuses.firstWhere((s) => e == s.data)) + .toSet(); + final committedSeverity = _activeFilters.severityList + ?.map((e) => severities.firstWhere((s) => e == s.data)) + .toSet(); + final committedAssignee = _activeFilters.assigneeId; + final committedTypes = _activeFilters.typeList; + + setSelectedFilter(Filters.status, data: committedStatus ?? {}); + setSelectedFilter(Filters.severity, data: committedSeverity ?? {}); + setSelectedFilter(Filters.assignee, data: committedAssignee?.id); + setSelectedFilter(Filters.type, data: committedTypes ?? []); + } + + @override + T getSelectedFilter(Filters type) { + switch (type) { + case Filters.status: + return statusFilter.getSelectedFilterData(); + + case Filters.severity: + return severityFilter.getSelectedFilterData(); + + case Filters.type: + return typeFilter.getSelectedFilterData(); + + case Filters.assignee: + return assigneeFilter.getSelectedFilterData(); + } + } + + @override + void setSelectedFilter(Filters type, {required data}) { + switch (type) { + case Filters.status: + statusFilter.updateSelectedData(data); + break; + + case Filters.severity: + severityFilter.updateSelectedData(data); + break; + + case Filters.type: + typeFilter.updateSelectedData(data); + break; + + case Filters.assignee: + assigneeFilter.updateSelectedData(data); + break; + } + } +} diff --git a/lib/modules/alarm/presentation/bloc/filters/filters/alarm_assignee_filter.dart b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_assignee_filter.dart new file mode 100644 index 00000000..57fa3659 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_assignee_filter.dart @@ -0,0 +1,34 @@ +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart'; + +class AlarmAssigneeFilter implements IAlarmFilter { + AlarmAssigneeFilter({required this.logger, T? initiallySelected}) { + selectedUsed = initiallySelected; + } + + T? selectedUsed; + final TbLogger logger; + + @override + T? getSelectedFilterData() { + logger.debug( + 'AlarmAssigneeFilter::getSelectedFilterData() -> $selectedUsed', + ); + + return selectedUsed; + } + + @override + void updateSelectedData(data) { + logger.debug( + 'AlarmAssigneeFilter::updateSelectedData($data)', + ); + + selectedUsed = data; + } + + @override + void reset() { + selectedUsed = null; + } +} diff --git a/lib/modules/alarm/presentation/bloc/filters/filters/alarm_severity_filter.dart b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_severity_filter.dart new file mode 100644 index 00000000..5c059971 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_severity_filter.dart @@ -0,0 +1,38 @@ +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart'; + +class AlarmSeverityFilter implements IAlarmFilter { + AlarmSeverityFilter({required this.logger, T? initiallySelected}) { + if (initiallySelected != null) { + alarmSeveritySelected.add(initiallySelected); + } + } + + final alarmSeveritySelected = {}; + final TbLogger logger; + + @override + Set getSelectedFilterData() { + logger.debug( + 'AlarmSeverityFilter::getSelectedFilterData() -> $alarmSeveritySelected', + ); + + return Set.of(alarmSeveritySelected); + } + + @override + void updateSelectedData(data) { + logger.debug( + 'AlarmStatusFilter::updateSelectedData($data)', + ); + + alarmSeveritySelected + ..clear() + ..addAll(data); + } + + @override + void reset() { + alarmSeveritySelected.clear(); + } +} diff --git a/lib/modules/alarm/presentation/bloc/filters/filters/alarm_status_filter.dart b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_status_filter.dart new file mode 100644 index 00000000..ba66747c --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_status_filter.dart @@ -0,0 +1,42 @@ +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart'; + +class AlarmStatusFilter implements IAlarmFilter { + AlarmStatusFilter({required this.logger, this.initiallySelected}) { + if (initiallySelected != null) { + alarmStatusSelected.add(initiallySelected as T); + } + } + + final alarmStatusSelected = {}; + final TbLogger logger; + T? initiallySelected; + + @override + Set getSelectedFilterData() { + logger.debug( + 'AlarmStatusFilter::getSelectedFilterData() -> $alarmStatusSelected', + ); + + return Set.of(alarmStatusSelected); + } + + @override + void updateSelectedData(data) { + logger.debug( + 'AlarmStatusFilter::updateSelectedData($data)', + ); + + alarmStatusSelected + ..clear() + ..addAll(data); + } + + @override + void reset() { + alarmStatusSelected.clear(); + if (initiallySelected != null) { + alarmStatusSelected.add(initiallySelected as T); + } + } +} diff --git a/lib/modules/alarm/presentation/bloc/filters/filters/alarm_type_filter.dart b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_type_filter.dart new file mode 100644 index 00000000..d55f6d2d --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/filters/filters/alarm_type_filter.dart @@ -0,0 +1,38 @@ +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart'; + +class AlarmTypeFilter implements IAlarmFilter { + AlarmTypeFilter({required this.logger, T? initiallySelected}) { + if (initiallySelected != null) { + alarmTypeSelected.add(initiallySelected); + } + } + + final alarmTypeSelected = {}; + final TbLogger logger; + + @override + Set getSelectedFilterData() { + logger.debug( + 'AlarmTypeFilter::getSelectedFilterData() -> $alarmTypeSelected', + ); + + return Set.of(alarmTypeSelected); + } + + @override + void updateSelectedData(data) { + logger.debug( + 'AlarmTypeFilter::updateSelectedData($data)', + ); + + alarmTypeSelected + ..clear() + ..addAll(data); + } + + @override + void reset() { + alarmTypeSelected.clear(); + } +} diff --git a/lib/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart b/lib/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart new file mode 100644 index 00000000..c6178a7c --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/filters/filters/i_alarm_filter.dart @@ -0,0 +1,7 @@ +abstract interface class IAlarmFilter { + T getSelectedFilterData(); + + void updateSelectedData(T data); + + void reset(); +} diff --git a/lib/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart b/lib/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart new file mode 100644 index 00000000..1f9fe853 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart @@ -0,0 +1,22 @@ +import 'package:thingsboard_app/modules/alarm/domain/entities/alarm_filters_entity.dart'; +import 'package:thingsboard_app/modules/alarm/domain/entities/filter_data_entity.dart'; + +enum Filters { status, severity, type, assignee } + +abstract interface class IAlarmFiltersService { + List get statuses; + + List get severities; + + AlarmFiltersEntity getCommittedFilters(); + + void commitChanges(); + + void reset(); + + void resetUnCommittedChanges(); + + T getSelectedFilter(Filters type); + + void setSelectedFilter(Filters type, {required T data}); +} diff --git a/lib/modules/alarm/presentation/view/alarms_filter_page.dart b/lib/modules/alarm/presentation/view/alarms_filter_page.dart index c1f7b2d3..01603041 100644 --- a/lib/modules/alarm/presentation/view/alarms_filter_page.dart +++ b/lib/modules/alarm/presentation/view/alarms_filter_page.dart @@ -6,18 +6,17 @@ import 'package:preload_page_view/preload_page_view.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/locator.dart'; -import 'package:thingsboard_app/modules/alarm/domain/entities/alarm_filters_entity.dart'; import 'package:thingsboard_app/modules/alarm/domain/entities/filter_data_entity.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/assignee/assignee_event.dart'; import 'package:thingsboard_app/modules/alarm/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/modules/alarm/presentation/bloc/filters/i_alarm_filters_service.dart'; import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_control_filters_button.dart'; import 'package:thingsboard_app/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart'; import 'package:thingsboard_app/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart'; import 'package:thingsboard_app/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart'; -import 'package:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class AlarmsFilterPage extends TbContextWidget { @@ -36,35 +35,12 @@ class AlarmsFilterPage extends TbContextWidget { } class _AlarmsFilterPageState extends TbContextState { - final alarmStatus = const [ - FilterDataEntity(label: 'Active', data: AlarmSearchStatus.ACTIVE), - FilterDataEntity(label: 'Cleared', data: AlarmSearchStatus.CLEARED), - FilterDataEntity(label: 'Acknowledged', data: AlarmSearchStatus.ACK), - FilterDataEntity(label: 'Unacknowledged', data: AlarmSearchStatus.UNACK), - ]; - - final alarmSeverity = const [ - FilterDataEntity(label: 'Critical', data: AlarmSeverity.CRITICAL), - FilterDataEntity(label: 'Major', data: AlarmSeverity.MAJOR), - FilterDataEntity(label: 'Minor', data: AlarmSeverity.MINOR), - FilterDataEntity(label: 'Warning', data: AlarmSeverity.WARNING), - FilterDataEntity(label: 'Indeterminate', data: AlarmSeverity.INDETERMINATE), - ]; - - final alarmStatusSelected = []; - final alarmSeveritySelected = []; + late final IAlarmFiltersService filtersService; /// This flag indicates that the user has made changes to the filters. /// For example, selecting a status, assignee, or any other filter option. bool filtersChanged = false; - /// This flag is used to indicate that the user has changed and applied filters by clicking the Update button. - /// It helps determine whether to reset filter changes when the user clicks the back or cancel button. - /// Logic: - /// -- If the filters were changed but not applied, clicking the back or cancel button will discard the changes. - /// -- If the filters were changed and applied, the filters will be preserved as they are. - bool filtersChangesApplied = false; - late final StreamSubscription listenNavigationChanges; @override @@ -100,38 +76,49 @@ class _AlarmsFilterPageState extends TbContextState { crossAxisAlignment: CrossAxisAlignment.start, children: [ FilterToggleBlockWidget( - key: ValueKey(alarmStatusSelected.length), + key: ValueKey( + filtersService.getSelectedFilter(Filters.status), + ), label: 'Alarm status list', - items: alarmStatus, - selected: alarmStatusSelected.toSet(), + items: filtersService.statuses, + selected: + filtersService.getSelectedFilter(Filters.status), onSelectedChanged: (values) { - alarmStatusSelected - ..clear() - ..addAll(values.cast()); + filtersService.setSelectedFilter( + Filters.status, + data: values.cast(), + ); setState(() { filtersChanged = true; }); }, - labelAtIndex: (index) => alarmStatus[index].label, + labelAtIndex: (index) => + filtersService.statuses[index].label, ), Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: FilterToggleBlockWidget( - key: ValueKey(alarmSeveritySelected.length), + key: ValueKey( + filtersService + .getSelectedFilter(Filters.severity), + ), label: 'Alarm severity list', - items: alarmSeverity, - selected: alarmSeveritySelected.toSet(), + items: filtersService.severities, + selected: filtersService + .getSelectedFilter(Filters.severity), onSelectedChanged: (values) { - alarmSeveritySelected - ..clear() - ..addAll(values.cast()); + filtersService.setSelectedFilter( + Filters.severity, + data: values.cast(), + ); setState(() { filtersChanged = true; }); }, - labelAtIndex: (index) => alarmSeverity[index].label, + labelAtIndex: (index) => + filtersService.severities[index].label, ), ), AlarmTypesWidget( @@ -162,24 +149,12 @@ class _AlarmsFilterPageState extends TbContextState { }, onUpdateTap: filtersChanged ? () { - final filters = AlarmFiltersEntity.fromUiFilters( - typeList: getIt() - .selectedTypes - .toList(), - status: alarmStatusSelected - .map((e) => e.data) - .toList() - .cast(), - severity: alarmSeveritySelected - .map((e) => e.data) - .toList() - .cast(), - userId: getIt().selectedUserId, - ); + filtersService.commitChanges(); getIt().add( AlarmFiltersUpdateEvent( - filtersEntity: filters, + filtersEntity: + filtersService.getCommittedFilters(), ), ); @@ -188,10 +163,6 @@ class _AlarmsFilterPageState extends TbContextState { duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, ); - - setState(() { - filtersChangesApplied = true; - }); } : null, ), @@ -207,7 +178,8 @@ class _AlarmsFilterPageState extends TbContextState { @override void initState() { - alarmStatusSelected.add(alarmStatus.first); + filtersService = getIt(); + listenNavigationChanges = widget .tbContext.bottomNavigationTabChangedStream.stream .listen((tabIndex) { @@ -220,18 +192,13 @@ class _AlarmsFilterPageState extends TbContextState { @override void dispose() { listenNavigationChanges.cancel(); - super.dispose(); } void _resetFilters() { setState(() { - alarmStatusSelected - ..clear() - ..add(alarmStatus.first); - alarmSeveritySelected.clear(); + filtersService.reset(); filtersChanged = false; - filtersChangesApplied = false; }); getIt().add( @@ -246,14 +213,22 @@ class _AlarmsFilterPageState extends TbContextState { } void _onBackButtonClick() { - widget.pageController.animateToPage( + widget.pageController + .animateToPage( 0, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, - ); - - if (filtersChanged && !filtersChangesApplied) { - _resetFilters(); - } + ) + .then((_) { + if (mounted && filtersChanged) { + setState(() { + filtersService.resetUnCommittedChanges(); + + getIt() + .add(const AlarmTypesResetUnCommittedChanges()); + getIt().add(const AssigneeResetUnCommittedChanges()); + }); + } + }); } } From 6b950078c094bd9830b5632bcf25baab4d83373e Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 30 Aug 2024 17:13:53 +0300 Subject: [PATCH 48/61] Passing kDebugMode to see a request/response logs. --- lib/core/context/tb_context.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index c28c8f74..18868899 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -100,6 +100,7 @@ class TbContext implements PopEntry { onLoadStarted: onLoadStarted, onLoadFinished: onLoadFinished, computeFunc: (callback, message) => compute(callback, message), + debugMode: kDebugMode, ); oauth2Client = TbOAuth2Client( @@ -148,6 +149,7 @@ class TbContext implements PopEntry { onLoadStarted: onLoadStarted, onLoadFinished: onLoadFinished, computeFunc: (callback, message) => compute(callback, message), + debugMode: kDebugMode, ); oauth2Client = TbOAuth2Client( From 12b4f0509f445330f18cb05ac210a97971eeb69a Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 9 Sep 2024 11:02:43 +0300 Subject: [PATCH 49/61] Fixed an issue the application minimizes after the user clicks the back button on the dashboard's screen. --- lib/modules/dashboard/main_dashboard_page.dart | 8 ++++---- .../presentation/view/fullscreen_dashboard_page.dart | 8 +------- .../dashboard/presentation/widgets/dashboard_widget.dart | 3 ++- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/modules/dashboard/main_dashboard_page.dart b/lib/modules/dashboard/main_dashboard_page.dart index 9b5bdc3e..a6272dd9 100644 --- a/lib/modules/dashboard/main_dashboard_page.dart +++ b/lib/modules/dashboard/main_dashboard_page.dart @@ -11,11 +11,11 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class MainDashboardPage extends TbContextWidget { MainDashboardPage( TbContext tbContext, { - this.controller, + required this.controller, super.key, }) : super(tbContext); - final DashboardPageController? controller; + final DashboardPageController controller; @override State createState() => _MainDashboardPageState(); @@ -80,7 +80,7 @@ class _MainDashboardPageState extends TbContextState pageController: widget.controller, controllerCallback: (controller) { _dashboardController = controller; - widget.controller?.setDashboardController(controller); + widget.controller.setDashboardController(controller); controller.hasRightLayout.addListener(() { hasRightLayout.value = controller.hasRightLayout.value; @@ -112,7 +112,7 @@ class _MainDashboardPageState extends TbContextState parent: rightLayoutMenuController, ); - widget.controller?.setDashboardTitleNotifier(dashboardTitleValue); + widget.controller.setDashboardTitleNotifier(dashboardTitleValue); } @override diff --git a/lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart b/lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart index 0b10a643..24bdacb7 100644 --- a/lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart +++ b/lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart @@ -47,13 +47,7 @@ class _FullscreenDashboardPageState builder: (context, canGoBack, widget) { return TbAppBar( tbContext, - leading: canGoBack - ? BackButton( - onPressed: () { - maybePop(); - }, - ) - : null, + leading: canGoBack ? BackButton(onPressed: maybePop) : null, showLoadingIndicator: false, elevation: 1, shadowColor: Colors.transparent, diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart index 9f225f31..111d98ab 100644 --- a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -74,7 +74,8 @@ class _DashboardState extends State { widget.pageController?.closeDashboard().then( (_) => dashboardLoading.value = true, ); - return true; + + return false; } return true; From 592a12991ed660142e81908479a1c44096b751a7 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 10 Sep 2024 09:51:31 +0300 Subject: [PATCH 50/61] Fixed the application minimizes after the user clicks the back button on the dashboard's screen. --- .../dashboard/presentation/widgets/dashboard_widget.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart index 111d98ab..d030c86f 100644 --- a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -74,11 +74,9 @@ class _DashboardState extends State { widget.pageController?.closeDashboard().then( (_) => dashboardLoading.value = true, ); - - return false; } - return true; + return false; }, child: Stack( children: [ From 3249482b3426048f6d4115260d3eab24776fe51b Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 10 Sep 2024 14:49:08 +0300 Subject: [PATCH 51/61] Fixed the application minimizes after the user clicks the back button on the dashboard's screen. --- .../presentation/widgets/dashboard_widget.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart index d030c86f..8e4702cf 100644 --- a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -71,9 +71,13 @@ class _DashboardState extends State { if (await controller?.canGoBack() == true) { await controller?.goBack(); } else { - widget.pageController?.closeDashboard().then( - (_) => dashboardLoading.value = true, - ); + if (widget.pageController != null) { + widget.pageController?.closeDashboard().then( + (_) => dashboardLoading.value = true, + ); + } else { + return true; + } } return false; From 0a9669e09e157b9f18daa6fcc9f9505a35fbbcde Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 10 Sep 2024 16:09:43 +0300 Subject: [PATCH 52/61] When error happened during the dashboard loading the mobile will hide the loading spinner. --- .../dashboard/presentation/widgets/dashboard_widget.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart index 8e4702cf..eca3b25a 100644 --- a/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -235,6 +235,10 @@ class _DashboardState extends State { '[JavaScript console] ${consoleMessage.messageLevel}: ' '${consoleMessage.message}', ); + + if (dashboardLoading.value) { + dashboardLoading.value = false; + } }, onLoadStart: (controller, url) async { log.debug('onLoadStart: $url'); From e222e98363b4f6b2e5f991f6e0a70eb8380c4304 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 11 Sep 2024 11:17:54 +0300 Subject: [PATCH 53/61] Fixed an issue after login or switching via QR code, the dashboards isn't loading. --- lib/modules/dashboard/di/dashboards_di.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/dashboard/di/dashboards_di.dart b/lib/modules/dashboard/di/dashboards_di.dart index 11ee343a..f8a96546 100644 --- a/lib/modules/dashboard/di/dashboards_di.dart +++ b/lib/modules/dashboard/di/dashboards_di.dart @@ -20,7 +20,7 @@ abstract class DashboardsDi { () => FetchDashboardsUseCase(tbClient), ); - locator.registerLazySingleton( + locator.registerFactory( () => DashboardsPaginationRepository( queryController: locator(), onFetchData: locator(), From 520fc652bc863913532ea4ee023157f4bc821d65 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Wed, 11 Sep 2024 14:42:53 +0300 Subject: [PATCH 54/61] OAuth providers cannot have a text anymore, just an icon. --- lib/core/auth/login/login_page.dart | 40 +++++------------------------ 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index e87e8d03..90f44858 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -24,12 +24,6 @@ class LoginPage extends TbPageWidget { } class _LoginPageState extends TbPageState { - final ButtonStyle _oauth2ButtonWithTextStyle = OutlinedButton.styleFrom( - padding: const EdgeInsets.all(16), - alignment: Alignment.centerLeft, - foregroundColor: Colors.black87, - ); - final ButtonStyle _oauth2IconButtonStyle = OutlinedButton.styleFrom( padding: const EdgeInsets.all(16), alignment: Alignment.center, @@ -342,7 +336,6 @@ class _LoginPageState extends TbPageState { index, _buildOAuth2Button( client, - clients.length == 2 ? client.name : null, true, index == clients.length - 1, ), @@ -388,7 +381,6 @@ class _LoginPageState extends TbPageState { Widget _buildOAuth2Button( OAuth2ClientInfo client, - String? text, bool expand, bool isLast, ) { @@ -412,32 +404,12 @@ class _LoginPageState extends TbPageState { } } icon ??= Icon(Icons.login, size: 24, color: Theme.of(context).primaryColor); - Widget button; - bool iconOnly = text == null; - if (iconOnly) { - button = OutlinedButton( - style: _oauth2IconButtonStyle, - onPressed: () => _oauth2ButtonPressed(client), - child: icon, - ); - } else { - button = OutlinedButton( - style: _oauth2ButtonWithTextStyle, - onPressed: () => _oauth2ButtonPressed(client), - child: Stack( - children: [ - Align(alignment: Alignment.centerLeft, child: icon), - SizedBox( - height: 24, - child: Align( - alignment: Alignment.center, - child: Text(text, textAlign: TextAlign.center), - ), - ), - ], - ), - ); - } + final button = OutlinedButton( + style: _oauth2IconButtonStyle, + onPressed: () => _oauth2ButtonPressed(client), + child: icon, + ); + if (expand) { return Expanded( child: Padding( From 86974f82a024b6cdd43d58d68cab4c388f4c6ece Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Thu, 12 Sep 2024 11:57:13 +0300 Subject: [PATCH 55/61] Fixed an issue If the user is logged in, but has not yet received a message, the counter is not displayed. --- .../notification/service/notifications_local_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/modules/notification/service/notifications_local_service.dart b/lib/modules/notification/service/notifications_local_service.dart index 111e4ee4..3a037755 100644 --- a/lib/modules/notification/service/notifications_local_service.dart +++ b/lib/modules/notification/service/notifications_local_service.dart @@ -53,6 +53,7 @@ final class NotificationsLocalService implements INotificationsLocalService { @override Future updateNotificationsCount(int count) async { + FlutterAppBadger.updateBadgeCount(count); storage.setItem(notificationCounterKey, count.toString()); notificationsNumberStream.add(count); } From 584b49f1f998f92b78130575e308de6579fc16d4 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 16 Sep 2024 11:07:47 +0300 Subject: [PATCH 56/61] Fixed an issue on the login page when infinite loader appears after unlocking the phone. --- lib/core/auth/login/login_page.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 90f44858..b3968c69 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -23,7 +23,8 @@ class LoginPage extends TbPageWidget { State createState() => _LoginPageState(); } -class _LoginPageState extends TbPageState { +class _LoginPageState extends TbPageState + with WidgetsBindingObserver { final ButtonStyle _oauth2IconButtonStyle = OutlinedButton.styleFrom( padding: const EdgeInsets.all(16), alignment: Alignment.center, @@ -37,6 +38,7 @@ class _LoginPageState extends TbPageState { @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); if (tbClient.isPreVerificationToken()) { SchedulerBinding.instance.addPostFrameCallback((_) { navigateTo('/login/mfa'); @@ -44,6 +46,19 @@ class _LoginPageState extends TbPageState { } } + @override + void dispose() { + super.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _isLoginNotifier.value = false; + } + } + @override Widget build(BuildContext context) { return Scaffold( From 5f739b165b5ced3efac50bf21d472146719d8cb0 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 17 Sep 2024 15:00:13 +0300 Subject: [PATCH 57/61] EntityType translation expanded to prevent crash issue. --- lib/core/entity/entities_base.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart index 55120204..34eaf4ef 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -19,6 +19,7 @@ const entityTypeTranslations = { EntityType.ASSET: 'Asset', EntityType.DEVICE: 'Device', EntityType.DEVICE_PROFILE: 'Device profile', + EntityType.ASSET_PROFILE: 'Asset profile', EntityType.ALARM: 'Alarm', EntityType.RULE_CHAIN: 'Rule chain', EntityType.RULE_NODE: 'Rule node', @@ -29,6 +30,13 @@ const entityTypeTranslations = { EntityType.API_USAGE_STATE: 'Api Usage State', EntityType.TB_RESOURCE: 'Resource', EntityType.OTA_PACKAGE: 'OTA package', + EntityType.RPC: 'RPC', + EntityType.QUEUE: 'Queue', + EntityType.NOTIFICATION_TARGET: 'Notification target', + EntityType.NOTIFICATION_TEMPLATE: 'Notification template', + EntityType.NOTIFICATION_REQUEST: 'Notification request', + EntityType.NOTIFICATION: 'Notification', + EntityType.NOTIFICATION_RULE: 'Notification rule', }; typedef EntityTapFunction = Function(T entity); From a5a3cfa941fb76a9a38b9bc9616835447628cf6d Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Fri, 20 Sep 2024 16:52:53 +0300 Subject: [PATCH 58/61] "Pull to refresh" doesn't work on the "Home" screen. --- lib/modules/dashboard/presentation/widgets/dashboards_grid.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/dashboard/presentation/widgets/dashboards_grid.dart b/lib/modules/dashboard/presentation/widgets/dashboards_grid.dart index 618098c8..0739c5a7 100644 --- a/lib/modules/dashboard/presentation/widgets/dashboards_grid.dart +++ b/lib/modules/dashboard/presentation/widgets/dashboards_grid.dart @@ -28,7 +28,7 @@ class DashboardsGridWidget extends StatelessWidget { Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () async { - getIt().pagingController.refresh(); + getIt().refresh(); }, child: PaginationGridWidget( pagingController: From b5ffa7047cd430c14aff16b13346cba29a245b13 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Mon, 23 Sep 2024 10:45:32 +0300 Subject: [PATCH 59/61] [Mobile] "Pull to refresh" doesn't work on the "Home" screen. --- lib/modules/dashboard/di/dashboards_di.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/dashboard/di/dashboards_di.dart b/lib/modules/dashboard/di/dashboards_di.dart index f8a96546..11ee343a 100644 --- a/lib/modules/dashboard/di/dashboards_di.dart +++ b/lib/modules/dashboard/di/dashboards_di.dart @@ -20,7 +20,7 @@ abstract class DashboardsDi { () => FetchDashboardsUseCase(tbClient), ); - locator.registerFactory( + locator.registerLazySingleton( () => DashboardsPaginationRepository( queryController: locator(), onFetchData: locator(), From b66d854056e804c0690825f2548393dd65bbb078 Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 24 Sep 2024 10:43:07 +0300 Subject: [PATCH 60/61] Fixed: After login or switching via QR code, the dashboards isn't loading. --- lib/modules/dashboard/di/dashboards_di.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/modules/dashboard/di/dashboards_di.dart b/lib/modules/dashboard/di/dashboards_di.dart index 11ee343a..7ffc24e7 100644 --- a/lib/modules/dashboard/di/dashboards_di.dart +++ b/lib/modules/dashboard/di/dashboards_di.dart @@ -31,7 +31,6 @@ abstract class DashboardsDi { } static void dispose(String scopeName) { - getIt().dispose(); getIt.dropScope(scopeName); } } From c1cac7f901dc810f1d207f273e206afb4c257f5c Mon Sep 17 00:00:00 2001 From: Yevhen Beshkarov Date: Tue, 24 Sep 2024 14:21:20 +0300 Subject: [PATCH 61/61] 1.3.0 release. --- android/build.gradle | 2 +- ios/Podfile.lock | 110 +++-- ios/Runner.xcodeproj/project.pbxproj | 20 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- pubspec.lock | 382 ++++++++++++++---- pubspec.yaml | 4 +- 6 files changed, 380 insertions(+), 140 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 9065050a..a3beefd2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.9.0' repositories { google() jcenter() diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 35007fd7..518902d6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,38 +1,38 @@ PODS: - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (10.29.0): - - FirebaseCore (= 10.29.0) - - Firebase/Messaging (10.29.0): + - Firebase/CoreOnly (11.0.0): + - FirebaseCore (= 11.0.0) + - Firebase/Messaging (11.0.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.29.0) - - firebase_core (3.3.0): - - Firebase/CoreOnly (= 10.29.0) + - FirebaseMessaging (~> 11.0.0) + - firebase_core (3.5.0): + - Firebase/CoreOnly (= 11.0.0) - Flutter - - firebase_messaging (15.0.4): - - Firebase/Messaging (= 10.29.0) + - firebase_messaging (15.1.2): + - Firebase/Messaging (= 11.0.0) - firebase_core - Flutter - - FirebaseCore (10.29.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.29.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.29.0): - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.29.0): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.3) - - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Reachability (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseCore (11.0.0): + - FirebaseCoreInternal (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreInternal (11.1.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.1.0): + - FirebaseCore (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.0.0): + - FirebaseCore (~> 11.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) - Flutter (1.0.0) - flutter_app_badger (1.3.0): - Flutter @@ -49,43 +49,41 @@ PODS: - Flutter - geolocator_apple (1.2.0): - Flutter - - GoogleDataTransport (9.4.1): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.13.3): + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Environment (8.0.2): - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.3): + - GoogleUtilities/Network (8.0.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.3)": + - "GoogleUtilities/NSData+zlib (8.0.2)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/Reachability (7.13.3): + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - image_picker_ios (0.0.1): - Flutter - MTBBarcodeScanner (5.0.11) - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter @@ -166,24 +164,24 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d - Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d - firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb - firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757 - FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 - FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 - FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd - FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 + Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 + firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56 + firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50 + FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 + FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c + FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 + FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_app_badger: b87fc231847b03b92ce1412aa351842e7e97932f flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 - GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb - nanopb: 438bc412db1928dac798aa6fd75726007be04262 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5ae1bbf6..cfaaa686 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 675E9C2D528E6C8211A6752E /* [CP] Embed Pods Frameworks */, + 34A2190E284CB469F0491F0D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -166,7 +167,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -208,6 +209,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 34A2190E284CB469F0491F0D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b52b2e69..e67b2808 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3b4b76e5..971ebe54 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter ThingsBoard Mobile Application publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.2.0 +version: 1.3.0 environment: sdk: ">=3.2.0 <4.0.0" @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - thingsboard_client: ^1.2.1 + thingsboard_client: ^1.3.0 intl: ^0.19.0 flutter_secure_storage: ^9.0.0 flutter_speed_dial: ^7.0.0