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/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/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/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 @@ 10.18.0) - - firebase_core (2.24.2): - - Firebase/CoreOnly (= 10.18.0) + - FirebaseMessaging (~> 11.0.0) + - firebase_core (3.5.0): + - Firebase/CoreOnly (= 11.0.0) - Flutter - - firebase_messaging (14.7.10): - - Firebase/Messaging (= 10.18.0) + - firebase_messaging (15.1.2): + - Firebase/Messaging (= 11.0.0) - firebase_core - Flutter - - FirebaseCore (10.18.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.20.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.20.0): - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.18.0): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.2) - - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Reachability (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - nanopb (< 2.30910.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_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,45 +49,53 @@ PODS: - Flutter - geolocator_apple (1.2.0): - Flutter - - GoogleDataTransport (9.3.0): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.12.0): + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.12.0): - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.12.0)" - - GoogleUtilities/Reachability (7.12.0): + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.0.2): - 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 (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 - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - PromisesObjC (2.3.1) + - PromisesObjC (2.4.0) - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner + - uni_links (0.0.1): + - Flutter - url_launcher_ios (0.0.1): - Flutter @@ -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`) @@ -102,6 +113,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: @@ -127,8 +139,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: @@ -143,35 +157,39 @@ 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: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 - firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5 - firebase_messaging: 90e8a6db84b6e1e876cebce4f30f01dc495e7014 - FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f - FirebaseCoreInternal: efeeb171ac02d623bdaefe121539939821e10811 - FirebaseInstallations: 558b1da7d65afeb996fd5c814332f013234ece4e - FirebaseMessaging: 9bc34a98d2e0237e1b121915120d4d48ddcf301e + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 + firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56 + firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50 + FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 + FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c + FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 + FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 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: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb - nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b -PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 +PODFILE CHECKSUM: 8a4b2eabe1a99180e46baba5a37492de09c643c4 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 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 @@ > 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..b3968c69 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -4,33 +4,31 @@ 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:thingsboard_app/generated/l10n.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'; 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); - +class _LoginPageState extends TbPageState + with WidgetsBindingObserver { 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); @@ -40,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'); @@ -50,248 +49,287 @@ 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( - 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 { + FocusScope.of(context).unfocus(); + 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) { @@ -313,7 +351,6 @@ class _LoginPageState extends TbPageState { index, _buildOAuth2Button( client, - clients.length == 2 ? client.name : null, true, index == clients.length - 1, ), @@ -326,6 +363,7 @@ class _LoginPageState extends TbPageState { child: OutlinedButton( style: _oauth2IconButtonStyle, onPressed: () async { + FocusScope.of(context).unfocus(); try { final barcode = await tbContext.navigateTo( '/qrCodeScan', @@ -351,18 +389,23 @@ class _LoginPageState extends TbPageState { ), ), ], - ) + ), ], ); } Widget _buildOAuth2Button( - OAuth2ClientInfo client, String? text, bool expand, bool isLast) { + OAuth2ClientInfo client, + 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 +418,20 @@ class _LoginPageState extends TbPageState { } } } - if (icon == null) { - 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), - Container( - height: 24, - child: Align( - alignment: Alignment.center, - child: Text(text, textAlign: TextAlign.center)), - ) - ])); - } + icon ??= Icon(Icons.login, size: 24, color: Theme.of(context).primaryColor); + final button = OutlinedButton( + style: _oauth2IconButtonStyle, + onPressed: () => _oauth2ButtonPressed(client), + child: icon, + ); + 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), @@ -414,12 +441,16 @@ 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); 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 +472,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..07d790e6 100644 --- a/lib/core/auth/login/reset_password_request_page.dart +++ b/lib/core/auth/login/reset_password_request_page.dart @@ -4,16 +4,15 @@ 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'; 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..19e99583 100644 --- a/lib/core/auth/login/two_factor_authentication_page.dart +++ b/lib/core/auth/login/two_factor_authentication_page.dart @@ -1,19 +1,21 @@ 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:thingsboard_app/thingsboard_client.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; -import 'package:thingsboard_app/generated/l10n.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/data/datasource/remote/i_noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart index a22b3383..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({ @@ -18,4 +18,8 @@ abstract interface class INoAuthRemoteDatasource { }); bool isAuthenticated(); + + AuthUser getAuthUserFromJwt(String jwt); + + AuthUser? getCurrentlyAuthenticatedUserOrNull(); } 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 4d9f9aa3..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({ @@ -71,4 +71,14 @@ class NoAuthRemoteDatasource implements INoAuthRemoteDatasource { bool isAuthenticated() { return tbContext.isAuthenticated; } + + @override + AuthUser getAuthUserFromJwt(String jwt) { + return thingsboardClient.getAuthUserFromJwt(jwt); + } + + @override + AuthUser? getCurrentlyAuthenticatedUserOrNull() { + return thingsboardClient.getAuthUser(); + } } diff --git a/lib/core/auth/noauth/data/repository/noauth_repository.dart b/lib/core/auth/noauth/data/repository/noauth_repository.dart index 3e4bc990..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}); @@ -50,4 +50,14 @@ class NoAuthRepository implements INoAuthRepository { bool isAuthenticated() { return remoteDatasource.isAuthenticated(); } + + @override + AuthUser getAuthUserFromJwt(String jwt) { + return remoteDatasource.getAuthUserFromJwt(jwt); + } + + @override + AuthUser? getCurrentlyAuthenticatedUserOrNull() { + return remoteDatasource.getCurrentlyAuthenticatedUserOrNull(); + } } 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 03db7f26..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({ @@ -18,4 +18,8 @@ abstract interface class INoAuthRepository { }); bool isAuthenticated(); + + AuthUser getAuthUserFromJwt(String jwt); + + AuthUser? getCurrentlyAuthenticatedUserOrNull(); } 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 1641e4a1..0a2dc553 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({ @@ -50,6 +50,14 @@ class SwitchEndpointUseCase extends UseCase { _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() + 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/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart index 808888fb..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,12 +11,12 @@ import 'package:thingsboard_app/locator.dart'; class SwitchEndpointNoAuthView extends TbPageWidget { SwitchEndpointNoAuthView({ - required this.tbContext, + required TbContext tbContext, required this.arguments, + super.key, }) : super(tbContext); final Map? arguments; - final TbContext tbContext; @override State createState() => _SwitchEndpointNoAuthViewState(); @@ -109,7 +109,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 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/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 5d197858..6e3d9868 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -12,40 +12,20 @@ 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/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'; 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'; -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}); +part 'has_tb_context.dart'; - Future closeDashboard({bool animate}); - - bool isDashboardOpen(); - - Future dashboardGoBack(); -} +enum NotificationType { info, warn, success, error } class TbContext implements PopEntry { static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); @@ -59,17 +39,17 @@ 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; - TbMainDashboardHolder? _mainDashboardHolder; - bool _closeMainFirst = false; StreamSubscription? _appLinkStreamSubscription; late bool _handleRootState; + @override final ValueNotifier canPopNotifier = ValueNotifier(false); + @override PopInvokedCallback get onPopInvoked => onPopInvokedImpl; GlobalKey messengerKey = @@ -97,6 +77,8 @@ class TbContext implements PopEntry { WidgetActionHandler get widgetActionHandler => _widgetActionHandler; + final bottomNavigationTabChangedStream = StreamController.broadcast(); + Future init() async { assert(() { if (_initialized) { @@ -118,6 +100,7 @@ class TbContext implements PopEntry { onLoadStarted: onLoadStarted, onLoadFinished: onLoadFinished, computeFunc: (callback, message) => compute(callback, message), + debugMode: kDebugMode, ); oauth2Client = TbOAuth2Client( @@ -166,6 +149,7 @@ class TbContext implements PopEntry { onLoadStarted: onLoadStarted, onLoadFinished: onLoadFinished, computeFunc: (callback, message) => compute(callback, message), + debugMode: kDebugMode, ); oauth2Client = TbOAuth2Client( @@ -177,15 +161,11 @@ 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.') : '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(); } @@ -211,23 +191,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 +250,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 +321,7 @@ class TbContext implements PopEntry { replace: true, clearStack: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), ); } } @@ -351,13 +335,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 +391,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 +418,7 @@ class TbContext implements PopEntry { '/home', replace: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), ); } } else { @@ -441,7 +427,7 @@ class TbContext implements PopEntry { replace: true, clearStack: true, transition: TransitionType.fadeIn, - transitionDuration: Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), ); } } @@ -506,11 +492,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) { @@ -522,7 +504,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) { @@ -531,7 +513,7 @@ class TbContext implements PopEntry { transition = TransitionType.native; } } - _closeMainFirst = isOpenedDashboard; + return await router.navigateTo( currentState!.context, path, @@ -544,21 +526,31 @@ 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 { + router.navigateTo( + currentState!.context, + '/dashboard', + routeSettings: RouteSettings( + arguments: DashboardArgumentsEntity( + dashboardId, + title: 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; }, @@ -568,7 +560,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); @@ -583,170 +574,56 @@ 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( - {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)) - ], - )); + 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'}) { + 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)) - ], - )); - } -} - -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); + 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)), + ], + ), + ); } } 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..34eaf4ef 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -1,15 +1,16 @@ 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:thingsboard_app/generated/l10n.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 Map entityTypeTranslations = { +const entityTypeTranslations = { EntityType.TENANT: 'Tenant', EntityType.TENANT_PROFILE: 'Tenant profile', EntityType.CUSTOMER: 'Customer', @@ -18,6 +19,7 @@ const Map 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', @@ -27,15 +29,25 @@ 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', + 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); typedef EntityCardWidgetBuilder = Widget Function( - BuildContext context, T entity); + BuildContext context, + T entity, +); class EntityCardSettings { bool dropShadow; + EntityCardSettings({this.dropShadow = true}); } @@ -55,15 +67,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 +92,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 +183,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 +207,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 +231,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 +294,13 @@ abstract class BaseEntitiesState } Future _refresh() { - if (_refreshCompleter == null) { - _refreshCompleter = Completer(); - } + _refreshCompleter ??= Completer(); if (_dataLoading) { _scheduleRefresh = true; } else { _refreshPagingController(); } + return _refreshCompleter!.future; } @@ -305,105 +355,48 @@ 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(), ); } } - -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: 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..f9ac224e 100644 --- a/lib/core/entity/entities_list_widget.dart +++ b/lib/core/entity/entities_list_widget.dart @@ -5,20 +5,22 @@ 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'; 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..e38732ee 100644 --- a/lib/core/entity/entity_details_page.dart +++ b/lib/core/entity/entity_details_page.dart @@ -1,16 +1,16 @@ 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 = - 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 deleted file mode 100644 index 32161b6f..00000000 --- a/lib/generated/intl/messages_all.dart +++ /dev/null @@ -1,67 +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; - -typedef Future LibraryLoader(); -Map _deferredLibraries = { - 'en': () => new SynchronousFuture(null), - 'zh': () => new SynchronousFuture(null), -}; - -MessageLookupByLibrary? _findExact(String localeName) { - switch (localeName) { - case 'en': - return messages_en.messages; - case 'zh': - return messages_zh.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 62ba8666..00000000 --- a/lib/generated/intl/messages_en.dart +++ /dev/null @@ -1,173 +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 d022ffa9..00000000 --- a/lib/generated/intl/messages_zh.dart +++ /dev/null @@ -1,107 +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 7965265c..00000000 --- a/lib/generated/l10n.dart +++ /dev/null @@ -1,1019 +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'), - ]; - } - - @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_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": "الرمز الاحتياطي" +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index feb56043..424628e1 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", @@ -108,5 +108,44 @@ "mfaProviderTopt": "Authenticator app", "mfaProviderSms": "SMS", "mfaProviderEmail": "Email", - "mfaProviderBackupCode": "Backup code" -} \ No newline at end of file + "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/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/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 6d8c29da..3f88a0f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,23 +2,20 @@ 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'; 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'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -28,7 +25,7 @@ void main() async { await setUpRootDependencies(); if (UniversalPlatform.isAndroid) { - await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true); + await InAppWebViewController.setWebContentsDebuggingEnabled(true); } try { @@ -51,125 +48,16 @@ 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); - - @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); - } - - 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; - } +class ThingsboardApp extends StatelessWidget { + const ThingsboardApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( + const SystemUiOverlayStyle( systemNavigationBarColor: Colors.white, statusBarColor: Colors.white, systemNavigationBarIconBrightness: Brightness.light, @@ -177,60 +65,23 @@ class ThingsboardAppState extends State ); return MaterialApp( - debugShowCheckedModeBanner: false, - localizationsDelegates: [ + scaffoldMessengerKey: + getIt().tbContext.messengerKey, + localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: S.delegate.supportedLocales, + 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: [ - 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, - ), - ), - ), + theme: tbTheme, + darkTheme: tbDarkTheme, + onGenerateRoute: getIt().router.generator, + navigatorObservers: [ + getIt().tbContext.routeObserver, + ], ); } } diff --git a/lib/modules/alarm/alarm_routes.dart b/lib/modules/alarm/alarm_routes.dart index f2851b27..b43e8b4c 100644 --- a/lib/modules/alarm/alarm_routes.dart +++ b/lib/modules/alarm/alarm_routes.dart @@ -1,25 +1,30 @@ 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); - } else { - return MainPage(tbContext, path: '/alarms'); - } - }); - 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 AlarmsSearchPage( + tbContext: 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..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:thingsboard_app/generated/l10n.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,293 +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(tbContext); - - @override - _AlarmCardState createState() => _AlarmCardState(alarm); -} - -class _AlarmCardState extends TbContextState { - bool loading = false; - AlarmInfo alarm; - - final entityDateFormat = DateFormat('yyyy-MM-dd'); - - _AlarmCardState(this.alarm) : super(); - - @override - void initState() { - super.initState(); - } - - @override - void didUpdateWidget(AlarmCard oldWidget) { - super.didUpdateWidget(oldWidget); - this.loading = false; - this.alarm = widget.alarm; - } - - @override - Widget build(BuildContext context) { - if (this.loading) { - return Container( - height: 134, - alignment: Alignment.center, - child: 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: BorderRadius.only( - topLeft: Radius.circular(4), - bottomLeft: Radius.circular(4))), - ))), - Row(mainAxisSize: MainAxisSize.max, children: [ - 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( - 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))) - ]) - ], - ), - SizedBox(width: 8) - ], - ), - 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 a28947db..e8c67e9a 100644 --- a/lib/modules/alarm/alarms_list.dart +++ b/lib/modules/alarm/alarms_list.dart @@ -1,14 +1,79 @@ +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, - {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, + onTryAgain: () => getIt().add( + const AlarmsRefreshPageEvent(), + ), + ), + ), + ), + ); + } } diff --git a/lib/modules/alarm/alarms_page.dart b/lib/modules/alarm/alarms_page.dart deleted file mode 100644 index ca0193c2..00000000 --- a/lib/modules/alarm/alarms_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/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(tbContext); - - @override - _AlarmsPageState 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: 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..dfadbf10 --- /dev/null +++ b/lib/modules/alarm/di/alarm_types_di.dart @@ -0,0 +1,65 @@ +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(), + filtersService: 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..cfb0a667 --- /dev/null +++ b/lib/modules/alarm/di/alarms_di.dart @@ -0,0 +1,80 @@ +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/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'; + +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( + () => AlarmFiltersService(logger: 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..c3cd9d64 --- /dev/null +++ b/lib/modules/alarm/di/assignee_di.dart @@ -0,0 +1,68 @@ +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(), + filtersService: 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..b66cb06c --- /dev/null +++ b/lib/modules/alarm/domain/entities/alarm_filters_entity.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; + +class AlarmFiltersEntity extends Equatable { + const AlarmFiltersEntity({ + this.typeList, + 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 const 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, + ); + } + + @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/domain/entities/assignee_entity.dart b/lib/modules/alarm/domain/entities/assignee_entity.dart new file mode 100644 index 00000000..4c5dcf8c --- /dev/null +++ b/lib/modules/alarm/domain/entities/assignee_entity.dart @@ -0,0 +1,40 @@ +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; + + 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.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..28f73ffe --- /dev/null +++ b/lib/modules/alarm/domain/pagination/alarm_types/alarm_types_query_ctrl.dart @@ -0,0 +1,26 @@ +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(); + } + + 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..31f7bc4c --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_bloc.dart @@ -0,0 +1,84 @@ +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'; + +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 IAlarmFiltersService filtersService; + + Future _onEvent( + AlarmTypesEvent event, + Emitter emit, + ) async { + switch (event) { + case AlarmTypesSelectedEvent(): + final types = filtersService.getSelectedFilter(Filters.type); + types.add(event.type); + filtersService.setSelectedFilter(Filters.type, data: types); + + emit( + AlarmTypeSelectedState( + selectedTypes: types, + allowToAddMore: types.length < + (paginationRepository.pagingController.itemList?.length ?? 0), + ), + ); + break; + + case AlarmTypesRemoveSelectedEvent(): + final types = filtersService.getSelectedFilter(Filters.type); + types.remove(event.type); + filtersService.setSelectedFilter(Filters.type, data: types); + + if (types.isNotEmpty) { + emit( + AlarmTypeSelectedState( + selectedTypes: types, + allowToAddMore: types.length < + (paginationRepository.pagingController.itemList?.length ?? 0), + ), + ); + } else { + emit(const AlarmTypesSelectionEmptyState()); + } + + break; + case AlarmTypesResetEvent(): + 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 new file mode 100644 index 00000000..00019b48 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarm_types/alarm_types_event.dart @@ -0,0 +1,38 @@ +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(); +} + +final class AlarmTypesResetUnCommittedChanges extends AlarmTypesEvent { + const AlarmTypesResetUnCommittedChanges(); +} 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..817a51ae --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/alarms_events.dart @@ -0,0 +1,38 @@ +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(); + + @override + List get props => [double.nan]; +} 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..6423e91b --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_bloc.dart @@ -0,0 +1,79 @@ +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/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'; + +class AssigneeBloc extends Bloc { + AssigneeBloc({ + required this.paginationRepository, + required this.fetchAssigneeUseCase, + required this.queryCtrl, + required this.filtersService, + }) : super(const AssigneeEmptyState()) { + on(_onEvent); + } + + final PaginationRepository paginationRepository; + final FetchAssigneeUseCase fetchAssigneeUseCase; + final AssigneeQueryCtrl queryCtrl; + final IAlarmFiltersService filtersService; + + Future _onEvent( + AssigneeEvent event, + Emitter emit, + ) async { + switch (event) { + case AssigneeSelectedEvent(): + filtersService.setSelectedFilter(Filters.assignee, data: 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(): + filtersService.setSelectedFilter(Filters.assignee, data: null); + emit(const AssigneeEmptyState()); + queryCtrl.onSearchText(null); + + break; + case AssigneeRefreshEvent(): + paginationRepository.refresh(); + + break; + + 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 new file mode 100644 index 00000000..f9562256 --- /dev/null +++ b/lib/modules/alarm/presentation/bloc/assignee/assignee_event.dart @@ -0,0 +1,46 @@ +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 AssigneeResetSearchTextEvent extends AssigneeEvent { + const AssigneeResetSearchTextEvent(); +} + +final class AssigneeResetEvent extends AssigneeEvent { + const AssigneeResetEvent(); +} + +final class AssigneeRefreshEvent extends AssigneeEvent { + const AssigneeRefreshEvent(); +} + +final class AssigneeResetUnCommittedChanges extends AssigneeEvent { + const AssigneeResetUnCommittedChanges(); +} 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/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 new file mode 100644 index 00000000..01603041 --- /dev/null +++ b/lib/modules/alarm/presentation/view/alarms_filter_page.dart @@ -0,0 +1,234 @@ +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/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/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 { + 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; + + 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: () { + _onBackButtonClick(); + }, + ), + ), + body: RefreshIndicator( + onRefresh: () async { + getIt().add(const AlarmTypesRefreshEvent()); + getIt().add(const AssigneeRefreshEvent()); + }, + child: SafeArea( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FilterToggleBlockWidget( + key: ValueKey( + filtersService.getSelectedFilter(Filters.status), + ), + label: 'Alarm status list', + items: filtersService.statuses, + selected: + filtersService.getSelectedFilter(Filters.status), + onSelectedChanged: (values) { + filtersService.setSelectedFilter( + Filters.status, + data: values.cast(), + ); + + setState(() { + filtersChanged = true; + }); + }, + labelAtIndex: (index) => + filtersService.statuses[index].label, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: FilterToggleBlockWidget( + key: ValueKey( + filtersService + .getSelectedFilter(Filters.severity), + ), + label: 'Alarm severity list', + items: filtersService.severities, + selected: filtersService + .getSelectedFilter(Filters.severity), + onSelectedChanged: (values) { + filtersService.setSelectedFilter( + Filters.severity, + data: values.cast(), + ); + + setState(() { + filtersChanged = true; + }); + }, + labelAtIndex: (index) => + filtersService.severities[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 ? _resetFilters : null, + onCancelTap: () { + _onBackButtonClick(); + }, + onUpdateTap: filtersChanged + ? () { + filtersService.commitChanges(); + + getIt().add( + AlarmFiltersUpdateEvent( + filtersEntity: + filtersService.getCommittedFilters(), + ), + ); + + widget.pageController.animateToPage( + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + } + : null, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + @override + void initState() { + filtersService = getIt(); + + listenNavigationChanges = widget + .tbContext.bottomNavigationTabChangedStream.stream + .listen((tabIndex) { + _onBackButtonClick(); + }); + + super.initState(); + } + + @override + void dispose() { + listenNavigationChanges.cancel(); + super.dispose(); + } + + void _resetFilters() { + setState(() { + filtersService.reset(); + 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, + ) + .then((_) { + if (mounted && filtersChanged) { + setState(() { + filtersService.resetUnCommittedChanges(); + + getIt() + .add(const AlarmTypesResetUnCommittedChanges()); + getIt().add(const AssigneeResetUnCommittedChanges()); + }); + } + }); + } +} 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..2fbfff22 --- /dev/null +++ b/lib/modules/alarm/presentation/view/alarms_page.dart @@ -0,0 +1,127 @@ +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'; +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: Text(S.of(context).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..acf0d13d --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/alarm_types/alarm_types_widget.dart @@ -0,0 +1,160 @@ +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, + topControl: const SizedBox.shrink(), + 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: [ + Flexible( + child: 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, + topControl: const SizedBox.shrink(), + 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..670ff588 --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/alarm_types/types_list_widget.dart @@ -0,0 +1,110 @@ +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'; +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: [ + Padding( + padding: const EdgeInsets.only(left: 16, top: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).alarmTypes, + style: const 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( + child: PagedListView.separated( + pagingController: getIt() + .paginationRepository + .pagingController, + shrinkWrap: true, + padding: + const EdgeInsets.symmetric(horizontal: 16, 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: [ + Flexible( + child: Text( + item.type, + style: const TextStyle( + fontSize: 16, + height: 1.5, + ), + ), + ), + ], + ), + ); + }, + firstPageProgressIndicatorBuilder: (_) { + return Container( + height: 200, + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator(size: 50.0), + ), + ); + }, + ), + separatorBuilder: (_, __) => + const Divider(thickness: 1, height: 32), + ), + ), + ], + ), + ), + ); + } +} 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..5fb95b71 --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/assignee/alarm_assignee_widget.dart @@ -0,0 +1,102 @@ +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'; +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: () async { + await UiUtils.showModalBottomSheet( + context: context, + topControl: const SizedBox.shrink(), + builder: (context) => AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 500), + child: AssigneeListWidget( + tbContext: tbContext, + onChanged: onChanged, + ), + ), + ); + getIt().add(const AssigneeResetSearchTextEvent()); + }, + child: Container( + 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, vertical: 8), + 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..26a65a27 --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/assignee/assignee_list_widget.dart @@ -0,0 +1,219 @@ +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'; +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 + + MediaQuery.of(context).viewInsets.bottom, + minHeight: MediaQuery.of(context).size.height * 0.3 + + MediaQuery.of(context).viewInsets.bottom, + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, top: 12, bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).assignee, + style: const 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( + 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, 12, 16, 12), + isDense: true, + ), + onChanged: (text) { + getIt().add( + AssigneeSearchEvent(searchText: text), + ); + }, + ), + ), + Icon( + Icons.search, + color: Colors.black.withOpacity(0.54), + size: 24, + ), + const SizedBox(width: 16), + ], + ), + ), + Flexible( + child: PagedListView.separated( + pagingController: getIt() + .paginationRepository + .pagingController, + padding: + const EdgeInsets.symmetric(horizontal: 16, 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), + size: 32, + ), + 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: 32), + ], + ), + ), + 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..6b022f17 --- /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: 32, + width: 32, + child: Center( + child: Text( + shortName, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + 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 new file mode 100644 index 00000000..e27da55e --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/assignee/user_info_widget.dart @@ -0,0 +1,66 @@ +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: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Visibility( + visible: showEmail, + child: Text( + email, + style: TextStyle( + 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 new file mode 100644 index 00000000..b70167e4 --- /dev/null +++ b/lib/modules/alarm/presentation/widgets/filter_toggle_block_widget.dart @@ -0,0 +1,84 @@ +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: 8, + 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 eca8f78f..3567486c 100644 --- a/lib/modules/asset/asset_details_page.dart +++ b/lib/modules/asset/asset_details_page.dart @@ -1,42 +1,48 @@ 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:thingsboard_app/generated/l10n.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(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..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 @@ -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..a61bdd7c 100644 --- a/lib/modules/asset/assets_list.dart +++ b/lib/modules/asset/assets_list.dart @@ -1,13 +1,16 @@ 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'; 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..30faec71 100644 --- a/lib/modules/asset/assets_list_widget.dart +++ b/lib/modules/asset/assets_list_widget.dart @@ -1,13 +1,15 @@ 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 { - 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..20e6be48 100644 --- a/lib/modules/audit_log/audit_log_details_page.dart +++ b/lib/modules/audit_log/audit_log_details_page.dart @@ -1,82 +1,104 @@ 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:thingsboard_app/generated/l10n.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; - 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..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', @@ -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..e3a9c25d 100644 --- a/lib/modules/audit_log/audit_logs_list.dart +++ b/lib/modules/audit_log/audit_logs_list.dart @@ -2,12 +2,14 @@ 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 { 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..c8755759 100644 --- a/lib/modules/customer/customer_details_page.dart +++ b/lib/modules/customer/customer_details_page.dart @@ -1,16 +1,18 @@ 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(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_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 836a420b..df3566ef 100644 --- a/lib/modules/customer/customers_list.dart +++ b/lib/modules/customer/customers_list.dart @@ -1,14 +1,16 @@ 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'; 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 deleted file mode 100644 index 2bc9bee8..00000000 --- a/lib/modules/dashboard/dashboard.dart +++ /dev/null @@ -1,509 +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) { - hasRightLayout.value = _hasRightLayout; - } - - onRightLayoutOpened(bool _rightLayoutOpened) { - 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}) - : this._home = home, - this._activeByDefault = activeByDefault, - this._titleCallback = titleCallback, - this._controllerCallback = controllerCallback, - super(tbContext, key: key); - - @override - _DashboardState 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; - - InAppWebViewGroupOptions options = InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( - useShouldOverrideUrlLoading: true, - mediaPlaybackRequiresUserGesture: false, - javaScriptEnabled: true, - cacheEnabled: true, - supportZoom: false, - // useOnDownloadStart: true - ), - android: AndroidInAppWebViewOptions( - useHybridComposition: true, - thirdPartyCookiesEnabled: true, - ), - ios: IOSInAppWebViewOptions( - allowsInlineMediaPlayback: true, - allowsBackForwardNavigationGestures: false, - ), - ); - - late Uri _initialUrl; - - @override - void initState() { - super.initState(); - dashboardActive.value = widget._activeByDefault; - _dashboardController = DashboardController(this); - if (widget._controllerCallback != null) { - widget._controllerCallback!(_dashboardController); - } - tbContext.isAuthenticatedListenable.addListener(_onAuthenticated); - if (tbContext.isAuthenticated) { - _onAuthenticated(); - } - } - - void _onAuthenticated() async { - if (tbContext.isAuthenticated) { - if (!readyState.value) { - _initialUrl = Uri.parse( - 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) { - _controller.future.then((controller) { - controller.postWebMessage( - message: WebMessage(data: jsonEncode(windowMessage)), - targetOrigin: Uri.parse('*')); - }); - } - } - } - } - - Future _goBack() async { - if (!UniversalPlatform.isWeb) { - if (_dashboardController.rightLayoutOpened.value) { - await _toggleRightLayout(); - return false; - } - if (!_controller.isCompleted) { - 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.future.then((controller) { - 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) { - controller.postWebMessage( - message: webMessage, targetOrigin: Uri.parse('*')); - } - }); - } - } - - Future _toggleRightLayout() async { - _controller.future.then((controller) { - var windowMessage = {'type': 'toggleDashboardLayout'}; - var webMessage = WebMessage(data: jsonEncode(windowMessage)); - controller.postWebMessage( - message: webMessage, targetOrigin: Uri.parse('*')); - }); - } - - 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 SizedBox.shrink(); - } else { - return Stack( - children: [ - UniversalPlatform.isWeb - ? Center(child: Text('Not implemented!')) - : InAppWebView( - key: webViewKey, - initialUrlRequest: URLRequest(url: _initialUrl), - initialOptions: options, - onWebViewCreated: (webViewController) { - log.debug("onWebViewCreated"); - webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardLoadedHandler", - callback: (args) async { - bool hasRightLayout = args[0]; - bool rightLayoutOpened = args[1]; - log.debug( - "Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened"); - _dashboardController - .onHasRightLayout(hasRightLayout); - _dashboardController - .onRightLayoutOpened(rightLayoutOpened); - dashboardLoading.value = false; - }, - ); - webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardLayoutHandler", - callback: (args) async { - bool rightLayoutOpened = args[0]; - log.debug( - "Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened"); - _dashboardController - .onRightLayoutOpened(rightLayoutOpened); - }, - ); - webViewController.addJavaScriptHandler( - handlerName: "tbMobileDashboardStateNameHandler", - callback: (args) async { - log.debug( - "Invoked tbMobileDashboardStateNameHandler: $args"); - if (args.isNotEmpty && args[0] is String) { - if (widget._titleCallback != null) { - widget._titleCallback!(args[0]); - } - } - }, - ); - webViewController.addJavaScriptHandler( - handlerName: "tbMobileNavigationHandler", - callback: (args) async { - log.debug( - "Invoked tbMobileNavigationHandler: $args", - ); - if (args.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, - ); - }, - ); - }, - 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 tryLocalNavigation(target); - } on UnimplementedError catch (e) { - 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); - } - }, - androidOnPermissionRequest: - (controller, origin, resources) async { - log.debug( - 'androidOnPermissionRequest origin: $origin, resources: $resources'); - return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT); - }, - ), - if (!UniversalPlatform.isWeb) - TwoValueListenableBuilder( - firstValueListenable: dashboardLoading, - secondValueListenable: dashboardActive, - builder: (context, loading, active, child) { - if (!loading && active) { - return 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: TbProgressIndicator(size: 50.0), - ); - } - }, - ) - ], - ); - } - }, - ), - ); - } - - 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(); - }, - ), - ); - } -} diff --git a/lib/modules/dashboard/dashboard_page.dart b/lib/modules/dashboard/dashboard_page.dart deleted file mode 100644 index b3707360..00000000 --- a/lib/modules/dashboard/dashboard_page.dart +++ /dev/null @@ -1,62 +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}) - : - // _dashboardId = dashboardId, - // _fullscreen = fullscreen, - _dashboardTitle = dashboardTitle, - // _state = state, - super(tbContext); - - @override - _DashboardPageState 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: Text( - 'Deprecated') //Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state, - //fullscreen: widget._fullscreen, titleCallback: (title) { - //dashboardTitleValue.value = title; - //} - //), - ); - } -} diff --git a/lib/modules/dashboard/dashboard_routes.dart b/lib/modules/dashboard/dashboard_routes.dart index 71e5a9b6..a67e2a13 100644 --- a/lib/modules/dashboard/dashboard_routes.dart +++ b/lib/modules/dashboard/dashboard_routes.dart @@ -1,42 +1,54 @@ 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 var dashboardsHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return DashboardsPage(tbContext); - }); + late final dashboardsHandler = Handler( + handlerFunc: (context, params) { + return DashboardsPage(tbContext); + }, + ); + + late final dashboardHandler = Handler( + handlerFunc: (context, params) { + final args = context?.settings?.arguments as DashboardArgumentsEntity; - 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], - fullscreen: fullscreen, - dashboardTitle: dashboardTitle, - state: state); - }); + return SingleDashboardView( + tbContext, + id: args.id, + title: args.title, + state: args.state, + hideToolbar: args.hideToolbar, + ); + }, + ); - late var fullscreenDashboardHandler = Handler( - handlerFunc: (BuildContext? context, Map params) { - return FullscreenDashboardPage(tbContext, params["id"]![0]); - }); + late final fullscreenDashboardHandler = Handler( + handlerFunc: (context, 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, + ) + ..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 3328bc5f..00000000 --- a/lib/modules/dashboard/dashboards_base.dart +++ /dev/null @@ -1,201 +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/utils/utils.dart'; -import 'package:thingsboard_client/thingsboard_client.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: 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()) - ], - ), - )) - ]); - } - - 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(tbContext); - - @override - _DashboardGridCardState createState() => _DashboardGridCardState(); -} - -class _DashboardGridCardState extends TbContextState { - _DashboardGridCardState() : super(); - - @override - void initState() { - super.initState(); - } - - @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)) - ])), - 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), - ))), - ) - ], - )); - } -} diff --git a/lib/modules/dashboard/dashboards_grid.dart b/lib/modules/dashboard/dashboards_grid.dart deleted file mode 100644 index fa255d64..00000000 --- a/lib/modules/dashboard/dashboards_grid.dart +++ /dev/null @@ -1,37 +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_client/thingsboard_client.dart'; - -import 'dashboards_base.dart'; - -class DashboardsGridWidget extends TbContextWidget { - DashboardsGridWidget(TbContext tbContext) : super(tbContext); - - @override - _DashboardsGridWidgetState 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(tbContext, pageKeyController); -} diff --git a/lib/modules/dashboard/dashboards_list.dart b/lib/modules/dashboard/dashboards_list.dart deleted file mode 100644 index 159ff0c0..00000000 --- a/lib/modules/dashboard/dashboards_list.dart +++ /dev/null @@ -1,13 +0,0 @@ -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_client/thingsboard_client.dart'; - -import 'dashboards_base.dart'; - -class DashboardsList extends BaseEntitiesWidget - with DashboardsBase, EntitiesListStateBase { - DashboardsList( - TbContext tbContext, PageKeyController pageKeyController) - : 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 a62f59fc..00000000 --- a/lib/modules/dashboard/dashboards_list_widget.dart +++ /dev/null @@ -1,16 +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_client/thingsboard_client.dart'; - -class DashboardsListWidget extends EntitiesListPageLinkWidget - with DashboardsBase { - DashboardsListWidget(TbContext tbContext, - {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 9a1a5e6b..00000000 --- a/lib/modules/dashboard/dashboards_page.dart +++ /dev/null @@ -1,27 +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(tbContext); - - @override - _DashboardsPageState 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(); - } -} diff --git a/lib/modules/dashboard/di/dashboards_di.dart b/lib/modules/dashboard/di/dashboards_di.dart new file mode 100644 index 00000000..7ffc24e7 --- /dev/null +++ b/lib/modules/dashboard/di/dashboards_di.dart @@ -0,0 +1,36 @@ +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.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 d5dc9937..a6272dd9 100644 --- a/lib/modules/dashboard/main_dashboard_page.dart +++ b/lib/modules/dashboard/main_dashboard_page.dart @@ -2,94 +2,34 @@ 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, { + required this.controller, + super.key, + }) : super(tbContext); - MainDashboardPage(TbContext tbContext, - {MainDashboardPageController? controller, String? dashboardTitle}) - : _controller = controller, - _dashboardTitle = dashboardTitle, - super(tbContext); + final DashboardPageController controller; @override - _MainDashboardPageState createState() => _MainDashboardPageState(); + State createState() => _MainDashboardPageState(); } 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: 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) { @@ -113,8 +53,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,37 +66,58 @@ class _MainDashboardPageState extends TbContextState return const SizedBox.shrink(); } }, - ) + ), ], ), body: ValueListenableBuilder( valueListenable: getIt().listenEndpointChanges, builder: (context, value, _) { - return Dashboard( + return DashboardWidget( tbContext, - 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 51% rename from lib/modules/dashboard/fullscreen_dashboard_page.dart rename to lib/modules/dashboard/presentation/view/fullscreen_dashboard_page.dart index 8bb0455d..24bdacb7 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'; @@ -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,40 +41,40 @@ 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, - 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..eca3b25a --- /dev/null +++ b/lib/modules/dashboard/presentation/widgets/dashboard_widget.dart @@ -0,0 +1,327 @@ +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, + ); + + @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 { + if (widget.pageController != null) { + widget.pageController?.closeDashboard().then( + (_) => dashboardLoading.value = true, + ); + } else { + return true; + } + } + + return false; + }, + 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.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 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}', + ); + + if (dashboardLoading.value) { + dashboardLoading.value = false; + } + }, + onLoadStart: (controller, url) async { + log.debug('onLoadStart: $url'); + }, + onLoadStop: (controller, url) async { + log.debug('onLoadStop: $url'); + if (webViewLoading) { + webViewLoading = false; + } + }, + onPermissionRequest: (controller, request) async { + log.debug( + 'onPermissionRequest resources: ${request.resources}', + ); + + return PermissionResponse( + action: PermissionResponseAction.GRANT, + resources: request.resources, + ); + }, + ), + 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..0739c5a7 --- /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().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/device/device_details_page.dart b/lib/modules/device/device_details_page.dart index c2c96555..4e7f88cf 100644 --- a/lib/modules/device/device_details_page.dart +++ b/lib/modules/device/device_details_page.dart @@ -1,22 +1,22 @@ 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) + 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..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:thingsboard_app/generated/l10n.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(); @@ -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..e39f919d 100644 --- a/lib/modules/device/device_profiles_grid.dart +++ b/lib/modules/device/device_profiles_grid.dart @@ -1,13 +1,15 @@ 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'; 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..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:thingsboard_app/generated/l10n.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 @@ -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..dc233e2c 100644 --- a/lib/modules/device/devices_list.dart +++ b/lib/modules/device/devices_list.dart @@ -2,16 +2,19 @@ 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 { 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..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'; @@ -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..400093c3 100644 --- a/lib/modules/device/devices_list_widget.dart +++ b/lib/modules/device/devices_list_widget.dart @@ -2,13 +2,15 @@ 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 { - 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..51fa4465 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -1,85 +1,49 @@ 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 dashboardUi; -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/widgets/tb_app_bar.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/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); - var homeDashboard = tbContext.homeDashboard; - var dashboardState = homeDashboard != null; - return Scaffold( - appBar: TbAppBar( - 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'))), - actions: [ - if (tbClient.isSystemAdmin()) - IconButton( - icon: Icon(Icons.search), - onPressed: () { - navigateTo('/tenants?search=true'); - }, - ) - ], - ), - body: Builder(builder: (context) { - if (dashboardState) { - return _buildDashboardHome(context, homeDashboard); - } else { - return _buildDefaultHome(context); - } - }), - ); + + final homeDashboard = tbContext.homeDashboard; + if (homeDashboard != null) { + return _buildDashboardHome(context, homeDashboard); + } else { + return _buildDefaultHome(context); + } } Widget _buildDashboardHome( - BuildContext context, HomeDashboardInfo dashboard) { - return HomeDashboard(tbContext, dashboard); + BuildContext context, + HomeDashboardInfo dashboard, + ) { + return HomeDashboardPage(tbContext, dashboard); } Widget _buildDefaultHome(BuildContext context) { if (tbClient.isSystemAdmin()) { return _buildSysAdminHome(context); } else { - return DashboardsGridWidget(tbContext); + return DashboardsPage(tbContext); } } @@ -87,23 +51,3 @@ class _HomePageState extends TbContextState return TenantsWidget(tbContext); } } - -class HomeDashboard extends TbContextWidget { - final HomeDashboardInfo dashboard; - - HomeDashboard(TbContext tbContext, this.dashboard) : super(tbContext); - - @override - _HomeDashboardState 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); - }); - } -} 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_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 03999d80..55480c7c 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -1,117 +1,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/generated/l10n.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: 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: Icon(Icons.notifications), - path: '/alarms'), - TbMainNavigationItem( - page: DevicesMainPage(tbContext), - title: 'Devices', - icon: 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: 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}) - : _path = path, + MainPage( + TbContext tbContext, { + super.key, + required String path, + }) : _path = path, super(tbContext); + final String _path; + @override - _MainPageState createState() => _MainPageState(); + State createState() => _MainPageState(); } class _MainPageState extends TbPageState @@ -142,7 +45,7 @@ class _MainPageState extends TbPageState _onTabAnimation() { var value = _tabController.animation!.value; - var targetIndex; + int targetIndex; if (value >= _tabController.previousIndex) { targetIndex = value.round(); } else { @@ -163,26 +66,30 @@ 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 - ? NeverScrollableScrollPhysics() - : null, - controller: _tabController, - children: _tabItems.map((item) => item.page).toList(), + body: TabBarView( + physics: const NeverScrollableScrollPhysics(), + 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), + 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) { @@ -205,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 b924746c..ca975656 100644 --- a/lib/modules/more/more_page.dart +++ b/lib/modules/more/more_page.dart @@ -1,20 +1,20 @@ 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: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/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(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/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/notification_page.dart b/lib/modules/notification/notification_page.dart index 1fc6cc04..bd921905 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(); @@ -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: 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: 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) { @@ -98,7 +98,7 @@ class _NotificationPageState extends TbPageState { ); }); }, - segments: [ + segments: const [ FilterSegments( label: 'Unread', value: NotificationsFilter.unread, 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..3a037755 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(); @@ -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); } 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..496543e5 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'; +import 'package:thingsboard_app/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..4ad5d803 100644 --- a/lib/modules/notification/widgets/notification_list.dart +++ b/lib/modules/notification/widgets/notification_list.dart @@ -4,43 +4,45 @@ 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 { - NotificationsList({ + const NotificationsList({ required this.pagingController, required this.thingsboardClient, required this.onClearNotification, required this.onReadNotification, required this.tbContext, + super.key, }); final ThingsboardClient thingsboardClient; 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, + 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..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:thingsboard_app/generated/l10n.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/thingsboard_client.dart'; class NotificationSlidableWidget extends StatefulWidget { const NotificationSlidableWidget({ @@ -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,12 @@ 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..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 { @@ -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..4b353a97 100644 --- a/lib/modules/profile/change_password_page.dart +++ b/lib/modules/profile/change_password_page.dart @@ -3,15 +3,15 @@ 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'; 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..52ea848a 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:flutter_gen/gen_l10n/messages.dart'; import 'package:form_builder_validators/form_builder_validators.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/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; - 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 ?? '', }); } @@ -151,30 +161,41 @@ 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(Duration(milliseconds: 300)); - _isLoadingNotifier.value = false; - showSuccessNotification('${S.of(context).profileSuccessNotification}', - duration: Duration(milliseconds: 1500)); - showSuccessNotification('${S.of(context).profileSuccessNotification}', - duration: 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; + } } } } _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..3e865c29 100644 --- a/lib/modules/tenant/tenant_details_page.dart +++ b/lib/modules/tenant/tenant_details_page.dart @@ -1,16 +1,18 @@ 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(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_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 543d33b9..3a62a440 100644 --- a/lib/modules/tenant/tenants_list.dart +++ b/lib/modules/tenant/tenants_list.dart @@ -1,14 +1,16 @@ 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'; 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 02b3af08..a215e2bc 100644 --- a/lib/modules/url/url_page.dart +++ b/lib/modules/url/url_page.dart @@ -31,7 +31,7 @@ class _UrlPageState extends TbPageState { onPressed: () { launchUrlString(widget.url, mode: LaunchMode.externalApplication); }, - icon: Icon(Icons.open_in_browser), + icon: const Icon(Icons.open_in_browser), ), ], ), @@ -39,13 +39,12 @@ 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 { - return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT, + onPermissionRequest: (controller, request) async { + return PermissionResponse( + resources: request.resources, + action: PermissionResponseAction.GRANT, ); }, ), 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 d2201a02..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(); @@ -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/_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/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/device_profile_cache.dart b/lib/utils/services/device_profile_cache.dart index ce188a38..d30417c8 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'; +import 'package:thingsboard_app/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/endpoint/endpoint_service.dart b/lib/utils/services/endpoint/endpoint_service.dart index 8629af92..b5512d81 100644 --- a/lib/utils/services/endpoint/endpoint_service.dart +++ b/lib/utils/services/endpoint/endpoint_service.dart @@ -8,14 +8,16 @@ 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; + _cachedEndpoint = endpoint; + _notifierValue.value = UniqueKey().toString(); await databaseService.setItem( DatabaseKeys.thingsBoardApiEndpointKey, @@ -25,24 +27,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; } } diff --git a/lib/utils/services/entity_query_api.dart b/lib/utils/services/entity_query_api.dart index eb66b754..d443147c 100644 --- a/lib/utils/services/entity_query_api.dart +++ b/lib/utils/services/entity_query_api.dart @@ -1,33 +1,40 @@ -import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:thingsboard_app/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/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 6ea62644..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._(); @@ -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/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/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/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/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/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/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/ui_utils.dart b/lib/utils/ui/ui_utils.dart new file mode 100644 index 00000000..ae1e932a --- /dev/null +++ b/lib/utils/ui/ui_utils.dart @@ -0,0 +1,18 @@ +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, + Widget? topControl, + }) async { + return showBarModalBottomSheet( + context: context, + builder: builder, + barrierColor: barrierColor, + topControl: topControl, + ); + } +} 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..a67385ae 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -3,25 +3,28 @@ 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;'; 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..7a69de00 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), ); } } @@ -79,26 +74,30 @@ 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; - 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, + this.leading, + }) : 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 +125,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 +145,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, + 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', + ), + ), + leading: widget.leading, + 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..edc760f5 100644 --- a/lib/widgets/two_page_view.dart +++ b/lib/widgets/two_page_view.dart @@ -31,23 +31,23 @@ 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 { 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; @@ -64,8 +70,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 +83,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; @@ -88,20 +100,21 @@ class _TwoPageViewState extends State { @override void dispose() { + _pageController.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..c5c6bfc5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,22 @@ # 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: name: _flutterfire_internals - sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 + sha256: "5fdcea390499dd26c808a3c662df5f4208d6bbc0643072eee94f1476249e2818" url: "https://pub.dev" source: hosted - version: "1.3.16" + version: "1.3.43" adaptive_number: 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: @@ -57,6 +89,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + url: "https://pub.dev" + source: hosted + version: "2.4.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + url: "https://pub.dev" + source: hosted + version: "7.3.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" characters: dependency: transitive description: @@ -97,8 +193,16 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - collection: + code_builder: dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: "direct main" description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a @@ -113,6 +217,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 +245,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: @@ -153,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" dbus: dependency: transitive description: @@ -165,26 +285,42 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" + 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: name: dio - sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" url: "https://pub.dev" source: hosted - version: "5.3.4" + version: "2.0.0" ed25519_edwards: dependency: transitive description: @@ -193,6 +329,22 @@ 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" + event_bus: + dependency: "direct main" + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" fading_edge_scrollview: dependency: "direct main" description: @@ -261,50 +413,50 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" + sha256: c7de9354eb2cd8bfe8059e1112174c9a58beda7051807207306bc48283277cfb url: "https://pub.dev" source: hosted - version: "2.24.2" + version: "3.5.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.3.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.18.1" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8" + sha256: "32ce60b747e755b48d7112d728d4f736ba82acd98ec825626558d444d385fa3a" url: "https://pub.dev" source: hosted - version: "14.7.10" + version: "15.1.2" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "54e283a0e41d81d854636ad0dad73066adc53407a60a7c3189c9656e2f1b6107" + sha256: "69671a0f1a40c7b7c46ad0283e6f34ca2a59a0362ca14a240a4ea01c46e8a521" url: "https://pub.dev" source: hosted - version: "4.5.18" + version: "4.5.45" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" + sha256: "6890111a9d01d7b13d0f6fe74850812c334e903d2c80a2d9356a3abb8c3a9e9a" url: "https://pub.dev" source: hosted - version: "3.5.18" + version: "3.9.1" fixnum: dependency: transitive description: @@ -326,22 +478,94 @@ 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: name: flutter_form_builder - sha256: "8973beed34b6d951d36bf688b52e9e3040b47b763c35c320bd6f4c2f6b13f3a2" + sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "3.0.0-beta.2" flutter_inappwebview: dependency: "direct main" description: name: flutter_inappwebview - sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "1.0.13" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_launcher_icons: dependency: "direct dev" description: @@ -350,30 +574,38 @@ 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: name: flutter_local_notifications - sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535" + sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" url: "https://pub.dev" source: hosted - version: "16.3.0" + version: "17.2.3" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.0.0+1" + version: "7.2.0" flutter_localizations: dependency: "direct main" description: flutter @@ -435,6 +667,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: @@ -473,18 +713,26 @@ packages: dependency: "direct main" description: name: form_builder_validators - sha256: "19aa5282b7cede82d0025ab031a98d0554b84aa2ba40f12013471a3b3e22bf02" + sha256: "475853a177bfc832ec12551f752fd0001278358a6d42d2364681ff15f48f67cf" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "10.0.1" + 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: name: geolocator - sha256: e946395fc608842bb2f6c914807e9183f86f3cb787f6b8f832753e5251036f02 + sha256: "149876cc5207a0f5daf4fdd3bfcf0a0f27258b3fe95108fa084f527ad0568f1b" url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "12.0.0" geolocator_android: dependency: transitive description: @@ -505,18 +753,18 @@ packages: dependency: transitive description: name: geolocator_platform_interface - sha256: "6c8d494d6948757c56720b778af742f6973f31fca1f702a7539b8917e4a2468a" + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.4" geolocator_web: dependency: transitive description: name: geolocator_web - sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58" + sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "4.1.1" geolocator_windows: dependency: transitive description: @@ -525,8 +773,56 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" - html: + 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" + graphs: dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + html: + dependency: "direct main" description: name: html sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" @@ -541,6 +837,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: @@ -633,10 +937,18 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "1.0.4" jovial_misc: dependency: transitive description: @@ -677,6 +989,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" logger: dependency: "direct main" description: @@ -685,22 +1037,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 +1073,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.0" mime: dependency: "direct main" description: @@ -725,30 +1085,70 @@ 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" + modal_bottom_sheet: + dependency: "direct main" + description: + name: modal_bottom_sheet + sha256: eac66ef8cb0461bf069a38c5eb0fa728cee525a531a8304bd3f7b2185407c67e + url: "https://pub.dev" + source: hosted + version: "3.0.0" + 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" + 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: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" path: 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 +1237,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 +1253,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + pretty_dio_logger: + dependency: transitive + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.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" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" qr_code_scanner: dependency: "direct main" description: @@ -853,6 +1293,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 +1338,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.12" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + 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,22 +1426,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" thingsboard_client: dependency: "direct main" description: name: thingsboard_client - sha256: addae7f2ae7c14234f97c64313f4f7a1f98765624cda04935dd5cce329d364bf + sha256: f63b137fbe4def1ae80b4198a7268d144b4acd88979ec629d9375e84edc852ee url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.3.0" + timeago: + dependency: "direct main" + description: + name: timeago + sha256: d3204eb4c788214883380253da7f23485320a58c11d145babc82ad16bf4e7764 + url: "https://pub.dev" + source: hosted + version: "3.6.1" timezone: dependency: transitive description: @@ -946,6 +1474,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.2" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" typed_data: dependency: transitive description: @@ -954,6 +1490,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: @@ -1030,10 +1590,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: @@ -1082,14 +1642,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.0.0" web_socket_channel: dependency: transitive description: @@ -1098,6 +1674,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: @@ -1139,5 +1723,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=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 5ceea60f..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,8 +11,8 @@ environment: dependencies: flutter: sdk: flutter - thingsboard_client: ^1.2.0 - intl: ^0.18.1 + thingsboard_client: ^1.3.0 + 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 @@ -56,8 +56,12 @@ dependencies: get_it: ^7.6.7 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 + modal_bottom_sheet: ^3.0.0 + event_bus: ^2.0.0 dev_dependencies: flutter_test: @@ -65,6 +69,7 @@ dev_dependencies: flutter_launcher_icons: ^0.13.1 mocktail: ^1.0.3 bloc_test: ^9.1.7 + flutter_lints: ^2.0.0 hive_generator: ^2.0.1 build_runner: ^2.4.9 @@ -72,6 +77,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. diff --git a/test/ core/noauth/switch_endpoint_test.dart b/test/ core/noauth/switch_endpoint_test.dart index c63fa112..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'; @@ -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/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 {} 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, + ); }); }