From 30987448a618334dc392637b1563225820a16141 Mon Sep 17 00:00:00 2001 From: liangsheng8708 <1933153216@qq.com> Date: Tue, 9 Jul 2024 22:34:59 +0800 Subject: [PATCH] V1.0 --- .gitignore | 46 + .metadata | 10 + android/.gitignore | 11 + android/app/build.gradle | 63 + android/app/src/debug/AndroidManifest.xml | 30 + android/app/src/main/AndroidManifest.xml | 114 + .../org/thingsboard/app/KeepAliveService.kt | 16 + .../org/thingsboard/app/MainActivity.kt | 20 + .../org/thingsboard/app/TbWebAuthHandler.kt | 50 + .../thingsboard/app/TbWebCallbackActivity.kt | 20 + .../res/drawable-v21/launch_background.xml | 11 + .../main/res/drawable/launch_background.xml | 11 + .../main/res/mipmap-hdpi/launcher_icon.png | Bin 0 -> 6389 bytes .../src/main/res/mipmap-hdpi/thingsboard.png | Bin 0 -> 4437 bytes .../main/res/mipmap-mdpi/launcher_icon.png | Bin 0 -> 3441 bytes .../src/main/res/mipmap-mdpi/thingsboard.png | Bin 0 -> 2631 bytes .../main/res/mipmap-xhdpi/launcher_icon.png | Bin 0 -> 8143 bytes .../src/main/res/mipmap-xhdpi/thingsboard.png | Bin 0 -> 6205 bytes .../main/res/mipmap-xxhdpi/launcher_icon.png | Bin 0 -> 15293 bytes .../main/res/mipmap-xxhdpi/thingsboard.png | Bin 0 -> 10254 bytes .../main/res/mipmap-xxxhdpi/launcher_icon.png | Bin 0 -> 19738 bytes .../main/res/mipmap-xxxhdpi/thingsboard.png | Bin 0 -> 14895 bytes .../app/src/main/res/values-night/styles.xml | 19 + android/app/src/main/res/values/styles.xml | 19 + .../main/res/xml/network_security_config.xml | 4 + .../app/src/main/res/xml/provider_paths.xml | 6 + android/app/src/profile/AndroidManifest.xml | 29 + android/build.gradle | 31 + android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + android/settings.gradle | 11 + android/settings_aar.gradle | 1 + assets/images/apple-logo.svg | 4 + assets/images/dashboard-placeholder.svg | 37 + assets/images/device-profile-placeholder.svg | 14 + assets/images/facebook-logo.svg | 5 + assets/images/github-logo.svg | 4 + assets/images/google-logo.svg | 8 + assets/images/qr_code_scanner.svg | 10 + assets/images/qr_code_scanner2.svg | 11 + assets/images/thingsboard.png | Bin 0 -> 21891 bytes assets/images/thingsboard.svg | 9 + assets/images/thingsboard_center.svg | 7 + assets/images/thingsboard_outer.svg | 8 + assets/images/thingsboard_with_title.svg | 36 + ios/.gitignore | 32 + ios/Flutter/AppFrameworkInfo.plist | 26 + ios/Flutter/Debug.xcconfig | 2 + ios/Flutter/Release.xcconfig | 2 + ios/Podfile | 41 + ios/Podfile.lock | 177 ++ ios/Runner.xcodeproj/project.pbxproj | 578 ++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 91 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + ios/Runner/AppDelegate.swift | 30 + .../AppIcon.appiconset/Contents.json | 122 + .../Icon-App-1024x1024@1x.png | Bin 0 -> 68188 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 841 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 2426 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 4639 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1515 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 4372 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 7142 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 2426 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 7046 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 12326 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 12326 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 18688 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 6508 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 16933 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 16927 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 12150 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 21057 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 40400 bytes .../LaunchImage.imageset/README.md | 5 + ios/Runner/Base.lproj/LaunchScreen.storyboard | 37 + ios/Runner/Base.lproj/Main.storyboard | 26 + ios/Runner/Info-Debug.plist | 79 + ios/Runner/Info-Release.plist | 68 + ios/Runner/Runner-Bridging-Header.h | 1 + ios/Runner/Runner.entitlements | 12 + ios/Runner/TbWebAuthHandler.swift | 74 + lib/config/routes/router.dart | 66 + lib/config/themes/tb_theme.dart | 84 + lib/constants/app_constants.dart | 7 + lib/constants/assets_path.dart | 19 + lib/constants/database_keys.dart | 4 + lib/core/auth/auth_routes.dart | 42 + lib/core/auth/login/login_page.dart | 455 ++++ .../auth/login/login_page_background.dart | 42 + .../login/reset_password_request_page.dart | 118 + .../login/two_factor_authentication_page.dart | 459 ++++ .../remote/i_noauth_remote_datasource.dart | 21 + .../remote/noauth_remote_datasource.dart | 74 + .../data/repository/noauth_repository.dart | 53 + lib/core/auth/noauth/di/noauth_di.dart | 52 + .../repository/i_noauth_repository.dart | 21 + .../usecases/switch_endpoint_usecase.dart | 106 + .../auth/noauth/presentation/bloc/bloc.dart | 3 + .../noauth/presentation/bloc/noauth_bloc.dart | 84 + .../presentation/bloc/noauth_events.dart | 39 + .../presentation/bloc/noauth_states.dart | 34 + .../view/switch_endpoint_noauth_view.dart | 150 ++ .../widgets/endpoint_name_widget.dart | 29 + .../widgets/noauth_loading_widget.dart | 18 + .../auth/noauth/routes/noauth_routes.dart | 23 + lib/core/auth/oauth2/app_secret_provider.dart | 15 + lib/core/auth/oauth2/tb_oauth2_client.dart | 124 + lib/core/auth/web/tb_web_auth.dart | 45 + lib/core/context/tb_context.dart | 752 ++++++ lib/core/context/tb_context_widget.dart | 98 + lib/core/entity/entities_base.dart | 409 +++ lib/core/entity/entities_grid.dart | 64 + lib/core/entity/entities_list.dart | 44 + lib/core/entity/entities_list_widget.dart | 218 ++ lib/core/entity/entity_details_page.dart | 207 ++ lib/core/entity/entity_grid_card.dart | 51 + lib/core/entity/entity_list_card.dart | 57 + lib/core/init/init_app.dart | 29 + lib/core/init/init_routes.dart | 21 + lib/core/logger/tb_log_output.dart | 11 + lib/core/logger/tb_logger.dart | 44 + lib/core/logger/tb_logs_filter.dart | 13 + lib/firebase_options.dart | 22 + lib/generated/intl/messages_all.dart | 67 + lib/generated/intl/messages_en.dart | 173 ++ lib/generated/intl/messages_zh.dart | 107 + lib/generated/l10n.dart | 1019 +++++++ lib/l10n/intl_en.arb | 112 + lib/l10n/intl_zh.arb | 90 + lib/locator.dart | 43 + lib/main.dart | 236 ++ lib/modules/alarm/alarm_routes.dart | 25 + lib/modules/alarm/alarms_base.dart | 322 +++ lib/modules/alarm/alarms_list.dart | 14 + lib/modules/alarm/alarms_page.dart | 57 + lib/modules/asset/asset_details_page.dart | 42 + lib/modules/asset/asset_routes.dart | 28 + lib/modules/asset/assets_base.dart | 133 + lib/modules/asset/assets_list.dart | 13 + lib/modules/asset/assets_list_widget.dart | 16 + lib/modules/asset/assets_page.dart | 49 + .../audit_log/audit_log_details_page.dart | 119 + lib/modules/audit_log/audit_logs_base.dart | 241 ++ lib/modules/audit_log/audit_logs_list.dart | 13 + lib/modules/audit_log/audit_logs_page.dart | 51 + lib/modules/audit_log/audit_logs_routes.dart | 20 + .../customer/customer_details_page.dart | 16 + lib/modules/customer/customer_routes.dart | 27 + lib/modules/customer/customers_base.dart | 20 + lib/modules/customer/customers_list.dart | 14 + lib/modules/customer/customers_page.dart | 49 + lib/modules/dashboard/dashboard.dart | 502 ++++ lib/modules/dashboard/dashboard_page.dart | 62 + lib/modules/dashboard/dashboard_routes.dart | 42 + lib/modules/dashboard/dashboards_base.dart | 201 ++ lib/modules/dashboard/dashboards_grid.dart | 37 + lib/modules/dashboard/dashboards_list.dart | 13 + .../dashboard/dashboards_list_widget.dart | 16 + lib/modules/dashboard/dashboards_page.dart | 27 + .../dashboard/fullscreen_dashboard_page.dart | 97 + .../dashboard/main_dashboard_page.dart | 163 ++ lib/modules/device/device_details_page.dart | 22 + lib/modules/device/device_profiles_base.dart | 462 ++++ lib/modules/device/device_profiles_grid.dart | 13 + lib/modules/device/device_routes.dart | 46 + lib/modules/device/devices_base.dart | 399 +++ lib/modules/device/devices_list.dart | 18 + lib/modules/device/devices_list_page.dart | 99 + lib/modules/device/devices_list_widget.dart | 21 + lib/modules/device/devices_main_page.dart | 38 + lib/modules/device/devices_page.dart | 31 + lib/modules/home/home_page.dart | 109 + lib/modules/home/home_routes.dart | 19 + lib/modules/main/main_page.dart | 212 ++ lib/modules/more/more_page.dart | 358 +++ .../controllers/notification_query_ctrl.dart | 45 + .../notification/notification_page.dart | 176 ++ .../i_notification_query_repository.dart | 11 + .../notification_pagination_repository.dart | 67 + .../repository/notification_repository.dart | 56 + .../routes/notification_routes.dart | 21 + .../i_notifications_local_service.dart | 11 + .../service/notifications_local_service.dart | 59 + .../widgets/filter_segmented_button.dart | 71 + .../no_notifications_found_widget.dart | 23 + .../widgets/notification_icon.dart | 2372 +++++++++++++++++ .../widgets/notification_list.dart | 61 + .../widgets/notification_slidable_widget.dart | 196 ++ .../widgets/notification_widget.dart | 160 ++ lib/modules/profile/change_password_page.dart | 184 ++ lib/modules/profile/profile_page.dart | 180 ++ lib/modules/profile/profile_routes.dart | 21 + lib/modules/tenant/tenant_details_page.dart | 16 + lib/modules/tenant/tenant_routes.dart | 27 + lib/modules/tenant/tenants_base.dart | 20 + lib/modules/tenant/tenants_list.dart | 14 + lib/modules/tenant/tenants_page.dart | 50 + lib/modules/tenant/tenants_widget.dart | 28 + lib/modules/url/url_page.dart | 54 + lib/modules/url/url_routes.dart | 24 + lib/utils/services/_tb_app_storage.dart | 3 + lib/utils/services/_tb_secure_storage.dart | 47 + lib/utils/services/_tb_web_local_storage.dart | 23 + lib/utils/services/device_profile_cache.dart | 29 + .../services/endpoint/endpoint_service.dart | 48 + .../services/endpoint/i_endpoint_service.dart | 18 + lib/utils/services/entity_query_api.dart | 79 + .../services/firebase/firebase_service.dart | 55 + .../services/firebase/i_firebase_service.dart | 11 + .../i_local_database_service.dart | 6 + .../local_database_service.dart | 31 + lib/utils/services/notification_service.dart | 310 +++ lib/utils/services/tb_app_storage.dart | 3 + lib/utils/services/widget_action_handler.dart | 321 +++ lib/utils/transition/page_transitions.dart | 54 + lib/utils/ui/qr_code_scanner.dart | 135 + lib/utils/ui_utils_routes.dart | 19 + lib/utils/usecase.dart | 9 + lib/utils/utils.dart | 254 ++ lib/widgets/tb_app_bar.dart | 179 ++ lib/widgets/tb_progress_indicator.dart | 84 + lib/widgets/two_page_view.dart | 107 + lib/widgets/two_value_listenable_builder.dart | 32 + pubspec.lock | 1631 ++++++++++++ pubspec.yaml | 108 + test/core/noauth/switch_endpoint_test.dart | 257 ++ test/mocks.dart | 13 + test/widget_test.dart | 26 + 235 files changed, 20348 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/org/thingsboard/app/KeepAliveService.kt create mode 100644 android/app/src/main/kotlin/org/thingsboard/app/MainActivity.kt create mode 100644 android/app/src/main/kotlin/org/thingsboard/app/TbWebAuthHandler.kt create mode 100644 android/app/src/main/kotlin/org/thingsboard/app/TbWebCallbackActivity.kt create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/launcher_icon.png create mode 100644 android/app/src/main/res/mipmap-hdpi/thingsboard.png create mode 100644 android/app/src/main/res/mipmap-mdpi/launcher_icon.png create mode 100644 android/app/src/main/res/mipmap-mdpi/thingsboard.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/launcher_icon.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/thingsboard.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/thingsboard.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/thingsboard.png create mode 100644 android/app/src/main/res/values-night/styles.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/main/res/xml/network_security_config.xml create mode 100644 android/app/src/main/res/xml/provider_paths.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle create mode 100644 android/settings_aar.gradle create mode 100644 assets/images/apple-logo.svg create mode 100644 assets/images/dashboard-placeholder.svg create mode 100644 assets/images/device-profile-placeholder.svg create mode 100644 assets/images/facebook-logo.svg create mode 100644 assets/images/github-logo.svg create mode 100644 assets/images/google-logo.svg create mode 100644 assets/images/qr_code_scanner.svg create mode 100644 assets/images/qr_code_scanner2.svg create mode 100644 assets/images/thingsboard.png create mode 100644 assets/images/thingsboard.svg create mode 100644 assets/images/thingsboard_center.svg create mode 100644 assets/images/thingsboard_outer.svg create mode 100644 assets/images/thingsboard_with_title.svg create mode 100644 ios/.gitignore create mode 100644 ios/Flutter/AppFrameworkInfo.plist create mode 100644 ios/Flutter/Debug.xcconfig create mode 100644 ios/Flutter/Release.xcconfig create mode 100644 ios/Podfile create mode 100644 ios/Podfile.lock create mode 100644 ios/Runner.xcodeproj/project.pbxproj create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner/AppDelegate.swift create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 ios/Runner/Base.lproj/Main.storyboard create mode 100644 ios/Runner/Info-Debug.plist create mode 100644 ios/Runner/Info-Release.plist create mode 100644 ios/Runner/Runner-Bridging-Header.h create mode 100644 ios/Runner/Runner.entitlements create mode 100644 ios/Runner/TbWebAuthHandler.swift create mode 100644 lib/config/routes/router.dart create mode 100644 lib/config/themes/tb_theme.dart create mode 100644 lib/constants/app_constants.dart create mode 100644 lib/constants/assets_path.dart create mode 100644 lib/constants/database_keys.dart create mode 100644 lib/core/auth/auth_routes.dart create mode 100644 lib/core/auth/login/login_page.dart create mode 100644 lib/core/auth/login/login_page_background.dart create mode 100644 lib/core/auth/login/reset_password_request_page.dart create mode 100644 lib/core/auth/login/two_factor_authentication_page.dart create mode 100644 lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart create mode 100644 lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart create mode 100644 lib/core/auth/noauth/data/repository/noauth_repository.dart create mode 100644 lib/core/auth/noauth/di/noauth_di.dart create mode 100644 lib/core/auth/noauth/domain/repository/i_noauth_repository.dart create mode 100644 lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/bloc.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/noauth_events.dart create mode 100644 lib/core/auth/noauth/presentation/bloc/noauth_states.dart create mode 100644 lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart create mode 100644 lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart create mode 100644 lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart create mode 100644 lib/core/auth/noauth/routes/noauth_routes.dart create mode 100644 lib/core/auth/oauth2/app_secret_provider.dart create mode 100644 lib/core/auth/oauth2/tb_oauth2_client.dart create mode 100644 lib/core/auth/web/tb_web_auth.dart create mode 100644 lib/core/context/tb_context.dart create mode 100644 lib/core/context/tb_context_widget.dart create mode 100644 lib/core/entity/entities_base.dart create mode 100644 lib/core/entity/entities_grid.dart create mode 100644 lib/core/entity/entities_list.dart create mode 100644 lib/core/entity/entities_list_widget.dart create mode 100644 lib/core/entity/entity_details_page.dart create mode 100644 lib/core/entity/entity_grid_card.dart create mode 100644 lib/core/entity/entity_list_card.dart create mode 100644 lib/core/init/init_app.dart create mode 100644 lib/core/init/init_routes.dart create mode 100644 lib/core/logger/tb_log_output.dart create mode 100644 lib/core/logger/tb_logger.dart create mode 100644 lib/core/logger/tb_logs_filter.dart create mode 100644 lib/firebase_options.dart create mode 100644 lib/generated/intl/messages_all.dart create mode 100644 lib/generated/intl/messages_en.dart create mode 100644 lib/generated/intl/messages_zh.dart create mode 100644 lib/generated/l10n.dart create mode 100644 lib/l10n/intl_en.arb create mode 100644 lib/l10n/intl_zh.arb create mode 100644 lib/locator.dart create mode 100644 lib/main.dart create mode 100644 lib/modules/alarm/alarm_routes.dart create mode 100644 lib/modules/alarm/alarms_base.dart create mode 100644 lib/modules/alarm/alarms_list.dart create mode 100644 lib/modules/alarm/alarms_page.dart create mode 100644 lib/modules/asset/asset_details_page.dart create mode 100644 lib/modules/asset/asset_routes.dart create mode 100644 lib/modules/asset/assets_base.dart create mode 100644 lib/modules/asset/assets_list.dart create mode 100644 lib/modules/asset/assets_list_widget.dart create mode 100644 lib/modules/asset/assets_page.dart create mode 100644 lib/modules/audit_log/audit_log_details_page.dart create mode 100644 lib/modules/audit_log/audit_logs_base.dart create mode 100644 lib/modules/audit_log/audit_logs_list.dart create mode 100644 lib/modules/audit_log/audit_logs_page.dart create mode 100644 lib/modules/audit_log/audit_logs_routes.dart create mode 100644 lib/modules/customer/customer_details_page.dart create mode 100644 lib/modules/customer/customer_routes.dart create mode 100644 lib/modules/customer/customers_base.dart create mode 100644 lib/modules/customer/customers_list.dart create mode 100644 lib/modules/customer/customers_page.dart create mode 100644 lib/modules/dashboard/dashboard.dart create mode 100644 lib/modules/dashboard/dashboard_page.dart create mode 100644 lib/modules/dashboard/dashboard_routes.dart create mode 100644 lib/modules/dashboard/dashboards_base.dart create mode 100644 lib/modules/dashboard/dashboards_grid.dart create mode 100644 lib/modules/dashboard/dashboards_list.dart create mode 100644 lib/modules/dashboard/dashboards_list_widget.dart create mode 100644 lib/modules/dashboard/dashboards_page.dart create mode 100644 lib/modules/dashboard/fullscreen_dashboard_page.dart create mode 100644 lib/modules/dashboard/main_dashboard_page.dart create mode 100644 lib/modules/device/device_details_page.dart create mode 100644 lib/modules/device/device_profiles_base.dart create mode 100644 lib/modules/device/device_profiles_grid.dart create mode 100644 lib/modules/device/device_routes.dart create mode 100644 lib/modules/device/devices_base.dart create mode 100644 lib/modules/device/devices_list.dart create mode 100644 lib/modules/device/devices_list_page.dart create mode 100644 lib/modules/device/devices_list_widget.dart create mode 100644 lib/modules/device/devices_main_page.dart create mode 100644 lib/modules/device/devices_page.dart create mode 100644 lib/modules/home/home_page.dart create mode 100644 lib/modules/home/home_routes.dart create mode 100644 lib/modules/main/main_page.dart create mode 100644 lib/modules/more/more_page.dart create mode 100644 lib/modules/notification/controllers/notification_query_ctrl.dart create mode 100644 lib/modules/notification/notification_page.dart create mode 100644 lib/modules/notification/repository/i_notification_query_repository.dart create mode 100644 lib/modules/notification/repository/notification_pagination_repository.dart create mode 100644 lib/modules/notification/repository/notification_repository.dart create mode 100644 lib/modules/notification/routes/notification_routes.dart create mode 100644 lib/modules/notification/service/i_notifications_local_service.dart create mode 100644 lib/modules/notification/service/notifications_local_service.dart create mode 100644 lib/modules/notification/widgets/filter_segmented_button.dart create mode 100644 lib/modules/notification/widgets/no_notifications_found_widget.dart create mode 100644 lib/modules/notification/widgets/notification_icon.dart create mode 100644 lib/modules/notification/widgets/notification_list.dart create mode 100644 lib/modules/notification/widgets/notification_slidable_widget.dart create mode 100644 lib/modules/notification/widgets/notification_widget.dart create mode 100644 lib/modules/profile/change_password_page.dart create mode 100644 lib/modules/profile/profile_page.dart create mode 100644 lib/modules/profile/profile_routes.dart create mode 100644 lib/modules/tenant/tenant_details_page.dart create mode 100644 lib/modules/tenant/tenant_routes.dart create mode 100644 lib/modules/tenant/tenants_base.dart create mode 100644 lib/modules/tenant/tenants_list.dart create mode 100644 lib/modules/tenant/tenants_page.dart create mode 100644 lib/modules/tenant/tenants_widget.dart create mode 100644 lib/modules/url/url_page.dart create mode 100644 lib/modules/url/url_routes.dart create mode 100644 lib/utils/services/_tb_app_storage.dart create mode 100644 lib/utils/services/_tb_secure_storage.dart create mode 100644 lib/utils/services/_tb_web_local_storage.dart create mode 100644 lib/utils/services/device_profile_cache.dart create mode 100644 lib/utils/services/endpoint/endpoint_service.dart create mode 100644 lib/utils/services/endpoint/i_endpoint_service.dart create mode 100644 lib/utils/services/entity_query_api.dart create mode 100644 lib/utils/services/firebase/firebase_service.dart create mode 100644 lib/utils/services/firebase/i_firebase_service.dart create mode 100644 lib/utils/services/local_database/i_local_database_service.dart create mode 100644 lib/utils/services/local_database/local_database_service.dart create mode 100644 lib/utils/services/notification_service.dart create mode 100644 lib/utils/services/tb_app_storage.dart create mode 100644 lib/utils/services/widget_action_handler.dart create mode 100644 lib/utils/transition/page_transitions.dart create mode 100644 lib/utils/ui/qr_code_scanner.dart create mode 100644 lib/utils/ui_utils_routes.dart create mode 100644 lib/utils/usecase.dart create mode 100644 lib/utils/utils.dart create mode 100644 lib/widgets/tb_app_bar.dart create mode 100644 lib/widgets/tb_progress_indicator.dart create mode 100644 lib/widgets/two_page_view.dart create mode 100644 lib/widgets/two_value_listenable_builder.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/core/noauth/switch_endpoint_test.dart create mode 100644 test/mocks.dart create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..be74985 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: adc687823a831bbebe28bdccfac1a628ca621513 + channel: stable + +project_type: app diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..b37341b --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,63 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 33 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "org.thingsboard.app" + minSdkVersion 21 + targetSdkVersion 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } + + dependencies { + implementation 'androidx.browser:browser:1.0.0' + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..5e6bc52 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b43c30e --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/org/thingsboard/app/KeepAliveService.kt b/android/app/src/main/kotlin/org/thingsboard/app/KeepAliveService.kt new file mode 100644 index 0000000..80d9b4f --- /dev/null +++ b/android/app/src/main/kotlin/org/thingsboard/app/KeepAliveService.kt @@ -0,0 +1,16 @@ +package org.thingsboard.app + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.IBinder + +class KeepAliveService: Service() { + companion object { + val binder = Binder() + } + + override fun onBind(intent: Intent): IBinder { + return binder + } +} diff --git a/android/app/src/main/kotlin/org/thingsboard/app/MainActivity.kt b/android/app/src/main/kotlin/org/thingsboard/app/MainActivity.kt new file mode 100644 index 0000000..85562c4 --- /dev/null +++ b/android/app/src/main/kotlin/org/thingsboard/app/MainActivity.kt @@ -0,0 +1,20 @@ +package org.thingsboard.app + +import androidx.annotation.NonNull +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +class MainActivity: FlutterActivity() { + + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + registerTbWebAuth(flutterEngine) + } + + fun registerTbWebAuth(flutterEngine: FlutterEngine) { + val channel = MethodChannel(flutterEngine.dartExecutor, "tb_web_auth") + channel.setMethodCallHandler(TbWebAuthHandler(this)) + } + +} diff --git a/android/app/src/main/kotlin/org/thingsboard/app/TbWebAuthHandler.kt b/android/app/src/main/kotlin/org/thingsboard/app/TbWebAuthHandler.kt new file mode 100644 index 0000000..8befaf3 --- /dev/null +++ b/android/app/src/main/kotlin/org/thingsboard/app/TbWebAuthHandler.kt @@ -0,0 +1,50 @@ +package org.thingsboard.app + +import android.content.Context +import android.content.Intent +import android.net.Uri + +import androidx.browser.customtabs.CustomTabsIntent + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.Registrar + +class TbWebAuthHandler(private val context: Context): MethodCallHandler { + companion object { + val callbacks = mutableMapOf() + } + + override fun onMethodCall(call: MethodCall, resultCallback: Result) { + when (call.method) { + "authenticate" -> { + val url = Uri.parse(call.argument("url")) + val callbackUrlScheme = call.argument("callbackUrlScheme")!! + val saveHistory = call.argument("saveHistory") + + callbacks[callbackUrlScheme] = resultCallback + + val intent = CustomTabsIntent.Builder().build() + val keepAliveIntent = Intent(context, KeepAliveService::class.java) + + intent.intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + if (saveHistory != null && !saveHistory) { + intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + } + intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent) + + intent.launchUrl(context, url) + } + "cleanUpDanglingCalls" -> { + callbacks.forEach{ (_, danglingResultCallback) -> + danglingResultCallback.error("CANCELED", "User canceled login", null) + } + callbacks.clear() + resultCallback.success(null) + } + else -> resultCallback.notImplemented() + } + } +} diff --git a/android/app/src/main/kotlin/org/thingsboard/app/TbWebCallbackActivity.kt b/android/app/src/main/kotlin/org/thingsboard/app/TbWebCallbackActivity.kt new file mode 100644 index 0000000..20dd9f8 --- /dev/null +++ b/android/app/src/main/kotlin/org/thingsboard/app/TbWebCallbackActivity.kt @@ -0,0 +1,20 @@ +package org.thingsboard.app + +import android.app.Activity +import android.net.Uri +import android.os.Bundle + +class TbWebCallbackActivity: Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val url = intent?.data + val scheme = url?.scheme + + if (scheme != null) { + TbWebAuthHandler.callbacks.remove(scheme)?.success(url.toString()) + } + + finish() + } +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..dbf4c27 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8e42fe6 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d41780ba8cefe8c0226df1c3d8da1a183097471e GIT binary patch literal 6389 zcmVs0=a)s7xXdW(*<8{ntL^LJ~s=gTD8#_4Zm>1aj`V=X`gM-#&o>{}Z(c zF#d;+dIa7yK=l3ar}BH{wNL;+zi=1I@88I?eG>o?Fk)h2xQ>pF#w-?#$K&xF^z`)H znM|e&o6YVG0KFIth8cswU=t)LN=#8y82}WE#o{MYsWhX$zCO9KvhuM+BB{*D$*J=8 z_7*CE_F4dYT>#M1+S5=f=eM^dTu zflw$+Dl03y)u&INd;pNXF0ft)Kui}G7xsk<7hG&?Y(D33IHMU1h6w=Zbpn*Oz(aGZ zrzomIBoh5uTwHv>%ggIgMn;BM29~Cvw>z#o0b)i)Md`V_yZ13OGy6hIOY0*BgQ45* z3c6tcMNth+O--lE%F6a9Cnx9n`uf&)7f{^+5YEBM%gb}=)vMP+4u>PHZ~s1%*@>B=H^x)=g2#BkIn!@o78Y?YwIm+HhUzRScmTV<_1f|V)2cFf`U!< z_Vx*#F}aQc#Bg_a#|SmX!op%7i^Xzyb9?Rlog@;;P05#`eN5PRur0}$)0@8AeAT_%PatmKT zX+;gx)C)0dCNGp;J&fV(w@WnIaAyV~NF^mDpIKX5#{+;ynxqLp#OLJXYaIl1-1N(rX9v2Fp3*gt^??7T&9#qvfK%+0rd?Eyrh)r}iBY&|_ak7;RXy(_;GQIi<{ zxSbA%&t8Mt`j_M*+PEEU%)z~%Eu8=B9tfM7$Y&fj3ychO!MZ0OoNUd($+jo?*~`or zwAgRX&o?zST}w<%oIi5p$fxZ^xAp*H`uO-nJo7iy)(go7 zO?mpzB$hyVRUJ^$W~eBjK!?i#x4t&;ul~JZa9LsbGt2;J(_Ux6> z(a~zNZgl|RXIdpCB@4{W&G#zjIptNgFh3|1s_R~YzOFW`pFaT{YrKMk5SXdkd090|6kxkud*-1%B z1Kr%*qL@tPE1~7mjfb%7cr373EC^cU1*87u+_GSdRC^=h0X2+gs0-UZpF;f7f1609 z(!!LK6kiVykB2g+{mK&C0*JX|#}1>ff%l`n+P!GOeKO3yf zc`cLUqlo-784jMl0@@rc*sx$CjBs=OZ^H+pr=+&F_NRjf4{lh$etos70#_A4B%~)K zB)EBcdd4uB%pR(Cc0B499FMw5%K#I$@^6LOa1Fw;hR!2C%g$ih*3ieFU0uFzOW$$SPqO^o2%6*Guq#?5Cv zeGW$Yx?pR;SJ)ti`r@)m*mNigvYtGL*%OAs9Pi--U=*RK2~J&3gwW_)V5qMHdsokd zKGtR$f!^Mrnwpw0Cnu*RrKP19ijjP45oLgg_FTDgWyq*eqpqmqb8x|d`_4g9#uH#N z88C8)6FA%T1hGU4chmCVVNMZ=WV0p=g+J>gXXb-**J)>1spR#oNTBYa1HD)zK2RE}aZLO^w>yyGEZQ zpA;V-|Ngjf<4}v5a}t>-1B5!QU0q#`+B3Jk*^ivN4q-93TiidJ#UM$aL@FgQ;eF43 zux9RuWP>P~`6aO8yVE4{p$V;;^C7(L<^Uz-)e!Wmc)c@_T#G?L zC=wG*{AT$yu_)APC(?%UY1*HNK z=7k~=(FoKKOdL`4bhz+=XFr%d!4vd!n!!Oq;4WQ%2#3#JB_Nrb7{LnvF>v(U4PtsN zA_E9>D6&XQ4JZIiK7*I~LhoLR{tkT;g4q?HB$Y~^#m2@?^z!n`l#gig0O5Pi)2C18 zSzBB0W-u7YA*&Rut#<$N2gF>zPm;i(MLy8W%m^x~>c}E3%#6Uo%ovy|avt1F?9Eg- zgdo+vBqc_psHrsxX#ods-I|&h>gmA7MP4w{%~1h}`&k7fn}5gM5r(>I76?>rilV6c zf`WpTwzjsR03f1oE)NiM)~s1Z2M!$ghR^3OP)D$11-^FgSyFJmJ=g&P7kH7nOH)Bj z`u%1q>^OEAq%9eDD;`+Sq4P(7MuY+q(WZPb1eVNruf=hvWIcnxeUVU6T@S+tIl$gE z|JHN~DlrlW1V@)GTedbLBBCYa%LBv?4Gp!OF=Ix!j*iaIw(f+_H0J7k*njFz;%{)l zQ$HA@@KhU{#BlzvWXQ-Zf%%h1fUQNVzrgED%Bo<)fpf$$x9EvXS0QdXQle}1sHw|6Xy#lpUoN_pAXM54{_329))Gk{$I)4{=7F;&7^3{wCE zz|qzmR?Qg){rmJnX9C$zp2K%1;vhY@Ia;+Ck?ajt^cxp?5irnCVZwRp_d8Hi*IW$a z-#>V(AE8Wg3(83OvSh|6@OgJIaI{F@vC|@vNCYu4G2pS?xPCb6D79(4nbT|joCnHnLkjRIh@m=O5ML>M=`f0x0L-H^I{ z`?lZk;lt0<(ToNNxdxrW!otm#mX<5kJp$x+7HTdKfy8u4l5kc~mElYe!IB!eD5($J~ z`Ug&4PK5dfOfQ;6p=@qRdng#?(HDXid6T(yx!Qo{jN0>Cd@?i$F|a5KM#*Vo+~JeS z?~tOnJEXF*GKSCRV+B!%nF$RL8m~c3P0dvU1B2$wtPP2}dLOJCV80lqg?%YO`A+0ZUCv+_4^_r zTb2YW`Txy9f9Gmx!SKNjq;MVJ zXhnR9t~RHI>bQ}T16vM7kq(H=s$@%S1<2LGij*MM=51yt?QKipb8Mjl0Qv+Gtw9J7 zpQ0$a$K9bh6$WoVdI@5#KTxYVNMm5qH+_r;$z_^1PmzKY8vyx*<#6l4W4LlBll0Bn z0ta)jpijNQ#lBaY3*L7068!Tp7rtIPiE!fW34_7F$po(eBo>RGu~@9GWm^ES`b`)q;ap z44<=|_%M7REBwc*#YZBMh}mp5qWB6xy!!h32fDhtDjCG~P?!H4ONs42Uxwt2Cj=yA zaPSY<0r_s_3~=sk0ZHjkNZEjWGfWJXrPbxMIx$QpH+|{@eU!3z8*=92My{{>j4B2sSVACA~Lw@}#&NrUd*z9@2(|UNCL6yV~&rfgshy z#AJ*NAQHt;NE9n6DGBY_v*&~^qa&Ljx8MaV*%l5pvZf-glHM=YRP%h^h6Phbk_feG z_Zd=x$V;nV44j z=g%)&SXj)Hk776v`W?g6)YPB>0|qSb4uI%V{WJN`j3+MO&67^WfVxNE8kxNijhvdE?terO=-t}-&Nto>okB1X631DoX1EF7k zLYM`0l9raX&(+m67yxSNJP5~QEv6q86*bkz#|Il}UFz-;f<5*Qght&YKERXPMB$*h)#go1|c}w^dOPOKu?E6HzfI+Pjmpu!oLlePKwTRf8B$#8g?3jjXI~AT=18siOa);B?$w z2nmlT{%6&^aWLfrceSP=5D1cjgM&ZWw{Kq#ZbiPDC*_8xr)Q5-r%r9Nv$Ml$w$o7TdinMEQ3lj)g{cqB})s&b=(+fIy z0j?)!6KB4E?My=Yt23|s{QR()GiPo{OiX;COqk(yq@urh^X5EHPtUDvHoMdO0hSk- ze*@k1LnY#10A7D0vz{UgTpscc%EEMU9+xH(6b5k%40C07%Bn}zPFZ}XtLYSfRcCoX7Zvv*1 z9EcVK??lWkh)YN($8b=PHo>5PT^969zwY|9-RRZO&``8}`}P^zwr#_M41_Ghxn61e zU`>{pnHkczZ{LqP>xY!DLl!aM+h2*Z#JcOt*<*;)#3T+2(eyklYMZZ8uC0)CLr&;y zd=jB-e^@^oTxI7N5u9yDE|Oe_JYgo2(6KGu5CB*(c_b|T_xtT`B`YiIw6n8w09|6J zYWuX9Sx`{W&>E5Gf;9_q9 zEB(hpKc&JGBi6<156IyxB=rq+xg^vha3`W}5x+FJuQd!C z|2~$$(Na8aho@Z&UkFHEgq;#qLX-S-Gcv!Z43_PTAil@lr8hCjW+x7W$3A@KDn!I4 z67arXKa2Fu+LHA2^hgg6j}=0puv|vgw^YfhA{E9F@ac4gKbGQ=V*EG)>`*?AU|$$WDsEHwn{YEl*n<9MVdz;7(f9RHTWmCs59 zFn`lA(sljV_igxM*63GemP)0P%*@OPe}Dhrd-v`kS=jFJ7uxJdZ59|9=&^R~+E7D7 z!=4(h`;85IkX;BXcb^79bshMQawDBp`D8Kvb_Stp(P3{|>6M!Nr2|Rl893JJDz|-P#sH%P0at;YY_l&U~a3}o@hHL#hL+9&^F;TO~M}0E8NbW`!W}_U+p@Zq}?>TTDz$@N3*1*|)}Hy}#HmSJmTL zMFz>fdzu-6leIYv>T3;yoU92$psu!3z9DK(MMXt%L`1}<<;$1H$tYEfTCbAeau;i| zOLDLi>of#u&z?Q+`TP5CH8nMDyU(IA9Ip<%pY@Eide|%$^fWaD2OD!RG13DaZ4Mad zY6DlB(|o$4gHm2zUK|=4x^eaD)ql&tLBG@PH-0p|l{`rNy?ghLo;`c^MiUbgo7bgD z9o&DL!)2QD?BvOlTfhAB%RgliOCxYJF){fYD`--1aPV+HKff=ntgMD;X(`nX-L<j0|%p3hrdU!sSi?Cb~?Ht1CGaqjg~A~GGOl9xicIb z9LDi@JU((WZ&IXEDO6Tg3LZax9CzZxi8F@|9YT@_!#I8)4Efy-gVR|JlHWI-Okx^f z;OOXRI$^>D$0<{$_`ABgjxjbi=Duzxj!w9$s!Eueni_xZ+_|vW*x2ms>}=E;q)kz4 zFhg!L&uQ-=c2y^nKNhdWX=~IFlr0}({29+rdytZ&(k6tUU?onQ68*n~G<~pY$8|SA^v&plSqK!G9~Qm%LcmbN=-*M}@I`}4 zuLmi7+$g@#x#H&O7itI%2>uN}|9|xlmQ+PNI_lAYA!zb18X)>w1c(L+0Z@L?z|rHn zMO#!&^13vrtp(GXMc-HccjdLvo0tEI^S*XayKB00000NkvXXu0mjf Db@Ud< literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/thingsboard.png b/android/app/src/main/res/mipmap-hdpi/thingsboard.png new file mode 100644 index 0000000000000000000000000000000000000000..b3dbfb0e724dd3f165af40e75c3a06892bd0380a GIT binary patch literal 4437 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/thingsboard.png b/android/app/src/main/res/mipmap-xhdpi/thingsboard.png new file mode 100644 index 0000000000000000000000000000000000000000..e5a808c94b2707640b4bd5746f36325f0fd68ba3 GIT binary patch literal 6205 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 bKF0q6#GSsle-Bx|00000NkvXXu0mjfHx7`= literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fb60f249fd82a00a56987152ba8f8ed545744e7f GIT binary patch literal 15293 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 Bjnn`D literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/thingsboard.png b/android/app/src/main/res/mipmap-xxhdpi/thingsboard.png new file mode 100644 index 0000000000000000000000000000000000000000..6fd5c6c18008b8fe97379cb5a95cc313c23d4676 GIT binary patch literal 10254 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 Ur3cv)jsO4v07*qoM6N<$f@b6nTmS$7 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9d28bdeb6f177b4c286886c4b642ac87d264d91e GIT binary patch literal 19738 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/thingsboard.png b/android/app/src/main/res/mipmap-xxxhdpi/thingsboard.png new file mode 100644 index 0000000000000000000000000000000000000000..ba90b6a49ddb0fe9d7e29cb8aa74576036aa2e48 GIT binary patch literal 14895 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..758a3c0 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..fb61d20 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..2439f15 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/app/src/main/res/xml/provider_paths.xml b/android/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..c0c21af --- /dev/null +++ b/android/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..3e3622e --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..9065050 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8bd70f3 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/assets/images/apple-logo.svg b/assets/images/apple-logo.svg new file mode 100644 index 0000000..533382c --- /dev/null +++ b/assets/images/apple-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/dashboard-placeholder.svg b/assets/images/dashboard-placeholder.svg new file mode 100644 index 0000000..a34426a --- /dev/null +++ b/assets/images/dashboard-placeholder.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/device-profile-placeholder.svg b/assets/images/device-profile-placeholder.svg new file mode 100644 index 0000000..88a5d12 --- /dev/null +++ b/assets/images/device-profile-placeholder.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/facebook-logo.svg b/assets/images/facebook-logo.svg new file mode 100644 index 0000000..ae257db --- /dev/null +++ b/assets/images/facebook-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/github-logo.svg b/assets/images/github-logo.svg new file mode 100644 index 0000000..e8dd199 --- /dev/null +++ b/assets/images/github-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/google-logo.svg b/assets/images/google-logo.svg new file mode 100644 index 0000000..585aedd --- /dev/null +++ b/assets/images/google-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/qr_code_scanner.svg b/assets/images/qr_code_scanner.svg new file mode 100644 index 0000000..ec73e77 --- /dev/null +++ b/assets/images/qr_code_scanner.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/qr_code_scanner2.svg b/assets/images/qr_code_scanner2.svg new file mode 100644 index 0000000..6c3e4b3 --- /dev/null +++ b/assets/images/qr_code_scanner2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/thingsboard.png b/assets/images/thingsboard.png new file mode 100644 index 0000000000000000000000000000000000000000..fd172a4b66f6dd1ab6dbcb8b3eca34361011dba8 GIT binary patch literal 21891 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 + + + + + + + + diff --git a/assets/images/thingsboard_center.svg b/assets/images/thingsboard_center.svg new file mode 100644 index 0000000..13f6d48 --- /dev/null +++ b/assets/images/thingsboard_center.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/thingsboard_outer.svg b/assets/images/thingsboard_outer.svg new file mode 100644 index 0000000..8dfa678 --- /dev/null +++ b/assets/images/thingsboard_outer.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/thingsboard_with_title.svg b/assets/images/thingsboard_with_title.svg new file mode 100644 index 0000000..2d98f2f --- /dev/null +++ b/assets/images/thingsboard_with_title.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..279576f --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..beb5349 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,177 @@ +PODS: + - device_info_plus (0.0.1): + - Flutter + - Firebase/CoreOnly (10.18.0): + - FirebaseCore (= 10.18.0) + - Firebase/Messaging (10.18.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 10.18.0) + - firebase_core (2.24.2): + - Firebase/CoreOnly (= 10.18.0) + - Flutter + - firebase_messaging (14.7.10): + - Firebase/Messaging (= 10.18.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) + - Flutter (1.0.0) + - flutter_inappwebview (0.0.1): + - Flutter + - flutter_inappwebview/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview/Core (0.0.1): + - Flutter + - OrderedSet (~> 5.0) + - flutter_local_notifications (0.0.1): + - Flutter + - flutter_secure_storage (6.0.0): + - 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): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.12.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.12.0): + - GoogleUtilities/Environment + - GoogleUtilities/Network (7.12.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.12.0)" + - GoogleUtilities/Reachability (7.12.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.12.0): + - GoogleUtilities/Logger + - 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) + - OrderedSet (5.0.0) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PromisesObjC (2.3.1) + - qr_code_scanner (0.2.0): + - Flutter + - MTBBarcodeScanner + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - 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_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`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - 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`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging + - GoogleDataTransport + - GoogleUtilities + - MTBBarcodeScanner + - nanopb + - OrderedSet + - PromisesObjC + +EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" + Flutter: + :path: Flutter + flutter_inappwebview: + :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" + geolocator_apple: + :path: ".symlinks/plugins/geolocator_apple/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + qr_code_scanner: + :path: ".symlinks/plugins/qr_code_scanner/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 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf + flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 + GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe + GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e + url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b + +PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 + +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5ae1bbf --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,578 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7814FBD9267B343300D19461 /* TbWebAuthHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7814FBD8267B343200D19461 /* TbWebAuthHandler.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B5AC1A002B6A93FB0044773A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AC19FF2B6A93FB0044773A /* StoreKit.framework */; }; + F500F8326F3BBF349D280138 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6358481EC006A0AA65BC8935 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 070516A08514271B237D43F2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6358481EC006A0AA65BC8935 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78018924264ABC4000ABF911 /* Info-Release.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Release.plist"; sourceTree = ""; }; + 78018925264ABC4000ABF911 /* Info-Debug.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Debug.plist"; sourceTree = ""; }; + 7814FBD8267B343200D19461 /* TbWebAuthHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TbWebAuthHandler.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + A298BF81631D8CBA51FE65A6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + AC2915A45ACE7E486FC84F0D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + B50AAC6C2B63E12600CC8CCD /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + B5AC19FF2B6A93FB0044773A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B5AC1A002B6A93FB0044773A /* StoreKit.framework in Frameworks */, + F500F8326F3BBF349D280138 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6DC9651F6A522896561E37DC /* Pods */ = { + isa = PBXGroup; + children = ( + 070516A08514271B237D43F2 /* Pods-Runner.debug.xcconfig */, + A298BF81631D8CBA51FE65A6 /* Pods-Runner.release.xcconfig */, + AC2915A45ACE7E486FC84F0D /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 6DC9651F6A522896561E37DC /* Pods */, + BB4711A7131D70453FCD2E69 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + B50AAC6C2B63E12600CC8CCD /* Runner.entitlements */, + 7814FBD8267B343200D19461 /* TbWebAuthHandler.swift */, + 78018925264ABC4000ABF911 /* Info-Debug.plist */, + 78018924264ABC4000ABF911 /* Info-Release.plist */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + BB4711A7131D70453FCD2E69 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B5AC19FF2B6A93FB0044773A /* StoreKit.framework */, + 6358481EC006A0AA65BC8935 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 7097A270124AE09C59F58A4B /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 675E9C2D528E6C8211A6752E /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 675E9C2D528E6C8211A6752E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7097A270124AE09C59F58A4B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7814FBD9267B343300D19461 /* TbWebAuthHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "Runner/Info-Release.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.thingsboard.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.thingsboard.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.thingsboard.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..b52b2e6 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..bb9634a --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,30 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + self.registerTbWebAuth() + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + private func registerTbWebAuth() { + let controller : FlutterViewController = window?.rootViewController as! FlutterViewController + let channel = FlutterMethodChannel(name: "tb_web_auth", binaryMessenger: controller.binaryMessenger) + let instance = TbWebAuthHandler() + channel.setMethodCallHandler({ + (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + instance.handle(call, result: result) + }) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..feb37de87b85183636499aa4254e23ba472fe562 GIT binary patch literal 68188 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 T-`E1y00000NkvXXu0mjfMrexo literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1f47badc31bc1e71a009dad5caea8af62f018c30 GIT binary patch literal 2426 zcmV-=35E8FP)6mTG+OW=2J+GS%5t{AT7HXSxkkNdawHYykH5M|D4_QR(;3a3Zq=y}Pr&wqO~c|@w!oc7D-=pq zV&MR(c$i$F!qUofN|id23_)EZ=+|*w(K2x#n(>0{RuF*N%h_F5u)I;^pEu@su#`pj zDKrfW!*Wm+nUQpiOQ$E87#$!P3lj=x1OplYUGvmfzfs#QYWKr7uwh!rHIiAsP;#`FeWP!GQZ6 zSVB=`VvzubY4c!l9o?t0klA3n*5t(0FplH!-S^A*b(QI{L1bCt@v|Hses+)1bevya zK7nmJd~xRqiX!vo<&zAjV|{C~&7{J9N0lV-sS2rB2-|V^*H;gjTU@2qFiAv%ym)*J zS(3Q@eTI#~Hg8;vNu(yiaPJX zZVd_{Xc!jj`7(+kGdY?j8upWng*Y`m!pZ4jWLf6a^eBrfcX|JlZ!t`p#g#lw!^W_x zeDuW}(U8v7i!;1(?l_7h@o;IKfbL^rbO4{K^rgvN4R(qv1o`4NPnL5G55)PyTjxo~ z!H+CQgv)29*w`$ykjYW0 z8`zG(?^F2w8|Rpv8s_HRMXulao-gnHz-FmR!>|~LhZ!GA;#2z59_-wM-R5nYHfztf zkR^$UkpTjl4_T5)M1zEbIydet^7vVfd}*5pOIezRjRd3$=Q6Sz~T- zoqVy%(V+yte(emi6GOW`*iUld>8*xoQ7qR8`ZZGV5Q-urNsvxNc>Vm(7>I|tHTRUe zkJhL)EZX}B1et7sLb=9juEa*ZjAOerO`Br5#hq4}ZvF*)9T*Wr&7!CdN`6 z9~$fSC>j=@hFUO9=`19{x;q1&fmF)%#nJkZ=<+%H3mDSu9wj)p#iDSb_ z{`Ag8u3efX9ti@FD^~dE^ZUq>#BYD`A_MWrzDV8|BmlB35%g<&RIT|`W+sQ2937xi zUB$FpwpC@BWGuv5uE^i6-{Ij8S+?sYre!1C)**rcA7^LAxOjS!NGQ`ZMusvyyB6DtLoHu_yOF9)sQCiQ6l?JQ15|-_R?vMa0zuQX$gFSTx`NSk9DOI#X$R__WT79CB#C4!#MO&4TsUzISHNnn#M8A+ zGV29C`1ftf)h4PU5s&zZhW)rg;JPl6pr7+6j!|zK*p5rCP$6I1=20d`A{t_RIJM{1 zx5u^mf&+9-!1epCJ(q zA`BFjWWeuNNY)(gx%StS(<@$R*k zXc#7c|Ia;&l{)Xeb)G9{r>QqgzPz^t64aX}%h@6yT>p-(N}W(ZV`H;|s>%!x#t7&> zdZmeKC)`2BOUEFQke>^uCWwXnOpg!Zx`KEl$abyCg_FmKh61>8mv|(I4%Vz$`8n)w-jD=8Dc^}wroot$>=p_HK z(s@9~73515>J5`x!=PBMky+nl;pqnJn`LaL)qU`5K1|Eu^zjjX^V%7v#|C?r0f+Wx z`UM22HBH|CXV zyuF$`2lf35JJ3$1>4WP>ABeffW2V%wLUs0^#bYD zE;;B~8=_agrkABWL&vLE-A^aA^F@D<8yvJx_j;ay-7wt?b%J#N<{*Fe`rc_Rukk&A s-4WDJ$Nr%I3V2zeR}Z{B-o5vK0nH~Dx4|!i4*&oF07*qoM6N<$g8dS=_5c6? literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3f08d839b719909354917a2549017a8fc1da3430 GIT binary patch literal 4639 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..d625591ec73244cd078ffca1147f4f2d6f635f25 GIT binary patch literal 1515 zcmVJNR7i=nmRoNd#~Fs7ncd}?6e&@pR2?Hzuw~1(Y`Avqv`Jb6 z0n#?T=wYue(+i3UPnLSq?`C&r z=9~9BzbkCpR!S+(6Cp&`cUz~uu9WIJ)4AW?Yp?15D^RClLWr(QdW~uC54uk&g&d^V zY5gGS^|JT9^IZFE=UsZI1%tdgRRUbs!*N}dr!Wj1U6XxY3^KUA*WT9$S_n9fi{rRR zO@dMs%XPN*k2ok+Xqq;QnF;1+(#Uh7GD!5^gaZ_c6~0@0&DPE#p@0v7y@MjA?GW@E zBw}Gq%V}9B`XhBN=&5%nN{-|5`1xxdJl!Ofh_d!_k7A|Hz3Ugad1;ztB1$OWBNX(V znfu;wcSf|YdL7r})oy|0P=rrEyw2mb*F0IzaeMJS=BHEWnuZY2Fm0-KlW@R~Y1!D0 zOC%IP%6`CJ$NE5}l!SslHgX3%T7AjZUV+hg6r~hje)oc@^e~2@^S7^8Ij%Oie|v$g z-8_Du&c}DJ5DfK-*PTsWtdvbB~ckguB<~h(|*_c)H2swH&79u)Mm3X*sQ^ zB*Ogeqa~V_L%mrhH5^6LL|>n`zfVDh5O_*aEZ5lHFLL*KhC5ehu^kV;PbV4;* zI!(3F;zFEn#{kDkhxZGKsh=l#bA_0PaAGNwkp;%)y z9w8bIymLC8)=g);m8WQ0Hi=l6P|y!R)3i7|u94d>a$GeL2!a8F+?yk^+XtAIjcK`< zw#&-KE?<4Wj_tSrc%H)ZR9``LO(et6S=c9GLd77oswDB2|Yx5J_ zTe?6j9N@Ux(#&`2GDG{PEX6V{9Zwxz@n*JPM^Mb<-l1h;*@$QmR$GgZQzv4vC>Kzy07ELP(B| zYi#E7{N<}>lxq#r$q3`g7$fm83-66FJ3d0S(PU%$fG6wQ_zi=}^e~$2ZXvxBtZP8i z%HjJ<8JecWmygy-5675HC3v{H#oT0)Pu{;m)3ka0=7`I4b#@O*EU#wC7pvrt%G_JZ zkcfp&ZtDRZMB1%fL%^qF*$zX|Aiw$GD(R6SF3zTj$3om%n8CCi3Z)vB?eOhNmZoL1 zc;P2}Z`w#9PU?eh(DQgu2th0yvM!f;hQtcqqbuUhQ*Os&M1tG&e8L zuwN)sZ(4X>|NVUfzn2}_9t-hfUUaS1hxLX<#7%-qF~1f&_Pp)-qZ+OU52moRr5YyOLBY z$sd}BR4NZCJGL_}*|KLeGukCm6eaEiLF`+jw>&gJfnYaCY2wPMViD->d+*uL@0^BU zS(Xdnx-I~MATavGjdiLF-)CMX)T(oy1rW1H;YUM=3#89Y1r@+EKp7pB?p#@%_ZnO~wxny8a<_ zkx)Ym4=ptQ{J2nl3YnyIarA+UEIWGmsP_CAyT}tp_x_N4@V+DmF*532el9ZlKZyKX z=%Tfwt~V((E()9Rd-C|A@eTYwk1`Xa1iyQJjEraBAo6qNptJlkvtXL0`#wo!QX&~A z$MM|Kab0j-5ClX~KoZX-`*TA5kj(GrJ@P2??{m;Oi+C4tTo=o7F#0B4!=!8U>Gn+e zrUgJe8e(=TjUr2@`}|)dyNmeujnSZm-XGYugK60~u8SmzD6)hA^i7LKyT@_0$$qKE zQKi92txdhvq1!V7SY4RnUw{4ysd$(m@%)s05s(>I*m#@=3OcHlp^&C!Q)_lPJgIYB zX=3y(qG63}GK#9mlqyZO_KtbISLX1zPP5a)vK<6K6h$;eCZCC6+x9tzE@FIA4haGc zqxa7V>Q{6DOv~n=T;tp4+kF3Wm;F)=)3OL_DpQ#Rp^(B+xlXCl#Bp5Wkq~o*G}&Z? zR6If?q@jlt3b`b)NGQ;n;|jPaj4qSRPLq+JR>rkfuC=)L{VRU+=NBB78^og_bXB3= z>QZZVv1|ujQ<%*sSzVZ7Wj044mmn1j6V^2pSwfOTR7FBlm2-jy;aop7^&xflw7iUO zHu~MNZAvE%9rMyU9$V(S@^=NJDOeJ9VXxcebdIYEEGj15e*a7HFQlS9@R<2 zbYw+FKtPhb8fjYg>32bRw|C#PDc4%qj!P=06VWvU!P~#q?9l4;h(~mi@$hL0`cN88 zrg9LXcqcrFqChmP5z#eHYAv>Qk6D~flS)L`KCG~PP{FYs5;2{SrqFEnC{-FrqCg>= zBp%ffa4`&%*L!7t_svUe+hOhMEVozZ2x$uYr5X>OzvZA@9w=b97SYcVEAxWAyp# z#v*Ii=1~=i*E`4j>i#o+`}Ip)*JbChLeH=$Wa51HVw?Mqw@_q><=LEXI0XqC)VPn{ z4+QmRFMuqGsEUm3xa^nfw7Le0BH=nNiI~n$ZZC6tb(Ta_r{3()>zh2;*yFg`!Z0nu zn!+C*ZgTJ8YmQEu0QCCXv^oa)bd2r83IJ=@=D508Afjv7j*H{iNTP@&iGfpyNxTn3 zQ69Z8EHUF+>6;cuCru8H>*$)o%52Ua8x%whk%el4Rl$ z9>3b-!SlC7LMk_}6fY%M(A42fj0PbSqi?cbs&Q~!Cq8h~Y%+?bDmacyr`PAv#x5_m z4p^O^;_liN3Yi33du8rFe$DoNg}&u5+*wy;zPP=@-+gf%%XU~SX8FyZUhsOi%;Swc z+8u* zjz?&94DLVLqT4em=99d5bI60|+Z@+gIF5^ehmCTnC?8*+V__zPCz@80=_h`0zjGMu4P&clKK#QJhZe0(Wb$fk8Q6Li1P~`L8ZrKivR*!nC zOV6;dEQfIi5%4@@_o&Lh|Moj-jW)l$d-F65Y{xxa<2uml8a&?EBciLs!Xa+2&Jxm; z5AI%k9sg8Y!a!O;Q)KjzGQhI$6w)(HUTv57?bk1PxV}SUO#1>Jj8tTSsIHPuM93tf zB%)zdMJ5#wlS+gcA0s&qv^sru4l5j;G%zje9G}Bcl%L$=9y$>MilRU~8e(-}ihL$U zA(uo|q|=VWv~1oUobca&dd|Ivn;e~Zi3h;-LKsyR$R?xAPN!HdW?7ldvpSzApN^xb zGR=;GWqYM(u*CZ;Kscl@HeQBCVSjDkjO4SBC2NgDU zN<4pkz>7CWw7Mp^gC;@{NX2zNzq!ai|7?w$SBmJG%Fw$E)8fgiJ-&LdP9m!F<>%L! zE2IO34XmT1v8u=t$#~eier7Sql0+h^lZ=H?@@RdR zCmVaLZy#}V(xlq#(CzgxO|SeQc@wj3JmEhyt1-p=&aI z)5fwK92ZXNZFWmF7G^Tc7Se=6D($Xy#sZQgkV!=NXk~_l=`=e>RkjaK_|+eup(!$- zTwfrftAnXOw7Y$(jW(_eiKvdQy{qk`Jf5-WMR;*k=<)xSWpi*`=l-MDJl)tMq$*rn zDllJ2p@(Ed5js7uM05?4R6I;Lqz>%D0OSi~;$c3$KF>e?^cMg0w|DsE-5Xq8C{S;8 z*xD=8?)LB^N5HWiYRwMEwKlROl1WCI<2NTw#Ye5#?ixJX+~=F8Z*X1s z;?6Qx=BL;`sPOx5H`v-QW12RbyT?3#z0XOljb%G$0!mdRR_1d2&8Mr}`Oz%#sP1KV zMPY02m|{MKrpO4w0DJdM4$5^-YAvE+jj41TT{{o>hx`vi)3;5VM5FgD+o9VxNyfsg z{bM{MkMYYixOzyoAjiyRO!y1Wb7)_OtC6RPI z!Y9|}xw<%oB1`BY)q4fvxRh&6Hg}I{w0c}$DKI@~3;k%+V5cz}G=%btS@4V!38~z= zQe-xtVk#4-kcpqht&7)ShjoQc&*a(Wfj2sJT(3^XbY^mKOv_=fRO6ecZ-|C9R_F3) znu4yY$g)VYW3YQvrE8ejj>DV%6JEV7AxZ+v#T@xmY|z>GzhM~Cg%>gJ5^OG$$w@V~rVU~(n_K)lAm1-D$&uvq2ozHG8@X3vNs`WPaA8+y9 zi)|vhMk*GjkW0`vZT3o4p1j&)bLV&fEnRAj4hP3I=BG2P%w~y2LW4wfUdT~gAYb-H z?D#_0^(>>QGOjD(x-PXwi>I4=eE(txNfP<|))H423#`9A;;RQ6>>O5+Wsx7>TI3gB ze8jb-0*zJ=S(bRbu}7uRrqwm*_bv8HHNN_Go%@eoQ)_k+MFG>YX|{W)io|p-K|T{l z9kg)clfdz^DEkQ#M3a(~i&w}k+aaHcu~f|R<6BG2suaL z3aKPxVZQwQI?Kf@@&JW3I|h$7cKPF@*R(o4KDsi^Y(7P`(c$sN9@S=>zG-3G4sad^ z`0*?IG96&upzDGlAOi8I&f3*EmS?@#$)#hcicC@U25Q%qrqMN-#bVBbbuj2>ljPGe za9u=6Wc#4PtDO?{W|v#5MgHNZx0oxWsn*-XA|d|g!8(U!@7?SCw0$}V7>BTCGwd#=|mJ!6p*EN9T0##c+&>Zw`@ASKJ{i7NfKF@ zPO~_jAr+6HYYI!n9N)d%q1`oTbqpN){8Yh@w^NJy`S-YXE^=*A6c9y;A-W#jTM|V? zMI7uIyocjB*tVCoP0OO*>e6U;(KUs3*P!0&&^IkqS@v4Si$_TSmn$z92^sXv`IzC{ zb6(@@V%ZKo!=zkmvVC|$z15}O>hW-Wmv}^HVaBV?-#mRoyJN7pk|CRlUf7485pLIB{tA`{B zEKH}F$|UF+CWj|=udK*2*O#YRyE@0()p_qNU4SS04#$Gf&-eyG$!h%GB(G?-dwlg^ zooAZ|7^da@76}(c5{ZUG+*p}rxtL{XHcLJgr*B!5t1Vg`15K65rQ=Lz<0PVC6#0?? zijV6CfrcQ|aS@#X8Gs~7B%)!msVD#;O(hxA$t0poW#Y`_668`b;*k)lBICL)OT`?v z?I4QYcvTYLy<8sUZBmPQW@a=ALVakgj*n3-+hO;pLZj6KKviXQO+^nW=$eA2D9DmH zcwrUXV+IWc0?S{&-*-Rct@1@ejeZ~3uw~n)uPG1&L{U5)iw>T;Xkc(D$T~a^0xTL- zCMJP}q03z6@c$2*7|o;eBtIHYM3zH^e5HtN#ysuI6md`9h;0l+RH*^Aov?U{nwz^+Lo70J9azW z?M_Z6C+DOyA0}V&$ILm&Op;D_kDWMis~yYj-RelT_I;rwN~E~&3rG;eTFZPW00O`& zEEMI=+z%p9cy;f6_uY5j-Kr!gilVWBplKR{AgsAA2m+e6mQ?=u^cW)?#lVP0`dU6- zBf9IMX#{Cn;*;EL1ZE7H-IG6F5QNON?9c0kY@uf3GNM}we`X3|be~|Pxf{8Pe+bCd z&Zjk(Qt*mqQa3|bXywd)-zX?YNM?K(nT?I)B^$r^q{y>Za3i*HK7&vCd$H~Rq_pxO z8-G!oo6l&rQ1g*(l;##&p#NV?R#D-WGm%m?x701W9v(`CQZ=?*+Bs%nBN-|cNV~;>B_&h5xYt%RZ|KjJj#Wf@DxxKl|({jH5Mlv ziLRm7>W=KMNrl6FVHB4zZlIm2ikfyAeKlnFgnY%`=i``FwVkK7!atg z^w85@&w(8s)K^t7HXY>a3wODCcZ7-AC1UX;ng$49l`OcOHaspnZigMG!;0M|VY5nf zwbrt?x7|={pC*)&pElaLIVs2W2sMo~8c%ZW#sL5R^%Wir&#4jbL= zb+pw7csMr8z{o7IgbW&p76GrzPD@=So0@BBZ3xg*>!;G^#_Mumw@FAA3m%u93Xc=X zvaS@gDLhS=Fvrg|6L9*v(P0)($eg`7z(1b&mWLy=$f|}_60urz4NJ-jvZ5jg0+K~U zmK6Z(Hi_DRmt8&0JhQcpEgcQiRC;kbY*-}=7Eu5M(!Rp?>6(?QXvM}A^mEgYA0lQW zeKax0iLbBmaC8n0s10~HxTAya);a_P?hQ|I{oW`u3n3Co1p#Pp3h>-BT^!xrNl!-u z9;Y2q6v^iEu|}C680Gk)Lp4o9QB-a{7^i=D8bwj53AlOl zh26Y$Y&X?@FQ75A5aP@8eSGl6W#)qsL{Z@IuFd@HjYCxX-AEQ=8(K=etyC!2H8XO< z)Zzy!{c=o$lzmtkW6eB(ob! zA?UK(sIT(rUVx%-e|U=VnMJzW>JUZU;wXyB#OxCHhNh4e1wnxNDjzO~4bTM^iN%S< zWjroBcAI3l6_c{UgOM3dUwuGud6k1ZHgR~@X53C&)= zpu4ReQ7r8|*~m!G)LY2bW;0@To5Yrmdd^(!XMQ=I{h z3=m5sxji_6qN+Tz^?;YgG(FWlu{KD)%^+!FQGJ`U~N#O9V-M6vL}nXk#EdJKh> zFh{sXDaL{+HBn&CmR7cPHgWd)0E?jrpPatM(BwRwEwzBaz}O7^L(?p+M6p^d?Cff0 z@3uB977;X!p~-npd~=o2=|yh#kFgw%a{TZ%+%7u;x}Q8SI>WC{p69}?N32E@Xu#m; z9KSqy4nj?d#lmgjje zI?Mn4eKC)fk>y;7Z>JvAB%GCUJDnW`1&fKTd~@ zXgmRc({9D-upt^$9MWKwEF9UjnRrs>Uq8RZy`gEQ=0gaAuA#XM@mQdFze%FVwoMK6 zwAJNg&ooVs6u&sv$LXv6wA2Uq)7KBu-B!1Tr~KmPlTbOaN*EEf2Ji(3oqfE>$ zfd(}JFGqKGQt5N!aoMT%d#I`O;&C|;1%afj5Q`^KQc0YAa5`)p+PR4r_x0ekTT`_1 znE_D{sHt>QUtNJs%4I*Ps$9K0!iC#IL}G~yuS+XYetYUV|Nhk#Zatjf`u$NR=ax*P ztqpU<%JuIAK@VZ)7FW1_f0SrEiD(hn+1<>R_WD#dD>uorxV*|Ym+mpM5aOAwZS3FH zp0i~kyQR|$q9*liNsZS^$P_)E?J=0}jFRwN{|ur42I_BB;e8Jk&T zWO4xw@Ve~m?QO&3a;&l4ilTCFXo?TMxXko?h|7J$*dzf^=C;>Y8(@%-Lyjvd^B!)|4Bb1kh6Rebv0ZAPaSQ8f)k)fgC?Ny3Oha6a4eXXNe_}RC--}`1MtD!2$^2uvyvJ-AsE^b!HI-L7>9z#hez1n*@Toj>`JQOmKm{SLrKZ_$oT>R`WJSg8 zw9!%@$Q(jO-iF(0=as{~L}Lj~oVv#N%o2A7Ci&ILb2uF~?ha0w3pNFn6)tvkHRXiH z>4In|93{9CK~Z#bl79|L7Lje68`<1klM$@Zyo|0H`L<>uaKQ-84DuNWu0)t$3ZtqT z9;cn=x=O58b5w#LQ0aB?$1m-{>v8f=AAQTzT!_1mrV#*>f^HT9a5}7bT@KV#X~|}_ zplKQdW3!z2<{JNbrJralkz$w}W1W;0et)^2+CT*_AL_;HvKLH%8@Xp@CbE#MO6n}n z9}^14h(r?z0=Qjvs(kLEStmgd@Ozyc+rNcpcXwg8Nod+y!RCC;Dx=K@SNP`QJwE&H zHfOH(6OP0Ih{coKADU)iY4tI#JXSOf9!<>i!55eL_G&-Tcw$ZEwv_qjdk=-i!a7Uq z;Yf^FJPDc}BzRm71e01XfoDF8S@EYc_KWmSL9a@d^#rZzYTzg1+Q#BpU zHEeFKq1x}EHsIy%VBxkWSymZLc_db=#It+4a5-#6=bFvGnYNqZ$T>1g5v&G8V+j(n zZX@+_jzqDJE?H5z`)HE?bK(q_`bJ3R3s*Bnkx^a^BC#aFl?c;wOUy2W0PuO7Y-*~; zDp@EdCq>bdJs+LA&gj%)Cdep9d&_~c3Y2Nk8PX?hvr2}U{_KTP6qSL|8GiZ6dCuJ! zAdxDs=W=gACv{mA1?)CS&%o9C=?{Gt;TrsKvgwXB5|ULj#W;*!pLkcTv>) zEuL6&v!T#d+1}a2&)<2DU?{>fy=_!@oR24vce+l@2KmibSGjQO5wWDaHnWwUb?TZ$ z6liMnBj*#%_}k1p!4>2}k10E`}Hwo#oV}d#uJ11*Izp;P*Ot z?dW#?==mK~`rM`hlNFVri8-$H4O3a+=D_xL0zQuk-Xb-$av|VzbL_xY6jj42S#msu z{H)UOONRp-K&4M7{aXF~)I1uIpv{e7t8g~o*q98z^!o!ZvW*Vw}0EAX!j7{qa`FoG1 z7#yEtdVZOe)fkDSl9M^h$wa0I34qIK6} zrIKWlziZ1DZXpAo|C=D_Re`sT?P2BQdB$ggs4BUKSC(*84VJ@EmR2IDDc9|ri}(28 zi_6R{t`LbQ^xQ$_&`HtBpJdi=4!guFhkN24y zOv}}NElc~e{s3UNS!u4V#Ov1c$~ok;wY?eu@uW;Fp*LOItkRm$zEB1+8`cQ7ltf1U z_hOX{kIT+0hkE(xYx`-buPhjD#1k@)#^+d#B@hL@##ZHbuN`O17Gc5Y@% zN4>eTFX$)r*mRKK@~Yl7S>>gv)^C1KD)C}AGRC0ec8V3PfP8$_oXU^DCE6GKlpe#j*W{|BF%PUd7z49Q_98w75#9WYx zxg}Ip#piL-lPZrFg0!jdd7Zp5 zJk#HkNrj2oAmKyu6pq%{4r`yNh>U+fP?Z z4VtDgHWTEtv$q+a39hNnrG*!XCm5YxB%YLYKC69rT=t@oTK?H12txS<%5q_OCCX=K zZ}Z+~7a5ybL_=doXCr^}?o0gTA0MHq&QHElMxzNHjn5K}#IaZeY6D)}PWxKtHS4qA z>*SA~+rgi`agfGZKNgEfg~!QD`+NAyw~o@$RE5=QA(2#&6$P`1V=frx(fAxmS;6J7 z($Q3n+hI49MgCb+c0RXI1d?pvv!|v$YnREmurtMa+_go4&bN$4wzmK~*)T=R+)o zB1n>jon1}*?6m`#h7GG^;pIcU0Q}9(r%j3yrl{mCx6MMI|AxRd~=h#*vF|OVnVP!S0KVHySMQdGPO}(vH5i>2b zb>$X}5N2P?Ah(F@>S^Hz2e+cADz6{ik<$sI*T5X??QP?mOZS+b3uQKITvoU{IE^5t z66)EmSIRpOQ?!CB5l)`IiN|H<$ga*z*JCzFl4VVaCmR%1<^J##w;qn8s2VPZl|5Tp zsR?*N*;J!cD8iXGcmiw)O?6L0~5Ue=|yak#Nl0=*L2+Enk$u9Jdx>cciL@4V{y*k z7-Vj71wnwuT0gtHTX5K|h8kLm8!D|=u%!?X1U;EvU+pv8HcKlJK0bYu6W?58VQCdf zvQS&;B^FQUPfO-$YZ}}R8?PPR#+xth=E~g>KK$w`6SGV74NUUORMB&HPfJF?X-!i# zjoHNzH~Po8KQztEg5EM!lS*NJcVm!PLdId2*uSlfw#Mo;QC&H*S$EJ;BT>zik!7n~O$HdGMHy@1i>rc*8Tj`~(u_{9|5=(OJ-Z206D&XqpO*%9Sx|e#=m}kiLvP*YAWdPdz~CV+{;@p?V-l+MGyqMZU-;y+rn}<#&1tu zXKH?#XgonORh19~z3b;n-w?n4-39JEoFJZ5G6Dr%qm!vFAG=MYcT)o`4OND7$cI8T zwKbjPW;c`~yh6lt%08;f?BWW`;TTT4m0dl}{QO7H>kS(g5u4S`>(B1MDp`2%cNdtN zU&iNga{TZ%{^XUtIcdXmRr$>qcTrR6rSH)MJG+``sPScZm|j@sgD);~`@saVq7d-u zQD0|k9cUVZW3${Dm}DgqM^;p(=9gJoiBOeFsF&k0Wq&VNLo2l-r5M3{uvsPc^|mt? zT)`p=96z$19jQ(jeIgn@k6tzKxE$OW7^k_ul4o~!(NOKr2sXPS>-V}iex#Qd_H|>o zN*P?TqH?8gm|G9WNy-WVpNqGDu!q-AD;?UfA2s{%vj8Y*t)$ z>l(pkLQqIU*0b2@T9_;KOopmzT)jKO$rs)Zza3n_m@HF4u z7~tHkLA|J{!D1-FZ%3GWF5VhqaXErT6zS=xXIFQNVS?VQIBCdt zDQ^mSnow%`F8>}q38B9_OI0;wStb(G-}ZEWc#3OxN4WW5oY}<{;z_;BN3vK@6cts~ zkR&ma$dOZTGZ96B?$%oV>POFUG}Vb&3<}v~Q$M$yJnoY~$%iALC<>u)jBq4|tf<5j zGAof7!O$ubvrCLlE%Io5ma&;2E30u7RYMTKVbfng)YV$Y$n+u)hNoGLCG<{B^z53? zRbyykp5L9h#lYwc30WbQkcq~UnU}rA z6Ed=_=;5-UKg8Ww>t|nY8!sQ+N=to!>4gxN?hMgCJk5MCjHYQ+S9;jl)y$qPt#r25 zWlHI0{FOW>S(df$+Y~_gCMceeIdSS5|L~h{39dwR9I1bVRBqQ|5pg-JRQo-&H&*k^ z)>d})w9r~#m3ei5rs=O2h{O^^V@W{6>9FB;+6y+^m7`QSiJSZrznoaQ7~yQ2S_oG$ zn=6U}feIIuK3AsfZL>-^Y*yS(8`XXfjWvGSo2uz-siCF5ikg52r_H(sug$8ztIY2$ zNmi!w%%5AnYbMCDtYu&LLqk5J*%Jtc^tgX&ehEPkaXR#d4WGw}zru;v>A>Z*VYf<% z7BN#n_`c_sIiQV%YgS=+noQ<<4ds+m;QKp(OMx^mqEP^_9wCGBYr<^Z9&~GsY}kV00}Lf;_F*em!H$3BmJ~ zW4$?y^)6;1bF}XN;lvEqv(=2C)SXgDv&!;GO>(m3LN?8feWhm}Y}CT7H-B7?sVeun z8J&`n<^L66qqWlrSE1|Wct}QkJh5=sW2@Hdjbz)^CppDSp#|mHXQQ!CEZkzJr1<72k46=XJhN$)i#z|irfD1BLRx4G8%15E;wd)H3}2~f%=J`F@@cM_`$ql` zCx3{1@b^u(99pIY%2Eks!_%1HM!0PoWsge@xY9g5kkU^RIio&W#<07*qoM6N<$f>qYcH2?qr literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..1f47badc31bc1e71a009dad5caea8af62f018c30 GIT binary patch literal 2426 zcmV-=35E8FP)6mTG+OW=2J+GS%5t{AT7HXSxkkNdawHYykH5M|D4_QR(;3a3Zq=y}Pr&wqO~c|@w!oc7D-=pq zV&MR(c$i$F!qUofN|id23_)EZ=+|*w(K2x#n(>0{RuF*N%h_F5u)I;^pEu@su#`pj zDKrfW!*Wm+nUQpiOQ$E87#$!P3lj=x1OplYUGvmfzfs#QYWKr7uwh!rHIiAsP;#`FeWP!GQZ6 zSVB=`VvzubY4c!l9o?t0klA3n*5t(0FplH!-S^A*b(QI{L1bCt@v|Hses+)1bevya zK7nmJd~xRqiX!vo<&zAjV|{C~&7{J9N0lV-sS2rB2-|V^*H;gjTU@2qFiAv%ym)*J zS(3Q@eTI#~Hg8;vNu(yiaPJX zZVd_{Xc!jj`7(+kGdY?j8upWng*Y`m!pZ4jWLf6a^eBrfcX|JlZ!t`p#g#lw!^W_x zeDuW}(U8v7i!;1(?l_7h@o;IKfbL^rbO4{K^rgvN4R(qv1o`4NPnL5G55)PyTjxo~ z!H+CQgv)29*w`$ykjYW0 z8`zG(?^F2w8|Rpv8s_HRMXulao-gnHz-FmR!>|~LhZ!GA;#2z59_-wM-R5nYHfztf zkR^$UkpTjl4_T5)M1zEbIydet^7vVfd}*5pOIezRjRd3$=Q6Sz~T- zoqVy%(V+yte(emi6GOW`*iUld>8*xoQ7qR8`ZZGV5Q-urNsvxNc>Vm(7>I|tHTRUe zkJhL)EZX}B1et7sLb=9juEa*ZjAOerO`Br5#hq4}ZvF*)9T*Wr&7!CdN`6 z9~$fSC>j=@hFUO9=`19{x;q1&fmF)%#nJkZ=<+%H3mDSu9wj)p#iDSb_ z{`Ag8u3efX9ti@FD^~dE^ZUq>#BYD`A_MWrzDV8|BmlB35%g<&RIT|`W+sQ2937xi zUB$FpwpC@BWGuv5uE^i6-{Ij8S+?sYre!1C)**rcA7^LAxOjS!NGQ`ZMusvyyB6DtLoHu_yOF9)sQCiQ6l?JQ15|-_R?vMa0zuQX$gFSTx`NSk9DOI#X$R__WT79CB#C4!#MO&4TsUzISHNnn#M8A+ zGV29C`1ftf)h4PU5s&zZhW)rg;JPl6pr7+6j!|zK*p5rCP$6I1=20d`A{t_RIJM{1 zx5u^mf&+9-!1epCJ(q zA`BFjWWeuNNY)(gx%StS(<@$R*k zXc#7c|Ia;&l{)Xeb)G9{r>QqgzPz^t64aX}%h@6yT>p-(N}W(ZV`H;|s>%!x#t7&> zdZmeKC)`2BOUEFQke>^uCWwXnOpg!Zx`KEl$abyCg_FmKh61>8mv|(I4%Vz$`8n)w-jD=8Dc^}wroot$>=p_HK z(s@9~73515>J5`x!=PBMky+nl;pqnJn`LaL)qU`5K1|Eu^zjjX^V%7v#|C?r0f+Wx z`UM22HBH|CXV zyuF$`2lf35JJ3$1>4WP>ABeffW2V%wLUs0^#bYD zE;;B~8=_agrkABWL&vLE-A^aA^F@D<8yvJx_j;ay-7wt?b%J#N<{*Fe`rc_Rukk&A s-4WDJ$Nr%I3V2zeR}Z{B-o5vK0nH~Dx4|!i4*&oF07*qoM6N<$g8dS=_5c6? literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..89c0960194ce4a71ddea557238a13fc269f6ff1a GIT binary patch literal 7046 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 b-0^ano6-sLn(G2T)-cqSbs)9RUxoe;JI}n@ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..bd262307df6a3c38a78b84420d7671b227080bcb GIT binary patch literal 12326 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 IB~3#92OuTDNdN!< literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bd262307df6a3c38a78b84420d7671b227080bcb GIT binary patch literal 12326 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 IB~3#92OuTDNdN!< literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..29833844ffa001fce7d13585f6ae0c8f65f80e1f GIT binary patch literal 18688 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%0D4I&$ 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 Ut}cIp|9yaxkx&$`6g3F^KR(iE761SM literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..02ed5bd39e0f82a785e091738474e2ea17917e0b GIT binary patch literal 16927 zcmXw>WmFq&*M)I+EiMi2?hb|E?pnNf@#0d76nA%b0u*;EF2&uwxD@^7dDr@Wg!~#Z znYrhjeXhOlM5rpuqN5O_KtVyFgXN^vf%nq?UPuVQUpE59GvE!`Sx(Ot3JPQ3zZY~e z69x$s6eSc`T0+w+>%7xCiF7!b_Q^8kqK0>VgRjkuM7$q|Qv3sPcut_SI&sXvyz^d@ zkPt-hV)z;2Wc@gq^DyZxcAu49IwSv4tw8Z7DWVF7| zxfxqk6&d++X5@AU8QiXNb47}o#aY(R);8izsj%Pzo3z(Zd-k;?(-NHJD9!EQpGpQ__B(OHx|Jbkn4B2g*;sz z-9{;C>!!EiT`*pE*$^kw#{&8@sA#t8)(|AK&~Q8WqxH|Y9yRtC7PG6T6Y=IJXS$EcG#5R>*4Fa!E@bK=e(je z*O~^~saWIY1zD93uM+&D=xifg&;j8lSGkGW`bF%8?K$wbXqy3}-FyTVoY3%{i{dvM zoQ8H6Xv{a!O^-H|cZM3)a(_30+xwjNhG(KNj>sXUZg2Kb+kIS-Jr)u2VeqH5Mry>* z1yuN=W}K@|>0m}Ch|&UNHi7!B$S5&eL6Zq5n@w9fhbc$$+6EpTH5{t&Bik2@I~BF_ z%*rr=i0hm0gL|78b-rp$Pya5p%6Q|w_9UrGc#4fWwACt-FB6FWprsmZ#=SXMjoOOJ z(30AzN3~E1`%72hM|I(+F;H-AA4bDRS>wbe4l+lvPbnJw3uo@IQp*TWXGbO1Rilm8 z(DaFv?rh!oMXU>f`hG7VGZYKY&5|;m0WYeZjzhkw?OS|&qaCihNHEt;c?|v8Dx8^4 z@cFp8Ze{q|T#sM+5qJN`5KSK50U-e&noN+5A1&FS|hKHb7YWmbp_F^e@SDFz!#O>du zo*cP1w;Q>|Oxa`%FC(|^4DJkRk;mNF-Uz15rL!+y8b3a&AGBDk?3WOX-L)2jCCH6EobfdR@ z7&LuNL-uD0yUGwQp67JjSD1-~N>W8DsHlK4UXGeT#gw-p@XGW<|1K3;w2Ha!7X;in zSEPw?GeP0l5nr^(ioRWs&#%|C!mnFaRYnVNKcrr^VboZhv>fZ<4W?3)1Vr6^zBbmNKdjMNE6an3ZQuPG(1>A zYEx5C_)NLp%iZF=>3PE^*xcNqty#CTX%@}d&A%3qirtq+Vl$dh>b}WQmjsw8d4rj} z3dX)0HARF&c5d>%?<9WwKGlTlu)9bTlOb9P34yohlJr!z0g)(%r{qKXt06~*8v|!A zhR*`IXYk8(+W)=~I0*S3Cj8F#&bJjf=l18&z;8VewcSGYX3pu6rvDifPszG*GJLGC z-m<{+b3_u&g78QS@=d0=t28vBgFK23JDMgfTLtkGwOW%=9ZFgi+(r#NQ)siLo(2wq z!5=-JLQ*>37QGOzUdIq&&$>nbfV==|D2zQ-teq zqYeI@Kh21CM`f!0g@3pHq#`j1v} zerGv~T?+5#{RBZg?{(V_)EWLZL^|ic`fQ?IYtbr>=#<>JK{=UucqY6~9h*2?9)eqL zBXJtX^r>S(Z55otzEi|i1~UT=8eI&MsauO^DAZY1febpdbdqMILIJ45 zlj{Tf1|ew~=~A5K#b0M>bqLv!>F>Sks)~sX78(bkoNp?sXYI#eR&kibw;$!lm~O0a6hk)WXC$A}Z<8 z`NA1_A_V5VP)SHCBq6_tN92^VK*4dxNiK#|Adx+h!OI8}VIlcFKE z8&%W#E9(u$mVXt+dltU*Hgpywa~|OYF*HR|f#WRicc9tAAt!Xk&ejA~y2wd3Af?p9 zE89{BW>Rg|z+z_teWjaYD4h$monk)at^O#ZG=+3091x{~?j2i2MueILSkg9v5V8#q z5}2U=u8~61_(#iBLl$*`*+Crmd`1lzy|bI1eK(86rgC=_qsbcaRV_TL{c|e~>8FT+ z2^bAXPONEU2E3Nm3=eyZNtZhg9qZaB?hMn^kKGEPabO}dElvFV^nZ9a(ljSNw)Kda zg8KF5{hU?CD}fc)IR9PXmgx0(r>82_J~Kq=q{#K(W}e%F7)l%N2BL?bK}ptqH~f!p z@L6qMbokM(;_wzCwPkUNy&ptMM=SR{k;@pFhB`f~dqP$hf!m6+Pa|{LVwm?VlIpR- zQ{37n$~|Goot<5_q6m9vP9r^=V7T2FiRGO!!{{j!Tu{b;2PTZk>WsMTm`Rd?c@f3* zu-AL?2Xdg|LDjBWrtxd9 z$PG+Ed{i2Roh*M0{xVu?mCSzuW3M%knMgKP$q8s_xir~*NOeRfhQozQ;ho=l|JR#Z z@AsfCeTt(S8{~&`;$qHXdY;J)qIqRZPq{DqLV1+R@rL?g;|oi3ti+AwH~ewVTglM zz`X46j{iz$N;r@PU2E-0Fe&-*^1Rfuu((E2;X<72v;uCNWx%;T0Bu*}6EQRUO=Z=J z4NqJnW#nzW9?`-;!>;-4;`pO=D@v=)XCePq(6X1~I@%d1#62x1+SHUe+>6?l{+FXS zZgZ8q?pK8vakZr)s1P(d3v_2T${#lXK4;O zp9*W|BEKK3DyHw_{5-xJx!?pRB`fksO`ckil}>_`q!K{mp9t9TGeYD)ZB*R<@>>Tk z87r-d@KUP_kzvhQ2MA;ew4w;prAF3TkpgiP+GXREpb3JpwW= z(Frde*oGsVb$#gsxG6cNgb3Tk2n$_6YY7c}Y%SZ>W!DLOhFKNxYXo>=Ny zq2~e=X>*rxS)z2PaN8G4LB>qUkZ`c&HZIRDHFl>_YGArNPI_`9!*3Si zqRR?BiUS^ao-bV-6?!?bgSh1)dcN1r$iIH{Oevg4z)^ds`wNRoKkg;tw;ndiKgUH| z@=T7RU8Y^NWYb@_rrWKD6iS>6kjXatnDRm;_Oc%oUg2?ML-(A2Eir=AMx0)(LRdKQ zAJPX}`AWf&DQ_<>7(OwdouI%)Glj6KCPZg$vkv;hD)GvG(*T$}@uTV&+zOA#D9jteE zZg>dq#+(m9dDOl0>a&}#MSv(zF{e;Sr!-~YH{R3+q&!DW>*5Bk9#7}YQJomIi3G;7 zy6*;6SB6^s?a{@$$m%F?xS#%RG|R1{lm~~-15N8hP5EUOI?vh$rnbWep;Yv^@75kp z`3EfeyyfFbq33S?xt(GN7>D4qOUL6A2dURPOi9Th6cnN3t+^<%ta~2Zd=QR?u<&L~ z$rP@tab9Mn)^<>GRN@^ky5Q62bcz&hd=8DqX&({gMn0|l<_5gikG;$fT$~icDkdVq zZKLWyMKmS8Qkt6FEnbY>)qejlBY56_=wfE8!E?f>fKCSJ4m_FOAGvPZebN3cM#}a^ zt6@B2#9MKe23;i;FrEF0kMo(Omy9)?P-IIvOrx8tsQQnVBU;zR#^|6t7T zj8@y}i7~f5&ZQLp{X@+Lk>p~Jmp;FqaO7jA)&7W2qt!vHw(xnD0zwen@XeIiyYMp9 zMqXT>rPckp4JdUk=x;Bw8~V?X10HVN-n$%Z%_RK%a#~X_RzoeVTXdCex?2B0DGDPg z3sJ|s8GVz8(tCSJ9}S+wAB!78Kf2#1eE40P5Oddvdx}?KfkF**IRgIy9pKnlgsbaJ zx~pLg9rod+kL+B(0#8Ny%WCL*6NV~ozYvN90;lWmzx4rwR5bSZvZHq$ISJqFbcM#I zywU=}2$U8bk_|<_Xb-QqIWEK9e^F#jrc!)^wO=s{&0^Wu1R*1ILNn!VQZF)B!^2-} z3%d$u^9wP^d)G{e!X zu`vGNF}pH5y4{4EZxGZ^wfn}x1k`pUC$fJag%M3Cu*{%^4aSAP7yb_wlSKsM`M(T; zwP;@(n9XDqSaMb%0;jFiI94Ck6cw!#ZUj4PdXYQo-aq8YKlTA-e=fogCX7zfYrBV| z!+no>2l=~^+|(kY`1=Un+vX;@q`F$2wH+cU#)My5wm}curf|d}Mz$2aaPD9t$oBl$ za3Q>V$wy^Hrhab9Ls5+EWB4>DlWe9xmq}{a?Ja)9q%FP!K3cl1{wrksqW|O`At@2t zoiT03oJ4DWmN6nU*h^i4x?EhX(G7hfh#7jK3HoJl!v|ae)lPyYET6v#WnwZ8u?wn0he7~uY}b-SZH zGxYv>lFRJL9wWZuoARe}fS0BuRIo}pCclsy%M{Z{&nSI&&Yh9 zrZyg8>GhfTc~c~W$ukVi-?{{+)^b3E=Wa>H55DVaFx$&Qwv=`&3&XWNi=O3v`z5;T zRwEmv_~USfOE_qhQ;gUSRxj56r}E?$1@I{I#TbrkJTfEYUkZOipxbg0aJk+g>$u<0 z3$;2#-_ODzvPW$T>f6!Q%-xm2IFZ3qFhjr=w*#;J zZP|u7W^q$2mJpM4_-L;RzVzUipE02}OZlMdv&YuP1XDVi=l^2e3G^bK;6 z8?!@I^0m+NaZV%U>cHU^uKlSSKVoRkN98vi*iujO@cPqBYOP=I zC3A;XUnW94cP@Qey02QX1>su9VavEuY}LdZbHn;#YN@UmTJi?0x5Sy!D4rp>`f z$iv<+>~y{VLr&T!8{S}Gii$+>GE`2&^HC$aGzTgwuX|%bVKuqvw_U_>IwYu;EDYfW zR~9_2!P*VM?_Kc>N^-YbOf-3HUH+s>Jo}HA>U=f|H5C~FX(=iNq9XZ6#&f49lw&LaSER?n#mm8yDZ zMkG#BDnUPZ59G>0CmKtF>0|<{1^;$zA|^dGZSnM5IlEryb+=_sOxaEKfcz@nC>Bq# zBDxJ>9##}Q8{DcyR{gk3jEP?j=T9TZV?&j0SQL|X;&vIitGq>;V0{$f<=Eg*qs$D6xH z5}TNelVZr|S+95U&nuzJ9!mpWk=CUW1%>P%;=AOYE32N>@0}XLGf<8eSeztf4LXk* z@vG6ohNK?%N$9TLh8M9jqI)iGOrM{M9~U&*r#D7CT))mZdT4}Oz0A4k-laba{6;D8 z4<`G7Ik~gTw2m|>jTF;VUrEo>x3PYjE)*e6sgnajroVf>+`8v^Tru}7tj?chL>T)c z(`?I|cBT1+xzj1RLpHFWg8$n+t+H`Ti1e&5dS2|me8Pi^_EH`E5w!m zfr@7e6IlzY#A&+rn4uXF?g)yvzwAKMdvG-VjhFBi&+xLdg-rT%$(X28yZHU-q~Uh= zPe5eP8s{*Ihc1(pl`=aUIuB7se|n|JG9p#clhbYm0kdscuv9bQ7AO(r&3OlBga0eG zD=G)GLq%_{SOYwn!=2*BxT!O9_~y%-6sf2mOQ*J@CkBFi)a81q=WbWKcAv?fjBtEU zjnddvqE1X6-Ts)q#D87rjqgVueUG1kwiU7-01}M9?ecFtUlIe*CB(z6m2|1Dfyk%X z!}%&|2${Ooj@%zdg*A>Q%Pn-0`m?on6fbUM!DlaD;>C7Z_3v@oBn=?5#S(m^q7;sE zx6v60-&Ox6*^o3l8a|`rUCd`wH=KZ*CdB%Uy>UexO|8MDKWP4U2MitFd(;OS*1VQ7 zo5!f^OU~=eusu2LLeB3gT4hR3^EX>TES1xMya`wffCGSoJcaR?z53|hL$u=dk8Tk- zFwEa}g}FSbU8O`-@z4`G`g5D^N6qm&l?U$`(L^T7gl<0*bOquUBs;E%Z4T+Y| zXHU4oTl47#;X6AM{?!`4CA6D{eHK~m0_oi4IYhwYUS1D{m1!9{QtE2?^efm)gpO!i z7Xh!VlO`AD&GKw#&hvW0T@PW>tNY}-0bFd7>vkVDv_GdN{THat~ zHHyl}g0dDbJ1DWIbu-_0rU$eB3W6F6K|z6#+;g$WMT}fRuI&zP;0WFM;9?e@@z=Zr z5S6NaXsuX1`j~#j)od?r%Z5m>u5i2(U$TdK{+-%8pr>~CJymt|!hlYl7T+<)V1Amx z`|{BJgu^%*2~RA+LxmZwJL1cL3@@N50f#5^UmbOPu=J=pRl07;)`c`eTl!P{>e53z zTdNa3$C&1xGP&qid7$WRwd)MC{t2vrqdji_w)gNB(~Q8))7}Fuma@`u26V?%+;+a| zo`Bbd-Ijncc`uP=TgWYAVg`6NR(TS*W6&k0ZAKVt>&s!?uNxa#aF94vISEUY((H9w zD{r=IV|6uWYt!+T=T9(Sm=pkEj2yd~u0ekO#!^{#(S=UW#DVgw0OqjfZkrHMWou{R zjTxFOGoyFPSSW09a}kbf(Q*E?+E6_zrWjq1O?4{>1lC>9kE#`RBh+sEgR|JHySul0 z)t0?fZ$|X?a7ZqJzUz`S7_9Pf({?$^Bca7jAYsOfI>OGVKOIK>lT;&*c7DR;%vni! z2RY%}*G&vi0t!Y)>bHTQz@mbk!m;&0xR?DdGTrWi|3vZ2$+-sqqrcUvLCrd&#GG+9 zQ^quHOG5FYT?IxSFvm0$DS_ISae6kSVH{k?C!@zMUu)NXl&nOK3m%n1RWU2&PY696 zD{SzzE9eW()qBD4`Fw(0NQphQC6wQtiPuksIGwFRgA-;kf5a2Zs%iSVm6PBE()~4x zh3I^{y1!CEv*O(UTOTySI=!Fq1@^zQq8T*)BqJJnSf~$^?V7t*E+b`Nj^A~E#M{WL ziPGtln5v7ow2Y6<((>W-R?t|A@pmM0XV$VlqaQky)xiM=KLNy7K zADkmiVtn!dwBTlm6wrTK4X*rSi#s&g2UIiruL!6=tz(bKM^@S;()=S@7{EOZDa2L! z>ejzMF3Un(Q2}*8`1dxpSSJmQg~#?AH^K0h7Z%g%$<|FBEPH!OOtPe*{nq{%dM)7| z%~Ex9Up*WbmW74X2Qwn#wYn#E$ z*DXr#Z=O_&8ZNzrRu3kV90>n5R&g5uw`)Wn`s-bPu?zBy6uF~MjLGumGk$W-bR@(En~YCA*_0MQxqo*lh;!Z61|QXlxxGq=BJq_NQ3iKL5c8&v@`v6sl$A(=YX;oe2QlXlKG{mI0zn znQ9S1CpLx&_RS3?LgphMbh0WL{h)>q@Ak1sCN?Gfr-|uOPh@PFpMaabpI>+<+iZ>-Pb zd3v1lKz;F~jVNF(nxX(A^Fi|*OGmpj^*L(mVHuTLT09{zL!66yX~*98K!!R*|+? zk)H+m-?sMyWcKf78>1b|#usM7k9qY4EPaXFNdjE92Qufv@7(9Hl7oA0_pBh1>_fI| zK-=kMxV6)oXlbFTgkJu@O?82Iaw{cjMMXrqC|OHbuBm|V(P9ud1O;IAQsU%5+c#|- z1X(gNtzrtg0D!KmY9%gZ^UR!8xJaXe;V|Zv&}vF9qIkO(fnp?IT8S5J3F1rnjw3gN zK5li_YPD`pmV^-hb?BK<5^ZANO;8m8yT@+M>j(r6l}fOIR@x^1H=qsSZyc`N)Tw4S zt51sZs5GS=C@i@o(KK~MOMOpNSY%qBM{M>IwE zHMFsa8v$y)bjpGmZi8gTKgce51*7x*7q$dV*7@VO8C8vxrg2+48zNt*3@aTx2fn!| z#Msi<$A2BLPWF~LP<09C!i0hFjqFoX!)PBF#&ELAQ?stqns=YSl=u?Ax0nb;NO8=3 zmF&@%FS6x4kt-io%P{}{EPz<5#!&Vfhg5+{0_YE%{LG4K^0|(D*1iIP$%qHnNJ#}F zI}ysR{-U!naWxh(4~26B7T$`4cD_hX{O`e{Bk^G7vE~pq@&EwwKQ6zS^FQ|f`#H_7 zs(?eK$(#w^+%57R5tmmPZ8uVWj1s25d7cKyB|6h`dwvCsKd^{)9{(8}XX^=m3vp6B z=u+=cpv7ei8S~u^{fT}Z@yGj7CQw@(lv4!~P8a2rqU}@l5U?=*CXeUYN_yF5R1%95 z&K!G8d~=8xV@u~UP2x#^J|K@s^fgltj??{jKPJ?Y%O|_;n+OgOnm(5)Y1a^ z(tEwEnpwurU0{Wnab`w4R?Feeri^uJ#b_{7H?l%fO{@AG{E0D&R(`YKPbE1em~&cH zBSzNKqoB1WcryhCLHc{21gz)8^kj;E&hKf7l%P_IwV!}r2bgQ0m~+H6l%Oy-AqbT# zCyH0P5}P9+|JMbE4!^U6nO_QiC3JB4$!WGUsIxRO<6Uj|$ z)i;DC1J?4EL_<^b{t zfJ(BU|GD~V-ZCQ;TodF1@V!DGXKIL+a68%ax0P{agqV^#Ei7fuaO}=uRU3f0tJTiW?(neX z6S}mp+zPD+;>0Fc&1KM}-)Ic{E5}}!JU0=}Iz3?{{Tv;w=Q=7YA_1`yq56T(N7i|N z607ZLn1h{QU}ONTFCPB9nqovTN6}B2K*BnizP5xm5l)G{`*n|1xXZ^V#q^7xDJdN` zmp^cW0{Fe**QIcw8h;>68J_n7Ew#Ya8y8o$Cir(HQ%CL`iZuMx+lhLKD{{}JvcB=PrQ|Dmg~A@ zU#s#tx`tvLdwrbLXulenY@)y4_xJ#h4$L4n!l5$}wq;+!tYx4@Tjw!lv?xQmpBX6h zMj{s`8mTLTM~GXrz8XuPULRTdSL${*5d9FHRqkLv#co8}LS9kJ+ zoS0}61ijUip#aQ!AQ3F3BV&$y05@vMGi6Vg2?D3z2;7ly_Y0d%3`)`Iq=urR%3F^ zAS0T@Zk#(G0VlGcvp#eu%2S+es^H)wW`mx8f_pX($@sV^=JoH&c=t|n!mgX!PT*i6 zr8zo#=}%NMX8xivIlbU>X9E;L9d4?0%{)*!y}Zmf{D^(QqsH4%0arb4_h`c$&4@O@ zFpRx88Tcsl^?_qzoFPwSh%B0B*4wFt@n3V5y>NrE|+pOPoNrWu2-Y$=hfGIk1Ihk z4B=l7-38hT$7z9P&R!61X(<3A^_8muGCL+*X$?cm0P2b!xSZ>1Wy|#bZ0^M9loQMT`A`rDBotfd;H#R&BD37Ue5U5?+_R=o5AT&M;BT zc=+23etEgu<^Oshs1cIUV9G)3dczUghh2hSR!-0H6aCc)Wd8B15ic%G`-m&N@vW2f z(D6Av!}ZP-`AYDxhkIZ8itVHebkix$yD@W$^urdMm4*}ag16fUr#5T3f|#Ak;#l9* zqobDgJBz}~7|98f0=gtM^bvVh&lG@G5v?7IHo1q1RZ`TqkqMy`e|%K*M1VS}9J6P_ zZ9^+R8$YoiYj@C5y}RC{@SajEp4*ScE*u>g>zCP4)r5B}{Rrtd=3G=?^MP`Fh^6A^ zO8st<(CB`)5H`yE9)>Dp$AQ&xd5`xH;9 z!N9iDa86F!HtP6N*x36J4WlDK)UNDDS_z#Zne znjy3?NqQE7*Mb(yS^Wk3{C7PtG$C~Le4uu7)>ula9Zp0l5s)&4fhfWq{Z9_VR(=#J zAP(%T!CU(Q@zU`NDfg2tB;yJ-ZpS(pEv>;=ac+gS!`<7H7Vi?ty$pb#HLfW#`<((e4J>y^d23PQM>-VbF2Il;Nst@NA z^9=r60hFS1aX6xWbn*3_8K(Y+kVo3>2GUHxM&<$7SgbB&^>u?)KwQ5MWuNyR|gG@#GL zC?~1g?Sqn@{Jgw|^?mQ^+hBn!WDw&kNxul1w3eH4U{$ zYl_VTQgs6PiwkyIJfO>*eVigBOE_i3W5f{7%+Kq_-#ay zvn}|M*R2WCsjNq)1^K2hp1+k`&?JC6UKYTl{J!M*NwMYliu*_!wcp=VfS3b%WyTy= z*V19HtIq{sWOe)Qf~Zli#;wq_T{X`Kwqw~m%~ej)B*=ofTw9zwF~w=42PV**ofqNc zFRM#EcCL(0Rg*aPsr@c)TK+>ofJ`2&yvXSVylI8ZFPH0Vn^7m?5!D=X)$7u7LcYwE zausGkbVli+IkKwBz^JgBtJ457V%W{rCa8%-%53}}x!a~QLYgJv0`+Xi)dNup|4WLl zz)8#H3#tSVvT-cx=Hsk#OO`S)*{Vsa56_n!2P{A4!2tNOucG;8$!8P6%SidB^2AQ6 zQ&!JlCT#lGswX16uqds4f8SjN{kCv_Tj<|xQ?jlCWI!-N;ICdcT7)zXYpT>pWWDnz z4Ysj!5{?|BlbhChe;KYw;ZI^-9KFB4{LCh$QSj1jRc@0S&-&bkmTd>$n1qr!iGm~15t6q&+_ zs)-T84=JmA8b2ivt~ov)(Rdq@E;3Q9a}C&YqjP1H^N(iI&Zi5TZ+59tDL|c6n@bz3 z-dsCyhsLqU;(8GwyZ0!c`nNT2V?=EMdB-Eyh3*qLeE6Rrr_ocgI5N4=zL@r zz=M(?Kx7#obhg+khX({2f(3^5swytcUn8v!h$;$d^lD6>0!f^HC;ZA{Xh4{(9PT$e zfNXSI;sa)#OQAI!K*n!QXl$xD{zAA+sfv(&ixr@PD3uFH%p;l&^M{2Q@{~iIYDvfeD-Bdd4EVWd?18~EflRLGS%~nMyr)v!zkkDWp-s&$wbH^jsy+_Ruaf?k@ zBS$KN;Y?XJYJS|`jY9jri1LwjfbUTOOJkS?GEUIRxyIhsKs?X`yRpv3^z6Hu;OY9; z7`?oH)OZ{up}TzEP37)|o9&|m)52uXqHutfWcD||eI*3eH=YxDYq$<(_JloHVH?Pk zTG!kH6RYn-xlet4Aw0$qvM;rNYej_|BT6tA_D`h=YHpHVNp1`r<&Yqg zsVQTeG(I1UO{CbW&1t0|5z=R|3dcSm{M@p_^YdoW$Hm%|bhnq?>W}~{%I+fO!FwGk z2`@jtjTE4j&O0v-2c!CVjYyuJ&2~;<&p#!r8;gFsAdtp{LjY*6X|z@*#6{!>bZefYC@#MjOh6Y#Q#S z-sp7ut_6s({5LMmzGgx})pocd;N}Z-GT!q1LtT8n!t56L`F`KoX=7*6T@yD?X9T{|c)w-ezUExcB^P-i{%5Nk1wAV9Vhwu-XkA%vVc7XQT&Xx%22+k_J3@ z20RGs#r-;yW>ZK{Zb~%##b_=wh4Bj5x4c5Vb{?lQzVLk<*hOTT{G=FCDzP-_z`V{`jWz7l7M5 z7J8DFtrSzj9q@61o3Gp4B^n28Oo1w3T<>bZtzn>UME_{6H4__+zn03~fDrvI3f9>W z37H=EeG;H5TWODWy!|Pzh8NY}4Uqr9E3~ zF?>89?%65&V9Ausj1zxI9jHwLi?VzK_yVu7wYwv&nHfVxs%=@E&F;;+0nblTHg*-0 z@|l1)y=@n}9&Js&5xkRxFus$;dC|rYU?EEf#8zvx<3Y&ymj1#wm)gQj){yPNMx4N~ zldl?4`4v6DlW{qFN;l0XTE(%z&S@BQ%Mfl75$Y(RUH z`FeuRvyW=*znsLl?5^zu2rTX(!;rJ;iP_~v5rB(`=?SjK-1Ps#*Ieb$KVL)TWyHGH zZb@^(IM(utr5C>4^f0-joqL~CSd~nVeA$v^Q-f+c9JasI=79{9Ksiv8<>*Ii$E~w@ zV?`y|*;u!uC`8mNxR~7T=$Ly1->VJuJ|JeB<_@OPev+YPYF7;Z^cdBOR#MSZuJU*5 z`(n(_NwygP5AeJVmzHLN0e4GOGufc~T<3>{dT$h9Rz+h@ntQOK=plSzY<#f)yWY9s zoD!$Cs*BQ7(N$5w@4rm|7`hkR%J2MI#zN#TLDgiBV}3%@>LzAAxpE+vmp+yb=dG4| z!L%f#qjUr?0{L1q@%z6i3M^$BmN5Gd#?IONIH6 zr{bC{H4_KC&Wo(^eP}oQuh%kaBFbNF20YO^-v6|^Z-i0B2mT9t&BM>UdfGahnHI~< zGU7{A_D~2`hO$LwvfRcsB-q!GoXG*aGdGs_U)6rUx)uULGR;)3bsl=KZx`ME#J~d7 z=n$OjFRk6}eyxQKAGaQN={8uu0vNW1nauz1otmf^>2A*>3=Xx-jgUjC^Bq> z8`8B=8-&pD$O8NXef&*dX(Uz`{~g6BYxEXfZ2%7Aq-As%XT28=;8KM4y_ipcegA#Y z(e_&{eGRAW_lDCmCAbS9Al3lUeBP(aNQTJg>qJ)Jt%iv{IVg6?P z3)Nt;nWUyN;S(Kc1Z#$#H8eQNTeLJ8*2ya7xItSL>7nP$vx|~3bdM)KDNQeOm6?sc))cR4=Fi2F}0y?8FoY3R2c(Tw3SKPY7y08K}@Ab-$vKIZdFAz7M1)Y znH7gyiH~TMnLC_0p-h#78=F(E45jDru87ZnDO~9}FRng`R z`=L_uJyd5#@$yzB1-$oq706S^jK!%(5JXZ9UoEHrS0$Ls+&{PV5Dua?>pQo(yk8wmb+h7JVNvz7NPmL(g zjjwVJm)%g7O=F5(O&ncaE3a?;tJ%z-wDj0dAq6&UwQasWNI*6M*a3sCLN~kv{!e9o zzJlv|-}jj@G>LcbiZ7@LAI9PLtR{TF;k(#TeoqUE`RJ}cdUF3XF8U>G>xz;>EW65% zUo&VBt~y9gVq%#2Chc>w`4uHfL2zF`)2w!b$&n~``{d5|1F6*y9f5y?kUP9@sWEc9 z!luuB1(T&4ieYraX`PP>?Eqa8DqH87sW3mY$Ck-UGEGzP-nPG&Wyapq>T zf|)wy0w?6rGZtL&OG2=3F_dYF{cUP{yTvEPN;I^MH?E{Te09Wo&K zbiO&{(8YClWZV&55TE{<9>VNQNEhGR$xMG;aB1+B;L5qVayh&H?dtBe@7)WhM@y{m zzg+k;`zY+ox;`NGoKAx|IXzE zjCS`WGi)t}pUTm(0z_hDu_JT7$w$OQ^eeOwZjxo!PDIiDm*iyu#jw63vIUF)5zlH& zzmFPWkSKAabXHRgJxSYoOkisdz5GFPAP$m%78iy3$)~5LNF3EYck|~q58;Oa5n!Oe z#_%6s$zVe|m#2=v@5|Dk(MRjB3hw&Q8(&bSs!3D)kEa`Go~BeKon#KQ!m+9+$&4Z9 zRKV&tr$6P~b6m$ccX3D>&!?9G}owFw6tnQ6eZ?^c1yWSxxXr5%trA}{w zoDB%_n8BCt+`@jjlj3!MGv_Un$5!meE6QNm=y8y7fXzByuEl!)^E^6zK1l7Z%O!ZsUIHP~v!yz;cHU}nz_9uj zMhm2bQ?IV}9oPO+46penMGaFaASK}!x_W=~aL^AjB1v@xrsN@LkH3)7WgO+oQEF4Z zBl^%x%2AyzH=8bCpHPS2E%lq%HOeR25^NQl3wU)+{D`80SA;m@sb;J`d;Jw_Kn9TCn93IW z6naa?K2@7-Do1fNz*ecxz+ASpHWSICKr*dM5}1l5-zEYCCwiBWB)PDG*aMJ{1Z; zh%95E$A(A z)JE+Sfmz-6(r{cjDn@J5HFlkrsMJp}Ue63(yRSb}kGXWUVn*8*O|8QUkoUrlbZe}L zpP!-(BTlNy1S&Kml1MGj|_Y zeG z?k4UravVY?{VG!FQ9l~q=42t#?#I;E%^+edeydk%@rD<87(y@I-qT6R$%XdCn5I`NZ<0`*Jf6puxs7bK_=e|)<-yB0yAf6UV#CJ&3^8)|OG z%e7W|t8(P@4>qa9wvXab_|5$VObB5&%eas}*blnFoWA4nJ_F?`0DW2`|!gz&O zUs|%LACgJf`yuuav+r_nL1dq)FTJnJui+QI=;U9rSY=PaRP9aNCND5sXbZ^3+gbh8 zW7+gG4m4J!Z{!_?cgjDGf{C5-J>I)T_$3*#!LL83OLI(!Dt$ELXCbQ)`D7CiNn95v zOk$j%Wdoz#gEMOA#ZVOXn{e?^Z4P$lt$Y2sqv7Av7{Y`tfdB*!44UN)kwm6&Le%Jr zQviL@xyhJk+1uMYcNn2#Z`2rKjYY^!wAS}VOG{=0h3~v-aDR`2);{i)oSs7b5k57k z)R*aC`dD?HpG)9NoT!9%eJ3HaLd{v?!6`sp4ohU;nPFxayA~1^g~%SAM2aHY*h(Zs zvhT9*`_3@$cbv}oo!@)Tx!&viuJ?ak@B9CsxvsgMndg4)&-2`$`+n|c3(?orILLaE z6$Anu)V!p21q9mjo&IIn1Jsz87+Zrtdx_pgS4mgUWPVoy-pTLJmLPbFjmv>CFA96=y+JxhKLAMR$S$7FzCg~*M7O} zpnFAX=O)pu=ZDVa8R0zd`l4ng&&FhyXFa~4m_9c?v2siK@O?r#ORwl`B-#1t#?D0-NVB-3QoAbXnyRO zy~xGcP_6u_b#6Vm_c{7~Ma9HQrgB5urU4kmf<tsASK%a#D(OuQya0@@D?DtAz4#^u8C(e(GCBjeXT4A%qea zrqXpD)^xXra>zmu%ghVD3Cwr!=fIj)j;HOk9-JY@IASgiR~a+N$I70iV)W!Mwt)x*e`ArPqU3Pf_4ln zRUZgShR+!?Z@u3~TR1$Ql9sA&tYf8`2|pr|W;g0_3q@QQoE)7UXtlT$)%a%qE~k|F z$T^lU^Vi-F%$=RDzEdWAoZfe*4MCoG?p!d7^q#@K_q%uk;j(^La?&C4g?43#W3&Ai zM0YBasX1mMXGFxr3j47&e>%s#dzH6%c2(s;)4?hBnRbI`!cM+m_v70^C(|g3>W%w_ zy~P->Te95UQUw+*K42fr2J>k zmh<0746rRT&c=aB8GdVQS(VN+;45q2PZ1+!uV3YJi5IT0jBQL^X)+nfb5_R6rh+fd zy1fMd+$yv3cv-Ek^P&G@j@O`gjj&yl+}{1NR$@KR&j>ybu%*g%z}&xjYw_S{*o|p0JO#LkirJ_d$gjdanP1O zGo9?zZC-iIg;)H*je)Y#0;{PGNgtQWKLqr9*($Nf3@YI>10;UPiBGoYXYX8w9ddq*5nV6M1afZx#;DPSt4Bc|i3 zig9qfA=<9fQ5>h|_ zkn{gKKCG*b&TsHI;tv)8K46|`SD3h{7z~Sr{a%AeQuhErehlbOHHbz)7KdHI5b^E= z8;rUK21gS7J%p{zZ~Ct81eaZNY;9l|7Yr6?N(6cp|F~*NPn1nP`Qc@ZXm6gHRLeVx-GE!(UTa2Bw`0t=JaYPatXM>?b0pOyJ z0FER^N)n9$w8iW&7od_hl6Fv8X(?GK24gELE`|}elfqp19fTgi5r|5(%kM`;hq48r z#3ZhZ*~-9Wp|ThmI8;)?RuU?69WDkHmzENj!N6^#?WApXp=@ms7x4rv8gQp07HyA# zx#H}11$4p@%KDl}0SQsDf7R%_ph4b>(z}W6~ z1nB)DvT;D;>@mRn{t-~W);s=*v*0q6p%d|TBr=+SQMLyx1*`!AXxAEk$S##<|9vcE2MnDjF$r;~7#u1gWh5>RcoiWb zE+TdTAtojO`(ZGQKC6GVSRVEVPULqR{4NaudcV{G=>^DEu;0?v56St^(f)p7-$~i zXi|Or=|q0){pw1Ro$*g4sRwSeT-$dj*6dTlX`%13WwQfAQ&v&-Udda;<(Fpn7!)3P zMxRL2DN&ZsbL37~3sHT=pIJFp`Rt6eky7h5SxnKJF=DYv>`Qr~{5+?E5+%FQG((Hs-dC zzb*=IX69G762jfMt;!$qV%j~NOfFbkor|>#P%NAxd`$N?RsxQu`&}=Y5ka5>N9bP$ zP+Y=sppu!SsiV$3vhM`To}*sNCs#lq?(dpv%0|E~TCZ1>rE$vmfZt0Bg(8=DN`m}Z zUX>qx2QnTBe(x8fyjT4cm}@!3^t-_1(G&L>P8>Bd+Gp?xeB%aTfmK3;mG$!7(_jXP za8lZ=Zqq~x%h)hc5fef?t1E_Qzr`D~MkCSH%q_2D z@juqUAKv}@9t1r9A9?VHTmQBPf4KE;d+@)FfqSYr{<9eVZ^OVJCh7NNqYyA?hp*D@ z^Vm|SWfn5Ber(Cj^3wAwQpS%j?&E;GPYR20ffql2?l(Gh&ws6HYGq0qE?H1uOX&0k zhYdAF3>Uz5DwU6XIt~F?@=*bYO|`=n7F)EkzgSUR>(ipcECoVS*JhiW9tGHZW4*bD zh4VY(RJio9EKi$(&=sBfk%^Z158J|Fd}htL8Z2NG8cq#DufE>0gIe*a+)W+#L2Xu|neV&T z?(K*?-?FdQNXvONtlI7l$DoX*?7@B!%r3VW-@NJC3z1QasccmZ8uxS+yd;F-rwnxRT!nIj)>Jh zL+(NA)2zXPkifP!Z62=s$NBYd1QdoY<_sbib)dZUP;7VU$?Apa0u%Z;mM4rKUz(n^ zuZnKV-*kU46B|b7&2uO9ak>9042qrk#K!?q65h8uhxAs`VM&3;(*}^`HKTnOnmuRZ z>yi@kBm*C^FVtx8mALy@SZv+-RKMXnCTtY_>MF6#6o*{VDo9e?7S@T+cQFoY(qH-oZ{6e=X-NB5Y?xL|Wmh zCC#?tX_vU*zN2L4o(|tOQ~Y*6mp|>3+^LDrqxn7`)$68Ug_}tu2Q2`Kchk19-C%*AGcm1bZPFUw;Ld}d;8;%~GoP+w^&m<%3@ioJt zQ@JCo3yG!)UY{$;$KC`GztuL2ZabbkR0Y|DVRL!nA_L_`>(oiS&v1Cv z1t9~L5QuHghA-HE%s|e7)XXG&%B!QyVl6xCrAv2*B)O9U3GBSr?jeasxR!qts;L|t zS>dvld**L5n%2H=@6I?sys1%B$@0!=bX$N2denmM#ym zXS{;2gMaGG?9Zdgj#L;53l&T}QG{6%7o^sxj-%YDl7)f6CRax1bGUf8`*0(onu!C_ z_!@)AIbe!|tX2lI^Tdx&t9@49dNA26$gASuG9yEaJ8>_%+^VO{rM2rT%DufS@k?ih zC=5Azw;V|pvx?jN^5P6_J~LSxODvyB7~vKL`hdn-vZMqUce0BnOAy9E%=BJ*^YZMY zrQdNfVk-JR%JtfJeBY5q5iWY2JE7v>P~$sW>(giXj`fM|)Yj zh%Hbvct=wZyt6X#cIMNdd-AD?7IslN=SmKDfG_?)QxvM_QC%>{o#k1x_VV@ykN7uh z^Lzn*mluXLY_vvGr{>DnH@7J1k=j{e6Hom-dUi$!AB%9y6`hhy*Tc-2Pu1+%!^8p( zobM`mUL|)Wyo2nTQ&-Iy&L_9r;AnWRSqL2HUOywkZ9(ZM;!r^(c6n%tq6)vqE5;-I zE-u)0%#`F&CKlso@jZ6d>^GEd;?9K)9bGwCb>hOu)ydqt?KNKMX~nM!X>YhAZ`5fW zF6i4D=G&29yNN>fw-Y_@ME$>%42{ioeEBL{TqXTX6n zf!3`^k0RP)t22IMV=1IJeh+oaE3<4pQKO7Df6pR*sBycRv@MbiwFnq6R_yNSnQR0O z1C(qTQCma~IFKWlM0q!r$OX%$^(+^+Nfnfbud$~f;{&ieO@n+IKh2^{BBnP@M_)UY zjKzGFm3*In-krVowZ6*2eoHqNA*-~HshAa-Tkrk6_`m~4lRD)j_y_}FK9#|yNLh=>py=C7y=*&L|@x5*UwuE7yJ%6EHvh1-H`ESDmP zf{5sB_a9-Cap->}7<{nD?YgcefvY`u-^!$0!-R&MQo>+Giy|FDme14e1rWW4Qt~x;+|HL^PH1@Qd&6pv28t`Fn7=M@)Q9 zqSgDDxH#yZPDKT*QExJO%lQI}GR|rGv+_iQT3KCDl~B#_cM4heJtwQ1Pnr#8dox*a zwOaOoNw$;I#OI?GS-1+mt`}ouSbURuVjxHUyZr3#7IUgd92|QLLWkmk2(D5F`@<6b z$C|AYRDfl%&$?N^OlL(NVZSh z(oPdJ^|Ws=Ff)r)W!%Fw-uK{eg=Qps`E1D%N?}8;!$#a7d#Tg8;I#~2MLZvEP3r)z zXzQ(2gJ@sVV#5Q98%wFPH$oIRL{)@TpYo{vIk8W7>wRwSQjiKuisa*ya_{D5O9m8t zT9ZiY<1%apoU{Jkt5EVb4~CctS)#M3oulK2m`gF;Vs{G6FhW|q&iflEaKB)1bN{&Qa- zx-n^Gn;V8-*HJ@5GL>#uDNa@|L@tV26s@itJqQj|6X*({x>hq$jyB)FIkUYpBHQdw zwRQ8K>+@;#pIgiTb_L0#>46{>-zvSN%UI&Mk%HXlg#I=IgGV@ru96I7_eAl*x$dQC z$AHYp*AEE5h9LYT;;AFCPFQzczooM?hDpoh=g1I_iW@I;cC18W=tqHMO9j-Eg-ot{ zrXH)@OC>U*Wl9!S~X% z`?7RHYnw9ksC9?rw(Kk2sN=Z-4^laT#oSunhZ2bQQ+zaalNEX)APb?x240Ajwp8P1 zlTNtxNwaP5c9*TmCj+<5;n=uRTb+gb8k|AK+;ONUrLQuCnm07G1xh5yHHVn*zk2N- z5E(cIv)q|sMnN>yLLijMTj6Un#xspd1%p4azA3c{>!5uMo{qeH#!yavk84A`R=`Sd zJK0`2A6)7!SzsS;E_X8DwxM`&)*}0fgx$Jz@di7Dla(F0`hq0E3Z9lO^jQDC^NV}$ zrKYEzFCAJn9Xnq#-p!sz8dhN$c`k(I1#YY4ei z-PTep{W_kKIDCmG+d3CLnxM{oQhIIL!2ffgL9EN{g6Xj!`r>U!BY09IN1iGbZ1IN< zbwEJHlEmFDf<8Qk9o%9P)wl25|Mjku+^%uZjE4 ziWa`UUrn@3TKae1Unq|)I?W0`6ePn|Bx4kHW@$r(1>Iv^I!l>dns^7SQq__+*V?TE zoI9E3+B9h|bNF=Rd|KYz6xK^3ZF^PY*N)mR-pu}IHS!!B_$m4e zHK?wj*K3t3^ulT}(%ufLBa}KgbEZZuO7~(FqUPA(*MP-mlY95FDhac?QI}BVPijU~ zS$^y}mW#=e3G!Y|?}%Z&lsmgA6eaHd-6z>V9-R!#tm!Wysy+cO)TgRzdB$vS-ZhT#ycx#HK{Yiv!XIp2YREls zZ$7LF2)JgL;{1AD>tl>5iaG(zI)q=a3!G0VHG-VYTm|+Ws}67Q^zdLwk$+sWc|iD- z+q^C`^QgQgxUcu2nf&{t=C0_Cp)X$S@;i%}T5ZDlMdEP2YQ~qfTgEZc1@7m2-w2&h z2M4bD`u0Dz=auQ>JOx~T(Le`v>JLO`C2Wl@7AJ2`0UP_4Vr&Go^5}CR-Xdz49^V(v zK|V{;;x`SRYs-Ko)Lv-LTd$GHl~|X2WMt>NqLrDv2j%g3!+A%innNH(^|phSJbHZr z9W926=IsUTrQy`oa%Qk+)(lv3vBV=QzS zHxP`g*U5R$Y&S#s1Y9lf76_-NQ{<&yp;{L-bQ7qLwtZI9FX5Jc8is_v%=9)gT{*63 zmo5%Wlz9ejDc|houpXaJOG}p{Li~|+)oia$w3OQ|zq!dA#`i5V|D`>~OH;xz>PGLt zVfJ~3?#+zD>W{wB?~v#9DzFa!*`@LS_$$XhT(|*2^}Dx#|7R|ZZzzfH{l~>W!u3xJ zy5s)z{p0^17c>0%%g+C4lK!yzuk8HG7zPyRGWr|GpB8`hbVXPA)2FLHefU_vL1nMv W-mB~NmX#X>d^FW{)v{Es-TGe^tAw}! literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe58eba1b16f0d7483e85bbdb5db48745148f52 GIT binary patch literal 21057 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

*>WV#vY9{ z^j_1c)tBg{J+#VR9AC3aUbs_evtj8cSntmp1FGfeZlt=FY~ZM~SA5gCUde>^qs8`0 zEIG$|CCc04Ev^Y9aN7k)%(t|dtFn7#$X=teZ#06>R=qGB*s5@xyGgCig#8&MYf_9N zGRWZ%2U@+3hx1szH^i(thc(COd1^0Ibcwv+p>eMBR+E$cSH83wTsbO#;mUH#`w7qu-e6o za0FO76}J0(QuJFcn?YQ%TU$kEXR!C2R+`fkX&kS8C)kcaE~APWx-TPWtYz#zL#x}$ z_Bg&`;dA8>w$x>#E1=edK{bPUFV7$=HP&#(=+x6U_6gFU^0vBnp&5obPp15ol${^2 zDhns6M+iE1qE{fqkx`t(LYBOEmXG~G$V@BS?ytLswfzu}&3#6bk%GL;g-owZBOC-b z$klP|(Z^VlH}L--7{nJ9Id5I!uy}8{mIGh&k+Z5Qo~M4$3^BeEn>6R$A|1d@31}a^ zBUszCF$RxcYnoOAL-?etbd*>zW*xpe@-r{%*jnlsD8Z$`Jt)&k9(!k>wi%%=GN2HK_R9+l!*f>;O* zfeJg8aq~MfyNa>;VSKWkgb~yh2YUzZf|D%J zGta2ljqsZdVM1PE30sW8k)++GCFmYxr)G69tx;8s;pJsq8M(0yLg2GXxI zc(~?D_%3x%^7`|F{C|6w6yx<1=!M9O1L8tUk zJ#U~GmxmZy$OOuv7lJ2W>$YZbY)wRFWo4#`d^_1cclisWF&ej%x)JHQ)<+9wqY~

A)S`6Y0};;_B6E~)o8 zX6J0xon4t1XJ20F1U_%;-JNF9jV|(7VZ|K9ITGa}v}_~LGJ-XFEQEMyIlQyF89LqS zrOC8ZIdu`9=v*SGmT7ctpTYEAp17e?(F9T6il;irRWwR_K8uX*~)-C!Fva5{(aR`aVNeJQ$4 z+6$epy#ZWktkZOMo7Q8JM^I63>ID+!oBWE92n|Z6-Ni^RGK4t6k=xUr`ZqFw~7qPj6`v96r+6I3#lO zMFLPF)D8_*jeKn&vA6xIO;5^nzD6+d`ljpgqpK)djInv^Ibo5x)nYEpx6fArke1vU zk^8t|@$IxKu&L=8JX+JBW%(ni-JzMJ#v8BokI-j~4)=xxOsrZP3Kt@8!}Sh|{dRvY zNo!nPDJW@RsE?LNsKDO0v9R#v`F7()H<;@()0ab1HS~<;@9{i-@e+Z+XTlfjqmx+> z=-{%;z8SgriG zhVoyasz;mfx2jU!0sCv-tttg#kH24oqePP5Us?NhMfk7Kvm?KBgs6YbR{d3=^e2{p zQs{S}`!7JMZ#(mcvpQn_hi&{TH0b|AEB~PR!)oQf^|b#DsycEOFsHu`K>paO{|lOb iZ~y;ZrRrl2K?yoZX>``gQ^1yk?kcG(X5D`D{Qm)tFeMcL literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5b1955abf8a313328adc0fb6811747651fdb7101 GIT binary patch literal 40400 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�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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info-Debug.plist b/ios/Runner/Info-Debug.plist new file mode 100644 index 0000000..25a227b --- /dev/null +++ b/ios/Runner/Info-Debug.plist @@ -0,0 +1,79 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ThingsBoard App + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + FirebaseAppDelegateProxyEnabled + + FirebaseMessagingAutoInitEnabled + + LSApplicationQueriesSchemes + + https + tel + mailto + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _dartobservatory._tcp + + NSCameraUsageDescription + Scan QrCode or take photos using your camera. + NSLocalNetworkUsageDescription + Allow flutter tools on your computer to connect and debug your application. + NSLocationAlwaysUsageDescription + Get location to place the device on a map. + NSLocationWhenInUseUsageDescription + Get location to place the device on a map. + NSPhotoLibraryUsageDescription + Access photo library to take existing photos. + UIBackgroundModes + + fetch + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Info-Release.plist b/ios/Runner/Info-Release.plist new file mode 100644 index 0000000..f82da45 --- /dev/null +++ b/ios/Runner/Info-Release.plist @@ -0,0 +1,68 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ThingsBoard App + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + FirebaseAppDelegateProxyEnabled + + FirebaseMessagingAutoInitEnabled + + LSApplicationQueriesSchemes + + https + tel + mailto + + LSRequiresIPhoneOS + + NSCameraUsageDescription + Scan QrCode or take photos using your camera. + NSLocationAlwaysUsageDescription + Get location to place the device on a map. + NSLocationWhenInUseUsageDescription + Get location to place the device on a map. + NSPhotoLibraryUsageDescription + Access photo library to take existing photos. + UIBackgroundModes + + fetch + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..e01669d --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:demo.thingsboard.io + + + diff --git a/ios/Runner/TbWebAuthHandler.swift b/ios/Runner/TbWebAuthHandler.swift new file mode 100644 index 0000000..d2e9e01 --- /dev/null +++ b/ios/Runner/TbWebAuthHandler.swift @@ -0,0 +1,74 @@ +import AuthenticationServices +import SafariServices +import Flutter +import UIKit + +public class TbWebAuthHandler: NSObject { + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if call.method == "authenticate" { + let url = URL(string: (call.arguments as! Dictionary)["url"] as! String)! + let callbackURLScheme = (call.arguments as! Dictionary)["callbackUrlScheme"] as! String + + var sessionToKeepAlive: Any? = nil // if we do not keep the session alive, it will get closed immediately while showing the dialog + let completionHandler = { (url: URL?, err: Error?) in + sessionToKeepAlive = nil + + if let err = err { + if #available(iOS 12, *) { + if case ASWebAuthenticationSessionError.canceledLogin = err { + result(FlutterError(code: "CANCELED", message: "User canceled login", details: nil)) + return + } + } + + if #available(iOS 11, *) { + if case SFAuthenticationError.canceledLogin = err { + result(FlutterError(code: "CANCELED", message: "User canceled login", details: nil)) + return + } + } + + result(FlutterError(code: "EUNKNOWN", message: err.localizedDescription, details: nil)) + return + } + + result(url!.absoluteString) + } + + if #available(iOS 12, *) { + let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: completionHandler) + + if #available(iOS 13, *) { + guard let provider = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController else { + result(FlutterError(code: "FAILED", message: "Failed to aquire root FlutterViewController" , details: nil)) + return + } + + session.presentationContextProvider = provider + } + + session.start() + sessionToKeepAlive = session + } else if #available(iOS 11, *) { + let session = SFAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: completionHandler) + session.start() + sessionToKeepAlive = session + } else { + result(FlutterError(code: "FAILED", message: "This plugin does currently not support iOS lower than iOS 11" , details: nil)) + } + } else if (call.method == "cleanUpDanglingCalls") { + // we do not keep track of old callbacks on iOS, so nothing to do here + result(nil) + } else { + result(FlutterMethodNotImplemented) + } + } +} + +@available(iOS 13, *) +extension FlutterViewController: ASWebAuthenticationPresentationContextProviding { + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return self.view.window! + } +} diff --git a/lib/config/routes/router.dart b/lib/config/routes/router.dart new file mode 100644 index 0000000..4c09604 --- /dev/null +++ b/lib/config/routes/router.dart @@ -0,0 +1,66 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/auth/auth_routes.dart'; +import 'package:thingsboard_app/core/auth/noauth/routes/noauth_routes.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/init/init_routes.dart'; +import 'package:thingsboard_app/modules/alarm/alarm_routes.dart'; +import 'package:thingsboard_app/modules/asset/asset_routes.dart'; +import 'package:thingsboard_app/modules/audit_log/audit_logs_routes.dart'; +import 'package:thingsboard_app/modules/customer/customer_routes.dart'; +import 'package:thingsboard_app/modules/dashboard/dashboard_routes.dart'; +import 'package:thingsboard_app/modules/device/device_routes.dart'; +import 'package:thingsboard_app/modules/home/home_routes.dart'; +import 'package:thingsboard_app/modules/notification/routes/notification_routes.dart'; +import 'package:thingsboard_app/modules/profile/profile_routes.dart'; +import 'package:thingsboard_app/modules/tenant/tenant_routes.dart'; +import 'package:thingsboard_app/modules/url/url_routes.dart'; +import 'package:thingsboard_app/utils/ui_utils_routes.dart'; + +class ThingsboardAppRouter { + final router = FluroRouter(); + late final _tbContext = TbContext(router); + + ThingsboardAppRouter() { + router.notFoundHandler = Handler( + handlerFunc: (BuildContext? context, Map> params) { + var settings = context!.settings; + return Scaffold( + appBar: AppBar(title: Text('Not Found')), + body: Center(child: Text('Route not defined: ${settings!.name}')), + ); + }); + + InitRoutes(_tbContext).registerRoutes(); + AuthRoutes(_tbContext).registerRoutes(); + UiUtilsRoutes(_tbContext).registerRoutes(); + HomeRoutes(_tbContext).registerRoutes(); + ProfileRoutes(_tbContext).registerRoutes(); + AssetRoutes(_tbContext).registerRoutes(); + DeviceRoutes(_tbContext).registerRoutes(); + AlarmRoutes(_tbContext).registerRoutes(); + DashboardRoutes(_tbContext).registerRoutes(); + AuditLogsRoutes(_tbContext).registerRoutes(); + CustomerRoutes(_tbContext).registerRoutes(); + TenantRoutes(_tbContext).registerRoutes(); + NotificationRoutes(_tbContext).registerRoutes(); + UrlPageRoutes(_tbContext).registerRoutes(); + NoAuthRoutes(_tbContext).registerRoutes(); + } + + TbContext get tbContext => _tbContext; +} + +abstract class TbRoutes { + final TbContext _tbContext; + + TbRoutes(this._tbContext); + + void registerRoutes() { + doRegisterRoutes(_tbContext.router); + } + + void doRegisterRoutes(FluroRouter router); + + TbContext get tbContext => _tbContext; +} diff --git a/lib/config/themes/tb_theme.dart b/lib/config/themes/tb_theme.dart new file mode 100644 index 0000000..aa98559 --- /dev/null +++ b/lib/config/themes/tb_theme.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/utils/transition/page_transitions.dart'; + +const int _tbPrimaryColorValue = 0xFF305680; +const Color _tbPrimaryColor = Color(_tbPrimaryColorValue); +const Color _tbSecondaryColor = Color(0xFF527dad); +const Color _tbDarkPrimaryColor = Color(0xFF9fa8da); + +const int _tbTextColorValue = 0xFF282828; +const Color _tbTextColor = Color(_tbTextColorValue); + +var tbTypography = Typography.material2018(); + +const tbMatIndigo = MaterialColor( + _tbPrimaryColorValue, + { + 50: Color(0xFFE8EAF6), + 100: Color(0xFFC5CAE9), + 200: Color(0xFF9FA8DA), + 300: Color(0xFF7986CB), + 400: Color(0xFF5C6BC0), + 500: _tbPrimaryColor, + 600: _tbSecondaryColor, + 700: Color(0xFF303F9F), + 800: Color(0xFF283593), + 900: Color(0xFF1A237E), + }, +); + +const tbDarkMatIndigo = MaterialColor( + _tbPrimaryColorValue, + { + 50: Color(0xFFE8EAF6), + 100: Color(0xFFC5CAE9), + 200: Color(0xFF9FA8DA), + 300: Color(0xFF7986CB), + 400: Color(0xFF5C6BC0), + 500: _tbDarkPrimaryColor, + 600: _tbSecondaryColor, + 700: Color(0xFF303F9F), + 800: _tbPrimaryColor, + 900: Color(0xFF1A237E), + }, +); + +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: { + 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); diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart new file mode 100644 index 0000000..fadfbfe --- /dev/null +++ b/lib/constants/app_constants.dart @@ -0,0 +1,7 @@ +abstract class ThingsboardAppConstants { + static const thingsBoardApiEndpoint = 'http://124.222.96.26:8080'; + static const thingsboardOAuth2CallbackUrlScheme = 'org.thingsboard.app.auth'; + + /// Not for production (only for debugging) + static const thingsboardOAuth2AppSecret = 'Your app secret here'; +} diff --git a/lib/constants/assets_path.dart b/lib/constants/assets_path.dart new file mode 100644 index 0000000..564a2f6 --- /dev/null +++ b/lib/constants/assets_path.dart @@ -0,0 +1,19 @@ +abstract class ThingsboardImage { + static final 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 = + 'assets/images/device-profile-placeholder.svg'; + + static final oauth2Logos = { + 'google-logo': 'assets/images/google-logo.svg', + 'github-logo': 'assets/images/github-logo.svg', + 'facebook-logo': 'assets/images/facebook-logo.svg', + 'apple-logo': 'assets/images/apple-logo.svg', + 'qr-code-logo': 'assets/images/qr_code_scanner.svg', + 'qr-code': 'assets/images/qr_code_scanner2.svg' + }; +} diff --git a/lib/constants/database_keys.dart b/lib/constants/database_keys.dart new file mode 100644 index 0000000..4c1eeab --- /dev/null +++ b/lib/constants/database_keys.dart @@ -0,0 +1,4 @@ +abstract final class DatabaseKeys { + static const thingsBoardApiEndpointKey = 'thingsBoardApiEndpoint'; + static const initialAppLink = 'initialAppLink'; +} diff --git a/lib/core/auth/auth_routes.dart b/lib/core/auth/auth_routes.dart new file mode 100644 index 0000000..527eb96 --- /dev/null +++ b/lib/core/auth/auth_routes.dart @@ -0,0 +1,42 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/auth/login/reset_password_request_page.dart'; +import 'package:thingsboard_app/core/auth/login/two_factor_authentication_page.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; + +import 'login/login_page.dart'; + +class AuthRoutes extends TbRoutes { + late var loginHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return LoginPage(tbContext); + }); + + late var resetPasswordRequestHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return ResetPasswordRequestPage(tbContext); + }); + + late var twoFactorAuthenticationHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return TwoFactorAuthenticationPage(tbContext); + }); + + AuthRoutes(TbContext tbContext) : super(tbContext); + + @override + void doRegisterRoutes(router) { + router + ..define('/login', handler: loginHandler) + ..define( + '/login/resetPasswordRequest', + handler: resetPasswordRequestHandler, + ) + ..define( + '/login/mfa', + handler: twoFactorAuthenticationHandler, + ); + } +} diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart new file mode 100644 index 0000000..0a8a79b --- /dev/null +++ b/lib/core/auth/login/login_page.dart @@ -0,0 +1,455 @@ +import 'dart:ui'; + +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +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/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); + + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends TbPageState { + final ButtonStyle _oauth2ButtonWithTextStyle = OutlinedButton.styleFrom( + padding: EdgeInsets.all(16), + alignment: Alignment.centerLeft, + foregroundColor: Colors.black87); + + final ButtonStyle _oauth2IconButtonStyle = OutlinedButton.styleFrom( + padding: EdgeInsets.all(16), alignment: Alignment.center); + + final _isLoginNotifier = ValueNotifier(false); + final _showPasswordNotifier = ValueNotifier(false); + + final _loginFormKey = GlobalKey(); + + @override + void initState() { + super.initState(); + if (tbClient.isPreVerificationToken()) { + SchedulerBinding.instance.addPostFrameCallback((_) { + navigateTo('/login/mfa'); + }); + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Stack(children: [ + LoginPageBackground(), + Positioned.fill(child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: EdgeInsets.fromLTRB(24, 71, 24, 24), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - (71 + 24)), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + 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!, + ), + 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, + ), + ), + ], + ), + ) + ], + ) + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 10, bottom: 16), + child: Row( + children: [ + Flexible(child: Divider()), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 16), + child: Text('${S.of(context).OR}'), + ), + Flexible(child: Divider()) + ], + ), + ), + FormBuilder( + key: _loginFormKey, + autovalidateMode: AutovalidateMode.disabled, + 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}'), + ), + 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)), + onPressed: () { + _login(); + }, + ), + 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( + color: + Colors.grey.shade200.withOpacity(0.2)), + child: Container( + padding: + EdgeInsets.only(bottom: bottomPadding), + alignment: Alignment.center, + child: TbProgressIndicator(size: 50.0), + ), + )))); + } else { + return SizedBox.shrink(); + } + }) + ])); + } + + Widget _buildOAuth2Buttons(List clients) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: const Center( + child: Text('LOGIN WITH'), + ), + ), + Row( + children: [ + ...clients + .asMap() + .map( + (index, client) => MapEntry( + index, + _buildOAuth2Button( + client, + clients.length == 2 ? client.name : null, + true, + index == clients.length - 1, + ), + ), + ) + .values + .toList(), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + style: _oauth2IconButtonStyle, + onPressed: () async { + try { + final barcode = await tbContext.navigateTo( + '/qrCodeScan', + transition: TransitionType.nativeModal, + ); + + if (barcode != null && barcode.code != null) { + tbContext.navigateByAppLink( + barcode.code, + ); + } else {} + } catch (e) { + log.error( + 'Login with qr code error', + e, + ); + } + }, + child: SvgPicture.asset( + ThingsboardImage.oauth2Logos['qr-code']!, + height: 24, + ), + ), + ), + ], + ) + ], + ); + } + + Widget _buildOAuth2Button( + OAuth2ClientInfo client, String? text, bool expand, bool isLast) { + Widget? icon; + if (client.icon != null) { + if (ThingsboardImage.oauth2Logos.containsKey(client.icon)) { + icon = SvgPicture.asset(ThingsboardImage.oauth2Logos[client.icon]!, + height: 24); + } else { + String strIcon = client.icon!; + if (strIcon.startsWith('mdi:')) { + strIcon = strIcon.substring(4); + } + var iconData = MdiIcons.fromString(strIcon); + if (iconData != null) { + icon = + Icon(iconData, size: 24, color: Theme.of(context).primaryColor); + } + } + } + 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)), + ) + ])); + } + if (expand) { + return Expanded( + child: Padding( + padding: EdgeInsets.only(right: isLast ? 0 : 8), + child: button, + )); + } else { + return Padding( + padding: EdgeInsets.only(bottom: isLast ? 0 : 8), + child: button, + ); + } + } + + void _oauth2ButtonPressed(OAuth2ClientInfo client) async { + _isLoginNotifier.value = true; + try { + final result = await tbContext.oauth2Client.authenticate(client.url); + if (result.success) { + await tbClient.setUserFromJwtToken( + result.accessToken, result.refreshToken, true); + } else { + _isLoginNotifier.value = false; + showErrorNotification(result.error!); + } + } catch (e) { + log.error('Auth Error:', e); + _isLoginNotifier.value = false; + } + } + + void _login() async { + FocusScope.of(context).unfocus(); + if (_loginFormKey.currentState?.saveAndValidate() ?? false) { + var formValue = _loginFormKey.currentState!.value; + String username = formValue['username']; + String password = formValue['password']; + _isLoginNotifier.value = true; + try { + await tbClient.login(LoginRequest(username, password)); + } catch (e) { + _isLoginNotifier.value = false; + if (!(e is ThingsboardError) || + e.errorCode == ThingsBoardErrorCode.general) { + await tbContext.onFatalError(e); + } + } + } + } + + void _forgotPassword() async { + navigateTo('/login/resetPasswordRequest'); + } +} diff --git a/lib/core/auth/login/login_page_background.dart b/lib/core/auth/login/login_page_background.dart new file mode 100644 index 0000000..0d79be5 --- /dev/null +++ b/lib/core/auth/login/login_page_background.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class LoginPageBackground extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: CustomPaint( + painter: + _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor), + )); + } +} + +class _LoginPageBackgroundPainter extends CustomPainter { + final Color color; + + const _LoginPageBackgroundPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color.withAlpha(14); + paint.style = PaintingStyle.fill; + var topPath = Path(); + topPath.moveTo(0, 0); + topPath.lineTo(size.width / 2, 0); + topPath.lineTo(0, size.height / 10); + topPath.close(); + canvas.drawPath(topPath, paint); + var bottomPath = Path(); + bottomPath.moveTo(0, size.height * 0.98); + bottomPath.lineTo(size.width, size.height * 0.78); + bottomPath.lineTo(size.width, size.height); + bottomPath.lineTo(0, size.height); + bottomPath.close(); + canvas.drawPath(bottomPath, paint); + } + + @override + bool shouldRepaint(covariant _LoginPageBackgroundPainter oldDelegate) { + return color != oldDelegate.color; + } +} diff --git a/lib/core/auth/login/reset_password_request_page.dart b/lib/core/auth/login/reset_password_request_page.dart new file mode 100644 index 0000000..e6e70a4 --- /dev/null +++ b/lib/core/auth/login/reset_password_request_page.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:thingsboard_app/core/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: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); + + @override + _ResetPasswordRequestPageState createState() => + _ResetPasswordRequestPageState(); +} + +class _ResetPasswordRequestPageState + extends TbPageState { + final _isLoadingNotifier = ValueNotifier(false); + + final _resetPasswordFormKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack(children: [ + LoginPageBackground(), + SizedBox.expand( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: TbAppBar( + tbContext, + title: Text('${S.of(context).passwordReset}'), + ), + 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), + ), + 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} *'), + ), + Spacer(), + ElevatedButton( + child: Text( + '${S.of(context).requestPasswordReset}'), + style: ElevatedButton.styleFrom( + padding: + EdgeInsets.symmetric(vertical: 16)), + onPressed: () { + _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)), + )); + } else { + return SizedBox.shrink(); + } + }) + ]))) + ])); + } + + void _requestPasswordReset() async { + FocusScope.of(context).unfocus(); + if (_resetPasswordFormKey.currentState?.saveAndValidate() ?? false) { + var formValue = _resetPasswordFormKey.currentState!.value; + String email = formValue['email']; + _isLoadingNotifier.value = true; + try { + await Future.delayed(Duration(milliseconds: 300)); + await tbClient.sendResetPasswordLink(email); + _isLoadingNotifier.value = false; + 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 new file mode 100644 index 0000000..d15d23e --- /dev/null +++ b/lib/core/auth/login/two_factor_authentication_page.dart @@ -0,0 +1,459 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:thingsboard_app/core/auth/login/login_page_background.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:collection/collection.dart'; + +typedef ProviderDescFunction = String Function( + BuildContext context, String? contact); +typedef TextFunction = String Function(BuildContext context); + +class TwoFactorAuthProviderLoginData { + TextFunction nameFunction; + ProviderDescFunction descFunction; + TextFunction placeholderFunction; + String icon; + TwoFactorAuthProviderLoginData( + {required this.nameFunction, + required this.descFunction, + required this.placeholderFunction, + required this.icon}); +} + +final Map + 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'), + 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'), + 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'), + 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') +}; + +class TwoFactorAuthenticationPage extends TbPageWidget { + TwoFactorAuthenticationPage(TbContext tbContext) : super(tbContext); + + @override + _TwoFactorAuthenticationPageState createState() => + _TwoFactorAuthenticationPageState(); +} + +class _TwoFactorAuthenticationPageState + extends TbPageState { + final _twoFactorAuthFormKey = GlobalKey(); + ValueNotifier _selectedProvider = + ValueNotifier(null); + TwoFaProviderType? _prevProvider; + int? _minVerificationPeriod; + List _allowProviders = []; + ValueNotifier _disableSendButton = ValueNotifier(false); + ValueNotifier _showResendAction = ValueNotifier(false); + ValueNotifier _hideResendButton = ValueNotifier(true); + Timer? _timer; + Timer? _tooManyRequestsTimer; + ValueNotifier _countDownTime = ValueNotifier(0); + + @override + void initState() { + super.initState(); + var providersInfo = tbContext.twoFactorAuthProviders; + TwoFaProviderType.values.forEach((provider) { + var 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) { + _sendCode(); + _showResendAction.value = true; + } + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + _updatedTime(); + }); + } + + @override + void dispose() { + if (_timer != null) { + _timer!.cancel(); + } + if (_tooManyRequestsTimer != null) { + _tooManyRequestsTimer!.cancel(); + } + super.dispose(); + } + + @override + 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: [ + 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: 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), + ), + 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) { + int maxLengthInput = 6; + TextInputType keyboardType = TextInputType.number; + String pattern = '[0-9]*'; + + if (providerType == TwoFaProviderType.BACKUP_CODE) { + maxLengthInput = 8; + pattern = '[0-9abcdef]*'; + keyboardType = TextInputType.text; + } + + List> validators = [ + FormBuilderValidators.required( + 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]!; + + return FormBuilderTextField( + name: 'verificationCode', + autofocus: true, + maxLength: maxLengthInput, + keyboardType: keyboardType, + validator: FormBuilderValidators.compose(validators), + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: providerFormData.placeholderFunction(context))); + } + + Future _sendVerificationCode(BuildContext context) async { + FocusScope.of(context).unfocus(); + if (_twoFactorAuthFormKey.currentState?.saveAndValidate() ?? false) { + var formValue = _twoFactorAuthFormKey.currentState!.value; + String verificationCode = formValue['verificationCode']; + try { + await tbClient.checkTwoFaVerificationCode( + _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); + } else if (e.status == 429) { + _twoFactorAuthFormKey.currentState!.fields['verificationCode']! + .invalidate(S.of(context).verificationCodeManyRequest); + _disableSendButton.value = true; + if (_tooManyRequestsTimer != null) { + _tooManyRequestsTimer!.cancel(); + } + _tooManyRequestsTimer = Timer(Duration(seconds: 5), () { + _twoFactorAuthFormKey.currentState!.fields['verificationCode']! + .validate(); + _disableSendButton.value = false; + }); + } else { + showErrorNotification(e.message ?? 'Code verification failed!'); + } + } else { + showErrorNotification('Code verification failed!'); + } + } + } + } + + Future _selectProvider(TwoFaProviderType? type) async { + _prevProvider = type == null ? _selectedProvider.value : null; + _selectedProvider.value = type; + _showResendAction.value = false; + if (type != null) { + var providersInfo = tbContext.twoFactorAuthProviders; + var providerConfig = + providersInfo!.firstWhereOrNull((config) => config.type == type)!; + if (type != TwoFaProviderType.TOTP && + type != TwoFaProviderType.BACKUP_CODE) { + _sendCode(); + _showResendAction.value = true; + _minVerificationPeriod = + providerConfig.minVerificationCodeSendPeriod ?? 30; + } + } + } + + Future _sendCode() async { + _hideResendButton.value = true; + _countDownTime.value = 0; + try { + await tbContext.tbClient + .getTwoFactorAuthService() + .requestTwoFaVerificationCode(_selectedProvider.value!, + requestConfig: RequestConfig(ignoreErrors: true)); + } catch (e) { + } finally { + _countDownTime.value = _minVerificationPeriod!; + } + } + + Future _goBack() async { + if (_prevProvider != null) { + await _selectProvider(_prevProvider); + } else { + tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true)); + } + return false; + } + + void _updatedTime() { + if (_countDownTime.value > 0) { + _countDownTime.value--; + if (_countDownTime.value == 0) { + _hideResendButton.value = false; + } + } + } +} diff --git a/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart new file mode 100644 index 0000000..a22b338 --- /dev/null +++ b/lib/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart @@ -0,0 +1,21 @@ +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract interface class INoAuthRemoteDatasource { + Future getJwtToken({ + required String host, + required String key, + }); + + Future setUserFromJwtToken(LoginResponse loginData); + + Future logout({RequestConfig? requestConfig, bool notifyUser = true}); + + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }); + + bool isAuthenticated(); +} diff --git a/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart new file mode 100644 index 0000000..4d9f9aa --- /dev/null +++ b/lib/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart @@ -0,0 +1,74 @@ +import 'dart:ui'; + +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NoAuthRemoteDatasource implements INoAuthRemoteDatasource { + const NoAuthRemoteDatasource({ + required this.thingsboardClient, + required this.tbLogger, + required this.tbContext, + }); + + final ThingsboardClient thingsboardClient; + final TbLogger tbLogger; + final TbContext tbContext; + + @override + Future getJwtToken({ + required String host, + required String key, + }) async { + try { + final data = await thingsboardClient.getLoginDataBySecretKey( + host: host, + key: key, + ); + + return data; + } catch (e) { + tbLogger.error('NoAuthRemoteDatasource:getJwtToken() message $e'); + rethrow; + } + } + + @override + Future setUserFromJwtToken(LoginResponse loginData) async { + await thingsboardClient.setUserFromJwtToken( + loginData.token, + loginData.refreshToken, + false, + ); + } + + @override + Future logout({ + RequestConfig? requestConfig, + bool notifyUser = true, + }) async { + await tbContext.logout( + requestConfig: requestConfig, + notifyUser: notifyUser, + ); + } + + @override + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }) async { + await tbContext.reInit( + endpoint: endpoint, + onDone: onDone, + onError: onError, + ); + } + + @override + bool isAuthenticated() { + return tbContext.isAuthenticated; + } +} diff --git a/lib/core/auth/noauth/data/repository/noauth_repository.dart b/lib/core/auth/noauth/data/repository/noauth_repository.dart new file mode 100644 index 0000000..3e4bc99 --- /dev/null +++ b/lib/core/auth/noauth/data/repository/noauth_repository.dart @@ -0,0 +1,53 @@ +import 'dart:ui'; + +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NoAuthRepository implements INoAuthRepository { + const NoAuthRepository({required this.remoteDatasource}); + + final INoAuthRemoteDatasource remoteDatasource; + + @override + Future getJwtToken({ + required String host, + required String key, + }) { + return remoteDatasource.getJwtToken(host: host, key: key); + } + + @override + Future setUserFromJwtToken(LoginResponse loginData) async { + await remoteDatasource.setUserFromJwtToken(loginData); + } + + @override + Future logout({ + RequestConfig? requestConfig, + bool notifyUser = true, + }) async { + await remoteDatasource.logout( + requestConfig: requestConfig, + notifyUser: notifyUser, + ); + } + + @override + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }) async { + await remoteDatasource.reInit( + endpoint: endpoint, + onDone: onDone, + onError: onError, + ); + } + + @override + bool isAuthenticated() { + return remoteDatasource.isAuthenticated(); + } +} diff --git a/lib/core/auth/noauth/di/noauth_di.dart b/lib/core/auth/noauth/di/noauth_di.dart new file mode 100644 index 0000000..2e1bf4c --- /dev/null +++ b/lib/core/auth/noauth/di/noauth_di.dart @@ -0,0 +1,52 @@ +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/i_noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/auth/noauth/data/datasource/remote/noauth_remote_datasource.dart'; +import 'package:thingsboard_app/core/auth/noauth/data/repository/noauth_repository.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; + +abstract final class NoAuthDi { + static void init({required TbContext tbContext}) { + getIt.pushNewScope( + scopeName: 'NoAuthDi', + init: (locator) { + // Datasource + locator.registerFactory( + () => NoAuthRemoteDatasource( + thingsboardClient: tbContext.tbClient, + tbLogger: locator(), + tbContext: tbContext, + ), + ); + + // Repository + locator.registerFactory( + () => NoAuthRepository( + remoteDatasource: locator(), + ), + ); + + // UseCases + locator.registerFactory( + () => SwitchEndpointUseCase( + repository: locator(), + logger: locator(), + ), + ); + + // Bloc + locator.registerLazySingleton( + () => NoAuthBloc( + switchEndpointUseCase: locator(), + ), + ); + }, + ); + } + + static void dispose() { + getIt.dropScope('NoAuthDi'); + } +} diff --git a/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart new file mode 100644 index 0000000..03db7f2 --- /dev/null +++ b/lib/core/auth/noauth/domain/repository/i_noauth_repository.dart @@ -0,0 +1,21 @@ +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract interface class INoAuthRepository { + Future getJwtToken({ + required String host, + required String key, + }); + + Future setUserFromJwtToken(LoginResponse loginData); + + Future logout({RequestConfig? requestConfig, bool notifyUser = true}); + + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }); + + bool isAuthenticated(); +} diff --git a/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart new file mode 100644 index 0000000..1641e4a --- /dev/null +++ b/lib/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/repository/i_noauth_repository.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/firebase_options.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/usecase.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +final class SwitchEndpointParams { + const SwitchEndpointParams({ + required this.data, + required this.onDone, + required this.onError, + }); + + final Map data; + final VoidCallback onDone; + final Function(String) onError; + + dynamic operator [](String key) => data[key]; +} + +class SwitchEndpointUseCase extends UseCase { + SwitchEndpointUseCase({ + required this.repository, + required this.logger, + }) : _progressSteamCtrl = StreamController.broadcast(); + + final INoAuthRepository repository; + + final TbLogger logger; + late final StreamController _progressSteamCtrl; + + Stream get stream => _progressSteamCtrl.stream; + + @override + Future call(SwitchEndpointParams params) async { + final uri = params['uri']!; + final host = params['host'] ?? uri.origin; + final key = params['secret']!; + final currentEndpoint = await getIt().getEndpoint(); + final isTheSameHost = + Uri.parse(host).host.compareTo(Uri.parse(currentEndpoint).host) == 0; + + try { + _progressSteamCtrl.add('Getting data from your host $host'); + final loginData = await repository.getJwtToken(host: host, key: key); + + if (repository.isAuthenticated()) { + _progressSteamCtrl.add('Logout you ...'); + await repository.logout( + requestConfig: RequestConfig(ignoreErrors: true), + notifyUser: false, + ); + } + + if (isTheSameHost) { + _progressSteamCtrl.add('Logging you into the host $host'); + } else { + _progressSteamCtrl.add('Switching you to the new host $host'); + } + + await repository.setUserFromJwtToken(loginData); + + if (!isTheSameHost) { + logger.debug('SwitchEndpointUseCase:deleteFB App'); + await getIt() + ..removeApp() + ..removeApp(name: currentEndpoint); + await getIt().setEndpoint(host); + + // If we revert to the original host configured in the app_constants + if (!await getIt().isCustomEndpoint()) { + await getIt().initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } + } + + // A re-initialization is required if we set 'notifyUser' to true for + // 'setUserFromJwtToken'. This code will be executed twice. + await repository.reInit( + endpoint: host, + onDone: params.onDone, + onError: (error) { + logger.error('SwitchEndpointUseCase:onError $error'); + params.onError(error.message ?? error.toString()); + }, + ); + } on ThingsboardError catch (e) { + logger.error('SwitchEndpointUseCase:ThingsboardError $e', e); + params.onError(e.message ?? e.toString()); + } catch (e) { + logger.error('SwitchEndpointUseCase:catch $e', e); + params.onError(e.toString()); + } + } + + void dispose() { + _progressSteamCtrl.close(); + } +} diff --git a/lib/core/auth/noauth/presentation/bloc/bloc.dart b/lib/core/auth/noauth/presentation/bloc/bloc.dart new file mode 100644 index 0000000..6b18dca --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'noauth_bloc.dart'; +export 'noauth_events.dart'; +export 'noauth_states.dart'; diff --git a/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart b/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart new file mode 100644 index 0000000..849ab7d --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/noauth_bloc.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thingsboard_app/core/auth/noauth/domain/usecases/switch_endpoint_usecase.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; + +class NoAuthBloc extends Bloc { + NoAuthBloc({required this.switchEndpointUseCase}) + : super(const NoAuthLoadingState()) { + switchEndpointProgressSubscription = switchEndpointUseCase.stream.listen( + (event) => add(SwitchEndpointProgressUpdateEvent(progressMessage: event)), + ); + + _switchEndpointEventHandler(); + _switchEndpointUpdatesHandler(); + _switchEndpointDoneEvent(); + _switchEndpointErrorEvent(); + } + + final SwitchEndpointUseCase switchEndpointUseCase; + late final StreamSubscription switchEndpointProgressSubscription; + + void _switchEndpointEventHandler() { + on( + (event, emit) async { + if (isClosed) { + return; + } + + if (event.parameters == null) { + emit( + const NoAuthErrorState( + message: 'An empty request data received.', + ), + ); + return; + } + + switchEndpointUseCase( + SwitchEndpointParams( + data: event.parameters!, + onDone: () { + add(const SwitchEndpointDoneEvent()); + }, + onError: (message) { + add(SwitchEndpointErrorEvent(message: message)); + }, + ), + ); + }, + ); + } + + void _switchEndpointUpdatesHandler() { + on( + (event, emit) async { + emit(NoAuthWipState(currentStateMessage: event.progressMessage)); + }, + ); + } + + void _switchEndpointDoneEvent() { + on( + (event, emit) async { + emit(const NoAuthDoneState()); + }, + ); + } + + void _switchEndpointErrorEvent() { + on( + (event, emit) async { + emit(NoAuthErrorState(message: event.message)); + }, + ); + } + + @override + Future close() { + switchEndpointProgressSubscription.cancel(); + switchEndpointUseCase.dispose(); + return super.close(); + } +} diff --git a/lib/core/auth/noauth/presentation/bloc/noauth_events.dart b/lib/core/auth/noauth/presentation/bloc/noauth_events.dart new file mode 100644 index 0000000..b3411e5 --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/noauth_events.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +sealed class NoAuthEvent extends Equatable { + const NoAuthEvent(); + + @override + List get props => []; +} + +final class SwitchToAnotherEndpointEvent extends NoAuthEvent { + const SwitchToAnotherEndpointEvent({required this.parameters}); + + final Map? parameters; + + @override + List get props => [parameters]; +} + +final class SwitchEndpointProgressUpdateEvent extends NoAuthEvent { + const SwitchEndpointProgressUpdateEvent({required this.progressMessage}); + + final String progressMessage; + + @override + List get props => [progressMessage]; +} + +final class SwitchEndpointDoneEvent extends NoAuthEvent { + const SwitchEndpointDoneEvent(); +} + +final class SwitchEndpointErrorEvent extends NoAuthEvent { + const SwitchEndpointErrorEvent({required this.message}); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/core/auth/noauth/presentation/bloc/noauth_states.dart b/lib/core/auth/noauth/presentation/bloc/noauth_states.dart new file mode 100644 index 0000000..aefd94f --- /dev/null +++ b/lib/core/auth/noauth/presentation/bloc/noauth_states.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +sealed class NoAuthState extends Equatable { + const NoAuthState(); + + @override + List get props => []; +} + +final class NoAuthLoadingState extends NoAuthState { + const NoAuthLoadingState(); +} + +final class NoAuthWipState extends NoAuthState { + const NoAuthWipState({required this.currentStateMessage}); + + final String currentStateMessage; + + @override + List get props => [currentStateMessage]; +} + +final class NoAuthErrorState extends NoAuthState { + const NoAuthErrorState({required this.message}); + + final String message; + + @override + List get props => [message]; +} + +final class NoAuthDoneState extends NoAuthState { + const NoAuthDoneState(); +} diff --git a/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart new file mode 100644 index 0000000..808888f --- /dev/null +++ b/lib/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/auth/noauth/di/noauth_di.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/locator.dart'; + +class SwitchEndpointNoAuthView extends TbPageWidget { + SwitchEndpointNoAuthView({ + required this.tbContext, + required this.arguments, + }) : super(tbContext); + + final Map? arguments; + final TbContext tbContext; + + @override + State createState() => _SwitchEndpointNoAuthViewState(); +} + +class _SwitchEndpointNoAuthViewState + extends TbPageState { + @override + Widget build(BuildContext context) { + if (getIt().isClosed) { + return const Scaffold( + body: NoAuthLoadingWidget(), + ); + } + + return BlocProvider.value( + value: getIt() + ..add( + SwitchToAnotherEndpointEvent( + parameters: widget.arguments, + ), + ), + child: Scaffold( + body: SafeArea( + child: BlocConsumer( + listener: (context, state) { + if (state is NoAuthErrorState) { + widget.tbContext.showErrorNotification(state.message); + Future.delayed(const Duration(seconds: 5), () { + if (mounted) { + widget.tbContext.pop(); + } + }); + } else if (state is NoAuthDoneState) { + GetIt.instance().close(); + getIt().router.navigateTo( + context, + '/home', + replace: true, + maintainState: false, + clearStack: true, + ); + } + }, + buildWhen: (_, state) => state is! NoAuthDoneState, + builder: (context, state) { + switch (state) { + case NoAuthLoadingState(): + return const NoAuthLoadingWidget(); + + case NoAuthWipState(): + return Stack( + alignment: AlignmentDirectional.center, + children: [ + const NoAuthLoadingWidget(), + Positioned( + top: MediaQuery.of(context).size.height / 2 + 80, + child: BlocBuilder( + buildWhen: (_, state) => state is NoAuthWipState, + builder: (context, state) { + if (state is NoAuthWipState) { + return SizedBox( + width: MediaQuery.of(context).size.width - 20, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + state.currentStateMessage, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge, + ), + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + }, + ), + ), + ], + ); + + case NoAuthErrorState(): + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 10), + Text( + 'Something went wrong ... Rollback', + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.w500), + ), + ], + ), + ); + + default: + return const SizedBox.shrink(); + } + }, + ), + ), + ), + ); + } + + @override + void initState() { + NoAuthDi.init(tbContext: widget.tbContext); + super.initState(); + } + + @override + void dispose() { + NoAuthDi.dispose(); + super.dispose(); + } +} diff --git a/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart new file mode 100644 index 0000000..7236433 --- /dev/null +++ b/lib/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class EndpointNameWidget extends StatelessWidget { + const EndpointNameWidget({required this.endpoint}); + + final String endpoint; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all( + color: Color(0xFF305680), + ), + ), + padding: const EdgeInsets.all(5), + child: Center( + child: Text( + Uri.parse(endpoint).host, + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(color: Color(0xFF305680)), + ), + ), + ); + } +} diff --git a/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart b/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart new file mode 100644 index 0000000..4ee5d30 --- /dev/null +++ b/lib/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; + +class NoAuthLoadingWidget extends StatelessWidget { + const NoAuthLoadingWidget(); + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Container( + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator(size: 50.0), + ), + ), + ); + } +} diff --git a/lib/core/auth/noauth/routes/noauth_routes.dart b/lib/core/auth/noauth/routes/noauth_routes.dart new file mode 100644 index 0000000..bf396f9 --- /dev/null +++ b/lib/core/auth/noauth/routes/noauth_routes.dart @@ -0,0 +1,23 @@ +import 'package:fluro/fluro.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/view/switch_endpoint_noauth_view.dart'; + +class NoAuthRoutes extends TbRoutes { + NoAuthRoutes(super.tbContext); + + static const noAuthPageRoutes = '/api/noauth/qr'; + + late final noAuthQrHandler = Handler( + handlerFunc: (context, params) { + return SwitchEndpointNoAuthView( + tbContext: tbContext, + arguments: context?.settings?.arguments as Map?, + ); + }, + ); + + @override + void doRegisterRoutes(FluroRouter router) { + router.define(noAuthPageRoutes, handler: noAuthQrHandler); + } +} diff --git a/lib/core/auth/oauth2/app_secret_provider.dart b/lib/core/auth/oauth2/app_secret_provider.dart new file mode 100644 index 0000000..f6f751e --- /dev/null +++ b/lib/core/auth/oauth2/app_secret_provider.dart @@ -0,0 +1,15 @@ +import 'package:thingsboard_app/constants/app_constants.dart'; + +abstract class AppSecretProvider { + Future getAppSecret(); + + factory AppSecretProvider.local() => _LocalAppSecretProvider(); +} + +/// Not for production (only for debugging) +class _LocalAppSecretProvider implements AppSecretProvider { + @override + Future getAppSecret() async { + return ThingsboardAppConstants.thingsboardOAuth2AppSecret; + } +} diff --git a/lib/core/auth/oauth2/tb_oauth2_client.dart b/lib/core/auth/oauth2/tb_oauth2_client.dart new file mode 100644 index 0000000..d943525 --- /dev/null +++ b/lib/core/auth/oauth2/tb_oauth2_client.dart @@ -0,0 +1,124 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:thingsboard_app/constants/app_constants.dart'; +import 'package:thingsboard_app/core/auth/web/tb_web_auth.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; + +import 'app_secret_provider.dart'; + +class TbOAuth2AuthenticateResult { + String? accessToken; + String? refreshToken; + String? error; + + TbOAuth2AuthenticateResult.success(this.accessToken, this.refreshToken); + + TbOAuth2AuthenticateResult.failed(this.error); + + bool get success => error == null; +} + +class TbOAuth2Client { + final TbContext _tbContext; + final AppSecretProvider _appSecretProvider; + + TbOAuth2Client( + {required TbContext tbContext, + required AppSecretProvider appSecretProvider}) + : _tbContext = tbContext, + _appSecretProvider = appSecretProvider; + + Future authenticate(String oauth2Url) async { + final appSecret = await _appSecretProvider.getAppSecret(); + final pkgName = _tbContext.packageName; + final jwt = JWT( + { + 'callbackUrlScheme': + ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme + }, + issuer: pkgName, + ); + final key = SecretKey(appSecret); + final appToken = jwt.sign(key, + algorithm: _HMACBase64Algorithm.HS512, expiresIn: Duration(minutes: 2)); + var url = + Uri.parse(await getIt().getEndpoint() + oauth2Url); + final params = Map.from(url.queryParameters); + params['pkg'] = pkgName; + params['appToken'] = appToken; + url = url.replace(queryParameters: params); + final result = await TbWebAuth.authenticate( + url: url.toString(), + callbackUrlScheme: + ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, + saveHistory: false); + final resultUri = Uri.parse(result); + final error = resultUri.queryParameters['error']; + if (error != null) { + return TbOAuth2AuthenticateResult.failed(error); + } else { + final accessToken = resultUri.queryParameters['accessToken']; + final refreshToken = resultUri.queryParameters['refreshToken']; + if (accessToken != null && refreshToken != null) { + return TbOAuth2AuthenticateResult.success(accessToken, refreshToken); + } else { + return TbOAuth2AuthenticateResult.failed( + 'No authentication credentials in response.'); + } + } + } +} + +class _HMACBase64Algorithm extends JWTAlgorithm { + static const HS512 = _HMACBase64Algorithm('HS512'); + + final String _name; + + const _HMACBase64Algorithm(this._name); + + @override + String get name => _name; + + @override + Uint8List sign(JWTKey key, Uint8List body) { + assert(key is SecretKey, 'key must be a SecretKey'); + final secretKey = key as SecretKey; + + final hmac = Hmac(_getHash(name), base64Decode(secretKey.key)); + + return Uint8List.fromList(hmac.convert(body).bytes); + } + + @override + bool verify(JWTKey key, Uint8List body, Uint8List signature) { + assert(key is SecretKey, 'key must be a SecretKey'); + + final actual = sign(key, body); + + if (actual.length != signature.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] != signature[i]) return false; + } + + return true; + } + + Hash _getHash(String name) { + switch (name) { + case 'HS256': + return sha256; + case 'HS384': + return sha384; + case 'HS512': + return sha512; + default: + throw ArgumentError.value(name, 'name', 'unknown hash name'); + } + } +} diff --git a/lib/core/auth/web/tb_web_auth.dart b/lib/core/auth/web/tb_web_auth.dart new file mode 100644 index 0000000..6013f3b --- /dev/null +++ b/lib/core/auth/web/tb_web_auth.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart' show MethodChannel; + +class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver { + final Function onResumed; + + _OnAppLifecycleResumeObserver(this.onResumed); + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + onResumed(); + } + } +} + +class TbWebAuth { + static const MethodChannel _channel = const MethodChannel('tb_web_auth'); + + static final _OnAppLifecycleResumeObserver _resumedObserver = + _OnAppLifecycleResumeObserver(() { + _cleanUpDanglingCalls(); + }); + + 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 + WidgetsBinding.instance.addObserver(_resumedObserver); + return await _channel.invokeMethod('authenticate', { + 'url': url, + 'callbackUrlScheme': callbackUrlScheme, + 'saveHistory': saveHistory, + }) as String; + } + + static Future _cleanUpDanglingCalls() async { + await _channel.invokeMethod('cleanUpDanglingCalls'); + WidgetsBinding.instance.removeObserver(_resumedObserver); + } +} diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart new file mode 100644 index 0000000..2845b0d --- /dev/null +++ b/lib/core/context/tb_context.dart @@ -0,0 +1,752 @@ +import 'dart:async'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:fluro/fluro.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:thingsboard_app/constants/database_keys.dart'; +import 'package:thingsboard_app/core/auth/oauth2/app_secret_provider.dart'; +import 'package:thingsboard_app/core/auth/oauth2/tb_oauth2_client.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/main/main_page.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; +import 'package:thingsboard_app/utils/services/notification_service.dart'; +import 'package:thingsboard_app/utils/services/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}); + + Future closeDashboard({bool animate}); + + bool isDashboardOpen(); + + Future dashboardGoBack(); +} + +class TbContext implements PopEntry { + static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + bool _initialized = false; + bool isUserLoaded = false; + final _isAuthenticated = ValueNotifier(false); + PlatformType? _oauth2PlatformType; + List? oauth2ClientInfos; + List? twoFactorAuthProviders; + User? userDetails; + HomeDashboardInfo? homeDashboard; + final _isLoadingNotifier = ValueNotifier(false); + final _log = TbLogger(); + late final _widgetActionHandler; + late final AndroidDeviceInfo? _androidInfo; + late final IosDeviceInfo? _iosInfo; + late final String packageName; + TbMainDashboardHolder? _mainDashboardHolder; + bool _closeMainFirst = false; + StreamSubscription? _appLinkStreamSubscription; + late bool _handleRootState; + + final ValueNotifier canPopNotifier = ValueNotifier(false); + + PopInvokedCallback get onPopInvoked => onPopInvokedImpl; + + GlobalKey messengerKey = + GlobalKey(); + late ThingsboardClient tbClient; + late TbOAuth2Client oauth2Client; + + final FluroRouter router; + final RouteObserver routeObserver = RouteObserver(); + + Listenable get isAuthenticatedListenable => _isAuthenticated; + + bool get isAuthenticated => _isAuthenticated.value; + + bool get hasOAuthClients => + oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty; + + TbContextState? currentState; + + TbContext(this.router) { + _widgetActionHandler = WidgetActionHandler(this); + } + + TbLogger get log => _log; + + WidgetActionHandler get widgetActionHandler => _widgetActionHandler; + + Future init() async { + assert(() { + if (_initialized) { + throw StateError('TbContext already initialized!'); + } + return true; + }()); + _handleRootState = true; + _initialized = true; + + final endpoint = await getIt().getEndpoint(); + log.debug('TbContext::init() endpoint: $endpoint'); + + tbClient = ThingsboardClient( + endpoint, + storage: getIt(), + onUserLoaded: onUserLoaded, + onError: onError, + onLoadStarted: onLoadStarted, + onLoadFinished: onLoadFinished, + computeFunc: (callback, message) => compute(callback, message), + ); + + oauth2Client = TbOAuth2Client( + tbContext: this, + appSecretProvider: AppSecretProvider.local(), + ); + + try { + if (UniversalPlatform.isAndroid) { + _androidInfo = await deviceInfoPlugin.androidInfo; + _oauth2PlatformType = PlatformType.ANDROID; + } else if (UniversalPlatform.isIOS) { + _iosInfo = await deviceInfoPlugin.iosInfo; + _oauth2PlatformType = PlatformType.IOS; + } else { + _oauth2PlatformType = PlatformType.WEB; + } + if (UniversalPlatform.isAndroid || UniversalPlatform.isIOS) { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + packageName = packageInfo.packageName; + } else { + packageName = 'web.app'; + } + await tbClient.init(); + } catch (e, s) { + log.error('Failed to init tbContext: $e', e, s); + await onFatalError(e); + } + } + + Future reInit({ + required String endpoint, + required VoidCallback onDone, + required ErrorCallback onError, + }) async { + log.debug('TbContext:reinit()'); + + _handleRootState = false; + _initialized = false; + + tbClient = ThingsboardClient( + endpoint, + storage: getIt(), + onUserLoaded: () => onUserLoaded(onDone: onDone), + onError: onError, + onLoadStarted: onLoadStarted, + onLoadFinished: onLoadFinished, + computeFunc: (callback, message) => compute(callback, message), + ); + + oauth2Client = TbOAuth2Client( + tbContext: this, + appSecretProvider: AppSecretProvider.local(), + ); + + await tbClient.init(); + _initialized = true; + } + + void setMainDashboardHolder(TbMainDashboardHolder holder) { + _mainDashboardHolder = holder; + } + + Future onFatalError(e) async { + var message = e is ThingsboardError + ? (e.message ?? 'Unknown error.') + : 'Unknown error.'; + message = 'Fatal application error occured:\n' + message + '.'; + await alert(title: 'Fatal error', message: message, ok: 'Close'); + logout(); + } + + void onError(ThingsboardError tbError) { + log.error('onError', tbError, tbError.getStackTrace()); + showErrorNotification(tbError.message!); + } + + void showErrorNotification(String message, {Duration? duration}) { + showNotification(message, NotificationType.error, duration: duration); + } + + void showInfoNotification(String message, {Duration? duration}) { + showNotification(message, NotificationType.info, duration: duration); + } + + void showWarnNotification(String message, {Duration? duration}) { + showNotification(message, NotificationType.warn, duration: duration); + } + + void showSuccessNotification(String message, {Duration? duration}) { + showNotification(message, NotificationType.success, duration: duration); + } + + void showNotification(String message, NotificationType type, + {Duration? duration}) { + duration ??= const Duration(days: 1); + Color backgroundColor; + var textColor = Color(0xFFFFFFFF); + switch (type) { + case NotificationType.info: + backgroundColor = Color(0xFF323232); + break; + case NotificationType.warn: + backgroundColor = Color(0xFFdc6d1b); + break; + case NotificationType.success: + backgroundColor = Color(0xFF008000); + break; + case NotificationType.error: + backgroundColor = Color(0xFF800000); + break; + } + final snackBar = SnackBar( + duration: duration, + backgroundColor: backgroundColor, + content: Text( + message, + style: TextStyle(color: textColor), + ), + action: SnackBarAction( + label: 'Close', + textColor: textColor, + onPressed: () { + messengerKey.currentState! + .hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss); + }, + ), + ); + messengerKey.currentState!.removeCurrentSnackBar(); + messengerKey.currentState!.showSnackBar(snackBar); + } + + void hideNotification() { + messengerKey.currentState!.removeCurrentSnackBar(); + } + + void onLoadStarted() { + log.debug('TbContext: On load started.'); + _isLoadingNotifier.value = true; + } + + void onLoadFinished() async { + log.debug('TbContext: On load finished.'); + _isLoadingNotifier.value = false; + } + + Future onUserLoaded({VoidCallback? onDone}) async { + try { + log.debug( + 'TbContext.onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}'); + isUserLoaded = true; + if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { + log.debug('authUser: ${tbClient.getAuthUser()}'); + if (tbClient.getAuthUser()!.userId != null) { + try { + userDetails = await tbClient.getUserService().getUser(); + homeDashboard = + await tbClient.getDashboardService().getHomeDashboardInfo(); + } catch (e) { + if (!_isConnectionError(e)) { + logout(); + } else { + rethrow; + } + } + } + } else { + if (tbClient.isPreVerificationToken()) { + log.debug('authUser: ${tbClient.getAuthUser()}'); + twoFactorAuthProviders = await tbClient + .getTwoFactorAuthService() + .getAvailableLoginTwoFaProviders(); + } else { + twoFactorAuthProviders = null; + } + + userDetails = null; + homeDashboard = null; + oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients( + pkgName: packageName, + platform: _oauth2PlatformType, + requestConfig: RequestConfig(followRedirect: false), + ); + } + + _isAuthenticated.value = + tbClient.isAuthenticated() && !tbClient.isPreVerificationToken(); + + if (isAuthenticated) { + onDone?.call(); + } + + if (_handleRootState) { + await updateRouteState(); + } + + if (isAuthenticated) { + if (getIt().apps.isNotEmpty) { + await NotificationService().init(tbClient, log, this); + } + } + } catch (e, s) { + log.error('TbContext.onUserLoaded: $e', e, s); + + if (_isConnectionError(e)) { + final res = await confirm( + title: 'Connection error', + message: 'Failed to connect to server', + cancel: 'Cancel', + ok: 'Retry', + ); + if (res == true) { + onUserLoaded(); + } else { + navigateTo( + '/login', + replace: true, + clearStack: true, + transition: TransitionType.fadeIn, + transitionDuration: Duration(milliseconds: 750), + ); + } + } + } finally { + try { + final link = await getIt().getItem( + DatabaseKeys.initialAppLink, + ); + navigateByAppLink(link); + } catch (e) { + log.error('TbContext:getInitialUri() exception $e'); + } + + if (_appLinkStreamSubscription == null) { + _appLinkStreamSubscription = linkStream.listen((link) { + navigateByAppLink(link); + }, onError: (err) { + log.error('linkStream.listen $err'); + }); + } + } + } + + Future navigateByAppLink(String? link) async { + if (link != null) { + final uri = Uri.parse(link); + await getIt().deleteItem( + DatabaseKeys.initialAppLink, + ); + + log.debug('TbContext: navigate by appLink $uri'); + navigateTo( + uri.path, + routeSettings: RouteSettings( + arguments: {...uri.queryParameters, 'uri': uri}, + ), + ); + } + } + + Future logout({ + RequestConfig? requestConfig, + bool notifyUser = true, + }) async { + log.debug('TbContext::logout($requestConfig, $notifyUser)'); + _handleRootState = true; + + if (getIt().apps.isNotEmpty) { + await NotificationService().logout(); + } + + await tbClient.logout( + requestConfig: requestConfig, + notifyUser: notifyUser, + ); + + _appLinkStreamSubscription?.cancel(); + _appLinkStreamSubscription = null; + } + + bool _isConnectionError(e) { + return e is ThingsboardError && + e.errorCode == ThingsBoardErrorCode.general && + e.message == 'Unable to connect'; + } + + Future updateRouteState() async { + log.debug( + 'TbContext:updateRouteState() ${currentState != null && currentState!.mounted}'); + if (currentState != null && currentState!.mounted) { + if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { + final defaultDashboardId = _defaultDashboardId(); + if (defaultDashboardId != null) { + bool fullscreen = _userForceFullscreen(); + if (!fullscreen) { + await navigateToDashboard(defaultDashboardId, animate: false); + navigateTo( + '/home', + replace: true, + closeDashboard: false, + transition: TransitionType.none, + ); + } else { + navigateTo( + '/fullscreenDashboard/$defaultDashboardId', + replace: true, + transition: TransitionType.fadeIn, + ); + } + } else { + navigateTo( + '/home', + replace: true, + transition: TransitionType.fadeIn, + transitionDuration: Duration(milliseconds: 750), + ); + } + } else { + navigateTo( + '/login', + replace: true, + clearStack: true, + transition: TransitionType.fadeIn, + transitionDuration: Duration(milliseconds: 750), + ); + } + } + } + + String? _defaultDashboardId() { + if (userDetails != null && userDetails!.additionalInfo != null) { + return userDetails!.additionalInfo!['defaultDashboardId']; + } + return null; + } + + bool _userForceFullscreen() { + return tbClient.getAuthUser()!.isPublic! || + (userDetails != null && + userDetails!.additionalInfo != null && + userDetails!.additionalInfo!['defaultDashboardFullscreen'] == true); + } + + bool isPhysicalDevice() { + if (UniversalPlatform.isAndroid) { + return _androidInfo!.isPhysicalDevice == true; + } else if (UniversalPlatform.isIOS) { + return _iosInfo!.isPhysicalDevice; + } else { + return false; + } + } + + String userAgent() { + String userAgent = 'Mozilla/5.0'; + if (UniversalPlatform.isAndroid) { + userAgent += + ' (Linux; Android ${_androidInfo!.version.release}; ${_androidInfo.model})'; + } else if (UniversalPlatform.isIOS) { + userAgent += ' (${_iosInfo!.model})'; + } + userAgent += + ' AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36'; + return userAgent; + } + + bool isHomePage() { + if (currentState != null) { + if (currentState is TbMainState) { + var mainState = currentState as TbMainState; + return mainState.isHomePage(); + } + } + return false; + } + + Future navigateTo( + String path, { + bool replace = false, + bool clearStack = false, + closeDashboard = true, + TransitionType? transition, + Duration? transitionDuration, + bool restoreDashboard = true, + RouteSettings? routeSettings, + }) async { + if (currentState != null) { + hideNotification(); + bool isOpenedDashboard = + _mainDashboardHolder?.isDashboardOpen() == true && closeDashboard; + if (isOpenedDashboard) { + _mainDashboardHolder?.openMain(); + } + if (currentState is TbMainState) { + var mainState = currentState as TbMainState; + if (mainState.canNavigate(path) && !replace) { + mainState.navigateToPath(path); + return; + } + } + if (TbMainNavigationItem.isMainPageState(this, path)) { + replace = true; + clearStack = true; + } + if (transition != TransitionType.nativeModal && isOpenedDashboard) { + transition = TransitionType.none; + } else if (transition == null) { + if (replace) { + transition = TransitionType.fadeIn; + } else { + transition = TransitionType.native; + } + } + _closeMainFirst = isOpenedDashboard; + return await router.navigateTo( + currentState!.context, + path, + transition: transition, + transitionDuration: transitionDuration, + replace: replace, + clearStack: clearStack, + routeSettings: routeSettings, + ); + } + } + + Future navigateToDashboard(String dashboardId, + {String? dashboardTitle, + String? state, + bool? hideToolbar, + bool animate = true}) async { + await _mainDashboardHolder?.navigateToDashboard(dashboardId, + dashboardTitle: dashboardTitle, + state: state, + hideToolbar: hideToolbar, + animate: animate); + } + + Future showFullScreenDialog(Widget dialog) { + return Navigator.of(currentState!.context).push( + new MaterialPageRoute( + builder: (BuildContext context) { + return dialog; + }, + fullscreenDialog: true, + ), + ); + } + + void pop([T? result, BuildContext? context]) async { + await closeMainIfNeeded(); + var targetContext = context ?? currentState?.context; + if (targetContext != null) { + router.pop(targetContext, result); + } + } + + Future maybePop([T? result]) async { + if (currentState != null) { + return Navigator.of(currentState!.context).maybePop(result); + } else { + return true; + } + } + + 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; + } + } + return false; + } + + Future alert( + {required String title, required String message, String ok = 'Ok'}) { + return showDialog( + context: currentState!.context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton(onPressed: () => pop(null, context), child: Text(ok)) + ], + )); + } + + Future confirm( + {required String title, + required String message, + String cancel = 'Cancel', + String ok = 'Ok'}) { + 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); + } +} diff --git a/lib/core/context/tb_context_widget.dart b/lib/core/context/tb_context_widget.dart new file mode 100644 index 0000000..5155f5b --- /dev/null +++ b/lib/core/context/tb_context_widget.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; + +abstract class RefreshableWidget extends Widget { + refresh(); +} + +abstract class TbContextStatelessWidget extends StatelessWidget + with HasTbContext { + TbContextStatelessWidget(TbContext tbContext, {Key? key}) : super(key: key) { + setTbContext(tbContext); + } +} + +abstract class TbContextWidget extends StatefulWidget with HasTbContext { + TbContextWidget(TbContext tbContext, {Key? key}) : super(key: key) { + setTbContext(tbContext); + } +} + +abstract class TbContextState extends State + with HasTbContext { + final bool handleLoading; + bool closeMainFirst = false; + + TbContextState({this.handleLoading = false}); + + @override + void initState() { + super.initState(); + setupTbContext(this); + } + + @override + void dispose() { + super.dispose(); + } + + Future willPop() async { + return true; + } +} + +mixin TbMainState { + bool canNavigate(String path); + + navigateToPath(String path); + + bool isHomePage(); +} + +abstract class TbPageWidget extends TbContextWidget { + TbPageWidget(TbContext tbContext, {Key? key}) : super(tbContext, key: key); +} + +abstract class TbPageState extends TbContextState + with RouteAware { + TbPageState({bool handleUserLoaded = false}) : super(handleLoading: true); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + subscribeRouteObserver(this); + } + + @override + void dispose() { + unsubscribeRouteObserver(this); + super.dispose(); + } + + @override + void didPush() { + setupCurrentState(this); + } + + @override + void didPopNext() { + hideNotification(); + setupCurrentState(this); + } +} + +class TextContextWidget extends TbContextWidget { + final String text; + + TextContextWidget(TbContext tbContext, this.text) : super(tbContext); + + @override + _TextContextWidgetState createState() => _TextContextWidgetState(); +} + +class _TextContextWidgetState extends TbContextState { + @override + Widget build(BuildContext context) { + return Scaffold(body: Center(child: Text(widget.text))); + } +} diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart new file mode 100644 index 0000000..489871f --- /dev/null +++ b/lib/core/entity/entities_base.dart @@ -0,0 +1,409 @@ +import 'dart:async'; + +import 'package:flutter/material.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/utils/utils.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +const Map entityTypeTranslations = { + EntityType.TENANT: 'Tenant', + EntityType.TENANT_PROFILE: 'Tenant profile', + EntityType.CUSTOMER: 'Customer', + EntityType.USER: 'User', + EntityType.DASHBOARD: 'Dashboard', + EntityType.ASSET: 'Asset', + EntityType.DEVICE: 'Device', + EntityType.DEVICE_PROFILE: 'Device profile', + EntityType.ALARM: 'Alarm', + EntityType.RULE_CHAIN: 'Rule chain', + EntityType.RULE_NODE: 'Rule node', + EntityType.EDGE: 'Edge', + EntityType.ENTITY_VIEW: 'Entity view', + EntityType.WIDGETS_BUNDLE: 'Widgets bundle', + EntityType.WIDGET_TYPE: 'Widget type', + EntityType.API_USAGE_STATE: 'Api Usage State', + EntityType.TB_RESOURCE: 'Resource', + EntityType.OTA_PACKAGE: 'OTA package' +}; + +typedef EntityTapFunction = Function(T entity); +typedef EntityCardWidgetBuilder = Widget Function( + BuildContext context, T entity); + +class EntityCardSettings { + bool dropShadow; + EntityCardSettings({this.dropShadow = true}); +} + +mixin EntitiesBase on HasTbContext { + final entityDateFormat = DateFormat('yyyy-MM-dd'); + + String get title; + + String get noItemsFoundText; + + Future> fetchEntities(P pageKey); + + Future onRefresh() => Future.value(); + + Widget? buildHeading(BuildContext context) => null; + + Key? getKey(T entity) => null; + + Widget buildEntityListCard(BuildContext context, T entity) { + return Text('${S.of(context).notImplemented}'); + } + + Widget buildEntityListWidgetCard(BuildContext context, T entity) { + return Text('${S.of(context).notImplemented}'); + } + + Widget buildEntityGridCard(BuildContext context, T entity) { + return Text('${S.of(context).notImplemented}'); + } + + double? gridChildAspectRatio() => null; + + EntityCardSettings entityListCardSettings(T entity) => EntityCardSettings(); + + EntityCardSettings entityGridCardSettings(T entity) => EntityCardSettings(); + + void onEntityTap(T entity); +} + +mixin ContactBasedBase on EntitiesBase { + @override + Widget buildEntityListCard(BuildContext context, T contact) { + var address = Utils.contactToShortAddress(contact); + return Container( + padding: 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) + ], + ), + ); + } +} + +abstract class PageKeyController

extends ValueNotifier> { + PageKeyController(P initialPageKey) : super(PageKeyValue(initialPageKey)); + + P nextPageKey(P pageKey); +} + +class PageKeyValue

{ + final P pageKey; + + PageKeyValue(this.pageKey); +} + +class PageLinkController extends PageKeyController { + PageLinkController({int pageSize = 20, String? searchText}) + : super(PageLink( + pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC))); + + @override + PageLink nextPageKey(PageLink pageKey) => pageKey.nextPageLink(); + + onSearchText(String searchText) { + value.pageKey.page = 0; + value.pageKey.textSearch = searchText; + notifyListeners(); + } +} + +class TimePageLinkController extends PageKeyController { + TimePageLinkController({int pageSize = 20, String? searchText}) + : super(TimePageLink( + pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC))); + + @override + TimePageLink nextPageKey(TimePageLink pageKey) => pageKey.nextPageLink(); + + onSearchText(String searchText) { + value.pageKey.page = 0; + value.pageKey.textSearch = searchText; + notifyListeners(); + } +} + +abstract class BaseEntitiesWidget extends TbContextWidget + with EntitiesBase { + final bool searchMode; + final PageKeyController

pageKeyController; + + BaseEntitiesWidget(TbContext tbContext, this.pageKeyController, + {this.searchMode = false}) + : super(tbContext); + + @override + Widget? buildHeading(BuildContext context) => searchMode + ? Text('Search results', + style: TextStyle( + color: Color(0xFFAFAFAF), fontSize: 16, height: 24 / 16)) + : null; +} + +abstract class BaseEntitiesState + extends TbContextState> { + late final PagingController pagingController; + Completer? _refreshCompleter; + bool _dataLoading = false; + bool _scheduleRefresh = false; + bool _reloadData = false; + + BaseEntitiesState(); + + @override + void initState() { + super.initState(); + pagingController = + PagingController(firstPageKey: widget.pageKeyController.value.pageKey); + widget.pageKeyController.addListener(_didChangePageKeyValue); + pagingController.addPageRequestListener((pageKey) { + _fetchPage(pageKey); + }); + } + + @override + void didUpdateWidget(BaseEntitiesWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.pageKeyController != oldWidget.pageKeyController) { + oldWidget.pageKeyController.removeListener(_didChangePageKeyValue); + widget.pageKeyController.addListener(_didChangePageKeyValue); + } + } + + @override + void dispose() { + widget.pageKeyController.removeListener(_didChangePageKeyValue); + pagingController.dispose(); + super.dispose(); + } + + void _didChangePageKeyValue() { + _reloadData = true; + _refresh(); + } + + Future _refresh() { + if (_refreshCompleter == null) { + _refreshCompleter = Completer(); + } + if (_dataLoading) { + _scheduleRefresh = true; + } else { + _refreshPagingController(); + } + return _refreshCompleter!.future; + } + + void _refreshPagingController() { + if (_reloadData) { + pagingController.refresh(); + _reloadData = false; + } else { + _fetchPage(widget.pageKeyController.value.pageKey, refresh: true); + } + } + + Future _fetchPage(P pageKey, {bool refresh = false}) async { + if (mounted) { + _dataLoading = true; + try { + hideNotification(); + final pageData = await widget.fetchEntities(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 = widget.pageKeyController.nextPageKey(pageKey); + pagingController.appendPage(pageData.data, nextPageKey); + } + } catch (error) { + if (mounted) { + pagingController.error = error; + } + } finally { + _dataLoading = false; + if (refresh) { + _refreshCompleter!.complete(); + _refreshCompleter = null; + } + if (_scheduleRefresh) { + _scheduleRefresh = false; + if (mounted) { + _refreshPagingController(); + } + } + } + } + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + 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()], + ), + ) + ]); + } + + Widget newPageProgressIndicatorBuilder(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + top: 16, + bottom: 16, + ), + child: Center(child: RefreshProgressIndicator()), + ); + } + + Widget noItemsFoundIndicatorBuilder(BuildContext context) { + return FirstPageExceptionIndicator( + title: widget.noItemsFoundText, + 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 new file mode 100644 index 0000000..d0808e9 --- /dev/null +++ b/lib/core/entity/entities_grid.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +import 'entities_base.dart'; +import 'entity_grid_card.dart'; + +mixin EntitiesGridStateBase on StatefulWidget { + @override + _EntitiesGridState createState() => _EntitiesGridState(); +} + +class _EntitiesGridState extends BaseEntitiesState { + _EntitiesGridState() : super(); + + @override + Widget pagedViewBuilder(BuildContext context) { + final heading = widget.buildHeading(context); + final gridChildAspectRatio = widget.gridChildAspectRatio() ?? 156 / 150; + + final slivers = []; + if (heading != null) { + slivers.add( + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + sliver: SliverToBoxAdapter(child: heading), + ), + ); + } + + slivers.add( + SliverPadding( + padding: EdgeInsets.all(16), + sliver: PagedSliverGrid( + showNewPageProgressIndicatorAsGridChild: false, + showNewPageErrorIndicatorAsGridChild: false, + showNoMoreItemsIndicatorAsGridChild: false, + pagingController: pagingController, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: gridChildAspectRatio, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + crossAxisCount: 2, + ), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => EntityGridCard( + item, + key: widget.getKey(item), + entityCardWidgetBuilder: widget.buildEntityGridCard, + onEntityTap: widget.onEntityTap, + settings: widget.entityGridCardSettings(item), + ), + firstPageProgressIndicatorBuilder: + firstPageProgressIndicatorBuilder, + newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder, + noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder, + ), + ), + ), + ); + + return CustomScrollView(slivers: slivers); + } +} diff --git a/lib/core/entity/entities_list.dart b/lib/core/entity/entities_list.dart new file mode 100644 index 0000000..0590716 --- /dev/null +++ b/lib/core/entity/entities_list.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; + +import 'entity_list_card.dart'; + +mixin EntitiesListStateBase on StatefulWidget { + @override + _EntitiesListState createState() => _EntitiesListState(); +} + +class _EntitiesListState extends BaseEntitiesState { + _EntitiesListState() : super(); + + @override + Widget pagedViewBuilder(BuildContext context) { + 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: 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)))); + return CustomScrollView(slivers: slivers); + } +} diff --git a/lib/core/entity/entities_list_widget.dart b/lib/core/entity/entities_list_widget.dart new file mode 100644 index 0000000..272f1f3 --- /dev/null +++ b/lib/core/entity/entities_list_widget.dart @@ -0,0 +1,218 @@ +import 'dart:async'; + +import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; +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 'entity_list_card.dart'; + +class EntitiesListWidgetController { + final List<_EntitiesListWidgetState> states = []; + + void _registerEntitiesWidgetState( + _EntitiesListWidgetState entitiesListWidgetState) { + states.add(entitiesListWidgetState); + } + + void _unregisterEntitiesWidgetState( + _EntitiesListWidgetState entitiesListWidgetState) { + states.remove(entitiesListWidgetState); + } + + Future refresh() { + return Future.wait(states.map((state) => state._refresh())); + } + + void dispose() { + states.clear(); + } +} + +abstract class EntitiesListPageLinkWidget + extends EntitiesListWidget { + EntitiesListPageLinkWidget(TbContext tbContext, + {EntitiesListWidgetController? controller}) + : super(tbContext, controller: controller); + + @override + PageKeyController createPageKeyController() => + PageLinkController(pageSize: 5); +} + +abstract class EntitiesListWidget extends TbContextWidget + with EntitiesBase { + final EntitiesListWidgetController? _controller; + + EntitiesListWidget(TbContext tbContext, + {EntitiesListWidgetController? controller}) + : _controller = controller, + super(tbContext); + + @override + _EntitiesListWidgetState createState() => + _EntitiesListWidgetState(_controller); + + PageKeyController

createPageKeyController(); + + void onViewAll(); +} + +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); + } + _refresh(); + } + + @override + void dispose() { + if (_controller != null) { + _controller._unregisterEntitiesWidgetState(this); + } + _pageKeyController.dispose(); + _entitiesStreamController.close(); + super.dispose(); + } + + Future _refresh() { + _entitiesStreamController.add(null); + var entitiesFuture = widget.fetchEntities(_pageKeyController.value.pageKey); + entitiesFuture.then((value) => _entitiesStreamController.add(value)); + return entitiesFuture; + } + + @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( + 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)); + }, + ), + Spacer(), + TextButton( + onPressed: () { + widget.onViewAll(); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero), + child: 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)), + ], + )); + } + + Widget _buildNoEntitiesFound() { + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Color(0xFFDEDEDE), style: BorderStyle.solid, width: 1), + borderRadius: BorderRadius.circular(4)), + child: Center( + child: Text(widget.noItemsFoundText, + style: 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())); + } +} diff --git a/lib/core/entity/entity_details_page.dart b/lib/core/entity/entity_details_page.dart new file mode 100644 index 0000000..a871175 --- /dev/null +++ b/lib/core/entity/entity_details_page.dart @@ -0,0 +1,207 @@ +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 '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); + + final valueTextStyle = + TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14); + + final String _defaultTitle; + final String _entityId; + final String? _subTitle; + final bool _showLoadingIndicator; + 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, + super(tbContext); + + @override + _EntityDetailsPageState createState() => _EntityDetailsPageState(); + + Future fetchEntity(String id); + + ValueNotifier? detailsTitle() { + return null; + } + + Widget buildEntityDetails(BuildContext context, T entity); +} + +class _EntityDetailsPageState + extends TbPageState> { + late Future entityFuture; + late ValueNotifier titleValue; + + @override + void initState() { + super.initState(); + entityFuture = widget.fetchEntity(widget._entityId); + ValueNotifier? detailsTitle = widget.detailsTitle(); + if (detailsTitle == null) { + titleValue = ValueNotifier(widget._defaultTitle); + entityFuture.then((value) { + if (value is HasName) { + titleValue.value = (value as HasName).getName(); + } + }); + } else { + titleValue = detailsTitle; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: widget._hideAppBar + ? null + : TbAppBar( + tbContext, + showLoadingIndicator: widget._showLoadingIndicator, + elevation: widget._appBarElevation, + title: ValueListenableBuilder( + valueListenable: titleValue, + builder: (context, title, _widget) { + 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)) + ]); + }, + ), + ), + body: FutureBuilder( + future: entityFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var entity = snapshot.data; + if (entity != null) { + return widget.buildEntityDetails(context, entity); + } else { + return Center(child: Text('Requested entity does not exists.')); + } + } else { + return Center( + child: TbProgressIndicator( + size: 50.0, + )); + } + }, + ), + ); + } +} + +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); + + @override + Widget buildEntityDetails(BuildContext context, T contact) { + return Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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), + ], + )), + ], + ), + 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), + ])); + } +} diff --git a/lib/core/entity/entity_grid_card.dart b/lib/core/entity/entity_grid_card.dart new file mode 100644 index 0000000..7f6ba74 --- /dev/null +++ b/lib/core/entity/entity_grid_card.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'entities_base.dart'; + +class EntityGridCard extends StatelessWidget { + final T _entity; + final EntityTapFunction? _onEntityTap; + 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, + 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, + ), + onTap: () { + if (_onEntityTap != null) { + _onEntityTap(_entity); + } + }); + } +} diff --git a/lib/core/entity/entity_list_card.dart b/lib/core/entity/entity_list_card.dart new file mode 100644 index 0000000..9ea3a40 --- /dev/null +++ b/lib/core/entity/entity_list_card.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +import 'entities_base.dart'; + +class EntityListCard extends StatelessWidget { + final bool _listWidgetCard; + final T _entity; + 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); + + @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( + borderRadius: BorderRadius.circular(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)), + ], + ), + ), + onTap: () { + if (_onEntityTap != null) { + _onEntityTap(_entity); + } + }); + } +} diff --git a/lib/core/init/init_app.dart b/lib/core/init/init_app.dart new file mode 100644 index 0000000..890f19d --- /dev/null +++ b/lib/core/init/init_app.dart @@ -0,0 +1,29 @@ +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_progress_indicator.dart'; + +class ThingsboardInitApp extends TbPageWidget { + ThingsboardInitApp(TbContext tbContext, {Key? key}) + : super(tbContext, key: key); + + @override + _ThingsboardInitAppState createState() => _ThingsboardInitAppState(); +} + +class _ThingsboardInitAppState extends TbPageState { + @override + void initState() { + super.initState(); + initTbContext(); + } + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + color: Colors.white, + child: TbProgressIndicator(size: 50.0), + ); + } +} diff --git a/lib/core/init/init_routes.dart b/lib/core/init/init_routes.dart new file mode 100644 index 0000000..698e53c --- /dev/null +++ b/lib/core/init/init_routes.dart @@ -0,0 +1,21 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; + +import 'init_app.dart'; + +class InitRoutes extends TbRoutes { + late var initHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return ThingsboardInitApp(tbContext); + }); + + InitRoutes(TbContext tbContext) : super(tbContext); + + @override + void doRegisterRoutes(router) { + router.define("/", handler: initHandler); + } +} diff --git a/lib/core/logger/tb_log_output.dart b/lib/core/logger/tb_log_output.dart new file mode 100644 index 0000000..2eaaf6e --- /dev/null +++ b/lib/core/logger/tb_log_output.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class TbLogOutput extends LogOutput { + @override + void output(OutputEvent event) { + for (final line in event.lines) { + debugPrint(line); + } + } +} diff --git a/lib/core/logger/tb_logger.dart b/lib/core/logger/tb_logger.dart new file mode 100644 index 0000000..a57332e --- /dev/null +++ b/lib/core/logger/tb_logger.dart @@ -0,0 +1,44 @@ +import 'package:logger/logger.dart'; +import 'package:thingsboard_app/core/logger/tb_log_output.dart'; +import 'package:thingsboard_app/core/logger/tb_logs_filter.dart'; + +class TbLogger { + final _logger = Logger( + filter: TbLogsFilter(), + printer: PrefixPrinter( + PrettyPrinter( + methodCount: 0, + errorMethodCount: 8, + lineLength: 200, + colors: false, + printEmojis: true, + printTime: false, + ), + ), + output: TbLogOutput(), + ); + + void trace(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.t(message, error: error, stackTrace: stackTrace); + } + + void debug(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.d(message, error: error, stackTrace: stackTrace); + } + + void info(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.i(message, error: error, stackTrace: stackTrace); + } + + void warn(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.w(message, error: error, stackTrace: stackTrace); + } + + void error(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.e(message, error: error, stackTrace: stackTrace); + } + + void fatal(dynamic message, [dynamic error, StackTrace? stackTrace]) { + _logger.f(message, error: error, stackTrace: stackTrace); + } +} diff --git a/lib/core/logger/tb_logs_filter.dart b/lib/core/logger/tb_logs_filter.dart new file mode 100644 index 0000000..4711a0e --- /dev/null +++ b/lib/core/logger/tb_logs_filter.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class TbLogsFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + if (kReleaseMode) { + return event.level.index >= Level.warning.index; + } else { + return true; + } + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..2403f66 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,22 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + throw UnsupportedError( + 'Firebase have not been configured - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } +} diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart new file mode 100644 index 0000000..32161b6 --- /dev/null +++ b/lib/generated/intl/messages_all.dart @@ -0,0 +1,67 @@ +// 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 new file mode 100644 index 0000000..62ba866 --- /dev/null +++ b/lib/generated/intl/messages_en.dart @@ -0,0 +1,173 @@ +// 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 new file mode 100644 index 0000000..d022ffa --- /dev/null +++ b/lib/generated/intl/messages_zh.dart @@ -0,0 +1,107 @@ +// 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 new file mode 100644 index 0000000..7965265 --- /dev/null +++ b/lib/generated/l10n.dart @@ -0,0 +1,1019 @@ +// 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_en.arb b/lib/l10n/intl_en.arb new file mode 100644 index 0000000..feb5604 --- /dev/null +++ b/lib/l10n/intl_en.arb @@ -0,0 +1,112 @@ +{ + "appTitle": "ThingsBoard", + + "home": "Home", + "alarms": "Alarms", + "devices": "Devices", + "more": "More", + + "customers": "Customers", + "assets": "Assets", + "auditLogs": "Audit Logs", + "logout": "Log Out", + "login": "Log In", + + "logoDefaultValue": "ThingsBoard Logo", + "loginNotification": "Login to your account", + "email": "Email", + "emailRequireText": "Email is required.", + "emailInvalidText": "Invalid email format.", + "username": "username", + "password": "Password", + "passwordRequireText": "Password is required.", + "passwordForgotText": "Forgot Password?", + "passwordReset": "Reset password", + "passwordResetText": "Enter the email associated with your account and we'll send an email with password reset link", + "requestPasswordReset": "Request password reset", + "passwordResetLinkSuccessfullySentNotification": "Password reset link was successfully sent!", + + "OR": "OR", + "No": "No", + "Yes": "Yes", + + "title": "Title", + "country": "Country", + "city": "City", + "stateOrProvince": "State / Province", + "postalCode": "Zip / Postal Code", + "address": "Address", + "address2": "Address 2", + "phone": "Phone", + + "alarmClearTitle": "Clear Alarm", + "alarmClearText": "Are you sure you want to clear Alarm?", + + "alarmAcknowledgeTitle": "Acknowledge Alarm", + "alarmAcknowledgeText": "Are you sure you want to acknowledge Alarm?", + + "assetName": "Asset name", + "type": "Type", + "label": "Label", + "assignedToCustomer": "Assigned to customer", + + "auditLogDetails": "Audit log details", + "entityType": "Entity Type", + "actionData": "Action data", + "failureDetails": "Failure details", + + "allDevices": "All devices", + "active": "Active", + "inactive": "Inactive", + + "systemAdministrator": "System Administrator", + "tenantAdministrator": "Tenant Administrator", + "customer": "Customer", + + "changePassword": "Change Password", + "currentPassword": "currentPassword", + "currentPasswordRequireText": "Current password is required.", + "currentPasswordStar": "Current password *", + "newPassword": "newPassword", + "newPasswordRequireText": "New password is required.", + "newPasswordStar": "New password *", + "newPassword2": "newPassword2", + "newPassword2RequireText": "New password again is required.", + "newPassword2Star": "New password again *", + "passwordErrorNotification": "Entered passwords must be same!", + + "emailStar": "Email *", + "firstName": "firstName", + "firstNameUpper": "First Name", + "lastName": "lastName", + "lastNameUpper": "Last Name", + "profileSuccessNotification": "Profile successfully updated", + "passwordSuccessNotification": "Password successfully changed", + + "notImplemented": "Not implemented!", + + "listIsEmptyText": "The list is currently empty.", + "tryAgain": "Try Again", + + "verifyYourIdentity": "Verify your identity", + "continueText": "Continue", + "resendCode": "Resend code", + "resendCodeWait": "Resend code in {time,plural, =1{1 second}other{{time} seconds}}", + "totpAuthDescription": "Please enter the security code from your authenticator app.", + "smsAuthDescription": "A security code has been sent to your phone at {contact}.", + "emailAuthDescription": "A security code has been sent to your email address at {contact}.", + "backupCodeAuthDescription": "Please enter one of your backup codes.", + "verificationCodeInvalid": "Invalid verification code format", + "toptAuthPlaceholder": "Code", + "smsAuthPlaceholder": "SMS code", + "emailAuthPlaceholder": "Email code", + "backupCodeAuthPlaceholder": "Backup code", + "verificationCodeIncorrect": "Verification code is incorrect", + "verificationCodeManyRequest": "Too many requests check verification code", + "tryAnotherWay": "Try another way", + "selectWayToVerify": "Select a way to verify", + "mfaProviderTopt": "Authenticator app", + "mfaProviderSms": "SMS", + "mfaProviderEmail": "Email", + "mfaProviderBackupCode": "Backup code" +} \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb new file mode 100644 index 0000000..b338e84 --- /dev/null +++ b/lib/l10n/intl_zh.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": "再试一次" +} \ No newline at end of file diff --git a/lib/locator.dart b/lib/locator.dart new file mode 100644 index 0000000..47a7071 --- /dev/null +++ b/lib/locator.dart @@ -0,0 +1,43 @@ +import 'package:get_it/get_it.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/utils/services/_tb_secure_storage.dart'; +import 'package:thingsboard_app/utils/services/endpoint/endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/local_database_service.dart'; + +import 'utils/services/firebase/firebase_service.dart'; + +final getIt = GetIt.instance; + +Future setUpRootDependencies() async { + final secureStorage = createAppStorage() as TbSecureStorage; + await secureStorage.init(); + + getIt + ..registerSingleton( + ThingsboardAppRouter(), + ) + ..registerLazySingleton( + () => TbLogger(), + ) + ..registerLazySingleton( + () => LocalDatabaseService( + storage: secureStorage, + logger: getIt(), + ), + ) + ..registerLazySingleton( + () => EndpointService( + databaseService: getIt(), + ), + ) + ..registerLazySingleton( + () => FirebaseService( + logger: getIt(), + endpointService: getIt(), + ), + ); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..6d8c29d --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,236 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/constants/database_keys.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +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(); +// await FlutterDownloader.initialize(); +// await Permission.storage.request(); + await Hive.initFlutter(); + + await setUpRootDependencies(); + if (UniversalPlatform.isAndroid) { + await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true); + } + + try { + getIt().initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } catch (e) { + log('main::FirebaseService.initializeApp() exception $e', error: e); + } + + try { + final uri = await getInitialUri(); + if (uri != null) { + await getIt().setItem( + DatabaseKeys.initialAppLink, + uri.toString(), + ); + } + } catch (e) { + log('main::getInitialUri() exception $e', error: e); + } + + runApp(ThingsboardApp()); +} + +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; + } + + @override + Widget build(BuildContext context) { + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + systemNavigationBarColor: Colors.white, + statusBarColor: Colors.white, + systemNavigationBarIconBrightness: Brightness.light, + ), + ); + + return MaterialApp( + debugShowCheckedModeBanner: false, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + onGenerateTitle: (BuildContext context) => S.of(context).appTitle, + themeMode: ThemeMode.light, + home: TwoPageView( + controller: _mainPageViewController, + first: MaterialApp( + debugShowCheckedModeBanner: false, + key: mainAppKey, + scaffoldMessengerKey: + getIt().tbContext.messengerKey, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + onGenerateTitle: (BuildContext context) => S.of(context).appTitle, + theme: tbTheme, + themeMode: ThemeMode.light, + darkTheme: tbDarkTheme, + onGenerateRoute: getIt().router.generator, + navigatorObservers: [ + getIt().tbContext.routeObserver, + ], + ), + second: MaterialApp( + debugShowCheckedModeBanner: false, + key: dashboardKey, + // scaffoldMessengerKey: appRouter.tbContext.messengerKey, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + onGenerateTitle: (BuildContext context) => S.of(context).appTitle, + theme: tbTheme, + themeMode: ThemeMode.light, + darkTheme: tbDarkTheme, + home: MainDashboardPage( + getIt().tbContext, + controller: _mainDashboardPageController, + ), + ), + ), + ); + } +} diff --git a/lib/modules/alarm/alarm_routes.dart b/lib/modules/alarm/alarm_routes.dart new file mode 100644 index 0000000..f2851b2 --- /dev/null +++ b/lib/modules/alarm/alarm_routes.dart @@ -0,0 +1,25 @@ +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'; + +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); + + @override + void doRegisterRoutes(router) { + router.define("/alarms", handler: alarmsHandler); + } +} diff --git a/lib/modules/alarm/alarms_base.dart b/lib/modules/alarm/alarms_base.dart new file mode 100644 index 0000000..ea452cf --- /dev/null +++ b/lib/modules/alarm/alarms_base.dart @@ -0,0 +1,322 @@ +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'; + +const Map alarmSeverityColors = { + AlarmSeverity.CRITICAL: Color(0xFFFF0000), + AlarmSeverity.MAJOR: Color(0xFFFFA500), + AlarmSeverity.MINOR: Color(0xFFFFCA3D), + AlarmSeverity.WARNING: Color(0xFFABAB00), + AlarmSeverity.INDETERMINATE: Color(0xFF00FF00), +}; + +const Map alarmSeverityTranslations = { + AlarmSeverity.CRITICAL: 'Critical', + AlarmSeverity.MAJOR: 'Major', + AlarmSeverity.MINOR: 'Minor', + AlarmSeverity.WARNING: 'Warning', + AlarmSeverity.INDETERMINATE: 'Indeterminate', +}; + +const Map alarmStatusTranslations = { + AlarmStatus.ACTIVE_ACK: 'Active Acknowledged', + AlarmStatus.ACTIVE_UNACK: 'Active Unacknowledged', + 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 new file mode 100644 index 0000000..a28947d --- /dev/null +++ b/lib/modules/alarm/alarms_list.dart @@ -0,0 +1,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_client/thingsboard_client.dart'; + +import 'alarms_base.dart'; + +class AlarmsList extends BaseEntitiesWidget + with AlarmsBase, EntitiesListStateBase { + AlarmsList( + TbContext tbContext, PageKeyController pageKeyController, + {searchMode = false}) + : super(tbContext, pageKeyController, searchMode: searchMode); +} diff --git a/lib/modules/alarm/alarms_page.dart b/lib/modules/alarm/alarms_page.dart new file mode 100644 index 0000000..ca0193c --- /dev/null +++ b/lib/modules/alarm/alarms_page.dart @@ -0,0 +1,57 @@ +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/asset/asset_details_page.dart b/lib/modules/asset/asset_details_page.dart new file mode 100644 index 0000000..eca8f78 --- /dev/null +++ b/lib/modules/asset/asset_details_page.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/entity/entity_details_page.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class AssetDetailsPage extends EntityDetailsPage { + AssetDetailsPage(TbContext tbContext, String assetId) + : super(tbContext, + entityId: assetId, + defaultTitle: 'Asset', + subTitle: 'Asset details'); + + @override + Future fetchEntity(String assetId) { + return tbClient.getAssetService().getAssetInfo(assetId); + } + + @override + Widget buildEntityDetails(BuildContext context, AssetInfo asset) { + 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), + ])); + } +} diff --git a/lib/modules/asset/asset_routes.dart b/lib/modules/asset/asset_routes.dart new file mode 100644 index 0000000..6a52743 --- /dev/null +++ b/lib/modules/asset/asset_routes.dart @@ -0,0 +1,28 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/widgets.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/modules/asset/assets_page.dart'; + +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); + }); + + late var assetDetailsHandler = Handler( + 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); + } +} diff --git a/lib/modules/asset/assets_base.dart b/lib/modules/asset/assets_base.dart new file mode 100644 index 0000000..ddf6e38 --- /dev/null +++ b/lib/modules/asset/assets_base.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +mixin AssetsBase on EntitiesBase { + @override + String get title => 'Assets'; + + @override + String get noItemsFoundText => 'No assets found'; + + @override + Future> fetchEntities(PageLink pageLink) { + if (tbClient.isTenantAdmin()) { + return tbClient.getAssetService().getTenantAssetInfos(pageLink); + } else { + return tbClient + .getAssetService() + .getCustomerAssetInfos(tbClient.getAuthUser()!.customerId!, pageLink); + } + } + + @override + void onEntityTap(AssetInfo asset) { + navigateTo('/asset/${asset.id!.id}'); + } + + @override + Widget buildEntityListCard(BuildContext context, AssetInfo asset) { + return _buildCard(context, asset); + } + + @override + Widget buildEntityListWidgetCard(BuildContext context, AssetInfo asset) { + return _buildListWidgetCard(context, asset); + } + + @override + Widget buildEntityGridCard(BuildContext context, AssetInfo asset) { + return Text(asset.name); + } + + Widget _buildCard(context, AssetInfo asset) { + return Row(mainAxisSize: MainAxisSize.max, children: [ + Flexible( + fit: FlexFit.tight, + child: Container( + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + 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))), + ), + 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) + ], + ), + )) + ]); + } + + Widget _buildListWidgetCard(BuildContext context, AssetInfo asset) { + 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: [ + 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)) + ], + )) + ]))) + ]); + } +} diff --git a/lib/modules/asset/assets_list.dart b/lib/modules/asset/assets_list.dart new file mode 100644 index 0000000..9f08002 --- /dev/null +++ b/lib/modules/asset/assets_list.dart @@ -0,0 +1,13 @@ +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 'assets_base.dart'; + +class AssetsList extends BaseEntitiesWidget + with AssetsBase, EntitiesListStateBase { + AssetsList(TbContext tbContext, PageKeyController pageKeyController, + {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 new file mode 100644 index 0000000..d02f95a --- /dev/null +++ b/lib/modules/asset/assets_list_widget.dart @@ -0,0 +1,16 @@ +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'; + +class AssetsListWidget extends EntitiesListPageLinkWidget + with AssetsBase { + AssetsListWidget(TbContext tbContext, + {EntitiesListWidgetController? controller}) + : super(tbContext, controller: controller); + + @override + void onViewAll() { + navigateTo('/assets'); + } +} diff --git a/lib/modules/asset/assets_page.dart b/lib/modules/asset/assets_page.dart new file mode 100644 index 0000000..b691130 --- /dev/null +++ b/lib/modules/asset/assets_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +import 'assets_list.dart'; + +class AssetsPage extends TbPageWidget { + final bool searchMode; + + AssetsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext); + + @override + _AssetsPageState createState() => _AssetsPageState(); +} + +class _AssetsPageState extends TbPageState { + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + var assetsList = AssetsList(tbContext, _pageLinkController, + searchMode: widget.searchMode); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => _pageLinkController.onSearchText(searchText), + ); + } else { + appBar = TbAppBar(tbContext, title: Text(assetsList.title), actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () { + navigateTo('/assets?search=true'); + }, + ) + ]); + } + return Scaffold(appBar: appBar, body: assetsList); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/audit_log/audit_log_details_page.dart b/lib/modules/audit_log/audit_log_details_page.dart new file mode 100644 index 0000000..acaaca7 --- /dev/null +++ b/lib/modules/audit_log/audit_log_details_page.dart @@ -0,0 +1,119 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_app/modules/audit_log/audit_logs_base.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class AuditLogDetailsPage extends TbContextWidget { + final AuditLog auditLog; + + AuditLogDetailsPage(TbContext tbContext, this.auditLog) : super(tbContext); + + @override + _AuditLogDetailsPageState createState() => _AuditLogDetailsPageState(); +} + +class _AuditLogDetailsPageState extends TbContextState { + final labelTextStyle = + TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14); + + final valueTextStyle = + TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14); + + final JsonEncoder encoder = new JsonEncoder.withIndent(' '); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + 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)) + ])), + body: Padding( + padding: 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), + 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!)) + ]), + ), + ); + } + + Widget buildBorderedText(String title, String content) { + return Stack( + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.fromLTRB(16, 18, 48, 18), + margin: EdgeInsets.only(top: 6), + decoration: BoxDecoration( + border: Border.all(color: 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), + ), + ), + ), + 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), + ), + )), + ], + ); + } +} diff --git a/lib/modules/audit_log/audit_logs_base.dart b/lib/modules/audit_log/audit_logs_base.dart new file mode 100644 index 0000000..7455ae4 --- /dev/null +++ b/lib/modules/audit_log/audit_logs_base.dart @@ -0,0 +1,241 @@ +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/modules/audit_log/audit_log_details_page.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +const Map actionTypeTranslations = { + ActionType.ADDED: 'Added', + ActionType.DELETED: 'Deleted', + ActionType.UPDATED: 'Updated', + ActionType.ATTRIBUTES_UPDATED: 'Attributes Updated', + ActionType.ATTRIBUTES_DELETED: 'Attributes Deleted', + ActionType.RPC_CALL: 'RPC Call', + ActionType.CREDENTIALS_UPDATED: 'Credentials Updated', + ActionType.ASSIGNED_TO_CUSTOMER: 'Assigned to Customer', + ActionType.UNASSIGNED_FROM_CUSTOMER: 'Unassigned from Customer', + ActionType.ACTIVATED: 'Activated', + ActionType.SUSPENDED: 'Suspended', + ActionType.CREDENTIALS_READ: 'Credentials read', + ActionType.ATTRIBUTES_READ: 'Attributes read', + ActionType.RELATION_ADD_OR_UPDATE: 'Relation updated', + ActionType.RELATION_DELETED: 'Relation deleted', + ActionType.RELATIONS_DELETED: 'All relation deleted', + ActionType.ALARM_ACK: 'Acknowledged', + ActionType.ALARM_CLEAR: 'Cleared', + ActionType.ALARM_DELETE: 'Alarm Deleted', + ActionType.ALARM_ASSIGNED: 'Alarm Assigned', + ActionType.ALARM_UNASSIGNED: 'Alarm Unassigned', + ActionType.LOGIN: 'Login', + ActionType.LOGOUT: 'Logout', + ActionType.LOCKOUT: 'Lockout', + ActionType.ASSIGNED_FROM_TENANT: 'Assigned from Tenant', + ActionType.ASSIGNED_TO_TENANT: 'Assigned to Tenant', + ActionType.PROVISION_SUCCESS: 'Device provisioned', + ActionType.PROVISION_FAILURE: 'Device provisioning was failed', + ActionType.TIMESERIES_UPDATED: 'Telemetry updated', + ActionType.TIMESERIES_DELETED: 'Telemetry deleted', + ActionType.ASSIGNED_TO_EDGE: 'Assigned to Edge', + ActionType.UNASSIGNED_FROM_EDGE: 'Unassigned from Edge', + ActionType.ADDED_COMMENT: 'Added Comment', + ActionType.UPDATED_COMMENT: 'Updated Comment', + ActionType.DELETED_COMMENT: 'Deleted Comment', + ActionType.SMS_SENT: 'SMS Sent' +}; + +const Map actionStatusTranslations = { + ActionStatus.SUCCESS: 'Success', + ActionStatus.FAILURE: 'Failure' +}; + +mixin AuditLogsBase on EntitiesBase { + @override + String get title => 'Audit Logs'; + + @override + String get noItemsFoundText => 'No audit logs found'; + + @override + Future> fetchEntities(TimePageLink pageLink) { + return tbClient.getAuditLogService().getAuditLogs(pageLink); + } + + @override + void onEntityTap(AuditLog auditLog) {} + + @override + Widget buildEntityListCard(BuildContext context, AuditLog auditLog) { + return _buildEntityListCard(context, auditLog); + } + + Widget _buildEntityListCard(BuildContext context, AuditLog auditLog) { + return AuditLogCard(tbContext, auditLog: auditLog); + } +} + +class AuditLogCard extends TbContextWidget { + final AuditLog auditLog; + + AuditLogCard(TbContext tbContext, {required this.auditLog}) + : super(tbContext); + + @override + _AuditLogCardState 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); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill( + 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( + 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), + 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) + ])) + ]) + ], + ); + } + + _auditLogDetails(AuditLog auditLog) { + tbContext + .showFullScreenDialog(new AuditLogDetailsPage(tbContext, auditLog)); + } +} diff --git a/lib/modules/audit_log/audit_logs_list.dart b/lib/modules/audit_log/audit_logs_list.dart new file mode 100644 index 0000000..c79abf6 --- /dev/null +++ b/lib/modules/audit_log/audit_logs_list.dart @@ -0,0 +1,13 @@ +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'; + +class AuditLogsList extends BaseEntitiesWidget + with AuditLogsBase, EntitiesListStateBase { + AuditLogsList( + TbContext tbContext, PageKeyController pageKeyController, + {searchMode = false}) + : 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 new file mode 100644 index 0000000..e415557 --- /dev/null +++ b/lib/modules/audit_log/audit_logs_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/modules/audit_log/audit_logs_list.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class AuditLogsPage extends TbPageWidget { + final bool searchMode; + + AuditLogsPage(TbContext tbContext, {this.searchMode = false}) + : super(tbContext); + + @override + _AuditLogsPageState createState() => _AuditLogsPageState(); +} + +class _AuditLogsPageState extends TbPageState { + final TimePageLinkController _timePageLinkController = + TimePageLinkController(); + + @override + Widget build(BuildContext context) { + var auditLogsList = AuditLogsList(tbContext, _timePageLinkController, + searchMode: widget.searchMode); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => + _timePageLinkController.onSearchText(searchText), + ); + } else { + appBar = TbAppBar(tbContext, title: Text(auditLogsList.title), actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () { + navigateTo('/auditLogs?search=true'); + }, + ) + ]); + } + return Scaffold(appBar: appBar, body: auditLogsList); + } + + @override + void dispose() { + _timePageLinkController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/audit_log/audit_logs_routes.dart b/lib/modules/audit_log/audit_logs_routes.dart new file mode 100644 index 0000000..f753c44 --- /dev/null +++ b/lib/modules/audit_log/audit_logs_routes.dart @@ -0,0 +1,20 @@ +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/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); + }); + + AuditLogsRoutes(TbContext tbContext) : super(tbContext); + + @override + void doRegisterRoutes(router) { + router.define("/auditLogs", handler: auditLogsHandler); + } +} diff --git a/lib/modules/customer/customer_details_page.dart b/lib/modules/customer/customer_details_page.dart new file mode 100644 index 0000000..07d0572 --- /dev/null +++ b/lib/modules/customer/customer_details_page.dart @@ -0,0 +1,16 @@ +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'; + +class CustomerDetailsPage extends ContactBasedDetailsPage { + CustomerDetailsPage(TbContext tbContext, String customerId) + : super(tbContext, + entityId: customerId, + defaultTitle: 'Customer', + subTitle: 'Customer details'); + + @override + Future fetchEntity(String customerId) { + return tbClient.getCustomerService().getCustomer(customerId); + } +} diff --git a/lib/modules/customer/customer_routes.dart b/lib/modules/customer/customer_routes.dart new file mode 100644 index 0000000..25311ec --- /dev/null +++ b/lib/modules/customer/customer_routes.dart @@ -0,0 +1,27 @@ +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 var 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); + } +} diff --git a/lib/modules/customer/customers_base.dart b/lib/modules/customer/customers_base.dart new file mode 100644 index 0000000..3369920 --- /dev/null +++ b/lib/modules/customer/customers_base.dart @@ -0,0 +1,20 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +mixin CustomersBase on EntitiesBase { + @override + String get title => 'Customers'; + + @override + String get noItemsFoundText => 'No customers found'; + + @override + Future> fetchEntities(PageLink pageLink) { + return tbClient.getCustomerService().getCustomers(pageLink); + } + + @override + void onEntityTap(Customer customer) { + navigateTo('/customer/${customer.id!.id}'); + } +} diff --git a/lib/modules/customer/customers_list.dart b/lib/modules/customer/customers_list.dart new file mode 100644 index 0000000..836a420 --- /dev/null +++ b/lib/modules/customer/customers_list.dart @@ -0,0 +1,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_client/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); +} diff --git a/lib/modules/customer/customers_page.dart b/lib/modules/customer/customers_page.dart new file mode 100644 index 0000000..15e0ebd --- /dev/null +++ b/lib/modules/customer/customers_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/modules/customer/customers_list.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class CustomersPage extends TbPageWidget { + final bool searchMode; + + CustomersPage(TbContext tbContext, {this.searchMode = false}) + : super(tbContext); + + @override + _CustomersPageState createState() => _CustomersPageState(); +} + +class _CustomersPageState extends TbPageState { + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + var customersList = CustomersList(tbContext, _pageLinkController, + searchMode: widget.searchMode); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => _pageLinkController.onSearchText(searchText), + ); + } else { + appBar = TbAppBar(tbContext, title: Text(customersList.title), actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () { + navigateTo('/customers?search=true'); + }, + ) + ]); + } + return Scaffold(appBar: appBar, body: customersList); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart new file mode 100644 index 0000000..44ed9f7 --- /dev/null +++ b/lib/modules/dashboard/dashboard.dart @@ -0,0 +1,502 @@ +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) { + var controller = await _controller.future; + await controller.postWebMessage( + message: WebMessage(data: jsonEncode(windowMessage)), + targetOrigin: Uri.parse('*')); + } + } + } + } + + Future _goBack() async { + if (!UniversalPlatform.isWeb) { + if (_dashboardController.rightLayoutOpened.value) { + await _toggleRightLayout(); + return false; + } + var controller = await _controller.future; + if (await controller.canGoBack()) { + await controller.goBack(); + return false; + } + } + return true; + } + + @override + void dispose() { + tbContext.isAuthenticatedListenable.removeListener(_onAuthenticated); + readyState.dispose(); + dashboardLoading.dispose(); + _dashboardController.dispose(); + super.dispose(); + } + + Future _activateDashboard() async { + if (!dashboardActive.value) { + dashboardActive.value = true; + } + } + + Future _deactivateDashboard() async { + if (dashboardActive.value) { + dashboardActive.value = false; + } + } + + Future _openDashboard(String dashboardId, + {String? state, bool? hideToolbar, bool fullscreen = false}) async { + dashboardLoading.value = true; + InAppWebViewController? controller; + if (!UniversalPlatform.isWeb) { + controller = await _controller.future; + } + var windowMessage = { + 'type': 'openDashboardMessage', + 'data': {'dashboardId': dashboardId} + }; + if (state != null) { + windowMessage['data']['state'] = state; + } + if (widget._home == true) { + windowMessage['data']['embedded'] = true; + } + if (hideToolbar == true) { + windowMessage['data']['hideToolbar'] = true; + } + var webMessage = WebMessage(data: jsonEncode(windowMessage)); + if (!UniversalPlatform.isWeb) { + await controller! + .postWebMessage(message: webMessage, targetOrigin: Uri.parse('*')); + } + } + + Future _toggleRightLayout() async { + var controller = await _controller.future; + var windowMessage = {'type': 'toggleDashboardLayout'}; + var webMessage = WebMessage(data: jsonEncode(windowMessage)); + await 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 new file mode 100644 index 0000000..b370736 --- /dev/null +++ b/lib/modules/dashboard/dashboard_page.dart @@ -0,0 +1,62 @@ +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 new file mode 100644 index 0000000..71e5a9b --- /dev/null +++ b/lib/modules/dashboard/dashboard_routes.dart @@ -0,0 +1,42 @@ +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'; + +class DashboardRoutes extends TbRoutes { + late var dashboardsHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return DashboardsPage(tbContext); + }); + + late var dashboardDetailsHandler = Handler( + handlerFunc: (BuildContext? context, Map> params) { + var fullscreen = params['fullscreen']?.first == 'true'; + var dashboardTitle = params['title']?.first; + var state = params['state']?.first; + return DashboardPage(tbContext, + dashboardId: params["id"]![0], + fullscreen: fullscreen, + dashboardTitle: dashboardTitle, + state: state); + }); + + late var fullscreenDashboardHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return FullscreenDashboardPage(tbContext, params["id"]![0]); + }); + + DashboardRoutes(TbContext tbContext) : super(tbContext); + + @override + void doRegisterRoutes(router) { + router.define("/dashboards", handler: dashboardsHandler); + router.define("/dashboard/:id", handler: dashboardDetailsHandler); + router.define("/fullscreenDashboard/:id", + handler: fullscreenDashboardHandler); + } +} diff --git a/lib/modules/dashboard/dashboards_base.dart b/lib/modules/dashboard/dashboards_base.dart new file mode 100644 index 0000000..3328bc5 --- /dev/null +++ b/lib/modules/dashboard/dashboards_base.dart @@ -0,0 +1,201 @@ +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 new file mode 100644 index 0000000..fa255d6 --- /dev/null +++ b/lib/modules/dashboard/dashboards_grid.dart @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..159ff0c --- /dev/null +++ b/lib/modules/dashboard/dashboards_list.dart @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..a62f59f --- /dev/null +++ b/lib/modules/dashboard/dashboards_list_widget.dart @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..9a1a5e6 --- /dev/null +++ b/lib/modules/dashboard/dashboards_page.dart @@ -0,0 +1,27 @@ +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/fullscreen_dashboard_page.dart b/lib/modules/dashboard/fullscreen_dashboard_page.dart new file mode 100644 index 0000000..8bb0455 --- /dev/null +++ b/lib/modules/dashboard/fullscreen_dashboard_page.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/dashboard/dashboard.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class FullscreenDashboardPage extends TbPageWidget { + final String fullscreenDashboardId; + final String? _dashboardTitle; + + FullscreenDashboardPage(TbContext tbContext, this.fullscreenDashboardId, + {String? dashboardTitle}) + : _dashboardTitle = dashboardTitle, + super(tbContext); + + @override + _FullscreenDashboardPageState createState() => + _FullscreenDashboardPageState(); +} + +class _FullscreenDashboardPageState + extends TbPageState { + late ValueNotifier dashboardTitleValue; + final ValueNotifier showBackValue = ValueNotifier(false); + + @override + void initState() { + super.initState(); + dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard'); + } + + @override + void dispose() { + super.dispose(); + } + + _onCanGoBack(bool canGoBack) { + showBackValue.value = canGoBack; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: ValueListenableBuilder( + valueListenable: showBackValue, + builder: (context, canGoBack, widget) { + return TbAppBar(tbContext, + leading: canGoBack + ? BackButton(onPressed: () { + maybePop(); + }) + : null, + showLoadingIndicator: false, + elevation: 1, + shadowColor: Colors.transparent, + title: ValueListenableBuilder( + valueListenable: dashboardTitleValue, + builder: (context, title, widget) { + return FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text(title)); + }, + ), + actions: [ + IconButton( + icon: Icon(Icons.settings), + onPressed: () => navigateTo('/profile?fullscreen=true')) + ]); + }), + ), + body: ValueListenableBuilder( + valueListenable: getIt().listenEndpointChanges, + builder: (context, _, __) => Dashboard( + tbContext, + key: UniqueKey(), + titleCallback: (title) { + dashboardTitleValue.value = title; + }, + controllerCallback: (controller) { + controller.canGoBack.addListener(() { + _onCanGoBack(controller.canGoBack.value); + }); + controller.openDashboard( + widget.fullscreenDashboardId, + fullscreen: true, + ); + }, + ), + ), + ); + } +} diff --git a/lib/modules/dashboard/main_dashboard_page.dart b/lib/modules/dashboard/main_dashboard_page.dart new file mode 100644 index 0000000..5468c1b --- /dev/null +++ b/lib/modules/dashboard/main_dashboard_page.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/dashboard/dashboard.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class MainDashboardPageController { + DashboardController? _dashboardController; + _MainDashboardPageState? _mainDashboardPageState; + + _setMainDashboardPageState(_MainDashboardPageState state) { + _mainDashboardPageState = state; + } + + _setDashboardController(DashboardController controller) { + _dashboardController = controller; + } + + Future dashboardGoBack() { + if (_dashboardController != null) { + return _dashboardController!.goBack(); + } else { + return Future.value(true); + } + } + + Future openDashboard(String dashboardId, + {String? dashboardTitle, String? state, bool? hideToolbar}) async { + if (dashboardTitle != null) { + _mainDashboardPageState?._updateTitle(dashboardTitle); + } + await _dashboardController?.openDashboard(dashboardId, + state: state, hideToolbar: hideToolbar); + } + + Future activateDashboard() async { + await _dashboardController?.activateDashboard(); + } + + Future deactivateDashboard() async { + await _dashboardController?.deactivateDashboard(); + } +} + +class MainDashboardPage extends TbContextWidget { + final String? _dashboardTitle; + final MainDashboardPageController? _controller; + + MainDashboardPage(TbContext tbContext, + {MainDashboardPageController? controller, String? dashboardTitle}) + : _controller = controller, + _dashboardTitle = dashboardTitle, + super(tbContext); + + @override + _MainDashboardPageState createState() => _MainDashboardPageState(); +} + +class _MainDashboardPageState extends TbContextState + with TickerProviderStateMixin { + late ValueNotifier dashboardTitleValue; + final ValueNotifier hasRightLayout = ValueNotifier(false); + DashboardController? _dashboardController; + 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; + } + + @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: ValueListenableBuilder( + valueListenable: getIt().listenEndpointChanges, + builder: (context, value, _) { + return Dashboard( + tbContext, + key: UniqueKey(), + activeByDefault: false, + titleCallback: (title) { + dashboardTitleValue.value = title; + }, + controllerCallback: (controller) { + _dashboardController = controller; + if (widget._controller != null) { + widget._controller!._setDashboardController(controller); + controller.hasRightLayout.addListener(() { + hasRightLayout.value = controller.hasRightLayout.value; + }); + controller.rightLayoutOpened.addListener(() { + if (controller.rightLayoutOpened.value) { + rightLayoutMenuController.forward(); + } else { + rightLayoutMenuController.reverse(); + } + }); + } + }, + ); + }, + ), + ); + } +} diff --git a/lib/modules/device/device_details_page.dart b/lib/modules/device/device_details_page.dart new file mode 100644 index 0000000..c2c9655 --- /dev/null +++ b/lib/modules/device/device_details_page.dart @@ -0,0 +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'; + +class DeviceDetailsPage extends EntityDetailsPage { + DeviceDetailsPage(TbContext tbContext, String deviceId) + : super(tbContext, entityId: deviceId, defaultTitle: 'Device'); + + @override + Future fetchEntity(String deviceId) { + return tbClient.getDeviceService().getDeviceInfo(deviceId); + } + + @override + Widget buildEntityDetails(BuildContext context, DeviceInfo device) { + return ListTile( + title: Text('${device.name}'), + subtitle: Text('${device.type}'), + ); + } +} diff --git a/lib/modules/device/device_profiles_base.dart b/lib/modules/device/device_profiles_base.dart new file mode 100644 index 0000000..b7b2850 --- /dev/null +++ b/lib/modules/device/device_profiles_base.dart @@ -0,0 +1,462 @@ +import 'dart:async'; + +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/generated/l10n.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(); + + @override + String get title => 'Devices'; + + @override + String get noItemsFoundText => 'No devices found'; + + @override + Future> fetchEntities(PageLink pageLink) { + return DeviceProfileCache.getDeviceProfileInfos(tbClient, pageLink); + } + + @override + void onEntityTap(DeviceProfileInfo deviceProfile) { + navigateTo('/deviceList?deviceType=${deviceProfile.name}'); + } + + @override + Future onRefresh() { + if (refreshDeviceCounts.onRefresh != null) { + return refreshDeviceCounts.onRefresh!(); + } else { + return Future.value(); + } + } + + @override + Widget? buildHeading(BuildContext context) { + return AllDevicesCard(tbContext, refreshDeviceCounts); + } + + @override + Widget buildEntityGridCard( + BuildContext context, DeviceProfileInfo deviceProfile) { + return DeviceProfileCard(tbContext, deviceProfile); + } + + @override + double? gridChildAspectRatio() { + return 156 / 200; + } +} + +class RefreshDeviceCounts { + Future Function()? onRefresh; +} + +class AllDevicesCard extends TbContextWidget { + final RefreshDeviceCounts refreshDeviceCounts; + + AllDevicesCard(TbContext tbContext, this.refreshDeviceCounts) + : super(tbContext); + + @override + _AllDevicesCardState createState() => _AllDevicesCardState(); +} + +class _AllDevicesCardState extends TbContextState { + final StreamController _activeDevicesCount = + StreamController.broadcast(); + final StreamController _inactiveDevicesCount = + StreamController.broadcast(); + + @override + void initState() { + super.initState(); + widget.refreshDeviceCounts.onRefresh = _countDevices; + _countDevices(); + } + + @override + void didUpdateWidget(AllDevicesCard oldWidget) { + super.didUpdateWidget(oldWidget); + widget.refreshDeviceCounts.onRefresh = _countDevices; + } + + @override + void dispose() { + _activeDevicesCount.close(); + _inactiveDevicesCount.close(); + widget.refreshDeviceCounts.onRefresh = null; + super.dispose(); + } + + Future _countDevices() { + _activeDevicesCount.add(null); + _inactiveDevicesCount.add(null); + Future activeDevicesCount = + EntityQueryApi.countDevices(tbClient, active: true); + Future inactiveDevicesCount = + EntityQueryApi.countDevices(tbClient, active: false); + Future> countsFuture = + Future.wait([activeDevicesCount, inactiveDevicesCount]); + countsFuture.then((counts) { + if (this.mounted) { + _activeDevicesCount.add(counts[0]); + _inactiveDevicesCount.add(counts[1]); + } + }); + return countsFuture; + } + + @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: 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), + ), + 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'); + }), + ), + // 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: 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'); + }); + } +} + +class DeviceProfileCard extends TbContextWidget { + final DeviceProfileInfo deviceProfile; + + DeviceProfileCard(TbContext tbContext, this.deviceProfile) : super(tbContext); + + @override + _DeviceProfileCardState createState() => _DeviceProfileCardState(); +} + +class _DeviceProfileCardState extends TbContextState { + late Future activeDevicesCount; + late Future inactiveDevicesCount; + + @override + void initState() { + super.initState(); + _countDevices(); + } + + @override + void didUpdateWidget(DeviceProfileCard oldWidget) { + super.didUpdateWidget(oldWidget); + _countDevices(); + } + + _countDevices() { + activeDevicesCount = EntityQueryApi.countDevices(tbClient, + deviceType: widget.deviceProfile.name, active: true); + inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, + deviceType: widget.deviceProfile.name, active: false); + } + + @override + Widget build(BuildContext context) { + var entity = widget.deviceProfile; + var hasImage = entity.image != null; + Widget image; + BoxFit imageFit; + double padding; + if (hasImage) { + image = Utils.imageFromTbImage(context, tbClient, entity.image!); + imageFit = BoxFit.contain; + padding = 8; + } else { + 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: [ + Expanded( + 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)))); + } + }, + ), + onTap: () { + navigateTo('/deviceList?active=true&deviceType=${entity.name}'); + }), + 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}'); + }) + ])); + } +} + +Widget _buildDeviceCount(BuildContext context, bool active, int count) { + Color color = active ? Color(0xFF008A00) : Color(0xFFAFAFAF); + return Padding( + padding: EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Stack( + children: [ + Icon(Icons.devices_other, size: 16, color: color), + if (!active) + CustomPaint( + size: Size.square(16), + painter: StrikeThroughPainter(color: color, offset: 2), + ) + ], + ), + 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)) + ], + ), + Icon(Icons.chevron_right, size: 16, color: Color(0xFFACACAC)) + ], + ), + ); +} + +class StrikeThroughPainter extends CustomPainter { + final Color color; + final double offset; + + StrikeThroughPainter({required this.color, this.offset = 0}); + + @override + 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); + paint.color = Colors.white; + canvas.drawLine(Offset(2, 0), Offset(size.width + 2, size.height), paint); + } + + @override + bool shouldRepaint(covariant StrikeThroughPainter oldDelegate) { + return color != oldDelegate.color; + } +} diff --git a/lib/modules/device/device_profiles_grid.dart b/lib/modules/device/device_profiles_grid.dart new file mode 100644 index 0000000..2953839 --- /dev/null +++ b/lib/modules/device/device_profiles_grid.dart @@ -0,0 +1,13 @@ +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 'device_profiles_base.dart'; + +class DeviceProfilesGrid extends BaseEntitiesWidget + with DeviceProfilesBase, EntitiesGridStateBase { + DeviceProfilesGrid( + TbContext tbContext, PageKeyController pageKeyController) + : super(tbContext, pageKeyController); +} diff --git a/lib/modules/device/device_routes.dart b/lib/modules/device/device_routes.dart new file mode 100644 index 0000000..2495e60 --- /dev/null +++ b/lib/modules/device/device_routes.dart @@ -0,0 +1,46 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/widgets.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/modules/device/devices_page.dart'; +import 'package:thingsboard_app/modules/main/main_page.dart'; + +import 'device_details_page.dart'; +import 'devices_list_page.dart'; + +class DeviceRoutes extends TbRoutes { + late var 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 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 var 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); + } +} diff --git a/lib/modules/device/devices_base.dart b/lib/modules/device/devices_base.dart new file mode 100644 index 0000000..57893e7 --- /dev/null +++ b/lib/modules/device/devices_base.dart @@ -0,0 +1,399 @@ +import 'dart:core'; + +import 'package:flutter/material.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/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 + String get title => 'Devices'; + + @override + String get noItemsFoundText => 'No devices found'; + + @override + Future> fetchEntities(EntityDataQuery dataQuery) { + return tbClient.getEntityQueryService().findEntityDataByQuery(dataQuery); + } + + @override + void onEntityTap(EntityData device) async { + var profile = await DeviceProfileCache.getDeviceProfileInfo( + 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); + } else { + if (tbClient.isTenantAdmin()) { + showWarnNotification( + 'Mobile dashboard should be configured in device profile!'); + } + } + } + + @override + Widget buildEntityListCard(BuildContext context, EntityData device) { + return _buildEntityListCard(context, device, false); + } + + @override + Widget buildEntityListWidgetCard(BuildContext context, EntityData device) { + return _buildEntityListCard(context, device, true); + } + + @override + Widget buildEntityGridCard(BuildContext context, EntityData device) { + return Text(device.field('name')!); + } + + bool displayCardImage(bool listWidgetCard) => listWidgetCard; + + Widget _buildEntityListCard( + 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( + pageSize: pageSize, + searchText: searchText, + deviceType: deviceType, + active: active)); + + @override + EntityDataQuery nextPageKey(EntityDataQuery deviceQuery) => + deviceQuery.next(); + + onSearchText(String searchText) { + value.pageKey.pageLink.page = 0; + value.pageKey.pageLink.textSearch = searchText; + notifyListeners(); + } +} + +class DeviceCard extends TbContextWidget { + final EntityData device; + final bool listWidgetCard; + final bool displayImage; + + DeviceCard(TbContext tbContext, + {required this.device, + this.listWidgetCard = false, + this.displayImage = false}) + : super(tbContext); + + @override + _DeviceCardState createState() => _DeviceCardState(); +} + +class _DeviceCardState extends TbContextState { + final entityDateFormat = DateFormat('yyyy-MM-dd'); + + late Future deviceProfileFuture; + + @override + void initState() { + super.initState(); + if (widget.displayImage || !widget.listWidgetCard) { + deviceProfileFuture = DeviceProfileCache.getDeviceProfileInfo( + tbClient, widget.device.field('type')!, widget.device.entityId.id!); + } + } + + @override + void didUpdateWidget(DeviceCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.displayImage || !widget.listWidgetCard) { + var oldDevice = oldWidget.device; + var device = widget.device; + if (oldDevice.field('type')! != device.field('type')!) { + deviceProfileFuture = DeviceProfileCache.getDeviceProfileInfo( + tbClient, widget.device.field('type')!, widget.device.entityId.id!); + } + } + } + + @override + Widget build(BuildContext context) { + if (widget.listWidgetCard) { + return buildListWidgetCard(context); + } else { + return buildCard(context); + } + } + + Widget buildCard(BuildContext context) { + 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( + future: deviceProfileFuture, + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + var profile = snapshot.data!; + bool hasDashboard = profile.defaultDashboardId != null; + 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 Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 20), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + 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) + ], + )) + ]); + } else { + return Container( + 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( + // 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( + ThingsboardImage.deviceProfilePlaceholder, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, BlendMode.overlay), + semanticsLabel: 'Device'); + imageFit = BoxFit.cover; + } + return ClipRRect( + borderRadius: + BorderRadius.horizontal(left: Radius.circular(4)), + child: Stack( + children: [ + Positioned.fill( + child: FittedBox( + fit: imageFit, + child: image, + )) + ], + )); + } else { + return Center( + child: RefreshProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Theme.of(tbContext.currentState!.context) + .colorScheme + .primary))); + } + }, + ), + ), + 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)), + ]) + ], + ))) + ]); + } +} diff --git a/lib/modules/device/devices_list.dart b/lib/modules/device/devices_list.dart new file mode 100644 index 0000000..089d7cf --- /dev/null +++ b/lib/modules/device/devices_list.dart @@ -0,0 +1,18 @@ +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'; + +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); + + @override + bool displayCardImage(bool listWidgetCard) => displayDeviceImage; +} diff --git a/lib/modules/device/devices_list_page.dart b/lib/modules/device/devices_list_page.dart new file mode 100644 index 0000000..27a3a3b --- /dev/null +++ b/lib/modules/device/devices_list_page.dart @@ -0,0 +1,99 @@ +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/device/devices_base.dart'; +import 'package:thingsboard_app/modules/device/devices_list.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class DevicesListPage extends TbPageWidget { + final String? deviceType; + final bool? active; + final bool searchMode; + + DevicesListPage(TbContext tbContext, + {this.deviceType, this.active, this.searchMode = false}) + : super(tbContext); + + @override + _DevicesListPageState createState() => _DevicesListPageState(); +} + +class _DevicesListPageState extends TbPageState { + late final DeviceQueryController _deviceQueryController; + + @override + void initState() { + super.initState(); + _deviceQueryController = DeviceQueryController( + deviceType: widget.deviceType, active: widget.active); + } + + @override + Widget build(BuildContext context) { + var devicesList = DevicesList(tbContext, _deviceQueryController, + searchMode: widget.searchMode, + displayDeviceImage: widget.deviceType == null); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => + _deviceQueryController.onSearchText(searchText), + ); + } else { + String titleText = widget.deviceType != null + ? widget.deviceType! + : '${S.of(context).allDevices}'; + String? subTitleText; + if (widget.active != null) { + subTitleText = widget.active == true + ? '${S.of(context).active}' + : '${S.of(context).inactive}'; + } + Column title = + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(titleText, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: subTitleText != null ? 16 : 20, + height: subTitleText != null ? 20 / 16 : 24 / 20)), + if (subTitleText != null) + Text(subTitleText, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .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('&')}'); + }, + ) + ]); + } + return Scaffold(appBar: appBar, body: devicesList); + } + + @override + void dispose() { + _deviceQueryController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/device/devices_list_widget.dart b/lib/modules/device/devices_list_widget.dart new file mode 100644 index 0000000..035fe30 --- /dev/null +++ b/lib/modules/device/devices_list_widget.dart @@ -0,0 +1,21 @@ +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'; + +class DevicesListWidget extends EntitiesListWidget + with DevicesBase { + DevicesListWidget(TbContext tbContext, + {EntitiesListWidgetController? controller}) + : super(tbContext, controller: controller); + + @override + void onViewAll() { + navigateTo('/devices'); + } + + @override + PageKeyController createPageKeyController() => + DeviceQueryController(pageSize: 5); +} diff --git a/lib/modules/device/devices_main_page.dart b/lib/modules/device/devices_main_page.dart new file mode 100644 index 0000000..e6c4e80 --- /dev/null +++ b/lib/modules/device/devices_main_page.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/modules/device/device_profiles_grid.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class DevicesMainPage extends TbContextWidget { + DevicesMainPage(TbContext tbContext) : super(tbContext); + + @override + _DevicesMainPageState createState() => _DevicesMainPageState(); +} + +class _DevicesMainPageState extends TbContextState + with AutomaticKeepAliveClientMixin { + final PageLinkController _pageLinkController = PageLinkController(); + + @override + bool get wantKeepAlive { + return true; + } + + @override + Widget build(BuildContext context) { + super.build(context); + var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController); + return Scaffold( + appBar: TbAppBar(tbContext, title: Text(deviceProfilesList.title)), + body: deviceProfilesList); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/device/devices_page.dart b/lib/modules/device/devices_page.dart new file mode 100644 index 0000000..bb7e295 --- /dev/null +++ b/lib/modules/device/devices_page.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/modules/device/device_profiles_grid.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class DevicesPage extends TbPageWidget { + DevicesPage(TbContext tbContext) : super(tbContext); + + @override + _DevicesPageState createState() => _DevicesPageState(); +} + +class _DevicesPageState extends TbPageState { + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController); + return Scaffold( + appBar: TbAppBar(tbContext, title: Text(deviceProfilesList.title)), + body: deviceProfilesList); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart new file mode 100644 index 0000000..bc567d4 --- /dev/null +++ b/lib/modules/home/home_page.dart @@ -0,0 +1,109 @@ +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/tenant/tenants_widget.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class HomePage extends TbContextWidget { + HomePage(TbContext tbContext) : super(tbContext); + + @override + _HomePageState 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); + } + }), + ); + } + + Widget _buildDashboardHome( + BuildContext context, HomeDashboardInfo dashboard) { + return HomeDashboard(tbContext, dashboard); + } + + Widget _buildDefaultHome(BuildContext context) { + if (tbClient.isSystemAdmin()) { + return _buildSysAdminHome(context); + } else { + return DashboardsGridWidget(tbContext); + } + } + + Widget _buildSysAdminHome(BuildContext context) { + 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 new file mode 100644 index 0000000..6dc2cb3 --- /dev/null +++ b/lib/modules/home/home_routes.dart @@ -0,0 +1,19 @@ +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/main/main_page.dart'; + +class HomeRoutes extends TbRoutes { + late var homeHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return MainPage(tbContext, path: '/home'); + }); + + HomeRoutes(TbContext tbContext) : super(tbContext); + + @override + void doRegisterRoutes(router) { + router.define("/home", handler: homeHandler); + } +} diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart new file mode 100644 index 0000000..03999d8 --- /dev/null +++ b/lib/modules/main/main_page.dart @@ -0,0 +1,212 @@ +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; + } + } + } +} + +class MainPage extends TbPageWidget { + final String _path; + + MainPage(TbContext tbContext, {required String path}) + : _path = path, + super(tbContext); + + @override + _MainPageState createState() => _MainPageState(); +} + +class _MainPageState extends TbPageState + with TbMainState, TickerProviderStateMixin { + late ValueNotifier _currentIndexNotifier; + late final List _tabItems; + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabItems = TbMainNavigationItem.getItems(tbContext); + final currentIndex = _indexFromPath(widget._path); + _tabController = TabController( + initialIndex: currentIndex, + length: _tabItems.length, + vsync: this, + ); + _currentIndexNotifier = ValueNotifier(currentIndex); + _tabController.animation!.addListener(_onTabAnimation); + } + + @override + void dispose() { + _tabController.animation!.removeListener(_onTabAnimation); + super.dispose(); + } + + _onTabAnimation() { + var value = _tabController.animation!.value; + var targetIndex; + if (value >= _tabController.previousIndex) { + targetIndex = value.round(); + } else { + targetIndex = value.floor(); + } + _currentIndexNotifier.value = targetIndex; + } + + @override + Future willPop() async { + if (_tabController.index > 0) { + _setIndex(0); + return false; + } + return true; + } + + @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(), + ), + 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) { + return _tabItems.indexWhere((item) => item.path == path); + } + + @override + bool canNavigate(String path) { + return _indexFromPath(path) > -1; + } + + @override + navigateToPath(String path) { + int targetIndex = _indexFromPath(path); + _setIndex(targetIndex); + } + + @override + bool isHomePage() { + return _tabController.index == 0; + } + + _setIndex(int index) { + hideNotification(); + _tabController.index = index; + } +} diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart new file mode 100644 index 0000000..b924746 --- /dev/null +++ b/lib/modules/more/more_page.dart @@ -0,0 +1,358 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/endpoint_name_widget.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; +import 'package:thingsboard_app/utils/services/notification_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class MorePage extends TbContextWidget { + MorePage(TbContext tbContext) : super(tbContext); + + @override + _MorePageState createState() => _MorePageState(); +} + +class _MorePageState extends TbContextState + with WidgetsBindingObserver { + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + backgroundColor: Colors.white, + body: Container( + padding: EdgeInsets.fromLTRB(16, 40, 16, 20), + child: Column( + 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(() {}); + }) + ], + ), + SizedBox(height: 22), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + _getUserDisplayName(), + style: TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 20, + height: 23 / 20, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: EndpointNameWidget( + endpoint: + getIt().getCachedEndpoint(), + ), + ), + ], + ), + SizedBox(height: 2), + Text(_getAuthorityName(context), + style: TextStyle( + 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)); + }) + ], + ), + ))); + } + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (getIt().apps.isNotEmpty) { + NotificationService().updateNotificationsCount(); + } + } + } + + Widget buildMoreMenuItems(BuildContext context) { + 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', + ), + ), + ); + } + }); + }).toList(); + return Column(children: items); + } + + String _getUserDisplayName() { + var user = tbContext.userDetails; + var name = ''; + if (user != null) { + if ((user.firstName != null && user.firstName!.isNotEmpty) || + (user.lastName != null && user.lastName!.isNotEmpty)) { + if (user.firstName != null) { + name += user.firstName!; + } + if (user.lastName != null) { + if (name.isNotEmpty) { + name += ' '; + } + name += user.lastName!; + } + } else { + name = user.email; + } + } + return name; + } + + String _getAuthorityName(BuildContext context) { + var user = tbContext.userDetails; + var name = ''; + if (user != null) { + var authority = user.authority; + switch (authority) { + case Authority.SYS_ADMIN: + name = '${S.of(context).systemAdministrator}'; + break; + case Authority.TENANT_ADMIN: + name = '${S.of(context).tenantAdministrator}'; + break; + case Authority.CUSTOMER_USER: + name = '${S.of(context).customer}'; + break; + default: + break; + } + } + return name; + } +} + +class MoreMenuItem { + final String title; + final IconData icon; + final String path; + final bool showAdditionalIcon; + final Widget? additionalIcon; + final bool disabled; + final String? disabledReasonMessage; + + MoreMenuItem({ + required this.title, + required this.icon, + required this.path, + this.showAdditionalIcon = false, + this.additionalIcon, + this.disabled = false, + this.disabledReasonMessage, + }); + + static List getItems( + TbContext tbContext, BuildContext context) { + if (tbContext.isAuthenticated) { + List items = []; + switch (tbContext.tbClient.getAuthUser()!.authority) { + case Authority.SYS_ADMIN: + items.add( + MoreMenuItem( + title: 'Notifications', + icon: Icons.notifications_active, + path: '/notifications', + showAdditionalIcon: true, + additionalIcon: _notificationNumberWidget(tbContext.tbClient), + disabled: getIt().apps.isEmpty, + disabledReasonMessage: 'Firebase is not configured.' + ' Please refer to the official Firebase documentation for' + ' guidance on how to do so.', + ), + ); + break; + case Authority.TENANT_ADMIN: + items.addAll([ + MoreMenuItem( + title: '${S.of(context).customers}', + icon: Icons.supervisor_account, + path: '/customers'), + MoreMenuItem( + title: '${S.of(context).assets}', + icon: Icons.domain, + path: '/assets'), + MoreMenuItem( + title: '${S.of(context).auditLogs}', + icon: Icons.track_changes, + path: '/auditLogs'), + MoreMenuItem( + title: 'Notifications', + icon: Icons.notifications_active, + path: '/notifications', + showAdditionalIcon: true, + additionalIcon: _notificationNumberWidget(tbContext.tbClient), + disabled: getIt().apps.isEmpty, + disabledReasonMessage: 'Notifications are not configured. ' + 'Please contact your system administrator.', + ), + ]); + break; + case Authority.CUSTOMER_USER: + items.addAll([ + MoreMenuItem( + title: '${S.of(context).assets}', + icon: Icons.domain, + path: '/assets'), + MoreMenuItem( + title: 'Notifications', + icon: Icons.notifications_active, + path: '/notifications', + showAdditionalIcon: true, + additionalIcon: _notificationNumberWidget(tbContext.tbClient), + disabled: getIt().apps.isEmpty, + disabledReasonMessage: 'Notifications are not configured. ' + 'Please contact your system administrator.', + ), + ]); + break; + case Authority.REFRESH_TOKEN: + break; + case Authority.ANONYMOUS: + break; + case Authority.PRE_VERIFICATION_TOKEN: + break; + } + return items; + } else { + return []; + } + } + + static Widget _notificationNumberWidget(ThingsboardClient tbClient) { + if (getIt().apps.isNotEmpty) { + NotificationService().updateNotificationsCount(); + } + + return StreamBuilder( + stream: NotificationsLocalService.notificationsNumberStream.stream, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data! > 0) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.red, + ), + padding: const EdgeInsets.all(3), + alignment: Alignment.center, + height: 20, + width: 20, + child: FittedBox( + fit: BoxFit.fitWidth, + child: Text( + '${snapshot.data! > 99 ? '99+' : snapshot.data}', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white), + ), + ), + ); + } + + return const SizedBox(width: 20); + }, + ); + } +} diff --git a/lib/modules/notification/controllers/notification_query_ctrl.dart b/lib/modules/notification/controllers/notification_query_ctrl.dart new file mode 100644 index 0000000..3b3c738 --- /dev/null +++ b/lib/modules/notification/controllers/notification_query_ctrl.dart @@ -0,0 +1,45 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NotificationQueryCtrl extends PageKeyController { + NotificationQueryCtrl({int pageSize = 20, String? searchText}) + : super( + PushNotificationQuery( + TimePageLink( + pageSize, + 0, + searchText, + SortOrder('createdTime', Direction.DESC), + ), + unreadOnly: true, + ), + ); + + @override + PushNotificationQuery nextPageKey(PushNotificationQuery pageKey) { + return PushNotificationQuery( + pageKey.pageLink.nextPageLink(), + unreadOnly: value.pageKey.unreadOnly, + ); + } + + void onSearchText(String searchText) { + final query = value.pageKey; + query.pageLink.page = 0; + query.pageLink.textSearch = searchText; + + notifyListeners(); + } + + void filterByReadStatus(bool unreadOnly) { + final query = value.pageKey; + query.pageLink.page = 0; + query.unreadOnly = unreadOnly; + + notifyListeners(); + } + + void refresh() { + notifyListeners(); + } +} diff --git a/lib/modules/notification/notification_page.dart b/lib/modules/notification/notification_page.dart new file mode 100644 index 0000000..1fc6cc0 --- /dev/null +++ b/lib/modules/notification/notification_page.dart @@ -0,0 +1,176 @@ +import 'dart:io'; + +import 'package:fluro/fluro.dart'; +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/notification/controllers/notification_query_ctrl.dart'; +import 'package:thingsboard_app/modules/notification/repository/notification_pagination_repository.dart'; +import 'package:thingsboard_app/modules/notification/repository/notification_repository.dart'; +import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; +import 'package:thingsboard_app/modules/notification/widgets/filter_segmented_button.dart'; +import 'package:thingsboard_app/modules/notification/widgets/notification_list.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +enum NotificationsFilter { all, unread } + +class NotificationPage extends TbPageWidget { + NotificationPage(TbContext tbContext) : super(tbContext); + + @override + State createState() => _NotificationPageState(); +} + +class _NotificationPageState extends TbPageState { + NotificationsFilter notificationsFilter = NotificationsFilter.unread; + late final NotificationPaginationRepository paginationRepository; + final notificationQueryCtrl = NotificationQueryCtrl(); + late final NotificationRepository notificationRepository; + + @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), + ); + } + }, + 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( + stream: NotificationsLocalService.notificationsNumberStream.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + _refresh(); + } + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 20), + child: FilterSegmentedButton( + selected: notificationsFilter, + onSelectionChanged: (newSelection) { + if (notificationsFilter == newSelection) { + return; + } + + setState(() { + notificationsFilter = newSelection; + + notificationRepository.filterByReadStatus( + notificationsFilter == NotificationsFilter.unread, + ); + }); + }, + segments: [ + FilterSegments( + label: 'Unread', + value: NotificationsFilter.unread, + ), + FilterSegments( + label: 'All', + value: NotificationsFilter.all, + ), + ], + ), + ), + Expanded( + child: NotificationsList( + pagingController: paginationRepository.pagingController, + thingsboardClient: tbClient, + tbContext: tbContext, + onClearNotification: (id, read) async { + await notificationRepository.deleteNotification(id); + if (!read) { + await notificationRepository + .decreaseNotificationBadgeCount(); + } + + if (mounted) { + notificationQueryCtrl.refresh(); + } + }, + onReadNotification: (id) async { + await notificationRepository.markNotificationAsRead(id); + await notificationRepository + .decreaseNotificationBadgeCount(); + + if (mounted) { + notificationQueryCtrl.refresh(); + } + }, + ), + ), + ], + ), + ); + }, + ), + ), + ); + } + + @override + void initState() { + paginationRepository = NotificationPaginationRepository( + tbClient: widget.tbContext.tbClient, + notificationQueryPageCtrl: notificationQueryCtrl, + )..init(); + + notificationRepository = NotificationRepository( + notificationQueryCtrl: notificationQueryCtrl, + thingsboardClient: widget.tbContext.tbClient, + ); + + super.initState(); + } + + @override + void dispose() { + paginationRepository.dispose(); + notificationQueryCtrl.dispose(); + super.dispose(); + } + + Future _refresh() async { + if (mounted) { + notificationQueryCtrl.refresh(); + } + } +} diff --git a/lib/modules/notification/repository/i_notification_query_repository.dart b/lib/modules/notification/repository/i_notification_query_repository.dart new file mode 100644 index 0000000..3a23320 --- /dev/null +++ b/lib/modules/notification/repository/i_notification_query_repository.dart @@ -0,0 +1,11 @@ +abstract interface class INotificationQueryRepository { + Future markAllAsRead(); + + Future markNotificationAsRead(String id); + + Future deleteNotification(String id); + + Future searchNotification(String searchText); + + Future filterByReadStatus(bool unreadOnly); +} diff --git a/lib/modules/notification/repository/notification_pagination_repository.dart b/lib/modules/notification/repository/notification_pagination_repository.dart new file mode 100644 index 0000000..6bf60f0 --- /dev/null +++ b/lib/modules/notification/repository/notification_pagination_repository.dart @@ -0,0 +1,67 @@ +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'; + +class NotificationPaginationRepository { + NotificationPaginationRepository({ + required this.notificationQueryPageCtrl, + required this.tbClient, + }); + + final NotificationQueryCtrl notificationQueryPageCtrl; + final ThingsboardClient tbClient; + late final PagingController pagingController; + + void init() { + pagingController = PagingController( + firstPageKey: notificationQueryPageCtrl.value.pageKey, + ); + + notificationQueryPageCtrl.addListener(_didChangePageKeyValue); + pagingController.addPageRequestListener((pageKey) { + _fetchPage(pageKey); + }); + } + + void dispose() { + notificationQueryPageCtrl.removeListener(_didChangePageKeyValue); + pagingController.dispose(); + } + + Future _fetchPage( + PushNotificationQuery pageKey, { + bool refresh = false, + }) async { + try { + final pageData = await tbClient.getNotificationService().getNotifications( + 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 = notificationQueryPageCtrl.nextPageKey(pageKey); + pagingController.appendPage(pageData.data, nextPageKey); + } + } catch (error) { + pagingController.error = error; + } + } + + void _didChangePageKeyValue() { + _refreshPagingController(); + } + + void _refreshPagingController() { + _fetchPage(notificationQueryPageCtrl.value.pageKey, refresh: true); + } +} diff --git a/lib/modules/notification/repository/notification_repository.dart b/lib/modules/notification/repository/notification_repository.dart new file mode 100644 index 0000000..5b01625 --- /dev/null +++ b/lib/modules/notification/repository/notification_repository.dart @@ -0,0 +1,56 @@ +import 'package:thingsboard_app/modules/notification/controllers/notification_query_ctrl.dart'; +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'; + +class NotificationRepository implements INotificationQueryRepository { + NotificationRepository({ + required this.notificationQueryCtrl, + required this.thingsboardClient, + }) : localService = NotificationsLocalService(); + + final NotificationQueryCtrl notificationQueryCtrl; + final ThingsboardClient thingsboardClient; + final INotificationsLocalService localService; + + @override + Future deleteNotification(String id) async { + return thingsboardClient.getNotificationService().deleteNotification(id); + } + + @override + Future markAllAsRead() async { + final response = await thingsboardClient + .getNotificationService() + .markAllNotificationsAsRead('MOBILE_APP'); + localService.clearNotificationBadgeCount(); + + return response; + } + + @override + Future markNotificationAsRead(String id) async { + return thingsboardClient + .getNotificationService() + .markNotificationAsRead(id); + } + + @override + Future searchNotification(String searchText) async { + notificationQueryCtrl.onSearchText(searchText); + } + + @override + Future filterByReadStatus(bool unreadOnly) async { + notificationQueryCtrl.filterByReadStatus(unreadOnly); + } + + Future decreaseNotificationBadgeCount() async { + localService.decreaseNotificationBadgeCount(); + } + + Future increaseNotificationBadgeCount() async { + localService.increaseNotificationBadgeCount(); + } +} diff --git a/lib/modules/notification/routes/notification_routes.dart b/lib/modules/notification/routes/notification_routes.dart new file mode 100644 index 0000000..0c0c7b2 --- /dev/null +++ b/lib/modules/notification/routes/notification_routes.dart @@ -0,0 +1,21 @@ +import 'package:fluro/fluro.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/modules/notification/notification_page.dart'; + +class NotificationRoutes extends TbRoutes { + NotificationRoutes(TbContext tbContext) : super(tbContext); + + static const notificationRoutePath = '/notifications'; + + late final notificationHandler = Handler( + handlerFunc: (context, params) { + return NotificationPage(tbContext); + }, + ); + + @override + void doRegisterRoutes(router) { + router.define(notificationRoutePath, handler: notificationHandler); + } +} diff --git a/lib/modules/notification/service/i_notifications_local_service.dart b/lib/modules/notification/service/i_notifications_local_service.dart new file mode 100644 index 0000000..42ea16c --- /dev/null +++ b/lib/modules/notification/service/i_notifications_local_service.dart @@ -0,0 +1,11 @@ +abstract interface class INotificationsLocalService { + Future increaseNotificationBadgeCount(); + + Future decreaseNotificationBadgeCount(); + + Future triggerNotificationCountStream(); + + Future clearNotificationBadgeCount(); + + Future updateNotificationsCount(int count); +} diff --git a/lib/modules/notification/service/notifications_local_service.dart b/lib/modules/notification/service/notifications_local_service.dart new file mode 100644 index 0000000..36ee09c --- /dev/null +++ b/lib/modules/notification/service/notifications_local_service.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/notification/service/i_notifications_local_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +final class NotificationsLocalService implements INotificationsLocalService { + NotificationsLocalService() : storage = getIt(); + + static const notificationCounterKey = 'notifications_counter'; + static final notificationsNumberStream = StreamController.broadcast(); + + late final TbStorage storage; + + @override + Future increaseNotificationBadgeCount() async { + final counter = await storage.getItem(notificationCounterKey); + final updatedCounter = int.parse(counter ?? '0') + 1; + await storage.setItem(notificationCounterKey, updatedCounter.toString()); + + FlutterAppBadger.updateBadgeCount(updatedCounter); + notificationsNumberStream.add(updatedCounter); + } + + @override + Future decreaseNotificationBadgeCount() async { + final counter = await storage.getItem(notificationCounterKey); + final updatedCounter = int.parse(counter ?? '0') - 1; + if (updatedCounter <= 0) { + FlutterAppBadger.removeBadge(); + notificationsNumberStream.add(0); + } else { + FlutterAppBadger.updateBadgeCount(updatedCounter); + await storage.setItem(notificationCounterKey, updatedCounter.toString()); + notificationsNumberStream.add(updatedCounter); + } + } + + @override + Future triggerNotificationCountStream() async { + final counter = await storage.getItem(notificationCounterKey); + notificationsNumberStream.add(int.parse(counter ?? '0')); + } + + @override + Future clearNotificationBadgeCount() async { + FlutterAppBadger.removeBadge(); + getIt().deleteItem(notificationCounterKey); + notificationsNumberStream.add(0); + } + + @override + Future updateNotificationsCount(int count) async { + 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 new file mode 100644 index 0000000..a9b116e --- /dev/null +++ b/lib/modules/notification/widgets/filter_segmented_button.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +class FilterSegmentedButton extends StatelessWidget { + FilterSegmentedButton({ + required this.segments, + required this.selected, + required this.onSelectionChanged, + }); + + final List segments; + final T selected; + final void Function(T) onSelectionChanged; + + final selectedTextStyle = TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ); + final unselectedTextStyle = TextStyle( + color: Colors.black.withOpacity(0.38), + fontWeight: FontWeight.w400, + ); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Colors.black.withOpacity(0.06), + ), + height: 32, + padding: const EdgeInsets.all(2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate( + segments.length, + (index) => Expanded( + child: InkWell( + onTap: () => onSelectionChanged(segments[index].value), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: segments[index].value == selected + ? Color(0xFF305680) + : null, + ), + child: Center( + child: Text( + segments[index].label, + style: segments[index].value == selected + ? selectedTextStyle + : unselectedTextStyle, + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +final class FilterSegments { + const FilterSegments({ + required this.label, + required this.value, + }); + + final String label; + final T value; +} diff --git a/lib/modules/notification/widgets/no_notifications_found_widget.dart b/lib/modules/notification/widgets/no_notifications_found_widget.dart new file mode 100644 index 0000000..a5b2f13 --- /dev/null +++ b/lib/modules/notification/widgets/no_notifications_found_widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class NoNotificationsFoundWidget extends StatelessWidget { + const NoNotificationsFoundWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'No notifications found', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/notification/widgets/notification_icon.dart b/lib/modules/notification/widgets/notification_icon.dart new file mode 100644 index 0000000..eb9d986 --- /dev/null +++ b/lib/modules/notification/widgets/notification_icon.dart @@ -0,0 +1,2372 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:thingsboard_app/constants/app_constants.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NotificationIcon extends StatelessWidget { + const NotificationIcon({required this.notification}); + + final PushNotification notification; + + @override + Widget build(BuildContext context) { + final iconData = _toIcon(notification.additionalConfig?['icon'] ?? {}); + + return iconData; + } + + Color _toColor(String? data) { + if (data != null) { + var hexColor = data.replaceAll("#", ""); + if (hexColor.length == 6) { + hexColor = "FF" + hexColor; + } + + if (hexColor.length == 8) { + final value = int.tryParse('0x$hexColor'); + if (value != null) { + return Color(value); + } + } + } + + return Colors.black54; + } + + Widget _toIcon(Map data) { + final imageData = data['icon']; + + if (imageData != null) { + if (imageData!.contains('mdi')) { + return Icon( + MdiIcons.fromString(imageData.split('mdi:').last), + color: _toColor(data['color']), + ); + + return SvgPicture.network( + '${ThingsboardAppConstants.thingsBoardApiEndpoint}/assets/mdi/${imageData.split('mdi:').last}.svg', + color: _toColor(data['color']), + ); + } + + return Icon( + materialIconsMap[imageData], + color: _toColor( + data['color'], + ), + ); + } + + return Icon(Icons.notifications, color: Colors.black54); + } +} + +const materialIconsMap = { + '10k': IconData(0xe000, fontFamily: 'MaterialIcons'), + '10mp': IconData(0xe001, fontFamily: 'MaterialIcons'), + '11mp': IconData(0xe002, fontFamily: 'MaterialIcons'), + '123': IconData(0xf04b5, fontFamily: 'MaterialIcons'), + '12mp': IconData(0xe003, fontFamily: 'MaterialIcons'), + '13mp': IconData(0xe004, fontFamily: 'MaterialIcons'), + '14mp': IconData(0xe005, fontFamily: 'MaterialIcons'), + '15mp': IconData(0xe006, fontFamily: 'MaterialIcons'), + '16mp': IconData(0xe007, fontFamily: 'MaterialIcons'), + '17mp': IconData(0xe008, fontFamily: 'MaterialIcons'), + '18_up_rating': IconData(0xf0784, fontFamily: 'MaterialIcons'), + '18mp': IconData(0xe009, fontFamily: 'MaterialIcons'), + '19mp': IconData(0xe00a, fontFamily: 'MaterialIcons'), + '1k': IconData(0xe00b, fontFamily: 'MaterialIcons'), + '1k_plus': IconData(0xe00c, fontFamily: 'MaterialIcons'), + '1x_mobiledata': IconData(0xe00d, fontFamily: 'MaterialIcons'), + '20mp': IconData(0xe00e, fontFamily: 'MaterialIcons'), + '21mp': IconData(0xe00f, fontFamily: 'MaterialIcons'), + '22mp': IconData(0xe010, fontFamily: 'MaterialIcons'), + '23mp': IconData(0xe011, fontFamily: 'MaterialIcons'), + '24mp': IconData(0xe012, fontFamily: 'MaterialIcons'), + '2k': IconData(0xe013, fontFamily: 'MaterialIcons'), + '2k_plus': IconData(0xe014, fontFamily: 'MaterialIcons'), + '2mp': IconData(0xe015, fontFamily: 'MaterialIcons'), + '30fps': IconData(0xe016, fontFamily: 'MaterialIcons'), + '30fps_select': IconData(0xe017, fontFamily: 'MaterialIcons'), + '360': IconData(0xe018, fontFamily: 'MaterialIcons'), + '3d_rotation': IconData(0xe019, fontFamily: 'MaterialIcons'), + '3g_mobiledata': IconData(0xe01a, fontFamily: 'MaterialIcons'), + '3k': IconData(0xe01b, fontFamily: 'MaterialIcons'), + '3k_plus': IconData(0xe01c, fontFamily: 'MaterialIcons'), + '3mp': IconData(0xe01d, fontFamily: 'MaterialIcons'), + '3p': IconData(0xe01e, fontFamily: 'MaterialIcons'), + '4g_mobiledata': IconData(0xe01f, fontFamily: 'MaterialIcons'), + '4g_plus_mobiledata': IconData(0xe020, fontFamily: 'MaterialIcons'), + '4k': IconData(0xe021, fontFamily: 'MaterialIcons'), + '4k_plus': IconData(0xe022, fontFamily: 'MaterialIcons'), + '4mp': IconData(0xe023, fontFamily: 'MaterialIcons'), + '5g': IconData(0xe024, fontFamily: 'MaterialIcons'), + '5k': IconData(0xe025, fontFamily: 'MaterialIcons'), + '5k_plus': IconData(0xe026, fontFamily: 'MaterialIcons'), + '5mp': IconData(0xe027, fontFamily: 'MaterialIcons'), + '60fps': IconData(0xe028, fontFamily: 'MaterialIcons'), + '60fps_select': IconData(0xe029, fontFamily: 'MaterialIcons'), + '6_ft_apart': IconData(0xe02a, fontFamily: 'MaterialIcons'), + '6k': IconData(0xe02b, fontFamily: 'MaterialIcons'), + '6k_plus': IconData(0xe02c, fontFamily: 'MaterialIcons'), + '6mp': IconData(0xe02d, fontFamily: 'MaterialIcons'), + '7k': IconData(0xe02e, fontFamily: 'MaterialIcons'), + '7k_plus': IconData(0xe02f, fontFamily: 'MaterialIcons'), + '7mp': IconData(0xe030, fontFamily: 'MaterialIcons'), + '8k': IconData(0xe031, fontFamily: 'MaterialIcons'), + '8k_plus': IconData(0xe032, fontFamily: 'MaterialIcons'), + '8mp': IconData(0xe033, fontFamily: 'MaterialIcons'), + '9k': IconData(0xe034, fontFamily: 'MaterialIcons'), + '9k_plus': IconData(0xe035, fontFamily: 'MaterialIcons'), + '9mp': IconData(0xe036, fontFamily: 'MaterialIcons'), + 'abc': IconData(0xf04b6, fontFamily: 'MaterialIcons'), + 'ac_unit': IconData(0xe037, fontFamily: 'MaterialIcons'), + 'access_alarm': IconData(0xe038, fontFamily: 'MaterialIcons'), + 'access_alarms': IconData(0xe039, fontFamily: 'MaterialIcons'), + 'access_time': IconData(0xe03a, fontFamily: 'MaterialIcons'), + 'access_time_filled': IconData(0xe03b, fontFamily: 'MaterialIcons'), + 'accessibility': IconData(0xe03c, fontFamily: 'MaterialIcons'), + 'accessibility_new': IconData(0xe03d, fontFamily: 'MaterialIcons'), + 'accessible': IconData(0xe03e, fontFamily: 'MaterialIcons'), + 'accessible_forward': IconData(0xe03f, fontFamily: 'MaterialIcons'), + 'account_balance': IconData(0xe040, fontFamily: 'MaterialIcons'), + 'account_balance_wallet': IconData(0xe041, fontFamily: 'MaterialIcons'), + 'account_box': IconData(0xe042, fontFamily: 'MaterialIcons'), + 'account_circle': IconData(0xe043, fontFamily: 'MaterialIcons'), + 'account_tree': IconData(0xe044, fontFamily: 'MaterialIcons'), + 'ad_units': IconData(0xe045, fontFamily: 'MaterialIcons'), + 'adb': IconData(0xe046, fontFamily: 'MaterialIcons'), + 'add': IconData(0xe047, fontFamily: 'MaterialIcons'), + 'add_a_photo': IconData(0xe048, fontFamily: 'MaterialIcons'), + 'add_alarm': IconData(0xe049, fontFamily: 'MaterialIcons'), + 'add_alert': IconData(0xe04a, fontFamily: 'MaterialIcons'), + 'add_box': IconData(0xe04b, fontFamily: 'MaterialIcons'), + 'add_business': IconData(0xe04c, fontFamily: 'MaterialIcons'), + 'add_call': IconData(0xe04d, fontFamily: 'MaterialIcons'), + 'add_card': IconData(0xf04b7, fontFamily: 'MaterialIcons'), + 'add_chart': IconData(0xe04e, fontFamily: 'MaterialIcons'), + 'add_circle': IconData(0xe04f, fontFamily: 'MaterialIcons'), + 'add_circle_outline': IconData(0xe050, fontFamily: 'MaterialIcons'), + 'add_comment': IconData(0xe051, fontFamily: 'MaterialIcons'), + 'add_home': IconData(0xf0785, fontFamily: 'MaterialIcons'), + 'add_home_work': IconData(0xf0786, fontFamily: 'MaterialIcons'), + 'add_ic_call': IconData(0xe052, fontFamily: 'MaterialIcons'), + 'add_link': IconData(0xe053, fontFamily: 'MaterialIcons'), + 'add_location': IconData(0xe054, fontFamily: 'MaterialIcons'), + 'add_location_alt': IconData(0xe055, fontFamily: 'MaterialIcons'), + 'add_moderator': IconData(0xe056, fontFamily: 'MaterialIcons'), + 'add_photo_alternate': IconData(0xe057, fontFamily: 'MaterialIcons'), + 'add_reaction': IconData(0xe058, fontFamily: 'MaterialIcons'), + 'add_road': IconData(0xe059, fontFamily: 'MaterialIcons'), + 'add_shopping_cart': IconData(0xe05a, fontFamily: 'MaterialIcons'), + 'add_task': IconData(0xe05b, fontFamily: 'MaterialIcons'), + 'add_to_drive': IconData(0xe05c, fontFamily: 'MaterialIcons'), + 'add_to_home_screen': IconData(0xe05d, fontFamily: 'MaterialIcons'), + 'add_to_photos': IconData(0xe05e, fontFamily: 'MaterialIcons'), + 'add_to_queue': IconData(0xe05f, fontFamily: 'MaterialIcons'), + 'addchart': IconData(0xe060, fontFamily: 'MaterialIcons'), + 'adf_scanner': IconData(0xf04b8, fontFamily: 'MaterialIcons'), + 'adjust': IconData(0xe061, fontFamily: 'MaterialIcons'), + 'admin_panel_settings': IconData(0xe062, fontFamily: 'MaterialIcons'), + 'adobe': IconData(0xf04b9, fontFamily: 'MaterialIcons'), + 'ads_click': IconData(0xf04ba, fontFamily: 'MaterialIcons'), + 'agriculture': IconData(0xe063, fontFamily: 'MaterialIcons'), + 'air': IconData(0xe064, fontFamily: 'MaterialIcons'), + 'airline_seat_flat': IconData(0xe065, fontFamily: 'MaterialIcons'), + 'airline_seat_flat_angled': IconData(0xe066, fontFamily: 'MaterialIcons'), + 'airline_seat_individual_suite': + IconData(0xe067, fontFamily: 'MaterialIcons'), + 'airline_seat_legroom_extra': IconData(0xe068, fontFamily: 'MaterialIcons'), + 'airline_seat_legroom_normal': IconData(0xe069, fontFamily: 'MaterialIcons'), + 'airline_seat_legroom_reduced': IconData(0xe06a, fontFamily: 'MaterialIcons'), + 'airline_seat_recline_extra': IconData(0xe06b, fontFamily: 'MaterialIcons'), + 'airline_seat_recline_normal': IconData(0xe06c, fontFamily: 'MaterialIcons'), + 'airline_stops': IconData(0xf04bb, fontFamily: 'MaterialIcons'), + 'airlines': IconData(0xf04bc, fontFamily: 'MaterialIcons'), + 'airplane_ticket': IconData(0xe06d, fontFamily: 'MaterialIcons'), + 'airplanemode_active': IconData(0xe06e, fontFamily: 'MaterialIcons'), + 'airplanemode_inactive': IconData(0xe06f, fontFamily: 'MaterialIcons'), + 'airplanemode_off': IconData(0xe06f, fontFamily: 'MaterialIcons'), + 'airplanemode_on': IconData(0xe06e, fontFamily: 'MaterialIcons'), + 'airplay': IconData(0xe070, fontFamily: 'MaterialIcons'), + 'airport_shuttle': IconData(0xe071, fontFamily: 'MaterialIcons'), + 'alarm': IconData(0xe072, fontFamily: 'MaterialIcons'), + 'alarm_add': IconData(0xe073, fontFamily: 'MaterialIcons'), + 'alarm_off': IconData(0xe074, fontFamily: 'MaterialIcons'), + 'alarm_on': IconData(0xe075, fontFamily: 'MaterialIcons'), + 'album': IconData(0xe076, fontFamily: 'MaterialIcons'), + 'align_horizontal_center': IconData(0xe077, fontFamily: 'MaterialIcons'), + 'align_horizontal_left': IconData(0xe078, fontFamily: 'MaterialIcons'), + 'align_horizontal_right': IconData(0xe079, fontFamily: 'MaterialIcons'), + 'align_vertical_bottom': IconData(0xe07a, fontFamily: 'MaterialIcons'), + 'align_vertical_center': IconData(0xe07b, fontFamily: 'MaterialIcons'), + 'align_vertical_top': IconData(0xe07c, fontFamily: 'MaterialIcons'), + 'all_inbox': IconData(0xe07d, fontFamily: 'MaterialIcons'), + 'all_inclusive': IconData(0xe07e, fontFamily: 'MaterialIcons'), + 'all_out': IconData(0xe07f, fontFamily: 'MaterialIcons'), + 'alt_route': IconData(0xe080, fontFamily: 'MaterialIcons'), + 'alternate_email': IconData(0xe081, fontFamily: 'MaterialIcons'), + 'amp_stories': IconData(0xe082, fontFamily: 'MaterialIcons'), + 'analytics': IconData(0xe083, fontFamily: 'MaterialIcons'), + 'anchor': IconData(0xe084, fontFamily: 'MaterialIcons'), + 'android': IconData(0xe085, fontFamily: 'MaterialIcons'), + 'animation': IconData(0xe086, fontFamily: 'MaterialIcons'), + 'announcement': IconData(0xe087, fontFamily: 'MaterialIcons'), + 'aod': IconData(0xe088, fontFamily: 'MaterialIcons'), + 'apartment': IconData(0xe089, fontFamily: 'MaterialIcons'), + 'api': IconData(0xe08a, fontFamily: 'MaterialIcons'), + 'app_blocking': IconData(0xe08b, fontFamily: 'MaterialIcons'), + 'app_registration': IconData(0xe08c, fontFamily: 'MaterialIcons'), + 'app_settings_alt': IconData(0xe08d, fontFamily: 'MaterialIcons'), + 'app_shortcut': IconData(0xf04bd, fontFamily: 'MaterialIcons'), + 'apple': IconData(0xf04be, fontFamily: 'MaterialIcons'), + 'approval': IconData(0xe08e, fontFamily: 'MaterialIcons'), + 'apps': IconData(0xe08f, fontFamily: 'MaterialIcons'), + 'apps_outage': IconData(0xf04bf, fontFamily: 'MaterialIcons'), + 'architecture': IconData(0xe090, fontFamily: 'MaterialIcons'), + 'archive': IconData(0xe091, fontFamily: 'MaterialIcons'), + 'area_chart': IconData(0xf04c0, fontFamily: 'MaterialIcons'), + 'arrow_back': + IconData(0xe092, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_back_ios': + IconData(0xe093, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_back_ios_new': + IconData(0xe094, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_circle_down': IconData(0xe095, fontFamily: 'MaterialIcons'), + 'arrow_circle_left': IconData(0xf04c1, fontFamily: 'MaterialIcons'), + 'arrow_circle_right': IconData(0xf04c2, fontFamily: 'MaterialIcons'), + 'arrow_circle_up': IconData(0xe096, fontFamily: 'MaterialIcons'), + 'arrow_downward': IconData(0xe097, fontFamily: 'MaterialIcons'), + 'arrow_drop_down': IconData(0xe098, fontFamily: 'MaterialIcons'), + 'arrow_drop_down_circle': IconData(0xe099, fontFamily: 'MaterialIcons'), + 'arrow_drop_up': IconData(0xe09a, fontFamily: 'MaterialIcons'), + 'arrow_forward': + IconData(0xe09b, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_forward_ios': + IconData(0xe09c, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_left': + IconData(0xe09d, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_outward': IconData(0xf0852, fontFamily: 'MaterialIcons'), + 'arrow_right': + IconData(0xe09e, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_right_alt': + IconData(0xe09f, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'arrow_upward': IconData(0xe0a0, fontFamily: 'MaterialIcons'), + 'art_track': IconData(0xe0a1, fontFamily: 'MaterialIcons'), + 'article': IconData(0xe0a2, fontFamily: 'MaterialIcons'), + 'aspect_ratio': IconData(0xe0a3, fontFamily: 'MaterialIcons'), + 'assessment': IconData(0xe0a4, fontFamily: 'MaterialIcons'), + 'assignment': + IconData(0xe0a5, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'assignment_add': IconData(0xf0853, fontFamily: 'MaterialIcons'), + 'assignment_ind': IconData(0xe0a6, fontFamily: 'MaterialIcons'), + 'assignment_late': IconData(0xe0a7, fontFamily: 'MaterialIcons'), + 'assignment_return': + IconData(0xe0a8, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'assignment_returned': IconData(0xe0a9, fontFamily: 'MaterialIcons'), + 'assignment_turned_in': IconData(0xe0aa, fontFamily: 'MaterialIcons'), + 'assist_walker': IconData(0xf0854, fontFamily: 'MaterialIcons'), + 'assistant': IconData(0xe0ab, fontFamily: 'MaterialIcons'), + 'assistant_direction': IconData(0xe0ac, fontFamily: 'MaterialIcons'), + 'assistant_navigation': IconData(0xe0ad, fontFamily: 'MaterialIcons'), + 'assistant_photo': IconData(0xe0ae, fontFamily: 'MaterialIcons'), + 'assured_workload': IconData(0xf04c3, fontFamily: 'MaterialIcons'), + 'atm': IconData(0xe0af, fontFamily: 'MaterialIcons'), + 'attach_email': IconData(0xe0b0, fontFamily: 'MaterialIcons'), + 'attach_file': IconData(0xe0b1, fontFamily: 'MaterialIcons'), + 'attach_money': IconData(0xe0b2, fontFamily: 'MaterialIcons'), + 'attachment': IconData(0xe0b3, fontFamily: 'MaterialIcons'), + 'attractions': IconData(0xe0b4, fontFamily: 'MaterialIcons'), + 'attribution': IconData(0xe0b5, fontFamily: 'MaterialIcons'), + 'audio_file': IconData(0xf04c4, fontFamily: 'MaterialIcons'), + 'audiotrack': IconData(0xe0b6, fontFamily: 'MaterialIcons'), + 'auto_awesome': IconData(0xe0b7, fontFamily: 'MaterialIcons'), + 'auto_awesome_mosaic': IconData(0xe0b8, fontFamily: 'MaterialIcons'), + 'auto_awesome_motion': IconData(0xe0b9, fontFamily: 'MaterialIcons'), + 'auto_delete': IconData(0xe0ba, fontFamily: 'MaterialIcons'), + 'auto_fix_high': IconData(0xe0bb, fontFamily: 'MaterialIcons'), + 'auto_fix_normal': IconData(0xe0bc, fontFamily: 'MaterialIcons'), + 'auto_fix_off': IconData(0xe0bd, fontFamily: 'MaterialIcons'), + 'auto_graph': IconData(0xe0be, fontFamily: 'MaterialIcons'), + 'auto_mode': IconData(0xf0787, fontFamily: 'MaterialIcons'), + 'auto_stories': IconData(0xe0bf, fontFamily: 'MaterialIcons'), + 'autofps_select': IconData(0xe0c0, fontFamily: 'MaterialIcons'), + 'autorenew': IconData(0xe0c1, fontFamily: 'MaterialIcons'), + 'av_timer': IconData(0xe0c2, fontFamily: 'MaterialIcons'), + 'baby_changing_station': IconData(0xe0c3, fontFamily: 'MaterialIcons'), + 'back_hand': IconData(0xf04c5, fontFamily: 'MaterialIcons'), + 'backpack': IconData(0xe0c4, fontFamily: 'MaterialIcons'), + 'backspace': + IconData(0xe0c5, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'backup': IconData(0xe0c6, fontFamily: 'MaterialIcons'), + 'backup_table': IconData(0xe0c7, fontFamily: 'MaterialIcons'), + 'badge': IconData(0xe0c8, fontFamily: 'MaterialIcons'), + 'bakery_dining': IconData(0xe0c9, fontFamily: 'MaterialIcons'), + 'balance': IconData(0xf04c6, fontFamily: 'MaterialIcons'), + 'balcony': IconData(0xe0ca, fontFamily: 'MaterialIcons'), + 'ballot': IconData(0xe0cb, fontFamily: 'MaterialIcons'), + 'bar_chart': IconData(0xe0cc, fontFamily: 'MaterialIcons'), + 'barcode_reader': IconData(0xf0855, fontFamily: 'MaterialIcons'), + 'batch_prediction': IconData(0xe0cd, fontFamily: 'MaterialIcons'), + 'bathroom': IconData(0xe0ce, fontFamily: 'MaterialIcons'), + 'bathtub': IconData(0xe0cf, fontFamily: 'MaterialIcons'), + 'battery_0_bar': IconData(0xf0788, fontFamily: 'MaterialIcons'), + 'battery_1_bar': IconData(0xf0789, fontFamily: 'MaterialIcons'), + 'battery_2_bar': IconData(0xf078a, fontFamily: 'MaterialIcons'), + 'battery_3_bar': IconData(0xf078b, fontFamily: 'MaterialIcons'), + 'battery_4_bar': IconData(0xf078c, fontFamily: 'MaterialIcons'), + 'battery_5_bar': IconData(0xf078d, fontFamily: 'MaterialIcons'), + 'battery_6_bar': IconData(0xf078e, fontFamily: 'MaterialIcons'), + 'battery_alert': IconData(0xe0d0, fontFamily: 'MaterialIcons'), + 'battery_charging_full': IconData(0xe0d1, fontFamily: 'MaterialIcons'), + 'battery_full': IconData(0xe0d2, fontFamily: 'MaterialIcons'), + 'battery_saver': IconData(0xe0d3, fontFamily: 'MaterialIcons'), + 'battery_std': IconData(0xe0d4, fontFamily: 'MaterialIcons'), + 'battery_unknown': + IconData(0xe0d5, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'beach_access': IconData(0xe0d6, fontFamily: 'MaterialIcons'), + 'bed': IconData(0xe0d7, fontFamily: 'MaterialIcons'), + 'bedroom_baby': IconData(0xe0d8, fontFamily: 'MaterialIcons'), + 'bedroom_child': IconData(0xe0d9, fontFamily: 'MaterialIcons'), + 'bedroom_parent': IconData(0xe0da, fontFamily: 'MaterialIcons'), + 'bedtime': IconData(0xe0db, fontFamily: 'MaterialIcons'), + 'bedtime_off': IconData(0xf04c7, fontFamily: 'MaterialIcons'), + 'beenhere': IconData(0xe0dc, fontFamily: 'MaterialIcons'), + 'bento': IconData(0xe0dd, fontFamily: 'MaterialIcons'), + 'bike_scooter': IconData(0xe0de, fontFamily: 'MaterialIcons'), + 'biotech': IconData(0xe0df, fontFamily: 'MaterialIcons'), + 'blender': IconData(0xe0e0, fontFamily: 'MaterialIcons'), + 'blind': IconData(0xf0856, fontFamily: 'MaterialIcons'), + 'blinds': IconData(0xf078f, fontFamily: 'MaterialIcons'), + 'blinds_closed': IconData(0xf0790, fontFamily: 'MaterialIcons'), + 'block': IconData(0xe0e1, fontFamily: 'MaterialIcons'), + 'block_flipped': IconData(0xe0e2, fontFamily: 'MaterialIcons'), + 'bloodtype': IconData(0xe0e3, fontFamily: 'MaterialIcons'), + 'bluetooth': IconData(0xe0e4, fontFamily: 'MaterialIcons'), + 'bluetooth_audio': IconData(0xe0e5, fontFamily: 'MaterialIcons'), + 'bluetooth_connected': IconData(0xe0e6, fontFamily: 'MaterialIcons'), + 'bluetooth_disabled': IconData(0xe0e7, fontFamily: 'MaterialIcons'), + 'bluetooth_drive': IconData(0xe0e8, fontFamily: 'MaterialIcons'), + 'bluetooth_searching': IconData(0xe0e9, fontFamily: 'MaterialIcons'), + 'blur_circular': IconData(0xe0ea, fontFamily: 'MaterialIcons'), + 'blur_linear': IconData(0xe0eb, fontFamily: 'MaterialIcons'), + 'blur_off': IconData(0xe0ec, fontFamily: 'MaterialIcons'), + 'blur_on': IconData(0xe0ed, fontFamily: 'MaterialIcons'), + 'bolt': IconData(0xe0ee, fontFamily: 'MaterialIcons'), + 'book': IconData(0xe0ef, fontFamily: 'MaterialIcons'), + 'book_online': IconData(0xe0f0, fontFamily: 'MaterialIcons'), + 'bookmark': IconData(0xe0f1, fontFamily: 'MaterialIcons'), + 'bookmark_add': IconData(0xe0f2, fontFamily: 'MaterialIcons'), + 'bookmark_added': IconData(0xe0f3, fontFamily: 'MaterialIcons'), + 'bookmark_border': IconData(0xe0f4, fontFamily: 'MaterialIcons'), + 'bookmark_outline': IconData(0xe0f4, fontFamily: 'MaterialIcons'), + 'bookmark_remove': IconData(0xe0f5, fontFamily: 'MaterialIcons'), + 'bookmarks': IconData(0xe0f6, fontFamily: 'MaterialIcons'), + 'border_all': IconData(0xe0f7, fontFamily: 'MaterialIcons'), + 'border_bottom': IconData(0xe0f8, fontFamily: 'MaterialIcons'), + 'border_clear': IconData(0xe0f9, fontFamily: 'MaterialIcons'), + 'border_color': IconData(0xe0fa, fontFamily: 'MaterialIcons'), + 'border_horizontal': IconData(0xe0fb, fontFamily: 'MaterialIcons'), + 'border_inner': IconData(0xe0fc, fontFamily: 'MaterialIcons'), + 'border_left': IconData(0xe0fd, fontFamily: 'MaterialIcons'), + 'border_outer': IconData(0xe0fe, fontFamily: 'MaterialIcons'), + 'border_right': IconData(0xe0ff, fontFamily: 'MaterialIcons'), + 'border_style': IconData(0xe100, fontFamily: 'MaterialIcons'), + 'border_top': IconData(0xe101, fontFamily: 'MaterialIcons'), + 'border_vertical': IconData(0xe102, fontFamily: 'MaterialIcons'), + 'boy': IconData(0xf04c8, fontFamily: 'MaterialIcons'), + 'branding_watermark': IconData(0xe103, fontFamily: 'MaterialIcons'), + 'breakfast_dining': IconData(0xe104, fontFamily: 'MaterialIcons'), + 'brightness_1': IconData(0xe105, fontFamily: 'MaterialIcons'), + 'brightness_2': IconData(0xe106, fontFamily: 'MaterialIcons'), + 'brightness_3': IconData(0xe107, fontFamily: 'MaterialIcons'), + 'brightness_4': IconData(0xe108, fontFamily: 'MaterialIcons'), + 'brightness_5': IconData(0xe109, fontFamily: 'MaterialIcons'), + 'brightness_6': IconData(0xe10a, fontFamily: 'MaterialIcons'), + 'brightness_7': IconData(0xe10b, fontFamily: 'MaterialIcons'), + 'brightness_auto': IconData(0xe10c, fontFamily: 'MaterialIcons'), + 'brightness_high': IconData(0xe10d, fontFamily: 'MaterialIcons'), + 'brightness_low': IconData(0xe10e, fontFamily: 'MaterialIcons'), + 'brightness_medium': IconData(0xe10f, fontFamily: 'MaterialIcons'), + 'broadcast_on_home': IconData(0xf0791, fontFamily: 'MaterialIcons'), + 'broadcast_on_personal': IconData(0xf0792, fontFamily: 'MaterialIcons'), + 'broken_image': IconData(0xe110, fontFamily: 'MaterialIcons'), + 'browse_gallery': IconData(0xf06ba, fontFamily: 'MaterialIcons'), + 'browser_not_supported': IconData(0xe111, fontFamily: 'MaterialIcons'), + 'browser_updated': IconData(0xf04c9, fontFamily: 'MaterialIcons'), + 'brunch_dining': IconData(0xe112, fontFamily: 'MaterialIcons'), + 'brush': IconData(0xe113, fontFamily: 'MaterialIcons'), + 'bubble_chart': IconData(0xe114, fontFamily: 'MaterialIcons'), + 'bug_report': IconData(0xe115, fontFamily: 'MaterialIcons'), + 'build': IconData(0xe116, fontFamily: 'MaterialIcons'), + 'build_circle': IconData(0xe117, fontFamily: 'MaterialIcons'), + 'bungalow': IconData(0xe118, fontFamily: 'MaterialIcons'), + 'burst_mode': IconData(0xe119, fontFamily: 'MaterialIcons'), + 'bus_alert': IconData(0xe11a, fontFamily: 'MaterialIcons'), + 'business': IconData(0xe11b, fontFamily: 'MaterialIcons'), + 'business_center': IconData(0xe11c, fontFamily: 'MaterialIcons'), + 'cabin': IconData(0xe11d, fontFamily: 'MaterialIcons'), + 'cable': IconData(0xe11e, fontFamily: 'MaterialIcons'), + 'cached': IconData(0xe11f, fontFamily: 'MaterialIcons'), + 'cake': IconData(0xe120, fontFamily: 'MaterialIcons'), + 'calculate': IconData(0xe121, fontFamily: 'MaterialIcons'), + 'calendar_month': IconData(0xf06bb, fontFamily: 'MaterialIcons'), + 'calendar_today': IconData(0xe122, fontFamily: 'MaterialIcons'), + 'calendar_view_day': IconData(0xe123, fontFamily: 'MaterialIcons'), + 'calendar_view_month': IconData(0xe124, fontFamily: 'MaterialIcons'), + 'calendar_view_week': IconData(0xe125, fontFamily: 'MaterialIcons'), + 'call': IconData(0xe126, fontFamily: 'MaterialIcons'), + 'call_end': IconData(0xe127, fontFamily: 'MaterialIcons'), + 'call_made': + IconData(0xe128, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'call_merge': + IconData(0xe129, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'call_missed': + IconData(0xe12a, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'call_missed_outgoing': + IconData(0xe12b, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'call_received': + IconData(0xe12c, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'call_split': + IconData(0xe12d, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'call_to_action': IconData(0xe12e, fontFamily: 'MaterialIcons'), + 'camera': IconData(0xe12f, fontFamily: 'MaterialIcons'), + 'camera_alt': IconData(0xe130, fontFamily: 'MaterialIcons'), + 'camera_enhance': IconData(0xe131, fontFamily: 'MaterialIcons'), + 'camera_front': IconData(0xe132, fontFamily: 'MaterialIcons'), + 'camera_indoor': IconData(0xe133, fontFamily: 'MaterialIcons'), + 'camera_outdoor': IconData(0xe134, fontFamily: 'MaterialIcons'), + 'camera_rear': IconData(0xe135, fontFamily: 'MaterialIcons'), + 'camera_roll': IconData(0xe136, fontFamily: 'MaterialIcons'), + 'cameraswitch': IconData(0xe137, fontFamily: 'MaterialIcons'), + 'campaign': IconData(0xe138, fontFamily: 'MaterialIcons'), + 'cancel': IconData(0xe139, fontFamily: 'MaterialIcons'), + 'cancel_presentation': IconData(0xe13a, fontFamily: 'MaterialIcons'), + 'cancel_schedule_send': IconData(0xe13b, fontFamily: 'MaterialIcons'), + 'candlestick_chart': IconData(0xf04ca, fontFamily: 'MaterialIcons'), + 'car_crash': IconData(0xf0793, fontFamily: 'MaterialIcons'), + 'car_rental': IconData(0xe13c, fontFamily: 'MaterialIcons'), + 'car_repair': IconData(0xe13d, fontFamily: 'MaterialIcons'), + 'card_giftcard': IconData(0xe13e, fontFamily: 'MaterialIcons'), + 'card_membership': IconData(0xe13f, fontFamily: 'MaterialIcons'), + 'card_travel': IconData(0xe140, fontFamily: 'MaterialIcons'), + 'carpenter': IconData(0xe141, fontFamily: 'MaterialIcons'), + 'cases': IconData(0xe142, fontFamily: 'MaterialIcons'), + 'casino': IconData(0xe143, fontFamily: 'MaterialIcons'), + 'cast': IconData(0xe144, fontFamily: 'MaterialIcons'), + 'cast_connected': IconData(0xe145, fontFamily: 'MaterialIcons'), + 'cast_for_education': IconData(0xe146, fontFamily: 'MaterialIcons'), + 'castle': IconData(0xf04cb, fontFamily: 'MaterialIcons'), + 'catching_pokemon': IconData(0xe147, fontFamily: 'MaterialIcons'), + 'category': IconData(0xe148, fontFamily: 'MaterialIcons'), + 'celebration': IconData(0xe149, fontFamily: 'MaterialIcons'), + 'cell_tower': IconData(0xf04cc, fontFamily: 'MaterialIcons'), + 'cell_wifi': IconData(0xe14a, fontFamily: 'MaterialIcons'), + 'center_focus_strong': IconData(0xe14b, fontFamily: 'MaterialIcons'), + 'center_focus_weak': IconData(0xe14c, fontFamily: 'MaterialIcons'), + 'chair': IconData(0xe14d, fontFamily: 'MaterialIcons'), + 'chair_alt': IconData(0xe14e, fontFamily: 'MaterialIcons'), + 'chalet': IconData(0xe14f, fontFamily: 'MaterialIcons'), + 'change_circle': IconData(0xe150, fontFamily: 'MaterialIcons'), + 'change_history': IconData(0xe151, fontFamily: 'MaterialIcons'), + 'charging_station': IconData(0xe152, fontFamily: 'MaterialIcons'), + 'chat': IconData(0xe153, fontFamily: 'MaterialIcons'), + 'chat_bubble': IconData(0xe154, fontFamily: 'MaterialIcons'), + 'chat_bubble_outline': IconData(0xe155, fontFamily: 'MaterialIcons'), + 'check': IconData(0xe156, fontFamily: 'MaterialIcons'), + 'check_box': IconData(0xe157, fontFamily: 'MaterialIcons'), + 'check_box_outline_blank': IconData(0xe158, fontFamily: 'MaterialIcons'), + 'check_circle': IconData(0xe159, fontFamily: 'MaterialIcons'), + 'check_circle_outline': IconData(0xe15a, fontFamily: 'MaterialIcons'), + 'checklist': IconData(0xe15b, fontFamily: 'MaterialIcons'), + 'checklist_rtl': IconData(0xe15c, fontFamily: 'MaterialIcons'), + 'checkroom': IconData(0xe15d, fontFamily: 'MaterialIcons'), + 'chevron_left': + IconData(0xe15e, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'chevron_right': + IconData(0xe15f, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'child_care': IconData(0xe160, fontFamily: 'MaterialIcons'), + 'child_friendly': IconData(0xe161, fontFamily: 'MaterialIcons'), + 'chrome_reader_mode': + IconData(0xe162, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'church': IconData(0xf04cd, fontFamily: 'MaterialIcons'), + 'circle': IconData(0xe163, fontFamily: 'MaterialIcons'), + 'circle_notifications': IconData(0xe164, fontFamily: 'MaterialIcons'), + 'class': IconData(0xe165, fontFamily: 'MaterialIcons'), + 'clean_hands': IconData(0xe166, fontFamily: 'MaterialIcons'), + 'cleaning_services': IconData(0xe167, fontFamily: 'MaterialIcons'), + 'clear': IconData(0xe168, fontFamily: 'MaterialIcons'), + 'clear_all': IconData(0xe169, fontFamily: 'MaterialIcons'), + 'close': IconData(0xe16a, fontFamily: 'MaterialIcons'), + 'close_fullscreen': IconData(0xe16b, fontFamily: 'MaterialIcons'), + 'closed_caption': IconData(0xe16c, fontFamily: 'MaterialIcons'), + 'closed_caption_disabled': IconData(0xe16d, fontFamily: 'MaterialIcons'), + 'closed_caption_off': IconData(0xe16e, fontFamily: 'MaterialIcons'), + 'cloud': IconData(0xe16f, fontFamily: 'MaterialIcons'), + 'cloud_circle': IconData(0xe170, fontFamily: 'MaterialIcons'), + 'cloud_done': IconData(0xe171, fontFamily: 'MaterialIcons'), + 'cloud_download': IconData(0xe172, fontFamily: 'MaterialIcons'), + 'cloud_off': IconData(0xe173, fontFamily: 'MaterialIcons'), + 'cloud_queue': IconData(0xe174, fontFamily: 'MaterialIcons'), + 'cloud_sync': IconData(0xf04ce, fontFamily: 'MaterialIcons'), + 'cloud_upload': IconData(0xe175, fontFamily: 'MaterialIcons'), + 'cloudy_snowing': IconData(0xf04cf, fontFamily: 'MaterialIcons'), + 'co2': IconData(0xf04d0, fontFamily: 'MaterialIcons'), + 'co_present': IconData(0xf04d1, fontFamily: 'MaterialIcons'), + 'code': IconData(0xe176, fontFamily: 'MaterialIcons'), + 'code_off': IconData(0xe177, fontFamily: 'MaterialIcons'), + 'coffee': IconData(0xe178, fontFamily: 'MaterialIcons'), + 'coffee_maker': IconData(0xe179, fontFamily: 'MaterialIcons'), + 'collections': IconData(0xe17a, fontFamily: 'MaterialIcons'), + 'collections_bookmark': IconData(0xe17b, fontFamily: 'MaterialIcons'), + 'color_lens': IconData(0xe17c, fontFamily: 'MaterialIcons'), + 'colorize': IconData(0xe17d, fontFamily: 'MaterialIcons'), + 'comment': IconData(0xe17e, fontFamily: 'MaterialIcons'), + 'comment_bank': IconData(0xe17f, fontFamily: 'MaterialIcons'), + 'comments_disabled': IconData(0xf04d2, fontFamily: 'MaterialIcons'), + 'commit': IconData(0xf04d3, fontFamily: 'MaterialIcons'), + 'commute': IconData(0xe180, fontFamily: 'MaterialIcons'), + 'compare': IconData(0xe181, fontFamily: 'MaterialIcons'), + 'compare_arrows': IconData(0xe182, fontFamily: 'MaterialIcons'), + 'compass_calibration': IconData(0xe183, fontFamily: 'MaterialIcons'), + 'compost': IconData(0xf04d4, fontFamily: 'MaterialIcons'), + 'compress': IconData(0xe184, fontFamily: 'MaterialIcons'), + 'computer': IconData(0xe185, fontFamily: 'MaterialIcons'), + 'confirmation_num': IconData(0xe186, fontFamily: 'MaterialIcons'), + 'confirmation_number': IconData(0xe186, fontFamily: 'MaterialIcons'), + 'connect_without_contact': IconData(0xe187, fontFamily: 'MaterialIcons'), + 'connected_tv': IconData(0xe188, fontFamily: 'MaterialIcons'), + 'connecting_airports': IconData(0xf04d5, fontFamily: 'MaterialIcons'), + 'construction': IconData(0xe189, fontFamily: 'MaterialIcons'), + 'contact_emergency': IconData(0xf0857, fontFamily: 'MaterialIcons'), + 'contact_mail': IconData(0xe18a, fontFamily: 'MaterialIcons'), + 'contact_page': IconData(0xe18b, fontFamily: 'MaterialIcons'), + 'contact_phone': IconData(0xe18c, fontFamily: 'MaterialIcons'), + 'contact_support': IconData(0xe18d, fontFamily: 'MaterialIcons'), + 'contactless': IconData(0xe18e, fontFamily: 'MaterialIcons'), + 'contacts': IconData(0xe18f, fontFamily: 'MaterialIcons'), + 'content_copy': IconData(0xe190, fontFamily: 'MaterialIcons'), + 'content_cut': IconData(0xe191, fontFamily: 'MaterialIcons'), + 'content_paste': IconData(0xe192, fontFamily: 'MaterialIcons'), + 'content_paste_go': IconData(0xf04d6, fontFamily: 'MaterialIcons'), + 'content_paste_off': IconData(0xe193, fontFamily: 'MaterialIcons'), + 'content_paste_search': IconData(0xf04d7, fontFamily: 'MaterialIcons'), + 'contrast': IconData(0xf04d8, fontFamily: 'MaterialIcons'), + 'control_camera': IconData(0xe194, fontFamily: 'MaterialIcons'), + 'control_point': IconData(0xe195, fontFamily: 'MaterialIcons'), + 'control_point_duplicate': IconData(0xe196, fontFamily: 'MaterialIcons'), + 'conveyor_belt': IconData(0xf0858, fontFamily: 'MaterialIcons'), + 'cookie': IconData(0xf04d9, fontFamily: 'MaterialIcons'), + 'copy': IconData(0xe190, fontFamily: 'MaterialIcons'), + 'copy_all': IconData(0xe197, fontFamily: 'MaterialIcons'), + 'copyright': IconData(0xe198, fontFamily: 'MaterialIcons'), + 'coronavirus': IconData(0xe199, fontFamily: 'MaterialIcons'), + 'corporate_fare': IconData(0xe19a, fontFamily: 'MaterialIcons'), + 'cottage': IconData(0xe19b, fontFamily: 'MaterialIcons'), + 'countertops': IconData(0xe19c, fontFamily: 'MaterialIcons'), + 'create': IconData(0xe19d, fontFamily: 'MaterialIcons'), + 'create_new_folder': IconData(0xe19e, fontFamily: 'MaterialIcons'), + 'credit_card': IconData(0xe19f, fontFamily: 'MaterialIcons'), + 'credit_card_off': IconData(0xe1a0, fontFamily: 'MaterialIcons'), + 'credit_score': IconData(0xe1a1, fontFamily: 'MaterialIcons'), + 'crib': IconData(0xe1a2, fontFamily: 'MaterialIcons'), + 'crisis_alert': IconData(0xf0794, fontFamily: 'MaterialIcons'), + 'crop': IconData(0xe1a3, fontFamily: 'MaterialIcons'), + 'crop_16_9': IconData(0xe1a4, fontFamily: 'MaterialIcons'), + 'crop_3_2': IconData(0xe1a5, fontFamily: 'MaterialIcons'), + 'crop_5_4': IconData(0xe1a6, fontFamily: 'MaterialIcons'), + 'crop_7_5': IconData(0xe1a7, fontFamily: 'MaterialIcons'), + 'crop_din': IconData(0xe1a8, fontFamily: 'MaterialIcons'), + 'crop_free': IconData(0xe1a9, fontFamily: 'MaterialIcons'), + 'crop_landscape': IconData(0xe1aa, fontFamily: 'MaterialIcons'), + 'crop_original': IconData(0xe1ab, fontFamily: 'MaterialIcons'), + 'crop_portrait': IconData(0xe1ac, fontFamily: 'MaterialIcons'), + 'crop_rotate': IconData(0xe1ad, fontFamily: 'MaterialIcons'), + 'crop_square': IconData(0xe1ae, fontFamily: 'MaterialIcons'), + 'cruelty_free': IconData(0xf04da, fontFamily: 'MaterialIcons'), + 'css': IconData(0xf04db, fontFamily: 'MaterialIcons'), + 'currency_bitcoin': IconData(0xf06bc, fontFamily: 'MaterialIcons'), + 'currency_exchange': IconData(0xf04dc, fontFamily: 'MaterialIcons'), + 'currency_franc': IconData(0xf04dd, fontFamily: 'MaterialIcons'), + 'currency_lira': IconData(0xf04de, fontFamily: 'MaterialIcons'), + 'currency_pound': IconData(0xf04df, fontFamily: 'MaterialIcons'), + 'currency_ruble': IconData(0xf04e0, fontFamily: 'MaterialIcons'), + 'currency_rupee': IconData(0xf04e1, fontFamily: 'MaterialIcons'), + 'currency_yen': IconData(0xf04e2, fontFamily: 'MaterialIcons'), + 'currency_yuan': IconData(0xf04e3, fontFamily: 'MaterialIcons'), + 'curtains': IconData(0xf0795, fontFamily: 'MaterialIcons'), + 'curtains_closed': IconData(0xf0796, fontFamily: 'MaterialIcons'), + 'cut': IconData(0xe191, fontFamily: 'MaterialIcons'), + 'cyclone': IconData(0xf0797, fontFamily: 'MaterialIcons'), + 'dangerous': IconData(0xe1af, fontFamily: 'MaterialIcons'), + 'dark_mode': IconData(0xe1b0, fontFamily: 'MaterialIcons'), + 'dashboard': IconData(0xe1b1, fontFamily: 'MaterialIcons'), + 'dashboard_customize': IconData(0xe1b2, fontFamily: 'MaterialIcons'), + 'data_array': IconData(0xf04e4, fontFamily: 'MaterialIcons'), + 'data_exploration': IconData(0xf04e5, fontFamily: 'MaterialIcons'), + 'data_object': IconData(0xf04e6, fontFamily: 'MaterialIcons'), + 'data_saver_off': IconData(0xe1b3, fontFamily: 'MaterialIcons'), + 'data_saver_on': IconData(0xe1b4, fontFamily: 'MaterialIcons'), + 'data_thresholding': IconData(0xf04e7, fontFamily: 'MaterialIcons'), + 'data_usage': IconData(0xe1b5, fontFamily: 'MaterialIcons'), + 'dataset': IconData(0xf0798, fontFamily: 'MaterialIcons'), + 'dataset_linked': IconData(0xf0799, fontFamily: 'MaterialIcons'), + 'date_range': IconData(0xe1b6, fontFamily: 'MaterialIcons'), + 'deblur': IconData(0xf04e8, fontFamily: 'MaterialIcons'), + 'deck': IconData(0xe1b7, fontFamily: 'MaterialIcons'), + 'dehaze': IconData(0xe1b8, fontFamily: 'MaterialIcons'), + 'delete': IconData(0xe1b9, fontFamily: 'MaterialIcons'), + 'delete_forever': IconData(0xe1ba, fontFamily: 'MaterialIcons'), + 'delete_outline': IconData(0xe1bb, fontFamily: 'MaterialIcons'), + 'delete_sweep': IconData(0xe1bc, fontFamily: 'MaterialIcons'), + 'delivery_dining': IconData(0xe1bd, fontFamily: 'MaterialIcons'), + 'density_large': IconData(0xf04e9, fontFamily: 'MaterialIcons'), + 'density_medium': IconData(0xf04ea, fontFamily: 'MaterialIcons'), + 'density_small': IconData(0xf04eb, fontFamily: 'MaterialIcons'), + 'departure_board': IconData(0xe1be, fontFamily: 'MaterialIcons'), + 'description': IconData(0xe1bf, fontFamily: 'MaterialIcons'), + 'deselect': IconData(0xf04ec, fontFamily: 'MaterialIcons'), + 'design_services': IconData(0xe1c0, fontFamily: 'MaterialIcons'), + 'desk': IconData(0xf079a, fontFamily: 'MaterialIcons'), + 'desktop_access_disabled': IconData(0xe1c1, fontFamily: 'MaterialIcons'), + 'desktop_mac': IconData(0xe1c2, fontFamily: 'MaterialIcons'), + 'desktop_windows': IconData(0xe1c3, fontFamily: 'MaterialIcons'), + 'details': IconData(0xe1c4, fontFamily: 'MaterialIcons'), + 'developer_board': IconData(0xe1c5, fontFamily: 'MaterialIcons'), + 'developer_board_off': IconData(0xe1c6, fontFamily: 'MaterialIcons'), + 'developer_mode': IconData(0xe1c7, fontFamily: 'MaterialIcons'), + 'device_hub': IconData(0xe1c8, fontFamily: 'MaterialIcons'), + 'device_thermostat': IconData(0xe1c9, fontFamily: 'MaterialIcons'), + 'device_unknown': + IconData(0xe1ca, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'devices': IconData(0xe1cb, fontFamily: 'MaterialIcons'), + 'devices_fold': IconData(0xf079b, fontFamily: 'MaterialIcons'), + 'devices_other': IconData(0xe1cc, fontFamily: 'MaterialIcons'), + 'dew_point': IconData(0xf0859, fontFamily: 'MaterialIcons'), + 'dialer_sip': IconData(0xe1cd, fontFamily: 'MaterialIcons'), + 'dialpad': IconData(0xe1ce, fontFamily: 'MaterialIcons'), + 'diamond': IconData(0xf04ed, fontFamily: 'MaterialIcons'), + 'difference': IconData(0xf04ee, fontFamily: 'MaterialIcons'), + 'dining': IconData(0xe1cf, fontFamily: 'MaterialIcons'), + 'dinner_dining': IconData(0xe1d0, fontFamily: 'MaterialIcons'), + 'directions': IconData(0xe1d1, fontFamily: 'MaterialIcons'), + 'directions_bike': IconData(0xe1d2, fontFamily: 'MaterialIcons'), + 'directions_boat': IconData(0xe1d3, fontFamily: 'MaterialIcons'), + 'directions_boat_filled': IconData(0xe1d4, fontFamily: 'MaterialIcons'), + 'directions_bus': IconData(0xe1d5, fontFamily: 'MaterialIcons'), + 'directions_bus_filled': IconData(0xe1d6, fontFamily: 'MaterialIcons'), + 'directions_car': IconData(0xe1d7, fontFamily: 'MaterialIcons'), + 'directions_car_filled': IconData(0xe1d8, fontFamily: 'MaterialIcons'), + 'directions_ferry': IconData(0xe1d3, fontFamily: 'MaterialIcons'), + 'directions_off': IconData(0xe1d9, fontFamily: 'MaterialIcons'), + 'directions_railway': IconData(0xe1da, fontFamily: 'MaterialIcons'), + 'directions_railway_filled': IconData(0xe1db, fontFamily: 'MaterialIcons'), + 'directions_run': IconData(0xe1dc, fontFamily: 'MaterialIcons'), + 'directions_subway': IconData(0xe1dd, fontFamily: 'MaterialIcons'), + 'directions_subway_filled': IconData(0xe1de, fontFamily: 'MaterialIcons'), + 'directions_train': IconData(0xe1da, fontFamily: 'MaterialIcons'), + 'directions_transit': IconData(0xe1df, fontFamily: 'MaterialIcons'), + 'directions_transit_filled': IconData(0xe1e0, fontFamily: 'MaterialIcons'), + 'directions_walk': IconData(0xe1e1, fontFamily: 'MaterialIcons'), + 'dirty_lens': IconData(0xe1e2, fontFamily: 'MaterialIcons'), + 'disabled_by_default': IconData(0xe1e3, fontFamily: 'MaterialIcons'), + 'disabled_visible': IconData(0xf04ef, fontFamily: 'MaterialIcons'), + 'disc_full': IconData(0xe1e4, fontFamily: 'MaterialIcons'), + 'discord': IconData(0xf04f0, fontFamily: 'MaterialIcons'), + 'discount': IconData(0xf06bd, fontFamily: 'MaterialIcons'), + 'display_settings': IconData(0xf04f1, fontFamily: 'MaterialIcons'), + 'diversity_1': IconData(0xf085a, fontFamily: 'MaterialIcons'), + 'diversity_2': IconData(0xf085b, fontFamily: 'MaterialIcons'), + 'diversity_3': IconData(0xf085c, fontFamily: 'MaterialIcons'), + 'dnd_forwardslash': IconData(0xe1eb, fontFamily: 'MaterialIcons'), + 'dns': IconData(0xe1e5, fontFamily: 'MaterialIcons'), + 'do_disturb': IconData(0xe1e6, fontFamily: 'MaterialIcons'), + 'do_disturb_alt': IconData(0xe1e7, fontFamily: 'MaterialIcons'), + 'do_disturb_off': IconData(0xe1e8, fontFamily: 'MaterialIcons'), + 'do_disturb_on': IconData(0xe1e9, fontFamily: 'MaterialIcons'), + 'do_not_disturb': IconData(0xe1ea, fontFamily: 'MaterialIcons'), + 'do_not_disturb_alt': IconData(0xe1eb, fontFamily: 'MaterialIcons'), + 'do_not_disturb_off': IconData(0xe1ec, fontFamily: 'MaterialIcons'), + 'do_not_disturb_on': IconData(0xe1ed, fontFamily: 'MaterialIcons'), + 'do_not_disturb_on_total_silence': + IconData(0xe1ee, fontFamily: 'MaterialIcons'), + 'do_not_step': IconData(0xe1ef, fontFamily: 'MaterialIcons'), + 'do_not_touch': IconData(0xe1f0, fontFamily: 'MaterialIcons'), + 'dock': IconData(0xe1f1, fontFamily: 'MaterialIcons'), + 'document_scanner': IconData(0xe1f2, fontFamily: 'MaterialIcons'), + 'domain': IconData(0xe1f3, fontFamily: 'MaterialIcons'), + 'domain_add': IconData(0xf04f2, fontFamily: 'MaterialIcons'), + 'domain_disabled': IconData(0xe1f4, fontFamily: 'MaterialIcons'), + 'domain_verification': IconData(0xe1f5, fontFamily: 'MaterialIcons'), + 'done': IconData(0xe1f6, fontFamily: 'MaterialIcons'), + 'done_all': IconData(0xe1f7, fontFamily: 'MaterialIcons'), + 'done_outline': IconData(0xe1f8, fontFamily: 'MaterialIcons'), + 'donut_large': IconData(0xe1f9, fontFamily: 'MaterialIcons'), + 'donut_small': IconData(0xe1fa, fontFamily: 'MaterialIcons'), + 'door_back': IconData(0xe1fb, fontFamily: 'MaterialIcons'), + 'door_front': IconData(0xe1fc, fontFamily: 'MaterialIcons'), + 'door_sliding': IconData(0xe1fd, fontFamily: 'MaterialIcons'), + 'doorbell': IconData(0xe1fe, fontFamily: 'MaterialIcons'), + 'double_arrow': IconData(0xe1ff, fontFamily: 'MaterialIcons'), + 'downhill_skiing': IconData(0xe200, fontFamily: 'MaterialIcons'), + 'download': IconData(0xe201, fontFamily: 'MaterialIcons'), + 'download_done': IconData(0xe202, fontFamily: 'MaterialIcons'), + 'download_for_offline': IconData(0xe203, fontFamily: 'MaterialIcons'), + 'downloading': IconData(0xe204, fontFamily: 'MaterialIcons'), + 'drafts': IconData(0xe205, fontFamily: 'MaterialIcons'), + 'drag_handle': IconData(0xe206, fontFamily: 'MaterialIcons'), + 'drag_indicator': IconData(0xe207, fontFamily: 'MaterialIcons'), + 'draw': IconData(0xf04f3, fontFamily: 'MaterialIcons'), + 'drive_eta': IconData(0xe208, fontFamily: 'MaterialIcons'), + 'drive_file_move': IconData(0xe209, fontFamily: 'MaterialIcons'), + 'drive_file_move_outline': IconData(0xe20a, fontFamily: 'MaterialIcons'), + 'drive_file_move_rtl': IconData(0xf04f4, fontFamily: 'MaterialIcons'), + 'drive_file_rename_outline': IconData(0xe20b, fontFamily: 'MaterialIcons'), + 'drive_folder_upload': IconData(0xe20c, fontFamily: 'MaterialIcons'), + 'dry': IconData(0xe20d, fontFamily: 'MaterialIcons'), + 'dry_cleaning': IconData(0xe20e, fontFamily: 'MaterialIcons'), + 'duo': IconData(0xe20f, fontFamily: 'MaterialIcons'), + 'dvr': + IconData(0xe210, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'dynamic_feed': IconData(0xe211, fontFamily: 'MaterialIcons'), + 'dynamic_form': IconData(0xe212, fontFamily: 'MaterialIcons'), + 'e_mobiledata': IconData(0xe213, fontFamily: 'MaterialIcons'), + 'earbuds': IconData(0xe214, fontFamily: 'MaterialIcons'), + 'earbuds_battery': IconData(0xe215, fontFamily: 'MaterialIcons'), + 'east': IconData(0xe216, fontFamily: 'MaterialIcons'), + 'eco': IconData(0xe217, fontFamily: 'MaterialIcons'), + 'edgesensor_high': IconData(0xe218, fontFamily: 'MaterialIcons'), + 'edgesensor_low': IconData(0xe219, fontFamily: 'MaterialIcons'), + 'edit': IconData(0xe21a, fontFamily: 'MaterialIcons'), + 'edit_attributes': IconData(0xe21b, fontFamily: 'MaterialIcons'), + 'edit_calendar': IconData(0xf04f5, fontFamily: 'MaterialIcons'), + 'edit_document': IconData(0xf085d, fontFamily: 'MaterialIcons'), + 'edit_location': IconData(0xe21c, fontFamily: 'MaterialIcons'), + 'edit_location_alt': IconData(0xe21d, fontFamily: 'MaterialIcons'), + 'edit_note': IconData(0xf04f6, fontFamily: 'MaterialIcons'), + 'edit_notifications': IconData(0xe21e, fontFamily: 'MaterialIcons'), + 'edit_off': IconData(0xe21f, fontFamily: 'MaterialIcons'), + 'edit_road': IconData(0xe220, fontFamily: 'MaterialIcons'), + 'edit_square': IconData(0xf085e, fontFamily: 'MaterialIcons'), + 'egg': IconData(0xf04f8, fontFamily: 'MaterialIcons'), + 'egg_alt': IconData(0xf04f7, fontFamily: 'MaterialIcons'), + 'eject': IconData(0xe221, fontFamily: 'MaterialIcons'), + 'elderly': IconData(0xe222, fontFamily: 'MaterialIcons'), + 'elderly_woman': IconData(0xf04f9, fontFamily: 'MaterialIcons'), + 'electric_bike': IconData(0xe223, fontFamily: 'MaterialIcons'), + 'electric_bolt': IconData(0xf079c, fontFamily: 'MaterialIcons'), + 'electric_car': IconData(0xe224, fontFamily: 'MaterialIcons'), + 'electric_meter': IconData(0xf079d, fontFamily: 'MaterialIcons'), + 'electric_moped': IconData(0xe225, fontFamily: 'MaterialIcons'), + 'electric_rickshaw': IconData(0xe226, fontFamily: 'MaterialIcons'), + 'electric_scooter': IconData(0xe227, fontFamily: 'MaterialIcons'), + 'electrical_services': IconData(0xe228, fontFamily: 'MaterialIcons'), + 'elevator': IconData(0xe229, fontFamily: 'MaterialIcons'), + 'email': IconData(0xe22a, fontFamily: 'MaterialIcons'), + 'emergency': IconData(0xf04fa, fontFamily: 'MaterialIcons'), + 'emergency_recording': IconData(0xf079e, fontFamily: 'MaterialIcons'), + 'emergency_share': IconData(0xf079f, fontFamily: 'MaterialIcons'), + 'emoji_emotions': IconData(0xe22b, fontFamily: 'MaterialIcons'), + 'emoji_events': IconData(0xe22c, fontFamily: 'MaterialIcons'), + 'emoji_flags': IconData(0xe22d, fontFamily: 'MaterialIcons'), + 'emoji_food_beverage': IconData(0xe22e, fontFamily: 'MaterialIcons'), + 'emoji_nature': IconData(0xe22f, fontFamily: 'MaterialIcons'), + 'emoji_objects': IconData(0xe230, fontFamily: 'MaterialIcons'), + 'emoji_people': IconData(0xe231, fontFamily: 'MaterialIcons'), + 'emoji_symbols': IconData(0xe232, fontFamily: 'MaterialIcons'), + 'emoji_transportation': IconData(0xe233, fontFamily: 'MaterialIcons'), + 'energy_savings_leaf': IconData(0xf07a0, fontFamily: 'MaterialIcons'), + 'engineering': IconData(0xe234, fontFamily: 'MaterialIcons'), + 'enhance_photo_translate': IconData(0xe131, fontFamily: 'MaterialIcons'), + 'enhanced_encryption': IconData(0xe235, fontFamily: 'MaterialIcons'), + 'equalizer': IconData(0xe236, fontFamily: 'MaterialIcons'), + 'error': IconData(0xe237, fontFamily: 'MaterialIcons'), + 'error_outline': IconData(0xe238, fontFamily: 'MaterialIcons'), + 'escalator': IconData(0xe239, fontFamily: 'MaterialIcons'), + 'escalator_warning': IconData(0xe23a, fontFamily: 'MaterialIcons'), + 'euro': IconData(0xe23b, fontFamily: 'MaterialIcons'), + 'euro_symbol': IconData(0xe23c, fontFamily: 'MaterialIcons'), + 'ev_station': IconData(0xe23d, fontFamily: 'MaterialIcons'), + 'event': IconData(0xe23e, fontFamily: 'MaterialIcons'), + 'event_available': IconData(0xe23f, fontFamily: 'MaterialIcons'), + 'event_busy': IconData(0xe240, fontFamily: 'MaterialIcons'), + 'event_note': + IconData(0xe241, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'event_repeat': IconData(0xf04fb, fontFamily: 'MaterialIcons'), + 'event_seat': IconData(0xe242, fontFamily: 'MaterialIcons'), + 'exit_to_app': IconData(0xe243, fontFamily: 'MaterialIcons'), + 'expand': IconData(0xe244, fontFamily: 'MaterialIcons'), + 'expand_circle_down': IconData(0xf04fc, fontFamily: 'MaterialIcons'), + 'expand_less': IconData(0xe245, fontFamily: 'MaterialIcons'), + 'expand_more': IconData(0xe246, fontFamily: 'MaterialIcons'), + 'explicit': IconData(0xe247, fontFamily: 'MaterialIcons'), + 'explore': IconData(0xe248, fontFamily: 'MaterialIcons'), + 'explore_off': IconData(0xe249, fontFamily: 'MaterialIcons'), + 'exposure': IconData(0xe24a, fontFamily: 'MaterialIcons'), + 'exposure_minus_1': IconData(0xe24b, fontFamily: 'MaterialIcons'), + 'exposure_minus_2': IconData(0xe24c, fontFamily: 'MaterialIcons'), + 'exposure_neg_1': IconData(0xe24b, fontFamily: 'MaterialIcons'), + 'exposure_neg_2': IconData(0xe24c, fontFamily: 'MaterialIcons'), + 'exposure_plus_1': IconData(0xe24d, fontFamily: 'MaterialIcons'), + 'exposure_plus_2': IconData(0xe24e, fontFamily: 'MaterialIcons'), + 'exposure_zero': IconData(0xe24f, fontFamily: 'MaterialIcons'), + 'extension': IconData(0xe250, fontFamily: 'MaterialIcons'), + 'extension_off': IconData(0xe251, fontFamily: 'MaterialIcons'), + 'face': IconData(0xe252, fontFamily: 'MaterialIcons'), + 'face_2': IconData(0xf085f, fontFamily: 'MaterialIcons'), + 'face_3': IconData(0xf0860, fontFamily: 'MaterialIcons'), + 'face_4': IconData(0xf0861, fontFamily: 'MaterialIcons'), + 'face_5': IconData(0xf0862, fontFamily: 'MaterialIcons'), + 'face_6': IconData(0xf0863, fontFamily: 'MaterialIcons'), + 'face_retouching_natural': IconData(0xe253, fontFamily: 'MaterialIcons'), + 'face_retouching_off': IconData(0xe254, fontFamily: 'MaterialIcons'), + 'facebook': IconData(0xe255, fontFamily: 'MaterialIcons'), + 'fact_check': IconData(0xe256, fontFamily: 'MaterialIcons'), + 'factory': IconData(0xf04fd, fontFamily: 'MaterialIcons'), + 'family_restroom': IconData(0xe257, fontFamily: 'MaterialIcons'), + 'fast_forward': IconData(0xe258, fontFamily: 'MaterialIcons'), + 'fast_rewind': IconData(0xe259, fontFamily: 'MaterialIcons'), + 'fastfood': IconData(0xe25a, fontFamily: 'MaterialIcons'), + 'favorite': IconData(0xe25b, fontFamily: 'MaterialIcons'), + 'favorite_border': IconData(0xe25c, fontFamily: 'MaterialIcons'), + 'favorite_outline': IconData(0xe25c, fontFamily: 'MaterialIcons'), + 'fax': IconData(0xf04fe, fontFamily: 'MaterialIcons'), + 'featured_play_list': + IconData(0xe25d, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'featured_video': + IconData(0xe25e, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'feed': IconData(0xe25f, fontFamily: 'MaterialIcons'), + 'feedback': IconData(0xe260, fontFamily: 'MaterialIcons'), + 'female': IconData(0xe261, fontFamily: 'MaterialIcons'), + 'fence': IconData(0xe262, fontFamily: 'MaterialIcons'), + 'festival': IconData(0xe263, fontFamily: 'MaterialIcons'), + 'fiber_dvr': IconData(0xe264, fontFamily: 'MaterialIcons'), + 'fiber_manual_record': IconData(0xe265, fontFamily: 'MaterialIcons'), + 'fiber_new': IconData(0xe266, fontFamily: 'MaterialIcons'), + 'fiber_pin': IconData(0xe267, fontFamily: 'MaterialIcons'), + 'fiber_smart_record': IconData(0xe268, fontFamily: 'MaterialIcons'), + 'file_copy': IconData(0xe269, fontFamily: 'MaterialIcons'), + 'file_download': IconData(0xe26a, fontFamily: 'MaterialIcons'), + 'file_download_done': IconData(0xe26b, fontFamily: 'MaterialIcons'), + 'file_download_off': IconData(0xe26c, fontFamily: 'MaterialIcons'), + 'file_open': IconData(0xf04ff, fontFamily: 'MaterialIcons'), + 'file_present': IconData(0xe26d, fontFamily: 'MaterialIcons'), + 'file_upload': IconData(0xe26e, fontFamily: 'MaterialIcons'), + 'file_upload_off': IconData(0xf0864, fontFamily: 'MaterialIcons'), + 'filter': IconData(0xe26f, fontFamily: 'MaterialIcons'), + 'filter_1': IconData(0xe270, fontFamily: 'MaterialIcons'), + 'filter_2': IconData(0xe271, fontFamily: 'MaterialIcons'), + 'filter_3': IconData(0xe272, fontFamily: 'MaterialIcons'), + 'filter_4': IconData(0xe273, fontFamily: 'MaterialIcons'), + 'filter_5': IconData(0xe274, fontFamily: 'MaterialIcons'), + 'filter_6': IconData(0xe275, fontFamily: 'MaterialIcons'), + 'filter_7': IconData(0xe276, fontFamily: 'MaterialIcons'), + 'filter_8': IconData(0xe277, fontFamily: 'MaterialIcons'), + 'filter_9': IconData(0xe278, fontFamily: 'MaterialIcons'), + 'filter_9_plus': IconData(0xe279, fontFamily: 'MaterialIcons'), + 'filter_alt': IconData(0xe27a, fontFamily: 'MaterialIcons'), + 'filter_alt_off': IconData(0xf0500, fontFamily: 'MaterialIcons'), + 'filter_b_and_w': IconData(0xe27b, fontFamily: 'MaterialIcons'), + 'filter_center_focus': IconData(0xe27c, fontFamily: 'MaterialIcons'), + 'filter_drama': IconData(0xe27d, fontFamily: 'MaterialIcons'), + 'filter_frames': IconData(0xe27e, fontFamily: 'MaterialIcons'), + 'filter_hdr': IconData(0xe27f, fontFamily: 'MaterialIcons'), + 'filter_list': IconData(0xe280, fontFamily: 'MaterialIcons'), + 'filter_list_alt': IconData(0xe281, fontFamily: 'MaterialIcons'), + 'filter_list_off': IconData(0xf0501, fontFamily: 'MaterialIcons'), + 'filter_none': IconData(0xe282, fontFamily: 'MaterialIcons'), + 'filter_tilt_shift': IconData(0xe283, fontFamily: 'MaterialIcons'), + 'filter_vintage': IconData(0xe284, fontFamily: 'MaterialIcons'), + 'find_in_page': IconData(0xe285, fontFamily: 'MaterialIcons'), + 'find_replace': IconData(0xe286, fontFamily: 'MaterialIcons'), + 'fingerprint': IconData(0xe287, fontFamily: 'MaterialIcons'), + 'fire_extinguisher': IconData(0xe288, fontFamily: 'MaterialIcons'), + 'fire_hydrant': IconData(0xe289, fontFamily: 'MaterialIcons'), + 'fire_hydrant_alt': IconData(0xf07a1, fontFamily: 'MaterialIcons'), + 'fire_truck': IconData(0xf07a2, fontFamily: 'MaterialIcons'), + 'fireplace': IconData(0xe28a, fontFamily: 'MaterialIcons'), + 'first_page': + IconData(0xe28b, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'fit_screen': IconData(0xe28c, fontFamily: 'MaterialIcons'), + 'fitbit': IconData(0xf0502, fontFamily: 'MaterialIcons'), + 'fitness_center': IconData(0xe28d, fontFamily: 'MaterialIcons'), + 'flag': IconData(0xe28e, fontFamily: 'MaterialIcons'), + 'flag_circle': IconData(0xf0503, fontFamily: 'MaterialIcons'), + 'flaky': IconData(0xe28f, fontFamily: 'MaterialIcons'), + 'flare': IconData(0xe290, fontFamily: 'MaterialIcons'), + 'flash_auto': IconData(0xe291, fontFamily: 'MaterialIcons'), + 'flash_off': IconData(0xe292, fontFamily: 'MaterialIcons'), + 'flash_on': IconData(0xe293, fontFamily: 'MaterialIcons'), + 'flashlight_off': IconData(0xe294, fontFamily: 'MaterialIcons'), + 'flashlight_on': IconData(0xe295, fontFamily: 'MaterialIcons'), + 'flatware': IconData(0xe296, fontFamily: 'MaterialIcons'), + 'flight': IconData(0xe297, fontFamily: 'MaterialIcons'), + 'flight_class': IconData(0xf0504, fontFamily: 'MaterialIcons'), + 'flight_land': + IconData(0xe298, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'flight_takeoff': + IconData(0xe299, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'flip': IconData(0xe29a, fontFamily: 'MaterialIcons'), + 'flip_camera_android': IconData(0xe29b, fontFamily: 'MaterialIcons'), + 'flip_camera_ios': IconData(0xe29c, fontFamily: 'MaterialIcons'), + 'flip_to_back': IconData(0xe29d, fontFamily: 'MaterialIcons'), + 'flip_to_front': IconData(0xe29e, fontFamily: 'MaterialIcons'), + 'flood': IconData(0xf07a3, fontFamily: 'MaterialIcons'), + 'flourescent': IconData(0xf0865, fontFamily: 'MaterialIcons'), + 'fluorescent': IconData(0xf0865, fontFamily: 'MaterialIcons'), + 'flutter_dash': IconData(0xe2a0, fontFamily: 'MaterialIcons'), + 'fmd_bad': IconData(0xe2a1, fontFamily: 'MaterialIcons'), + 'fmd_good': IconData(0xe2a2, fontFamily: 'MaterialIcons'), + 'foggy': IconData(0xf0505, fontFamily: 'MaterialIcons'), + 'folder': IconData(0xe2a3, fontFamily: 'MaterialIcons'), + 'folder_copy': IconData(0xf0506, fontFamily: 'MaterialIcons'), + 'folder_delete': IconData(0xf0507, fontFamily: 'MaterialIcons'), + 'folder_off': IconData(0xf0508, fontFamily: 'MaterialIcons'), + 'folder_open': IconData(0xe2a4, fontFamily: 'MaterialIcons'), + 'folder_shared': IconData(0xe2a5, fontFamily: 'MaterialIcons'), + 'folder_special': IconData(0xe2a6, fontFamily: 'MaterialIcons'), + 'folder_zip': IconData(0xf0509, fontFamily: 'MaterialIcons'), + 'follow_the_signs': IconData(0xe2a7, fontFamily: 'MaterialIcons'), + 'font_download': IconData(0xe2a8, fontFamily: 'MaterialIcons'), + 'font_download_off': IconData(0xe2a9, fontFamily: 'MaterialIcons'), + 'food_bank': IconData(0xe2aa, fontFamily: 'MaterialIcons'), + 'forest': IconData(0xf050a, fontFamily: 'MaterialIcons'), + 'fork_left': IconData(0xf050b, fontFamily: 'MaterialIcons'), + 'fork_right': IconData(0xf050c, fontFamily: 'MaterialIcons'), + 'forklift': IconData(0xf0866, fontFamily: 'MaterialIcons'), + 'format_align_center': IconData(0xe2ab, fontFamily: 'MaterialIcons'), + 'format_align_justify': IconData(0xe2ac, fontFamily: 'MaterialIcons'), + 'format_align_left': IconData(0xe2ad, fontFamily: 'MaterialIcons'), + 'format_align_right': IconData(0xe2ae, fontFamily: 'MaterialIcons'), + 'format_bold': IconData(0xe2af, fontFamily: 'MaterialIcons'), + 'format_clear': IconData(0xe2b0, fontFamily: 'MaterialIcons'), + 'format_color_fill': IconData(0xe2b1, fontFamily: 'MaterialIcons'), + 'format_color_reset': IconData(0xe2b2, fontFamily: 'MaterialIcons'), + 'format_color_text': IconData(0xe2b3, fontFamily: 'MaterialIcons'), + 'format_indent_decrease': + IconData(0xe2b4, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'format_indent_increase': + IconData(0xe2b5, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'format_italic': IconData(0xe2b6, fontFamily: 'MaterialIcons'), + 'format_line_spacing': IconData(0xe2b7, fontFamily: 'MaterialIcons'), + 'format_list_bulleted': + IconData(0xe2b8, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'format_list_bulleted_add': IconData(0xf0867, fontFamily: 'MaterialIcons'), + 'format_list_numbered': IconData(0xe2b9, fontFamily: 'MaterialIcons'), + 'format_list_numbered_rtl': IconData(0xe2ba, fontFamily: 'MaterialIcons'), + 'format_overline': IconData(0xf050d, fontFamily: 'MaterialIcons'), + 'format_paint': IconData(0xe2bb, fontFamily: 'MaterialIcons'), + 'format_quote': IconData(0xe2bc, fontFamily: 'MaterialIcons'), + 'format_shapes': IconData(0xe2bd, fontFamily: 'MaterialIcons'), + 'format_size': IconData(0xe2be, fontFamily: 'MaterialIcons'), + 'format_strikethrough': IconData(0xe2bf, fontFamily: 'MaterialIcons'), + 'format_textdirection_l_to_r': IconData(0xe2c0, fontFamily: 'MaterialIcons'), + 'format_textdirection_r_to_l': IconData(0xe2c1, fontFamily: 'MaterialIcons'), + 'format_underline': IconData(0xe2c2, fontFamily: 'MaterialIcons'), + 'format_underlined': IconData(0xe2c2, fontFamily: 'MaterialIcons'), + 'fort': IconData(0xf050e, fontFamily: 'MaterialIcons'), + 'forum': IconData(0xe2c3, fontFamily: 'MaterialIcons'), + 'forward': + IconData(0xe2c4, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'forward_10': IconData(0xe2c5, fontFamily: 'MaterialIcons'), + 'forward_30': IconData(0xe2c6, fontFamily: 'MaterialIcons'), + 'forward_5': IconData(0xe2c7, fontFamily: 'MaterialIcons'), + 'forward_to_inbox': IconData(0xe2c8, fontFamily: 'MaterialIcons'), + 'foundation': IconData(0xe2c9, fontFamily: 'MaterialIcons'), + 'free_breakfast': IconData(0xe2ca, fontFamily: 'MaterialIcons'), + 'free_cancellation': IconData(0xf050f, fontFamily: 'MaterialIcons'), + 'front_hand': IconData(0xf0510, fontFamily: 'MaterialIcons'), + 'front_loader': IconData(0xf0868, fontFamily: 'MaterialIcons'), + 'fullscreen': IconData(0xe2cb, fontFamily: 'MaterialIcons'), + 'fullscreen_exit': IconData(0xe2cc, fontFamily: 'MaterialIcons'), + 'functions': + IconData(0xe2cd, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'g_mobiledata': IconData(0xe2ce, fontFamily: 'MaterialIcons'), + 'g_translate': IconData(0xe2cf, fontFamily: 'MaterialIcons'), + 'gamepad': IconData(0xe2d0, fontFamily: 'MaterialIcons'), + 'games': IconData(0xe2d1, fontFamily: 'MaterialIcons'), + 'garage': IconData(0xe2d2, fontFamily: 'MaterialIcons'), + 'gas_meter': IconData(0xf07a4, fontFamily: 'MaterialIcons'), + 'gavel': IconData(0xe2d3, fontFamily: 'MaterialIcons'), + 'generating_tokens': IconData(0xf0511, fontFamily: 'MaterialIcons'), + 'gesture': IconData(0xe2d4, fontFamily: 'MaterialIcons'), + 'get_app': IconData(0xe2d5, fontFamily: 'MaterialIcons'), + 'gif': IconData(0xe2d6, fontFamily: 'MaterialIcons'), + 'gif_box': IconData(0xf0512, fontFamily: 'MaterialIcons'), + 'girl': IconData(0xf0513, fontFamily: 'MaterialIcons'), + 'gite': IconData(0xe2d7, fontFamily: 'MaterialIcons'), + 'golf_course': IconData(0xe2d8, fontFamily: 'MaterialIcons'), + 'gpp_bad': IconData(0xe2d9, fontFamily: 'MaterialIcons'), + 'gpp_good': IconData(0xe2da, fontFamily: 'MaterialIcons'), + 'gpp_maybe': IconData(0xe2db, fontFamily: 'MaterialIcons'), + 'gps_fixed': IconData(0xe2dc, fontFamily: 'MaterialIcons'), + 'gps_not_fixed': IconData(0xe2dd, fontFamily: 'MaterialIcons'), + 'gps_off': IconData(0xe2de, fontFamily: 'MaterialIcons'), + 'grade': IconData(0xe2df, fontFamily: 'MaterialIcons'), + 'gradient': IconData(0xe2e0, fontFamily: 'MaterialIcons'), + 'grading': IconData(0xe2e1, fontFamily: 'MaterialIcons'), + 'grain': IconData(0xe2e2, fontFamily: 'MaterialIcons'), + 'graphic_eq': IconData(0xe2e3, fontFamily: 'MaterialIcons'), + 'grass': IconData(0xe2e4, fontFamily: 'MaterialIcons'), + 'grid_3x3': IconData(0xe2e5, fontFamily: 'MaterialIcons'), + 'grid_4x4': IconData(0xe2e6, fontFamily: 'MaterialIcons'), + 'grid_goldenratio': IconData(0xe2e7, fontFamily: 'MaterialIcons'), + 'grid_off': IconData(0xe2e8, fontFamily: 'MaterialIcons'), + 'grid_on': IconData(0xe2e9, fontFamily: 'MaterialIcons'), + 'grid_view': IconData(0xe2ea, fontFamily: 'MaterialIcons'), + 'group': IconData(0xe2eb, fontFamily: 'MaterialIcons'), + 'group_add': IconData(0xe2ec, fontFamily: 'MaterialIcons'), + 'group_off': IconData(0xf0514, fontFamily: 'MaterialIcons'), + 'group_remove': IconData(0xf0515, fontFamily: 'MaterialIcons'), + 'group_work': IconData(0xe2ed, fontFamily: 'MaterialIcons'), + 'groups': IconData(0xe2ee, fontFamily: 'MaterialIcons'), + 'groups_2': IconData(0xf0869, fontFamily: 'MaterialIcons'), + 'groups_3': IconData(0xf086a, fontFamily: 'MaterialIcons'), + 'h_mobiledata': IconData(0xe2ef, fontFamily: 'MaterialIcons'), + 'h_plus_mobiledata': IconData(0xe2f0, fontFamily: 'MaterialIcons'), + 'hail': IconData(0xe2f1, fontFamily: 'MaterialIcons'), + 'handshake': IconData(0xf06be, fontFamily: 'MaterialIcons'), + 'handyman': IconData(0xe2f2, fontFamily: 'MaterialIcons'), + 'hardware': IconData(0xe2f3, fontFamily: 'MaterialIcons'), + 'hd': IconData(0xe2f4, fontFamily: 'MaterialIcons'), + 'hdr_auto': IconData(0xe2f5, fontFamily: 'MaterialIcons'), + 'hdr_auto_select': IconData(0xe2f6, fontFamily: 'MaterialIcons'), + 'hdr_enhanced_select': IconData(0xe2f7, fontFamily: 'MaterialIcons'), + 'hdr_off': IconData(0xe2f8, fontFamily: 'MaterialIcons'), + 'hdr_off_select': IconData(0xe2f9, fontFamily: 'MaterialIcons'), + 'hdr_on': IconData(0xe2fa, fontFamily: 'MaterialIcons'), + 'hdr_on_select': IconData(0xe2fb, fontFamily: 'MaterialIcons'), + 'hdr_plus': IconData(0xe2fc, fontFamily: 'MaterialIcons'), + 'hdr_strong': IconData(0xe2fd, fontFamily: 'MaterialIcons'), + 'hdr_weak': IconData(0xe2fe, fontFamily: 'MaterialIcons'), + 'headphones': IconData(0xe2ff, fontFamily: 'MaterialIcons'), + 'headphones_battery': IconData(0xe300, fontFamily: 'MaterialIcons'), + 'headset': IconData(0xe301, fontFamily: 'MaterialIcons'), + 'headset_mic': IconData(0xe302, fontFamily: 'MaterialIcons'), + 'headset_off': IconData(0xe303, fontFamily: 'MaterialIcons'), + 'healing': IconData(0xe304, fontFamily: 'MaterialIcons'), + 'health_and_safety': IconData(0xe305, fontFamily: 'MaterialIcons'), + 'hearing': IconData(0xe306, fontFamily: 'MaterialIcons'), + 'hearing_disabled': IconData(0xe307, fontFamily: 'MaterialIcons'), + 'heart_broken': IconData(0xf0516, fontFamily: 'MaterialIcons'), + 'heat_pump': IconData(0xf07a5, fontFamily: 'MaterialIcons'), + 'height': IconData(0xe308, fontFamily: 'MaterialIcons'), + 'help': + IconData(0xe309, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'help_center': IconData(0xe30a, fontFamily: 'MaterialIcons'), + 'help_outline': + IconData(0xe30b, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'hevc': IconData(0xe30c, fontFamily: 'MaterialIcons'), + 'hexagon': IconData(0xf0517, fontFamily: 'MaterialIcons'), + 'hide_image': IconData(0xe30d, fontFamily: 'MaterialIcons'), + 'hide_source': IconData(0xe30e, fontFamily: 'MaterialIcons'), + 'high_quality': IconData(0xe30f, fontFamily: 'MaterialIcons'), + 'highlight': IconData(0xe310, fontFamily: 'MaterialIcons'), + 'highlight_alt': IconData(0xe311, fontFamily: 'MaterialIcons'), + 'highlight_off': IconData(0xe312, fontFamily: 'MaterialIcons'), + 'highlight_remove': IconData(0xe312, fontFamily: 'MaterialIcons'), + 'hiking': IconData(0xe313, fontFamily: 'MaterialIcons'), + 'history': IconData(0xe314, fontFamily: 'MaterialIcons'), + 'history_edu': IconData(0xe315, fontFamily: 'MaterialIcons'), + 'history_toggle_off': IconData(0xe316, fontFamily: 'MaterialIcons'), + 'hive': IconData(0xf0518, fontFamily: 'MaterialIcons'), + 'hls': IconData(0xf0519, fontFamily: 'MaterialIcons'), + 'hls_off': IconData(0xf051a, fontFamily: 'MaterialIcons'), + 'holiday_village': IconData(0xe317, fontFamily: 'MaterialIcons'), + 'home': IconData(0xe318, fontFamily: 'MaterialIcons'), + 'home_filled': IconData(0xe319, fontFamily: 'MaterialIcons'), + 'home_max': IconData(0xe31a, fontFamily: 'MaterialIcons'), + 'home_mini': IconData(0xe31b, fontFamily: 'MaterialIcons'), + 'home_repair_service': IconData(0xe31c, fontFamily: 'MaterialIcons'), + 'home_work': IconData(0xe31d, fontFamily: 'MaterialIcons'), + 'horizontal_distribute': IconData(0xe31e, fontFamily: 'MaterialIcons'), + 'horizontal_rule': IconData(0xe31f, fontFamily: 'MaterialIcons'), + 'horizontal_split': IconData(0xe320, fontFamily: 'MaterialIcons'), + 'hot_tub': IconData(0xe321, fontFamily: 'MaterialIcons'), + 'hotel': IconData(0xe322, fontFamily: 'MaterialIcons'), + 'hotel_class': IconData(0xf051b, fontFamily: 'MaterialIcons'), + 'hourglass_bottom': IconData(0xe323, fontFamily: 'MaterialIcons'), + 'hourglass_disabled': IconData(0xe324, fontFamily: 'MaterialIcons'), + 'hourglass_empty': IconData(0xe325, fontFamily: 'MaterialIcons'), + 'hourglass_full': IconData(0xe326, fontFamily: 'MaterialIcons'), + 'hourglass_top': IconData(0xe327, fontFamily: 'MaterialIcons'), + 'house': IconData(0xe328, fontFamily: 'MaterialIcons'), + 'house_siding': IconData(0xe329, fontFamily: 'MaterialIcons'), + 'houseboat': IconData(0xe32a, fontFamily: 'MaterialIcons'), + 'how_to_reg': IconData(0xe32b, fontFamily: 'MaterialIcons'), + 'how_to_vote': IconData(0xe32c, fontFamily: 'MaterialIcons'), + 'html': IconData(0xf051c, fontFamily: 'MaterialIcons'), + 'http': IconData(0xe32d, fontFamily: 'MaterialIcons'), + 'https': IconData(0xe32e, fontFamily: 'MaterialIcons'), + 'hub': IconData(0xf051d, fontFamily: 'MaterialIcons'), + 'hvac': IconData(0xe32f, fontFamily: 'MaterialIcons'), + 'ice_skating': IconData(0xe330, fontFamily: 'MaterialIcons'), + 'icecream': IconData(0xe331, fontFamily: 'MaterialIcons'), + 'image': IconData(0xe332, fontFamily: 'MaterialIcons'), + 'image_aspect_ratio': IconData(0xe333, fontFamily: 'MaterialIcons'), + 'image_not_supported': IconData(0xe334, fontFamily: 'MaterialIcons'), + 'image_search': IconData(0xe335, fontFamily: 'MaterialIcons'), + 'imagesearch_roller': IconData(0xe336, fontFamily: 'MaterialIcons'), + 'import_contacts': IconData(0xe337, fontFamily: 'MaterialIcons'), + 'import_export': IconData(0xe338, fontFamily: 'MaterialIcons'), + 'important_devices': IconData(0xe339, fontFamily: 'MaterialIcons'), + 'inbox': IconData(0xe33a, fontFamily: 'MaterialIcons'), + 'incomplete_circle': IconData(0xf051e, fontFamily: 'MaterialIcons'), + 'indeterminate_check_box': IconData(0xe33b, fontFamily: 'MaterialIcons'), + 'info': IconData(0xe33c, fontFamily: 'MaterialIcons'), + 'info_outline': IconData(0xe33d, fontFamily: 'MaterialIcons'), + 'input': + IconData(0xe33e, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'insert_chart': IconData(0xe33f, fontFamily: 'MaterialIcons'), + 'insert_comment': IconData(0xe341, fontFamily: 'MaterialIcons'), + 'insert_drive_file': IconData(0xe342, fontFamily: 'MaterialIcons'), + 'insert_emoticon': IconData(0xe343, fontFamily: 'MaterialIcons'), + 'insert_invitation': IconData(0xe344, fontFamily: 'MaterialIcons'), + 'insert_link': IconData(0xe345, fontFamily: 'MaterialIcons'), + 'insert_page_break': IconData(0xf0520, fontFamily: 'MaterialIcons'), + 'insert_photo': IconData(0xe346, fontFamily: 'MaterialIcons'), + 'insights': IconData(0xe347, fontFamily: 'MaterialIcons'), + 'install_desktop': IconData(0xf0521, fontFamily: 'MaterialIcons'), + 'install_mobile': IconData(0xf0522, fontFamily: 'MaterialIcons'), + 'integration_instructions': IconData(0xe348, fontFamily: 'MaterialIcons'), + 'interests': IconData(0xf0523, fontFamily: 'MaterialIcons'), + 'interpreter_mode': IconData(0xf0524, fontFamily: 'MaterialIcons'), + 'inventory': IconData(0xe349, fontFamily: 'MaterialIcons'), + 'inventory_2': IconData(0xe34a, fontFamily: 'MaterialIcons'), + 'invert_colors': IconData(0xe34b, fontFamily: 'MaterialIcons'), + 'invert_colors_off': IconData(0xe34c, fontFamily: 'MaterialIcons'), + 'invert_colors_on': IconData(0xe34b, fontFamily: 'MaterialIcons'), + 'ios_share': IconData(0xe34d, fontFamily: 'MaterialIcons'), + 'iron': IconData(0xe34e, fontFamily: 'MaterialIcons'), + 'iso': IconData(0xe34f, fontFamily: 'MaterialIcons'), + 'javascript': IconData(0xf0525, fontFamily: 'MaterialIcons'), + 'join_full': IconData(0xf0526, fontFamily: 'MaterialIcons'), + 'join_inner': IconData(0xf0527, fontFamily: 'MaterialIcons'), + 'join_left': IconData(0xf0528, fontFamily: 'MaterialIcons'), + 'join_right': IconData(0xf0529, fontFamily: 'MaterialIcons'), + 'kayaking': IconData(0xe350, fontFamily: 'MaterialIcons'), + 'kebab_dining': IconData(0xf052a, fontFamily: 'MaterialIcons'), + 'key': IconData(0xf052b, fontFamily: 'MaterialIcons'), + 'key_off': IconData(0xf052c, fontFamily: 'MaterialIcons'), + 'keyboard': IconData(0xe351, fontFamily: 'MaterialIcons'), + 'keyboard_alt': IconData(0xe352, fontFamily: 'MaterialIcons'), + 'keyboard_arrow_down': IconData(0xe353, fontFamily: 'MaterialIcons'), + 'keyboard_arrow_left': IconData(0xe354, fontFamily: 'MaterialIcons'), + 'keyboard_arrow_right': IconData(0xe355, fontFamily: 'MaterialIcons'), + 'keyboard_arrow_up': IconData(0xe356, fontFamily: 'MaterialIcons'), + 'keyboard_backspace': + IconData(0xe357, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'keyboard_capslock': IconData(0xe358, fontFamily: 'MaterialIcons'), + 'keyboard_command_key': IconData(0xf052d, fontFamily: 'MaterialIcons'), + 'keyboard_control': IconData(0xe402, fontFamily: 'MaterialIcons'), + 'keyboard_control_key': IconData(0xf052e, fontFamily: 'MaterialIcons'), + 'keyboard_double_arrow_down': IconData(0xf052f, fontFamily: 'MaterialIcons'), + 'keyboard_double_arrow_left': IconData(0xf0530, fontFamily: 'MaterialIcons'), + 'keyboard_double_arrow_right': IconData(0xf0531, fontFamily: 'MaterialIcons'), + 'keyboard_double_arrow_up': IconData(0xf0532, fontFamily: 'MaterialIcons'), + 'keyboard_hide': IconData(0xe359, fontFamily: 'MaterialIcons'), + 'keyboard_option_key': IconData(0xf0533, fontFamily: 'MaterialIcons'), + 'keyboard_return': IconData(0xe35a, fontFamily: 'MaterialIcons'), + 'keyboard_tab': + IconData(0xe35b, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'keyboard_voice': IconData(0xe35c, fontFamily: 'MaterialIcons'), + 'king_bed': IconData(0xe35d, fontFamily: 'MaterialIcons'), + 'kitchen': IconData(0xe35e, fontFamily: 'MaterialIcons'), + 'kitesurfing': IconData(0xe35f, fontFamily: 'MaterialIcons'), + 'label': + IconData(0xe360, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'label_important': + IconData(0xe361, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'label_important_outline': IconData(0xe362, fontFamily: 'MaterialIcons'), + 'label_off': + IconData(0xe363, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'label_outline': + IconData(0xe364, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'lan': IconData(0xf0534, fontFamily: 'MaterialIcons'), + 'landscape': IconData(0xe365, fontFamily: 'MaterialIcons'), + 'landslide': IconData(0xf07a6, fontFamily: 'MaterialIcons'), + 'language': IconData(0xe366, fontFamily: 'MaterialIcons'), + 'laptop': IconData(0xe367, fontFamily: 'MaterialIcons'), + 'laptop_chromebook': IconData(0xe368, fontFamily: 'MaterialIcons'), + 'laptop_mac': IconData(0xe369, fontFamily: 'MaterialIcons'), + 'laptop_windows': IconData(0xe36a, fontFamily: 'MaterialIcons'), + 'last_page': + IconData(0xe36b, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'launch': + IconData(0xe36c, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'layers': IconData(0xe36d, fontFamily: 'MaterialIcons'), + 'layers_clear': IconData(0xe36e, fontFamily: 'MaterialIcons'), + 'leaderboard': IconData(0xe36f, fontFamily: 'MaterialIcons'), + 'leak_add': IconData(0xe370, fontFamily: 'MaterialIcons'), + 'leak_remove': IconData(0xe371, fontFamily: 'MaterialIcons'), + 'leave_bags_at_home': IconData(0xe439, fontFamily: 'MaterialIcons'), + 'legend_toggle': IconData(0xe372, fontFamily: 'MaterialIcons'), + 'lens': IconData(0xe373, fontFamily: 'MaterialIcons'), + 'lens_blur': IconData(0xe374, fontFamily: 'MaterialIcons'), + 'library_add': IconData(0xe375, fontFamily: 'MaterialIcons'), + 'library_add_check': IconData(0xe376, fontFamily: 'MaterialIcons'), + 'library_books': IconData(0xe377, fontFamily: 'MaterialIcons'), + 'library_music': IconData(0xe378, fontFamily: 'MaterialIcons'), + 'light': IconData(0xe379, fontFamily: 'MaterialIcons'), + 'light_mode': IconData(0xe37a, fontFamily: 'MaterialIcons'), + 'lightbulb': IconData(0xe37b, fontFamily: 'MaterialIcons'), + 'lightbulb_circle': IconData(0xf07a7, fontFamily: 'MaterialIcons'), + 'lightbulb_outline': IconData(0xe37c, fontFamily: 'MaterialIcons'), + 'line_axis': IconData(0xf0535, fontFamily: 'MaterialIcons'), + 'line_style': IconData(0xe37d, fontFamily: 'MaterialIcons'), + 'line_weight': IconData(0xe37e, fontFamily: 'MaterialIcons'), + 'linear_scale': IconData(0xe37f, fontFamily: 'MaterialIcons'), + 'link': IconData(0xe380, fontFamily: 'MaterialIcons'), + 'link_off': IconData(0xe381, fontFamily: 'MaterialIcons'), + 'linked_camera': IconData(0xe382, fontFamily: 'MaterialIcons'), + 'liquor': IconData(0xe383, fontFamily: 'MaterialIcons'), + 'list': + IconData(0xe384, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'list_alt': + IconData(0xe385, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'live_help': + IconData(0xe386, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'live_tv': IconData(0xe387, fontFamily: 'MaterialIcons'), + 'living': IconData(0xe388, fontFamily: 'MaterialIcons'), + 'local_activity': IconData(0xe389, fontFamily: 'MaterialIcons'), + 'local_airport': IconData(0xe38a, fontFamily: 'MaterialIcons'), + 'local_atm': IconData(0xe38b, fontFamily: 'MaterialIcons'), + 'local_attraction': IconData(0xe389, fontFamily: 'MaterialIcons'), + 'local_bar': IconData(0xe38c, fontFamily: 'MaterialIcons'), + 'local_cafe': IconData(0xe38d, fontFamily: 'MaterialIcons'), + 'local_car_wash': IconData(0xe38e, fontFamily: 'MaterialIcons'), + 'local_convenience_store': IconData(0xe38f, fontFamily: 'MaterialIcons'), + 'local_dining': IconData(0xe390, fontFamily: 'MaterialIcons'), + 'local_drink': IconData(0xe391, fontFamily: 'MaterialIcons'), + 'local_fire_department': IconData(0xe392, fontFamily: 'MaterialIcons'), + 'local_florist': IconData(0xe393, fontFamily: 'MaterialIcons'), + 'local_gas_station': IconData(0xe394, fontFamily: 'MaterialIcons'), + 'local_grocery_store': IconData(0xe395, fontFamily: 'MaterialIcons'), + 'local_hospital': IconData(0xe396, fontFamily: 'MaterialIcons'), + 'local_hotel': IconData(0xe397, fontFamily: 'MaterialIcons'), + 'local_laundry_service': IconData(0xe398, fontFamily: 'MaterialIcons'), + 'local_library': IconData(0xe399, fontFamily: 'MaterialIcons'), + 'local_mall': IconData(0xe39a, fontFamily: 'MaterialIcons'), + 'local_movies': IconData(0xe39b, fontFamily: 'MaterialIcons'), + 'local_offer': IconData(0xe39c, fontFamily: 'MaterialIcons'), + 'local_parking': IconData(0xe39d, fontFamily: 'MaterialIcons'), + 'local_pharmacy': IconData(0xe39e, fontFamily: 'MaterialIcons'), + 'local_phone': IconData(0xe39f, fontFamily: 'MaterialIcons'), + 'local_pizza': IconData(0xe3a0, fontFamily: 'MaterialIcons'), + 'local_play': IconData(0xe3a1, fontFamily: 'MaterialIcons'), + 'local_police': IconData(0xe3a2, fontFamily: 'MaterialIcons'), + 'local_post_office': IconData(0xe3a3, fontFamily: 'MaterialIcons'), + 'local_print_shop': IconData(0xe3a4, fontFamily: 'MaterialIcons'), + 'local_printshop': IconData(0xe3a4, fontFamily: 'MaterialIcons'), + 'local_restaurant': IconData(0xe390, fontFamily: 'MaterialIcons'), + 'local_see': IconData(0xe3a5, fontFamily: 'MaterialIcons'), + 'local_shipping': IconData(0xe3a6, fontFamily: 'MaterialIcons'), + 'local_taxi': IconData(0xe3a7, fontFamily: 'MaterialIcons'), + 'location_city': IconData(0xe3a8, fontFamily: 'MaterialIcons'), + 'location_disabled': IconData(0xe3a9, fontFamily: 'MaterialIcons'), + 'location_history': IconData(0xe498, fontFamily: 'MaterialIcons'), + 'location_off': IconData(0xe3aa, fontFamily: 'MaterialIcons'), + 'location_on': IconData(0xe3ab, fontFamily: 'MaterialIcons'), + 'location_pin': IconData(0xe3ac, fontFamily: 'MaterialIcons'), + 'location_searching': IconData(0xe3ad, fontFamily: 'MaterialIcons'), + 'lock': IconData(0xe3ae, fontFamily: 'MaterialIcons'), + 'lock_clock': IconData(0xe3af, fontFamily: 'MaterialIcons'), + 'lock_open': IconData(0xe3b0, fontFamily: 'MaterialIcons'), + 'lock_outline': IconData(0xe3b1, fontFamily: 'MaterialIcons'), + 'lock_person': IconData(0xf07a8, fontFamily: 'MaterialIcons'), + 'lock_reset': IconData(0xf0536, fontFamily: 'MaterialIcons'), + 'login': IconData(0xe3b2, fontFamily: 'MaterialIcons'), + 'logo_dev': IconData(0xf0537, fontFamily: 'MaterialIcons'), + 'logout': IconData(0xe3b3, fontFamily: 'MaterialIcons'), + 'looks': IconData(0xe3b4, fontFamily: 'MaterialIcons'), + 'looks_3': IconData(0xe3b5, fontFamily: 'MaterialIcons'), + 'looks_4': IconData(0xe3b6, fontFamily: 'MaterialIcons'), + 'looks_5': IconData(0xe3b7, fontFamily: 'MaterialIcons'), + 'looks_6': IconData(0xe3b8, fontFamily: 'MaterialIcons'), + 'looks_one': IconData(0xe3b9, fontFamily: 'MaterialIcons'), + 'looks_two': IconData(0xe3ba, fontFamily: 'MaterialIcons'), + 'loop': IconData(0xe3bb, fontFamily: 'MaterialIcons'), + 'loupe': IconData(0xe3bc, fontFamily: 'MaterialIcons'), + 'low_priority': IconData(0xe3bd, fontFamily: 'MaterialIcons'), + 'loyalty': IconData(0xe3be, fontFamily: 'MaterialIcons'), + 'lte_mobiledata': IconData(0xe3bf, fontFamily: 'MaterialIcons'), + 'lte_plus_mobiledata': IconData(0xe3c0, fontFamily: 'MaterialIcons'), + 'luggage': IconData(0xe3c1, fontFamily: 'MaterialIcons'), + 'lunch_dining': IconData(0xe3c2, fontFamily: 'MaterialIcons'), + 'lyrics': IconData(0xf07a9, fontFamily: 'MaterialIcons'), + 'macro_off': IconData(0xf086b, fontFamily: 'MaterialIcons'), + 'mail': IconData(0xe3c3, fontFamily: 'MaterialIcons'), + 'mail_lock': IconData(0xf07aa, fontFamily: 'MaterialIcons'), + 'mail_outline': IconData(0xe3c4, fontFamily: 'MaterialIcons'), + 'male': IconData(0xe3c5, fontFamily: 'MaterialIcons'), + 'man': IconData(0xf0538, fontFamily: 'MaterialIcons'), + 'man_2': IconData(0xf086c, fontFamily: 'MaterialIcons'), + 'man_3': IconData(0xf086d, fontFamily: 'MaterialIcons'), + 'man_4': IconData(0xf086e, fontFamily: 'MaterialIcons'), + 'manage_accounts': IconData(0xe3c6, fontFamily: 'MaterialIcons'), + 'manage_history': IconData(0xf07ab, fontFamily: 'MaterialIcons'), + 'manage_search': IconData(0xe3c7, fontFamily: 'MaterialIcons'), + 'map': IconData(0xe3c8, fontFamily: 'MaterialIcons'), + 'maps_home_work': IconData(0xe3c9, fontFamily: 'MaterialIcons'), + 'maps_ugc': IconData(0xe3ca, fontFamily: 'MaterialIcons'), + 'margin': IconData(0xe3cb, fontFamily: 'MaterialIcons'), + 'mark_as_unread': IconData(0xe3cc, fontFamily: 'MaterialIcons'), + 'mark_chat_read': IconData(0xe3cd, fontFamily: 'MaterialIcons'), + 'mark_chat_unread': IconData(0xe3ce, fontFamily: 'MaterialIcons'), + 'mark_email_read': IconData(0xe3cf, fontFamily: 'MaterialIcons'), + 'mark_email_unread': IconData(0xe3d0, fontFamily: 'MaterialIcons'), + 'mark_unread_chat_alt': IconData(0xf0539, fontFamily: 'MaterialIcons'), + 'markunread': IconData(0xe3d1, fontFamily: 'MaterialIcons'), + 'markunread_mailbox': IconData(0xe3d2, fontFamily: 'MaterialIcons'), + 'masks': IconData(0xe3d3, fontFamily: 'MaterialIcons'), + 'maximize': IconData(0xe3d4, fontFamily: 'MaterialIcons'), + 'media_bluetooth_off': IconData(0xe3d5, fontFamily: 'MaterialIcons'), + 'media_bluetooth_on': IconData(0xe3d6, fontFamily: 'MaterialIcons'), + 'mediation': IconData(0xe3d7, fontFamily: 'MaterialIcons'), + 'medical_information': IconData(0xf07ac, fontFamily: 'MaterialIcons'), + 'medical_services': IconData(0xe3d8, fontFamily: 'MaterialIcons'), + 'medication': IconData(0xe3d9, fontFamily: 'MaterialIcons'), + 'medication_liquid': IconData(0xf053a, fontFamily: 'MaterialIcons'), + 'meeting_room': IconData(0xe3da, fontFamily: 'MaterialIcons'), + 'memory': IconData(0xe3db, fontFamily: 'MaterialIcons'), + 'menu': IconData(0xe3dc, fontFamily: 'MaterialIcons'), + 'menu_book': IconData(0xe3dd, fontFamily: 'MaterialIcons'), + 'menu_open': IconData(0xe3de, fontFamily: 'MaterialIcons'), + 'merge': IconData(0xf053b, fontFamily: 'MaterialIcons'), + 'merge_type': IconData(0xe3df, fontFamily: 'MaterialIcons'), + 'message': IconData(0xe3e0, fontFamily: 'MaterialIcons'), + 'messenger': IconData(0xe154, fontFamily: 'MaterialIcons'), + 'messenger_outline': IconData(0xe155, fontFamily: 'MaterialIcons'), + 'mic': IconData(0xe3e1, fontFamily: 'MaterialIcons'), + 'mic_external_off': IconData(0xe3e2, fontFamily: 'MaterialIcons'), + 'mic_external_on': IconData(0xe3e3, fontFamily: 'MaterialIcons'), + 'mic_none': IconData(0xe3e4, fontFamily: 'MaterialIcons'), + 'mic_off': IconData(0xe3e5, fontFamily: 'MaterialIcons'), + 'microwave': IconData(0xe3e6, fontFamily: 'MaterialIcons'), + 'military_tech': IconData(0xe3e7, fontFamily: 'MaterialIcons'), + 'minimize': IconData(0xe3e8, fontFamily: 'MaterialIcons'), + 'minor_crash': IconData(0xf07ad, fontFamily: 'MaterialIcons'), + 'miscellaneous_services': IconData(0xe3e9, fontFamily: 'MaterialIcons'), + 'missed_video_call': IconData(0xe3ea, fontFamily: 'MaterialIcons'), + 'mms': IconData(0xe3eb, fontFamily: 'MaterialIcons'), + 'mobile_friendly': IconData(0xe3ec, fontFamily: 'MaterialIcons'), + 'mobile_off': IconData(0xe3ed, fontFamily: 'MaterialIcons'), + 'mobile_screen_share': + IconData(0xe3ee, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'mobiledata_off': IconData(0xe3ef, fontFamily: 'MaterialIcons'), + 'mode': IconData(0xe3f0, fontFamily: 'MaterialIcons'), + 'mode_comment': IconData(0xe3f1, fontFamily: 'MaterialIcons'), + 'mode_edit': IconData(0xe3f2, fontFamily: 'MaterialIcons'), + 'mode_edit_outline': IconData(0xe3f3, fontFamily: 'MaterialIcons'), + 'mode_fan_off': IconData(0xf07ae, fontFamily: 'MaterialIcons'), + 'mode_night': IconData(0xe3f4, fontFamily: 'MaterialIcons'), + 'mode_of_travel': IconData(0xf053c, fontFamily: 'MaterialIcons'), + 'mode_standby': IconData(0xe3f5, fontFamily: 'MaterialIcons'), + 'model_training': IconData(0xe3f6, fontFamily: 'MaterialIcons'), + 'monetization_on': IconData(0xe3f7, fontFamily: 'MaterialIcons'), + 'money': IconData(0xe3f8, fontFamily: 'MaterialIcons'), + 'money_off': IconData(0xe3f9, fontFamily: 'MaterialIcons'), + 'money_off_csred': IconData(0xe3fa, fontFamily: 'MaterialIcons'), + 'monitor': IconData(0xe3fb, fontFamily: 'MaterialIcons'), + 'monitor_heart': IconData(0xf053d, fontFamily: 'MaterialIcons'), + 'monitor_weight': IconData(0xe3fc, fontFamily: 'MaterialIcons'), + 'monochrome_photos': IconData(0xe3fd, fontFamily: 'MaterialIcons'), + 'mood': IconData(0xe3fe, fontFamily: 'MaterialIcons'), + 'mood_bad': IconData(0xe3ff, fontFamily: 'MaterialIcons'), + 'moped': IconData(0xe400, fontFamily: 'MaterialIcons'), + 'more': IconData(0xe401, fontFamily: 'MaterialIcons'), + 'more_horiz': IconData(0xe402, fontFamily: 'MaterialIcons'), + 'more_time': IconData(0xe403, fontFamily: 'MaterialIcons'), + 'more_vert': IconData(0xe404, fontFamily: 'MaterialIcons'), + 'mosque': IconData(0xf053e, fontFamily: 'MaterialIcons'), + 'motion_photos_auto': IconData(0xe405, fontFamily: 'MaterialIcons'), + 'motion_photos_off': IconData(0xe406, fontFamily: 'MaterialIcons'), + 'motion_photos_on': IconData(0xe407, fontFamily: 'MaterialIcons'), + 'motion_photos_pause': IconData(0xe408, fontFamily: 'MaterialIcons'), + 'motion_photos_paused': IconData(0xe409, fontFamily: 'MaterialIcons'), + 'motorcycle': IconData(0xe40a, fontFamily: 'MaterialIcons'), + 'mouse': IconData(0xe40b, fontFamily: 'MaterialIcons'), + 'move_down': IconData(0xf053f, fontFamily: 'MaterialIcons'), + 'move_to_inbox': IconData(0xe40c, fontFamily: 'MaterialIcons'), + 'move_up': IconData(0xf0540, fontFamily: 'MaterialIcons'), + 'movie': IconData(0xe40d, fontFamily: 'MaterialIcons'), + 'movie_creation': IconData(0xe40e, fontFamily: 'MaterialIcons'), + 'movie_edit': IconData(0xf08b9, fontFamily: 'MaterialIcons'), + 'movie_filter': IconData(0xe40f, fontFamily: 'MaterialIcons'), + 'moving': IconData(0xe410, fontFamily: 'MaterialIcons'), + 'mp': IconData(0xe411, fontFamily: 'MaterialIcons'), + 'multiline_chart': + IconData(0xe412, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'multiple_stop': IconData(0xe413, fontFamily: 'MaterialIcons'), + 'multitrack_audio': IconData(0xe2e3, fontFamily: 'MaterialIcons'), + 'museum': IconData(0xe414, fontFamily: 'MaterialIcons'), + 'music_note': IconData(0xe415, fontFamily: 'MaterialIcons'), + 'music_off': IconData(0xe416, fontFamily: 'MaterialIcons'), + 'music_video': IconData(0xe417, fontFamily: 'MaterialIcons'), + 'my_library_add': IconData(0xe375, fontFamily: 'MaterialIcons'), + 'my_library_books': IconData(0xe377, fontFamily: 'MaterialIcons'), + 'my_library_music': IconData(0xe378, fontFamily: 'MaterialIcons'), + 'my_location': IconData(0xe418, fontFamily: 'MaterialIcons'), + 'nat': IconData(0xe419, fontFamily: 'MaterialIcons'), + 'nature': IconData(0xe41a, fontFamily: 'MaterialIcons'), + 'nature_people': IconData(0xe41b, fontFamily: 'MaterialIcons'), + 'navigate_before': + IconData(0xe41c, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'navigate_next': + IconData(0xe41d, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'navigation': IconData(0xe41e, fontFamily: 'MaterialIcons'), + 'near_me': IconData(0xe41f, fontFamily: 'MaterialIcons'), + 'near_me_disabled': IconData(0xe420, fontFamily: 'MaterialIcons'), + 'nearby_error': IconData(0xe421, fontFamily: 'MaterialIcons'), + 'nearby_off': IconData(0xe422, fontFamily: 'MaterialIcons'), + 'nest_cam_wired_stand': IconData(0xf07af, fontFamily: 'MaterialIcons'), + 'network_cell': IconData(0xe423, fontFamily: 'MaterialIcons'), + 'network_check': IconData(0xe424, fontFamily: 'MaterialIcons'), + 'network_locked': IconData(0xe425, fontFamily: 'MaterialIcons'), + 'network_ping': IconData(0xf06bf, fontFamily: 'MaterialIcons'), + 'network_wifi': IconData(0xe426, fontFamily: 'MaterialIcons'), + 'network_wifi_1_bar': IconData(0xf07b0, fontFamily: 'MaterialIcons'), + 'network_wifi_2_bar': IconData(0xf07b1, fontFamily: 'MaterialIcons'), + 'network_wifi_3_bar': IconData(0xf07b2, fontFamily: 'MaterialIcons'), + 'new_label': IconData(0xe427, fontFamily: 'MaterialIcons'), + 'new_releases': IconData(0xe428, fontFamily: 'MaterialIcons'), + 'newspaper': IconData(0xf0541, fontFamily: 'MaterialIcons'), + 'next_plan': IconData(0xe429, fontFamily: 'MaterialIcons'), + 'next_week': + IconData(0xe42a, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'nfc': IconData(0xe42b, fontFamily: 'MaterialIcons'), + 'night_shelter': IconData(0xe42c, fontFamily: 'MaterialIcons'), + 'nightlife': IconData(0xe42d, fontFamily: 'MaterialIcons'), + 'nightlight': IconData(0xe42e, fontFamily: 'MaterialIcons'), + 'nights_stay': IconData(0xe430, fontFamily: 'MaterialIcons'), + 'no_accounts': IconData(0xe431, fontFamily: 'MaterialIcons'), + 'no_adult_content': IconData(0xf07b3, fontFamily: 'MaterialIcons'), + 'no_backpack': IconData(0xe432, fontFamily: 'MaterialIcons'), + 'no_cell': IconData(0xe433, fontFamily: 'MaterialIcons'), + 'no_crash': IconData(0xf07b4, fontFamily: 'MaterialIcons'), + 'no_drinks': IconData(0xe434, fontFamily: 'MaterialIcons'), + 'no_encryption': IconData(0xe435, fontFamily: 'MaterialIcons'), + 'no_encryption_gmailerrorred': IconData(0xe436, fontFamily: 'MaterialIcons'), + 'no_flash': IconData(0xe437, fontFamily: 'MaterialIcons'), + 'no_food': IconData(0xe438, fontFamily: 'MaterialIcons'), + 'no_luggage': IconData(0xe439, fontFamily: 'MaterialIcons'), + 'no_meals': IconData(0xe43a, fontFamily: 'MaterialIcons'), + 'no_meals_ouline': IconData(0xe43b, fontFamily: 'MaterialIcons'), + 'no_meeting_room': IconData(0xe43c, fontFamily: 'MaterialIcons'), + 'no_photography': IconData(0xe43d, fontFamily: 'MaterialIcons'), + 'no_sim': IconData(0xe43e, fontFamily: 'MaterialIcons'), + 'no_stroller': IconData(0xe43f, fontFamily: 'MaterialIcons'), + 'no_transfer': IconData(0xe440, fontFamily: 'MaterialIcons'), + 'noise_aware': IconData(0xf07b5, fontFamily: 'MaterialIcons'), + 'noise_control_off': IconData(0xf07b6, fontFamily: 'MaterialIcons'), + 'nordic_walking': IconData(0xe441, fontFamily: 'MaterialIcons'), + 'north': IconData(0xe442, fontFamily: 'MaterialIcons'), + 'north_east': IconData(0xe443, fontFamily: 'MaterialIcons'), + 'north_west': IconData(0xe444, fontFamily: 'MaterialIcons'), + 'not_accessible': IconData(0xe445, fontFamily: 'MaterialIcons'), + 'not_interested': IconData(0xe446, fontFamily: 'MaterialIcons'), + 'not_listed_location': IconData(0xe447, fontFamily: 'MaterialIcons'), + 'not_started': IconData(0xe448, fontFamily: 'MaterialIcons'), + 'note': + IconData(0xe449, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'note_add': IconData(0xe44a, fontFamily: 'MaterialIcons'), + 'note_alt': + IconData(0xe44b, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'notes': IconData(0xe44c, fontFamily: 'MaterialIcons'), + 'notification_add': IconData(0xe44d, fontFamily: 'MaterialIcons'), + 'notification_important': IconData(0xe44e, fontFamily: 'MaterialIcons'), + 'notifications': IconData(0xe44f, fontFamily: 'MaterialIcons'), + 'notifications_active': IconData(0xe450, fontFamily: 'MaterialIcons'), + 'notifications_none': IconData(0xe451, fontFamily: 'MaterialIcons'), + 'notifications_off': IconData(0xe452, fontFamily: 'MaterialIcons'), + 'notifications_on': IconData(0xe450, fontFamily: 'MaterialIcons'), + 'notifications_paused': IconData(0xe453, fontFamily: 'MaterialIcons'), + 'now_wallpaper': IconData(0xe6ca, fontFamily: 'MaterialIcons'), + 'now_widgets': IconData(0xe6e6, fontFamily: 'MaterialIcons'), + 'numbers': IconData(0xf0542, fontFamily: 'MaterialIcons'), + 'offline_bolt': IconData(0xe454, fontFamily: 'MaterialIcons'), + 'offline_pin': IconData(0xe455, fontFamily: 'MaterialIcons'), + 'offline_share': IconData(0xe456, fontFamily: 'MaterialIcons'), + 'oil_barrel': IconData(0xf07b7, fontFamily: 'MaterialIcons'), + 'on_device_training': IconData(0xf07b8, fontFamily: 'MaterialIcons'), + 'ondemand_video': IconData(0xe457, fontFamily: 'MaterialIcons'), + 'online_prediction': IconData(0xe458, fontFamily: 'MaterialIcons'), + 'opacity': IconData(0xe459, fontFamily: 'MaterialIcons'), + 'open_in_browser': IconData(0xe45a, fontFamily: 'MaterialIcons'), + 'open_in_full': IconData(0xe45b, fontFamily: 'MaterialIcons'), + 'open_in_new': + IconData(0xe45c, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'open_in_new_off': IconData(0xe45d, fontFamily: 'MaterialIcons'), + 'open_with': IconData(0xe45e, fontFamily: 'MaterialIcons'), + 'other_houses': IconData(0xe45f, fontFamily: 'MaterialIcons'), + 'outbond': IconData(0xe460, fontFamily: 'MaterialIcons'), + 'outbound': IconData(0xe461, fontFamily: 'MaterialIcons'), + 'outbox': IconData(0xe462, fontFamily: 'MaterialIcons'), + 'outdoor_grill': IconData(0xe463, fontFamily: 'MaterialIcons'), + 'outgoing_mail': IconData(0xe464, fontFamily: 'MaterialIcons'), + 'outlet': IconData(0xe465, fontFamily: 'MaterialIcons'), + 'output': IconData(0xf0543, fontFamily: 'MaterialIcons'), + 'padding': IconData(0xe467, fontFamily: 'MaterialIcons'), + 'pages': IconData(0xe468, fontFamily: 'MaterialIcons'), + 'pageview': IconData(0xe469, fontFamily: 'MaterialIcons'), + 'paid': IconData(0xe46a, fontFamily: 'MaterialIcons'), + 'palette': IconData(0xe46b, fontFamily: 'MaterialIcons'), + 'pallet': IconData(0xf086f, fontFamily: 'MaterialIcons'), + 'pan_tool': IconData(0xe46c, fontFamily: 'MaterialIcons'), + 'pan_tool_alt': IconData(0xf0544, fontFamily: 'MaterialIcons'), + 'panorama': IconData(0xe46d, fontFamily: 'MaterialIcons'), + 'panorama_fish_eye': IconData(0xe46e, fontFamily: 'MaterialIcons'), + 'panorama_fisheye': IconData(0xe46e, fontFamily: 'MaterialIcons'), + 'panorama_horizontal': IconData(0xe46f, fontFamily: 'MaterialIcons'), + 'panorama_horizontal_select': IconData(0xe470, fontFamily: 'MaterialIcons'), + 'panorama_photosphere': IconData(0xe471, fontFamily: 'MaterialIcons'), + 'panorama_photosphere_select': IconData(0xe472, fontFamily: 'MaterialIcons'), + 'panorama_vertical': IconData(0xe473, fontFamily: 'MaterialIcons'), + 'panorama_vertical_select': IconData(0xe474, fontFamily: 'MaterialIcons'), + 'panorama_wide_angle': IconData(0xe475, fontFamily: 'MaterialIcons'), + 'panorama_wide_angle_select': IconData(0xe476, fontFamily: 'MaterialIcons'), + 'paragliding': IconData(0xe477, fontFamily: 'MaterialIcons'), + 'park': IconData(0xe478, fontFamily: 'MaterialIcons'), + 'party_mode': IconData(0xe479, fontFamily: 'MaterialIcons'), + 'password': IconData(0xe47a, fontFamily: 'MaterialIcons'), + 'paste': IconData(0xe192, fontFamily: 'MaterialIcons'), + 'pattern': IconData(0xe47b, fontFamily: 'MaterialIcons'), + 'pause': IconData(0xe47c, fontFamily: 'MaterialIcons'), + 'pause_circle': IconData(0xe47d, fontFamily: 'MaterialIcons'), + 'pause_circle_filled': IconData(0xe47e, fontFamily: 'MaterialIcons'), + 'pause_circle_outline': IconData(0xe47f, fontFamily: 'MaterialIcons'), + 'pause_presentation': IconData(0xe480, fontFamily: 'MaterialIcons'), + 'payment': IconData(0xe481, fontFamily: 'MaterialIcons'), + 'payments': IconData(0xe482, fontFamily: 'MaterialIcons'), + 'paypal': IconData(0xf0545, fontFamily: 'MaterialIcons'), + 'pedal_bike': IconData(0xe483, fontFamily: 'MaterialIcons'), + 'pending': IconData(0xe484, fontFamily: 'MaterialIcons'), + 'pending_actions': IconData(0xe485, fontFamily: 'MaterialIcons'), + 'pentagon': IconData(0xf0546, fontFamily: 'MaterialIcons'), + 'people': IconData(0xe486, fontFamily: 'MaterialIcons'), + 'people_alt': IconData(0xe487, fontFamily: 'MaterialIcons'), + 'people_outline': IconData(0xe488, fontFamily: 'MaterialIcons'), + 'percent': IconData(0xf0547, fontFamily: 'MaterialIcons'), + 'perm_camera_mic': IconData(0xe489, fontFamily: 'MaterialIcons'), + 'perm_contact_cal': IconData(0xe48a, fontFamily: 'MaterialIcons'), + 'perm_contact_calendar': IconData(0xe48a, fontFamily: 'MaterialIcons'), + 'perm_data_setting': IconData(0xe48b, fontFamily: 'MaterialIcons'), + 'perm_device_info': IconData(0xe48c, fontFamily: 'MaterialIcons'), + 'perm_device_information': IconData(0xe48c, fontFamily: 'MaterialIcons'), + 'perm_identity': IconData(0xe48d, fontFamily: 'MaterialIcons'), + 'perm_media': IconData(0xe48e, fontFamily: 'MaterialIcons'), + 'perm_phone_msg': IconData(0xe48f, fontFamily: 'MaterialIcons'), + 'perm_scan_wifi': IconData(0xe490, fontFamily: 'MaterialIcons'), + 'person': IconData(0xe491, fontFamily: 'MaterialIcons'), + 'person_2': IconData(0xf0870, fontFamily: 'MaterialIcons'), + 'person_3': IconData(0xf0871, fontFamily: 'MaterialIcons'), + 'person_4': IconData(0xf0872, fontFamily: 'MaterialIcons'), + 'person_add': IconData(0xe492, fontFamily: 'MaterialIcons'), + 'person_add_alt': IconData(0xe493, fontFamily: 'MaterialIcons'), + 'person_add_alt_1': IconData(0xe494, fontFamily: 'MaterialIcons'), + 'person_add_disabled': IconData(0xe495, fontFamily: 'MaterialIcons'), + 'person_off': IconData(0xe496, fontFamily: 'MaterialIcons'), + 'person_outline': IconData(0xe497, fontFamily: 'MaterialIcons'), + 'person_pin': IconData(0xe498, fontFamily: 'MaterialIcons'), + 'person_pin_circle': IconData(0xe499, fontFamily: 'MaterialIcons'), + 'person_remove': IconData(0xe49a, fontFamily: 'MaterialIcons'), + 'person_remove_alt_1': IconData(0xe49b, fontFamily: 'MaterialIcons'), + 'person_search': IconData(0xe49c, fontFamily: 'MaterialIcons'), + 'personal_injury': IconData(0xe49d, fontFamily: 'MaterialIcons'), + 'personal_video': IconData(0xe49e, fontFamily: 'MaterialIcons'), + 'pest_control': IconData(0xe49f, fontFamily: 'MaterialIcons'), + 'pest_control_rodent': IconData(0xe4a0, fontFamily: 'MaterialIcons'), + 'pets': IconData(0xe4a1, fontFamily: 'MaterialIcons'), + 'phishing': IconData(0xf0548, fontFamily: 'MaterialIcons'), + 'phone': IconData(0xe4a2, fontFamily: 'MaterialIcons'), + 'phone_android': IconData(0xe4a3, fontFamily: 'MaterialIcons'), + 'phone_bluetooth_speaker': IconData(0xe4a4, fontFamily: 'MaterialIcons'), + 'phone_callback': IconData(0xe4a5, fontFamily: 'MaterialIcons'), + 'phone_disabled': IconData(0xe4a6, fontFamily: 'MaterialIcons'), + 'phone_enabled': IconData(0xe4a7, fontFamily: 'MaterialIcons'), + 'phone_forwarded': IconData(0xe4a8, fontFamily: 'MaterialIcons'), + 'phone_in_talk': IconData(0xe4a9, fontFamily: 'MaterialIcons'), + 'phone_iphone': IconData(0xe4aa, fontFamily: 'MaterialIcons'), + 'phone_locked': IconData(0xe4ab, fontFamily: 'MaterialIcons'), + 'phone_missed': IconData(0xe4ac, fontFamily: 'MaterialIcons'), + 'phone_paused': IconData(0xe4ad, fontFamily: 'MaterialIcons'), + 'phonelink': IconData(0xe4ae, fontFamily: 'MaterialIcons'), + 'phonelink_erase': IconData(0xe4af, fontFamily: 'MaterialIcons'), + 'phonelink_lock': IconData(0xe4b0, fontFamily: 'MaterialIcons'), + 'phonelink_off': IconData(0xe4b1, fontFamily: 'MaterialIcons'), + 'phonelink_ring': IconData(0xe4b2, fontFamily: 'MaterialIcons'), + 'phonelink_setup': IconData(0xe4b3, fontFamily: 'MaterialIcons'), + 'photo': IconData(0xe4b4, fontFamily: 'MaterialIcons'), + 'photo_album': IconData(0xe4b5, fontFamily: 'MaterialIcons'), + 'photo_camera': IconData(0xe4b6, fontFamily: 'MaterialIcons'), + 'photo_camera_back': IconData(0xe4b7, fontFamily: 'MaterialIcons'), + 'photo_camera_front': IconData(0xe4b8, fontFamily: 'MaterialIcons'), + 'photo_filter': IconData(0xe4b9, fontFamily: 'MaterialIcons'), + 'photo_library': IconData(0xe4ba, fontFamily: 'MaterialIcons'), + 'photo_size_select_actual': IconData(0xe4bb, fontFamily: 'MaterialIcons'), + 'photo_size_select_large': IconData(0xe4bc, fontFamily: 'MaterialIcons'), + 'photo_size_select_small': IconData(0xe4bd, fontFamily: 'MaterialIcons'), + 'php': IconData(0xf0549, fontFamily: 'MaterialIcons'), + 'piano': IconData(0xe4be, fontFamily: 'MaterialIcons'), + 'piano_off': IconData(0xe4bf, fontFamily: 'MaterialIcons'), + 'picture_as_pdf': IconData(0xe4c0, fontFamily: 'MaterialIcons'), + 'picture_in_picture': IconData(0xe4c1, fontFamily: 'MaterialIcons'), + 'picture_in_picture_alt': IconData(0xe4c2, fontFamily: 'MaterialIcons'), + 'pie_chart': IconData(0xe4c3, fontFamily: 'MaterialIcons'), + 'pie_chart_outline': IconData(0xe4c5, fontFamily: 'MaterialIcons'), + 'pin': IconData(0xe4c6, fontFamily: 'MaterialIcons'), + 'pin_drop': IconData(0xe4c7, fontFamily: 'MaterialIcons'), + 'pin_end': IconData(0xf054b, fontFamily: 'MaterialIcons'), + 'pin_invoke': IconData(0xf054c, fontFamily: 'MaterialIcons'), + 'pinch': IconData(0xf054d, fontFamily: 'MaterialIcons'), + 'pivot_table_chart': IconData(0xe4c8, fontFamily: 'MaterialIcons'), + 'pix': IconData(0xf054e, fontFamily: 'MaterialIcons'), + 'place': IconData(0xe4c9, fontFamily: 'MaterialIcons'), + 'plagiarism': IconData(0xe4ca, fontFamily: 'MaterialIcons'), + 'play_arrow': IconData(0xe4cb, fontFamily: 'MaterialIcons'), + 'play_circle': IconData(0xe4cc, fontFamily: 'MaterialIcons'), + 'play_circle_fill': IconData(0xe4cd, fontFamily: 'MaterialIcons'), + 'play_circle_filled': IconData(0xe4cd, fontFamily: 'MaterialIcons'), + 'play_circle_outline': IconData(0xe4ce, fontFamily: 'MaterialIcons'), + 'play_disabled': IconData(0xe4cf, fontFamily: 'MaterialIcons'), + 'play_for_work': IconData(0xe4d0, fontFamily: 'MaterialIcons'), + 'play_lesson': IconData(0xe4d1, fontFamily: 'MaterialIcons'), + 'playlist_add': + IconData(0xe4d2, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'playlist_add_check': IconData(0xe4d3, fontFamily: 'MaterialIcons'), + 'playlist_add_check_circle': IconData(0xf054f, fontFamily: 'MaterialIcons'), + 'playlist_add_circle': IconData(0xf0550, fontFamily: 'MaterialIcons'), + 'playlist_play': IconData(0xe4d4, fontFamily: 'MaterialIcons'), + 'playlist_remove': IconData(0xf0551, fontFamily: 'MaterialIcons'), + 'plumbing': IconData(0xe4d5, fontFamily: 'MaterialIcons'), + 'plus_one': IconData(0xe4d6, fontFamily: 'MaterialIcons'), + 'podcasts': IconData(0xe4d7, fontFamily: 'MaterialIcons'), + 'point_of_sale': IconData(0xe4d8, fontFamily: 'MaterialIcons'), + 'policy': IconData(0xe4d9, fontFamily: 'MaterialIcons'), + 'poll': IconData(0xe4da, fontFamily: 'MaterialIcons'), + 'polyline': IconData(0xf0552, fontFamily: 'MaterialIcons'), + 'polymer': IconData(0xe4db, fontFamily: 'MaterialIcons'), + 'pool': IconData(0xe4dc, fontFamily: 'MaterialIcons'), + 'portable_wifi_off': IconData(0xe4dd, fontFamily: 'MaterialIcons'), + 'portrait': IconData(0xe4de, fontFamily: 'MaterialIcons'), + 'post_add': IconData(0xe4df, fontFamily: 'MaterialIcons'), + 'power': IconData(0xe4e0, fontFamily: 'MaterialIcons'), + 'power_input': IconData(0xe4e1, fontFamily: 'MaterialIcons'), + 'power_off': IconData(0xe4e2, fontFamily: 'MaterialIcons'), + 'power_settings_new': IconData(0xe4e3, fontFamily: 'MaterialIcons'), + 'precision_manufacturing': IconData(0xe4e4, fontFamily: 'MaterialIcons'), + 'pregnant_woman': IconData(0xe4e5, fontFamily: 'MaterialIcons'), + 'present_to_all': IconData(0xe4e6, fontFamily: 'MaterialIcons'), + 'preview': IconData(0xe4e7, fontFamily: 'MaterialIcons'), + 'price_change': IconData(0xe4e8, fontFamily: 'MaterialIcons'), + 'price_check': IconData(0xe4e9, fontFamily: 'MaterialIcons'), + 'print': IconData(0xe4ea, fontFamily: 'MaterialIcons'), + 'print_disabled': IconData(0xe4eb, fontFamily: 'MaterialIcons'), + 'priority_high': IconData(0xe4ec, fontFamily: 'MaterialIcons'), + 'privacy_tip': IconData(0xe4ed, fontFamily: 'MaterialIcons'), + 'private_connectivity': IconData(0xf0553, fontFamily: 'MaterialIcons'), + 'production_quantity_limits': IconData(0xe4ee, fontFamily: 'MaterialIcons'), + 'propane': IconData(0xf07b9, fontFamily: 'MaterialIcons'), + 'propane_tank': IconData(0xf07ba, fontFamily: 'MaterialIcons'), + 'psychology': IconData(0xe4ef, fontFamily: 'MaterialIcons'), + 'psychology_alt': IconData(0xf0873, fontFamily: 'MaterialIcons'), + 'public': IconData(0xe4f0, fontFamily: 'MaterialIcons'), + 'public_off': IconData(0xe4f1, fontFamily: 'MaterialIcons'), + 'publish': IconData(0xe4f2, fontFamily: 'MaterialIcons'), + 'published_with_changes': IconData(0xe4f3, fontFamily: 'MaterialIcons'), + 'punch_clock': IconData(0xf0554, fontFamily: 'MaterialIcons'), + 'push_pin': IconData(0xe4f4, fontFamily: 'MaterialIcons'), + 'qr_code': IconData(0xe4f5, fontFamily: 'MaterialIcons'), + 'qr_code_2': IconData(0xe4f6, fontFamily: 'MaterialIcons'), + 'qr_code_scanner': IconData(0xe4f7, fontFamily: 'MaterialIcons'), + 'query_builder': IconData(0xe4f8, fontFamily: 'MaterialIcons'), + 'query_stats': IconData(0xe4f9, fontFamily: 'MaterialIcons'), + 'question_answer': IconData(0xe4fa, fontFamily: 'MaterialIcons'), + 'question_mark': IconData(0xf0555, fontFamily: 'MaterialIcons'), + 'queue': IconData(0xe4fb, fontFamily: 'MaterialIcons'), + 'queue_music': + IconData(0xe4fc, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'queue_play_next': IconData(0xe4fd, fontFamily: 'MaterialIcons'), + 'quick_contacts_dialer': IconData(0xe18c, fontFamily: 'MaterialIcons'), + 'quick_contacts_mail': IconData(0xe18a, fontFamily: 'MaterialIcons'), + 'quickreply': IconData(0xe4fe, fontFamily: 'MaterialIcons'), + 'quiz': IconData(0xe4ff, fontFamily: 'MaterialIcons'), + 'quora': IconData(0xf0556, fontFamily: 'MaterialIcons'), + 'r_mobiledata': IconData(0xe500, fontFamily: 'MaterialIcons'), + 'radar': IconData(0xe501, fontFamily: 'MaterialIcons'), + 'radio': IconData(0xe502, fontFamily: 'MaterialIcons'), + 'radio_button_checked': IconData(0xe503, fontFamily: 'MaterialIcons'), + 'radio_button_off': IconData(0xe504, fontFamily: 'MaterialIcons'), + 'radio_button_on': IconData(0xe503, fontFamily: 'MaterialIcons'), + 'radio_button_unchecked': IconData(0xe504, fontFamily: 'MaterialIcons'), + 'railway_alert': IconData(0xe505, fontFamily: 'MaterialIcons'), + 'ramen_dining': IconData(0xe506, fontFamily: 'MaterialIcons'), + 'ramp_left': IconData(0xf0557, fontFamily: 'MaterialIcons'), + 'ramp_right': IconData(0xf0558, fontFamily: 'MaterialIcons'), + 'rate_review': IconData(0xe507, fontFamily: 'MaterialIcons'), + 'raw_off': IconData(0xe508, fontFamily: 'MaterialIcons'), + 'raw_on': IconData(0xe509, fontFamily: 'MaterialIcons'), + 'read_more': IconData(0xe50a, fontFamily: 'MaterialIcons'), + 'real_estate_agent': IconData(0xe50b, fontFamily: 'MaterialIcons'), + 'rebase_edit': IconData(0xf0874, fontFamily: 'MaterialIcons'), + 'receipt': IconData(0xe50c, fontFamily: 'MaterialIcons'), + 'receipt_long': IconData(0xe50d, fontFamily: 'MaterialIcons'), + 'recent_actors': IconData(0xe50e, fontFamily: 'MaterialIcons'), + 'recommend': IconData(0xe50f, fontFamily: 'MaterialIcons'), + 'record_voice_over': IconData(0xe510, fontFamily: 'MaterialIcons'), + 'rectangle': IconData(0xf0559, fontFamily: 'MaterialIcons'), + 'recycling': IconData(0xf055a, fontFamily: 'MaterialIcons'), + 'reddit': IconData(0xf055b, fontFamily: 'MaterialIcons'), + 'redeem': IconData(0xe511, fontFamily: 'MaterialIcons'), + 'redo': + IconData(0xe512, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'reduce_capacity': IconData(0xe513, fontFamily: 'MaterialIcons'), + 'refresh': IconData(0xe514, fontFamily: 'MaterialIcons'), + 'remember_me': IconData(0xe515, fontFamily: 'MaterialIcons'), + 'remove': IconData(0xe516, fontFamily: 'MaterialIcons'), + 'remove_circle': IconData(0xe517, fontFamily: 'MaterialIcons'), + 'remove_circle_outline': IconData(0xe518, fontFamily: 'MaterialIcons'), + 'remove_done': IconData(0xe519, fontFamily: 'MaterialIcons'), + 'remove_from_queue': IconData(0xe51a, fontFamily: 'MaterialIcons'), + 'remove_moderator': IconData(0xe51b, fontFamily: 'MaterialIcons'), + 'remove_red_eye': IconData(0xe51c, fontFamily: 'MaterialIcons'), + 'remove_road': IconData(0xf07bb, fontFamily: 'MaterialIcons'), + 'remove_shopping_cart': IconData(0xe51d, fontFamily: 'MaterialIcons'), + 'reorder': IconData(0xe51e, fontFamily: 'MaterialIcons'), + 'repartition': IconData(0xf0875, fontFamily: 'MaterialIcons'), + 'repeat': IconData(0xe51f, fontFamily: 'MaterialIcons'), + 'repeat_on': IconData(0xe520, fontFamily: 'MaterialIcons'), + 'repeat_one': IconData(0xe521, fontFamily: 'MaterialIcons'), + 'repeat_one_on': IconData(0xe522, fontFamily: 'MaterialIcons'), + 'replay': IconData(0xe523, fontFamily: 'MaterialIcons'), + 'replay_10': IconData(0xe524, fontFamily: 'MaterialIcons'), + 'replay_30': IconData(0xe525, fontFamily: 'MaterialIcons'), + 'replay_5': IconData(0xe526, fontFamily: 'MaterialIcons'), + 'replay_circle_filled': IconData(0xe527, fontFamily: 'MaterialIcons'), + 'reply': + IconData(0xe528, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'reply_all': + IconData(0xe529, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'report': IconData(0xe52a, fontFamily: 'MaterialIcons'), + 'report_gmailerrorred': IconData(0xe52b, fontFamily: 'MaterialIcons'), + 'report_off': IconData(0xe52c, fontFamily: 'MaterialIcons'), + 'report_problem': IconData(0xe52d, fontFamily: 'MaterialIcons'), + 'request_page': IconData(0xe52e, fontFamily: 'MaterialIcons'), + 'request_quote': IconData(0xe52f, fontFamily: 'MaterialIcons'), + 'reset_tv': IconData(0xe530, fontFamily: 'MaterialIcons'), + 'restart_alt': IconData(0xe531, fontFamily: 'MaterialIcons'), + 'restaurant': IconData(0xe532, fontFamily: 'MaterialIcons'), + 'restaurant_menu': IconData(0xe533, fontFamily: 'MaterialIcons'), + 'restore': IconData(0xe534, fontFamily: 'MaterialIcons'), + 'restore_from_trash': IconData(0xe535, fontFamily: 'MaterialIcons'), + 'restore_page': IconData(0xe536, fontFamily: 'MaterialIcons'), + 'reviews': IconData(0xe537, fontFamily: 'MaterialIcons'), + 'rice_bowl': IconData(0xe538, fontFamily: 'MaterialIcons'), + 'ring_volume': IconData(0xe539, fontFamily: 'MaterialIcons'), + 'rocket': IconData(0xf055c, fontFamily: 'MaterialIcons'), + 'rocket_launch': IconData(0xf055d, fontFamily: 'MaterialIcons'), + 'roller_shades': IconData(0xf07bc, fontFamily: 'MaterialIcons'), + 'roller_shades_closed': IconData(0xf07bd, fontFamily: 'MaterialIcons'), + 'roller_skating': IconData(0xf06c0, fontFamily: 'MaterialIcons'), + 'roofing': IconData(0xe53a, fontFamily: 'MaterialIcons'), + 'room': IconData(0xe53b, fontFamily: 'MaterialIcons'), + 'room_preferences': IconData(0xe53c, fontFamily: 'MaterialIcons'), + 'room_service': IconData(0xe53d, fontFamily: 'MaterialIcons'), + 'rotate_90_degrees_ccw': IconData(0xe53e, fontFamily: 'MaterialIcons'), + 'rotate_90_degrees_cw': IconData(0xf055e, fontFamily: 'MaterialIcons'), + 'rotate_left': IconData(0xe53f, fontFamily: 'MaterialIcons'), + 'rotate_right': IconData(0xe540, fontFamily: 'MaterialIcons'), + 'route': IconData(0xf0561, fontFamily: 'MaterialIcons'), + 'router': IconData(0xe542, fontFamily: 'MaterialIcons'), + 'rowing': IconData(0xe543, fontFamily: 'MaterialIcons'), + 'rss_feed': IconData(0xe544, fontFamily: 'MaterialIcons'), + 'rsvp': IconData(0xe545, fontFamily: 'MaterialIcons'), + 'rtt': IconData(0xe546, fontFamily: 'MaterialIcons'), + 'rule': IconData(0xe547, fontFamily: 'MaterialIcons'), + 'rule_folder': IconData(0xe548, fontFamily: 'MaterialIcons'), + 'run_circle': IconData(0xe549, fontFamily: 'MaterialIcons'), + 'running_with_errors': IconData(0xe54a, fontFamily: 'MaterialIcons'), + 'rv_hookup': IconData(0xe54b, fontFamily: 'MaterialIcons'), + 'safety_check': IconData(0xf07be, fontFamily: 'MaterialIcons'), + 'safety_divider': IconData(0xe54c, fontFamily: 'MaterialIcons'), + 'sailing': IconData(0xe54d, fontFamily: 'MaterialIcons'), + 'sanitizer': IconData(0xe54e, fontFamily: 'MaterialIcons'), + 'satellite': IconData(0xe54f, fontFamily: 'MaterialIcons'), + 'satellite_alt': IconData(0xf0562, fontFamily: 'MaterialIcons'), + 'save': IconData(0xe550, fontFamily: 'MaterialIcons'), + 'save_alt': IconData(0xe551, fontFamily: 'MaterialIcons'), + 'save_as': IconData(0xf0563, fontFamily: 'MaterialIcons'), + 'saved_search': IconData(0xe552, fontFamily: 'MaterialIcons'), + 'savings': IconData(0xe553, fontFamily: 'MaterialIcons'), + 'scale': IconData(0xf0564, fontFamily: 'MaterialIcons'), + 'scanner': IconData(0xe554, fontFamily: 'MaterialIcons'), + 'scatter_plot': IconData(0xe555, fontFamily: 'MaterialIcons'), + 'schedule': IconData(0xe556, fontFamily: 'MaterialIcons'), + 'schedule_send': IconData(0xe557, fontFamily: 'MaterialIcons'), + 'schema': IconData(0xe558, fontFamily: 'MaterialIcons'), + 'school': IconData(0xe559, fontFamily: 'MaterialIcons'), + 'science': IconData(0xe55a, fontFamily: 'MaterialIcons'), + 'score': IconData(0xe55b, fontFamily: 'MaterialIcons'), + 'scoreboard': IconData(0xf06c1, fontFamily: 'MaterialIcons'), + 'screen_lock_landscape': IconData(0xe55c, fontFamily: 'MaterialIcons'), + 'screen_lock_portrait': IconData(0xe55d, fontFamily: 'MaterialIcons'), + 'screen_lock_rotation': IconData(0xe55e, fontFamily: 'MaterialIcons'), + 'screen_rotation': IconData(0xe55f, fontFamily: 'MaterialIcons'), + 'screen_rotation_alt': IconData(0xf07bf, fontFamily: 'MaterialIcons'), + 'screen_search_desktop': IconData(0xe560, fontFamily: 'MaterialIcons'), + 'screen_share': + IconData(0xe561, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'screenshot': IconData(0xe562, fontFamily: 'MaterialIcons'), + 'screenshot_monitor': IconData(0xf07c0, fontFamily: 'MaterialIcons'), + 'scuba_diving': IconData(0xf06c2, fontFamily: 'MaterialIcons'), + 'sd': IconData(0xe563, fontFamily: 'MaterialIcons'), + 'sd_card': IconData(0xe564, fontFamily: 'MaterialIcons'), + 'sd_card_alert': IconData(0xe565, fontFamily: 'MaterialIcons'), + 'sd_storage': IconData(0xe566, fontFamily: 'MaterialIcons'), + 'search': IconData(0xe567, fontFamily: 'MaterialIcons'), + 'search_off': IconData(0xe568, fontFamily: 'MaterialIcons'), + 'security': IconData(0xe569, fontFamily: 'MaterialIcons'), + 'security_update': IconData(0xe56a, fontFamily: 'MaterialIcons'), + 'security_update_good': IconData(0xe56b, fontFamily: 'MaterialIcons'), + 'security_update_warning': IconData(0xe56c, fontFamily: 'MaterialIcons'), + 'segment': IconData(0xe56d, fontFamily: 'MaterialIcons'), + 'select_all': IconData(0xe56e, fontFamily: 'MaterialIcons'), + 'self_improvement': IconData(0xe56f, fontFamily: 'MaterialIcons'), + 'sell': IconData(0xe570, fontFamily: 'MaterialIcons'), + 'send': + IconData(0xe571, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'send_and_archive': IconData(0xe572, fontFamily: 'MaterialIcons'), + 'send_time_extension': IconData(0xf0565, fontFamily: 'MaterialIcons'), + 'send_to_mobile': IconData(0xe573, fontFamily: 'MaterialIcons'), + 'sensor_door': IconData(0xe574, fontFamily: 'MaterialIcons'), + 'sensor_occupied': IconData(0xf07c1, fontFamily: 'MaterialIcons'), + 'sensor_window': IconData(0xe575, fontFamily: 'MaterialIcons'), + 'sensors': IconData(0xe576, fontFamily: 'MaterialIcons'), + 'sensors_off': IconData(0xe577, fontFamily: 'MaterialIcons'), + 'sentiment_dissatisfied': IconData(0xe578, fontFamily: 'MaterialIcons'), + 'sentiment_neutral': IconData(0xe579, fontFamily: 'MaterialIcons'), + 'sentiment_satisfied': IconData(0xe57a, fontFamily: 'MaterialIcons'), + 'sentiment_satisfied_alt': IconData(0xe57b, fontFamily: 'MaterialIcons'), + 'sentiment_very_dissatisfied': IconData(0xe57c, fontFamily: 'MaterialIcons'), + 'sentiment_very_satisfied': IconData(0xe57d, fontFamily: 'MaterialIcons'), + 'set_meal': IconData(0xe57e, fontFamily: 'MaterialIcons'), + 'settings': IconData(0xe57f, fontFamily: 'MaterialIcons'), + 'settings_accessibility': IconData(0xe580, fontFamily: 'MaterialIcons'), + 'settings_applications': IconData(0xe581, fontFamily: 'MaterialIcons'), + 'settings_backup_restore': IconData(0xe582, fontFamily: 'MaterialIcons'), + 'settings_bluetooth': IconData(0xe583, fontFamily: 'MaterialIcons'), + 'settings_brightness': IconData(0xe584, fontFamily: 'MaterialIcons'), + 'settings_cell': IconData(0xe585, fontFamily: 'MaterialIcons'), + 'settings_display': IconData(0xe584, fontFamily: 'MaterialIcons'), + 'settings_ethernet': IconData(0xe586, fontFamily: 'MaterialIcons'), + 'settings_input_antenna': IconData(0xe587, fontFamily: 'MaterialIcons'), + 'settings_input_component': IconData(0xe588, fontFamily: 'MaterialIcons'), + 'settings_input_composite': IconData(0xe589, fontFamily: 'MaterialIcons'), + 'settings_input_hdmi': IconData(0xe58a, fontFamily: 'MaterialIcons'), + 'settings_input_svideo': IconData(0xe58b, fontFamily: 'MaterialIcons'), + 'settings_overscan': IconData(0xe58c, fontFamily: 'MaterialIcons'), + 'settings_phone': IconData(0xe58d, fontFamily: 'MaterialIcons'), + 'settings_power': IconData(0xe58e, fontFamily: 'MaterialIcons'), + 'settings_remote': IconData(0xe58f, fontFamily: 'MaterialIcons'), + 'settings_suggest': IconData(0xe590, fontFamily: 'MaterialIcons'), + 'settings_system_daydream': IconData(0xe591, fontFamily: 'MaterialIcons'), + 'settings_voice': IconData(0xe592, fontFamily: 'MaterialIcons'), + 'severe_cold': IconData(0xf07c2, fontFamily: 'MaterialIcons'), + 'shape_line': IconData(0xf0876, fontFamily: 'MaterialIcons'), + 'share': IconData(0xe593, fontFamily: 'MaterialIcons'), + 'share_arrival_time': IconData(0xe594, fontFamily: 'MaterialIcons'), + 'share_location': IconData(0xe595, fontFamily: 'MaterialIcons'), + 'shelves': IconData(0xf0877, fontFamily: 'MaterialIcons'), + 'shield': IconData(0xe596, fontFamily: 'MaterialIcons'), + 'shield_moon': IconData(0xf0566, fontFamily: 'MaterialIcons'), + 'shop': IconData(0xe597, fontFamily: 'MaterialIcons'), + 'shop_2': IconData(0xe598, fontFamily: 'MaterialIcons'), + 'shop_two': IconData(0xe599, fontFamily: 'MaterialIcons'), + 'shopify': IconData(0xf0567, fontFamily: 'MaterialIcons'), + 'shopping_bag': IconData(0xe59a, fontFamily: 'MaterialIcons'), + 'shopping_basket': IconData(0xe59b, fontFamily: 'MaterialIcons'), + 'shopping_cart': IconData(0xe59c, fontFamily: 'MaterialIcons'), + 'shopping_cart_checkout': IconData(0xf0568, fontFamily: 'MaterialIcons'), + 'short_text': + IconData(0xe59d, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'shortcut': IconData(0xe59e, fontFamily: 'MaterialIcons'), + 'show_chart': + IconData(0xe59f, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'shower': IconData(0xe5a0, fontFamily: 'MaterialIcons'), + 'shuffle': IconData(0xe5a1, fontFamily: 'MaterialIcons'), + 'shuffle_on': IconData(0xe5a2, fontFamily: 'MaterialIcons'), + 'shutter_speed': IconData(0xe5a3, fontFamily: 'MaterialIcons'), + 'sick': IconData(0xe5a4, fontFamily: 'MaterialIcons'), + 'sign_language': IconData(0xf07c3, fontFamily: 'MaterialIcons'), + 'signal_cellular_0_bar': IconData(0xe5a5, fontFamily: 'MaterialIcons'), + 'signal_cellular_4_bar': IconData(0xe5a6, fontFamily: 'MaterialIcons'), + 'signal_cellular_alt': IconData(0xe5a7, fontFamily: 'MaterialIcons'), + 'signal_cellular_alt_1_bar': IconData(0xf07c4, fontFamily: 'MaterialIcons'), + 'signal_cellular_alt_2_bar': IconData(0xf07c5, fontFamily: 'MaterialIcons'), + 'signal_cellular_connected_no_internet_0_bar': + IconData(0xe5a8, fontFamily: 'MaterialIcons'), + 'signal_cellular_connected_no_internet_4_bar': + IconData(0xe5a9, fontFamily: 'MaterialIcons'), + 'signal_cellular_no_sim': IconData(0xe5aa, fontFamily: 'MaterialIcons'), + 'signal_cellular_nodata': IconData(0xe5ab, fontFamily: 'MaterialIcons'), + 'signal_cellular_null': IconData(0xe5ac, fontFamily: 'MaterialIcons'), + 'signal_cellular_off': IconData(0xe5ad, fontFamily: 'MaterialIcons'), + 'signal_wifi_0_bar': IconData(0xe5ae, fontFamily: 'MaterialIcons'), + 'signal_wifi_4_bar': IconData(0xe5af, fontFamily: 'MaterialIcons'), + 'signal_wifi_4_bar_lock': IconData(0xe5b0, fontFamily: 'MaterialIcons'), + 'signal_wifi_bad': IconData(0xe5b1, fontFamily: 'MaterialIcons'), + 'signal_wifi_connected_no_internet_4': + IconData(0xe5b2, fontFamily: 'MaterialIcons'), + 'signal_wifi_off': IconData(0xe5b3, fontFamily: 'MaterialIcons'), + 'signal_wifi_statusbar_4_bar': IconData(0xe5b4, fontFamily: 'MaterialIcons'), + 'signal_wifi_statusbar_connected_no_internet_4': + IconData(0xe5b5, fontFamily: 'MaterialIcons'), + 'signal_wifi_statusbar_null': IconData(0xe5b6, fontFamily: 'MaterialIcons'), + 'signpost': IconData(0xf0569, fontFamily: 'MaterialIcons'), + 'sim_card': IconData(0xe5b7, fontFamily: 'MaterialIcons'), + 'sim_card_alert': IconData(0xe5b8, fontFamily: 'MaterialIcons'), + 'sim_card_download': IconData(0xe5b9, fontFamily: 'MaterialIcons'), + 'single_bed': IconData(0xe5ba, fontFamily: 'MaterialIcons'), + 'sip': IconData(0xe5bb, fontFamily: 'MaterialIcons'), + 'skateboarding': IconData(0xe5bc, fontFamily: 'MaterialIcons'), + 'skip_next': IconData(0xe5bd, fontFamily: 'MaterialIcons'), + 'skip_previous': IconData(0xe5be, fontFamily: 'MaterialIcons'), + 'sledding': IconData(0xe5bf, fontFamily: 'MaterialIcons'), + 'slideshow': IconData(0xe5c0, fontFamily: 'MaterialIcons'), + 'slow_motion_video': IconData(0xe5c1, fontFamily: 'MaterialIcons'), + 'smart_button': IconData(0xe5c2, fontFamily: 'MaterialIcons'), + 'smart_display': IconData(0xe5c3, fontFamily: 'MaterialIcons'), + 'smart_screen': IconData(0xe5c4, fontFamily: 'MaterialIcons'), + 'smart_toy': IconData(0xe5c5, fontFamily: 'MaterialIcons'), + 'smartphone': IconData(0xe5c6, fontFamily: 'MaterialIcons'), + 'smoke_free': IconData(0xe5c7, fontFamily: 'MaterialIcons'), + 'smoking_rooms': IconData(0xe5c8, fontFamily: 'MaterialIcons'), + 'sms': IconData(0xe5c9, fontFamily: 'MaterialIcons'), + 'sms_failed': IconData(0xe5ca, fontFamily: 'MaterialIcons'), + 'snapchat': IconData(0xf056a, fontFamily: 'MaterialIcons'), + 'snippet_folder': IconData(0xe5cb, fontFamily: 'MaterialIcons'), + 'snooze': IconData(0xe5cc, fontFamily: 'MaterialIcons'), + 'snowboarding': IconData(0xe5cd, fontFamily: 'MaterialIcons'), + 'snowing': IconData(0xf056b, fontFamily: 'MaterialIcons'), + 'snowmobile': IconData(0xe5ce, fontFamily: 'MaterialIcons'), + 'snowshoeing': IconData(0xe5cf, fontFamily: 'MaterialIcons'), + 'soap': IconData(0xe5d0, fontFamily: 'MaterialIcons'), + 'social_distance': IconData(0xe5d1, fontFamily: 'MaterialIcons'), + 'solar_power': IconData(0xf07c6, fontFamily: 'MaterialIcons'), + 'sort': + IconData(0xe5d2, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'sort_by_alpha': IconData(0xe5d3, fontFamily: 'MaterialIcons'), + 'sos': IconData(0xf07c7, fontFamily: 'MaterialIcons'), + 'soup_kitchen': IconData(0xf056c, fontFamily: 'MaterialIcons'), + 'source': IconData(0xe5d4, fontFamily: 'MaterialIcons'), + 'south': IconData(0xe5d5, fontFamily: 'MaterialIcons'), + 'south_america': IconData(0xf056d, fontFamily: 'MaterialIcons'), + 'south_east': IconData(0xe5d6, fontFamily: 'MaterialIcons'), + 'south_west': IconData(0xe5d7, fontFamily: 'MaterialIcons'), + 'spa': IconData(0xe5d8, fontFamily: 'MaterialIcons'), + 'space_bar': IconData(0xe5d9, fontFamily: 'MaterialIcons'), + 'space_dashboard': IconData(0xe5da, fontFamily: 'MaterialIcons'), + 'spatial_audio': IconData(0xf07c8, fontFamily: 'MaterialIcons'), + 'spatial_audio_off': IconData(0xf07c9, fontFamily: 'MaterialIcons'), + 'spatial_tracking': IconData(0xf07ca, fontFamily: 'MaterialIcons'), + 'speaker': IconData(0xe5db, fontFamily: 'MaterialIcons'), + 'speaker_group': IconData(0xe5dc, fontFamily: 'MaterialIcons'), + 'speaker_notes': IconData(0xe5dd, fontFamily: 'MaterialIcons'), + 'speaker_notes_off': IconData(0xe5de, fontFamily: 'MaterialIcons'), + 'speaker_phone': IconData(0xe5df, fontFamily: 'MaterialIcons'), + 'speed': IconData(0xe5e0, fontFamily: 'MaterialIcons'), + 'spellcheck': IconData(0xe5e1, fontFamily: 'MaterialIcons'), + 'splitscreen': IconData(0xe5e2, fontFamily: 'MaterialIcons'), + 'spoke': IconData(0xf056e, fontFamily: 'MaterialIcons'), + 'sports': IconData(0xe5e3, fontFamily: 'MaterialIcons'), + 'sports_bar': IconData(0xe5e4, fontFamily: 'MaterialIcons'), + 'sports_baseball': IconData(0xe5e5, fontFamily: 'MaterialIcons'), + 'sports_basketball': IconData(0xe5e6, fontFamily: 'MaterialIcons'), + 'sports_cricket': IconData(0xe5e7, fontFamily: 'MaterialIcons'), + 'sports_esports': IconData(0xe5e8, fontFamily: 'MaterialIcons'), + 'sports_football': IconData(0xe5e9, fontFamily: 'MaterialIcons'), + 'sports_golf': IconData(0xe5ea, fontFamily: 'MaterialIcons'), + 'sports_gymnastics': IconData(0xf06c3, fontFamily: 'MaterialIcons'), + 'sports_handball': IconData(0xe5eb, fontFamily: 'MaterialIcons'), + 'sports_hockey': IconData(0xe5ec, fontFamily: 'MaterialIcons'), + 'sports_kabaddi': IconData(0xe5ed, fontFamily: 'MaterialIcons'), + 'sports_martial_arts': IconData(0xf056f, fontFamily: 'MaterialIcons'), + 'sports_mma': IconData(0xe5ee, fontFamily: 'MaterialIcons'), + 'sports_motorsports': IconData(0xe5ef, fontFamily: 'MaterialIcons'), + 'sports_rugby': IconData(0xe5f0, fontFamily: 'MaterialIcons'), + 'sports_score': IconData(0xe5f1, fontFamily: 'MaterialIcons'), + 'sports_soccer': IconData(0xe5f2, fontFamily: 'MaterialIcons'), + 'sports_tennis': IconData(0xe5f3, fontFamily: 'MaterialIcons'), + 'sports_volleyball': IconData(0xe5f4, fontFamily: 'MaterialIcons'), + 'square': IconData(0xf0570, fontFamily: 'MaterialIcons'), + 'square_foot': IconData(0xe5f5, fontFamily: 'MaterialIcons'), + 'ssid_chart': IconData(0xf0571, fontFamily: 'MaterialIcons'), + 'stacked_bar_chart': IconData(0xe5f6, fontFamily: 'MaterialIcons'), + 'stacked_line_chart': IconData(0xe5f7, fontFamily: 'MaterialIcons'), + 'stadium': IconData(0xf0572, fontFamily: 'MaterialIcons'), + 'stairs': IconData(0xe5f8, fontFamily: 'MaterialIcons'), + 'star': IconData(0xe5f9, fontFamily: 'MaterialIcons'), + 'star_border': IconData(0xe5fa, fontFamily: 'MaterialIcons'), + 'star_border_purple500': IconData(0xe5fb, fontFamily: 'MaterialIcons'), + 'star_half': + IconData(0xe5fc, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'star_outline': IconData(0xe5fd, fontFamily: 'MaterialIcons'), + 'star_purple500': IconData(0xe5fe, fontFamily: 'MaterialIcons'), + 'star_rate': IconData(0xe5ff, fontFamily: 'MaterialIcons'), + 'stars': IconData(0xe600, fontFamily: 'MaterialIcons'), + 'start': IconData(0xf0573, fontFamily: 'MaterialIcons'), + 'stay_current_landscape': IconData(0xe601, fontFamily: 'MaterialIcons'), + 'stay_current_portrait': IconData(0xe602, fontFamily: 'MaterialIcons'), + 'stay_primary_landscape': IconData(0xe603, fontFamily: 'MaterialIcons'), + 'stay_primary_portrait': IconData(0xe604, fontFamily: 'MaterialIcons'), + 'sticky_note_2': IconData(0xe605, fontFamily: 'MaterialIcons'), + 'stop': IconData(0xe606, fontFamily: 'MaterialIcons'), + 'stop_circle': IconData(0xe607, fontFamily: 'MaterialIcons'), + 'stop_screen_share': IconData(0xe608, fontFamily: 'MaterialIcons'), + 'storage': IconData(0xe609, fontFamily: 'MaterialIcons'), + 'store': IconData(0xe60a, fontFamily: 'MaterialIcons'), + 'store_mall_directory': IconData(0xe60b, fontFamily: 'MaterialIcons'), + 'storefront': IconData(0xe60c, fontFamily: 'MaterialIcons'), + 'storm': IconData(0xe60d, fontFamily: 'MaterialIcons'), + 'straight': IconData(0xf0574, fontFamily: 'MaterialIcons'), + 'straighten': IconData(0xe60e, fontFamily: 'MaterialIcons'), + 'stream': IconData(0xe60f, fontFamily: 'MaterialIcons'), + 'streetview': IconData(0xe610, fontFamily: 'MaterialIcons'), + 'strikethrough_s': IconData(0xe611, fontFamily: 'MaterialIcons'), + 'stroller': IconData(0xe612, fontFamily: 'MaterialIcons'), + 'style': IconData(0xe613, fontFamily: 'MaterialIcons'), + 'subdirectory_arrow_left': IconData(0xe614, fontFamily: 'MaterialIcons'), + 'subdirectory_arrow_right': IconData(0xe615, fontFamily: 'MaterialIcons'), + 'subject': + IconData(0xe616, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'subscript': IconData(0xe617, fontFamily: 'MaterialIcons'), + 'subscriptions': IconData(0xe618, fontFamily: 'MaterialIcons'), + 'subtitles': IconData(0xe619, fontFamily: 'MaterialIcons'), + 'subtitles_off': IconData(0xe61a, fontFamily: 'MaterialIcons'), + 'subway': IconData(0xe61b, fontFamily: 'MaterialIcons'), + 'summarize': IconData(0xe61c, fontFamily: 'MaterialIcons'), + 'sunny': IconData(0xf0575, fontFamily: 'MaterialIcons'), + 'sunny_snowing': IconData(0xf0576, fontFamily: 'MaterialIcons'), + 'superscript': IconData(0xe61d, fontFamily: 'MaterialIcons'), + 'supervised_user_circle': IconData(0xe61e, fontFamily: 'MaterialIcons'), + 'supervisor_account': IconData(0xe61f, fontFamily: 'MaterialIcons'), + 'support': IconData(0xe620, fontFamily: 'MaterialIcons'), + 'support_agent': IconData(0xe621, fontFamily: 'MaterialIcons'), + 'surfing': IconData(0xe622, fontFamily: 'MaterialIcons'), + 'swap_calls': IconData(0xe624, fontFamily: 'MaterialIcons'), + 'swap_horiz': IconData(0xe625, fontFamily: 'MaterialIcons'), + 'swap_horizontal_circle': IconData(0xe626, fontFamily: 'MaterialIcons'), + 'swap_vert': IconData(0xe627, fontFamily: 'MaterialIcons'), + 'swap_vert_circle': IconData(0xe628, fontFamily: 'MaterialIcons'), + 'swap_vertical_circle': IconData(0xe628, fontFamily: 'MaterialIcons'), + 'swipe': IconData(0xe629, fontFamily: 'MaterialIcons'), + 'swipe_down': IconData(0xf0578, fontFamily: 'MaterialIcons'), + 'swipe_down_alt': IconData(0xf0577, fontFamily: 'MaterialIcons'), + 'swipe_left': IconData(0xf057a, fontFamily: 'MaterialIcons'), + 'swipe_left_alt': IconData(0xf0579, fontFamily: 'MaterialIcons'), + 'swipe_right': IconData(0xf057c, fontFamily: 'MaterialIcons'), + 'swipe_right_alt': IconData(0xf057b, fontFamily: 'MaterialIcons'), + 'swipe_up': IconData(0xf057e, fontFamily: 'MaterialIcons'), + 'swipe_up_alt': IconData(0xf057d, fontFamily: 'MaterialIcons'), + 'swipe_vertical': IconData(0xf057f, fontFamily: 'MaterialIcons'), + 'switch_access_shortcut': IconData(0xf0581, fontFamily: 'MaterialIcons'), + 'switch_access_shortcut_add': IconData(0xf0580, fontFamily: 'MaterialIcons'), + 'switch_account': IconData(0xe62a, fontFamily: 'MaterialIcons'), + 'switch_camera': IconData(0xe62b, fontFamily: 'MaterialIcons'), + 'switch_left': IconData(0xe62c, fontFamily: 'MaterialIcons'), + 'switch_right': IconData(0xe62d, fontFamily: 'MaterialIcons'), + 'switch_video': IconData(0xe62e, fontFamily: 'MaterialIcons'), + 'synagogue': IconData(0xf0582, fontFamily: 'MaterialIcons'), + 'sync': IconData(0xe62f, fontFamily: 'MaterialIcons'), + 'sync_alt': IconData(0xe630, fontFamily: 'MaterialIcons'), + 'sync_disabled': IconData(0xe631, fontFamily: 'MaterialIcons'), + 'sync_lock': IconData(0xf0583, fontFamily: 'MaterialIcons'), + 'sync_problem': IconData(0xe632, fontFamily: 'MaterialIcons'), + 'system_security_update': IconData(0xe633, fontFamily: 'MaterialIcons'), + 'system_security_update_good': IconData(0xe634, fontFamily: 'MaterialIcons'), + 'system_security_update_warning': + IconData(0xe635, fontFamily: 'MaterialIcons'), + 'system_update': IconData(0xe636, fontFamily: 'MaterialIcons'), + 'system_update_alt': IconData(0xe637, fontFamily: 'MaterialIcons'), + 'system_update_tv': IconData(0xe637, fontFamily: 'MaterialIcons'), + 'tab': IconData(0xe638, fontFamily: 'MaterialIcons'), + 'tab_unselected': IconData(0xe639, fontFamily: 'MaterialIcons'), + 'table_bar': IconData(0xf0584, fontFamily: 'MaterialIcons'), + 'table_chart': IconData(0xe63a, fontFamily: 'MaterialIcons'), + 'table_restaurant': IconData(0xf0585, fontFamily: 'MaterialIcons'), + 'table_rows': IconData(0xe63b, fontFamily: 'MaterialIcons'), + 'table_view': IconData(0xe63c, fontFamily: 'MaterialIcons'), + 'tablet': IconData(0xe63d, fontFamily: 'MaterialIcons'), + 'tablet_android': IconData(0xe63e, fontFamily: 'MaterialIcons'), + 'tablet_mac': IconData(0xe63f, fontFamily: 'MaterialIcons'), + 'tag': IconData(0xe640, fontFamily: 'MaterialIcons'), + 'tag_faces': IconData(0xe641, fontFamily: 'MaterialIcons'), + 'takeout_dining': IconData(0xe642, fontFamily: 'MaterialIcons'), + 'tap_and_play': IconData(0xe643, fontFamily: 'MaterialIcons'), + 'tapas': IconData(0xe644, fontFamily: 'MaterialIcons'), + 'task': IconData(0xe645, fontFamily: 'MaterialIcons'), + 'task_alt': IconData(0xe646, fontFamily: 'MaterialIcons'), + 'taxi_alert': IconData(0xe647, fontFamily: 'MaterialIcons'), + 'telegram': IconData(0xf0586, fontFamily: 'MaterialIcons'), + 'temple_buddhist': IconData(0xf0587, fontFamily: 'MaterialIcons'), + 'temple_hindu': IconData(0xf0588, fontFamily: 'MaterialIcons'), + 'terminal': IconData(0xf0589, fontFamily: 'MaterialIcons'), + 'terrain': IconData(0xe648, fontFamily: 'MaterialIcons'), + 'text_decrease': IconData(0xf058a, fontFamily: 'MaterialIcons'), + 'text_fields': IconData(0xe649, fontFamily: 'MaterialIcons'), + 'text_format': IconData(0xe64a, fontFamily: 'MaterialIcons'), + 'text_increase': IconData(0xf058b, fontFamily: 'MaterialIcons'), + 'text_rotate_up': IconData(0xe64b, fontFamily: 'MaterialIcons'), + 'text_rotate_vertical': IconData(0xe64c, fontFamily: 'MaterialIcons'), + 'text_rotation_angledown': IconData(0xe64d, fontFamily: 'MaterialIcons'), + 'text_rotation_angleup': IconData(0xe64e, fontFamily: 'MaterialIcons'), + 'text_rotation_down': IconData(0xe64f, fontFamily: 'MaterialIcons'), + 'text_rotation_none': IconData(0xe650, fontFamily: 'MaterialIcons'), + 'text_snippet': IconData(0xe651, fontFamily: 'MaterialIcons'), + 'textsms': IconData(0xe652, fontFamily: 'MaterialIcons'), + 'texture': IconData(0xe653, fontFamily: 'MaterialIcons'), + 'theater_comedy': IconData(0xe654, fontFamily: 'MaterialIcons'), + 'theaters': IconData(0xe655, fontFamily: 'MaterialIcons'), + 'thermostat': IconData(0xe656, fontFamily: 'MaterialIcons'), + 'thermostat_auto': IconData(0xe657, fontFamily: 'MaterialIcons'), + 'thumb_down': IconData(0xe658, fontFamily: 'MaterialIcons'), + 'thumb_down_alt': IconData(0xe659, fontFamily: 'MaterialIcons'), + 'thumb_down_off_alt': IconData(0xe65a, fontFamily: 'MaterialIcons'), + 'thumb_up': IconData(0xe65b, fontFamily: 'MaterialIcons'), + 'thumb_up_alt': IconData(0xe65c, fontFamily: 'MaterialIcons'), + 'thumb_up_off_alt': IconData(0xe65d, fontFamily: 'MaterialIcons'), + 'thumbs_up_down': IconData(0xe65e, fontFamily: 'MaterialIcons'), + 'thunderstorm': IconData(0xf07cb, fontFamily: 'MaterialIcons'), + 'tiktok': IconData(0xf058c, fontFamily: 'MaterialIcons'), + 'time_to_leave': IconData(0xe65f, fontFamily: 'MaterialIcons'), + 'timelapse': IconData(0xe660, fontFamily: 'MaterialIcons'), + 'timeline': IconData(0xe661, fontFamily: 'MaterialIcons'), + 'timer': IconData(0xe662, fontFamily: 'MaterialIcons'), + 'timer_10': IconData(0xe663, fontFamily: 'MaterialIcons'), + 'timer_10_select': IconData(0xe664, fontFamily: 'MaterialIcons'), + 'timer_3': IconData(0xe665, fontFamily: 'MaterialIcons'), + 'timer_3_select': IconData(0xe666, fontFamily: 'MaterialIcons'), + 'timer_off': IconData(0xe667, fontFamily: 'MaterialIcons'), + 'tips_and_updates': IconData(0xf058d, fontFamily: 'MaterialIcons'), + 'tire_repair': IconData(0xf06c4, fontFamily: 'MaterialIcons'), + 'title': IconData(0xe668, fontFamily: 'MaterialIcons'), + 'toc': + IconData(0xe669, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'today': IconData(0xe66a, fontFamily: 'MaterialIcons'), + 'toggle_off': IconData(0xe66b, fontFamily: 'MaterialIcons'), + 'toggle_on': IconData(0xe66c, fontFamily: 'MaterialIcons'), + 'token': IconData(0xf058e, fontFamily: 'MaterialIcons'), + 'toll': IconData(0xe66d, fontFamily: 'MaterialIcons'), + 'tonality': IconData(0xe66e, fontFamily: 'MaterialIcons'), + 'topic': IconData(0xe66f, fontFamily: 'MaterialIcons'), + 'tornado': IconData(0xf07cc, fontFamily: 'MaterialIcons'), + 'touch_app': IconData(0xe670, fontFamily: 'MaterialIcons'), + 'tour': IconData(0xe671, fontFamily: 'MaterialIcons'), + 'toys': IconData(0xe672, fontFamily: 'MaterialIcons'), + 'track_changes': IconData(0xe673, fontFamily: 'MaterialIcons'), + 'traffic': IconData(0xe674, fontFamily: 'MaterialIcons'), + 'train': IconData(0xe675, fontFamily: 'MaterialIcons'), + 'tram': IconData(0xe676, fontFamily: 'MaterialIcons'), + 'transcribe': IconData(0xf07cd, fontFamily: 'MaterialIcons'), + 'transfer_within_a_station': IconData(0xe677, fontFamily: 'MaterialIcons'), + 'transform': IconData(0xe678, fontFamily: 'MaterialIcons'), + 'transgender': IconData(0xe679, fontFamily: 'MaterialIcons'), + 'transit_enterexit': IconData(0xe67a, fontFamily: 'MaterialIcons'), + 'translate': IconData(0xe67b, fontFamily: 'MaterialIcons'), + 'travel_explore': IconData(0xe67c, fontFamily: 'MaterialIcons'), + 'trending_down': + IconData(0xe67d, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'trending_flat': + IconData(0xe67e, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'trending_neutral': IconData(0xe67e, fontFamily: 'MaterialIcons'), + 'trending_up': + IconData(0xe67f, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'trip_origin': IconData(0xe680, fontFamily: 'MaterialIcons'), + 'trolley': IconData(0xf0878, fontFamily: 'MaterialIcons'), + 'troubleshoot': IconData(0xf07ce, fontFamily: 'MaterialIcons'), + 'try': IconData(0xe681, fontFamily: 'MaterialIcons'), + 'tsunami': IconData(0xf07cf, fontFamily: 'MaterialIcons'), + 'tty': IconData(0xe682, fontFamily: 'MaterialIcons'), + 'tune': IconData(0xe683, fontFamily: 'MaterialIcons'), + 'tungsten': IconData(0xe684, fontFamily: 'MaterialIcons'), + 'turn_left': IconData(0xf058f, fontFamily: 'MaterialIcons'), + 'turn_right': IconData(0xf0590, fontFamily: 'MaterialIcons'), + 'turn_slight_left': IconData(0xf0593, fontFamily: 'MaterialIcons'), + 'turn_slight_right': IconData(0xf0594, fontFamily: 'MaterialIcons'), + 'turned_in': IconData(0xe685, fontFamily: 'MaterialIcons'), + 'turned_in_not': IconData(0xe686, fontFamily: 'MaterialIcons'), + 'tv': IconData(0xe687, fontFamily: 'MaterialIcons'), + 'tv_off': IconData(0xe688, fontFamily: 'MaterialIcons'), + 'two_wheeler': IconData(0xe689, fontFamily: 'MaterialIcons'), + 'type_specimen': IconData(0xf07d0, fontFamily: 'MaterialIcons'), + 'u_turn_left': IconData(0xf0595, fontFamily: 'MaterialIcons'), + 'u_turn_right': IconData(0xf0596, fontFamily: 'MaterialIcons'), + 'umbrella': IconData(0xe68a, fontFamily: 'MaterialIcons'), + 'unarchive': IconData(0xe68b, fontFamily: 'MaterialIcons'), + 'undo': + IconData(0xe68c, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'unfold_less': IconData(0xe68d, fontFamily: 'MaterialIcons'), + 'unfold_less_double': IconData(0xf0879, fontFamily: 'MaterialIcons'), + 'unfold_more': IconData(0xe68e, fontFamily: 'MaterialIcons'), + 'unfold_more_double': IconData(0xf087a, fontFamily: 'MaterialIcons'), + 'unpublished': IconData(0xe68f, fontFamily: 'MaterialIcons'), + 'unsubscribe': IconData(0xe690, fontFamily: 'MaterialIcons'), + 'upcoming': IconData(0xe691, fontFamily: 'MaterialIcons'), + 'update': IconData(0xe692, fontFamily: 'MaterialIcons'), + 'update_disabled': IconData(0xe693, fontFamily: 'MaterialIcons'), + 'upgrade': IconData(0xe694, fontFamily: 'MaterialIcons'), + 'upload': IconData(0xe695, fontFamily: 'MaterialIcons'), + 'upload_file': IconData(0xe696, fontFamily: 'MaterialIcons'), + 'usb': IconData(0xe697, fontFamily: 'MaterialIcons'), + 'usb_off': IconData(0xe698, fontFamily: 'MaterialIcons'), + 'vaccines': IconData(0xf0597, fontFamily: 'MaterialIcons'), + 'vape_free': IconData(0xf06c5, fontFamily: 'MaterialIcons'), + 'vaping_rooms': IconData(0xf06c6, fontFamily: 'MaterialIcons'), + 'verified': IconData(0xe699, fontFamily: 'MaterialIcons'), + 'verified_user': IconData(0xe69a, fontFamily: 'MaterialIcons'), + 'vertical_align_bottom': IconData(0xe69b, fontFamily: 'MaterialIcons'), + 'vertical_align_center': IconData(0xe69c, fontFamily: 'MaterialIcons'), + 'vertical_align_top': IconData(0xe69d, fontFamily: 'MaterialIcons'), + 'vertical_distribute': IconData(0xe69e, fontFamily: 'MaterialIcons'), + 'vertical_shades': IconData(0xf07d1, fontFamily: 'MaterialIcons'), + 'vertical_shades_closed': IconData(0xf07d2, fontFamily: 'MaterialIcons'), + 'vertical_split': IconData(0xe69f, fontFamily: 'MaterialIcons'), + 'vibration': IconData(0xe6a0, fontFamily: 'MaterialIcons'), + 'video_call': IconData(0xe6a1, fontFamily: 'MaterialIcons'), + 'video_camera_back': IconData(0xe6a2, fontFamily: 'MaterialIcons'), + 'video_camera_front': IconData(0xe6a3, fontFamily: 'MaterialIcons'), + 'video_chat': IconData(0xf087b, fontFamily: 'MaterialIcons'), + 'video_collection': IconData(0xe6a5, fontFamily: 'MaterialIcons'), + 'video_file': IconData(0xf0598, fontFamily: 'MaterialIcons'), + 'video_label': IconData(0xe6a4, fontFamily: 'MaterialIcons'), + 'video_library': IconData(0xe6a5, fontFamily: 'MaterialIcons'), + 'video_settings': IconData(0xe6a6, fontFamily: 'MaterialIcons'), + 'video_stable': IconData(0xe6a7, fontFamily: 'MaterialIcons'), + 'videocam': IconData(0xe6a8, fontFamily: 'MaterialIcons'), + 'videocam_off': IconData(0xe6a9, fontFamily: 'MaterialIcons'), + 'videogame_asset': IconData(0xe6aa, fontFamily: 'MaterialIcons'), + 'videogame_asset_off': IconData(0xe6ab, fontFamily: 'MaterialIcons'), + 'view_agenda': IconData(0xe6ac, fontFamily: 'MaterialIcons'), + 'view_array': IconData(0xe6ad, fontFamily: 'MaterialIcons'), + 'view_carousel': IconData(0xe6ae, fontFamily: 'MaterialIcons'), + 'view_column': IconData(0xe6af, fontFamily: 'MaterialIcons'), + 'view_comfortable': IconData(0xe6b0, fontFamily: 'MaterialIcons'), + 'view_comfy': IconData(0xe6b0, fontFamily: 'MaterialIcons'), + 'view_comfy_alt': IconData(0xf0599, fontFamily: 'MaterialIcons'), + 'view_compact': IconData(0xe6b1, fontFamily: 'MaterialIcons'), + 'view_compact_alt': IconData(0xf059a, fontFamily: 'MaterialIcons'), + 'view_cozy': IconData(0xf059b, fontFamily: 'MaterialIcons'), + 'view_day': IconData(0xe6b2, fontFamily: 'MaterialIcons'), + 'view_headline': IconData(0xe6b3, fontFamily: 'MaterialIcons'), + 'view_in_ar': IconData(0xe6b4, fontFamily: 'MaterialIcons'), + 'view_kanban': IconData(0xf059c, fontFamily: 'MaterialIcons'), + 'view_list': + IconData(0xe6b5, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'view_module': IconData(0xe6b6, fontFamily: 'MaterialIcons'), + 'view_quilt': + IconData(0xe6b7, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'view_sidebar': IconData(0xe6b8, fontFamily: 'MaterialIcons'), + 'view_stream': IconData(0xe6b9, fontFamily: 'MaterialIcons'), + 'view_timeline': IconData(0xf059d, fontFamily: 'MaterialIcons'), + 'view_week': IconData(0xe6ba, fontFamily: 'MaterialIcons'), + 'vignette': IconData(0xe6bb, fontFamily: 'MaterialIcons'), + 'villa': IconData(0xe6bc, fontFamily: 'MaterialIcons'), + 'visibility': IconData(0xe6bd, fontFamily: 'MaterialIcons'), + 'visibility_off': IconData(0xe6be, fontFamily: 'MaterialIcons'), + 'voice_chat': IconData(0xe6bf, fontFamily: 'MaterialIcons'), + 'voice_over_off': IconData(0xe6c0, fontFamily: 'MaterialIcons'), + 'voicemail': IconData(0xe6c1, fontFamily: 'MaterialIcons'), + 'volcano': IconData(0xf07d3, fontFamily: 'MaterialIcons'), + 'volume_down': IconData(0xe6c2, fontFamily: 'MaterialIcons'), + 'volume_down_alt': IconData(0xf059e, fontFamily: 'MaterialIcons'), + 'volume_mute': IconData(0xe6c3, fontFamily: 'MaterialIcons'), + 'volume_off': IconData(0xe6c4, fontFamily: 'MaterialIcons'), + 'volume_up': IconData(0xe6c5, fontFamily: 'MaterialIcons'), + 'volunteer_activism': IconData(0xe6c6, fontFamily: 'MaterialIcons'), + 'vpn_key': IconData(0xe6c7, fontFamily: 'MaterialIcons'), + 'vpn_key_off': IconData(0xf059f, fontFamily: 'MaterialIcons'), + 'vpn_lock': IconData(0xe6c8, fontFamily: 'MaterialIcons'), + 'vrpano': IconData(0xe6c9, fontFamily: 'MaterialIcons'), + 'wallet': IconData(0xf07d4, fontFamily: 'MaterialIcons'), + 'wallet_giftcard': IconData(0xe13e, fontFamily: 'MaterialIcons'), + 'wallet_membership': IconData(0xe13f, fontFamily: 'MaterialIcons'), + 'wallet_travel': IconData(0xe140, fontFamily: 'MaterialIcons'), + 'wallpaper': IconData(0xe6ca, fontFamily: 'MaterialIcons'), + 'warehouse': IconData(0xf05a0, fontFamily: 'MaterialIcons'), + 'warning': IconData(0xe6cb, fontFamily: 'MaterialIcons'), + 'warning_amber': IconData(0xe6cc, fontFamily: 'MaterialIcons'), + 'wash': IconData(0xe6cd, fontFamily: 'MaterialIcons'), + 'watch': IconData(0xe6ce, fontFamily: 'MaterialIcons'), + 'watch_later': IconData(0xe6cf, fontFamily: 'MaterialIcons'), + 'watch_off': IconData(0xf05a1, fontFamily: 'MaterialIcons'), + 'water': IconData(0xe6d0, fontFamily: 'MaterialIcons'), + 'water_damage': IconData(0xe6d1, fontFamily: 'MaterialIcons'), + 'water_drop': IconData(0xf05a2, fontFamily: 'MaterialIcons'), + 'waterfall_chart': IconData(0xe6d2, fontFamily: 'MaterialIcons'), + 'waves': IconData(0xe6d3, fontFamily: 'MaterialIcons'), + 'waving_hand': IconData(0xf05a3, fontFamily: 'MaterialIcons'), + 'wb_auto': IconData(0xe6d4, fontFamily: 'MaterialIcons'), + 'wb_cloudy': IconData(0xe6d5, fontFamily: 'MaterialIcons'), + 'wb_incandescent': IconData(0xe6d6, fontFamily: 'MaterialIcons'), + 'wb_iridescent': IconData(0xe6d7, fontFamily: 'MaterialIcons'), + 'wb_shade': IconData(0xe6d8, fontFamily: 'MaterialIcons'), + 'wb_sunny': IconData(0xe6d9, fontFamily: 'MaterialIcons'), + 'wb_twighlight': IconData(0xe6da, fontFamily: 'MaterialIcons'), + 'wb_twilight': IconData(0xe6db, fontFamily: 'MaterialIcons'), + 'wc': IconData(0xe6dc, fontFamily: 'MaterialIcons'), + 'web': IconData(0xe6dd, fontFamily: 'MaterialIcons'), + 'web_asset': IconData(0xe6de, fontFamily: 'MaterialIcons'), + 'web_asset_off': IconData(0xe6df, fontFamily: 'MaterialIcons'), + 'web_stories': IconData(0xe6e0, fontFamily: 'MaterialIcons'), + 'webhook': IconData(0xf05a4, fontFamily: 'MaterialIcons'), + 'wechat': IconData(0xf05a5, fontFamily: 'MaterialIcons'), + 'weekend': IconData(0xe6e1, fontFamily: 'MaterialIcons'), + 'west': IconData(0xe6e2, fontFamily: 'MaterialIcons'), + 'whatshot': IconData(0xe6e3, fontFamily: 'MaterialIcons'), + 'wheelchair_pickup': IconData(0xe6e4, fontFamily: 'MaterialIcons'), + 'where_to_vote': IconData(0xe6e5, fontFamily: 'MaterialIcons'), + 'widgets': IconData(0xe6e6, fontFamily: 'MaterialIcons'), + 'width_full': IconData(0xf07d5, fontFamily: 'MaterialIcons'), + 'width_normal': IconData(0xf07d6, fontFamily: 'MaterialIcons'), + 'width_wide': IconData(0xf07d7, fontFamily: 'MaterialIcons'), + 'wifi': IconData(0xe6e7, fontFamily: 'MaterialIcons'), + 'wifi_1_bar': IconData(0xf07d8, fontFamily: 'MaterialIcons'), + 'wifi_2_bar': IconData(0xf07d9, fontFamily: 'MaterialIcons'), + 'wifi_calling': IconData(0xe6e8, fontFamily: 'MaterialIcons'), + 'wifi_calling_3': IconData(0xe6e9, fontFamily: 'MaterialIcons'), + 'wifi_channel': IconData(0xf05a7, fontFamily: 'MaterialIcons'), + 'wifi_find': IconData(0xf05a8, fontFamily: 'MaterialIcons'), + 'wifi_lock': IconData(0xe6ea, fontFamily: 'MaterialIcons'), + 'wifi_off': IconData(0xe6eb, fontFamily: 'MaterialIcons'), + 'wifi_password': IconData(0xf05a9, fontFamily: 'MaterialIcons'), + 'wifi_protected_setup': IconData(0xe6ec, fontFamily: 'MaterialIcons'), + 'wifi_tethering': IconData(0xe6ed, fontFamily: 'MaterialIcons'), + 'wifi_tethering_error': IconData(0xf05aa, fontFamily: 'MaterialIcons'), + 'wifi_tethering_off': IconData(0xe6ef, fontFamily: 'MaterialIcons'), + 'wind_power': IconData(0xf07da, fontFamily: 'MaterialIcons'), + 'window': IconData(0xe6f0, fontFamily: 'MaterialIcons'), + 'wine_bar': IconData(0xe6f1, fontFamily: 'MaterialIcons'), + 'woman': IconData(0xf05ab, fontFamily: 'MaterialIcons'), + 'woman_2': IconData(0xf087c, fontFamily: 'MaterialIcons'), + 'woo_commerce': IconData(0xf05ac, fontFamily: 'MaterialIcons'), + 'wordpress': IconData(0xf05ad, fontFamily: 'MaterialIcons'), + 'work': IconData(0xe6f2, fontFamily: 'MaterialIcons'), + 'work_history': IconData(0xf07db, fontFamily: 'MaterialIcons'), + 'work_off': IconData(0xe6f3, fontFamily: 'MaterialIcons'), + 'work_outline': IconData(0xe6f4, fontFamily: 'MaterialIcons'), + 'workspace_premium': IconData(0xf05ae, fontFamily: 'MaterialIcons'), + 'workspaces': IconData(0xe6f5, fontFamily: 'MaterialIcons'), + 'workspaces_filled': IconData(0xe6f6, fontFamily: 'MaterialIcons'), + 'workspaces_outline': IconData(0xe6f7, fontFamily: 'MaterialIcons'), + 'wrap_text': + IconData(0xe6f8, fontFamily: 'MaterialIcons', matchTextDirection: true), + 'wrong_location': IconData(0xe6f9, fontFamily: 'MaterialIcons'), + 'wysiwyg': IconData(0xe6fa, fontFamily: 'MaterialIcons'), + 'yard': IconData(0xe6fb, fontFamily: 'MaterialIcons'), + 'youtube_searched_for': IconData(0xe6fc, fontFamily: 'MaterialIcons'), + 'zoom_in': IconData(0xe6fd, fontFamily: 'MaterialIcons'), + 'zoom_in_map': IconData(0xf05af, fontFamily: 'MaterialIcons'), + 'zoom_out': IconData(0xe6fe, fontFamily: 'MaterialIcons'), + 'zoom_out_map': IconData(0xe6ff, fontFamily: 'MaterialIcons'), +}; diff --git a/lib/modules/notification/widgets/notification_list.dart b/lib/modules/notification/widgets/notification_list.dart new file mode 100644 index 0000000..64c61d9 --- /dev/null +++ b/lib/modules/notification/widgets/notification_list.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/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/widgets/tb_progress_indicator.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NotificationsList extends StatelessWidget { + NotificationsList({ + required this.pagingController, + required this.thingsboardClient, + required this.onClearNotification, + required this.onReadNotification, + required this.tbContext, + }); + + final ThingsboardClient thingsboardClient; + final Function(String id, bool read) onClearNotification; + final ValueChanged onReadNotification; + final TbContext tbContext; + final PagingController pagingController; + + @override + Widget build(BuildContext context) { + return PagedListView.separated( + pagingController: pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return NotificationSlidableWidget( + child: NotificationWidget( + notification: item as PushNotification, + thingsboardClient: thingsboardClient, + onClearNotification: onClearNotification, + onReadNotification: onReadNotification, + tbContext: tbContext, + ), + notification: item, + onReadNotification: onReadNotification, + onClearNotification: onClearNotification, + tbContext: tbContext, + thingsboardClient: thingsboardClient, + ); + }, + firstPageProgressIndicatorBuilder: (_) => SizedBox.expand( + child: Container( + color: const Color(0x99FFFFFF), + child: const Center( + child: TbProgressIndicator( + size: 50.0, + ), + ), + ), + ), + noItemsFoundIndicatorBuilder: (_) => const NoNotificationsFoundWidget(), + ), + separatorBuilder: (_, __) => const Divider(thickness: 1), + ); + } +} diff --git a/lib/modules/notification/widgets/notification_slidable_widget.dart b/lib/modules/notification/widgets/notification_slidable_widget.dart new file mode 100644 index 0000000..03117b2 --- /dev/null +++ b/lib/modules/notification/widgets/notification_slidable_widget.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NotificationSlidableWidget extends StatefulWidget { + const NotificationSlidableWidget({ + required this.child, + required this.notification, + required this.thingsboardClient, + required this.onClearNotification, + required this.onReadNotification, + required this.tbContext, + }); + + final Widget child; + final PushNotification notification; + final ThingsboardClient thingsboardClient; + final Function(String id, bool read) onClearNotification; + final ValueChanged onReadNotification; + final TbContext tbContext; + + @override + State createState() => _NotificationSlidableWidget(); +} + +class _NotificationSlidableWidget extends State { + bool loading = false; + + @override + Widget build(BuildContext context) { + if (loading) { + return Container( + height: 134, + alignment: Alignment.center, + child: RefreshProgressIndicator(), + ); + } + + return Slidable( + key: ValueKey(widget.notification.id!.id), + child: widget.child, + startActionPane: widget.notification.status == PushNotificationStatus.READ + ? null + : ActionPane( + extentRatio: 0.3, + motion: const ScrollMotion(), + children: [ + SlidableAction( + onPressed: (context) => widget.onReadNotification( + widget.notification.id!.id!, + ), + backgroundColor: Color(0xFF198038), + foregroundColor: Colors.white, + icon: Icons.check_circle_outline, + label: 'Mark as read', + borderRadius: BorderRadius.circular(8), + padding: const EdgeInsets.symmetric(horizontal: 4), + ), + ], + ), + endActionPane: ActionPane( + extentRatio: _endExtentRatio(widget.notification), + motion: const ScrollMotion(), + children: [ + ..._buildAlarmRelatedButtons(widget.notification), + SlidableAction( + onPressed: (context) { + widget.onClearNotification( + widget.notification.id!.id!, + widget.notification.status == PushNotificationStatus.READ, + ); + }, + backgroundColor: Color(0xFFD12730).withOpacity(0.94), + foregroundColor: Colors.white, + icon: Icons.delete, + label: 'Delete', + borderRadius: _buildAlarmRelatedButtons(widget.notification).isEmpty + ? BorderRadius.circular(8) + : BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + ], + ), + ); + } + + List _buildAlarmRelatedButtons(PushNotification notification) { + final items = []; + + final type = notification.type; + if (type == PushNotificationType.ALARM) { + final status = notification.info?.alarmStatus; + final id = notification.info?.alarmId; + + if (id != null) { + if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK] + .contains(status)) { + items.add( + SlidableAction( + onPressed: (context) => _ackAlarm(id, context), + backgroundColor: Color(0xFF198038), + foregroundColor: Colors.white, + icon: Icons.done, + label: 'Acknowledge', + padding: const EdgeInsets.symmetric(horizontal: 4), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + ); + } + + if ([AlarmStatus.ACTIVE_UNACK, AlarmStatus.ACTIVE_ACK] + .contains(status)) { + items.add( + SlidableAction( + onPressed: (context) => _clearAlarm(id, context), + backgroundColor: Color(0xFF757575), + foregroundColor: Colors.white, + icon: Icons.clear, + label: 'Clear', + borderRadius: items.isEmpty + ? BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ) + : BorderRadius.zero, + ), + ); + } + } + } + + return items; + } + + 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}'); + + if (res != null && res) { + setState(() { + loading = true; + }); + try { + await widget.thingsboardClient.getAlarmService().ackAlarm(alarmId); + } catch (_) {} + + setState(() { + loading = false; + widget.onReadNotification(widget.notification.id!.id!); + }); + } + } + + 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}'); + if (res != null && res) { + setState(() { + loading = true; + }); + + try { + await widget.thingsboardClient.getAlarmService().clearAlarm(alarmId); + } catch (_) {} + + setState(() { + loading = false; + widget.onReadNotification(widget.notification.id!.id!); + }); + } + } + + double _endExtentRatio(PushNotification notification) { + final items = _buildAlarmRelatedButtons(notification); + if (items.isEmpty) { + return 0.27; + } else if (items.length == 1) { + return 0.53; + } + + return 0.8; + } +} diff --git a/lib/modules/notification/widgets/notification_widget.dart b/lib/modules/notification/widgets/notification_widget.dart new file mode 100644 index 0000000..7025b28 --- /dev/null +++ b/lib/modules/notification/widgets/notification_widget.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +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/utils/services/notification_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class NotificationWidget extends StatelessWidget { + const NotificationWidget({ + required this.notification, + required this.thingsboardClient, + required this.onClearNotification, + required this.onReadNotification, + required this.tbContext, + }); + + final PushNotification notification; + final ThingsboardClient thingsboardClient; + final Function(String id, bool readed) onClearNotification; + final ValueChanged onReadNotification; + final TbContext tbContext; + + @override + Widget build(BuildContext context) { + final diff = DateTime.now().difference( + DateTime.fromMillisecondsSinceEpoch(notification.createdTime!), + ); + + final severity = notification.info?.alarmSeverity; + + return InkWell( + onTap: () { + NotificationService.handleClickOnNotification( + notification.additionalConfig?['onClick'] ?? {}, + tbContext, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + decoration: BoxDecoration( + border: notification.info?.alarmSeverity != null + ? Border.all( + color: alarmSeverityColors[notification.info?.alarmSeverity]!, + ) + : null, + borderRadius: BorderRadius.circular(5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + NotificationIcon(notification: notification), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 7), + child: Text( + notification.subject, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + Flexible( + child: Html( + data: notification.text, + ), + ), + ], + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + width: 30, + child: Text( + timeago.format( + DateTime.now().subtract(diff), + locale: 'en_short', + ), + textAlign: TextAlign.center, + ), + ), + Row( + children: [ + Visibility( + visible: notification.status != + PushNotificationStatus.READ, + child: SizedBox( + width: 30, + height: 50, + child: IconButton( + onPressed: () => onReadNotification( + notification.id!.id!, + ), + icon: Icon( + Icons.check_circle_outline, + color: Colors.black.withOpacity(0.38), + ), + ), + ), + ), + Visibility( + visible: notification.status == + PushNotificationStatus.READ, + child: SizedBox( + width: 30, + height: 50, + ), + ), + ], + ), + Visibility( + visible: severity != null, + child: Container( + decoration: BoxDecoration( + color: + alarmSeverityColors[severity]?.withOpacity(0.1), + borderRadius: BorderRadius.circular(5), + ), + padding: const EdgeInsets.all(5), + child: Text( + alarmSeverityTranslations[severity] ?? '', + style: TextStyle( + color: alarmSeverityColors[AlarmSeverity.CRITICAL]!, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/profile/change_password_page.dart b/lib/modules/profile/change_password_page.dart new file mode 100644 index 0000000..cba6f88 --- /dev/null +++ b/lib/modules/profile/change_password_page.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; + +class ChangePasswordPage extends TbContextWidget { + ChangePasswordPage(TbContext tbContext) : super(tbContext); + + @override + _ChangePasswordPageState createState() => _ChangePasswordPageState(); +} + +class _ChangePasswordPageState extends TbContextState { + final _isLoadingNotifier = ValueNotifier(false); + + final _showCurrentPasswordNotifier = ValueNotifier(false); + final _showNewPasswordNotifier = ValueNotifier(false); + final _showNewPassword2Notifier = ValueNotifier(false); + + final _changePasswordFormKey = GlobalKey(); + + @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}'))) + ]), + ))), + ), + 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(); + } + }) + ], + )); + } + + Future _changePassword() async { + FocusScope.of(context).unfocus(); + if (_changePasswordFormKey.currentState?.saveAndValidate() ?? false) { + var formValue = _changePasswordFormKey.currentState!.value; + String currentPassword = formValue['currentPassword']; + String newPassword = formValue['newPassword']; + String newPassword2 = formValue['newPassword2']; + if (newPassword != newPassword2) { + showErrorNotification('${S.of(context).passwordErrorNotification}'); + } else { + _isLoadingNotifier.value = true; + try { + await Future.delayed(Duration(milliseconds: 300)); + await tbClient.changePassword(currentPassword, newPassword); + pop(true); + } catch (e) { + _isLoadingNotifier.value = false; + } + } + } + } +} diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart new file mode 100644 index 0000000..01baddb --- /dev/null +++ b/lib/modules/profile/profile_page.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_app/modules/profile/change_password_page.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class ProfilePage extends TbPageWidget { + final bool _fullscreen; + + ProfilePage(TbContext tbContext, {bool fullscreen = false}) + : _fullscreen = fullscreen, + super(tbContext); + + @override + _ProfilePageState createState() => _ProfilePageState(); +} + +class _ProfilePageState extends TbPageState { + final _isLoadingNotifier = ValueNotifier(true); + + final _profileFormKey = GlobalKey(); + + User? _currentUser; + + @override + void initState() { + super.initState(); + _loadUser(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: TbAppBar( + tbContext, + title: const Text('Profile'), + actions: [ + 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}'), + ), + SizedBox(height: 24), + FormBuilderTextField( + name: 'lastName', + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: '${S.of(context).lastNameUpper}'), + ), + SizedBox(height: 24), + OutlinedButton( + style: OutlinedButton.styleFrom( + padding: 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(); + } + }) + ], + )); + } + + Future _loadUser() async { + _isLoadingNotifier.value = true; + _currentUser = await tbClient.getUserService().getUser(); + _setUser(); + _isLoadingNotifier.value = false; + } + + _setUser() { + _profileFormKey.currentState?.patchValue({ + 'email': _currentUser!.email, + 'firstName': _currentUser!.firstName ?? '', + 'lastName': _currentUser!.lastName ?? '' + }); + } + + Future _saveProfile() async { + if (_currentUser != null) { + FocusScope.of(context).unfocus(); + if (_profileFormKey.currentState?.saveAndValidate() ?? false) { + var 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)); + } + } + } + + _changePassword() async { + var res = await tbContext + .showFullScreenDialog(new ChangePasswordPage(tbContext)); + if (res == true) { + showSuccessNotification('${S.of(context).passwordSuccessNotification}', + duration: Duration(milliseconds: 1500)); + } + } +} diff --git a/lib/modules/profile/profile_routes.dart b/lib/modules/profile/profile_routes.dart new file mode 100644 index 0000000..1d48089 --- /dev/null +++ b/lib/modules/profile/profile_routes.dart @@ -0,0 +1,21 @@ +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 '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); + }); + + ProfileRoutes(TbContext tbContext) : super(tbContext); + + @override + void doRegisterRoutes(router) { + router.define("/profile", handler: profileHandler); + } +} diff --git a/lib/modules/tenant/tenant_details_page.dart b/lib/modules/tenant/tenant_details_page.dart new file mode 100644 index 0000000..31e0d66 --- /dev/null +++ b/lib/modules/tenant/tenant_details_page.dart @@ -0,0 +1,16 @@ +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'; + +class TenantDetailsPage extends ContactBasedDetailsPage { + TenantDetailsPage(TbContext tbContext, String tenantId) + : super(tbContext, + entityId: tenantId, + defaultTitle: 'Tenant', + subTitle: 'Tenant details'); + + @override + Future fetchEntity(String tenantId) { + return tbClient.getTenantService().getTenant(tenantId); + } +} diff --git a/lib/modules/tenant/tenant_routes.dart b/lib/modules/tenant/tenant_routes.dart new file mode 100644 index 0000000..365667d --- /dev/null +++ b/lib/modules/tenant/tenant_routes.dart @@ -0,0 +1,27 @@ +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); + }); + + late var tenantDetailsHandler = Handler( + 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); + } +} diff --git a/lib/modules/tenant/tenants_base.dart b/lib/modules/tenant/tenants_base.dart new file mode 100644 index 0000000..a61dabe --- /dev/null +++ b/lib/modules/tenant/tenants_base.dart @@ -0,0 +1,20 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +mixin TenantsBase on EntitiesBase { + @override + String get title => 'Tenants'; + + @override + String get noItemsFoundText => 'No tenants found'; + + @override + Future> fetchEntities(PageLink pageLink) { + return tbClient.getTenantService().getTenants(pageLink); + } + + @override + void onEntityTap(Tenant tenant) { + navigateTo('/tenant/${tenant.id!.id}'); + } +} diff --git a/lib/modules/tenant/tenants_list.dart b/lib/modules/tenant/tenants_list.dart new file mode 100644 index 0000000..543d33b --- /dev/null +++ b/lib/modules/tenant/tenants_list.dart @@ -0,0 +1,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_client/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); +} diff --git a/lib/modules/tenant/tenants_page.dart b/lib/modules/tenant/tenants_page.dart new file mode 100644 index 0000000..77e7b93 --- /dev/null +++ b/lib/modules/tenant/tenants_page.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +import 'tenants_list.dart'; + +class TenantsPage extends TbPageWidget { + final bool searchMode; + + TenantsPage(TbContext tbContext, {this.searchMode = false}) + : super(tbContext); + + @override + _TenantsPageState createState() => _TenantsPageState(); +} + +class _TenantsPageState extends TbPageState { + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + var tenantsList = TenantsList(tbContext, _pageLinkController, + searchMode: widget.searchMode); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => _pageLinkController.onSearchText(searchText), + ); + } else { + appBar = TbAppBar(tbContext, title: Text(tenantsList.title), actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () { + navigateTo('/tenants?search=true'); + }, + ) + ]); + } + return Scaffold(appBar: appBar, body: tenantsList); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/tenant/tenants_widget.dart b/lib/modules/tenant/tenants_widget.dart new file mode 100644 index 0000000..7f5f66e --- /dev/null +++ b/lib/modules/tenant/tenants_widget.dart @@ -0,0 +1,28 @@ +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 'tenants_list.dart'; + +class TenantsWidget extends TbContextWidget { + TenantsWidget(TbContext tbContext) : super(tbContext); + + @override + _TenantsWidgetState createState() => _TenantsWidgetState(); +} + +class _TenantsWidgetState extends TbContextState { + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + return TenantsList(tbContext, _pageLinkController); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } +} diff --git a/lib/modules/url/url_page.dart b/lib/modules/url/url_page.dart new file mode 100644 index 0000000..02b3af0 --- /dev/null +++ b/lib/modules/url/url_page.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; +import 'package:universal_platform/universal_platform.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class UrlPage extends TbPageWidget { + UrlPage({ + required this.url, + required TbContext tbContext, + super.key, + }) : super(tbContext); + + final String url; + + @override + State createState() => _UrlPageState(); +} + +class _UrlPageState extends TbPageState { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TbAppBar( + tbContext, + title: const Text('Url'), + actions: [ + IconButton( + onPressed: () { + launchUrlString(widget.url, mode: LaunchMode.externalApplication); + }, + icon: Icon(Icons.open_in_browser), + ), + ], + ), + body: UniversalPlatform.isWeb + ? const Center(child: Text('Not implemented!')) + : InAppWebView( + initialUrlRequest: URLRequest( + url: Uri.parse(widget.url), + ), + androidOnPermissionRequest: + (controller, origin, resources) async { + return PermissionRequestResponse( + resources: resources, + action: PermissionRequestResponseAction.GRANT, + ); + }, + ), + ); + } +} diff --git a/lib/modules/url/url_routes.dart b/lib/modules/url/url_routes.dart new file mode 100644 index 0000000..3603664 --- /dev/null +++ b/lib/modules/url/url_routes.dart @@ -0,0 +1,24 @@ +import 'package:fluro/fluro.dart'; +import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/modules/url/url_page.dart'; + +class UrlPageRoutes extends TbRoutes { + UrlPageRoutes(TbContext tbContext) : super(tbContext); + + static const urlPageRoutes = '/url/:link'; + + late final urlPageHandler = Handler( + handlerFunc: (context, params) { + return UrlPage( + url: Uri.decodeQueryComponent(params['link']?.first ?? ''), + tbContext: tbContext, + ); + }, + ); + + @override + void doRegisterRoutes(router) { + router.define(urlPageRoutes, handler: urlPageHandler); + } +} diff --git a/lib/utils/services/_tb_app_storage.dart b/lib/utils/services/_tb_app_storage.dart new file mode 100644 index 0000000..199bd02 --- /dev/null +++ b/lib/utils/services/_tb_app_storage.dart @@ -0,0 +1,3 @@ +import 'package:thingsboard_client/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 new file mode 100644 index 0000000..d2201a0 --- /dev/null +++ b/lib/utils/services/_tb_secure_storage.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +TbStorage createAppStorage() => TbSecureStorage(); + +class TbSecureStorage implements TbStorage { + late Box encryptedBox; + + Future init() async { + const secureStorage = FlutterSecureStorage(); + // if key not exists return null + final encryptionKeyString = await secureStorage.read(key: 'key'); + if (encryptionKeyString == null) { + final key = Hive.generateSecureKey(); + await secureStorage.write( + key: 'key', + value: base64UrlEncode(key), + ); + } + + final key = await secureStorage.read(key: 'key'); + final encryptionKeyUint8List = base64Url.decode(key!); + + encryptedBox = await Hive.openBox( + 'securedStorage', + encryptionCipher: HiveAesCipher(encryptionKeyUint8List), + ); + } + + @override + Future deleteItem(String key) async { + return await encryptedBox.delete(key); + } + + @override + Future getItem(String key) async { + return await encryptedBox.get(key); + } + + @override + Future setItem(String key, String value) async { + return await encryptedBox.put(key, value); + } +} diff --git a/lib/utils/services/_tb_web_local_storage.dart b/lib/utils/services/_tb_web_local_storage.dart new file mode 100644 index 0000000..5d1647e --- /dev/null +++ b/lib/utils/services/_tb_web_local_storage.dart @@ -0,0 +1,23 @@ +import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:universal_html/html.dart' as html; + +TbStorage createAppStorage() => TbWebLocalStorage(); + +class TbWebLocalStorage implements TbStorage { + final html.Storage _localStorage = html.window.localStorage; + + @override + Future deleteItem(String key) async { + _localStorage.remove(key); + } + + @override + Future getItem(String key) async { + return _localStorage[key]; + } + + @override + Future setItem(String key, String value) async { + _localStorage[key] = value; + } +} diff --git a/lib/utils/services/device_profile_cache.dart b/lib/utils/services/device_profile_cache.dart new file mode 100644 index 0000000..ce188a3 --- /dev/null +++ b/lib/utils/services/device_profile_cache.dart @@ -0,0 +1,29 @@ +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract class DeviceProfileCache { + static final _cache = Map(); + + static Future getDeviceProfileInfo( + ThingsboardClient tbClient, String name, String deviceId) async { + var deviceProfile = _cache[name]; + if (deviceProfile == null) { + var device = await tbClient.getDeviceService().getDevice(deviceId); + deviceProfile = await tbClient + .getDeviceProfileService() + .getDeviceProfileInfo(device!.deviceProfileId!.id!); + _cache[name] = deviceProfile!; + } + return deviceProfile; + } + + static Future> getDeviceProfileInfos( + ThingsboardClient tbClient, PageLink pageLink) async { + var deviceProfileInfos = await tbClient + .getDeviceProfileService() + .getDeviceProfileInfos(pageLink); + deviceProfileInfos.data.forEach((deviceProfile) { + _cache[deviceProfile.name] = deviceProfile; + }); + return deviceProfileInfos; + } +} diff --git a/lib/utils/services/endpoint/endpoint_service.dart b/lib/utils/services/endpoint/endpoint_service.dart new file mode 100644 index 0000000..8629af9 --- /dev/null +++ b/lib/utils/services/endpoint/endpoint_service.dart @@ -0,0 +1,48 @@ +import 'package:flutter/foundation.dart'; +import 'package:thingsboard_app/constants/app_constants.dart'; +import 'package:thingsboard_app/constants/database_keys.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; + +class EndpointService implements IEndpointService { + EndpointService({required this.databaseService}); + + final ILocalDatabaseService databaseService; + final _cachedEndpoint = ValueNotifier(null); + + @override + ValueListenable get listenEndpointChanges => _cachedEndpoint; + + @override + Future setEndpoint(String endpoint) async { + _cachedEndpoint.value = endpoint; + + await databaseService.setItem( + DatabaseKeys.thingsBoardApiEndpointKey, + endpoint, + ); + } + + @override + Future getEndpoint() async { + _cachedEndpoint.value ??= await databaseService.getItem( + DatabaseKeys.thingsBoardApiEndpointKey, + ); + + return _cachedEndpoint.value ?? + ThingsboardAppConstants.thingsBoardApiEndpoint; + } + + @override + Future isCustomEndpoint() async { + _cachedEndpoint.value ??= await getEndpoint(); + return _cachedEndpoint.value != + ThingsboardAppConstants.thingsBoardApiEndpoint; + } + + @override + String getCachedEndpoint() { + return _cachedEndpoint.value ?? + ThingsboardAppConstants.thingsBoardApiEndpoint; + } +} diff --git a/lib/utils/services/endpoint/i_endpoint_service.dart b/lib/utils/services/endpoint/i_endpoint_service.dart new file mode 100644 index 0000000..007dac4 --- /dev/null +++ b/lib/utils/services/endpoint/i_endpoint_service.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +/// This service provides information about the current active endpoint. +/// Since we have a feature that allows for changing endpoints, there is some +/// logic associated with the active endpoint, such as dashboard loading and OAuth2A. +abstract interface class IEndpointService { + Future setEndpoint(String endpoint); + + Future getEndpoint(); + + Future isCustomEndpoint(); + + /// At times, we need to retrieve the endpoint synchronously. + /// We might consider using Hive in the future. + String getCachedEndpoint(); + + ValueListenable get listenEndpointChanges; +} diff --git a/lib/utils/services/entity_query_api.dart b/lib/utils/services/entity_query_api.dart new file mode 100644 index 0000000..eb66b75 --- /dev/null +++ b/lib/utils/services/entity_query_api.dart @@ -0,0 +1,79 @@ +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract class EntityQueryApi { + static final activeDeviceKeyFilter = KeyFilter( + key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), + valueType: EntityKeyValueType.BOOLEAN, + predicate: BooleanFilterPredicate( + operation: BooleanOperation.EQUAL, + value: FilterPredicateValue(true))); + + static final inactiveDeviceKeyFilter = KeyFilter( + 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') + ]; + + static final defaultDeviceAttributes = [ + EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active') + ]; + + static Future countDevices(ThingsboardClient tbClient, + {String? deviceType, bool? active}) { + EntityFilter deviceFilter; + if (deviceType != null) { + deviceFilter = + DeviceTypeFilter(deviceType: deviceType, deviceNameFilter: ''); + } else { + deviceFilter = EntityTypeFilter(entityType: EntityType.DEVICE); + } + EntityCountQuery deviceCountQuery = + EntityCountQuery(entityFilter: deviceFilter); + if (active != null) { + deviceCountQuery.keyFilters = [ + active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter + ]; + } + return tbClient + .getEntityQueryService() + .countEntitiesByQuery(deviceCountQuery); + } + + static EntityDataQuery createDefaultDeviceQuery( + {int pageSize = 20, + String? searchText, + String? deviceType, + bool? active}) { + EntityFilter deviceFilter; + List? keyFilters; + if (deviceType != null) { + deviceFilter = + DeviceTypeFilter(deviceType: deviceType, deviceNameFilter: ''); + } else { + deviceFilter = EntityTypeFilter(entityType: EntityType.DEVICE); + } + if (active != null) { + 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))); + } +} diff --git a/lib/utils/services/firebase/firebase_service.dart b/lib/utils/services/firebase/firebase_service.dart new file mode 100644 index 0000000..884e76b --- /dev/null +++ b/lib/utils/services/firebase/firebase_service.dart @@ -0,0 +1,55 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/i_firebase_service.dart'; + +class FirebaseService implements IFirebaseService { + FirebaseService({ + required this.logger, + required this.endpointService, + }); + + final TbLogger logger; + final _apps = []; + final IEndpointService endpointService; + + @override + List get apps => _apps; + + @override + Future initializeApp({ + String name = defaultFirebaseAppName, + FirebaseOptions? options, + }) async { + logger.debug('FirebaseService::initializeApp(name: $name)'); + + try { + if (await endpointService.isCustomEndpoint()) { + throw UnimplementedError( + 'The current limitation is that Firebase can only be ' + 'used with the endpoint with which the app was initially initialized.', + ); + } + + final app = await Firebase.initializeApp(options: options, name: name); + _apps.add(name); + + return app; + } catch (e) { + logger.error('FirebaseService:initializeApp $e'); + } + + return null; + } + + @override + Future removeApp({String name = defaultFirebaseAppName}) async { + try { + await Firebase.app(name).delete(); + } catch (e) { + logger.error('FirebaseService:removeApp $e'); + } finally { + _apps.remove(name); + } + } +} diff --git a/lib/utils/services/firebase/i_firebase_service.dart b/lib/utils/services/firebase/i_firebase_service.dart new file mode 100644 index 0000000..a908fa1 --- /dev/null +++ b/lib/utils/services/firebase/i_firebase_service.dart @@ -0,0 +1,11 @@ +import 'package:firebase_core/firebase_core.dart'; + +abstract interface class IFirebaseService { + const IFirebaseService(); + + Future initializeApp({String name, FirebaseOptions? options}); + + Future removeApp({String name}); + + List get apps; +} diff --git a/lib/utils/services/local_database/i_local_database_service.dart b/lib/utils/services/local_database/i_local_database_service.dart new file mode 100644 index 0000000..e3d324c --- /dev/null +++ b/lib/utils/services/local_database/i_local_database_service.dart @@ -0,0 +1,6 @@ +import 'package:thingsboard_client/thingsboard_client.dart'; + +/// The aim of this service is to consolidate operations with +/// the local database provider into one centralized location. + +abstract interface class ILocalDatabaseService implements TbStorage {} diff --git a/lib/utils/services/local_database/local_database_service.dart b/lib/utils/services/local_database/local_database_service.dart new file mode 100644 index 0000000..ee57cb8 --- /dev/null +++ b/lib/utils/services/local_database/local_database_service.dart @@ -0,0 +1,31 @@ +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/utils/services/local_database/i_local_database_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class LocalDatabaseService implements ILocalDatabaseService { + const LocalDatabaseService({ + required this.storage, + required this.logger, + }); + + final TbStorage storage; + final TbLogger logger; + + @override + Future deleteItem(String key) async { + logger.debug('LocalDatabaseService::deleteItem($key)'); + await storage.deleteItem(key); + } + + @override + Future getItem(String key) async { + logger.debug('LocalDatabaseService::getItem($key)'); + return storage.getItem(key); + } + + @override + Future setItem(String key, String value) async { + logger.debug('LocalDatabaseService::setItem($key, $value)'); + await storage.setItem(key, value); + } +} diff --git a/lib/utils/services/notification_service.dart b/lib/utils/services/notification_service.dart new file mode 100644 index 0000000..6ea6264 --- /dev/null +++ b/lib/utils/services/notification_service.dart @@ -0,0 +1,310 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/modules/notification/service/i_notifications_local_service.dart'; +import 'package:thingsboard_app/modules/notification/service/notifications_local_service.dart'; +import 'package:thingsboard_app/utils/utils.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class NotificationService { + static final NotificationService _instance = NotificationService._(); + static FirebaseMessaging _messaging = FirebaseMessaging.instance; + late NotificationDetails _notificationDetails; + late TbLogger _log; + late ThingsboardClient _tbClient; + late TbContext _tbContext; + final INotificationsLocalService _localService = NotificationsLocalService(); + StreamSubscription? _foregroundMessageSubscription; + StreamSubscription? _onMessageOpenedAppSubscription; + StreamSubscription? _onTokenRefreshSubscription; + + String? _fcmToken; + + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + + NotificationService._(); + + factory NotificationService() => _instance; + + Future init( + ThingsboardClient tbClient, + TbLogger log, + TbContext context, + ) async { + _log = log; + _tbClient = tbClient; + _tbContext = context; + + _log.debug('NotificationService::init()'); + + final message = await FirebaseMessaging.instance.getInitialMessage(); + if (message != null) { + NotificationService.handleClickOnNotification( + message.data, + _tbContext, + ); + } + + _onMessageOpenedAppSubscription = + FirebaseMessaging.onMessageOpenedApp.listen( + (message) async { + NotificationService.handleClickOnNotification( + message.data, + _tbContext, + ); + }, + ); + + final settings = await _requestPermission(); + _log.debug( + 'Notification authorizationStatus: ${settings.authorizationStatus}'); + if (settings.authorizationStatus == AuthorizationStatus.authorized || + settings.authorizationStatus == AuthorizationStatus.provisional) { + await _getAndSaveToken(); + + _onTokenRefreshSubscription = + FirebaseMessaging.instance.onTokenRefresh.listen((token) { + if (_fcmToken != null) { + _tbClient.getUserService().removeMobileSession(_fcmToken!).then((_) { + _fcmToken = token; + if (_fcmToken != null) { + _saveToken(_fcmToken!); + } + }); + } + }); + + await _initFlutterLocalNotificationsPlugin(); + await _configFirebaseMessaging(); + _subscribeOnForegroundMessage(); + await updateNotificationsCount(); + } + } + + Future updateNotificationsCount() async { + final localService = NotificationsLocalService(); + + await localService.updateNotificationsCount( + await _getNotificationsCountRemote(), + ); + } + + Future getToken() async { + _fcmToken = await _messaging.getToken(); + return _fcmToken; + } + + Future initialMessage() async { + return _messaging.getInitialMessage(); + } + + Future logout() async { + getIt().debug('NotificationService::logout()'); + if (_fcmToken != null) { + getIt().debug( + 'NotificationService::logout() removeMobileSession', + ); + _tbClient.getUserService().removeMobileSession(_fcmToken!); + } + + await _foregroundMessageSubscription?.cancel(); + await _onMessageOpenedAppSubscription?.cancel(); + await _onTokenRefreshSubscription?.cancel(); + await _messaging.deleteToken(); + await _messaging.setAutoInitEnabled(false); + await flutterLocalNotificationsPlugin.cancelAll(); + await _localService.clearNotificationBadgeCount(); + } + + Future _configFirebaseMessaging() async { + await _messaging.setAutoInitEnabled(true); + } + + Future _initFlutterLocalNotificationsPlugin() async { + const initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/thingsboard'); + + const initializationSettingsIOS = DarwinInitializationSettings(); + + const initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); + + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (response) { + if (response.notificationResponseType == + NotificationResponseType.selectedNotification) { + final data = json.decode(response.payload ?? ''); + handleClickOnNotification(data, _tbContext); + } + }, + ); + + final androidPlatformChannelSpecifics = AndroidNotificationDetails( + 'general', + 'General notifications', + importance: Importance.max, + priority: Priority.high, + channelDescription: 'This channel is used for general notifications', + showWhen: false, + ); + + const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); + + _notificationDetails = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); + } + + Future _requestPermission() async { + _messaging = FirebaseMessaging.instance; + final result = await _messaging.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: true, + sound: true, + ); + + if (result.authorizationStatus == AuthorizationStatus.denied) { + return result; + } + + return result; + } + + Future _resetToken(String? token) async { + if (token != null) { + _tbClient.getUserService().removeMobileSession(token); + } + + await _messaging.deleteToken(); + return await getToken(); + } + + Future _getAndSaveToken() async { + String? fcmToken = await getToken(); + _log.debug('FCM token: $fcmToken'); + + if (fcmToken != null) { + MobileSessionInfo? mobileInfo = + await _tbClient.getUserService().getMobileSession(fcmToken); + if (mobileInfo != null) { + int timeAfterCreatedToken = DateTime.now().millisecondsSinceEpoch - + mobileInfo.fcmTokenTimestamp; + if (timeAfterCreatedToken > Duration(days: 30).inMilliseconds) { + fcmToken = await _resetToken(fcmToken); + if (fcmToken != null) { + await _saveToken(fcmToken); + } + } + } else { + await _saveToken(fcmToken); + } + } + } + + Future _saveToken(String token) async { + await _tbClient.getUserService().saveMobileSession( + token, MobileSessionInfo(DateTime.now().millisecondsSinceEpoch)); + } + + void showNotification(RemoteMessage message) async { + final notification = message.notification; + + if (notification != null) { + flutterLocalNotificationsPlugin.show( + notification.hashCode, + notification.title, + notification.body, + _notificationDetails, + payload: json.encode(message.data), + ); + + _localService.increaseNotificationBadgeCount(); + } + } + + void _subscribeOnForegroundMessage() { + _foregroundMessageSubscription = + FirebaseMessaging.onMessage.listen((message) { + _log.debug('Message:' + message.toString()); + if (message.sentTime == null) { + final map = message.toMap(); + map['sentTime'] = DateTime.now().millisecondsSinceEpoch; + showNotification(RemoteMessage.fromMap(map)); + } else { + showNotification(message); + } + }); + } + + static void handleClickOnNotification( + Map data, + TbContext tbContext, + ) { + if (data['enabled'] == true || data['onClick.enabled'] == 'true') { + switch (data['linkType'] ?? data['onClick.linkType']) { + case 'DASHBOARD': + final dashboardId = + data['dashboardId'] ?? data['onClick.dashboardId']; + var entityId; + if ((data['stateEntityId'] ?? data['onClick.stateEntityId']) != + null && + (data['stateEntityType'] ?? data['onClick.stateEntityType']) != + null) { + entityId = EntityId.fromTypeAndUuid( + entityTypeFromString( + data['stateEntityType'] ?? data['onClick.stateEntityType']), + data['stateEntityId'] ?? data['onClick.stateEntityId'], + ); + } + + final state = Utils.createDashboardEntityState( + entityId, + stateId: data['dashboardState'] ?? data['onClick.dashboardState'], + ); + + if (dashboardId != null) { + tbContext.navigateToDashboard(dashboardId, state: state); + } + + break; + case 'LINK': + final link = data['link'] ?? data['onClick.link']; + if (link != null) { + if (Uri.parse(link).isAbsolute) { + tbContext.navigateTo('/url/${Uri.encodeComponent(link)}'); + } else { + tbContext.navigateTo(link); + } + } + + break; + } + } else { + tbContext.navigateTo('/notifications', replace: true); + } + } + + Future _getNotificationsCountRemote() async { + try { + return _tbClient + .getNotificationService() + .getUnreadNotificationsCount('MOBILE_APP'); + } catch (_) { + return 0; + } + } +} diff --git a/lib/utils/services/tb_app_storage.dart b/lib/utils/services/tb_app_storage.dart new file mode 100644 index 0000000..f871f7d --- /dev/null +++ b/lib/utils/services/tb_app_storage.dart @@ -0,0 +1,3 @@ +export '_tb_app_storage.dart' + if (dart.library.io) '_tb_secure_storage.dart' + if (dart.library.html) '_tb_web_local_storage.dart'; diff --git a/lib/utils/services/widget_action_handler.dart b/lib/utils/services/widget_action_handler.dart new file mode 100644 index 0000000..6b9c91b --- /dev/null +++ b/lib/utils/services/widget_action_handler.dart @@ -0,0 +1,321 @@ +import 'dart:io'; +import 'package:fluro/fluro.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class WidgetMobileActionResult { + T? result; + bool hasResult = false; + String? error; + bool hasError = false; + + WidgetMobileActionResult.errorResult(this.error) + : hasError = true, + hasResult = false; + + WidgetMobileActionResult.successResult(this.result) + : hasError = false, + hasResult = true; + + WidgetMobileActionResult.emptyResult() + : hasError = false, + hasResult = false; + + Map toJson() { + var json = {}; + json['hasError'] = hasError; + json['hasResult'] = hasResult; + json['error'] = error; + json['result'] = result?.toJson(); + return json; + } +} + +class MobileActionResult { + MobileActionResult(); + + factory MobileActionResult.launched(bool launched) { + return _LaunchResult(launched); + } + + factory MobileActionResult.image(String imageUrl) { + return _ImageResult(imageUrl); + } + + factory MobileActionResult.qrCode(String code, String format) { + return _QrCodeResult(code, format); + } + + factory MobileActionResult.location(num latitude, num longitude) { + return _LocationResult(latitude, longitude); + } + + Map toJson() { + var json = {}; + return json; + } +} + +class _LaunchResult extends MobileActionResult { + bool launched; + _LaunchResult(this.launched); + + @override + Map toJson() { + var json = super.toJson(); + json['launched'] = launched; + return json; + } +} + +class _ImageResult extends MobileActionResult { + String imageUrl; + _ImageResult(this.imageUrl); + + @override + Map toJson() { + var json = super.toJson(); + json['imageUrl'] = imageUrl; + return json; + } +} + +class _QrCodeResult extends MobileActionResult { + String code; + String format; + _QrCodeResult(this.code, this.format); + + @override + Map toJson() { + var json = super.toJson(); + json['code'] = code; + json['format'] = format; + return json; + } +} + +class _LocationResult extends MobileActionResult { + num latitude; + num longitude; + _LocationResult(this.latitude, this.longitude); + + @override + Map toJson() { + var json = super.toJson(); + json['latitude'] = latitude; + json['longitude'] = longitude; + return json; + } +} + +enum WidgetMobileActionType { + takePictureFromGallery, + takePhoto, + mapDirection, + mapLocation, + scanQrCode, + makePhoneCall, + getLocation, + takeScreenshot, + unknown +} + +WidgetMobileActionType widgetMobileActionTypeFromString(String value) { + return WidgetMobileActionType.values.firstWhere( + (e) => e.toString().split('.')[1].toUpperCase() == value.toUpperCase(), + orElse: () => WidgetMobileActionType.unknown); +} + +class WidgetActionHandler with HasTbContext { + WidgetActionHandler(TbContext tbContext) { + setTbContext(tbContext); + } + + Future> handleWidgetMobileAction( + List args, InAppWebViewController controller) async { + var result = await _handleWidgetMobileAction(args, controller); + return result.toJson(); + } + + Future _handleWidgetMobileAction( + List args, InAppWebViewController controller) async { + if (args.isNotEmpty && args[0] is String) { + var actionType = widgetMobileActionTypeFromString(args[0]); + switch (actionType) { + case WidgetMobileActionType.takePictureFromGallery: + return await _takePicture(ImageSource.gallery); + case WidgetMobileActionType.takePhoto: + return await _takePicture(ImageSource.camera); + case WidgetMobileActionType.mapDirection: + return await _launchMap(args, true); + case WidgetMobileActionType.mapLocation: + return await _launchMap(args, false); + case WidgetMobileActionType.scanQrCode: + return await _scanQrCode(); + case WidgetMobileActionType.makePhoneCall: + return await _makePhoneCall(args); + case WidgetMobileActionType.getLocation: + return await _getLocation(); + case WidgetMobileActionType.takeScreenshot: + return await _takeScreenshot(controller); + case WidgetMobileActionType.unknown: + return WidgetMobileActionResult.errorResult( + 'Unknown actionType: ${args[0]}'); + } + } else { + return WidgetMobileActionResult.errorResult( + 'actionType is not provided.'); + } + } + + Future _takePicture(ImageSource source) async { + try { + final picker = ImagePicker(); + final pickedFile = await picker.pickImage(source: source); + if (pickedFile != null) { + var mimeType = lookupMimeType(pickedFile.path); + if (mimeType != null) { + var image = File(pickedFile.path); + List imageBytes = await image.readAsBytes(); + String imageUrl = + UriData.fromBytes(imageBytes, mimeType: mimeType).toString(); + return WidgetMobileActionResult.successResult( + MobileActionResult.image(imageUrl)); + } else { + return WidgetMobileActionResult.errorResult( + 'Unknown picture mime type'); + } + } else { + return WidgetMobileActionResult.emptyResult(); + } + } catch (e) { + return _handleError(e); + } + } + + Future _launchMap( + List args, bool directionElseLocation) async { + try { + num? lat; + num? lon; + if (args.length > 2 && args[1] is num && args[2] is num) { + lat = args[1]; + lon = args[2]; + } else { + return WidgetMobileActionResult.errorResult( + 'Missing target latitude or longitude arguments!'); + } + var url = 'https://www.google.com/maps/'; + url += directionElseLocation + ? 'dir/?api=1&destination=$lat,$lon' + : 'search/?api=1&query=$lat,$lon'; + return WidgetMobileActionResult.successResult(await _tryLaunch(url)); + } catch (e) { + return _handleError(e); + } + } + + Future _scanQrCode() async { + try { + 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())); + } else { + return WidgetMobileActionResult.emptyResult(); + } + } catch (e) { + return _handleError(e); + } + } + + Future _makePhoneCall(List args) async { + try { + var phoneNumber; + if (args.length > 1 && args[1] != null) { + phoneNumber = args[1]; + } else { + return WidgetMobileActionResult.errorResult( + 'Missing or invalid phone number!'); + } + return WidgetMobileActionResult.successResult( + await _tryLaunch('tel://$phoneNumber')); + } catch (e) { + return _handleError(e); + } + } + + Future _getLocation() async { + try { + bool serviceEnabled; + LocationPermission permission; + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return WidgetMobileActionResult.errorResult( + '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.'); + } + } + if (permission == LocationPermission.deniedForever) { + return WidgetMobileActionResult.errorResult( + 'Location permissions are permanently denied, we cannot request permissions.'); + } + var position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + return WidgetMobileActionResult.successResult( + MobileActionResult.location(position.latitude, position.longitude)); + } catch (e) { + return _handleError(e); + } + } + + Future _takeScreenshot( + 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)); + } else { + return WidgetMobileActionResult.emptyResult(); + } + } catch (e) { + return _handleError(e); + } + } + + Future _tryLaunch(String url) async { + if (await canLaunchUrlString(url)) { + await launchUrlString(url); + return MobileActionResult.launched(true); + } else { + log.error('Could not launch $url'); + return MobileActionResult.launched(false); + } + } + + WidgetMobileActionResult _handleError(e) { + String error; + if (e is PlatformException) { + error = e.message ?? e.code; + } else { + error = e.toString(); + } + return WidgetMobileActionResult.errorResult(error); + } +} diff --git a/lib/utils/transition/page_transitions.dart b/lib/utils/transition/page_transitions.dart new file mode 100644 index 0000000..5192e31 --- /dev/null +++ b/lib/utils/transition/page_transitions.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class FadeOpenPageTransitionsBuilder extends PageTransitionsBuilder { + /// Constructs a page transition animation that slides the page up. + const FadeOpenPageTransitionsBuilder(); + + @override + Widget buildTransitions( + PageRoute? route, + BuildContext? context, + Animation animation, + Animation? secondaryAnimation, + Widget child, + ) { + return FadeOpenPageTransition(routeAnimation: animation, child: child); + } +} + +class FadeOpenPageTransition extends StatelessWidget { + FadeOpenPageTransition({ + Key? key, + required Animation + routeAnimation, // The route's linear 0.0 - 1.0 animation. + required this.child, + }) : _positionAnimation = + routeAnimation.drive(_leftRightTween.chain(_fastOutSlowInTween)), + _opacityAnimation = routeAnimation.drive(_easeInTween), + super(key: key); + + // Fractional offset from 1/4 screen below the top to fully on screen. + static final Tween _leftRightTween = Tween( + begin: const Offset(0.5, 0.0), + end: Offset.zero, + ); + static final Animatable _fastOutSlowInTween = + CurveTween(curve: Curves.fastOutSlowIn); + static final Animatable _easeInTween = + CurveTween(curve: Curves.easeIn); + + final Animation _positionAnimation; + final Animation _opacityAnimation; + final Widget child; + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _positionAnimation, + child: FadeTransition( + opacity: _opacityAnimation, + child: child, + ), + ); + } +} diff --git a/lib/utils/ui/qr_code_scanner.dart b/lib/utils/ui/qr_code_scanner.dart new file mode 100644 index 0000000..779fcc2 --- /dev/null +++ b/lib/utils/ui/qr_code_scanner.dart @@ -0,0 +1,135 @@ +import 'dart:io'; +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; +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); + + @override + _QrCodeScannerPageState createState() => _QrCodeScannerPageState(); +} + +class _QrCodeScannerPageState extends TbPageState { + Timer? simulatedQrTimer; + QRViewController? controller; + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } else if (Platform.isIOS) { + controller!.resumeCamera(); + } + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + controller?.dispose(); + if (simulatedQrTimer != null) { + simulatedQrTimer!.cancel(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + _buildQrView(context), + 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( + 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( + 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', + ), + ], + ), + ) + ], + )); + } + + Widget _buildQrView(BuildContext context) { + // For this example we check how width or tall the device is and change the scanArea and overlay accordingly. + var scanArea = (MediaQuery.of(context).size.width < 400 || + MediaQuery.of(context).size.height < 400) + ? 150.0 + : 300.0; + // To ensure the Scanner view is properly sizes after rotation + // we need to listen for Flutter SizeChanged notification and update controller + return QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea), + ); + } + + void _onQRViewCreated(QRViewController controller) { + setState(() { + this.controller = controller; + }); + if (isPhysicalDevice) { + controller.scannedDataStream.take(1).listen((scanData) { + pop(scanData); + }); + } else { + simulatedQrTimer = Timer(Duration(seconds: 3), () { + pop(Barcode('test code', BarcodeFormat.qrcode, null)); + }); + } + } +} diff --git a/lib/utils/ui_utils_routes.dart b/lib/utils/ui_utils_routes.dart new file mode 100644 index 0000000..308e1f0 --- /dev/null +++ b/lib/utils/ui_utils_routes.dart @@ -0,0 +1,19 @@ +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/utils/ui/qr_code_scanner.dart'; + +class UiUtilsRoutes extends TbRoutes { + late var qrCodeScannerHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return QrCodeScannerPage(tbContext); + }); + + UiUtilsRoutes(TbContext tbContext) : super(tbContext); + + @override + void doRegisterRoutes(router) { + router.define("/qrCodeScan", handler: qrCodeScannerHandler); + } +} diff --git a/lib/utils/usecase.dart b/lib/utils/usecase.dart new file mode 100644 index 0000000..1ea3751 --- /dev/null +++ b/lib/utils/usecase.dart @@ -0,0 +1,9 @@ +abstract class UseCase { + const UseCase(); + + Output call(Input params); +} + +class NoParams { + const NoParams(); +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..9e4b367 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,254 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:jovial_svg/jovial_svg.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/endpoint/i_endpoint_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract class Utils { + static const _tbImagePrefix = 'tb-image;'; + static const _imageBase64UrlPrefix = 'data:image/'; + static final _imagesUrlRegexp = + RegExp('\/api\/images\/(tenant|system)\/(.*)'); + static final _noImageDataUri = UriData.parse( + '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}) { + var stateObj = [ + {'params': {}} + ]; + if (entityId != null) { + stateObj[0]['params']['entityId'] = entityId.toJson(); + } + if (entityName != null) { + stateObj[0]['params']['entityName'] = entityName; + } + if (entityLabel != null) { + stateObj[0]['params']['entityLabel'] = entityLabel; + } + if (stateId != null) { + stateObj[0]['id'] = stateId; + } + var stateJson = json.encode(stateObj); + var encodedUri = Uri.encodeComponent(stateJson); + encodedUri = + encodedUri.replaceAllMapped(RegExp(r'%([0-9A-F]{2})'), (match) { + var p1 = match.group(1)!; + return String.fromCharCode(int.parse(p1, radix: 16)); + }); + return Uri.encodeComponent(base64.encode(utf8.encode(encodedUri))); + } + + static String? contactToShortAddress(ContactBased contact) { + var addressParts = []; + if (contact.country != null) { + addressParts.add(contact.country!); + } + if (contact.city != null) { + addressParts.add(contact.city!); + } + if (contact.address != null) { + addressParts.add(contact.address!); + } + if (addressParts.isNotEmpty) { + return addressParts.join(', '); + } else { + return null; + } + } + + static Widget imageFromTbImage( + 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); + } 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); + } + var parts = imageUrl.split('/'); + var key = parts[parts.length - 1]; + parts[parts.length - 1] = Uri.encodeComponent(key); + var encodedUrl = parts.join('/'); + var imageLink = + getIt().getCachedEndpoint() + encodedUrl; + + 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); + } else if (_isValidUrl(imageUrl)) { + 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); + } + } + } + + 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, + width: width, + height: height, + semanticLabel: semanticLabel, + onError: 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); + } else { + 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, + onError: 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)); + if (color != null) { + var colorFilter = ColorFilter.mode(color, BlendMode.srcIn); + image = ColorFiltered( + colorFilter: colorFilter, + child: image, + ); + } + if (height != null || width != null) { + image = SizedBox( + width: width, + height: height, + child: image, + ); + } + return image; + } + + 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); + } + + 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) { + return url.replaceFirst(_tbImagePrefix, ''); + } + + static bool _isImageResourceUrl(String url) { + return _imagesUrlRegexp.hasMatch(url); + } + + static bool _isValidUrl(String url) { + return Uri.tryParse(url) != null; + } + + static bool _isBase64DataImageUrl(String url) { + return url.startsWith(_imageBase64UrlPrefix); + } +} diff --git a/lib/widgets/tb_app_bar.dart b/lib/widgets/tb_app_bar.dart new file mode 100644 index 0000000..b6460fb --- /dev/null +++ b/lib/widgets/tb_app_bar.dart @@ -0,0 +1,179 @@ +import 'dart:async'; + +import 'package:stream_transform/stream_transform.dart'; +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; + +class TbAppBar extends TbContextWidget implements PreferredSizeWidget { + final Widget? leading; + final Widget? title; + final List? actions; + final double? elevation; + final Color? shadowColor; + final bool showLoadingIndicator; + + @override + final Size preferredSize; + + TbAppBar(TbContext tbContext, + {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(); +} + +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( + valueListenable: loadingNotifier, + builder: (context, bool loading, child) { + if (loading) { + return LinearProgressIndicator(); + } else { + return Container(height: 4); + } + })); + } + return Column( + children: children, + ); + } + + AppBar buildDefaultBar() { + return AppBar( + leading: widget.leading, + title: widget.title, + actions: widget.actions, + elevation: widget.elevation ?? 8, + shadowColor: widget.shadowColor ?? Color(0xFFFFFFFF).withAlpha(150), + ); + } +} + +class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget { + final double? elevation; + final Color? shadowColor; + final bool showLoadingIndicator; + final String? searchHint; + final void Function(String searchText)? onSearch; + + @override + final Size preferredSize; + + TbAppSearchBar(TbContext tbContext, + {this.elevation = 8, + this.shadowColor, + this.showLoadingIndicator = false, + this.searchHint, + this.onSearch}) + : preferredSize = + Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)), + super(tbContext); + + @override + _TbAppSearchBarState createState() => _TbAppSearchBarState(); +} + +class _TbAppSearchBarState extends TbContextState { + final TextEditingController _filter = new TextEditingController(); + final _textUpdates = StreamController(); + + @override + void initState() { + super.initState(); + // _textUpdates.add(''); + _filter.addListener(() { + _textUpdates.add(_filter.text); + }); + _textUpdates.stream + .skip(1) + .debounce(const Duration(milliseconds: 150)) + .distinct() + .forEach((element) => widget.onSearch!(element)); + } + + @override + void dispose() { + _filter.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + List children = []; + children.add(buildSearchBar()); + if (widget.showLoadingIndicator) { + children.add(ValueListenableBuilder( + valueListenable: loadingNotifier, + builder: (context, bool loading, child) { + if (loading) { + return LinearProgressIndicator(); + } else { + return Container(height: 4); + } + })); + } + return Column( + children: children, + ); + } + + 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(); + } + }) + ]); + } +} diff --git a/lib/widgets/tb_progress_indicator.dart b/lib/widgets/tb_progress_indicator.dart new file mode 100644 index 0000000..99ecaef --- /dev/null +++ b/lib/widgets/tb_progress_indicator.dart @@ -0,0 +1,84 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:thingsboard_app/constants/assets_path.dart'; + +class TbProgressIndicator extends ProgressIndicator { + final double size; + + const TbProgressIndicator({ + Key? key, + this.size = 36.0, + Animation? valueColor, + String? semanticsLabel, + String? semanticsValue, + }) : super( + key: key, + value: null, + valueColor: valueColor, + semanticsLabel: semanticsLabel, + semanticsValue: semanticsValue, + ); + + @override + _TbProgressIndicatorState createState() => _TbProgressIndicatorState(); + + Color _getValueColor(BuildContext context) => + valueColor?.value ?? Theme.of(context).primaryColor; +} + +class _TbProgressIndicatorState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late CurvedAnimation _rotation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + upperBound: 1, + animationBehavior: AnimationBehavior.preserve); + _rotation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); + _controller.repeat(); + } + + @override + void didUpdateWidget(TbProgressIndicator oldWidget) { + super.didUpdateWidget(oldWidget); + if (!_controller.isAnimating) _controller.repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + 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)), + builder: (BuildContext context, Widget? child) { + return Transform.rotate( + angle: _rotation.value * pi * 2, child: child); + }, + ) + ], + ); + } +} diff --git a/lib/widgets/two_page_view.dart b/lib/widgets/two_page_view.dart new file mode 100644 index 0000000..ef44a4e --- /dev/null +++ b/lib/widgets/two_page_view.dart @@ -0,0 +1,107 @@ +import 'package:flutter/widgets.dart'; +import 'package:preload_page_view/preload_page_view.dart'; + +class TwoPageViewController { + _TwoPageViewState? _state; + + setTransitionIndexedStackState(_TwoPageViewState state) { + _state = state; + } + + Future open(int index, {bool animate = true}) async { + if (_state != null) { + return _state!._open(index, animate: animate); + } + return false; + } + + Future close(int index, {bool animate = true}) async { + if (_state != null) { + return _state!._close(index, animate: animate); + } + return false; + } + + int? get index => _state?._selectedIndex; +} + +class TwoPageView extends StatefulWidget { + final Widget first; + final Widget second; + 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); + + @override + _TwoPageViewState createState() => _TwoPageViewState(); +} + +class _TwoPageViewState extends State { + late List _pages; + bool _reverse = false; + int _selectedIndex = 0; + final PreloadPageController _pageController = PreloadPageController(); + + @override + void initState() { + widget.controller?.setTransitionIndexedStackState(this); + _pages = [widget.first, widget.second]; + super.initState(); + } + + Future _open(int index, {bool animate = true}) async { + if (_selectedIndex != index) { + _selectedIndex = index; + if (index == 0) { + setState(() { + _reverse = true; + }); + } + await _pageController.animateToPage(_selectedIndex, + duration: widget.duration, curve: Curves.fastOutSlowIn); + return true; + } + return false; + } + + 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); + if (index == 0) { + setState(() { + _reverse = false; + }); + } + return true; + } + return false; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PreloadPageView( + children: _pages, + physics: NeverScrollableScrollPhysics(), + reverse: _reverse, + onPageChanged: (int position) { + _selectedIndex = position; + }, + preloadPagesCount: 2, + controller: _pageController, + ); + } +} diff --git a/lib/widgets/two_value_listenable_builder.dart b/lib/widgets/two_value_listenable_builder.dart new file mode 100644 index 0000000..2d81ffb --- /dev/null +++ b/lib/widgets/two_value_listenable_builder.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +class TwoValueListenableBuilder extends StatelessWidget { + TwoValueListenableBuilder({ + Key? key, + required this.firstValueListenable, + required this.secondValueListenable, + required this.builder, + this.child, + }) : super(key: key); + + final ValueListenable firstValueListenable; + final ValueListenable secondValueListenable; + final Widget? child; + final Widget Function(BuildContext context, A a, B b, Widget? child) builder; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: firstValueListenable, + builder: (_, a, __) { + return ValueListenableBuilder( + valueListenable: secondValueListenable, + builder: (context, b, __) { + return builder(context, a, b, child); + }, + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..d069ccf --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1631 @@ +# 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 + url: "https://pub.dev" + source: hosted + version: "1.3.16" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + 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: + name: archive + sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + url: "https://pub.dev" + source: hosted + version: "3.4.9" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + 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: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + 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: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + 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: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + 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: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + 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: + name: cross_file + sha256: "2f9d2cbccb76127ba28528cb3ae2c2326a122446a83de5a056aaa3880d3882c5" + url: "https://pub.dev" + source: hosted + version: "0.3.3+7" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" + url: "https://pub.dev" + source: hosted + version: "0.17.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + dart_jsonwebtoken: + dependency: "direct main" + description: + name: dart_jsonwebtoken + sha256: "6703695f581fc54d0a7e5f281c5538735167605bb9e5abd208c8b330625a92b1" + 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: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + url: "https://pub.dev" + source: hosted + version: "9.1.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + dio: + dependency: transitive + description: + name: dio + sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" + url: "https://pub.dev" + source: hosted + version: "5.3.4" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + fading_edge_scrollview: + dependency: "direct main" + description: + name: fading_edge_scrollview + sha256: bebff5b4551c021c484783a47ec9242aedc881c0c6991bb471c73f85e5694fd0 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" + url: "https://pub.dev" + source: hosted + version: "2.24.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 + url: "https://pub.dev" + source: hosted + version: "2.10.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8" + url: "https://pub.dev" + source: hosted + version: "14.7.10" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "54e283a0e41d81d854636ad0dad73066adc53407a60a7c3189c9656e2f1b6107" + url: "https://pub.dev" + source: hosted + version: "4.5.18" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" + url: "https://pub.dev" + source: hosted + version: "3.5.18" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + fluro: + dependency: "direct main" + description: + name: fluro + sha256: "24d07d0b285b213ec2045b83e85d076185fa5c23651e44dae0ac6755784b97d0" + url: "https://pub.dev" + source: hosted + version: "2.0.5" + flutter: + dependency: "direct main" + 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: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" + flutter_form_builder: + dependency: "direct main" + description: + name: flutter_form_builder + sha256: "8973beed34b6d951d36bf688b52e9e3040b47b763c35c320bd6f4c2f6b13f3a2" + url: "https://pub.dev" + source: hosted + version: "9.1.1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "5717c89f126c24e310ef36770a7f4446cba81cb27abd0ce8292d098f45682efc" + url: "https://pub.dev" + source: hosted + version: "3.0.0-alpha.5" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 + url: "https://pub.dev" + source: hosted + version: "5.8.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535" + url: "https://pub.dev" + source: hosted + version: "16.3.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + url: "https://pub.dev" + source: hosted + version: "7.0.0+1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + url: "https://pub.dev" + source: hosted + version: "2.0.17" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + 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: + name: flutter_speed_dial + sha256: "698a037274a66dbae8697c265440e6acb6ab6cae9ac5f95c749e7944d8f28d41" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_staggered_grid_view: + dependency: transitive + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + form_builder_validators: + dependency: "direct main" + description: + name: form_builder_validators + sha256: "19aa5282b7cede82d0025ab031a98d0554b84aa2ba40f12013471a3b3e22bf02" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: e946395fc608842bb2f6c914807e9183f86f3cb787f6b8f832753e5251036f02 + url: "https://pub.dev" + source: hosted + version: "10.1.0" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "741579fa6c9e412984d2bdb2fbaa54e3c3f7587c60aeacfe6e058358a11f40f8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: ab90ae811c42ec2f6021e01eca71df00dee6ff1e69d2c2dafd4daeb0b793f73d + url: "https://pub.dev" + source: hosted + version: "2.3.2" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "6c8d494d6948757c56720b778af742f6973f31fca1f702a7539b8917e4a2468a" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af + url: "https://pub.dev" + source: hosted + version: "0.2.2" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + 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: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + 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: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + url: "https://pub.dev" + source: hosted + version: "4.1.3" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d6a6e78821086b0b737009b09363018309bbc6de3fd88cc5c26bc2bb44a4957f + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7" + url: "https://pub.dev" + source: hosted + version: "0.8.8+4" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + url: "https://pub.dev" + source: hosted + version: "2.9.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + infinite_scroll_pagination: + dependency: "direct main" + description: + name: infinite_scroll_pagination + sha256: b68bce20752fcf36c7739e60de4175494f74e99e9a69b4dd2fe3a1dd07a7f16a + url: "https://pub.dev" + source: hosted + version: "4.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + jovial_misc: + dependency: transitive + description: + name: jovial_misc + sha256: f6e64f789ee311025bb367be9c9afe9759f76dd8209070b7f38e735b5f529eb1 + url: "https://pub.dev" + source: hosted + version: "0.8.5" + jovial_svg: + dependency: "direct main" + description: + name: jovial_svg + sha256: "917c63f774f3a9053777b55d824a60090ab87e65a7f6219ca1c478ccd0314f4a" + url: "https://pub.dev" + source: hosted + version: "1.1.19" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + jwt_decoder: + dependency: transitive + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + 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: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + material_design_icons_flutter: + dependency: "direct main" + description: + name: material_design_icons_flutter + sha256: "6f986b7a51f3ad4c00e33c5c84e8de1bdd140489bbcdc8b66fc1283dad4dea5a" + url: "https://pub.dev" + source: hosted + version: "7.0.7296" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + mime: + dependency: "direct main" + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + numerus: + dependency: transitive + description: + name: numerus + sha256: "0087ef729d63b96cb347a9c44b9c592f21cecb3605b415bbd18710aef80ce5cb" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + url: "https://pub.dev" + source: hosted + version: "2.1.7" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + 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: + name: preload_page_view + sha256: "488a10c158c5c2e9ba9d77e5dbc09b1e49e37a20df2301e5ba02992eac802b7a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + 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: + name: qr_code_scanner + sha256: f23b68d893505a424f0bd2e324ebea71ed88465d572d26bb8d2e78a4749591fd + 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 + source: sdk + version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + 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: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: "direct main" + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + url: "https://pub.dev" + source: hosted + version: "1.24.9" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" + thingsboard_client: + dependency: "direct main" + description: + name: thingsboard_client + sha256: c9187ef06fa4534a86c7ba06a39f3b0b87df7be20ecfb60b98721e80242fac77 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + timeago: + dependency: "direct main" + description: + name: timeago + sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de" + url: "https://pub.dev" + source: hosted + version: "3.7.0" + timezone: + dependency: transitive + description: + name: timezone + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + 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: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + 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: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + universal_platform: + dependency: "direct main" + description: + name: universal_platform + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + url: "https://pub.dev" + source: hosted + version: "6.2.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + 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: + name: win32 + sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + xml: + dependency: transitive + description: + name: xml + sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 + url: "https://pub.dev" + source: hosted + version: "6.4.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..5ceea60 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,108 @@ +name: thingsboard_app +description: Flutter ThingsBoard Mobile Application + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.2.0 + +environment: + sdk: ">=3.2.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + thingsboard_client: ^1.2.0 + intl: ^0.18.1 + flutter_secure_storage: ^9.0.0 + flutter_speed_dial: ^7.0.0 + cupertino_icons: ^1.0.6 + fluro: ^2.0.5 + flutter_svg: ^2.0.9 + jovial_svg: ^1.1.19 + auto_size_text: ^3.0.0-nullsafety.0 + infinite_scroll_pagination: ^4.0.0 + fading_edge_scrollview: ^4.0.0 + stream_transform: ^2.1.0 + flutter_inappwebview: ^5.8.0 +# flutter_downloader: ^1.6.0 +# permission_handler: ^8.0.0+2 +# path_provider: ^2.0.2 + url_launcher: ^6.2.1 + image_picker: ^1.0.4 + 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 + material_design_icons_flutter: ^7.0.7296 + package_info_plus: ^5.0.1 + 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 + 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 + flutter_app_badger: ^1.5.0 + timeago: ^3.6.1 + flutter_slidable: ^3.0.1 + flutter_bloc: ^8.1.5 + get_it: ^7.6.7 + equatable: ^2.0.5 + uni_links: ^0.5.1 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_launcher_icons: ^0.13.1 + mocktail: ^1.0.3 + bloc_test: ^9.1.7 + hive_generator: ^2.0.1 + build_runner: ^2.4.9 + +flutter: + uses-material-design: true + assets: + - assets/images/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages + +flutter_icons: + android: "launcher_icon" + ios: true + remove_alpha_ios: true + image_path: "assets/images/thingsboard.png" +flutter_intl: + enabled: true diff --git a/test/core/noauth/switch_endpoint_test.dart b/test/core/noauth/switch_endpoint_test.dart new file mode 100644 index 0000000..c63fa11 --- /dev/null +++ b/test/core/noauth/switch_endpoint_test.dart @@ -0,0 +1,257 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:thingsboard_app/core/auth/noauth/di/noauth_di.dart'; +import 'package:thingsboard_app/core/auth/noauth/presentation/bloc/bloc.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/logger/tb_logger.dart'; +import 'package:thingsboard_app/locator.dart'; +import 'package:thingsboard_app/utils/services/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'; + +void main() { + late final TbContext tbContext; + late final ThingsboardClient tbClient; + late final IFirebaseService firebaseService; + late final IEndpointService endpointService; + + setUpAll(() { + tbContext = MockTbContext(); + tbClient = MockTbClient(); + firebaseService = MockFirebaseService(); + endpointService = MockEndpointService(); + + when(() => tbContext.tbClient).thenReturn(tbClient); + when(() => firebaseService.removeApp()).thenAnswer((_) => Future.value()); + when(() => endpointService.setEndpoint(any())).thenAnswer( + (_) => Future.value(), + ); + + getIt.registerLazySingleton(() => TbLogger()); + getIt.registerLazySingleton(() => firebaseService); + getIt.registerLazySingleton(() => endpointService); + + NoAuthDi.init(tbContext: tbContext); + }); + + tearDownAll(() { + getIt().close(); + NoAuthDi.dispose(); + }); + + group('Switch Endpoint Group Test', () { + group('SwitchToAnotherEndpointEvent', () { + blocTest( + 'An empty request data', + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent(parameters: null), + ), + expect: () => [ + isA().having( + (e) => e.message, + 'error message', + 'An empty request data received.', + ) + ], + ); + + blocTest( + 'JWT Token is invalid', + setUp: () { + when( + () => tbClient.getLoginDataBySecretKey( + host: any(named: 'host'), + key: any(named: 'key'), + ), + ).thenThrow( + ThingsboardError(message: 'JWT Token is invalid'), + ); + }, + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent( + parameters: { + 'host': 'host', + 'secret': 'key', + 'uri': 'uri', + }, + ), + ), + expect: () => [ + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Getting data from your host host', + ), + isA().having( + (e) => e.message, + 'error message', + 'JWT Token is invalid', + ), + ], + ); + + blocTest( + 'An error on TbClient re-init', + setUp: () { + when( + () => tbClient.getLoginDataBySecretKey( + host: any(named: 'host'), + key: any(named: 'key'), + ), + ).thenAnswer( + (_) => Future.value(LoginResponse('token', 'refreshToken')), + ); + + when( + () => tbContext.logout( + requestConfig: any(named: 'requestConfig'), + notifyUser: any(named: 'notifyUser'), + ), + ).thenAnswer((_) => Future.value()); + + when( + () => tbClient.setUserFromJwtToken(any(), any(), any()), + ).thenAnswer((_) => Future.value()); + + when( + () => tbContext.reInit( + endpoint: any(named: 'endpoint'), + onDone: any(named: 'onDone'), + onError: any(named: 'onError'), + ), + ).thenAnswer( + (invocation) { + final onError = invocation.namedArguments[Symbol('onError')]; + onError( + ThingsboardError(message: 'TBClient re-init error message'), + ); + + return Future.value(); + }, + ); + }, + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent( + parameters: { + 'host': 'https://host.com', + 'secret': 'key', + 'uri': 'uri', + }, + ), + ), + expect: () => [ + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Getting data from your host https://host.com', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Logout you ...', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Switching you to the new host https://host.com', + ), + isA().having( + (e) => e.message, + 'error message', + 'TBClient re-init error message', + ), + ], + ); + + blocTest( + 'Switch endpoint success', + setUp: () { + when( + () => tbClient.getLoginDataBySecretKey( + host: any(named: 'host'), + key: any(named: 'key'), + ), + ).thenAnswer( + (_) => Future.value(LoginResponse('token', 'refreshToken')), + ); + + when( + () => tbContext.logout( + requestConfig: any(named: 'requestConfig'), + notifyUser: any(named: 'notifyUser'), + ), + ).thenAnswer((_) => Future.value()); + + when( + () => tbClient.setUserFromJwtToken(any(), any(), any()), + ).thenAnswer((_) => Future.value()); + + when( + () => tbContext.reInit( + endpoint: any(named: 'endpoint'), + onDone: any(named: 'onDone'), + onError: any(named: 'onError'), + ), + ).thenAnswer( + (invocation) { + final onDone = invocation.namedArguments[Symbol('onDone')]; + onDone(); + + return Future.value(); + }, + ); + }, + build: () { + return NoAuthBloc( + switchEndpointUseCase: getIt(), + ); + }, + act: (bloc) => bloc.add( + const SwitchToAnotherEndpointEvent( + parameters: { + 'host': 'https://host.com', + 'secret': 'key', + 'uri': 'uri', + }, + ), + ), + expect: () => [ + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Getting data from your host https://host.com', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Logout you ...', + ), + isA().having( + (e) => e.currentStateMessage, + 'progress message', + 'Switching you to the new host https://host.com', + ), + isA(), + ], + ); + }); + }); +} diff --git a/test/mocks.dart b/test/mocks.dart new file mode 100644 index 0000000..7ad22af --- /dev/null +++ b/test/mocks.dart @@ -0,0 +1,13 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/utils/services/endpoint/endpoint_service.dart'; +import 'package:thingsboard_app/utils/services/firebase/firebase_service.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class MockTbContext extends Mock implements TbContext {} + +class MockTbClient extends Mock implements ThingsboardClient {} + +class MockFirebaseService extends Mock implements FirebaseService {} + +class MockEndpointService extends Mock implements EndpointService {} diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..fab161e --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,26 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +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()); + + // Verify that our counter starts at 0. + expect(find.byWidgetPredicate((widget) { + if (widget is MaterialApp) { + return widget.title == 'ThingsBoard'; + } + return false; + }), findsOneWidget); + }); +}