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 0000000..d41780b Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ 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 0000000..b3dbfb0 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/thingsboard.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..d49065b Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/thingsboard.png b/android/app/src/main/res/mipmap-mdpi/thingsboard.png new file mode 100644 index 0000000..ee1251b Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/thingsboard.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..0290615 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ 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 0000000..e5a808c Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/thingsboard.png differ 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 0000000..fb60f24 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ 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 0000000..6fd5c6c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/thingsboard.png differ 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 0000000..9d28bde Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ 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 0000000..ba90b6a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/thingsboard.png differ 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 0000000..fd172a4 Binary files /dev/null and b/assets/images/thingsboard.png differ diff --git a/assets/images/thingsboard.svg b/assets/images/thingsboard.svg new file mode 100644 index 0000000..dfc7ba6 --- /dev/null +++ b/assets/images/thingsboard.svg @@ -0,0 +1,9 @@ + + + + + + + + + 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 0000000..feb37de Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..9e68503 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ 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 0000000..1f47bad Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ 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 0000000..3f08d83 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ 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 0000000..d625591 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..56970a1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..61094d1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ 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 0000000..1f47bad Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ 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 0000000..89c0960 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ 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 0000000..bd26230 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ 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 0000000..bd26230 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ 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 0000000..2983384 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..d750b3a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..37c6ff1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ 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 0000000..02ed5bd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9c5ea6c Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ 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 0000000..fbe58eb Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ 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 0000000..5b1955a Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ 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); + }); +}