diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# 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/ + +# 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..2e239db --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# 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. + +version: + revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: android + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: ios + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: linux + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: macos + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: web + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: windows + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +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 +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..fd80708 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,84 @@ +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' +// START: FlutterFire Configuration +apply plugin: 'com.google.gms.google-services' +// END: FlutterFire Configuration +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "com.example.sfm_app" + compileSdkVersion 34 + ndkVersion flutter.ndkVersion + + compileOptions { + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // START: Flutter Local Notifications + multiDexEnabled true + // END: Flutter Local Notifications + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.sfm_app" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 23 + 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 + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.google.firebase:firebase-messaging-directboot:20.2.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' +} diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..7b35503 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "910110439150", + "project_id": "sfm-notification", + "storage_bucket": "sfm-notification.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:910110439150:android:c3dbc3b4a85d7cb75b65ff", + "android_client_info": { + "package_name": "com.example.sfm_app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyADdZ66_q4HALlb0-yEGgeyGnsbwmlvDsA" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c8e16ef --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/sfm_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/sfm_app/MainActivity.kt new file mode 100644 index 0000000..495c626 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/sfm_app/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.sfm_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} 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..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + 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..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/raw/warning_alarm.mp3 b/android/app/src/main/res/raw/warning_alarm.mp3 new file mode 100644 index 0000000..9879346 Binary files /dev/null and b/android/app/src/main/res/raw/warning_alarm.mp3 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..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + 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..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..164baa5 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,34 @@ +buildscript { + ext.kotlin_version = '1.8.20' + repositories { + google() + mavenCentral() + } + + dependencies { + // START: FlutterFire Configuration + classpath 'com.google.gms:google-services:4.3.15' + // END: FlutterFire Configuration + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +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..3c472b9 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-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/assets/icons/en_icon.png b/assets/icons/en_icon.png new file mode 100644 index 0000000..8be3aa9 Binary files /dev/null and b/assets/icons/en_icon.png differ diff --git a/assets/icons/fire_station_marker.png b/assets/icons/fire_station_marker.png new file mode 100644 index 0000000..d98d46a Binary files /dev/null and b/assets/icons/fire_station_marker.png differ diff --git a/assets/icons/flame_icon.png b/assets/icons/flame_icon.png new file mode 100644 index 0000000..5836ce8 Binary files /dev/null and b/assets/icons/flame_icon.png differ diff --git a/assets/icons/hospital_marker.png b/assets/icons/hospital_marker.png new file mode 100644 index 0000000..b75ef26 Binary files /dev/null and b/assets/icons/hospital_marker.png differ diff --git a/assets/icons/normal_icon.png b/assets/icons/normal_icon.png new file mode 100644 index 0000000..8ab83a4 Binary files /dev/null and b/assets/icons/normal_icon.png differ diff --git a/assets/icons/offline_icon.png b/assets/icons/offline_icon.png new file mode 100644 index 0000000..0d4cbc9 Binary files /dev/null and b/assets/icons/offline_icon.png differ diff --git a/assets/icons/vi_icon.png b/assets/icons/vi_icon.png new file mode 100644 index 0000000..249be61 Binary files /dev/null and b/assets/icons/vi_icon.png differ diff --git a/assets/images/background_bottom.png b/assets/images/background_bottom.png new file mode 100644 index 0000000..663b6b1 Binary files /dev/null and b/assets/images/background_bottom.png differ diff --git a/assets/images/background_top.png b/assets/images/background_top.png new file mode 100644 index 0000000..fece45e Binary files /dev/null and b/assets/images/background_top.png differ diff --git a/assets/images/error_page.png b/assets/images/error_page.png new file mode 100644 index 0000000..9d31b00 Binary files /dev/null and b/assets/images/error_page.png differ diff --git a/assets/images/fire_warning.png b/assets/images/fire_warning.png new file mode 100644 index 0000000..6b96f43 Binary files /dev/null and b/assets/images/fire_warning.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..9e3c594 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/low_battery.png b/assets/images/low_battery.png new file mode 100644 index 0000000..7ede477 Binary files /dev/null and b/assets/images/low_battery.png differ diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..1e67bba --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"sfm-notification","appId":"1:910110439150:android:c3dbc3b4a85d7cb75b65ff","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"sfm-notification","configurations":{"android":"1:910110439150:android:c3dbc3b4a85d7cb75b65ff","ios":"1:910110439150:ios:1d111f0cdd0240a35b65ff","macos":"1:910110439150:ios:1d111f0cdd0240a35b65ff","web":"1:910110439150:web:b3dc22fa9ecb953b5b65ff","windows":"1:910110439150:web:7c413e6bc22555575b65ff"}}}}}} \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.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/ephemeral/ +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..9625e10 --- /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 + 11.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..ac0f289 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,68 @@ +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + # You can remove unused permissions here + # for more information: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h + # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.calendar + # 'PERMISSION_EVENTS=1', + + ## dart: PermissionGroup.calendarFullAccess + # 'PERMISSION_EVENTS_FULL_ACCESS=1', + + ## dart: PermissionGroup.reminders + # 'PERMISSION_REMINDERS=1', + + ## dart: PermissionGroup.contacts + # 'PERMISSION_CONTACTS=1', + + ## dart: PermissionGroup.camera + 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.microphone + # 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.speech + # 'PERMISSION_SPEECH_RECOGNIZER=1', + + ## dart: PermissionGroup.photos + # 'PERMISSION_PHOTOS=1', + + ## The 'PERMISSION_LOCATION' macro enables the `locationWhenInUse` and `locationAlways` permission. If + ## the application only requires `locationWhenInUse`, only specify the `PERMISSION_LOCATION_WHENINUSE` + ## macro. + ## + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + 'PERMISSION_LOCATION=1', + 'PERMISSION_LOCATION_WHENINUSE=0', + + ## dart: PermissionGroup.notification + 'PERMISSION_NOTIFICATIONS=1', + + ## dart: PermissionGroup.mediaLibrary + # 'PERMISSION_MEDIA_LIBRARY=1', + + ## dart: PermissionGroup.sensors + # 'PERMISSION_SENSORS=1', + + ## dart: PermissionGroup.bluetooth + # 'PERMISSION_BLUETOOTH=1', + + ## dart: PermissionGroup.appTrackingTransparency + # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', + + ## dart: PermissionGroup.criticalAlerts + 'PERMISSION_CRITICAL_ALERTS=1', + + ## dart: PermissionGroup.criticalAlerts + 'PERMISSION_ASSISTANT=1', + ] + + end + end +end \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..94e73de --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,613 @@ +// !$*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 */; }; + 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 */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy 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 */ + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + 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 = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 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 */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 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"; + }; + 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 */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency 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 = 11.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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 11.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 = 11.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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 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..e42adcb --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + 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..b7872dd --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,25 @@ +import UIKit +import Flutter +import flutter_local_notifications + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // This is required to make any communication available in the action isolate. + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) + } + GMSServices.provideAPIKey("AIzaSyDI8b-PUgKUgj5rHdtgEHCwWjUXYJrqYhE") + GeneratedPluginRegistrant.register(with: self) + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate + } + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} 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..dc9ada4 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..7353c41 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..797d452 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..6ed2d93 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..4cd7b00 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..fe73094 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..321773c 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..797d452 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..502f463 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..0ec3034 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..0ec3034 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..e9f5fea 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..84ac32a 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..8953cba 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..0467bf1 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..9da19ea 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..9da19ea 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..9da19ea 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.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..fa8c652 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Sfm App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + sfm_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + + NSLocationWhenInUseUsageDescription + Need location when in use + NSLocationAlwaysAndWhenInUseUsageDescription + Always and when in use! + NSLocationUsageDescription + Older devices need location. + NSLocationAlwaysUsageDescription + Can I have location always? + + NSCameraUsageDescription + camera + + + NSCameraUsageDescription + Camera permission is required for barcode scanning. + + + + 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/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..494619b --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,4 @@ +arb-dir: lib/product/lang/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +untranslated-messages-file: sfmTranslateErrors.txt diff --git a/lib/feature/auth/login/bloc/login_bloc.dart b/lib/feature/auth/login/bloc/login_bloc.dart new file mode 100644 index 0000000..a892c57 --- /dev/null +++ b/lib/feature/auth/login/bloc/login_bloc.dart @@ -0,0 +1,18 @@ +import 'dart:async'; +import '../../../../product/base/bloc/base_bloc.dart'; + +class LoginBloc extends BlocBase{ + + final loginRequest = StreamController>.broadcast(); + StreamSink> get sinkLoginRequest => loginRequest.sink; + Stream> get streamLoginRequest => loginRequest.stream; + + final isShowPassword = StreamController.broadcast(); + StreamSink get sinkIsShowPassword => isShowPassword.sink; + Stream get streamIsShowPassword => isShowPassword.stream; + + @override + void dispose() { + } + +} \ No newline at end of file diff --git a/lib/feature/auth/login/model/login_model.dart b/lib/feature/auth/login/model/login_model.dart new file mode 100644 index 0000000..04b87d0 --- /dev/null +++ b/lib/feature/auth/login/model/login_model.dart @@ -0,0 +1,17 @@ +class LoginModel { + int? exp; + int? iat; + String? id; + String? iss; + String? role; + + LoginModel({this.exp, this.iat, this.id, this.iss, this.role}); + + LoginModel.fromJson(Map json) { + exp = json['exp']; + iat = json['iat']; + id = json['id']; + iss = json['iss']; + role = json['role']; + } +} diff --git a/lib/feature/auth/login/screen/login_screen.dart b/lib/feature/auth/login/screen/login_screen.dart new file mode 100644 index 0000000..1de4387 --- /dev/null +++ b/lib/feature/auth/login/screen/login_screen.dart @@ -0,0 +1,234 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:convert'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../product/constant/app/api_path_constant.dart'; +import '../../../../product/constant/enums/app_route_enums.dart'; +import '../model/login_model.dart'; +import '../../../../product/cache/local_manager.dart'; +import '../../../../product/constant/enums/local_keys_enums.dart'; +import '../../../../product/services/api_services.dart'; +import '../../../../product/shared/shared_snack_bar.dart'; +import '../../../../product/constant/icon/icon_constants.dart'; +import '../../../../product/extention/context_extention.dart'; +import '../../../../product/constant/image/image_constants.dart'; +import '../../../../product/services/language_services.dart'; +import '../bloc/login_bloc.dart'; +import '../../../../product/base/bloc/base_bloc.dart'; +import '../../../../product/shared/shared_background.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + late LoginBloc loginBloc; + Map loginRequest = {"username": "", "password": ""}; + final _formKey = GlobalKey(); + bool isShowPassword = true; + APIServices apiServices = APIServices(); + @override + void initState() { + super.initState(); + loginBloc = BlocProvider.of(context); + checkLogin(context); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SharedBackground( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + const Spacer(), + Expanded( + flex: 4, + child: Image.asset( + ImageConstants.instance.getImage("logo"), + height: context.dynamicHeight(0.2), + ), + ), + const Spacer(), + ], + ), + SizedBox( + height: context.mediumValue, + ), + StreamBuilder>( + stream: loginBloc.streamLoginRequest, + builder: (context, loginResquestSnapshot) { + return Padding( + padding: context.paddingLow, + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + textInputAction: TextInputAction.next, + validator: (value) { + if (value == "null" || value!.isEmpty) { + return appLocalization(context) + .login_account_not_empty; + } + return null; + }, + onSaved: (username) { + loginRequest["username"] = username!; + loginBloc.sinkLoginRequest.add(loginRequest); + }, + decoration: InputDecoration( + hintText: + appLocalization(context).login_account_hint, + prefixIcon: Padding( + padding: context.dynamicPadding(16), + child: IconConstants.instance + .getMaterialIcon(Icons.person), + ), + ), + ), + SizedBox(height: context.lowValue), + StreamBuilder( + stream: loginBloc.streamIsShowPassword, + builder: (context, isShowPassSnapshot) { + return TextFormField( + textInputAction: TextInputAction.done, + obscureText: + isShowPassSnapshot.data ?? isShowPassword, + validator: (value) { + if (value == null || value.isEmpty) { + return appLocalization(context) + .login_password_not_empty; + } + return null; + }, + onSaved: (password) { + loginRequest["password"] = password!; + loginBloc.sinkLoginRequest.add(loginRequest); + }, + decoration: InputDecoration( + hintText: appLocalization(context) + .login_password_hint, + prefixIcon: Padding( + padding: context.dynamicPadding(16), + child: IconConstants.instance + .getMaterialIcon(Icons.lock), + ), + suffixIcon: IconButton( + onPressed: () { + isShowPassword = !isShowPassword; + loginBloc.sinkIsShowPassword + .add(isShowPassword); + }, + icon: isShowPassword + ? IconConstants.instance + .getMaterialIcon( + Icons.visibility, + ) + : IconConstants.instance + .getMaterialIcon( + Icons.visibility_off, + ), + ), + ), + ); + }, + ), + SizedBox( + height: context.lowValue, + ), + ElevatedButton( + style: const ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue), + foregroundColor: + MaterialStatePropertyAll(Colors.white), + ), + onPressed: () { + validate(); + }, + child: Text( + appLocalization(context).login_button_content), + ), + ], + ), + ), + ); + }, + ) + ], + ), + ), + ), + ); + } + + void validate() async { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + var data = + await apiServices.login(APIPathConstants.LOGIN_PATH, loginRequest); + if (data != "") { + Map tokenData = jsonDecode(data); + String token = tokenData['token']; + LocaleManager.instance.setString(PreferencesKeys.TOKEN, token); + String userToken = getBaseToken(token); + var decode = decodeBase64Token(userToken); + LoginModel loginModel = + LoginModel.fromJson(jsonDecode(decode) as Map); + LocaleManager.instance.setString(PreferencesKeys.UID, loginModel.id!); + LocaleManager.instance.setInt(PreferencesKeys.EXP, loginModel.exp!); + LocaleManager.instance + .setString(PreferencesKeys.ROLE, loginModel.role!); + context.goNamed(AppRoutes.HOME.name); + } else { + showErrorTopSnackBarCustom( + context, appLocalization(context).login_incorrect_usernameOrPass); + } + } + } + + void checkLogin(BuildContext context) async { + // ThemeNotifier themeNotifier = context.watch(); + await LocaleManager.prefrencesInit(); + // String theme = LocaleManager.instance.getStringValue(PreferencesKeys.THEME); + String token = LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN); + int exp = LocaleManager.instance.getIntValue(PreferencesKeys.EXP); + log("Token cu: ${LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN)}"); + log("UID: ${LocaleManager.instance.getStringValue(PreferencesKeys.UID)}"); + log("EXP: ${LocaleManager.instance.getIntValue(PreferencesKeys.EXP)}"); + log("Role: ${LocaleManager.instance.getStringValue(PreferencesKeys.ROLE)}"); + log("Theme: ${LocaleManager.instance.getStringValue(PreferencesKeys.THEME)}"); + log("Lang: ${LocaleManager.instance.getStringValue(PreferencesKeys.LANGUAGE_CODE)}"); + // log("Theme: $theme"); + // if (theme == AppThemes.DARK.name) { + // themeNotifier.changeValue(AppThemes.DARK); + // } else { + // themeNotifier.changeValue(AppThemes.LIGHT); + // } + int timeNow = DateTime.now().millisecondsSinceEpoch ~/ 1000; + if (token != "" && (exp - timeNow) > 7200) { + context.goNamed(AppRoutes.HOME.name); + } + } + + getBaseToken(String token) { + List parts = token.split('.'); + String userToken = parts[1]; + return userToken; + } + + decodeBase64Token(String value) { + List res = base64.decode(base64.normalize(value)); + return utf8.decode(res); + } +} diff --git a/lib/feature/bell/bell_bloc.dart b/lib/feature/bell/bell_bloc.dart new file mode 100644 index 0000000..98cb082 --- /dev/null +++ b/lib/feature/bell/bell_bloc.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import '../../product/base/bloc/base_bloc.dart'; +import 'bell_model.dart'; + +class BellBloc extends BlocBase { + + final bellItems = StreamController>.broadcast(); + StreamSink> get sinkBellItems => bellItems.sink; + Stream> get streamBellItems => bellItems.stream; + + final isLoading = StreamController.broadcast(); + StreamSink get sinkIsLoading => isLoading.sink; + Stream get streamIsLoading => isLoading.stream; + + final hasMore = StreamController.broadcast(); + StreamSink get sinkHasMore => hasMore.sink; + Stream get streamHasMore => hasMore.stream; + + @override + void dispose() {} +} diff --git a/lib/feature/bell/bell_model.dart b/lib/feature/bell/bell_model.dart new file mode 100644 index 0000000..da3c086 --- /dev/null +++ b/lib/feature/bell/bell_model.dart @@ -0,0 +1,75 @@ +class Bell { + int? offset; + String? result; + List? items; + + Bell({ + this.offset, + this.result, + this.items, + }); + + Bell.fromJson(Map json) { + offset = json["offset"]; + result = json["result"]; + if (json['items'] != null) { + items = []; + json['items'].forEach((v) { + items!.add(BellItems.fromJson(v)); + }); + } else { + items = []; + } + } +} + +class BellItems { + String? id; + String? notifiType; + String? userId; + String? eventType; + ItemDetail? itemDetail; + int? status; + DateTime? createdAt; + DateTime? updatedAt; + + BellItems( + {this.id, this.eventType, this.status, this.createdAt, this.updatedAt}); + + BellItems.fromJson(Map json) { + id = json["id"]; + notifiType = json["notifi_type"]; + userId = json["user_id"]; + eventType = json["event_type"]; + status = json["status"]; + itemDetail = + json['detail'] != null ? ItemDetail.fromJson(json['detail']) : null; + createdAt = DateTime.parse(json["created_at"]); + updatedAt = DateTime.parse(json["updated_at"]); + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => BellItems.fromJson(e)).toList(); + } +} + +class ItemDetail { + String? sourceId; + String? sourceName; + String? targetId; + String? targetName; + + ItemDetail({ + this.sourceId, + this.sourceName, + this.targetId, + this.targetName, + }); + + ItemDetail.fromJson(Map json) { + sourceId = json["source_id"]; + sourceName = json["source_name"]; + targetId = json["target_id"]; + targetName = json["target_name"]; + } +} diff --git a/lib/feature/bell/bell_screen.dart b/lib/feature/bell/bell_screen.dart new file mode 100644 index 0000000..982fbca --- /dev/null +++ b/lib/feature/bell/bell_screen.dart @@ -0,0 +1,253 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import '../../product/extention/context_extention.dart'; +import '../../product/services/language_services.dart'; +import '../../product/constant/enums/app_theme_enums.dart'; +import 'bell_bloc.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/services/api_services.dart'; +import 'bell_model.dart'; + +class BellScreen extends StatefulWidget { + const BellScreen({super.key}); + + @override + State createState() => _BellScreenState(); +} + +class _BellScreenState extends State { + late BellBloc bellBloc; + APIServices apiServices = APIServices(); + Timer? getBellTimer; + int offset = 0; + Bell bell = Bell(); + List items = []; + bool check = true; + bool hasMore = true; + final controller = ScrollController(); + + @override + void initState() { + super.initState(); + bellBloc = BlocProvider.of(context); + getBellNotification(offset); + controller.addListener( + () { + if (controller.position.maxScrollExtent == controller.offset) { + offset += 20; + getBellNotification(offset); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: bellBloc.streamIsLoading, + builder: (context, isLoadingSnapshot) { + return Scaffold( + appBar: AppBar( + title: Text(appLocalization(context).bell_page_title), + centerTitle: true, + ), + body: StreamBuilder>( + stream: bellBloc.streamBellItems, + initialData: items, + builder: (context, bellSnapshot) { + return check + ? Center( + child: CircularProgressIndicator( + value: context.highValue, + ), + ) + : bellSnapshot.data?.isEmpty ?? true + ? Center( + child: Text( + appLocalization(context).bell_page_no_items_body), + ) + : SizedBox( + width: double.infinity, + height: double.infinity, + child: RefreshIndicator( + onRefresh: refresh, + child: ListView.builder( + controller: controller, + itemCount: (bellSnapshot.data!.length + 1), + itemBuilder: (context, index) { + if (index < bellSnapshot.data!.length) { + return GestureDetector( + onTap: () async { + List read = []; + read.add(bellSnapshot.data![index].id!); + int code = await apiServices + .updateStatusOfNotification(read); + if (code == 200) { + read.clear(); + } else { + read.clear(); + } + refresh(); + }, + child: Column( + children: [ + ListTile( + title: Text( + getBellEvent( + context, + bellSnapshot.data![index] + .itemDetail!.sourceName!, + bellSnapshot + .data![index].eventType!, + bellSnapshot.data![index] + .itemDetail!.targetName!, + ), + overflow: TextOverflow.ellipsis, + maxLines: 3, + style: + const TextStyle(fontSize: 15), + ), + trailing: Text( + timeAgo( + context, + bellSnapshot + .data![index].createdAt!, + ), + ), + ), + const Divider( + height: 1, + ), + ], + ), + ); + } else { + return Padding( + padding: const EdgeInsets.all(8.0), + child: StreamBuilder( + stream: bellBloc.streamHasMore, + builder: (context, hasMoreSnapshot) { + return Center( + child: hasMoreSnapshot.data ?? hasMore + ? const CircularProgressIndicator() + : Text( + appLocalization(context) + .bell_read_all, + ), + ); + }, + ), + ); + } + }, + ), + ), + ); + }, + ), + ); + }, + ); + } + + Future refresh() async { + check = true; + offset = 0; + items.clear(); + bellBloc.sinkBellItems.add(items); + hasMore = true; + getBellNotification(offset); + } + + Future getBellNotification(int offset) async { + bell = await apiServices.getBellNotifications( + offset.toString(), (offset + 20).toString()); + + if (bell.items!.isEmpty) { + hasMore = false; + bellBloc.sinkHasMore.add(hasMore); + } else { + for (var item in bell.items!) { + items.add(item); + } + } + bellBloc.bellItems.add(items); + check = false; + } + + bool checkStatus(List bells) { + for (var bell in bells) { + if (bell.status == 0) { + return false; + } + } + return true; + } + + Future colorByTheme(int status) async { + String theme = await apiServices.checkTheme(); + if (theme == AppThemes.LIGHT.name && status == 1) { + return Colors.white; + } else if (theme == AppThemes.DARK.name && status == 1) { + return Colors.black; + } else { + return const Color.fromARGB(255, 90, 175, 214); + } + } + + String timeAgo(BuildContext context, DateTime dateTime) { + final duration = DateTime.now().difference(dateTime); + + if (duration.inDays > 0) { + return '${duration.inDays} ${appLocalization(context).bell_days_ago}'; + } else if (duration.inHours > 0) { + return '${duration.inHours} ${appLocalization(context).bell_hours_ago}'; + } else if (duration.inMinutes > 0) { + return '${duration.inMinutes} ${appLocalization(context).bell_minutes_ago}'; + } else { + return appLocalization(context).bell_just_now; + } + } + + String getBellEvent(BuildContext context, String deviceName, String eventCode, + String targetName) { + String message = ""; + + if (eventCode == "001") { + message = + "${appLocalization(context).device_title} $deviceName ${appLocalization(context).disconnect_message_lowercase}"; + } else if (eventCode == "002") { + message = + "${appLocalization(context).device_title} $deviceName ${appLocalization(context).gf_connected_lowercase}"; + } else if (eventCode == "003") { + message = + "${appLocalization(context).device_title} $deviceName ${appLocalization(context).smoke_detecting_message_lowercase}"; + } else if (eventCode == "004") { + message = + "${appLocalization(context).device_title} $deviceName ${appLocalization(context).error_message_lowercase}"; + } else if (eventCode == "005") { + message = + "${appLocalization(context).device_title} $deviceName ${appLocalization(context).bell_operate_normal}"; + } else if (eventCode == "006") { + message = + "${appLocalization(context).bell_battery_device} $deviceName ${appLocalization(context).low_message_lowercase}"; + } else if (eventCode == "101") { + message = + "${appLocalization(context).bell_user_uppercase} ${appLocalization(context).bell_user_joined_group}"; + } else if (eventCode == "102") { + message = + "${appLocalization(context).bell_user_uppercase} $deviceName ${appLocalization(context).bell_leave_group} $targetName "; + } else if (eventCode == "103") { + message = + "${appLocalization(context).device_title} $deviceName ${appLocalization(context).bell_user_added_group} $targetName"; + } else if (eventCode == "104") { + message = + "${appLocalization(context).device_title} $deviceName ${appLocalization(context).bell_user_kick_group} $targetName"; + } else { + message = appLocalization(context).bell_invalid_code; + } + + return message; + } +} diff --git a/lib/feature/devices/add_new_device_widget.dart b/lib/feature/devices/add_new_device_widget.dart new file mode 100644 index 0000000..6c0f436 --- /dev/null +++ b/lib/feature/devices/add_new_device_widget.dart @@ -0,0 +1,97 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import '../../product/utils/response_status_utils.dart'; +import '../../product/constant/enums/role_enums.dart'; +import '../../product/services/api_services.dart'; +import '../../product/utils/qr_utils.dart'; +import '../../product/constant/icon/icon_constants.dart'; +import '../../product/extention/context_extention.dart'; +import '../../product/services/language_services.dart'; + +addNewDevice(BuildContext context, String role) async { + TextEditingController extIDController = TextEditingController(text: ""); + TextEditingController deviceNameController = TextEditingController(text: ""); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + dismissDirection: DismissDirection.none, + duration: context.dynamicMinutesDuration(1), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${appLocalization(context).add_device_title}: ', + style: context.titleMediumTextStyle), + Container( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: () async { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + icon: IconConstants.instance.getMaterialIcon(Icons.close)), + ) + ], + ), + TextField( + textInputAction: TextInputAction.next, + controller: extIDController, + decoration: InputDecoration( + hintText: appLocalization(context).input_extID_device_hintText, + suffixIcon: IconButton( + onPressed: () async { + String result = + await QRScanUtils.instance.scanQR(context); + extIDController.text = result; + }, + icon: IconConstants.instance + .getMaterialIcon(Icons.qr_code_scanner_outlined))), + ), + role == RoleEnums.ADMIN.name + ? TextField( + textInputAction: TextInputAction.done, + controller: deviceNameController, + decoration: InputDecoration( + hintText: + appLocalization(context).input_name_device_hintText), + ) + : const SizedBox.shrink(), + Center( + child: TextButton( + onPressed: () async { + String extID = extIDController.text; + String deviceName = deviceNameController.text; + addDevices(context, role, extID, deviceName); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + child: Text(appLocalization(context).add_button_content)), + ) + ], + ), + ), + ); +} + +void addDevices( + BuildContext context, String role, String extID, String deviceName) async { + APIServices apiServices = APIServices(); + Map body = {}; + if (role == RoleEnums.ADMIN.name) { + body = {"ext_id": extID, "name": deviceName}; + int statusCode = await apiServices.createDeviceByAdmin(body); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_create_device_success, + appLocalization(context).notification_create_device_failed); + } else { + body = {"ext_id": extID}; + int statusCode = await apiServices.registerDevice(body); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_add_device_success, + appLocalization(context).notification_device_not_exist); + } +} diff --git a/lib/feature/devices/delete_device_widget.dart b/lib/feature/devices/delete_device_widget.dart new file mode 100644 index 0000000..49ac52a --- /dev/null +++ b/lib/feature/devices/delete_device_widget.dart @@ -0,0 +1,61 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import '../../product/constant/enums/role_enums.dart'; +import '../../product/services/api_services.dart'; +import '../../product/services/language_services.dart'; +import '../../product/utils/response_status_utils.dart'; + +handleDeleteDevice(BuildContext context, String thingID, String role) { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Text(appLocalization(dialogContext).delete_device_dialog_title), + content: + Text(appLocalization(dialogContext).delete_device_dialog_content), + actions: [ + TextButton( + child: Text(appLocalization(dialogContext).cancel_button_content), + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + ), + TextButton( + child: Text( + appLocalization(dialogContext).delete_button_content, + style: const TextStyle(color: Colors.red), + ), + onPressed: () { + deleteOrUnregisterDevice(context, thingID, role); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); +} + +deleteOrUnregisterDevice( + BuildContext context, String thingID, String role) async { + APIServices apiServices = APIServices(); + if (role == RoleEnums.USER.name) { + Map body = { + "thing_id": thingID, + }; + int statusCode = await apiServices.unregisterDevice(body); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_delete_device_success, + appLocalization(context).notification_delete_device_failed); + } else { + int statusCode = await apiServices.deleteDeviceByAdmin(thingID); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_delete_device_success, + appLocalization(context).notification_delete_device_failed); + } +} diff --git a/lib/feature/devices/device_detail/device_detail_bloc.dart b/lib/feature/devices/device_detail/device_detail_bloc.dart new file mode 100644 index 0000000..474dd83 --- /dev/null +++ b/lib/feature/devices/device_detail/device_detail_bloc.dart @@ -0,0 +1,79 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:sfm_app/product/services/api_services.dart'; +import 'package:sfm_app/product/utils/device_utils.dart'; + +import '../device_model.dart'; + +import '../../../product/base/bloc/base_bloc.dart'; + +class DetailDeviceBloc extends BlocBase { + APIServices apiServices = APIServices(); + + final deviceInfo = StreamController.broadcast(); + StreamSink get sinkDeviceInfo => deviceInfo.sink; + Stream get streamDeviceInfo => deviceInfo.stream; + + final deviceSensor = StreamController>.broadcast(); + StreamSink> get sinkDeviceSensor => deviceSensor.sink; + Stream> get streamDeviceSensor => deviceSensor.stream; + + final deviceLocation = StreamController.broadcast(); + StreamSink get sinkDeviceLocation => deviceLocation.sink; + Stream get streamDeviceLocation => deviceLocation.stream; + + @override + void dispose() {} + + void getDeviceDetail( + BuildContext context, + String thingID, + Completer controller, + ) async { + String body = await apiServices.getDeviceInfomation(thingID); + if (body != "") { + final data = jsonDecode(body); + Device device = Device.fromJson(data); + sinkDeviceInfo.add(device); + if (device.areaPath != null) { + String fullLocation = await DeviceUtils.instance + .getFullDeviceLocation(context, device.areaPath!); + log("Location: $fullLocation"); + sinkDeviceLocation.add(fullLocation); + } + Map sensorMap = {}; + if (device.status!.sensors != null) { + sensorMap = DeviceUtils.instance + .getDeviceSensors(context, device.status!.sensors!); + } else { + sensorMap = DeviceUtils.instance.getDeviceSensors(context, []); + } + sinkDeviceSensor.add(sensorMap); + if (device.settings!.latitude! != "" && + device.settings!.longitude! != "") { + final CameraPosition cameraPosition = CameraPosition( + target: LatLng( + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + ), + zoom: 13, + ); + final GoogleMapController mapController = await controller.future; + mapController + .animateCamera(CameraUpdate.newCameraPosition(cameraPosition)); + } + } + } + + void findLocation(BuildContext context, String areaPath) async { + String fullLocation = + await DeviceUtils.instance.getFullDeviceLocation(context, areaPath); + sinkDeviceLocation.add(fullLocation); + } +} diff --git a/lib/feature/devices/device_detail/device_detail_screen.dart b/lib/feature/devices/device_detail/device_detail_screen.dart new file mode 100644 index 0000000..57a9ae5 --- /dev/null +++ b/lib/feature/devices/device_detail/device_detail_screen.dart @@ -0,0 +1,361 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:simple_ripple_animation/simple_ripple_animation.dart'; +import 'dart:math' as math; +import '../device_model.dart'; +import '../../../product/base/bloc/base_bloc.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/utils/device_utils.dart'; + +import '../../../product/constant/icon/icon_constants.dart'; +import 'device_detail_bloc.dart'; + +class DetailDeviceScreen extends StatefulWidget { + const DetailDeviceScreen({super.key, required this.thingID}); + final String thingID; + @override + State createState() => _DetailDeviceScreenState(); +} + +class _DetailDeviceScreenState extends State { + List imageAssets = [ + IconConstants.instance.getIcon("normal_icon"), + IconConstants.instance.getIcon("offline_icon"), + IconConstants.instance.getIcon("flame_icon"), + ]; + String stateImgAssets(int state) { + String imgStringAsset; + if (state == 0) { + imgStringAsset = imageAssets[0]; + } else if (state == 1) { + imgStringAsset = imageAssets[1]; + } else { + imgStringAsset = imageAssets[2]; + } + return imgStringAsset; + } + + late DetailDeviceBloc detailDeviceBloc; + Completer controller = Completer(); + CameraPosition initialCamera = const CameraPosition( + target: LatLng(20.966048511844402, 105.74977710843086), zoom: 15); + @override + void initState() { + super.initState(); + detailDeviceBloc = BlocProvider.of(context); + } + + @override + Widget build(BuildContext context) { + double screenWidth = MediaQuery.of(context).size.width; + return StreamBuilder( + stream: detailDeviceBloc.streamDeviceInfo, + builder: (context, deviceSnapshot) { + if (deviceSnapshot.data?.extId == null) { + detailDeviceBloc.getDeviceDetail(context, widget.thingID, controller); + return const Center( + child: CircularProgressIndicator(), + ); + } else { + return StreamBuilder>( + stream: detailDeviceBloc.streamDeviceSensor, + builder: (context, sensorSnapshot) { + if (sensorSnapshot.data != null) { + return Scaffold( + appBar: AppBar( + title: Text(appLocalization(context).detail_message), + centerTitle: true, + ), + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + // device Name + Card( + child: Container( + width: context.dynamicWidth(1), + height: context.highValue, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + child: Center( + child: Text( + '${appLocalization(context).device_title}: ${deviceSnapshot.data!.name}', + style: const TextStyle( + fontSize: 20, + ), + ), + ), + ), + ), + // Tinh trang va nhiet do + Row( + children: [ + Card( + child: Container( + width: (screenWidth - 20) / 2, + height: context.highValue, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + padding: + const EdgeInsets.fromLTRB(5, 5, 0, 5), + alignment: Alignment.centerLeft, + child: Row( + children: [ + SizedBox( + height: 25, + width: 25, + child: RippleAnimation( + color: DeviceUtils.instance + .getColorRiple( + deviceSnapshot.data!.state!), + delay: + const Duration(milliseconds: 800), + repeat: true, + minRadius: 40, + ripplesCount: 6, + duration: const Duration( + milliseconds: 6 * 300), + child: CircleAvatar( + minRadius: 20, + maxRadius: 20, + backgroundImage: AssetImage( + stateImgAssets( + deviceSnapshot.data!.state!, + ), + ), + ), + ), + ), + SizedBox( + width: context.lowValue, + ), + Text( + DeviceUtils.instance.checkStateDevice( + context, + deviceSnapshot.data!.state!, + ), + style: const TextStyle( + fontSize: 15, + ), + ), + ], + ), + ), + ), + Card( + child: SizedBox( + width: (screenWidth - 20) / 2, + height: context.highValue, + child: Container( + alignment: Alignment.centerLeft, + padding: + const EdgeInsets.fromLTRB(5, 5, 0, 5), + child: Row( + children: [ + const Icon( + Icons.thermostat, + color: Colors.blue, + size: 30, + ), + const SizedBox( + width: 10, + ), + Text( + "${appLocalization(context).paginated_data_table_column_deviceTemperature}: ${sensorSnapshot.data?['sensorTemp'] ?? 100}", + style: const TextStyle( + fontSize: 15, + ), + ), + ], + ), + ), + ), + ), + ], + ), + Row( + children: [ + Card( + child: Container( + width: (screenWidth - 20) / 2, + height: context.highValue, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + alignment: Alignment.centerLeft, + padding: + const EdgeInsets.fromLTRB(10, 5, 0, 5), + child: Row( + children: [ + Transform.rotate( + angle: 90 * math.pi / 180, + child: Icon( + DeviceUtils.instance.getBatteryIcon( + int.parse( + sensorSnapshot + .data!['sensorBattery'], + ), + ), + color: Colors.blue, + size: 30, + ), + ), + SizedBox( + width: context.lowValue, + ), + Text( + "${appLocalization(context).paginated_data_table_column_deviceBaterry}: ${sensorSnapshot.data!['sensorBattery']}%", + style: const TextStyle( + fontSize: 15, + ), + ), + ], + ), + ), + ), + Card( + child: Container( + width: (screenWidth - 20) / 2, + height: context.highValue, + alignment: Alignment.centerLeft, + padding: + const EdgeInsets.fromLTRB(10, 5, 0, 5), + child: Row( + children: [ + Icon( + DeviceUtils.instance.getSignalIcon( + context, + sensorSnapshot.data!['sensorCsq'], + ), + color: Colors.blue, + size: 30, + ), + SizedBox( + width: context.lowValue, + ), + Text( + "${appLocalization(context).paginated_data_table_column_deviceSignal}: ${sensorSnapshot.data!['sensorCsq']}", + style: const TextStyle(fontSize: 15), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + ], + ), + ), + ), + ], + ), + Card( + child: Container( + padding: const EdgeInsets.all(10.0), + height: context.highValue, + child: Row( + children: [ + const Icon( + Icons.location_on, + color: Colors.blue, + size: 30, + ), + SizedBox( + width: context.lowValue, + ), + Expanded( + child: StreamBuilder( + stream: + detailDeviceBloc.streamDeviceLocation, + builder: (context, locationSnapshot) { + if (locationSnapshot.data != null) { + return Text( + locationSnapshot.data ?? "", + style: + const TextStyle(fontSize: 13), + maxLines: 3, + overflow: TextOverflow.ellipsis, + softWrap: true, + ); + } else { + detailDeviceBloc.findLocation(context, + deviceSnapshot.data!.areaPath!); + return Text(appLocalization(context) + .undefine_message); + } + }, + ), + ), + ], + ), + ), + ), + Card( + child: Container( + height: 300, + padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(15), + ), + ), + child: deviceSnapshot.data!.settings!.latitude != + "" + ? GoogleMap( + initialCameraPosition: initialCamera, + mapType: MapType.normal, + markers: { + Marker( + markerId: MarkerId( + deviceSnapshot.data!.thingId!), + position: LatLng( + double.parse(deviceSnapshot + .data!.settings!.latitude!), + double.parse(deviceSnapshot + .data!.settings!.longitude!), + ), + ), + }, + onMapCreated: (mapcontroller) { + controller.complete(mapcontroller); + }, + mapToolbarEnabled: false, + zoomControlsEnabled: false, + liteModeEnabled: true, + ) + : Center( + child: Text( + appLocalization(context) + .detail_device_dont_has_location_message, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } else { + return Scaffold( + appBar: AppBar(), + body: const Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ); + } + }, + ); + } +} diff --git a/lib/feature/devices/device_model.dart b/lib/feature/devices/device_model.dart new file mode 100644 index 0000000..a58cf8a --- /dev/null +++ b/lib/feature/devices/device_model.dart @@ -0,0 +1,171 @@ +import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +// class Device { +// int? offset; +// List? items; + +// Device({ +// this.offset, +// this.items, +// }); +// Device.fromJson(Map json) { +// offset = json['offset']; +// if (json['items'] != null) { +// items = []; +// json['items'].forEach((v) { +// items!.add(Item.fromJson(v)); +// }); +// } +// } +// } + +class Device with ClusterItem { + String? extId; + String? thingId; + String? thingKey; + List? channels; + String? areaPath; + String? fvers; + String? name; + HardwareInfo? hardwareInfo; + Settings? settings; + Status? status; + DateTime? connectionTime; + int? state; + String? visibility; + DateTime? createdAt; + DateTime? updatedAt; + + Device({ + this.extId, + this.thingId, + this.thingKey, + this.channels, + this.areaPath, + this.fvers, + this.name, + this.hardwareInfo, + this.settings, + this.status, + this.connectionTime, + this.state, + this.visibility, + this.createdAt, + this.updatedAt, + }); + Device.fromJson(Map json) { + extId = json['ext_id']; + thingId = json['thing_id']; + thingKey = json['thing_key']; + if (json['channels'] != null) { + channels = []; + json['channels'].forEach((v) { + channels!.add(v); + }); + } + areaPath = json['area_path']; + fvers = json['fvers']; + name = json['name']; + hardwareInfo = json['hardware_info'] != null + ? HardwareInfo.fromJson(json['hardware_info']) + : null; + settings = + json['settings'] != null ? Settings.fromJson(json['settings']) : null; + status = json['status'] != null ? Status.fromJson(json['status']) : null; + connectionTime = DateTime.parse(json['connection_time']); + state = json['state']; + visibility = json['visibility']; + createdAt = DateTime.parse(json['created_at']); + updatedAt = DateTime.parse(json['updated_at']); + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => Device.fromJson(e)).toList(); + } + + @override + LatLng get location { + double latitude = double.tryParse(settings?.latitude ?? '') ?? 34.639971; + double longitude = double.tryParse(settings?.longitude ?? '') ?? -39.664027; + return LatLng(latitude, longitude); + } +} + +class HardwareInfo { + String? gsm; + String? imei; + String? imsi; + String? phone; + DateTime? updatedAt; + + HardwareInfo({ + this.gsm, + this.imei, + this.imsi, + this.phone, + this.updatedAt, + }); + HardwareInfo.fromJson(Map json) { + gsm = json['gsm']; + imei = json['imei']; + imsi = json['imsi']; + phone = json['phone']; + updatedAt = DateTime.parse(json['updated_at']); + } +} + +class Settings { + String? latitude; + String? longitude; + DateTime? updatedAt; + + Settings({ + this.latitude, + this.longitude, + this.updatedAt, + }); + Settings.fromJson(Map json) { + latitude = json['latitude']; + longitude = json['longitude']; + updatedAt = DateTime.parse(json['updated_at']); + } +} + +class Status { + int? readOffset; + List? sensors; + DateTime? updatedAt; + + Status({ + this.readOffset, + this.sensors, + this.updatedAt, + }); + + Status.fromJson(Map json) { + readOffset = json['read_offset']; + if (json['sensors'] != null) { + sensors = []; + json['sensors'].forEach((v) { + sensors!.add(Sensor.fromJson(v)); + }); + } + updatedAt = DateTime.parse(json['updated_at']); + } +} + +class Sensor { + String? name; + int? value; + int? time; + dynamic x; + + Sensor({this.name, this.value, this.time, this.x}); + Sensor.fromJson(Map json) { + name = json['n']; + value = json['v']; + time = json['t']; + x = json['x']; + } +} diff --git a/lib/feature/devices/device_update/device_update_bloc.dart b/lib/feature/devices/device_update/device_update_bloc.dart new file mode 100644 index 0000000..c01fc87 --- /dev/null +++ b/lib/feature/devices/device_update/device_update_bloc.dart @@ -0,0 +1,255 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:intl/intl.dart'; +import '../../../product/services/api_services.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/shared/model/ward_model.dart'; +import '../../../product/utils/response_status_utils.dart'; + +import '../../../product/shared/model/district_model.dart'; +import '../../../product/shared/model/province_model.dart'; +import '../device_model.dart'; +import '../../../product/base/bloc/base_bloc.dart'; + +class DeviceUpdateBloc extends BlocBase { + APIServices apiServices = APIServices(); + + final deviceInfo = StreamController.broadcast(); + StreamSink get sinkDeviceInfo => deviceInfo.sink; + Stream get streamDeviceInfo => deviceInfo.stream; + + // DeviceUpdateScreen + final isChanged = StreamController.broadcast(); + StreamSink get sinkIsChanged => isChanged.sink; + Stream get streamIsChanged => isChanged.stream; + + final provinceData = StreamController>.broadcast(); + StreamSink> get sinkProvinceData => provinceData.sink; + Stream> get streamProvinceData => provinceData.stream; + + final listProvinces = + StreamController>>.broadcast(); + StreamSink>> get sinkListProvinces => + listProvinces.sink; + Stream>> get streamListProvinces => + listProvinces.stream; + + final districtData = StreamController>.broadcast(); + StreamSink> get sinkDistrictData => districtData.sink; + Stream> get streamDistrictData => districtData.stream; + + final listDistricts = + StreamController>>.broadcast(); + StreamSink>> get sinkListDistricts => + listDistricts.sink; + Stream>> get streamListDistricts => + listDistricts.stream; + + final wardData = StreamController>.broadcast(); + StreamSink> get sinkWardData => wardData.sink; + Stream> get streamWardData => wardData.stream; + + final listWards = StreamController>>.broadcast(); + StreamSink>> get sinkListWards => listWards.sink; + Stream>> get streamListWards => listWards.stream; + + // Show Maps in DeviceUpdateScreen + + final markers = StreamController>.broadcast(); + StreamSink> get sinkMarkers => markers.sink; + Stream> get streamMarkers => markers.stream; + + final searchLocation = StreamController.broadcast(); + StreamSink get sinkSearchLocation => + searchLocation.sink; + Stream get streamSearchLocation => + searchLocation.stream; + + @override + void dispose() { + // deviceInfo.done; + } + + Future getAllProvinces() async { + List> provincesData = []; + provincesData.clear(); + sinkListProvinces.add(provincesData); + final body = await apiServices.getAllProvinces(); + final data = jsonDecode(body); + List items = data["items"]; + + final provinces = Province.fromJsonDynamicList(items); + for (var province in provinces) { + provincesData.add( + DropdownMenuItem(value: province, child: Text(province.fullName!))); + } + sinkListProvinces.add(provincesData); + } + + Future getAllDistricts(String provinceID) async { + List> districtsData = []; + districtsData.clear(); + sinkListDistricts.add(districtsData); + final body = await apiServices.getAllDistricts(provinceID); + final data = jsonDecode(body); + List items = data["items"]; + final districts = District.fromJsonDynamicList(items); + for (var district in districts) { + districtsData.add( + DropdownMenuItem(value: district, child: Text(district.fullName!))); + } + sinkListDistricts.add(districtsData); + } + + Future getAllWards(String districtID) async { + List> wardsData = []; + wardsData.clear(); + sinkListWards.add(wardsData); + final body = await apiServices.getAllWards(districtID); + final data = jsonDecode(body); + List items = data["items"]; + final wards = Ward.fromJsonDynamicList(items); + for (var ward in wards) { + wardsData.add(DropdownMenuItem(value: ward, child: Text(ward.fullName!))); + } + sinkListWards.add(wardsData); + } + + Future getDeviceInfomation( + String thingID, + List> districtsData, + List> wardsData, + TextEditingController deviceNameController, + TextEditingController latitudeController, + TextEditingController longitudeController) async { + String body = await apiServices.getDeviceInfomation(thingID); + final data = jsonDecode(body); + Device device = Device.fromJson(data); + sinkDeviceInfo.add(device); + deviceNameController.text = device.name ?? ""; + latitudeController.text = device.settings!.latitude ?? ""; + longitudeController.text = device.settings!.longitude ?? ""; + if (device.areaPath != "") { + List areaPath = device.areaPath!.split('_'); + String provinceCode = areaPath[0]; + String districtCode = areaPath[1]; + String wardCode = areaPath[2]; + getAllDistricts(provinceCode); + getAllWards(districtCode); + final provinceResponse = await apiServices.getProvinceByID(provinceCode); + final provincesData = jsonDecode(provinceResponse); + Province province = Province.fromJson(provincesData['data']); + final districtResponse = await apiServices.getDistrictByID(districtCode); + final districtData = jsonDecode(districtResponse); + District district = District.fromJson(districtData['data']); + final wardResponse = await apiServices.getWardByID(wardCode); + final wardData = jsonDecode(wardResponse); + Ward ward = Ward.fromJson(wardData['data']); + Map provinceData = { + "name": province.fullName!, + "code": province.code! + }; + sinkProvinceData.add(provinceData); + Map districData = { + "name": district.fullName!, + "code": district.code!, + }; + sinkDistrictData.add(districData); + Map wardMap = { + "name": ward.fullName!, + "code": ward.code!, + }; + sinkWardData.add(wardMap); + } + } + + Future getProvinceByName(String name) async { + final response = await apiServices.getProvincesByName(name); + final data = jsonDecode(response); + if (data != null && + data.containsKey('items') && + data['items'] != null && + data['items'].isNotEmpty) { + List items = data['items']; + List provinces = Province.fromJsonDynamicList(items); + if (provinces.isNotEmpty) { + return provinces[0]; + } + } + return Province(name: "null"); + } + + Future getDistrictByName(String name, String provinceCode) async { + final response = await apiServices.getDistrictsByName(name); + if (response != "") { + final data = jsonDecode(response); + List items = data['items']; + if (items.isNotEmpty) { + List districts = District.fromJsonDynamicList(items); + if (districts.isNotEmpty) { + for (var district in districts) { + if (district.provinceCode == provinceCode) { + return district; + } + } + } + } + } + return District(name: "null"); + } + + Future getWardByName(String name, String districtCode) async { + final response = await apiServices.getWarsdByName(name); + final data = jsonDecode(response); + if (data != null && data['items'] != null) { + List items = data['items']; + if (items.isNotEmpty) { + List wards = Ward.fromJsonDynamicList(items); + if (wards.isNotEmpty) { + for (var ward in wards) { + if (ward.districtCode == districtCode) { + return ward; + } + } + } + } + } + return Ward(name: "null"); + } + + Future updateDevice( + BuildContext context, + String thingID, + String name, + String latitude, + String longitude, + String provinceCode, + String districtCode, + String wardCode, + ) async { + DateTime dateTime = DateTime.now(); + String formattedDateTime = + DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime); + Map body = { + "name": name, + "area_province": provinceCode, + "area_district": districtCode, + "area_ward": wardCode, + "latitude": latitude, + "longitude": longitude, + "note": "User updated device infomation at $formattedDateTime", + }; + int statusCode = await apiServices.updateOwnerDevice(thingID, body); + showSnackBarResponseByStatusCodeNoIcon( + context, + statusCode, + appLocalization(context).notification_update_device_success, + appLocalization(context).notification_update_device_failed, + ); + } +} diff --git a/lib/feature/devices/device_update/device_update_screen.dart b/lib/feature/devices/device_update/device_update_screen.dart new file mode 100644 index 0000000..2d8ffbb --- /dev/null +++ b/lib/feature/devices/device_update/device_update_screen.dart @@ -0,0 +1,515 @@ +import 'package:flutter/material.dart'; +import 'package:search_choices/search_choices.dart'; + +import '../device_model.dart'; +import 'device_update_bloc.dart'; +import 'map_dialog.dart'; +import '../../../product/base/bloc/base_bloc.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/api_services.dart'; +import '../../../product/services/language_services.dart'; + +import '../../../product/shared/model/district_model.dart'; +import '../../../product/shared/model/province_model.dart'; +import '../../../product/shared/model/ward_model.dart'; + +class DeviceUpdateScreen extends StatefulWidget { + const DeviceUpdateScreen({super.key, required this.thingID}); + final String thingID; + @override + State createState() => _DeviceUpdateScreenState(); +} + +class _DeviceUpdateScreenState extends State { + late DeviceUpdateBloc deviceUpdateBloc; + APIServices apiServices = APIServices(); + Device device = Device(); + bool isChanged = false; + TextEditingController deviceNameController = TextEditingController(); + TextEditingController deviceLatitudeController = TextEditingController(); + TextEditingController deviceLongitudeController = TextEditingController(); + List> provincesData = []; + String? selectedProvince = ""; + List> districtsData = []; + String? selectedDistrict = ""; + List> wardsData = []; + String? selectedWard = ""; + + // static String provinceCode = ""; + // static String districtCode = ""; + // static String wardCode = ""; + + Map provinceData = {}; + Map districtData = {}; + Map wardData = {}; + + @override + void initState() { + super.initState(); + deviceUpdateBloc = BlocProvider.of(context); + deviceUpdateBloc.getAllProvinces(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text(appLocalization(context).device_update_title), + ), + body: StreamBuilder>( + stream: deviceUpdateBloc.streamProvinceData, + builder: (context, provinceNameSnapshot) { + return StreamBuilder>( + stream: deviceUpdateBloc.streamDistrictData, + builder: (context, districtNameSnapshot) { + return StreamBuilder>( + stream: deviceUpdateBloc.streamWardData, + builder: (context, wardNameSnapshot) { + return SafeArea( + child: StreamBuilder( + stream: deviceUpdateBloc.streamDeviceInfo, + initialData: device, + builder: (context, deviceInfoSnapshot) { + if (deviceInfoSnapshot.data!.thingId == null) { + deviceUpdateBloc.getDeviceInfomation( + widget.thingID, + districtsData, + wardsData, + deviceNameController, + deviceLatitudeController, + deviceLongitudeController); + return const Center( + child: CircularProgressIndicator(), + ); + } else { + return StreamBuilder( + stream: deviceUpdateBloc.streamIsChanged, + initialData: isChanged, + builder: (context, isChangedSnapshot) { + return SingleChildScrollView( + child: Padding( + padding: context.paddingLow, + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${appLocalization(context).input_name_device_device}:", + style: context + .titleMediumTextStyle), + Padding( + padding: + context.paddingLowVertical, + child: TextField( + onChanged: (value) { + isChangedListener(); + }, + textInputAction: + TextInputAction.next, + controller: + deviceNameController, + decoration: InputDecoration( + hintText: appLocalization( + context) + .input_name_device_hintText, + ), + ), + ), + Text( + "${appLocalization(context).device_update_location}:", + style: context + .titleMediumTextStyle), + Padding( + padding: + context.paddingLowVertical, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + SizedBox( + width: context + .dynamicWidth(0.6), + child: Column( + children: [ + SizedBox( + child: TextField( + onChanged: (value) { + isChangedListener(); + }, + textInputAction: + TextInputAction + .next, + controller: + deviceLatitudeController, + decoration: + InputDecoration( + label: Text( + "${appLocalization(context).update_device_dialog_location_longitude}:"), + hintText: appLocalization( + context) + .update_device_dialog_location_longitude_hintText, + ), + ), + ), + Padding( + padding: context + .paddingLowVertical, + child: SizedBox( + child: TextField( + onChanged: + (value) { + isChangedListener(); + }, + controller: + deviceLongitudeController, + textInputAction: + TextInputAction + .next, + decoration: + InputDecoration( + label: Text( + "${appLocalization(context).update_device_dialog_location_latitude}:"), + hintText: appLocalization( + context) + .update_device_dialog_location_latitude_hintText, + ), + ), + ), + ), + ], + ), + ), + Center( + child: IconButton.filled( + style: const ButtonStyle( + backgroundColor: + MaterialStatePropertyAll( + Colors + .lightGreen)), + // iconSize: 24, + onPressed: () async { + showMapDialog( + context, + deviceUpdateBloc, + deviceLatitudeController, + deviceLongitudeController); + }, + icon: const Icon( + Icons.map_outlined, + ), + ), + ), + SizedBox( + width: context.lowValue), + ], + ), + ), + Text( + "${appLocalization(context).device_update_province}:", + style: context + .titleMediumTextStyle), + Padding( + padding: + context.paddingLowVertical, + child: StreamBuilder< + List< + DropdownMenuItem< + Province>>>( + stream: deviceUpdateBloc + .streamListProvinces, + builder: (dialogContext, + listProvinces) { + return Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius + .all( + Radius.circular(20), + ), + border: Border.all(), + ), + child: SearchChoices.single( + items: + listProvinces.data ?? + provincesData, + hint: provinceNameSnapshot + .data != + null + ? Text( + provinceNameSnapshot + .data![ + 'name'] ?? + "", + ) + : appLocalization( + context) + .update_device_dialog_location_province_hintText, + searchHint: appLocalization( + context) + .update_device_dialog_location_province_searchHint, + displayClearIcon: false, + onChanged: (value) { + isChangedListener(); + // provinceCode = + // value.code; + selectedProvince = + value.fullName; + provinceData['name'] = + value.fullName; + provinceData['code'] = + value.code; + deviceUpdateBloc + .sinkProvinceData + .add(provinceData); + deviceUpdateBloc + .getAllDistricts( + value.code); + selectedDistrict = ""; + districtData['name'] = + selectedDistrict!; + // deviceUpdateBloc.sinkDistrictName + // .add(selectedDistrict); + deviceUpdateBloc + .sinkDistrictData + .add(districtData); + selectedWard = ""; + wardData['name'] = + selectedWard!; + deviceUpdateBloc + .sinkWardData + .add(wardData); + }, + isExpanded: true, + ), + ); + }, + ), + ), + Text( + "${appLocalization(context).device_update_district}:", + style: context + .titleMediumTextStyle), + Padding( + padding: + context.paddingLowVertical, + child: StreamBuilder< + List< + DropdownMenuItem< + District>>>( + stream: deviceUpdateBloc + .streamListDistricts, + builder: (dialogContext, + listDistricts) { + return Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius + .all( + Radius.circular(20), + ), + border: Border.all(), + ), + child: SearchChoices.single( + items: + listDistricts.data ?? + districtsData, + hint: districtNameSnapshot + .data != + null + ? Text( + districtNameSnapshot + .data![ + 'name'] ?? + selectedDistrict!, + ) + : appLocalization( + context) + .update_device_dialog_location_district_hintText, + searchHint: appLocalization( + context) + .update_device_dialog_location_district_searchHint, + displayClearIcon: false, + onChanged: (value) { + isChangedListener(); + // districtCode = + // value.code; + selectedDistrict = + value.fullName; + districtData['name'] = + value.fullName!; + districtData['code'] = + value.code; + deviceUpdateBloc + .sinkDistrictData + .add(districtData); + deviceUpdateBloc + .getAllWards( + value.code); + selectedWard = ""; + wardData['name'] = + selectedWard!; + deviceUpdateBloc + .sinkWardData + .add(wardData); + }, + isExpanded: true, + ), + ); + }, + ), + ), + Text( + "${appLocalization(context).device_update_ward}:", + style: context + .titleMediumTextStyle), + Padding( + padding: + context.paddingLowVertical, + child: StreamBuilder< + List>>( + stream: deviceUpdateBloc + .streamListWards, + builder: + (dialogContext, listWards) { + return Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius + .all( + Radius.circular( + 20)), + border: Border.all()), + child: SearchChoices.single( + items: listWards.data ?? + wardsData, + hint: wardNameSnapshot + .data != + null + ? Text( + wardNameSnapshot + .data![ + 'name'] ?? + selectedWard!, + ) + : appLocalization( + context) + .update_device_dialog_location_ward_hintText, + searchHint: appLocalization( + context) + .update_device_dialog_location_ward_searchHint, + displayClearIcon: false, + onChanged: (value) { + isChangedListener(); + // wardCode = value.code; + selectedWard = + value.fullName; + wardData['name'] = + value.fullName!; + wardData['code'] = + value.code!; + deviceUpdateBloc + .sinkWardData + .add(wardData); + }, + isExpanded: true, + ), + ); + }, + ), + ), + if (isChangedSnapshot.data == true) + Center( + child: SizedBox( + width: + context.dynamicWidth(0.6), + child: TextButton( + style: ButtonStyle( + foregroundColor: + MaterialStateProperty + .all(Colors + .white), + backgroundColor: + MaterialStateProperty + .all(Colors + .blue)), + onPressed: () async { + String provinceCode = + provinceNameSnapshot + .data![ + "code"] ?? + ""; + String districtCode = + districtNameSnapshot + .data![ + "code"] ?? + ""; + String wardCode = + wardNameSnapshot + .data![ + "code"] ?? + ""; + String latitude = + deviceLatitudeController + .value.text; + String longitude = + deviceLongitudeController + .value.text; + String deviceName = + deviceNameController + .value.text; + // log("ProvinceCode: $provinceCode"); + // log("DistrictCode: $districtCode"); + // log("WardCode: $wardCode"); + // log("Latitude: $latitude"); + // log("Longitude: $longitude"); + // log("Device Name: $deviceName"); + await deviceUpdateBloc + .updateDevice( + context, + deviceInfoSnapshot + .data!.thingId!, + deviceName, + latitude, + longitude, + provinceCode, + districtCode, + wardCode, + ); + Future.delayed( + // ignore: use_build_context_synchronously + context.lowDuration, + () { + Navigator.pop( + context); + }); + }, + child: Text(appLocalization( + context) + .update_button_content)), + ), + ), + ], + ), + ), + ); + }, + ); + } + }, + ), + ); + }); + }); + }), + ); + } + + void isChangedListener() { + isChanged = true; + deviceUpdateBloc.sinkIsChanged.add(isChanged); + } +} diff --git a/lib/feature/devices/device_update/geocode_model.dart b/lib/feature/devices/device_update/geocode_model.dart new file mode 100644 index 0000000..55eae98 --- /dev/null +++ b/lib/feature/devices/device_update/geocode_model.dart @@ -0,0 +1,47 @@ +class Geocode { + List? addressComponents; + String? formattedAddress; + Geocode({ + this.addressComponents, + this.formattedAddress, + }); + + Geocode.fromJson(Map json) { + formattedAddress = json['formatted_address']; + if (json['address_components'] != null) { + addressComponents = []; + json['address_components'].forEach((v) { + addressComponents!.add(AddressComponent.fromJson(v)); + }); + } + } + Map toJson() { + final Map data = {}; + data['formatted_address'] = formattedAddress; + if (addressComponents != null) { + data['address_components'] = + addressComponents!.map((e) => e.toJson()).toList(); + } + return data; + } +} + +class AddressComponent { + String? longName; + String? shortName; + List? types; + AddressComponent({this.longName, this.shortName, this.types}); + + AddressComponent.fromJson(Map json) { + longName = json['long_name']; + shortName = json['short_name']; + types = json['types'].cast(); + } + Map toJson() { + final Map data = {}; + data['long_name'] = longName; + data['short_name'] = shortName; + data['type'] = types; + return data; + } +} diff --git a/lib/feature/devices/device_update/map_dialog.dart b/lib/feature/devices/device_update/map_dialog.dart new file mode 100644 index 0000000..5c31ead --- /dev/null +++ b/lib/feature/devices/device_update/map_dialog.dart @@ -0,0 +1,298 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'package:http/http.dart' as http; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'device_update_bloc.dart'; +import '../../../product/constant/app/app_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/shared/find_location_maps/shared_map_search_location.dart'; + +import '../../../product/shared/find_location_maps/model/prediction_model.dart'; +import '../../../product/shared/shared_transition.dart'; +import 'geocode_model.dart'; + +showMapDialog( + BuildContext context, + DeviceUpdateBloc deviceUpdateBloc, + TextEditingController latitudeController, + TextEditingController longitudeController) async { + const CameraPosition defaultPosition = CameraPosition( + target: LatLng(20.985424, 105.738354), + zoom: 12, + ); + TextEditingController searchLocationController = TextEditingController(); + TextEditingController mapDialogLatitudeController = TextEditingController(); + TextEditingController mapDialogLongitudeController = TextEditingController(); + Completer ggmapController = Completer(); + final streamController = StreamController.broadcast(); + showGeneralDialog( + barrierDismissible: false, + transitionDuration: context.normalDuration, + transitionBuilder: transitionsLeftToRight, + context: context, + pageBuilder: (context, animation, secondaryAnimation) { + return StreamBuilder>( + stream: deviceUpdateBloc.streamMarkers, + builder: (context, markerSnapshot) { + if (!markerSnapshot.hasData) { + if (latitudeController.value.text != "" && + longitudeController.value.text != "") { + double latitude = double.parse(latitudeController.text); + double longitude = double.parse(longitudeController.text); + addMarker( + LatLng(latitude, longitude), + ggmapController, + deviceUpdateBloc, + mapDialogLatitudeController, + mapDialogLongitudeController, + ); + } + } + return Scaffold( + appBar: AppBar( + title: Text(appLocalization(context) + .update_device_dialog_maps_dialog_title), + centerTitle: true, + actions: [ + IconButton( + onPressed: () async { + String latitude = mapDialogLatitudeController.text; + String longitude = mapDialogLongitudeController.text; + log("Finish -- Latitude: $latitude, longitude: $longitude --"); + getDataFromApi(latitude, longitude, deviceUpdateBloc); + latitudeController.text = + mapDialogLatitudeController.text; + longitudeController.text = + mapDialogLongitudeController.text; + bool isChange = true; + deviceUpdateBloc.sinkIsChanged.add(isChange); + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.check)) + ], + ), + body: Stack( + children: [ + GoogleMap( + onTap: (location) async { + addMarker( + location, + ggmapController, + deviceUpdateBloc, + mapDialogLatitudeController, + mapDialogLongitudeController, + ); + }, + markers: markerSnapshot.data ?? {}, + onMapCreated: (GoogleMapController mapController) { + ggmapController.complete(mapController); + streamController.add(mapController); + }, + initialCameraPosition: defaultPosition, + ), + Container( + // color: Colors.white, + height: 80, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(10), + child: StreamBuilder( + stream: deviceUpdateBloc.streamSearchLocation, + builder: (context, searchLocation) { + return NearBySearchSFM( + textInputAction: TextInputAction.done, + textEditingController: + searchLocation.data ?? searchLocationController, + googleAPIKey: ApplicationConstants.MAP_KEY, + locationLatitude: 20.985424, + locationLongitude: 105.738354, + radius: 50000, + inputDecoration: InputDecoration( + hintText: appLocalization(context) + .update_device_dialog_search_location_hint, + border: InputBorder.none), + debounceTime: 600, + isLatLngRequired: true, + getPlaceDetailWithLatLng: (Prediction prediction) { + FocusScope.of(context).unfocus(); + addMarker( + LatLng(double.parse(prediction.lat!), + double.parse(prediction.lng!)), + ggmapController, + deviceUpdateBloc, + mapDialogLatitudeController, + mapDialogLongitudeController); + }, + itemClick: (Prediction prediction) { + searchLocationController.text = + prediction.structuredFormatting!.mainText!; + deviceUpdateBloc.sinkSearchLocation + .add(searchLocationController); + searchLocationController.selection = + TextSelection.fromPosition(TextPosition( + offset: prediction.structuredFormatting! + .mainText!.length)); + }, + boxDecoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey, + width: 0.5, + ), + borderRadius: const BorderRadius.all( + Radius.circular(20))), + ); + }), + ) + ], + ), + ); + }); + }, + ); +} + +addMarker( + LatLng position, + Completer mapController, + DeviceUpdateBloc deviceUpdateBloc, + TextEditingController mapDialogLatitudeController, + TextEditingController mapDialogLongitudeController, +) async { + log("AddMarker -- Latitude: ${position.latitude}, longitude: ${position.longitude} --"); + + Set marker = {}; + deviceUpdateBloc.sinkMarkers.add(marker); + Marker newMarker = Marker( + markerId: const MarkerId('value'), + position: LatLng(position.latitude, position.longitude), + icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen), + draggable: true, + onDragEnd: (position) { + mapDialogLatitudeController.text = position.latitude.toString(); + mapDialogLongitudeController.text = position.longitude.toString(); + }, + ); + marker.add(newMarker); + deviceUpdateBloc.sinkMarkers.add(marker); + mapDialogLatitudeController.text = position.latitude.toString(); + mapDialogLongitudeController.text = position.longitude.toString(); + updateCameraPosition(position, 14, mapController); +} + +void getDataFromApi(String latitude, String longitude, + DeviceUpdateBloc deviceUpdateBloc) async { + String path = + "maps/api/geocode/json?latlng=$latitude,$longitude&language=vi&result_type=political&key=${ApplicationConstants.MAP_KEY}"; + var url = Uri.parse('https://maps.googleapis.com/$path'); + + final response = await http.get(url); + + if (response.statusCode != 200) { + log("Loi: ${response.statusCode}"); + return; + } + + Map data = jsonDecode(response.body); + if (!data.containsKey('results') || data['results'].isEmpty) { + log("Khong co result"); + return; + } + + List results = data['results']; + List geocodes = + results.map((result) => Geocode.fromJson(result)).toList(); + + Map locations = + _extractLocationComponents(geocodes[0].addressComponents!); + + // In ra thông tin của các location + locations.forEach((key, value) { + log("$key: $value"); + }); + + await _processLocations(locations, deviceUpdateBloc); +} + +Map _extractLocationComponents( + List addressComponents) { + Map locations = {}; + + for (var addressComponent in addressComponents) { + String longName = addressComponent.longName ?? ""; + if (addressComponent.types!.contains('administrative_area_level_3') || + addressComponent.types!.contains('sublocality_level_1')) { + locations['wardkey'] = longName; + } else if (addressComponent.types! + .contains('administrative_area_level_2') || + addressComponent.types!.contains('sublocality_level_2') || + addressComponent.types!.contains('locality')) { + locations['districtkey'] = longName; + } else if (addressComponent.types! + .contains('administrative_area_level_1')) { + locations['provincekey'] = longName; + } + } + + return locations; +} + +Future _processLocations( + Map locations, DeviceUpdateBloc deviceUpdateBloc) async { + String provinceNameFromAPI = locations['provincekey'] ?? ""; + String districtNameFromAPI = locations['districtkey'] ?? ""; + String wardNameFromAPI = locations['wardkey'] ?? ""; + + final province = + await deviceUpdateBloc.getProvinceByName(provinceNameFromAPI); + if (province.name != "null") { + log("Province: ${province.fullName}, ProvinceCode: ${province.code}"); + deviceUpdateBloc.sinkProvinceData + .add({"code": province.code!, "name": province.fullName!}); + deviceUpdateBloc.getAllProvinces(); + + final district = await deviceUpdateBloc.getDistrictByName( + districtNameFromAPI, province.code!); + log("Districtname: ${district.fullName}, districtCode: ${district.code}"); + deviceUpdateBloc.getAllDistricts(province.code!); + if (district.name != "null") { + deviceUpdateBloc.sinkDistrictData + .add({"code": district.code!, "name": district.fullName!}); + final ward = + await deviceUpdateBloc.getWardByName(wardNameFromAPI, district.code!); + log("Wardname: ${ward.fullName}, WardCode: ${ward.code}"); + deviceUpdateBloc.getAllWards(district.code!); + if (ward.name != "null") { + log("Xac dinh duoc het thong tin tu toa do"); + deviceUpdateBloc.sinkWardData + .add({"code": ward.code!, "name": ward.fullName!}); + } else { + deviceUpdateBloc.sinkWardData.add({}); + } + } else { + deviceUpdateBloc.sinkDistrictData.add({}); + } + } else { + deviceUpdateBloc.sinkProvinceData.add({}); + } +} + +Future updateCameraPosition(LatLng location, double zoom, + Completer mapController) async { + final CameraPosition cameraPosition = CameraPosition( + target: LatLng( + location.latitude, + location.longitude, + ), + zoom: zoom, + ); + final GoogleMapController mapControllerNew = await mapController.future; + mapControllerNew.animateCamera( + CameraUpdate.newCameraPosition( + cameraPosition, + ), + ); +} diff --git a/lib/feature/devices/devices_manager_bloc.dart b/lib/feature/devices/devices_manager_bloc.dart new file mode 100644 index 0000000..f265938 --- /dev/null +++ b/lib/feature/devices/devices_manager_bloc.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'device_model.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/services/api_services.dart'; + +import '../../product/utils/device_utils.dart'; + +class DevicesManagerBloc extends BlocBase { + APIServices apiServices = APIServices(); + + final userRole = StreamController.broadcast(); + StreamSink get sinkUserRole => userRole.sink; + Stream get streamUserRole => userRole.stream; + + final allDevices = StreamController>.broadcast(); + StreamSink> get sinkAllDevices => allDevices.sink; + Stream> get streamAllDevices => allDevices.stream; + + @override + void dispose() {} + + void getDevice() async { + String body = await apiServices.getOwnerDevices(); + if (body != "") { + final data = jsonDecode(body); + List items = data['items']; + List originalDevices = Device.fromJsonDynamicList(items); + List devices = + DeviceUtils.instance.sortDeviceByState(originalDevices); + sinkAllDevices.add(devices); + } + } +} diff --git a/lib/feature/devices/devices_manager_screen.dart b/lib/feature/devices/devices_manager_screen.dart new file mode 100644 index 0000000..bc76489 --- /dev/null +++ b/lib/feature/devices/devices_manager_screen.dart @@ -0,0 +1,274 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'add_new_device_widget.dart'; +import 'delete_device_widget.dart'; +import 'device_model.dart'; +import 'devices_manager_bloc.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/constant/enums/app_route_enums.dart'; +import '../../product/constant/enums/role_enums.dart'; +import '../../product/constant/icon/icon_constants.dart'; +import '../../product/extention/context_extention.dart'; +import '../../product/services/api_services.dart'; +import '../../product/services/language_services.dart'; +import '../../product/utils/device_utils.dart'; + +class DevicesManagerScreen extends StatefulWidget { + const DevicesManagerScreen({super.key}); + + @override + State createState() => _DevicesManagerScreenState(); +} + +class _DevicesManagerScreenState extends State { + late DevicesManagerBloc devicesManagerBloc; + String role = "Undefine"; + APIServices apiServices = APIServices(); + List devices = []; + Timer? getAllDevicesTimer; + + @override + void initState() { + super.initState(); + devicesManagerBloc = BlocProvider.of(context); + getUserRole(); + // devicesManagerBloc.getDevice(); + // getAllOwnerDevices(); + // const duration = Duration(seconds: 10); + // getAllDevicesTimer = + // Timer.periodic(duration, (Timer t) => devicesManagerBloc.getDevice()); + } + + @override + void dispose() { + getAllDevicesTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: StreamBuilder>( + stream: devicesManagerBloc.streamAllDevices, + initialData: devices, + builder: (context, allDeviceSnapshot) { + if (allDeviceSnapshot.data?.isEmpty ?? devices.isEmpty) { + devicesManagerBloc.getDevice(); + return const Center(child: CircularProgressIndicator()); + } else { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + StreamBuilder( + stream: devicesManagerBloc.streamUserRole, + initialData: role, + builder: (context, roleSnapshot) { + return PaginatedDataTable( + header: Center( + child: Text( + appLocalization(context) + .paginated_data_table_title, + style: context.titleLargeTextStyle, + ), + ), + columns: [ + if (roleSnapshot.data == RoleEnums.ADMIN.name || + roleSnapshot.data == RoleEnums.USER.name) + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_action))), + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_deviceName))), + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_deviceStatus))), + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_deviceBaterry))), + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_deviceSignal))), + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_deviceTemperature))), + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_deviceHump))), + DataColumn( + label: Center( + child: Text(appLocalization(context) + .paginated_data_table_column_devicePower))), + ], + onPageChanged: (int pageIndex) { + // log('Chuyen page: $pageIndex'); + }, + rowsPerPage: 5, + actions: [ + if (roleSnapshot.data == RoleEnums.USER.name || + roleSnapshot.data == RoleEnums.ADMIN.name) + IconButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all( + Colors.green), + iconColor: + MaterialStateProperty.all( + Colors.white)), + onPressed: () { + ScaffoldMessenger.of(context) + .clearSnackBars(); + addNewDevice( + context, roleSnapshot.data ?? role); + }, + icon: IconConstants.instance + .getMaterialIcon(Icons.add)) + ], + source: DeviceSource( + devices: allDeviceSnapshot.data ?? devices, + context: context, + devicesBloc: devicesManagerBloc, + role: role)); + }) + ], + ), + ); + } + }), + ); + } + + void getUserRole() async { + role = await apiServices.getUserRole(); + devicesManagerBloc.sinkUserRole.add(role); + } +} + +class DeviceSource extends DataTableSource { + String role; + APIServices apiServices = APIServices(); + List devices; + final DevicesManagerBloc devicesBloc; + final BuildContext context; + DeviceSource( + {required this.devices, + required this.context, + required this.devicesBloc, + required this.role}); + @override + DataRow? getRow(int index) { + if (index >= devices.length) { + return null; + } + final device = devices[index]; + Map sensorMap = DeviceUtils.instance + .getDeviceSensors(context, device.status?.sensors ?? []); + String deviceState = + DeviceUtils.instance.checkStateDevice(context, device.state!); + return DataRow.byIndex( + // color: getTableRowColor(device.state!), + index: index, + cells: [ + if (role == RoleEnums.USER.name || role == RoleEnums.ADMIN.name) + DataCell( + Center( + child: Row( + children: [ + IconButton( + // style: ButtonStyle(), + hoverColor: Colors.black, + onPressed: () { + context.pushNamed(AppRoutes.DEVICE_UPDATE.name, + pathParameters: {'thingID': device.thingId!}); + }, + icon: const Icon(Icons.build, color: Colors.blue)), + IconButton( + onPressed: () async { + handleDeleteDevice(context, device.thingId!, role); + }, + icon: const Icon(Icons.delete, color: Colors.red)), + ], + ), + ), + ), + DataCell( + Text(device.name!, + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor(device.state!))), onTap: () { + // log(device.thingId.toString()); + context.pushNamed(AppRoutes.DEVICE_DETAIL.name, + pathParameters: {'thingID': device.thingId!}); + }), + DataCell( + Center( + child: Text(deviceState, + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor(device.state!)))), onTap: () { + // log(device.thingId.toString()); + context.pushNamed(AppRoutes.DEVICE_DETAIL.name, + pathParameters: {'thingID': device.thingId!}); + }), + DataCell( + Center( + child: Center( + child: Text(sensorMap['sensorBattery'] + "%", + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor(device.state!))))), + onTap: () => context.pushNamed(AppRoutes.DEVICE_DETAIL.name, + pathParameters: {'thingID': device.thingId!}), + ), + DataCell( + Center( + child: Center( + child: Text(sensorMap['sensorCsq'], + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor(device.state!))))), + ), + DataCell( + Center( + child: Text(sensorMap['sensorTemp'], + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor(device.state!)))), + ), + DataCell( + Center( + child: Text(sensorMap['sensorHum'], + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor(device.state!)))), + ), + DataCell( + Center( + child: Text(sensorMap['sensorVolt'], + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor(device.state!)))), + ), + ], + ); + } + + @override + int get rowCount => devices.length; + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => 0; +} diff --git a/lib/feature/error/not_found_screen.dart b/lib/feature/error/not_found_screen.dart new file mode 100644 index 0000000..e62ee83 --- /dev/null +++ b/lib/feature/error/not_found_screen.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sfm_app/product/constant/enums/app_route_enums.dart'; +import 'package:sfm_app/product/constant/image/image_constants.dart'; + +class NotFoundScreen extends StatelessWidget { + const NotFoundScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + ImageConstants.instance.getImage('error_page'), + fit: BoxFit.cover, + ), + Positioned( + bottom: MediaQuery.of(context).size.height * 0.15, + left: MediaQuery.of(context).size.width * 0.3, + right: MediaQuery.of(context).size.width * 0.3, + child: TextButton( + onPressed: () { + context.goNamed(AppRoutes.LOGIN.name); + }, + child: Text( + "Go Home".toUpperCase(), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/feature/home/device_alias_model.dart b/lib/feature/home/device_alias_model.dart new file mode 100644 index 0000000..e2ca4b7 --- /dev/null +++ b/lib/feature/home/device_alias_model.dart @@ -0,0 +1,79 @@ + +import '../devices/device_model.dart'; + +class DeviceWithAlias { + String? extId; + String? thingId; + String? thingKey; + List? channels; + String? areaPath; + String? fvers; + String? name; + HardwareInfo? hardwareInfo; + Settings? settings; + Status? status; + DateTime? connectionTime; + int? state; + String? visibility; + DateTime? createdAt; + DateTime? updatedAt; + bool? isOwner; + String? ownerId; + String? ownerName; + String? alias; + + DeviceWithAlias({ + this.extId, + this.thingId, + this.thingKey, + this.channels, + this.areaPath, + this.fvers, + this.name, + this.hardwareInfo, + this.settings, + this.status, + this.connectionTime, + this.state, + this.visibility, + this.createdAt, + this.updatedAt, + this.isOwner, + this.ownerId, + this.ownerName, + this.alias, + }); + + DeviceWithAlias.fromJson(Map json) { + extId = json['ext_id']; + thingId = json['thing_id']; + thingKey = json['thing_key']; + if (json['channels'] != null) { + channels = []; + json['channels'].forEach((v) { + channels!.add(v); + }); + } + areaPath = json['area_path']; + fvers = json['fvers']; + name = json['name']; + hardwareInfo = json['hardware_info'] != null + ? HardwareInfo.fromJson(json['hardware_info']) + : null; + settings = + json['settings'] != null ? Settings.fromJson(json['settings']) : null; + status = json['status'] != null ? Status.fromJson(json['status']) : null; + connectionTime = DateTime.parse(json['connection_time']); + state = json['state']; + visibility = json['visibility']; + createdAt = DateTime.parse(json['created_at']); + updatedAt = DateTime.parse(json['updated_at']); + isOwner = json['is_owner']; + ownerId = json['owner_id']; + ownerName = json['owner_name']; + alias = json['alias']; + } + static List fromJsonDynamicList(List list) { + return list.map((e) => DeviceWithAlias.fromJson(e)).toList(); + } +} diff --git a/lib/feature/home/home_bloc.dart b/lib/feature/home/home_bloc.dart new file mode 100644 index 0000000..e4f7f21 --- /dev/null +++ b/lib/feature/home/home_bloc.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'device_alias_model.dart'; + +import '../../product/base/bloc/base_bloc.dart'; + +class HomeBloc extends BlocBase { + + final allDevicesAlias = StreamController>.broadcast(); + StreamSink> get sinkAllDevicesAlias => + allDevicesAlias.sink; + Stream> get streamAllDevicesAlias => + allDevicesAlias.stream; + + final onlineDevicesAlias = + StreamController>.broadcast(); + StreamSink> get sinkOnlineDevicesAlias => + onlineDevicesAlias.sink; + Stream> get streamOnlineDevicesAlias => + onlineDevicesAlias.stream; + + final offlineDevicesAlias = + StreamController>.broadcast(); + StreamSink> get sinkOfflineDevicesAlias => + offlineDevicesAlias.sink; + Stream> get streamOfflineDevicesAlias => + offlineDevicesAlias.stream; + + final warningDevicesAlias = + StreamController>.broadcast(); + StreamSink> get sinkWarningDevicesAlias => + warningDevicesAlias.sink; + Stream> get streamWarningDevicesAlias => + warningDevicesAlias.stream; + + final notUseDevicesAlias = + StreamController>.broadcast(); + StreamSink> get sinkNotUseDevicesAlias => + notUseDevicesAlias.sink; + Stream> get streamNotUseDevicesAlias => + notUseDevicesAlias.stream; + + final allDevicesAliasJoined = + StreamController>.broadcast(); + StreamSink> get sinkAllDevicesAliasJoined => + allDevicesAliasJoined.sink; + Stream> get streamAllDevicesAliasJoined => + allDevicesAliasJoined.stream; + + final onlineDevicesAliasJoined = + StreamController>.broadcast(); + StreamSink> get sinkOnlineDevicesAliasJoined => + onlineDevicesAliasJoined.sink; + Stream> get streamOnlineDevicesAliasJoined => + onlineDevicesAliasJoined.stream; + + final offlineDevicesAliasJoined = + StreamController>.broadcast(); + StreamSink> get sinkOfflineDevicesAliasJoined => + offlineDevicesAliasJoined.sink; + Stream> get streamOfflineDevicesAliasJoined => + offlineDevicesAliasJoined.stream; + + final warningDevicesAliasJoined = + StreamController>.broadcast(); + StreamSink> get sinkWarningDevicesAliasJoined => + warningDevicesAliasJoined.sink; + Stream> get streamWarningDevicesAliasJoined => + warningDevicesAliasJoined.stream; + + final notUseDevicesAliasJoined = + StreamController>.broadcast(); + StreamSink> get sinkNotUseDevicesAliasJoined => + notUseDevicesAliasJoined.sink; + Stream> get streamNotUseDevicesAliasJoined => + notUseDevicesAliasJoined.stream; + + final countNotitication = StreamController.broadcast(); + StreamSink get sinkCountNotitication => countNotitication.sink; + Stream get streamCountNotitication => countNotitication.stream; + + final ownerDevicesStatus = + StreamController>>.broadcast(); + StreamSink>> + get sinkOwnerDevicesStatus => ownerDevicesStatus.sink; + Stream>> get streamOwnerDevicesStatus => + ownerDevicesStatus.stream; + + + @override + void dispose() {} +} diff --git a/lib/feature/home/home_screen.dart b/lib/feature/home/home_screen.dart new file mode 100644 index 0000000..38d8aae --- /dev/null +++ b/lib/feature/home/home_screen.dart @@ -0,0 +1,411 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'shared/alert_card.dart'; +import 'shared/warning_card.dart'; +import '../../product/utils/device_utils.dart'; +import 'device_alias_model.dart'; +import 'shared/overview_card.dart'; +import '../settings/device_notification_settings/device_notification_settings_model.dart'; +import '../../product/extention/context_extention.dart'; +import '../../product/services/api_services.dart'; +import '../../product/services/language_services.dart'; +import 'home_bloc.dart'; +import '../../product/base/bloc/base_bloc.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + late HomeBloc homeBloc; + APIServices apiServices = APIServices(); + List devices = []; + List allDevicesAlias = []; + List onlineDevicesAlias = []; + List offlineDevicesAlias = []; + List warningDevicesAlias = []; + List notUseDevicesAlias = []; + + List allDevicesAliasJoined = []; + List onlineDevicesAliasJoined = []; + List offlineDevicesAliasJoined = []; + List warningDevicesAliasJoined = []; + List notUseDevicesAliasJoined = []; + bool isFunctionCall = false; + Timer? getAllDevicesTimer; + int notificationCount = 0; + Map> ownerDevicesStatus = {}; + List ownerDevicesState = []; + + @override + void initState() { + super.initState(); + homeBloc = BlocProvider.of(context); + const duration = Duration(seconds: 20); + getOwnerAndJoinedDevices(); + getAllDevicesTimer = + Timer.periodic(duration, (Timer t) => getOwnerAndJoinedDevices()); + } + + @override + void dispose() { + getAllDevicesTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: context.paddingLow, + child: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + Text( + appLocalization(context).notification, + style: context.titleMediumTextStyle, + ), + SizedBox(width: context.lowValue), + StreamBuilder( + stream: homeBloc.streamCountNotitication, + builder: (context, countSnapshot) { + return Text( + "(${countSnapshot.data ?? 0})", + style: context.titleMediumTextStyle, + ); + }, + ) + ], + ), + SizedBox( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: StreamBuilder>>( + stream: homeBloc.streamOwnerDevicesStatus, + builder: (context, snapshot) { + if (snapshot.data?['state'] != null || + snapshot.data?['battery'] != null) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (snapshot.data?['state'] != null) + ...snapshot.data!['state']! + .map( + (item) => FutureBuilder( + future: warningCard( + context, apiServices, item), + builder: (context, warningCardSnapshot) { + if (warningCardSnapshot.hasData) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400, + maxHeight: 260, + ), + child: warningCardSnapshot.data!, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ) + .toList(), + if (snapshot.data?['battery'] != null) + ...snapshot.data!['battery']! + .map( + (batteryItem) => FutureBuilder( + future: notificationCard( + context, + "lowBattery", + "Cảnh báo pin yếu", + batteryItem.name!, + batteryItem.areaPath!, + ), + builder: (context, warningCardSnapshot) { + if (warningCardSnapshot.hasData) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400, + maxHeight: 260, + ), + child: warningCardSnapshot.data!, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ) + .toList(), + ]); + } else { + return Padding( + padding: context.paddingMedium, + child: Center( + child: Row( + children: [ + const Icon( + Icons.check_circle_outline_rounded, + size: 40, + color: Colors.green, + ), + SizedBox(width: context.lowValue), + Text( + appLocalization(context) + .notification_description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + textAlign: TextAlign.start, + ), + ], + ), + ), + ); + } + }, + ), + ), + ), + StreamBuilder>( + stream: homeBloc.streamAllDevicesAlias, + initialData: allDevicesAlias, + builder: (context, allDeviceSnapshot) { + return StreamBuilder>( + stream: homeBloc.streamOnlineDevicesAlias, + initialData: onlineDevicesAlias, + builder: (context, onlineDeviceSnapshot) { + return StreamBuilder>( + stream: homeBloc.streamOfflineDevicesAlias, + initialData: offlineDevicesAlias, + builder: (context, deviceDashboardSnapshot) { + return StreamBuilder>( + stream: homeBloc.streamWarningDevicesAlias, + initialData: warningDevicesAlias, + builder: (context, warningDeviceSnapshot) { + return StreamBuilder>( + stream: homeBloc.streamNotUseDevicesAlias, + initialData: notUseDevicesAlias, + builder: (context, notUseSnapshot) { + return OverviewCard( + isOwner: true, + total: allDeviceSnapshot.data!.length, + active: onlineDeviceSnapshot.data!.length, + inactive: + deviceDashboardSnapshot.data!.length, + warning: warningDeviceSnapshot.data!.length, + unused: notUseSnapshot.data!.length, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ), + SizedBox(height: context.lowValue), + StreamBuilder>( + stream: homeBloc.streamAllDevicesAliasJoined, + initialData: allDevicesAliasJoined, + builder: (context, allDeviceSnapshot) { + if (allDeviceSnapshot.data?.isEmpty ?? true) { + return const SizedBox.shrink(); + } + return StreamBuilder>( + stream: homeBloc.streamOnlineDevicesAliasJoined, + initialData: onlineDevicesAliasJoined, + builder: (context, onlineDeviceSnapshot) { + return StreamBuilder>( + stream: homeBloc.streamOfflineDevicesAliasJoined, + initialData: offlineDevicesAliasJoined, + builder: (context, deviceDashboardSnapshot) { + return StreamBuilder>( + stream: homeBloc.streamWarningDevicesAliasJoined, + initialData: warningDevicesAliasJoined, + builder: (context, warningDeviceSnapshot) { + return StreamBuilder>( + stream: homeBloc.streamNotUseDevicesAliasJoined, + initialData: notUseDevicesAliasJoined, + builder: (context, notUseSnapshot) { + return OverviewCard( + isOwner: false, + total: allDeviceSnapshot.data!.length, + active: onlineDeviceSnapshot.data!.length, + inactive: + deviceDashboardSnapshot.data!.length, + warning: warningDeviceSnapshot.data!.length, + unused: notUseSnapshot.data!.length, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ), + ], + ), + ), + ); + } + + void getOwnerAndJoinedDevices() async { + String response = await apiServices.getDashBoardDevices(); + final data = jsonDecode(response); + List result = data["items"]; + devices = DeviceWithAlias.fromJsonDynamicList(result); + getOwnerDeviceState(devices); + getDevicesStatusAlias(devices); + checkSettingdevice(devices); + } + + void getOwnerDeviceState(List allDevices) async { + List ownerDevices = []; + for (var device in allDevices) { + if (device.isOwner!) { + ownerDevices.add(device); + } + } + if (ownerDevicesState.isEmpty || + ownerDevicesState.length < devices.length) { + ownerDevicesState.clear(); + ownerDevicesStatus.clear(); + homeBloc.sinkOwnerDevicesStatus.add(ownerDevicesStatus); + int count = 0; + for (var device in ownerDevices) { + Map sensorMap = DeviceUtils.instance + .getDeviceSensors(context, device.status?.sensors ?? []); + if (device.state == 1 || device.state == 3) { + ownerDevicesStatus["state"] ??= []; + ownerDevicesStatus["state"]!.add(device); + homeBloc.sinkOwnerDevicesStatus.add(ownerDevicesStatus); + count++; + } + if (sensorMap['sensorBattery'] != + appLocalization(context).no_data_message) { + if (double.parse(sensorMap['sensorBattery']) <= 20) { + ownerDevicesStatus['battery'] ??= []; + ownerDevicesStatus['battery']!.add(device); + homeBloc.sinkOwnerDevicesStatus.add(ownerDevicesStatus); + count++; + } + } + notificationCount = count; + homeBloc.sinkCountNotitication.add(notificationCount); + } + } + } + + void getDevicesStatusAlias(List devices) async { + clearAllDeviceStatusAlias(); + for (DeviceWithAlias device in devices) { + if (device.isOwner == true) { + allDevicesAlias.add(device); + if (device.state! == 0 || device.state! == 1) { + onlineDevicesAlias.add(device); + homeBloc.sinkOnlineDevicesAlias.add(onlineDevicesAlias); + } + if (device.state! == -1) { + offlineDevicesAlias.add(device); + homeBloc.sinkOfflineDevicesAlias.add(offlineDevicesAlias); + } + if (device.state! == 1) { + warningDevicesAlias.add(device); + homeBloc.sinkWarningDevicesAlias.add(warningDevicesAlias); + } + if (device.state! == -2) { + notUseDevicesAlias.add(device); + homeBloc.sinkNotUseDevicesAlias.add(notUseDevicesAlias); + } + } else { + allDevicesAliasJoined.add(device); + if (device.state! == 0 || device.state! == 1) { + onlineDevicesAliasJoined.add(device); + homeBloc.sinkOnlineDevicesAliasJoined.add(onlineDevicesAliasJoined); + } + if (device.state! == -1) { + offlineDevicesAliasJoined.add(device); + homeBloc.sinkOfflineDevicesAliasJoined.add(offlineDevicesAliasJoined); + } + if (device.state! == 1) { + warningDevicesAliasJoined.add(device); + homeBloc.sinkWarningDevicesAliasJoined.add(warningDevicesAliasJoined); + } + if (device.state! == -2) { + notUseDevicesAliasJoined.add(device); + homeBloc.sinkNotUseDevicesAliasJoined.add(notUseDevicesAliasJoined); + } + } + } + checkSettingdevice(allDevicesAliasJoined); + homeBloc.sinkAllDevicesAlias.add(allDevicesAlias); + homeBloc.sinkAllDevicesAliasJoined.add(allDevicesAliasJoined); + } + + void checkSettingdevice(List devices) async { + if (isFunctionCall) { + } else { + String? response = + await apiServices.getAllSettingsNotificationOfDevices(); + if (response != "") { + final data = jsonDecode(response); + final result = data['data']; + // log("Data ${DeviceNotificationSettings.mapFromJson(jsonDecode(data)).values.toList()}"); + List list = + DeviceNotificationSettings.mapFromJson(result).values.toList(); + // log("List: $list"); + Set thingIdsInList = + list.map((device) => device.thingId!).toSet(); + for (var device in devices) { + if (!thingIdsInList.contains(device.thingId)) { + log("Device with Thing ID ${device.thingId} is not in the notification settings list."); + await apiServices.setupDeviceNotification( + device.thingId!, device.name!); + } else { + log("All devives are in the notification settings list."); + } + } + } else { + log("apiServices: getAllSettingsNotificationofDevices error!"); + } + } + isFunctionCall = true; + } + + void clearAllDeviceStatusAlias() { + allDevicesAlias.clear(); + homeBloc.sinkAllDevicesAlias.add(allDevicesAlias); + onlineDevicesAlias.clear(); + homeBloc.sinkOnlineDevicesAlias.add(onlineDevicesAlias); + offlineDevicesAlias.clear(); + homeBloc.sinkOfflineDevicesAlias.add(offlineDevicesAlias); + warningDevicesAlias.clear(); + homeBloc.sinkWarningDevicesAlias.add(warningDevicesAlias); + notUseDevicesAlias.clear(); + homeBloc.sinkNotUseDevicesAlias.add(notUseDevicesAlias); + allDevicesAliasJoined.clear(); + homeBloc.sinkAllDevicesAliasJoined.add(allDevicesAliasJoined); + onlineDevicesAliasJoined.clear(); + homeBloc.sinkOnlineDevicesAliasJoined.add(onlineDevicesAliasJoined); + offlineDevicesAliasJoined.clear(); + homeBloc.sinkOfflineDevicesAliasJoined.add(offlineDevicesAliasJoined); + warningDevicesAliasJoined.clear(); + homeBloc.sinkWarningDevicesAliasJoined.add(warningDevicesAliasJoined); + notUseDevicesAliasJoined.clear(); + homeBloc.sinkNotUseDevicesAliasJoined.add(notUseDevicesAliasJoined); + } +} diff --git a/lib/feature/home/shared/alert_card.dart b/lib/feature/home/shared/alert_card.dart new file mode 100644 index 0000000..f4f7cbf --- /dev/null +++ b/lib/feature/home/shared/alert_card.dart @@ -0,0 +1,130 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import '../../../product/constant/image/image_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/utils/device_utils.dart'; + +import '../../../product/constant/icon/icon_constants.dart'; + +Future notificationCard( + BuildContext context, + String notiticationType, + String notificationTitle, + String notificationDevicename, + String notificationLocation) async { + String location = await DeviceUtils.instance + .getFullDeviceLocation(context, notificationLocation); + String path = ""; + DateTime time = DateTime.now(); + if (notiticationType == "lowBattery") { + path = ImageConstants.instance.getImage("low_battery"); + } + return Card( + child: Padding( + padding: context.paddingLow, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: Text( + notificationTitle, + style: const TextStyle( + letterSpacing: 1, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 250, 84, 34), + fontSize: 18, + ), + ), + ), + SizedBox(height: context.lowValue), + SizedBox( + child: Text( + "${appLocalization(context).device_title} $notificationDevicename", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + textAlign: TextAlign.start, + ), + ), + ], + ), + SizedBox( + height: context.dynamicWidth(0.15), + width: context.dynamicWidth(0.15), + child: Image.asset(path), + ), + ], + ), + SizedBox(height: context.lowValue), + Row( + children: [ + IconConstants.instance + .getMaterialIcon(Icons.location_on_outlined), + SizedBox( + width: context.lowValue, + ), + Expanded( + child: Text( + location, + style: const TextStyle(fontSize: 15), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + textAlign: TextAlign.start, + ), + ), + ], + ), + SizedBox(height: context.lowValue), + Row( + children: [ + IconConstants.instance.getMaterialIcon(Icons.schedule), + SizedBox( + width: context.lowValue, + ), + Expanded( + child: Text( + time.toString(), + style: const TextStyle(fontSize: 15), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + textAlign: TextAlign.start, + ), + ), + ], + ), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton( + style: const ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.blueAccent)), + onPressed: () {}, + child: Text( + appLocalization(context).detail_message, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ); +} diff --git a/lib/feature/home/shared/overview_card.dart b/lib/feature/home/shared/overview_card.dart new file mode 100644 index 0000000..dfa68ad --- /dev/null +++ b/lib/feature/home/shared/overview_card.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:sfm_app/feature/home/shared/status_card.dart'; +import 'package:sfm_app/product/extention/context_extention.dart'; +import 'package:sfm_app/product/services/language_services.dart'; + +class OverviewCard extends StatelessWidget { + final bool isOwner; + final int total; + final int active; + final int inactive; + final int warning; + final int unused; + + const OverviewCard( + {super.key, + required this.isOwner, + required this.total, + required this.active, + required this.inactive, + required this.warning, + required this.unused}); + + @override + Widget build(BuildContext context) { + return Card( + margin: context.paddingLow, + elevation: 8, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Padding( + padding: context.paddingNormal, + child: Column( + children: [ + Text( + isOwner + ? appLocalization(context).overview_message + : appLocalization(context).interfamily_page_name, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: context.normalValue), + Column( + children: [ + StatusCard( + label: appLocalization(context).total_nof_devices_message, + count: total, + color: Colors.blue, + ), + StatusCard( + label: appLocalization(context).active_devices_message, + count: active, + color: Colors.green, + ), + StatusCard( + label: appLocalization(context).inactive_devices_message, + count: inactive, + color: Colors.grey, + ), + StatusCard( + label: appLocalization(context).warning_devices_message, + count: warning, + color: Colors.orange, + ), + StatusCard( + label: appLocalization(context).unused_devices_message, + count: unused, + color: Colors.yellow, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/feature/home/shared/status_card.dart b/lib/feature/home/shared/status_card.dart new file mode 100644 index 0000000..54ab04e --- /dev/null +++ b/lib/feature/home/shared/status_card.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class StatusCard extends StatelessWidget { + final String label; + final int count; + final Color color; + + const StatusCard( + {super.key, + required this.label, + required this.count, + required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: color.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: color, + width: 1, + ), + ), + padding: const EdgeInsets.all(15), + margin: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 18)), + Text( + count.toString(), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} diff --git a/lib/feature/home/shared/warning_card.dart b/lib/feature/home/shared/warning_card.dart new file mode 100644 index 0000000..4fff4ae --- /dev/null +++ b/lib/feature/home/shared/warning_card.dart @@ -0,0 +1,248 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import '../device_alias_model.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/constant/image/image_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/api_services.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/utils/device_utils.dart'; +import '../../../product/shared/shared_snack_bar.dart'; + +Future warningCard( + BuildContext context, APIServices apiServices, DeviceWithAlias item) async { + Color backgroundColor = Colors.blue; + Color textColor = Colors.white; + String message = ""; + String fullLocation = + await DeviceUtils.instance.getFullDeviceLocation(context, item.areaPath!); + String time = ""; + for (var sensor in item.status!.sensors!) { + if (sensor.name! == "11") { + DateTime dateTime = + DateTime.fromMillisecondsSinceEpoch((sensor.time!) * 1000); + time = DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime); + } + } + if (item.state! == 3) { + backgroundColor = Colors.grey; + textColor = Colors.black; + message = appLocalization(context).in_progress_message; + } else if (item.state! == 2) { + backgroundColor = const Color.fromARGB(255, 6, 138, 72); + textColor = const Color.fromARGB(255, 255, 255, 255); + message = appLocalization(context).gf_in_firefighting_message; + } else if (item.state! == 1) { + backgroundColor = const Color.fromARGB(255, 250, 63, 63); + textColor = Colors.white; + message = appLocalization(context).button_fake_fire_message; + } else { + backgroundColor = Colors.black; + textColor = Colors.white; + message = appLocalization(context).disconnect_message_uppercase; + } + return Card( + // color: Color.fromARGB(255, 208, 212, 217), + child: Padding( + padding: context.paddingLow, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: Text( + appLocalization(context).smoke_detecting_message, + style: const TextStyle( + letterSpacing: 1, + fontWeight: FontWeight.bold, + color: Colors.red, + fontSize: 18, + ), + ), + ), + SizedBox(height: context.lowValue), + SizedBox( + child: Text( + "${appLocalization(context).device_title}: ${item.name}", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + textAlign: TextAlign.start, + ), + ), + ], + ), + SizedBox( + height: context.dynamicWidth(0.15), + width: context.dynamicWidth(0.15), + child: Image.asset( + ImageConstants.instance.getImage("fire_warning")), + ) + ], + ), + SizedBox(height: context.lowValue), + Row( + children: [ + IconConstants.instance + .getMaterialIcon(Icons.location_on_outlined), + SizedBox( + width: context.lowValue, + ), + Expanded( + child: Text( + fullLocation, + style: const TextStyle(fontSize: 15), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + textAlign: TextAlign.start, + ), + ), + ], + ), + SizedBox(height: context.lowValue), + Row( + children: [ + IconConstants.instance.getMaterialIcon(Icons.schedule), + SizedBox( + width: context.lowValue, + ), + Expanded( + child: Text( + time, + style: const TextStyle(fontSize: 15), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + textAlign: TextAlign.start, + ), + ), + ], + ), + SizedBox( + height: context.lowValue, + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton.outlined( + onPressed: () async => {}, + // displayListOfFireStationPhoneNumbers(testDevice), + icon: IconConstants.instance.getMaterialIcon(Icons.call), + iconSize: 25, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.blue[300]!), + ), + ), + const SizedBox(width: 10), + IconButton.outlined( + onPressed: () async { + String markerLabel = "Destination"; + MapsLauncher.launchCoordinates( + double.parse(item.settings!.latitude!), + double.parse(item.settings!.longitude!), + markerLabel); + }, + icon: const Icon(Icons.directions), + iconSize: 25, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.blue[300]!), + ), + ), + SizedBox(width: context.mediumValue), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: OutlinedButton( + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(backgroundColor)), + onPressed: () async { + if (message == + appLocalization(context).button_fake_fire_message) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.warning), + iconColor: Colors.red, + title: Text(appLocalization(context) + .confirm_fake_fire_message), + content: Text(appLocalization(context) + .confirm_fake_fire_body), + actions: [ + TextButton( + onPressed: () async { + int statusCode = await apiServices + .confirmFakeFireByUser(item.thingId!); + if (statusCode == 200) { + showNoIconTopSnackBar( + context, + appLocalization(context) + .notification_confirm_fake_fire_success, + Colors.green, + Colors.white); + } else { + showNoIconTopSnackBar( + context, + appLocalization(context) + .notification_confirm_fake_fire_failed, + Colors.red, + Colors.red); + } + Navigator.of(context).pop(); + }, + child: Text( + appLocalization(context) + .confirm_fake_fire_sure_message, + style: const TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(appLocalization(context) + .cancel_button_content), + ), + ], + ), + ); + } else { + showNoIconTopSnackBar( + context, + appLocalization(context).let_PCCC_handle_message, + Colors.orange, + Colors.white); + } + }, + child: Text( + message, + style: TextStyle(color: textColor), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); +} diff --git a/lib/feature/inter_family/group_detail/add_device_to_group_dialog.dart b/lib/feature/inter_family/group_detail/add_device_to_group_dialog.dart new file mode 100644 index 0000000..75ced8d --- /dev/null +++ b/lib/feature/inter_family/group_detail/add_device_to_group_dialog.dart @@ -0,0 +1,134 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'group_detail_bloc.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/services/language_services.dart'; + +import '../../devices/device_model.dart'; +import 'group_detail_model.dart'; + +addDeviceDialog(BuildContext context, DetailGroupBloc detailGroupBloc, + String groupID, List devices) async { + List ownerDevices = await detailGroupBloc.getOwnerDevices(); + List selectedItems = []; + List selectedDevices = []; + if (devices.isNotEmpty) { + ownerDevices.removeWhere((element) => + devices.any((device) => device.thingId == element.thingId)); + } + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Center(child: Text(appLocalization(context).add_device_title)), + content: DropdownButtonFormField2( + isExpanded: true, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + hint: Text( + appLocalization(context).choose_device_dropdownButton, + style: const TextStyle(fontSize: 14), + ), + items: ownerDevices + .map((item) => DropdownMenuItem( + value: item.thingId, + child: StatefulBuilder(builder: (context, menuSetState) { + final isSelected = selectedItems.contains(item.thingId); + final isNameSelected = selectedItems.contains(item.name); + + return InkWell( + onTap: () { + isSelected + ? selectedItems.remove(item.thingId) + : selectedItems.add(item.thingId!); + isNameSelected + ? selectedDevices.remove(item.name) + : selectedDevices.add(item.name!); + menuSetState(() {}); + }, + child: SizedBox( + height: double.infinity, + child: Row( + children: [ + if (isSelected) + const Icon(Icons.check_box_outlined) + else + const Icon(Icons.check_box_outline_blank), + const SizedBox(width: 10), + Expanded( + child: Text( + item.name!, + style: const TextStyle(fontSize: 14), + )) + ], + ), + ), + ); + }))) + .toList(), + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.only(right: 8), + ), + onChanged: (value) { + // thingID = value!; + // setState(() {}); + }, + onSaved: (value) {}, + selectedItemBuilder: (context) { + return selectedDevices.map( + (item) { + return Container( + alignment: AlignmentDirectional.center, + child: Text( + item, + style: const TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + ); + }, + ).toList(); + }, + iconStyleData: IconStyleData( + icon: IconConstants.instance + .getMaterialIcon(Icons.arrow_drop_down)), + dropdownStyleData: DropdownStyleData( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric(horizontal: 16), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text( + appLocalization(context).cancel_button_content, + style: const TextStyle(color: Colors.red), + ), + ), + TextButton( + onPressed: () async { + for (var device in selectedItems) { + await detailGroupBloc.addDeviceToGroup( + context, groupID, device); + await detailGroupBloc.getGroupDetail(groupID); + } + }, + child: Text(appLocalization(context).add_device_title)) + ], + ); + }); +} diff --git a/lib/feature/inter_family/group_detail/group_detail_bloc.dart b/lib/feature/inter_family/group_detail/group_detail_bloc.dart new file mode 100644 index 0000000..9442a43 --- /dev/null +++ b/lib/feature/inter_family/group_detail/group_detail_bloc.dart @@ -0,0 +1,118 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; +import '../../devices/device_model.dart'; +import '../../../product/base/bloc/base_bloc.dart'; +import '../../../product/services/api_services.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/utils/response_status_utils.dart'; + +import 'group_detail_model.dart'; + +class DetailGroupBloc extends BlocBase { + APIServices apiServices = APIServices(); + final detailGroup = StreamController.broadcast(); + StreamSink get sinkDetailGroup => detailGroup.sink; + Stream get streamDetailGroup => detailGroup.stream; + + final warningDevice = StreamController>.broadcast(); + StreamSink> get sinkWarningDevice => warningDevice.sink; + Stream> get streamWarningDevice => warningDevice.stream; + + final message = StreamController.broadcast(); + StreamSink get sinkMessage => message.sink; + Stream get streamMessage => message.stream; + + @override + void dispose() {} + + Future getGroupDetail(String groupID) async { + final body = await apiServices.getGroupDetail(groupID); + final data = jsonDecode(body); + List warningDevices = []; + if (data != null) { + GroupDetail group = GroupDetail.fromJson(data); + sinkDetailGroup.add(group); + if (group.devices != null) { + for (var device in group.devices!) { + if (device.state == 1) { + warningDevices.add(device); + } + } + sinkWarningDevice.add(warningDevices); + } + } + } + + Future approveUserToGroup(BuildContext context, String groupID, + String userID, String userName) async { + Map body = {"group_id": groupID, "user_id": userID}; + int statusCode = await apiServices.approveGroup(body); + showSnackBarResponseByStatusCode(context, statusCode, + "Đã duyệt $userName vào nhóm!", "Duyệt $userName thất bại!"); + } + + Future deleteOrUnapproveUser(BuildContext context, String groupID, + String userID, String userName) async { + int statusCode = await apiServices.deleteUserInGroup(groupID, userID); + showSnackBarResponseByStatusCode(context, statusCode, + "Đã xóa người dùng $userName", "Xóa người dùng $userName thất bại"); + } + + Future deleteDevice(BuildContext context, String groupID, + String thingID, String deviceName) async { + int statusCode = await apiServices.deleteDeviceInGroup(groupID, thingID); + showSnackBarResponseByStatusCode(context, statusCode, + "Đã xóa thiết bị $deviceName", "Xóa thiết bị $deviceName thất bại"); + } + + Future leaveGroup( + BuildContext context, String groupID, String userID) async { + int statusCode = await apiServices.deleteUserInGroup(groupID, userID); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_leave_group_success, + appLocalization(context).notification_leave_group_failed); + } + + Future updateDeviceNameInGroup( + BuildContext context, String thingID, String newAlias) async { + Map body = {"thing_id": thingID, "alias": newAlias}; + int statusCode = await apiServices.updateDeviceAlias(body); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_update_device_success, + appLocalization(context).notification_update_device_failed, + ); + } + + Future> getOwnerDevices() async { + List allDevices = []; + String body = await apiServices.getOwnerDevices(); + if (body != "") { + final data = jsonDecode(body); + List items = data['items']; + allDevices = Device.fromJsonDynamicList(items); + } + return allDevices; + } + + Future addDeviceToGroup( + BuildContext context, String groupID, String thingID) async { + Map body = { + "thing_id": thingID, + }; + int statusCode = await apiServices.addDeviceToGroup(groupID, body); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_add_device_success, + appLocalization(context).notification_add_device_failed, + ); + } +} diff --git a/lib/feature/inter_family/group_detail/group_detail_model.dart b/lib/feature/inter_family/group_detail/group_detail_model.dart new file mode 100644 index 0000000..247f3eb --- /dev/null +++ b/lib/feature/inter_family/group_detail/group_detail_model.dart @@ -0,0 +1,133 @@ +class GroupDetail { + String? id; + String? type; + String? name; + String? description; + String? ownerId; + String? status; + String? visibility; + DateTime? createdAt; + DateTime? updatedAt; + List? users; + List? devices; + + GroupDetail({ + required this.id, + this.type, + this.name, + this.description, + this.ownerId, + this.status, + this.visibility, + this.createdAt, + this.updatedAt, + this.users, + this.devices, + }); + + GroupDetail.fromJson(Map json) { + id = json["id"]; + type = json["type"]; + name = json["name"]; + description = json["description"]; + ownerId = json["ownerId"]; + status = json["status"]; + visibility = json["visibility"]; + createdAt = DateTime.parse(json["created_at"]); + updatedAt = DateTime.parse(json['updated_at']); + if (json['users'] != null) { + users = []; + json["users"].forEach((v) { + final user = GroupUser.fromJson(v); + users!.add(user); + }); + } + if (json['devices'] != null) { + devices = []; + json["devices"].forEach((v) { + final device = DeviceOfGroup.fromJson(v); + devices!.add(device); + }); + } + } + + // static List fromJsonDynamicList(List list) { + // return list.map((e) => GroupDetail.fromJson(e)).toList(); + // } +} + +class GroupUser { + String? id; + String? username; + String? role; + String? name; + String? email; + String? phone; + bool? isOwner; + String? status; + String? address; + String? latitude; + String? longitude; + + GroupUser({ + required this.id, + this.username, + this.role, + this.name, + this.email, + this.phone, + this.isOwner, + this.status, + this.address, + this.latitude, + this.longitude, + }); + + GroupUser.fromJson(Map json) { + id = json["id"]; + username = json["username"]; + role = json["role"]; + name = json["name"]; + email = json['email']; + phone = json['phone']; + isOwner = json['is_owner']; + status = json['status']; + address = json['address']; + latitude = json['latitude']; + longitude = json['longitude']; + } + + static List fromJsonList(List list) { + return list.map((e) => GroupUser.fromJson(e)).toList(); + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => GroupUser.fromJson(e)).toList(); + } +} + +class DeviceOfGroup { + String? thingId; + String? name; + String? alias; + DateTime? connectionTime; + int? state; + String? visibility; + + DeviceOfGroup({ + required this.thingId, + this.name, + this.alias, + this.connectionTime, + this.state, + this.visibility, + }); + DeviceOfGroup.fromJson(Map json) { + thingId = json['thing_id']; + name = json['name']; + alias = json['alias']; + connectionTime = DateTime.parse(json['connection_time']); + state = json['state']; + visibility = json['visibility']; + } +} diff --git a/lib/feature/inter_family/group_detail/group_detail_screen.dart b/lib/feature/inter_family/group_detail/group_detail_screen.dart new file mode 100644 index 0000000..4e48749 --- /dev/null +++ b/lib/feature/inter_family/group_detail/group_detail_screen.dart @@ -0,0 +1,552 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'group_detail_bloc.dart'; +import 'group_detail_model.dart'; +import '../../../product/base/bloc/base_bloc.dart'; +import '../../../product/constant/app/app_constants.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/api_services.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/utils/device_utils.dart'; +import '../../../product/utils/response_status_utils.dart'; + +import 'add_device_to_group_dialog.dart'; + +class DetailGroupScreen extends StatefulWidget { + const DetailGroupScreen({super.key, required this.group, required this.role}); + final String group; + final String role; + @override + State createState() => _DetailGroupScreenState(); +} + +class _DetailGroupScreenState extends State { + late DetailGroupBloc detailGroupBloc; + final scaffoldKey = GlobalKey(); + @override + void initState() { + super.initState(); + detailGroupBloc = BlocProvider.of(context); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: detailGroupBloc.streamDetailGroup, + builder: (context, detailGroupSnapshot) { + if (detailGroupSnapshot.data?.id == null) { + detailGroupBloc.getGroupDetail(widget.group); + return const Center( + child: CircularProgressIndicator(), + ); + } else { + return Scaffold( + key: scaffoldKey, + appBar: AppBar( + title: Center( + child: Text( + detailGroupSnapshot.data?.name ?? + appLocalization(context).loading_message, + ), + ), + actions: [ + IconButton( + onPressed: () { + if (scaffoldKey.currentState != null) { + scaffoldKey.currentState?.openEndDrawer(); + } else { + // log("_scaffoldKey.currentState is null"); + } + }, + icon: const Icon(Icons.info_outline_rounded), + ), + ], + ), + endDrawer: endDrawer(detailGroupSnapshot.data!), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + addDeviceDialog(context, detailGroupBloc, widget.group, + detailGroupSnapshot.data?.devices ?? []); + }, + icon: const Icon(Icons.add), + // backgroundColor: const Color.fromARGB(255, 109, 244, 96), + label: Text(appLocalization(context).add_device_title), + ), + body: SafeArea( + child: groupBody( + detailGroupSnapshot.data?.devices ?? [], + ), + ), + ); + } + }); + } + + Widget endDrawer(GroupDetail groupDetail) { + APIServices apiServices = APIServices(); + return Drawer( + width: context.dynamicWidth(0.75), + child: ListView( + children: [ + Center( + child: Text(groupDetail.name ?? "", + style: Theme.of(context).textTheme.titleLarge)), + Center( + child: Text(groupDetail.description ?? "", + style: Theme.of(context).textTheme.bodyMedium)), + Visibility( + visible: widget.role == "owner", + child: Theme( + data: + Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + title: Text( + "${appLocalization(context).approve_user} (${getNumberInGroup(groupDetail, "PENDING")})", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + children: [ + if (groupDetail.users != null && + groupDetail.users!.isNotEmpty) + for (var user in groupDetail.users!) + if (user.status == "PENDING") + ListTile( + title: Text(user.name!), + subtitle: Text(user.email!), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 48, + child: IconButton( + onPressed: () async { + await detailGroupBloc.approveUserToGroup( + context, + widget.group, + user.id!, + user.name!); + detailGroupBloc + .getGroupDetail(widget.group); + }, + icon: const Icon( + Icons.check, + color: Colors.green, + ), + ), + ), + SizedBox( + width: 48, + child: IconButton( + onPressed: () async { + await detailGroupBloc.deleteOrUnapproveUser( + context, + widget.group, + user.id!, + user.name!); + await detailGroupBloc + .getGroupDetail(widget.group); + }, + icon: const Icon( + Icons.close, + color: Colors.red, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + title: Text( + "${appLocalization(context).member_title} (${getNumberInGroup(groupDetail, "JOINED")})", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + children: [ + if (groupDetail.users != null && groupDetail.users!.isNotEmpty) + for (var user in groupDetail.users!) + if (user.status == "JOINED") + ListTile( + title: Text(user.name ?? 'Error'), + subtitle: Text(user.email ?? 'Error'), + trailing: widget.role == "owner" + ? PopupMenuButton( + icon: IconConstants.instance + .getMaterialIcon(Icons.more_horiz), + itemBuilder: (contex) => [ + PopupMenuItem( + value: 1, + child: Text(appLocalization(context) + .change_button_content)), + PopupMenuItem( + onTap: () async { + await detailGroupBloc + .deleteOrUnapproveUser( + context, + widget.group, + user.id!, + user.name!); + await detailGroupBloc + .getGroupDetail(widget.group); + }, + value: 2, + child: Text(appLocalization(context) + .delete_button_content)), + ], + ) + : null, + ), + ], + ), + ), + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + title: Text( + "${appLocalization(context).devices_title} (${getNumberInGroup(groupDetail, "DEVICES")})", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + children: [ + if (groupDetail.devices != null && + groupDetail.devices!.isNotEmpty) + for (var device in groupDetail.devices!) + if (device.visibility == "PUBLIC") + ListTile( + title: Text( + device.alias != "" ? device.alias! : device.name!), + // subtitle: Text(device.thingId ?? 'Error'), + trailing: widget.role == + ApplicationConstants.OWNER_GROUP + ? PopupMenuButton( + icon: IconConstants.instance + .getMaterialIcon(Icons.more_horiz), + itemBuilder: (contex) => [ + PopupMenuItem( + onTap: () { + Navigator.pop(context); + showChangeAlias(device); + }, + value: 1, + child: Text(appLocalization(context) + .change_button_content)), + PopupMenuItem( + onTap: () async { + await detailGroupBloc.deleteDevice( + context, + widget.group, + device.thingId!, + device.name!, + ); + }, + value: 2, + child: Text( + appLocalization(context) + .delete_button_content, + ), + ), + ], + ) + : PopupMenuButton( + icon: IconConstants.instance.getMaterialIcon( + Icons.more_horiz, + ), + itemBuilder: ((itemContext) => [ + PopupMenuItem( + onTap: () async { + Navigator.pop(itemContext); + Future.delayed( + context.normalDuration, + () => showChangeAlias(device), + ); + }, + value: 1, + child: Text( + appLocalization(context) + .change_button_content, + ), + ), + ]), + ), + ), + ], + ), + ), + SizedBox(height: context.dynamicHeight(0.3)), + if (widget.role == "owner") + TextButton( + onPressed: () { + Future.delayed(context.normalDuration) + .then((value) => Navigator.pop(context)) + .then( + (value) => { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Center( + child: Text(appLocalization(context) + .delete_group_title)), + content: Text( + appLocalization(context) + .delete_device_dialog_content, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text( + appLocalization(context) + .cancel_button_content, + ), + ), + TextButton( + onPressed: () async { + Navigator.of(dialogContext).pop(); + Future.delayed(context.lowDuration).then( + (value) => Navigator.pop(context), + ); + int statusCode = await apiServices + .deleteGroup(widget.group); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context) + .notification_delete_group_success, + appLocalization(context) + .notification_delete_group_failed); + }, + child: Text( + appLocalization(context) + .confirm_button_content, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ); + }, + ), + }, + ); + }, + style: const ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red), + foregroundColor: MaterialStatePropertyAll(Colors.white), + ), + child: Text( + appLocalization(context).delete_group_title, + ), + ) + else + TextButton( + onPressed: () async { + String uid = await apiServices.getUID(); + Future.delayed(context.lowDuration) + .then((value) => Navigator.pop(context)) + .then((value) => { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Center( + child: Text( + appLocalization(context) + .leave_group_title, + ), + ), + content: Text(appLocalization(context) + .leave_group_content), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text( + appLocalization(context) + .cancel_button_content, + ), + ), + TextButton( + onPressed: () async { + Navigator.of(dialogContext).pop(); + Future.delayed(context.normalDuration) + .then((value) => + Navigator.pop(context)); + await detailGroupBloc.leaveGroup( + context, widget.group, uid); + }, + child: Text( + appLocalization(context) + .confirm_button_content, + style: const TextStyle(color: Colors.red), + )) + ], + ); + }) + }); + }, + style: const ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red), + foregroundColor: MaterialStatePropertyAll(Colors.white), + ), + child: Text( + appLocalization(context).leave_group_title, + ), + ), + ], + ), + ); + } + + Widget groupBody(List devices) { + devices = softDeviceByState(devices); + List deviceState = []; + if (devices.isNotEmpty) { + for (var device in devices) { + String state = + DeviceUtils.instance.checkStateDevice(context, device.state!); + deviceState.add(state); + } + } + + return Expanded( + child: Center( + child: devices.isEmpty + ? Center( + child: Text(appLocalization(context).dont_have_device), + ) + : StreamBuilder>( + stream: detailGroupBloc.streamWarningDevice, + builder: (context, warningDeviceSnapshot) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${appLocalization(context).warning_message} ${warningDeviceSnapshot.data?.length ?? '0'}", + style: const TextStyle( + color: Colors.red, + ), + ), + Expanded( + child: ListView.builder( + itemCount: devices.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text(devices[index].alias != "" + ? devices[index].alias! + : devices[index].name!,), + trailing: Text( + DeviceUtils.instance.checkStateDevice( + context, devices[index].state!), + style: TextStyle( + color: DeviceUtils.instance + .getTableRowColor( + devices[index].state!,),), + ), + ); + }, + ), + ), + ], + ); + },), + ), + ); + } + + showChangeAlias(DeviceOfGroup device) async { + TextEditingController aliasController = + TextEditingController(text: device.alias); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.white, + dismissDirection: DismissDirection.none, + duration: const Duration(minutes: 1), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${appLocalization(context).map_result}: ', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black)), + Container( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: () async { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + icon: const Icon(Icons.close)), + ) + ], + ), + TextField( + controller: aliasController, + decoration: InputDecoration( + label: + Text(appLocalization(context).input_name_device_device), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)))), + ), + const SizedBox(height: 5), + Center( + child: TextButton( + style: const ButtonStyle( + foregroundColor: MaterialStatePropertyAll(Colors.white), + backgroundColor: MaterialStatePropertyAll(Colors.green)), + onPressed: () async { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + String alias = aliasController.text; + await detailGroupBloc.updateDeviceNameInGroup( + context, device.thingId!, alias); + await detailGroupBloc.getGroupDetail(widget.group); + }, + child: Text(appLocalization(context).confirm_button_content)), + ) + ], + ), + ), + ); + } +} + +List softDeviceByState(List devices) { + List sortedDevices = List.from(devices); + sortedDevices.sort((a, b) { + int stateOrder = [2, 1, 3, 0, -1, 3].indexOf(a.state!) - + [2, 1, 3, 0, -1, 3].indexOf(b.state!); + return stateOrder; + }); + + return sortedDevices; +} + +int getNumberInGroup(GroupDetail group, String name) { + int number = 0; + + if (group.id != "") { + if (name == "PENDING" || name == "JOINED") { + for (var user in group.users ?? []) { + if (user.status == name) { + number++; + } + } + } else { + for (var device in group.devices ?? []) { + if (device.visibility == "PUBLIC") { + number++; + } + } + } + } + + return number; +} diff --git a/lib/feature/inter_family/groups/groups_model.dart b/lib/feature/inter_family/groups/groups_model.dart new file mode 100644 index 0000000..f79b473 --- /dev/null +++ b/lib/feature/inter_family/groups/groups_model.dart @@ -0,0 +1,46 @@ +class Group { + String? id; + String? type; + String? name; + String? ownerId; + String? status; + String? visibility; + DateTime? createdAt; + DateTime? updatedAt; + bool? isOwner; + String? description; + + Group({ + required this.id, + this.type, + this.name, + this.ownerId, + this.status, + this.visibility, + this.createdAt, + this.updatedAt, + this.isOwner, + this.description, + }); + + Group.fromJson(Map json) { + id = json["id"]; + type = json["type"]; + name = json["name"]; + ownerId = json["owner_id"]; + status = json["status"]; + visibility = json["visibility"]; + createdAt = DateTime.parse(json["created_at"]); + updatedAt = DateTime.parse(json["updated_at"]); + isOwner = json["is_owner"]; + description = json["description"]; + } + + static List fromJsonList(List list) { + return list.map((e) => Group.fromJson(e)).toList(); + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => Group.fromJson(e)).toList(); + } +} diff --git a/lib/feature/inter_family/groups/groups_screen.dart b/lib/feature/inter_family/groups/groups_screen.dart new file mode 100644 index 0000000..fbffa89 --- /dev/null +++ b/lib/feature/inter_family/groups/groups_screen.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../product/constant/enums/app_route_enums.dart'; +import 'groups_model.dart'; +import '../inter_family_bloc.dart'; +import '../inter_family_widget.dart'; +import '../../../product/base/bloc/base_bloc.dart'; +import '../../../product/constant/app/app_constants.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/language_services.dart'; + +import 'groups_widget.dart'; + +class GroupsScreen extends StatefulWidget { + const GroupsScreen({super.key, required this.role}); + final String role; + @override + State createState() => _GroupsScreenState(); +} + +class _GroupsScreenState extends State { + late InterFamilyBloc interFamilyBloc; + + @override + void initState() { + super.initState(); + interFamilyBloc = BlocProvider.of(context); + // interFamilyBloc.getAllGroup(widget.role); + } + + @override + Widget build(BuildContext context) { + if (widget.role == ApplicationConstants.OWNER_GROUP || + widget.role == ApplicationConstants.PARTICIPANT_GROUP) { + interFamilyBloc.getAllGroup(widget.role); + return StreamBuilder>( + stream: interFamilyBloc.streamCurrentGroups, + builder: (context, groupsSnapshot) { + return Scaffold( + body: groupsSnapshot.data?.isEmpty ?? true + ? const Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + itemCount: groupsSnapshot.data!.length, + itemBuilder: (context, index) { + return ListTile( + onTap: () { + context.pushNamed(AppRoutes.GROUP_DETAIL.name, + pathParameters: { + "groupId": groupsSnapshot.data![index].id! + }, + extra: widget.role); + }, + leading: IconConstants.instance + .getMaterialIcon(Icons.diversity_2), + title: Text( + groupsSnapshot.data![index].name ?? '', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + groupsSnapshot.data![index].description ?? ""), + trailing: + widget.role == ApplicationConstants.OWNER_GROUP + ? PopupMenuButton( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(8.0), + bottomRight: Radius.circular(8.0), + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + ), + ), + itemBuilder: (ctx) => [ + _buildPopupMenuItem( + groupsSnapshot.data![index], + context, + appLocalization(context) + .share_group_title, + Icons.share, + 4), + _buildPopupMenuItem( + groupsSnapshot.data![index], + context, + appLocalization(context) + .change_group_infomation_title, + Icons.settings_backup_restore, + 2), + _buildPopupMenuItem( + groupsSnapshot.data![index], + context, + appLocalization(context) + .delete_group_title, + Icons.delete_forever_rounded, + 3), + ], + icon: const Icon(Icons.more_horiz), + ) + : const SizedBox.shrink(), + ); + }, + ), + ); + }); + } else { + return const SizedBox.shrink(); + } + } + + PopupMenuItem _buildPopupMenuItem(Group group, BuildContext context, + String title, IconData iconData, int position) { + return PopupMenuItem( + onTap: () { + if (title == appLocalization(context).share_group_title) { + Future.delayed(context.lowDuration, () { + shareGroup(context, group); + }); + } else if (title == + appLocalization(context).change_group_infomation_title) { + Future.delayed(context.lowDuration, () { + createOrJoinGroupDialog( + context, + interFamilyBloc, + widget.role, + appLocalization(context).change_group_infomation_content, + appLocalization(context).group_name_title, + group.name!, + false, + group.id!, + appLocalization(context).description_group, + group.description ?? ""); + }); + } else if (title == appLocalization(context).delete_group_title) { + Future.delayed(context.lowDuration, () { + showActionDialog( + context, + widget.role, + interFamilyBloc, + appLocalization(context).delete_group_title, + appLocalization(context).delete_group_content, + group); + }); + } else {} + }, + value: position, + child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon( + iconData, + color: Colors.black, + ), + const SizedBox(width: 10), + Text(title), + ], + ), + ); + } +} diff --git a/lib/feature/inter_family/groups/groups_widget.dart b/lib/feature/inter_family/groups/groups_widget.dart new file mode 100644 index 0000000..f23d70a --- /dev/null +++ b/lib/feature/inter_family/groups/groups_widget.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import '../inter_family_bloc.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/language_services.dart'; + +import 'groups_model.dart'; + +shareGroup(BuildContext context, Group group) { + showGeneralDialog( + barrierColor: Colors.black.withOpacity(0.5), + transitionBuilder: (context, a1, a2, widget) { + return Material( + child: Scaffold( + appBar: AppBar( + title: + Center(child: Text(appLocalization(context).share_group_title)), + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: IconConstants.instance.getMaterialIcon(Icons.arrow_back), + ), + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: QrImageView( + data: '${group.id}.${group.name!}', + size: 250, + ), + ), + const SizedBox(height: 10), + Text(group.name!), + const SizedBox(height: 10), + OutlinedButton( + onPressed: () {}, + child: const Text('Capture and Save as Image'), + ), + ], + ), + ), + ); + }, + transitionDuration: context.lowDuration, + barrierDismissible: true, + barrierLabel: '', + context: context, + pageBuilder: (context, animation1, animation2) { + return const SizedBox(); + }, + ); +} + +showActionDialog( + BuildContext context, + String role, + InterFamilyBloc interFamilyBloc, + String dialogTitle, + String dialogContent, + Group group) { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Center(child: Text(dialogTitle)), + content: Text(dialogContent), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text(appLocalization(context).cancel_button_content), + ), + TextButton( + onPressed: () async { + if (dialogTitle == appLocalization(context).delete_group_title) { + Navigator.of(dialogContext).pop(); + await interFamilyBloc.deleteGroup(context, group.id!); + interFamilyBloc.getAllGroup(role); + } else {} + }, + child: Text( + appLocalization(context).confirm_button_content, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ); + }, + ); +} diff --git a/lib/feature/inter_family/inter_family_bloc.dart b/lib/feature/inter_family/inter_family_bloc.dart new file mode 100644 index 0000000..2541080 --- /dev/null +++ b/lib/feature/inter_family/inter_family_bloc.dart @@ -0,0 +1,119 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import '../../product/constant/app/app_constants.dart'; + +import '../../product/services/api_services.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/services/language_services.dart'; +import '../../product/utils/response_status_utils.dart'; +import 'groups/groups_model.dart'; + +class InterFamilyBloc extends BlocBase { + APIServices apiServices = APIServices(); + + final isLoading = StreamController.broadcast(); + StreamSink get sinkIsLoading => isLoading.sink; + Stream get streamIsLoading => isLoading.stream; + + final titleScreen = StreamController.broadcast(); + StreamSink get sinkTitleScreen => titleScreen.sink; + Stream get streamTitleScreen => titleScreen.stream; + + final selectedScreen = StreamController.broadcast(); + StreamSink get sinkSelectedScreen => selectedScreen.sink; + Stream get streamSelectedScreen => selectedScreen.stream; + + final currentGroups = StreamController>.broadcast(); + StreamSink> get sinkCurrentGroups => currentGroups.sink; + Stream> get streamCurrentGroups => currentGroups.stream; + + @override + void dispose() {} + + void getAllGroup(String role) async { + List groups = []; + sinkCurrentGroups.add(groups); + final body = await apiServices.getAllGroups(); + + if (body.isNotEmpty) { + final data = jsonDecode(body); + List items = data["items"]; + groups = Group.fromJsonDynamicList(items); + groups = sortGroupByName(groups); + + List currentGroups = groups.where( + (group) { + bool isPublic = group.visibility == "PUBLIC"; + + if (role == ApplicationConstants.OWNER_GROUP) { + return group.isOwner == true && isPublic; + } + + if (role == ApplicationConstants.PARTICIPANT_GROUP) { + return group.isOwner == null && isPublic; + } + + return false; + }, + ).toList(); + + sinkCurrentGroups.add(currentGroups); + } else { + log("Get groups from API failed"); + } + log("Inter Family Role: $role"); + } + + Future createGroup( + BuildContext context, String name, String description) async { + APIServices apiServices = APIServices(); + Map body = {"name": name, "description": description}; + int? statusCode = await apiServices.createGroup(body); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_add_group_success, + appLocalization(context).notification_add_group_failed); + } + + Future changeGroupInfomation(BuildContext context, String groupID, + String name, String description) async { + Map body = {"name": name, "description": description}; + int statusCode = await apiServices.updateGroup(body, groupID); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_update_group_success, + appLocalization(context).notification_update_group_failed); + } + + Future joinGroup(BuildContext context, String groupID) async { + Map body = { + "group_id": groupID, + }; + int statusCode = await apiServices.joinGroup(groupID, body); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_join_request_group_success, + appLocalization(context).notification_join_request_group_failed); + } + + Future deleteGroup(BuildContext context, String groupID) async { + int statusCode = await apiServices.deleteGroup(groupID); + showSnackBarResponseByStatusCode( + context, + statusCode, + appLocalization(context).notification_delete_group_success, + appLocalization(context).notification_delete_group_failed); + } + + List sortGroupByName(List groups) { + return groups..sort((a, b) => (a.name ?? '').compareTo(b.name ?? '')); + } +} diff --git a/lib/feature/inter_family/inter_family_screen.dart b/lib/feature/inter_family/inter_family_screen.dart new file mode 100644 index 0000000..c192618 --- /dev/null +++ b/lib/feature/inter_family/inter_family_screen.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; + +import 'groups/groups_screen.dart'; +import 'inter_family_bloc.dart'; +import 'inter_family_widget.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/constant/app/app_constants.dart'; +import '../../product/constant/icon/icon_constants.dart'; +import '../../product/extention/context_extention.dart'; +import '../../product/services/language_services.dart'; + +class InterFamilyScreen extends StatefulWidget { + const InterFamilyScreen({super.key}); + + @override + State createState() => _InterFamilyScreenState(); +} + +class _InterFamilyScreenState extends State { + late InterFamilyBloc interFamilyBloc; + String title = ""; + int _selectedIndex = 0; + bool isLoading = false; + @override + void initState() { + super.initState(); + interFamilyBloc = BlocProvider.of(context); + } + + final _widgetOptions = [ + BlocProvider( + blocBuilder: () => InterFamilyBloc(), + child: const GroupsScreen( + role: ApplicationConstants.OWNER_GROUP, + ), + ), + BlocProvider( + blocBuilder: () => InterFamilyBloc(), + child: const GroupsScreen( + role: ApplicationConstants.PARTICIPANT_GROUP, + ), + ), + ]; + + @override + Widget build(BuildContext context) { + checkTitle(_selectedIndex); + return StreamBuilder( + stream: interFamilyBloc.streamSelectedScreen, + initialData: _selectedIndex, + builder: (context, selectSnapshot) { + return Scaffold( + appBar: AppBar( + actions: [ + ElevatedButton( + onPressed: () { + if (selectSnapshot.data == 0) { + createOrJoinGroupDialog( + context, + interFamilyBloc, + selectSnapshot.data! == 0 + ? ApplicationConstants.OWNER_GROUP + : ApplicationConstants.PARTICIPANT_GROUP, + appLocalization(context).add_new_group, + appLocalization(context).group_name_title, + "", + false, + "", + "", + ""); + } else { + createOrJoinGroupDialog( + context, + interFamilyBloc, + selectSnapshot.data! == 0 + ? ApplicationConstants.OWNER_GROUP + : ApplicationConstants.PARTICIPANT_GROUP, + appLocalization(context).join_group, + appLocalization(context).group_id_title, + '', + true, + "", + appLocalization(context).group_name_title, + ""); + } + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: IconConstants.instance.getMaterialIcon(Icons.add), + ), + ], + leading: Builder( + builder: (context) { + return IconButton( + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + icon: const Icon(Icons.menu), + ); + }, + ), + title: StreamBuilder( + stream: interFamilyBloc.streamTitleScreen, + initialData: title, + builder: (context, titleSnapshot) { + return Center( + child: Text(titleSnapshot.data ?? title), + ); + }, + ), + ), + drawer: Drawer( + width: context.dynamicWidth(0.4), + child: ListView( + padding: EdgeInsets.zero, + children: [ + ListTile( + title: Text(appLocalization(context).my_group_title), + selected: _selectedIndex == 0, + onTap: () { + _onItemTapped(0); + title = appLocalization(context).my_group_title; + interFamilyBloc.sinkTitleScreen.add(title); + Navigator.pop(context); + }, + ), + ListTile( + title: Text(appLocalization(context).invite_group), + selected: _selectedIndex == 1, + onTap: () { + _onItemTapped(1); + title = appLocalization(context).invite_group; + interFamilyBloc.sinkTitleScreen.add(title); + Navigator.pop(context); + }, + ), + ], + ), + ), + body: _widgetOptions[selectSnapshot.data ?? _selectedIndex], + ); + }, + ); + } + + void checkTitle(int index) { + if (index == 0) { + title = appLocalization(context).my_group_title; + interFamilyBloc.sinkTitleScreen.add(title); + } else { + title = appLocalization(context).invite_group; + interFamilyBloc.sinkTitleScreen.add(title); + } + } + + void _onItemTapped(int index) { + _selectedIndex = index; + interFamilyBloc.sinkSelectedScreen.add(_selectedIndex); + isLoading = false; + interFamilyBloc.sinkIsLoading.add(isLoading); + } +} diff --git a/lib/feature/inter_family/inter_family_widget.dart b/lib/feature/inter_family/inter_family_widget.dart new file mode 100644 index 0000000..87c857e --- /dev/null +++ b/lib/feature/inter_family/inter_family_widget.dart @@ -0,0 +1,152 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'inter_family_bloc.dart'; +import '../../product/constant/icon/icon_constants.dart'; +import '../../product/services/language_services.dart'; +import '../../product/utils/qr_utils.dart'; + +createOrJoinGroupDialog( + BuildContext context, + InterFamilyBloc interFamilyBloc, + String role, + String titleDialogName, + String titleTextField1, + String textFieldContent1, + bool icon, + String groupID, + String titleTextField2, + String textFieldContent2) { + TextEditingController editingController = TextEditingController(); + TextEditingController readOnlycontroller = TextEditingController(); + TextEditingController descriptionController = TextEditingController(); + + if (textFieldContent1 != "") { + editingController.text = textFieldContent1; + } + if (textFieldContent2 != "") { + descriptionController.text = textFieldContent2; + } + // final byte = b + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Center(child: Text(titleDialogName)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + icon + ? Column( + children: [ + TextField( + controller: editingController, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () async { + String data = await QRScanUtils.instance + .scanQR(dialogContext); + Map items = + QRScanUtils.instance.getQRData(data); + String groupID = items['group_id']; + String groupName = items['group_name']; + editingController.text = groupID; + readOnlycontroller.text = groupName; + }, + icon: IconConstants.instance + .getMaterialIcon(Icons.qr_code_outlined)), + label: Text(titleTextField1), + border: const OutlineInputBorder( + borderRadius: + BorderRadius.all(Radius.circular(16.0))), + ), + ), + const SizedBox(height: 10), + TextField( + readOnly: true, + controller: readOnlycontroller, + decoration: InputDecoration( + label: Text(titleTextField2), + border: const OutlineInputBorder( + borderRadius: + BorderRadius.all(Radius.circular(16.0)))), + ), + ], + ) + : Column( + children: [ + TextField( + textInputAction: TextInputAction.next, + controller: editingController, + decoration: InputDecoration( + label: Text(titleTextField1), + border: const OutlineInputBorder( + borderRadius: + BorderRadius.all(Radius.circular(16.0)))), + ), + const SizedBox(height: 10), + TextField( + onChanged: (value) {}, + maxLines: 3, + textInputAction: TextInputAction.done, + controller: descriptionController, + decoration: InputDecoration( + labelText: + appLocalization(context).description_group, + border: const OutlineInputBorder( + borderRadius: + BorderRadius.all(Radius.circular(16.0)))), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text( + appLocalization(context).cancel_button_content, + style: const TextStyle(color: Colors.red), + ), + ), + TextButton( + onPressed: () async { + if (!icon) { + String groupName = editingController.text; + String description = descriptionController.text; + if (titleDialogName == + appLocalization(context).add_new_group) { + try { + await interFamilyBloc.createGroup( + context, groupName, description); + interFamilyBloc.getAllGroup(role); + Navigator.of(dialogContext).pop(); + } catch (e) { + // log("Lỗi khi tạo nhóm: $e"); + } + } else if (titleDialogName == + appLocalization(context) + .change_group_infomation_content) { + try { + await interFamilyBloc.changeGroupInfomation( + context, groupID, groupName, description); + interFamilyBloc.getAllGroup(role); + Navigator.of(dialogContext).pop(); + } catch (e) { + // log("Lỗi khi sửa nhóm: $e"); + } + } + } else { + String groupId = editingController.text; + await interFamilyBloc.joinGroup(context, groupId); + Navigator.of(dialogContext).pop(); + } + }, + child: Text(appLocalization(context).confirm_button_content)) + ], + ); + }); +} diff --git a/lib/feature/log/device_logs_bloc.dart b/lib/feature/log/device_logs_bloc.dart new file mode 100644 index 0000000..9acf5d9 --- /dev/null +++ b/lib/feature/log/device_logs_bloc.dart @@ -0,0 +1,82 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:sfm_app/feature/devices/device_model.dart'; +import 'package:sfm_app/product/base/bloc/base_bloc.dart'; +import 'package:sfm_app/product/services/api_services.dart'; +import 'package:sfm_app/product/utils/date_time_utils.dart'; + +import '../../product/utils/device_utils.dart'; +import 'device_logs_model.dart'; + +class DeviceLogsBloc extends BlocBase { + APIServices apiServices = APIServices(); + final fromDate = StreamController.broadcast(); + StreamSink get sinkFromDate => fromDate.sink; + Stream get streamFromDate => fromDate.stream; + + final hasMore = StreamController.broadcast(); + StreamSink get sinkHasMore => hasMore.sink; + Stream get streamHasMore => hasMore.stream; + + final allDevices = StreamController>.broadcast(); + StreamSink> get sinkAllDevices => allDevices.sink; + Stream> get streamAllDevices => allDevices.stream; + + final sensors = StreamController>.broadcast(); + StreamSink> get sinkSensors => sensors.sink; + Stream> get streamSensors => sensors.stream; + + @override + void dispose() {} + + void getAllDevices() async { + String body = await apiServices.getOwnerDevices(); + if (body != "") { + final data = jsonDecode(body); + List items = data['items']; + List originalDevices = Device.fromJsonDynamicList(items); + List devices = + DeviceUtils.instance.sortDeviceByState(originalDevices); + sinkAllDevices.add(devices); + } + } + + void getDeviceLogByThingID( + int offset, + String thingID, + DateTime fromDate, + List sensors, + ) async { + log("SensorLength: ${sensors.length}"); + String fromDateString = + DateTimeUtils.instance.formatDateTimeToString(fromDate); + String now = DateTimeUtils.instance.formatDateTimeToString(DateTime.now()); + // List sensors = []; + Map params = { + 'thing_id': thingID, + 'from': fromDateString, + 'to': now, + 'limit': '30', + "offset": offset.toString(), + "asc": "true" + }; + final body = await apiServices.getLogsOfDevice(thingID, params); + if (body != "") { + final data = jsonDecode(body); + DeviceLog devicesListLog = DeviceLog.fromJson(data); + if (devicesListLog.sensors!.isEmpty) { + bool hasMore = false; + sinkHasMore.add(hasMore); + } + if (devicesListLog.sensors!.isNotEmpty) { + for (var sensor in devicesListLog.sensors!) { + sensors.add(sensor); + } + } + + sinkSensors.add(sensors); + } + } +} diff --git a/lib/feature/log/device_logs_model.dart b/lib/feature/log/device_logs_model.dart new file mode 100644 index 0000000..45fa032 --- /dev/null +++ b/lib/feature/log/device_logs_model.dart @@ -0,0 +1,66 @@ +class DeviceLog { + String? thingId; + DateTime? from; + DateTime? to; + int? total; + int? offset; + List? sensors; + + DeviceLog({ + this.thingId, + this.from, + this.to, + this.total, + this.offset, + this.sensors, + }); + + DeviceLog.fromJson(Map json) { + thingId = json["thing_id"]; + from = DateTime.parse(json["from"]); + to = DateTime.parse(json["to"]); + total = json["total"]; + offset = json["offset"]; + if (json['sensors'] != null) { + sensors = []; + json['sensors'].forEach((v) { + sensors!.add(SensorLogs.fromJson(v)); + }); + } + } +} + +class SensorLogs { + String? name; + int? value; + int? time; + LogDetail? detail; + + SensorLogs({ + this.name, + this.value, + this.time, + this.detail, + }); + SensorLogs.fromJson(Map json) { + name = json["n"]; + value = json["v"]; + time = json["t"]; + detail = json["x"] != null ? LogDetail.fromJson(json["x"]) : null; + } +} + +class LogDetail { + String? username; + String? note; + + LogDetail({ + this.username, + this.note, + }); + + LogDetail.fromJson(Map json) { + username = json["username"]; + note = json["note"]; + } +} diff --git a/lib/feature/log/device_logs_screen.dart b/lib/feature/log/device_logs_screen.dart new file mode 100644 index 0000000..9eae191 --- /dev/null +++ b/lib/feature/log/device_logs_screen.dart @@ -0,0 +1,295 @@ +import 'dart:developer'; + +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:sfm_app/feature/devices/device_model.dart'; +import 'package:sfm_app/feature/log/device_logs_bloc.dart'; +import 'package:sfm_app/product/constant/icon/icon_constants.dart'; +import 'package:sfm_app/product/extention/context_extention.dart'; +import 'package:sfm_app/product/services/language_services.dart'; +import 'package:sfm_app/product/shared/shared_snack_bar.dart'; +import 'package:sfm_app/product/utils/date_time_utils.dart'; +import 'package:sfm_app/product/utils/device_utils.dart'; + +import '../../product/base/bloc/base_bloc.dart'; +import 'device_logs_model.dart'; + +class DeviceLogsScreen extends StatefulWidget { + const DeviceLogsScreen({super.key}); + + @override + State createState() => _DeviceLogsScreenState(); +} + +class _DeviceLogsScreenState extends State { + TextEditingController fromDate = TextEditingController(); + String fromDateApi = ''; + DateTime? dateTime; + String thingID = ""; + late DeviceLogsBloc deviceLogsBloc; + List allDevices = []; + List sensors = []; + int offset = 0; + final controller = ScrollController(); + bool hasMore = true; + @override + void initState() { + super.initState(); + deviceLogsBloc = BlocProvider.of(context); + controller.addListener( + () { + if (controller.position.maxScrollExtent == controller.offset) { + offset += 30; + deviceLogsBloc.getDeviceLogByThingID( + offset, thingID, dateTime!, sensors); + } + }, + ); + } + + @override + void dispose() { + controller.dispose(); + sensors.clear(); + deviceLogsBloc.sinkSensors.add(sensors); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: deviceLogsBloc.streamAllDevices, + builder: (context, allDevicesSnapshot) { + if (allDevicesSnapshot.data?[0].thingId == null) { + deviceLogsBloc.getAllDevices(); + return const Center( + child: CircularProgressIndicator(), + ); + } else { + return StreamBuilder>( + stream: deviceLogsBloc.streamSensors, + initialData: sensors, + builder: (context, sensorsSnapshot) { + return Padding( + padding: context.paddingLow, + child: Scaffold( + body: SafeArea( + child: Column( + children: [ + DropdownButtonFormField2( + isExpanded: true, + decoration: InputDecoration( + contentPadding: context.paddingLowVertical, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + hint: Text( + appLocalization(context) + .choose_device_dropdownButton, + style: const TextStyle( + fontSize: 14, + ), + ), + items: allDevicesSnapshot.data?.isNotEmpty ?? false + ? allDevicesSnapshot.data! + .map( + (device) => DropdownMenuItem( + value: device.thingId, + child: Text( + device.name!, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ) + .toList() + : [], + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.only(right: 8), + ), + onChanged: (value) { + thingID = value!; + setState(() {}); + }, + onSaved: (value) { + log("On Saved"); + }, + iconStyleData: const IconStyleData( + icon: Icon( + Icons.arrow_drop_down, + ), + iconSize: 24, + ), + dropdownStyleData: DropdownStyleData( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric(horizontal: 16), + ), + ), + Padding( + padding: context.paddingLowVertical, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: context.dynamicWidth(0.6), + child: TextField( + controller: fromDate, + decoration: InputDecoration( + icon: + IconConstants.instance.getMaterialIcon( + Icons.calendar_month_outlined, + ), + labelText: appLocalization(context) + .choose_date_start_datePicker, + ), + readOnly: true, + onTap: () async { + DateTime? datePicker = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2023), + lastDate: DateTime(2050), + ); + dateTime = datePicker; + if (datePicker != null) { + fromDateApi = DateTimeUtils.instance + .formatDateTimeToString(datePicker); + setState( + () { + fromDate.text = + DateFormat('yyyy-MM-dd') + .format(datePicker); + }, + ); + } + }, + ), + ), + Center( + child: TextButton.icon( + style: const ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.green), + ), + onPressed: () { + if (fromDateApi.isEmpty) { + showNoIconTopSnackBar( + context, + appLocalization(context) + .notification_enter_all_inf, + Colors.red, + Colors.white, + ); + } else { + deviceLogsBloc.getDeviceLogByThingID( + offset, thingID, dateTime!, sensors); + } + log("ThingID: $thingID"); + log("From Date: ${DateTimeUtils.instance.formatDateTimeToString(dateTime!)}"); + }, + icon: IconConstants.instance + .getMaterialIcon(Icons.search), + label: Text( + appLocalization(context) + .find_button_content, + ), + ), + ), + ], + ), + ), + Divider( + height: context.lowValue, + ), + Expanded( + child: sensorsSnapshot.data?.isEmpty ?? false + ? Center( + child: Text( + appLocalization(context).no_data_message), + ) + : RefreshIndicator( + onRefresh: refresh, + child: ListView.builder( + controller: controller, + itemCount: sensorsSnapshot.data!.length + 1, + itemBuilder: (context, index) { + if (index < + sensorsSnapshot.data!.length) { + return logDetail( + sensorsSnapshot.data![index], + index, + ); + } else { + return Padding( + padding: context.paddingLow, + child: StreamBuilder( + stream: + deviceLogsBloc.streamHasMore, + builder: + (context, hasMoreSnapshot) { + return Center( + child: hasMoreSnapshot.data ?? + hasMore + ? const CircularProgressIndicator() + : Text( + appLocalization(context) + .main_no_data), + ); + }, + ), + ); + } + }, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + }, + ); + } + + Widget logDetail(SensorLogs sensor, int index) { + return Column( + children: [ + ListTile( + subtitle: + Text(DeviceUtils.instance.getDeviceSensorsLog(context, sensor)), + // leading: const Icon(Icons.sensors_outlined), + leading: Text(index.toString()), + title: Text( + DateTimeUtils.instance + .convertCurrentMillisToDateTimeString(sensor.time ?? 0), + ), + ), + const Divider( + thickness: 0.5, + indent: 50, + endIndent: 100, + ), + ], + ); + } + + Future refresh() async { + offset = 0; + sensors.clear(); + deviceLogsBloc.sensors.add(sensors); + hasMore = true; + deviceLogsBloc.sinkHasMore.add(hasMore); + deviceLogsBloc.getDeviceLogByThingID(offset, thingID, dateTime!, sensors); + } +} diff --git a/lib/feature/main/main_bloc.dart b/lib/feature/main/main_bloc.dart new file mode 100644 index 0000000..9ae5707 --- /dev/null +++ b/lib/feature/main/main_bloc.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:sfm_app/product/base/bloc/base_bloc.dart'; + +import '../bell/bell_model.dart'; + +class MainBloc extends BlocBase { + final bellBloc = StreamController.broadcast(); + StreamSink get sinkBellBloc => bellBloc.sink; + Stream get streamBellBloc => bellBloc.stream; + + final title = StreamController.broadcast(); + StreamSink get sinkTitle => title.sink; + Stream get streamTitle => title.stream; + + final role = StreamController.broadcast(); + StreamSink get sinkRole => role.sink; + Stream get streamRole => role.stream; + + final language = StreamController.broadcast(); + StreamSink get sinkLanguage => language.sink; + Stream get streamLanguage => language.stream; + + final themeMode = StreamController.broadcast(); + StreamSink get sinkThemeMode => themeMode.sink; + Stream get streamThemeMode => themeMode.stream; + + final isVNIcon = StreamController.broadcast(); + StreamSink get sinkIsVNIcon => isVNIcon.sink; + Stream get streamIsVNIcon => isVNIcon.stream; + + final currentPageIndex = StreamController.broadcast(); + StreamSink get sinkCurrentPageIndex => currentPageIndex.sink; + Stream get streamCurrentPageIndex => currentPageIndex.stream; + + @override + void dispose() {} +} diff --git a/lib/feature/main/main_screen.dart b/lib/feature/main/main_screen.dart new file mode 100644 index 0000000..ec8d258 --- /dev/null +++ b/lib/feature/main/main_screen.dart @@ -0,0 +1,393 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:badges/badges.dart' as badges; +import 'package:sfm_app/feature/home/home_bloc.dart'; +import 'package:sfm_app/product/constant/app/app_constants.dart'; +import 'package:sfm_app/product/constant/enums/app_route_enums.dart'; +import 'package:sfm_app/product/constant/enums/role_enums.dart'; +import '../devices/devices_manager_bloc.dart'; +import '../devices/devices_manager_screen.dart'; +import '../home/home_screen.dart'; +import '../inter_family/inter_family_bloc.dart'; +import '../inter_family/inter_family_screen.dart'; +import '../log/device_logs_bloc.dart'; +import '../log/device_logs_screen.dart'; +import 'main_bloc.dart'; +import '../map/map_bloc.dart'; +import '../map/map_screen.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/constant/enums/app_theme_enums.dart'; +import '../../product/extention/context_extention.dart'; +import '../../product/services/api_services.dart'; +import '../../main.dart'; +import '../../product/constant/icon/icon_constants.dart'; +import '../../product/constant/lang/language_constants.dart'; +import '../../product/services/language_services.dart'; +import '../../product/theme/theme_notifier.dart'; +import '../bell/bell_model.dart'; + +class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + APIServices apiServices = APIServices(); + late MainBloc mainBloc; + bool isVN = true; + bool isLight = true; + String role = 'Unknown'; + String titlePage = "Page Title"; + int currentPageIndex = 0; + final _badgeKey = GlobalKey(); + Bell bell = Bell(); + + void initialCheck() async { + role = await apiServices.getUserRole(); + mainBloc.sinkRole.add(role); + String language = await apiServices.checkLanguage(); + String theme = await apiServices.checkTheme(); + if (language == LanguageConstants.VIETNAM) { + isVN = true; + } else { + isVN = false; + } + if (theme == AppThemes.LIGHT.name) { + isLight = true; + } else { + isLight = false; + } + mainBloc.sinkIsVNIcon.add(isVN); + mainBloc.sinkThemeMode.add(isLight); + log("role: $role"); + } + + @override + void initState() { + super.initState(); + mainBloc = BlocProvider.of(context); + initialCheck(); + getBellNotification(); + } + + @override + Widget build(BuildContext context) { + ThemeNotifier themeNotifier = context.watch(); + checkSelectedIndex(currentPageIndex); + + List userDestinations = [ + NavigationDestination( + selectedIcon: IconConstants.instance.getMaterialIcon(Icons.home), + icon: IconConstants.instance.getMaterialIcon(Icons.home_outlined), + label: appLocalization(context).home_page_destination, + tooltip: appLocalization(context).home_page_destination, + ), + NavigationDestination( + selectedIcon: IconConstants.instance.getMaterialIcon(Icons.settings), + icon: IconConstants.instance.getMaterialIcon(Icons.settings_outlined), + label: appLocalization(context).manager_page_destination, + tooltip: appLocalization(context).device_manager_page_name, + ), + NavigationDestination( + selectedIcon: IconConstants.instance.getMaterialIcon(Icons.location_on), + icon: + IconConstants.instance.getMaterialIcon(Icons.location_on_outlined), + label: appLocalization(context).map_page_destination, + tooltip: appLocalization(context).map_page_destination, + ), + NavigationDestination( + // selectedIcon: IconConstants.instance.getMaterialIcon(Icons.histor), + icon: IconConstants.instance.getMaterialIcon(Icons.history_rounded), + label: appLocalization(context).history_page_destination, + tooltip: appLocalization(context).history_page_destination_tooltip, + ), + NavigationDestination( + selectedIcon: IconConstants.instance.getMaterialIcon(Icons.group), + icon: IconConstants.instance.getMaterialIcon(Icons.group_outlined), + label: appLocalization(context).group_page_destination, + tooltip: appLocalization(context).group_page_destination_tooltip, + ), + ]; + + List userBody = [ + BlocProvider(child: const HomeScreen(), blocBuilder: () => HomeBloc()), + BlocProvider( + child: const DevicesManagerScreen(), + blocBuilder: () => DevicesManagerBloc()), + BlocProvider( + child: const MapScreen(), + blocBuilder: () => MapBloc(), + ), + BlocProvider( + child: const DeviceLogsScreen(), + blocBuilder: () => DeviceLogsBloc(), + ), + BlocProvider( + child: const InterFamilyScreen(), + blocBuilder: () => InterFamilyBloc(), + ), + ]; + + List modDestinations = [ + NavigationDestination( + selectedIcon: IconConstants.instance.getMaterialIcon(Icons.home), + icon: IconConstants.instance.getMaterialIcon(Icons.home_outlined), + label: appLocalization(context).home_page_destination, + tooltip: appLocalization(context).home_page_destination, + ), + NavigationDestination( + selectedIcon: IconConstants.instance.getMaterialIcon(Icons.settings), + icon: IconConstants.instance.getMaterialIcon(Icons.settings_outlined), + label: appLocalization(context).manager_page_destination, + tooltip: appLocalization(context).device_manager_page_name, + ), + NavigationDestination( + selectedIcon: IconConstants.instance.getMaterialIcon(Icons.location_on), + icon: + IconConstants.instance.getMaterialIcon(Icons.location_on_outlined), + label: appLocalization(context).map_page_destination, + tooltip: appLocalization(context).map_page_destination, + ), + ]; + + List modBody = [ + BlocProvider(child: const HomeScreen(), blocBuilder: () => HomeBloc()), + BlocProvider( + child: const DevicesManagerScreen(), + blocBuilder: () => DevicesManagerBloc()), + BlocProvider( + child: const MapScreen(), + blocBuilder: () => MapBloc(), + ), + ]; + + return StreamBuilder( + stream: mainBloc.streamRole, + initialData: role, + builder: (context, roleSnapshot) { + return StreamBuilder( + stream: mainBloc.streamCurrentPageIndex, + initialData: currentPageIndex, + builder: (context, indexSnapshot) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + centerTitle: true, + title: StreamBuilder( + stream: mainBloc.streamTitle, + initialData: titlePage, + builder: (context, titleSnapshot) { + return Text( + titleSnapshot.data ?? ApplicationConstants.APP_NAME, + ); + }, + ), + actions: [ + StreamBuilder( + stream: mainBloc.streamThemeMode, + initialData: isLight, + builder: (context, themeModeSnapshot) { + return IconButton( + onPressed: () { + themeNotifier.changeTheme(); + isLight = !isLight; + mainBloc.sinkThemeMode.add(isLight); + }, + icon: Icon( + themeModeSnapshot.data ?? isLight + ? Icons.light_mode_outlined + : Icons.dark_mode_outlined, + ), + ); + }, + ), + StreamBuilder( + stream: mainBloc.streamIsVNIcon, + initialData: isVN, + builder: (context, isVnSnapshot) { + return IconButton( + onPressed: () async { + log("Locale: ${LanguageServices().getLocale()}"); + Locale locale = await LanguageServices().setLocale( + isVN + ? LanguageConstants.ENGLISH + : LanguageConstants.VIETNAM); + MyApp.setLocale(context, locale); + isVN = !isVN; + mainBloc.sinkIsVNIcon.add(isVN); + }, + icon: Image.asset( + IconConstants.instance.getIcon( + isVnSnapshot.data ?? isVN + ? 'vi_icon' + : 'en_icon'), + height: 24, + width: 24, + ), + ); + }, + ), + StreamBuilder( + stream: mainBloc.streamBellBloc, + builder: (context, bellSnapshot) { + return checkStatus(bellSnapshot.data?.items ?? []) + ? IconButton( + onPressed: () { + context.pushNamed(AppRoutes.BELL.name); + }, + icon: const Icon( + Icons.notifications, + ), + ) + : GestureDetector( + child: badges.Badge( + badgeStyle: const badges.BadgeStyle( + shape: badges.BadgeShape.twitter, + ), + key: _badgeKey, + badgeContent: const Icon( + CupertinoIcons.circle_filled, + color: Colors.red, + size: 5, + ), + badgeAnimation: + const badges.BadgeAnimation.slide( + animationDuration: + Duration(milliseconds: 200), + colorChangeAnimationDuration: + Duration(seconds: 1), + loopAnimation: false, + curve: Curves.decelerate, + colorChangeAnimationCurve: Curves.easeInCirc, + ), + showBadge: true, + // ignorePointer: false, + child: const Icon( + Icons.notifications, + size: 30, + ), + ), + onTap: () { + context.pushNamed(AppRoutes.BELL.name); + }, + ); + }, + ), + PopupMenuButton( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(8.0), + bottomRight: Radius.circular(8.0), + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + ), + ), + icon: const Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + value: ApplicationConstants.SETTINGS_PATH, + onTap: () { + context.pushNamed(AppRoutes.SETTINGS.name); + }, + child: Row( + children: [ + const Icon(Icons.person), + const SizedBox(width: 5), + Text(appLocalization(context).profile_icon_title) + ], + ), + ), + PopupMenuItem( + value: ApplicationConstants.LOGOUT_PATH, + onTap: () { + Future.delayed( + const Duration(milliseconds: 200), + () async { + await apiServices.logOut(context); + }, + ); + }, + child: Row( + children: [ + const Icon(Icons.logout), + const SizedBox(width: 5), + Text( + appLocalization(context).log_out, + ), + ], + ), + ), + ]; + }, + ) + ], + ), + bottomNavigationBar: Container( + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(50)), + padding: context.paddingLow, + child: NavigationBar( + onDestinationSelected: (index) { + currentPageIndex = index; + mainBloc.sinkCurrentPageIndex.add(currentPageIndex); + checkSelectedIndex(currentPageIndex); + }, + selectedIndex: indexSnapshot.data ?? currentPageIndex, + destinations: roleSnapshot.data == RoleEnums.USER.name + ? userDestinations + : modDestinations, + ), + ), + body: IndexedStack( + index: indexSnapshot.data ?? currentPageIndex, + children: roleSnapshot.data == RoleEnums.USER.name + ? userBody + : modBody, + ), + ); + }, + ); + }, + ); + } + + Future getBellNotification() async { + bell = await apiServices.getBellNotifications("0", "20"); + mainBloc.bellBloc.add(bell); + } + + bool checkStatus(List bells) { + if (bells.isEmpty) { + return true; + } + return !bells.any((bell) => bell.status == 0); + } + + void checkSelectedIndex(int current) { + if (current == 0) { + titlePage = appLocalization(context).home_page_name; + mainBloc.sinkTitle.add(titlePage); + } else if (current == 1) { + titlePage = appLocalization(context).device_manager_page_name; + mainBloc.sinkTitle.add(titlePage); + } else if (current == 2) { + titlePage = appLocalization(context).map_page_destination; + mainBloc.sinkTitle.add(titlePage); + } else if (current == 3) { + titlePage = appLocalization(context).device_log_page_name; + mainBloc.sinkTitle.add(titlePage); + } else if (current == 4) { + titlePage = appLocalization(context).interfamily_page_name; + mainBloc.sinkTitle.add(titlePage); + } + } +} diff --git a/lib/feature/map/map_bloc.dart b/lib/feature/map/map_bloc.dart new file mode 100644 index 0000000..9cd7830 --- /dev/null +++ b/lib/feature/map/map_bloc.dart @@ -0,0 +1,74 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import '../../product/services/map_services.dart'; +import '../../product/shared/shared_snack_bar.dart'; +import '../devices/device_model.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/services/api_services.dart'; + +class MapBloc extends BlocBase { + APIServices apiServices = APIServices(); + MapServices mapServices = MapServices(); + @override + void dispose() {} + + final mapType = StreamController.broadcast(); + StreamSink get sinkmapType => mapType.sink; + Stream get streammapType => mapType.stream; + + final mapController = StreamController.broadcast(); + StreamSink get sinkmapController => mapController.sink; + Stream get streammapController => mapController.stream; + + final allDevices = StreamController>.broadcast(); + StreamSink> get sinkAllDevices => allDevices.sink; + Stream> get streamAllDevices => allDevices.stream; + + final allMarker = StreamController>.broadcast(); + StreamSink> get sinkAllMarker => allMarker.sink; + Stream> get streamAllMarker => allMarker.stream; + + final polylines = StreamController>.broadcast(); + StreamSink> get sinkPolylines => polylines.sink; + Stream> get streamPolylines => polylines.stream; + + Future updateCameraPosition( + Completer controller, + double latitude, + double longitude, + double zoom, + ) async { + final CameraPosition cameraPosition = + CameraPosition(target: LatLng(latitude, longitude), zoom: zoom); + final GoogleMapController mapController = await controller.future; + mapController.animateCamera(CameraUpdate.newCameraPosition(cameraPosition)); + } + + Future findTheWay( + BuildContext context, + Completer controller, + LatLng origin, + LatLng destination, + ) async { + List polylines = []; + final polylineCoordinates = + await mapServices.findTheWay(origin, destination); + if (polylineCoordinates.isNotEmpty) { + polylines = polylineCoordinates; + sinkPolylines.add(polylines); + await updateCameraPosition( + controller, + destination.latitude, + destination.longitude, + 13.0, + ); + } else { + showNoIconTopSnackBar( + context, "Không tìm thấy đường", Colors.orange, Colors.white); + } + } +} diff --git a/lib/feature/map/map_screen.dart b/lib/feature/map/map_screen.dart new file mode 100644 index 0000000..a7615cf --- /dev/null +++ b/lib/feature/map/map_screen.dart @@ -0,0 +1,237 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:sfm_app/feature/devices/device_model.dart'; +import 'package:sfm_app/feature/map/map_bloc.dart'; +import 'package:sfm_app/feature/map/widget/on_tap_marker_widget.dart'; +import 'package:sfm_app/product/base/bloc/base_bloc.dart'; +import 'package:sfm_app/product/constant/icon/icon_constants.dart'; +import 'package:sfm_app/product/services/api_services.dart'; + +class MapScreen extends StatefulWidget { + const MapScreen({super.key}); + + @override + State createState() => _MapScreenState(); +} + +class _MapScreenState extends State { + late BitmapDescriptor normalIcon; + late BitmapDescriptor offlineIcon; + late BitmapDescriptor abnormalIcon; + late BitmapDescriptor flameIcon; + late BitmapDescriptor hospitalIcon; + late BitmapDescriptor fireStationIcon; + late MapBloc mapBloc; + late ClusterManager clusterManager; + MapType mapType = MapType.terrain; + APIServices apiServices = APIServices(); + final streamController = StreamController.broadcast(); + List devices = []; + Completer _controller = Completer(); + List imageAssets = [ + IconConstants.instance.getIcon("normal_icon"), + IconConstants.instance.getIcon("offline_icon"), + IconConstants.instance.getIcon("flame_icon"), + IconConstants.instance.getIcon("hospital_marker"), + IconConstants.instance.getIcon("fire_station_marker"), + ]; + static const CameraPosition _myPosition = CameraPosition( + target: LatLng(20.976108, 105.791666), + zoom: 12, + ); + Set markersAll = {}; + List markers = []; + LatLng myLocation = const LatLng(213761, 123123); + + @override + void initState() { + super.initState(); + mapBloc = BlocProvider.of(context); + _loadIcons(); + getAllMarkers(); + clusterManager = _initClusterManager(); + } + + @override + void dispose() { + streamController.close(); + _controller = Completer(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + StreamBuilder>( + stream: mapBloc.streamAllMarker, + builder: (context, markerSnapshot) { + return StreamBuilder>( + stream: mapBloc.streamPolylines, + builder: (context, polylinesSnapshot) { + return GoogleMap( + initialCameraPosition: _myPosition, + mapType: mapType, + onMapCreated: (GoogleMapController controller) { + if (!_controller.isCompleted) { + _controller.complete(controller); + } + streamController.sink.add(controller); + clusterManager.setMapId(controller.mapId); + }, + markers: markerSnapshot.data ?? markersAll + ..addAll(markers), + zoomControlsEnabled: true, + myLocationEnabled: true, + mapToolbarEnabled: false, + onCameraMove: (position) { + clusterManager.onCameraMove(position); + }, + onCameraIdle: () { + clusterManager.updateMap(); + }, + polylines: { + Polyline( + polylineId: const PolylineId('router'), + points: polylinesSnapshot.data ?? [], + color: Colors.deepPurpleAccent, + width: 8, + ), + }, + ); + }, + ); + }, + ), + ], + ), + ); + } + + Future _loadIcons() async { + List> iconFutures = imageAssets.map((asset) { + return BitmapDescriptor.fromAssetImage(const ImageConfiguration(), asset); + }).toList(); + + List icons = await Future.wait(iconFutures); + + normalIcon = icons[0]; + offlineIcon = icons[1]; + flameIcon = icons[2]; + hospitalIcon = icons[3]; + fireStationIcon = icons[4]; + } + + ClusterManager _initClusterManager() { + return ClusterManager( + devices, + _updateMarkers, + markerBuilder: _getmarkerBuilder(), + ); + } + + Future Function(Cluster) _getmarkerBuilder() => + (cluster) async { + return Marker( + markerId: MarkerId( + cluster.getId(), + ), + position: cluster.location, + onTap: () { + onTapMarker( + context, + _controller, + mapBloc, + myLocation, + cluster.items, + imageAssets, + markers, + hospitalIcon, + fireStationIcon); + }, + icon: getMarkerIcon(cluster), + ); + }; + + BitmapDescriptor getMarkerIcon(Cluster cluster) { + if (cluster.items.length == 1) { + Device item = cluster.items.first; + if (item.state == 0) { + return normalIcon; + } else if (item.state == 1) { + return flameIcon; + } else { + return offlineIcon; + } + } + bool hasStateOne = false; + bool hasOtherState = false; + + for (var item in cluster.items) { + if (item.state == 1) { + hasStateOne = true; + break; + } else if (item.state != 0) { + hasOtherState = true; + } + } + + // log("Has state = 1: $hasStateOne, Has other state: $hasOtherState"); + + if (hasStateOne) { + return flameIcon; // flameIcon + } else if (hasOtherState) { + return normalIcon; // normalIcon + } else { + return offlineIcon; // offlineIcon + } + } + + bool checkStateMarker(Cluster cluster) { + bool hasStateOne = false; + bool hasOtherState = false; + + for (var item in cluster.items) { + if (item.state == 1) { + hasStateOne = true; + break; + } else if (item.state != 0) { + hasOtherState = true; + } + } + + // log("Has state = 1: $hasStateOne, Has other state: $hasOtherState"); + + if (hasStateOne) { + return true; // flameIcon + } else if (hasOtherState) { + return false; // normalIcon + } else { + return true; // offlineIcon + } + } + + void _updateMarkers(Set marker) { + log("Update Marker"); + markersAll = marker; + mapBloc.sinkAllMarker.add(marker); + } + + void getAllMarkers() async { + String response = await apiServices.getOwnerDevices(); + if (response != "") { + final data = jsonDecode(response); + List result = data['items']; + final devicesList = Device.fromJsonDynamicList(result); + for (var device in devicesList) { + devices.add(device); + } + } + } +} diff --git a/lib/feature/map/widget/on_tap_marker_widget.dart b/lib/feature/map/widget/on_tap_marker_widget.dart new file mode 100644 index 0000000..2f39c06 --- /dev/null +++ b/lib/feature/map/widget/on_tap_marker_widget.dart @@ -0,0 +1,344 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'show_direction_widget.dart'; +import 'show_nearest_place.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../map_bloc.dart'; +import '../../../product/services/api_services.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/utils/device_utils.dart'; + +import '../../devices/device_model.dart'; + +onTapMarker( + BuildContext context, + Completer controller, + MapBloc mapBloc, + LatLng myLocation, + Iterable devices, + List imageAssets, + List otherMarkers, + BitmapDescriptor hospitalIcon, + BitmapDescriptor fireStationIcon, +) { + LatLng testLocation = const LatLng(20.985453, 105.738381); + showModalBottomSheet( + context: context, + builder: (BuildContext modalBottomSheetContext) { + if (devices.length == 1) { + List devicesList = devices.toList(); + final device = devicesList[0]; + String deviceState = DeviceUtils.instance + .checkStateDevice(modalBottomSheetContext, device.state ?? 3); + String imgStringAsset; + if (device.state == 0) { + imgStringAsset = imageAssets[0]; + } else if (device.state == 1) { + imgStringAsset = imageAssets[2]; + } else { + imgStringAsset = imageAssets[1]; + } + return Padding( + padding: context.paddingLow, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${appLocalization(context).device_title}: ${device.name}", + style: context.titleLargeTextStyle, + ), + ], + ), + SizedBox( + height: context.lowValue, + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + Navigator.pop(modalBottomSheetContext); + var destination = LatLng( + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + ); + mapBloc.findTheWay( + context, + controller, + testLocation, + destination, + ); + String deviceLocations = await DeviceUtils.instance + .getFullDeviceLocation(context, device.areaPath!); + String yourLocation = + appLocalization(context).map_your_location; + showDirections( + context, + controller, + otherMarkers, + mapBloc, + yourLocation, + deviceLocations, + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + ); + }, + style: const ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.blue), + foregroundColor: MaterialStatePropertyAll(Colors.white), + ), + child: Row( + children: [ + IconConstants.instance + .getMaterialIcon(Icons.turn_right), + Text(appLocalization(modalBottomSheetContext) + .map_show_direction), + ], + ), + ), + SizedBox( + width: context.lowValue, + ), + ElevatedButton.icon( + onPressed: () { + Navigator.pop(modalBottomSheetContext); + showNearPlacesSideSheet( + context, + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + "Bệnh viện gần nhất", + 5000, + "hospital", + mapBloc, + controller, + otherMarkers, + hospitalIcon, + fireStationIcon, + ); + }, + style: const ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.pink), + foregroundColor: MaterialStatePropertyAll(Colors.white), + ), + icon: IconConstants.instance + .getMaterialIcon(Icons.local_hospital), + label: Text(appLocalization(modalBottomSheetContext) + .map_nearby_hospital), + ), + SizedBox( + width: context.lowValue, + ), + ElevatedButton.icon( + onPressed: () { + Navigator.pop(modalBottomSheetContext); + showNearPlacesSideSheet( + context, + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + "đội pccc gần nhất", + 5000, + "fire_station", + mapBloc, + controller, + otherMarkers, + hospitalIcon, + fireStationIcon, + ); + }, + style: const ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red), + foregroundColor: MaterialStatePropertyAll(Colors.white), + ), + icon: IconConstants.instance + .getMaterialIcon(Icons.fire_truck_outlined), + label: Text(appLocalization(modalBottomSheetContext) + .map_nearby_firestation), + ), + ], + ), + ), + SizedBox(height: context.lowValue), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${appLocalization(modalBottomSheetContext).paginated_data_table_column_deviceStatus}: '), + SizedBox( + height: 25, + width: 25, + child: CircleAvatar( + minRadius: 20, + maxRadius: 20, + backgroundImage: AssetImage(imgStringAsset), + ), + ), + SizedBox(width: context.lowValue), + Text( + deviceState, + style: context.bodyMediumTextStyle, + ), + ], + ), + ], + ), + ); + // Tiếp tục xử lý mảng devices + } else if (devices.length > 1) { + DeviceSource deviceSource = DeviceSource( + DeviceUtils.instance.sortDeviceByState(devices.toList()), + modalBottomSheetContext, + controller, + mapBloc, + ); + // Devices là một phần tử + return Padding( + padding: context.paddingLow, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Text( + appLocalization(modalBottomSheetContext) + .paginated_data_table_title, + style: context.titleLargeTextStyle, + ), + ), + PaginatedDataTable( + columns: [ + DataColumn( + label: Center( + child: Text( + appLocalization(modalBottomSheetContext) + .input_name_device_device, + ), + ), + ), + DataColumn( + label: Center( + child: Text( + appLocalization(modalBottomSheetContext) + .paginated_data_table_column_deviceStatus, + ), + ), + ), + DataColumn( + label: Center( + child: Text( + appLocalization(modalBottomSheetContext) + .paginated_data_table_column_deviceBaterry, + ), + ), + ), + DataColumn( + label: Center( + child: Text( + appLocalization(modalBottomSheetContext) + .paginated_data_table_column_deviceSignal, + ), + ), + ), + ], + source: deviceSource, + rowsPerPage: 5, + ), + ], + ), + ); + } else { + return Text(appLocalization(modalBottomSheetContext).undefine_message); + } + }, + ); +} + +class DeviceSource extends DataTableSource { + BuildContext context; + MapBloc mapBloc; + final List devices; + final Completer controller; + DeviceSource(this.devices, this.context, this.controller, this.mapBloc); + APIServices apiServices = APIServices(); + @override + DataRow? getRow(int index) { + if (index >= devices.length) { + return null; + } + final device = devices[index]; + Map sensorMap = DeviceUtils.instance + .getDeviceSensors(context, device.status?.sensors ?? []); + String deviceState = + DeviceUtils.instance.checkStateDevice(context, device.state!); + return DataRow.byIndex( + color: MaterialStatePropertyAll( + DeviceUtils.instance.getTableRowColor(device.state ?? -1), + ), + index: index, + cells: [ + DataCell( + Text(device.name!), + onTap: () { + mapBloc.updateCameraPosition( + controller, + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + 16, + ); + Navigator.pop(context); + }, + ), + DataCell( + Text(deviceState), + onTap: () { + mapBloc.updateCameraPosition( + controller, + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + 16, + ); + }, + ), + DataCell( + Text(sensorMap['sensorBattery'] + "%"), + onTap: () { + mapBloc.updateCameraPosition( + controller, + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + 16, + ); + }, + ), + DataCell( + Text(sensorMap['sensorCsq']), + onTap: () { + mapBloc.updateCameraPosition( + controller, + double.parse(device.settings!.latitude!), + double.parse(device.settings!.longitude!), + 16, + ); + }, + ), + ], + ); + } + + @override + bool get isRowCountApproximate => false; + @override + int get rowCount => devices.length; + @override + int get selectedRowCount => 0; +} diff --git a/lib/feature/map/widget/show_direction_widget.dart b/lib/feature/map/widget/show_direction_widget.dart new file mode 100644 index 0000000..2d9ab49 --- /dev/null +++ b/lib/feature/map/widget/show_direction_widget.dart @@ -0,0 +1,124 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:sfm_app/product/constant/icon/icon_constants.dart'; +import 'package:sfm_app/product/extention/context_extention.dart'; +import 'package:sfm_app/product/services/language_services.dart'; + +import '../map_bloc.dart'; + +showDirections( + BuildContext context, + Completer controller, + List markers, + MapBloc mapBloc, + String originalName, + String destinationLocation, + double devicelat, + double devicelng, +) { + TextEditingController originController = + TextEditingController(text: originalName); + TextEditingController destinationController = + TextEditingController(text: destinationLocation); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.transparent, + // dismissDirection: DismissDirection.none, + duration: const Duration(minutes: 5), + content: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + appLocalization(context).map_show_direction, + style: context.titleLargeTextStyle, + ), + Container( + alignment: Alignment.centerRight, + child: IconButton.outlined( + onPressed: () async { + await mapBloc.updateCameraPosition( + controller, + devicelat, + devicelng, + 13.0, + ); + List polylineCoordinates = []; + mapBloc.sinkPolylines.add(polylineCoordinates); + markers.clear(); + if (context.mounted) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + } + }, + icon: IconConstants.instance.getMaterialIcon(Icons.close), + ), + ) + ], + ), + Row( + children: [ + Text( + '${appLocalization(context).map_start}: ', + ), + SizedBox(width: context.lowValue), + Expanded( + child: TextField( + controller: originController, + readOnly: true, + ), + ), + ], + ), + SizedBox( + height: context.lowValue, + ), + Row( + children: [ + Text( + '${appLocalization(context).map_destination}: ', + ), + SizedBox(width: context.lowValue), + Expanded( + child: TextField( + controller: destinationController, + readOnly: true, + ), + ), + ], + ), + SizedBox(height: context.lowValue), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () async { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + List polylineCoordinates = []; + mapBloc.sinkPolylines.add(polylineCoordinates); + MapsLauncher.launchCoordinates(devicelat, devicelng); + }, + icon: IconConstants.instance + .getMaterialIcon(Icons.near_me_rounded), + label: Text( + appLocalization(context).map_stream, + ), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.blue[300]!), + ), + ), + ], + ), + ], + ), + ), + ); +} diff --git a/lib/feature/map/widget/show_nearest_place.dart b/lib/feature/map/widget/show_nearest_place.dart new file mode 100644 index 0000000..beab93a --- /dev/null +++ b/lib/feature/map/widget/show_nearest_place.dart @@ -0,0 +1,226 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import '../map_bloc.dart'; +import 'show_direction_widget.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/language_services.dart'; +import '../../../product/services/map_services.dart'; +import '../../../product/shared/model/near_by_search_model.dart'; + +showNearPlacesSideSheet( + BuildContext context, + double latitude, + double longitude, + String searchKey, + int radius, + String type, + MapBloc mapBloc, + Completer controller, + List markers, + BitmapDescriptor hospitalIcon, + BitmapDescriptor fireStationIcon, +) async { + double screenWidth = MediaQuery.of(context).size.width; + double screenHeight = MediaQuery.of(context).size.height; + LatLng testLocation = LatLng(latitude, longitude); + List placeDetails = []; + placeDetails = + await findLocation(latitude, longitude, searchKey, radius, type); + await mapBloc.updateCameraPosition(controller, latitude, longitude, 12.0); + if (placeDetails.isNotEmpty) { + for (int i = 0; i < placeDetails.length; i++) { + Marker marker = Marker( + markerId: MarkerId(placeDetails[i].result!.placeId!), + position: LatLng(placeDetails[i].result!.geometry!.location!.lat!, + placeDetails[i].result!.geometry!.location!.lng!), + infoWindow: InfoWindow(title: placeDetails[i].result!.name!), + icon: + searchKey == "Bệnh viện gần nhất" ? hospitalIcon : fireStationIcon, + ); + markers.add(marker); + } + } + showModalBottomSheet( + context: context, + builder: (modalBottomSheetContext) { + return Container( + padding: context.paddingLow, + width: screenWidth, + height: screenHeight / 3, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Center( + child: Text( + '${appLocalization(modalBottomSheetContext).map_result}: ', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: () async { + Navigator.pop(modalBottomSheetContext); + markers.clear(); + await mapBloc.updateCameraPosition( + controller, + latitude, + longitude, + 14.0, + ); + }, + icon: IconConstants.instance.getMaterialIcon(Icons.close), + ), + ), + ], + ), + if (placeDetails.isNotEmpty) + Expanded( + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: placeDetails.length, + itemBuilder: (BuildContext listViewContext, int index) { + final place = placeDetails[index]; + return GestureDetector( + onTap: () async { + await mapBloc.updateCameraPosition( + controller, + place.result!.geometry!.location!.lat!, + place.result!.geometry!.location!.lng!, + 17.0, + ); + }, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + child: Container( + padding: listViewContext.paddingLow, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + ), + width: screenWidth / 1.5, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + place.result!.name!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: listViewContext.lowValue), + Text( + '${appLocalization(listViewContext).change_profile_address} ${place.result!.formattedAddress} '), + SizedBox(height: listViewContext.lowValue), + if (place.result?.openingHours?.openNow == null) + Text( + appLocalization(listViewContext) + .map_always_opened, + style: const TextStyle( + color: Colors.green, + ), + ) + else + place.result?.openingHours?.openNow ?? false + ? Text( + appLocalization(listViewContext) + .map_openning, + style: const TextStyle( + color: Colors.green, + ), + ) + : Text( + appLocalization(listViewContext) + .map_closed, + style: const TextStyle( + color: Colors.red, + ), + ), + SizedBox( + height: listViewContext.lowValue, + ), + Center( + child: ElevatedButton.icon( + style: const ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue), + foregroundColor: + MaterialStatePropertyAll(Colors.white), + ), + onPressed: () async { + Navigator.pop(modalBottomSheetContext); + var destination = LatLng( + place.result!.geometry!.location!.lat!, + place.result!.geometry!.location!.lng!); + mapBloc.findTheWay(context, controller, + testLocation, destination); + // findTheWay(myLocation, destination); + await mapBloc.updateCameraPosition( + controller, + place.result!.geometry!.location!.lat!, + place.result!.geometry!.location!.lng!, + 13.0, + ); + showDirections( + context, + controller, + markers, + mapBloc, + appLocalization(context) + .map_your_location, + place.result!.name!, + latitude, + longitude, + ); + }, + icon: IconConstants.instance + .getMaterialIcon(Icons.turn_right), + label: Text(appLocalization(listViewContext) + .map_show_direction), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ) + else + Center( + child: Text( + appLocalization(modalBottomSheetContext).map_no_results, + ), + ), + ], + ), + ); + }, + ); +} + +Future> findLocation(double latitude, double longitude, + String searchKey, int radius, String type) async { + MapServices mapServices = MapServices(); + + final nearByPlaces = await mapServices.getNearbyPlaces( + latitude, longitude, searchKey, radius, type); + + return nearByPlaces.isNotEmpty ? nearByPlaces : []; +} diff --git a/lib/feature/settings/device_notification_settings/device_notification_settings_bloc.dart b/lib/feature/settings/device_notification_settings/device_notification_settings_bloc.dart new file mode 100644 index 0000000..5cd8ed8 --- /dev/null +++ b/lib/feature/settings/device_notification_settings/device_notification_settings_bloc.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'device_notification_settings_model.dart'; +import '../../../product/base/bloc/base_bloc.dart'; + +class DeviceNotificationSettingsBloc extends BlocBase { + final listNotifications = + StreamController>.broadcast(); + StreamSink> get sinkListNotifications => + listNotifications.sink; + Stream> get streamListNotifications => + listNotifications.stream; + + final deviceThingID = StreamController.broadcast(); + StreamSink get sinkDeviceThingID => deviceThingID.sink; + Stream get streamDeviceThingID => deviceThingID.stream; + + final deviceNotificationSettingMap = + StreamController>.broadcast(); + StreamSink> get sinkDeviceNotificationSettingMap => + deviceNotificationSettingMap.sink; + Stream> get streamDeviceNotificationSettingMap => + deviceNotificationSettingMap.stream; + + final isDataChange = StreamController.broadcast(); + StreamSink get sinkIsDataChange => isDataChange.sink; + Stream get streamIsDataChange => isDataChange.stream; + + final iconChange = StreamController.broadcast(); + StreamSink get sinkIconChange => iconChange.sink; + Stream get streamIconChange => iconChange.stream; + + final messageChange = StreamController.broadcast(); + StreamSink get sinkMessageChange => messageChange.sink; + Stream get streaMmessageChange => messageChange.stream; + + + @override + void dispose() {} +} diff --git a/lib/feature/settings/device_notification_settings/device_notification_settings_model.dart b/lib/feature/settings/device_notification_settings/device_notification_settings_model.dart new file mode 100644 index 0000000..e6e84ef --- /dev/null +++ b/lib/feature/settings/device_notification_settings/device_notification_settings_model.dart @@ -0,0 +1,56 @@ +class DeviceNotificationSettings { + String? id; + String? userId; + String? thingId; + DeviceAliasSettings? settings; + DateTime? updatedAt; + + DeviceNotificationSettings({ + this.id, + this.userId, + this.thingId, + this.settings, + this.updatedAt, + }); + + DeviceNotificationSettings.fromJson(Map json) { + id = json["id"]; + userId = json["user_id"]; + thingId = json["thing_id"]; + settings = json["settings"] != null + ? DeviceAliasSettings.fromJson(json["settings"]) + : null; + updatedAt = DateTime.parse(json["updated_at"]); + } + + static List mapToList( + Map map) { + return map.values.toList(); + } + + static Map mapFromJson( + Map json) { + final Map devices = {}; + json.forEach((key, value) { + devices[key] = DeviceNotificationSettings.fromJson(value); + }); + return devices; + } +} + +class DeviceAliasSettings { + String? alias; + Map? notifiSettings; + + DeviceAliasSettings({ + this.alias, + this.notifiSettings, + }); + + DeviceAliasSettings.fromJson(Map json) { + alias = json['alias']; + if (json['notifi_settings'] != null) { + notifiSettings = Map.from(json['notifi_settings']); + } + } +} diff --git a/lib/feature/settings/device_notification_settings/device_notification_settings_screen.dart b/lib/feature/settings/device_notification_settings/device_notification_settings_screen.dart new file mode 100644 index 0000000..9054e03 --- /dev/null +++ b/lib/feature/settings/device_notification_settings/device_notification_settings_screen.dart @@ -0,0 +1,313 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:convert'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import '../../../product/shared/shared_snack_bar.dart'; +import 'device_notification_settings_bloc.dart'; +import 'device_notification_settings_model.dart'; +import '../../../product/base/bloc/base_bloc.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/api_services.dart'; +import '../../../product/services/language_services.dart'; + +class DeviceNotificationSettingsScreen extends StatefulWidget { + const DeviceNotificationSettingsScreen({super.key}); + + @override + State createState() => + _DeviceNotificationSettingsScreenState(); +} + +class _DeviceNotificationSettingsScreenState + extends State { + late DeviceNotificationSettingsBloc deviceNotificationSettingsBloc; + String thingID = ""; + List deviceNotifications = []; + APIServices apiServices = APIServices(); + @override + void initState() { + super.initState(); + deviceNotificationSettingsBloc = BlocProvider.of(context); + getNotificationSetting(); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: deviceNotificationSettingsBloc.streamListNotifications, + initialData: deviceNotifications, + builder: (context, notificationSnapshot) { + return StreamBuilder( + stream: deviceNotificationSettingsBloc.streamDeviceThingID, + initialData: thingID, + builder: (context, deviceThingIdSnapshot) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + appLocalization(context).profile_setting, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + body: deviceNotifications.isEmpty + ? Center( + child: CircularProgressIndicator( + value: context.mediumValue, + ), + ) + : Padding( + padding: context.paddingLow, + child: Column( + children: [ + DropdownButtonFormField2< + DeviceNotificationSettings>( + isExpanded: true, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + hint: Text( + appLocalization(context) + .choose_device_dropdownButton, + style: const TextStyle(fontSize: 14), + ), + iconStyleData: const IconStyleData( + icon: Icon( + Icons.arrow_drop_down, + ), + iconSize: 24, + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric(horizontal: 16), + ), + items: deviceNotifications + .map((e) => DropdownMenuItem< + DeviceNotificationSettings>( + value: e, + child: Text(e.settings!.alias!), + )) + .toList(), + onChanged: (value) { + thingID = value!.thingId ?? ""; + deviceNotificationSettingsBloc + .sinkDeviceThingID + .add(thingID); + }, + ), + if (deviceThingIdSnapshot.data! != "") + listNotificationSetting( + deviceThingIdSnapshot.data ?? thingID, + deviceNotifications) + ], + ), + ), + ); + }); + }); + } + + void getNotificationSetting() async { + String? response = await apiServices.getAllSettingsNotificationOfDevices(); + final data = jsonDecode(response); + final result = data['data']; + // log("Data ${DeviceNotificationSettings.mapFromJson(jsonDecode(data)).values.toList()}"); + deviceNotifications = + DeviceNotificationSettings.mapFromJson(result).values.toList(); + deviceNotificationSettingsBloc.sinkListNotifications + .add(deviceNotifications); + } + + Widget listNotificationSetting( + String thingID, List devices) { + DeviceNotificationSettings chooseDevice = DeviceNotificationSettings(); + + for (var device in devices) { + if (device.thingId == thingID) { + chooseDevice = device; + break; + } + } + IconData selectIcon = Icons.unpublished_outlined; + String selectMessage = appLocalization(context) + .change_profile_device_notification_deselect_all; + bool isChange = false; + bool isDataChange = false; + // Lấy notifiSettings + final notifiSettings = chooseDevice.settings?.notifiSettings ?? {}; + deviceNotificationSettingsBloc.sinkDeviceNotificationSettingMap + .add(notifiSettings); + return SingleChildScrollView( + child: StreamBuilder>( + stream: + deviceNotificationSettingsBloc.streamDeviceNotificationSettingMap, + initialData: notifiSettings, + builder: (context, deviceNotificationSettingSnapshot) { + return StreamBuilder( + stream: deviceNotificationSettingsBloc.streamIsDataChange, + initialData: isDataChange, + builder: (context, isDataChangeSnapshot) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton.icon( + onPressed: () { + changeIconAndMessage( + isChange, + selectIcon, + selectMessage, + notifiSettings, + isDataChange); + isChange = !isChange; + }, + icon: StreamBuilder( + stream: deviceNotificationSettingsBloc + .streamIconChange, + initialData: selectIcon, + builder: (context, iconSnapshot) { + return Icon( + iconSnapshot.data ?? selectIcon); + }), + label: StreamBuilder( + stream: deviceNotificationSettingsBloc + .streaMmessageChange, + builder: (context, messageSnapshot) { + return Text( + messageSnapshot.data ?? selectMessage); + })), + if (isDataChangeSnapshot.data ?? isDataChange) + IconButton( + onPressed: () { + updateDeviceNotification( + thingID, notifiSettings, isDataChange); + }, + icon: const Icon(Icons.done)) + ], + ), + ListView.builder( + shrinkWrap: true, + itemCount: + deviceNotificationSettingSnapshot.data?.length ?? + notifiSettings.length, + itemBuilder: (context, index) { + final key = deviceNotificationSettingSnapshot + .data!.keys + .elementAt(index); + final value = + deviceNotificationSettingSnapshot.data![key]; + return CheckboxListTile( + value: value == 1, + onChanged: (newValue) { + notifiSettings[key] = newValue == true ? 1 : 0; + deviceNotificationSettingsBloc + .sinkDeviceNotificationSettingMap + .add(notifiSettings); + isDataChange = true; + deviceNotificationSettingsBloc.sinkIsDataChange + .add(isDataChange); + }, + title: Text(getNotificationEvent(key)), + ); + }, + ), + if (isDataChangeSnapshot.data ?? isDataChange) + Center( + child: TextButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all( + Colors.green), + foregroundColor: + MaterialStateProperty.all( + Colors.white)), + onPressed: () async { + updateDeviceNotification( + thingID, notifiSettings, isDataChange); + }, + child: Text(appLocalization(context) + .update_button_content)), + ) + ], + ); + }); + }), + ); + } + + void changeIconAndMessage(bool isChange, IconData icon, String message, + Map notifiSettings, bool isDataChange) { + if (isChange == false) { + icon = Icons.published_with_changes_outlined; + message = appLocalization(context) + .change_profile_device_notification_select_all; + notifiSettings.updateAll((key, value) => 0); + } else { + icon = Icons.unpublished_outlined; + message = appLocalization(context) + .change_profile_device_notification_deselect_all; + notifiSettings.updateAll((key, value) => 1); + } + isDataChange = true; + deviceNotificationSettingsBloc.sinkIsDataChange.add(isDataChange); + deviceNotificationSettingsBloc.sinkDeviceNotificationSettingMap + .add(notifiSettings); + deviceNotificationSettingsBloc.sinkIconChange.add(icon); + deviceNotificationSettingsBloc.sinkMessageChange.add(message); + } + + String getNotificationEvent(String eventCode) { + String event = ""; + if (eventCode == "001") { + event = "Thiết bị mất kết nối"; + } else if (eventCode == "002") { + event = "Hoạt động của thiết bị"; + } else if (eventCode == "003") { + event = "Cảnh báo của thiết bị"; + } else if (eventCode == "004") { + event = "Thiết bị lỗi"; + } else if (eventCode == "005") { + event = "Thiết bị bình thường"; + } else if (eventCode == "006") { + event = "Thiết bị yếu pin"; + } else if (eventCode == "101") { + event = "Người dùng tham gia nhóm"; + } else if (eventCode == "102") { + event = "Người dùng rời nhóm"; + } else if (eventCode == "103") { + event = "Thiết bị tham gia nhóm"; + } else if (eventCode == "104") { + event = "Thiết bị bị xóa khỏi nhóm"; + } else { + event = "Chưa xác định"; + } + + return event; + } + + void updateDeviceNotification(String thingID, Map notifiSettings, + bool isDataChange) async { + int statusCode = await apiServices.updateDeviceNotificationSettings( + thingID, notifiSettings); + if (statusCode == 200) { + showNoIconTopSnackBar( + context, + appLocalization(context).notification_update_device_settings_success, + Colors.green, + Colors.white); + } else { + showNoIconTopSnackBar( + context, + appLocalization(context).notification_update_device_settings_failed, + Colors.red, + Colors.white); + } + isDataChange = false; + deviceNotificationSettingsBloc.sinkIsDataChange.add(isDataChange); + } +} diff --git a/lib/feature/settings/profile/profile_model.dart b/lib/feature/settings/profile/profile_model.dart new file mode 100644 index 0000000..a38ae68 --- /dev/null +++ b/lib/feature/settings/profile/profile_model.dart @@ -0,0 +1,37 @@ +class User { + String? id; + String? username; + String? role; + String? name; + String? email; + String? phone; + String? address; + String? notes; + String? latitude; + String? longitude; + + User( + {this.address, + this.email, + this.id, + this.name, + this.notes, + this.phone, + this.role, + this.username, + this.latitude, + this.longitude}); + + User.fromJson(Map json) { + id = json['id']; + username = json['username']; + role = json['role']; + name = json['name']; + email = json['email']; + phone = json['phone']; + address = json['address']; + notes = json['notes']; + latitude = json['latitude']; + longitude = json['longitude']; + } +} diff --git a/lib/feature/settings/profile/profile_screen.dart b/lib/feature/settings/profile/profile_screen.dart new file mode 100644 index 0000000..b72780d --- /dev/null +++ b/lib/feature/settings/profile/profile_screen.dart @@ -0,0 +1,440 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import '../../../product/shared/shared_snack_bar.dart'; +import '../../../product/constant/icon/icon_constants.dart'; +import '../../../product/services/api_services.dart'; +import '../settings_bloc.dart'; +import '../../../product/shared/shared_input_decoration.dart'; +import '../../../product/extention/context_extention.dart'; +import '../../../product/services/language_services.dart'; + +import 'profile_model.dart'; + +changeUserInfomation( + BuildContext context, User user, SettingsBloc settingsBloc) { + final formKey = GlobalKey(); + String username = user.name ?? ""; + String email = user.email ?? ""; + String tel = user.phone ?? ""; + String address = user.address ?? ""; + bool isChange = false; + APIServices apiServices = APIServices(); + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + context: context, + builder: (modalBottomSheetContext) { + return StreamBuilder( + stream: settingsBloc.streamIsChangeProfileInfomation, + initialData: isChange, + builder: (context, isChangeSnapshot) { + return Scaffold( + appBar: AppBar( + title: Text(appLocalization(context).change_profile_title), + centerTitle: true, + actions: [ + isChangeSnapshot.data ?? isChange + ? IconButton( + onPressed: () async { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + String latitude = user.latitude ?? ""; + String longitude = user.longitude ?? ""; + Map body = { + "name": username, + "email": email, + "phone": tel, + "address": address, + "latitude": latitude, + "longitude": longitude + }; + int statusCode = + await apiServices.updateUserProfile(body); + if (statusCode == 200) { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_profile_success, + Colors.green, + Colors.white); + } else { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_profile_failed, + Colors.redAccent, + Colors.white); + } + Navigator.pop(modalBottomSheetContext); + } + }, + icon: + IconConstants.instance.getMaterialIcon(Icons.check), + ) + : const SizedBox.shrink() + ], + ), + body: SingleChildScrollView( + child: Form( + key: formKey, + child: Padding( + padding: context.paddingLow, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalization(context).change_profile_username, + style: context.titleMediumTextStyle, + ), + Padding( + padding: context.paddingLowVertical, + child: TextFormField( + initialValue: username, + textInputAction: TextInputAction.next, + validator: (usernameValue) { + if (usernameValue == "null" || + usernameValue!.isEmpty) { + return appLocalization(modalBottomSheetContext) + .login_account_not_empty; + } + return null; + }, + onChanged: (value) { + isChange = true; + settingsBloc.sinkIsChangeProfileInfomation + .add(isChange); + }, + onSaved: (newUsername) { + username = newUsername!; + }, + decoration: borderRadiusTopLeftAndBottomRight( + modalBottomSheetContext, + appLocalization(modalBottomSheetContext) + .change_profile_username_hint), + ), + ), + Text( + appLocalization(context).change_profile_email, + style: context.titleMediumTextStyle, + ), + Padding( + padding: context.paddingLowVertical, + child: TextFormField( + initialValue: email, + textInputAction: TextInputAction.next, + validator: (emailValue) { + if (emailValue == "null" || emailValue!.isEmpty) { + return appLocalization(modalBottomSheetContext) + .change_profile_email_not_empty; + } + return null; + }, + onChanged: (value) { + isChange = true; + settingsBloc.sinkIsChangeProfileInfomation + .add(isChange); + }, + onSaved: (newEmail) { + email = newEmail!; + }, + decoration: borderRadiusTopLeftAndBottomRight( + modalBottomSheetContext, + appLocalization(modalBottomSheetContext) + .change_profile_email_hint), + ), + ), + Text( + appLocalization(context).change_profile_tel, + style: context.titleMediumTextStyle, + ), + Padding( + padding: context.paddingLowVertical, + child: TextFormField( + initialValue: tel, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.phone, + validator: (telValue) { + if (telValue == "null" || telValue!.isEmpty) { + return appLocalization(modalBottomSheetContext) + .change_profile_tel_not_empty; + } + return null; + }, + onChanged: (value) { + isChange = true; + settingsBloc.sinkIsChangeProfileInfomation + .add(isChange); + }, + onSaved: (newTel) { + tel = newTel!; + }, + decoration: borderRadiusTopLeftAndBottomRight( + modalBottomSheetContext, + appLocalization(modalBottomSheetContext) + .change_profile_tel_hint), + ), + ), + Text( + appLocalization(context).change_profile_address, + style: context.titleMediumTextStyle, + ), + Padding( + padding: context.paddingLowVertical, + child: TextFormField( + initialValue: address, + textInputAction: TextInputAction.done, + onSaved: (newAddress) { + address = newAddress!; + }, + onChanged: (value) { + isChange = true; + settingsBloc.sinkIsChangeProfileInfomation + .add(isChange); + }, + decoration: borderRadiusTopLeftAndBottomRight( + modalBottomSheetContext, + appLocalization(modalBottomSheetContext) + .change_profile_address_hint), + ), + ), + SizedBox(height: context.mediumValue), + isChangeSnapshot.data ?? isChange + ? Center( + child: TextButton( + onPressed: () async { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + String latitude = user.latitude ?? ""; + String longitude = user.longitude ?? ""; + Map body = { + "name": username, + "email": email, + "phone": tel, + "address": address, + "latitude": latitude, + "longitude": longitude + }; + int statusCode = await apiServices + .updateUserProfile(body); + if (statusCode == 200) { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_profile_success, + Colors.green, + Colors.white); + } else { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_profile_failed, + Colors.redAccent, + Colors.white); + } + Navigator.pop(modalBottomSheetContext); + } + }, + style: const ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue), + foregroundColor: + MaterialStatePropertyAll(Colors.white), + ), + child: Text(appLocalization(context) + .update_button_content), + ), + ) + : const SizedBox.shrink(), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ); +} + +changeUserPassword(BuildContext context, SettingsBloc settingsBloc) { + final formKey = GlobalKey(); + String oldPass = ""; + String newPass = ""; + APIServices apiServices = APIServices(); + bool isChange = false; + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + context: context, + builder: (modalBottomSheetContext) { + return StreamBuilder( + stream: settingsBloc.streamIsChangeProfileInfomation, + initialData: isChange, + builder: (context, isChangeSnapshot) { + return Scaffold( + appBar: AppBar( + title: Text(appLocalization(context).change_profile_title), + centerTitle: true, + actions: [ + isChangeSnapshot.data ?? isChange + ? IconButton( + onPressed: () async { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + Map body = { + "password_old": oldPass, + "password_new": newPass, + }; + int statusCode = + await apiServices.updateUserPassword(body); + if (statusCode == 200) { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_password_success, + Colors.green, + Colors.white); + } else { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_password_failed, + Colors.redAccent, + Colors.white); + } + Navigator.pop(modalBottomSheetContext); + } + }, + icon: + IconConstants.instance.getMaterialIcon(Icons.check), + ) + : const SizedBox.shrink() + ], + ), + body: SingleChildScrollView( + child: Form( + key: formKey, + child: Padding( + padding: context.paddingLow, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalization(context).change_profile_old_pass, + style: context.titleMediumTextStyle, + ), + Padding( + padding: context.paddingLowVertical, + child: TextFormField( + initialValue: oldPass, + textInputAction: TextInputAction.next, + validator: (oldPassValue) { + if (oldPassValue == "null" || + oldPassValue!.isEmpty) { + return appLocalization(modalBottomSheetContext) + .change_profile_old_pass_not_empty; + } + return null; + }, + onChanged: (value) { + isChange = true; + settingsBloc.sinkIsChangeProfileInfomation + .add(isChange); + }, + onSaved: (newOldPass) { + oldPass = newOldPass!; + }, + decoration: borderRadiusTopLeftAndBottomRight( + modalBottomSheetContext, + appLocalization(modalBottomSheetContext) + .change_profile_old_pass_hint), + ), + ), + Text( + appLocalization(context).change_profile_new_pass, + style: context.titleMediumTextStyle, + ), + Padding( + padding: context.paddingLowVertical, + child: TextFormField( + initialValue: newPass, + textInputAction: TextInputAction.done, + validator: (newPassValue) { + if (newPassValue == "null" || + newPassValue!.isEmpty) { + return appLocalization(modalBottomSheetContext) + .change_profile_new_pass_not_empty; + } + return null; + }, + onChanged: (value) { + isChange = true; + settingsBloc.sinkIsChangeProfileInfomation + .add(isChange); + }, + onSaved: (newnewPass) { + newPass = newnewPass!; + }, + decoration: borderRadiusTopLeftAndBottomRight( + modalBottomSheetContext, + appLocalization(modalBottomSheetContext) + .change_profile_new_pass_hint), + ), + ), + SizedBox(height: context.mediumValue), + isChangeSnapshot.data ?? isChange + ? Center( + child: TextButton( + onPressed: () async { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + Map body = { + "password_old": oldPass, + "password_new": newPass, + }; + int statusCode = await apiServices + .updateUserPassword(body); + if (statusCode == 200) { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_password_success, + Colors.green, + Colors.white); + } else { + showNoIconTopSnackBar( + modalBottomSheetContext, + appLocalization(context) + .notification_update_password_failed, + Colors.redAccent, + Colors.white); + } + Navigator.pop(modalBottomSheetContext); + } + }, + style: const ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue), + foregroundColor: + MaterialStatePropertyAll(Colors.white), + ), + child: Text(appLocalization(context) + .update_button_content), + ), + ) + : const SizedBox.shrink() + ], + ), + ), + ), + ), + ); + }, + ); + }, + ); +} diff --git a/lib/feature/settings/settings_bloc.dart b/lib/feature/settings/settings_bloc.dart new file mode 100644 index 0000000..ea88d57 --- /dev/null +++ b/lib/feature/settings/settings_bloc.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'profile/profile_model.dart'; +import '../../product/base/bloc/base_bloc.dart'; + +class SettingsBloc extends BlocBase { +// Settings Screen + final userProfile = StreamController.broadcast(); + StreamSink get sinkUserProfile => userProfile.sink; + Stream get streamUserProfile => userProfile.stream; + + +// Profile Screen + final isChangeProfileInfomation = StreamController.broadcast(); + StreamSink get sinkIsChangeProfileInfomation => + isChangeProfileInfomation.sink; + Stream get streamIsChangeProfileInfomation => + isChangeProfileInfomation.stream; + + + + @override + void dispose() { + } +} diff --git a/lib/feature/settings/settings_screen.dart b/lib/feature/settings/settings_screen.dart new file mode 100644 index 0000000..0ff5c12 --- /dev/null +++ b/lib/feature/settings/settings_screen.dart @@ -0,0 +1,153 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../product/constant/app/app_constants.dart'; +import 'profile/profile_screen.dart'; +import '../../product/constant/icon/icon_constants.dart'; +import '../../product/extention/context_extention.dart'; +import '../../product/services/api_services.dart'; +import 'profile/profile_model.dart'; +import 'settings_bloc.dart'; +import '../../product/base/bloc/base_bloc.dart'; +import '../../product/services/language_services.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + late SettingsBloc settingsBloc; + User user = User(); + APIServices apiServices = APIServices(); + @override + void initState() { + super.initState(); + settingsBloc = BlocProvider.of(context); + getUserProfile(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(appLocalization(context).profile_page_title), + centerTitle: true, + ), + body: StreamBuilder( + stream: settingsBloc.streamUserProfile, + initialData: user, + builder: (context, userSnapshot) { + return userSnapshot.data?.id == "" || user.id == "" + ? Center( + child: CircularProgressIndicator( + value: context.highValue, + ), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: 70, + child: CircleAvatar( + radius: 60, + child: CircleAvatar( + radius: 50, + child: Text( + getAvatarContent( + userSnapshot.data?.username ?? ""), + style: const TextStyle( + fontSize: 35, fontWeight: FontWeight.bold), + ), + ), + ), + ), + SizedBox(height: context.lowValue), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + userSnapshot.data?.name ?? "User Name", + style: const TextStyle( + fontWeight: FontWeight.w900, fontSize: 26), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [Text(userSnapshot.data?.email ?? "Email")], + ), + SizedBox(height: context.mediumValue), + cardContent( + Icons.account_circle_rounded, + appLocalization(context).profile_change_info, + ), + SizedBox(height: context.lowValue), + cardContent( + Icons.lock_outline, + appLocalization(context).profile_change_pass, + ), + SizedBox(height: context.lowValue), + cardContent( + Icons.settings_outlined, + appLocalization(context).profile_setting, + ), + SizedBox(height: context.lowValue), + cardContent( + Icons.logout_outlined, + appLocalization(context).log_out, + ), + ], + ); + }), + ); + } + + cardContent(IconData icon, String content) { + return GestureDetector( + onTap: () async { + if (icon == Icons.account_circle_rounded) { + changeUserInfomation(context, user, settingsBloc); + } else if (icon == Icons.lock_outline) { + changeUserPassword(context, settingsBloc); + } else if (icon == Icons.settings_outlined) { + context.push(ApplicationConstants.DEVICE_NOTIFICATIONS_SETTINGS); + } else { + await apiServices.logOut(context); + } + }, + child: Card( + margin: const EdgeInsets.only(left: 35, right: 35, bottom: 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + child: ListTile( + leading: IconConstants.instance.getMaterialIcon(icon), + title: Text( + content, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + trailing: const Icon( + Icons.arrow_forward_ios_outlined, + ), + ), + ), + ); + } + + void getUserProfile() async { + String data = await apiServices.getUserDetail(); + user = User.fromJson(jsonDecode(data)); + settingsBloc.sinkUserProfile.add(user); + } + + String getAvatarContent(String username) { + String name = ""; + if (username.isNotEmpty) { + name = username[0].toUpperCase(); + } + return name; + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..ee20712 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,86 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// 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 { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyCD8s_CD1dzXuY2EdgzulRJqZV7TY-1chY', + appId: '1:910110439150:web:b3dc22fa9ecb953b5b65ff', + messagingSenderId: '910110439150', + projectId: 'sfm-notification', + authDomain: 'sfm-notification.firebaseapp.com', + storageBucket: 'sfm-notification.firebasestorage.app', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyADdZ66_q4HALlb0-yEGgeyGnsbwmlvDsA', + appId: '1:910110439150:android:c3dbc3b4a85d7cb75b65ff', + messagingSenderId: '910110439150', + projectId: 'sfm-notification', + storageBucket: 'sfm-notification.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyCpcnAulIA5fxrSM0uHSf0uWSzzlM208Fw', + appId: '1:910110439150:ios:1d111f0cdd0240a35b65ff', + messagingSenderId: '910110439150', + projectId: 'sfm-notification', + storageBucket: 'sfm-notification.firebasestorage.app', + iosBundleId: 'com.example.sfmApp', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyCpcnAulIA5fxrSM0uHSf0uWSzzlM208Fw', + appId: '1:910110439150:ios:1d111f0cdd0240a35b65ff', + messagingSenderId: '910110439150', + projectId: 'sfm-notification', + storageBucket: 'sfm-notification.firebasestorage.app', + iosBundleId: 'com.example.sfmApp', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyCD8s_CD1dzXuY2EdgzulRJqZV7TY-1chY', + appId: '1:910110439150:web:7c413e6bc22555575b65ff', + messagingSenderId: '910110439150', + projectId: 'sfm-notification', + authDomain: 'sfm-notification.firebaseapp.com', + storageBucket: 'sfm-notification.firebasestorage.app', + ); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..b869359 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,80 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:sfm_app/product/services/language_services.dart'; +import 'feature/main/main_bloc.dart'; +import 'product/base/bloc/base_bloc.dart'; +import 'product/constant/navigation/navigation_router.dart'; +import 'product/theme/provider/app_provider.dart'; +import 'product/theme/theme_notifier.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + runApp( + MultiProvider( + providers: [...ApplicationProvider.instance.dependItems], + child: BlocProvider( + child: const MyApp(), + blocBuilder: () => MainBloc(), + ), + ), + ); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); + static void setLocale(BuildContext context, Locale newLocale) { + _MyAppState? state = context.findAncestorStateOfType<_MyAppState>(); + state?.setLocale(newLocale); + } +} + +class _MyAppState extends State { + Locale? _locale; + late MainBloc mainBloc; + LanguageServices languageServices = LanguageServices(); + // late ThemeNotifier themeNotifier; + setLocale(Locale locale) { + _locale = locale; + mainBloc.sinkLanguage.add(_locale); + } + + @override + void initState() { + super.initState(); + mainBloc = BlocProvider.of(context); + // themeNotifier = Provider.of(context, listen: false); + // ThemeNotifier().loadThemeFromPreferences(); + // log("ThemeKey1: ${LocaleManager.instance.getStringValue(PreferencesKeys.THEME)}"); + // log("Date: ${DateTime.parse("2024-11-16T07:17:36.785Z")}"); + } + + @override + void didChangeDependencies() { + languageServices.getLocale().then((locale) => {setLocale(locale)}); + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + final router = goRouter(); + return StreamBuilder( + stream: mainBloc.streamLanguage, + initialData: _locale, + builder: (context, languageSnapshot) { + return MaterialApp.router( + theme: context.watch().currentTheme, + routerConfig: router, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: languageSnapshot.data, + ); + }, + ); + } +} diff --git a/lib/product/base/bloc/base_bloc.dart b/lib/product/base/bloc/base_bloc.dart new file mode 100644 index 0000000..0f80078 --- /dev/null +++ b/lib/product/base/bloc/base_bloc.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +typedef BlocBuilder = T Function(); +typedef BlocDisposer = Function(T); + +abstract class BlocBase { + void dispose(); +} + +class _BlocProviderInherited extends InheritedWidget { + const _BlocProviderInherited({ + Key? key, + required Widget child, + required this.bloc, + }) : super(key: key, child: child); + + final T bloc; + + @override + bool updateShouldNotify(_BlocProviderInherited oldWidget) => false; +} + +class BlocProvider extends StatefulWidget { + const BlocProvider({ + Key? key, + required this.child, + required this.blocBuilder, + this.blocDispose, + }) : super(key: key); + + final Widget child; + final BlocBuilder blocBuilder; + final BlocDisposer? blocDispose; + + @override + BlocProviderState createState() => BlocProviderState(); + + static T of(BuildContext context) { + _BlocProviderInherited provider = context + .getElementForInheritedWidgetOfExactType<_BlocProviderInherited>() + ?.widget as _BlocProviderInherited; + return provider.bloc; + } +} + +class BlocProviderState extends State> { + late T bloc; + + @override + void initState() { + super.initState(); + bloc = widget.blocBuilder(); + } + + @override + void dispose() { + if (widget.blocDispose != null) { + widget.blocDispose!(bloc); + } else { + bloc.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _BlocProviderInherited( + bloc: bloc, + child: widget.child, + ); + } +} diff --git a/lib/product/base/screen/base_screen.dart b/lib/product/base/screen/base_screen.dart new file mode 100644 index 0000000..7f83b0c --- /dev/null +++ b/lib/product/base/screen/base_screen.dart @@ -0,0 +1,142 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +abstract class BasePageScreen extends StatefulWidget { + const BasePageScreen({Key? key}) : super(key: key); +} + +abstract class BasePageScreenState extends State { + bool _isBack = true; + bool _isHome = true; + bool _isShowTitle = true; + + final _loadingController = StreamController(); + + String appBarTitle(); + String appBarSubTitle(); + + void onClickBackButton(); + + void onClickHome(); + + void isBackButton(bool isBack) { + _isBack = isBack; + } + + void isHomeButton(bool isHome) { + _isHome = isHome; + } + + void isShowTitle(bool isShowTitle) { + _isShowTitle = isShowTitle; + } + + void showLoading() { + _loadingController.add(true); + } + + void hideLoading() { + _loadingController.add(false); + } +} + +mixin BaseScreen on BasePageScreenState { + Widget body(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + // flexibleSpace: Container( + // decoration: const BoxDecoration( + // image: DecorationImage( + // image: AssetImage('assets/images/bg_header.jpeg'), + // fit: BoxFit.cover)), + // ), + title: Column( + children: [ + Visibility( + visible: _isShowTitle, + child: Text( + appBarTitle(), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + Text( + appBarSubTitle(), + maxLines: _isShowTitle ? 1 : 2, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ) + ], + ), + leading: _isBack + ? IconButton( + icon: const Icon( + Icons.arrow_back_ios, + color: Colors.white, + ), + onPressed: () { + // onClickBackButton(); + Navigator.of(context).pushNamedAndRemoveUntil( + "/main", (Route route) => false); + }, + ) + : Container(), + actions: [ + _isHome + ? IconButton( + icon: const Icon( + Icons.home, + color: Colors.white, + ), + onPressed: () { + onClickHome(); + }, + ) + : Container() + ], + ), + body: Stack( + children: [ + Container( + height: double.infinity, + color: Colors.white, + child: body(), + ), + StreamBuilder( + stream: _loadingController.stream, + builder: (_, snapshot) { + return snapshot.data == true + ? Positioned.fill( + child: _buildLoader(), + ) + : const SizedBox(); + }, + ), + ], + )); + } + + Widget _buildLoader() { + return Container( + color: Colors.black54, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + } + + @override + void dispose() { + _loadingController.close(); + super.dispose(); + } +} diff --git a/lib/product/base/widget/dialog/request_permission_dialog.dart b/lib/product/base/widget/dialog/request_permission_dialog.dart new file mode 100644 index 0000000..91e4d0d --- /dev/null +++ b/lib/product/base/widget/dialog/request_permission_dialog.dart @@ -0,0 +1,61 @@ +import 'package:app_settings/app_settings.dart'; +import 'package:flutter/material.dart'; +import '../../../extention/context_extention.dart'; +import '../../../services/language_services.dart'; + +class RequestPermissionDialog { + showRequestPermissionDialog(BuildContext context, IconData icon, + String dialogContent, AppSettingsType type) { + showDialog( + useRootNavigator: false, + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Center( + child: Icon(icon), + ), + content: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: context.paddingNormalVertical, + child: Text( + dialogContent, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 18), + ), + ), + Divider(height: context.lowValue), + GestureDetector( + onTap: () { + AppSettings.openAppSettings(type: type); + }, + child: Padding( + padding: + context.paddingNormalVertical, // Cách giữa các phần tử + child: Text(appLocalization(context).allow_message, + style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + Divider(height: context.lowValue), + GestureDetector( + onTap: () { + Navigator.of(dialogContext).pop(); // Đóng dialog + }, + child: Padding( + padding: + context.paddingNormalVertical, // Cách giữa các phần tử + child: Text(appLocalization(context).decline_message, + style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/product/cache/local_manager.dart b/lib/product/cache/local_manager.dart new file mode 100644 index 0000000..500596d --- /dev/null +++ b/lib/product/cache/local_manager.dart @@ -0,0 +1,42 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +import '../constant/enums/local_keys_enums.dart'; + +class LocaleManager { + LocaleManager._init() { + SharedPreferences.getInstance().then((value) { + _preferences = value; + }); + } + static final LocaleManager _instance = LocaleManager._init(); + + SharedPreferences? _preferences; + + static LocaleManager get instance => _instance; + + static Future prefrencesInit() async { + instance._preferences ??= await SharedPreferences.getInstance(); + } + + void setString(PreferencesKeys key, String value) async { + await _preferences?.setString(key.toString(), value); + } + + void setInt(PreferencesKeys key, int value) async { + await _preferences?.setInt(key.toString(), value); + } + + Future setStringValue(PreferencesKeys key, String value) async { + await _preferences!.setString(key.toString(), value); + } + + String getStringValue(PreferencesKeys key) => + _preferences?.getString(key.toString()) ?? ''; + + int getIntValue(PreferencesKeys key) => + _preferences?.getInt(key.toString()) ?? 0; + + Future deleteStringValue(PreferencesKeys key) async { + await _preferences!.remove(key.toString()); + } +} diff --git a/lib/product/constant/app/api_path_constant.dart b/lib/product/constant/app/api_path_constant.dart new file mode 100644 index 0000000..bcdbeb5 --- /dev/null +++ b/lib/product/constant/app/api_path_constant.dart @@ -0,0 +1,24 @@ +// ignore_for_file: constant_identifier_names + +class APIPathConstants { + static const LOGIN_PATH = "/api/login"; + static const LOGOUT_PATH = "/api/logout"; + static const BELL_NOTIFICATIONS_PATH = "/api/notifications/bell-list"; + static const BELL_UPDATE_READ_NOTIFICATIONS_PATH = + "/api/notifications/bell-read"; + static const DEVICE_NOTIFICATION_SETTINGS = "/api/users/my-device-settings"; + static const DASHBOARD_DEVICES = "/api/dashboard/devices"; + static const USER_PATH = "/api/users"; + static const USER_PROFILE_PATH = "/api/users/profile"; + static const PROVINCES_PATH = "/api/vn/provinces"; + static const DISTRICTS_PATH = "/api/vn/districts"; + static const WARDS_PATH = "/api/vn/wards"; + static const DEVICE_PATH = "/api/devices"; + static const DEVICE_REGISTER_PATH = "/api/devices/register"; + static const DEVICE_UNREGISTER_PATH = "/api/devices/unregister"; + static const ALL_GROUPS_PATH = "/api/groups/user"; + static const GROUPS_PATH = "/api/groups"; + static const JOIN_GROUP_PATH = "/api/groups/join"; + static const APPROVE_GROUP_PATH = "/api/groups/approve"; + static const DEVICE_LOGS_PATH = "/api/device-logs"; +} diff --git a/lib/product/constant/app/app_constants.dart b/lib/product/constant/app/app_constants.dart new file mode 100644 index 0000000..94eab82 --- /dev/null +++ b/lib/product/constant/app/app_constants.dart @@ -0,0 +1,21 @@ +// ignore_for_file: constant_identifier_names + +class ApplicationConstants { + static const APP_NAME = "Smatec SFM"; + static const DOMAIN = "sfm.smatec.com.vn"; + static const MAP_KEY = "AIzaSyDI8b-PUgKUgj5rHdtgEHCwWjUXYJrqYhE"; + static const LOGIN_PATH = "/login"; + static const LOGOUT_PATH = "/logout"; + static const HOME_PATH = "/"; + static const SETTINGS_PATH = "/settings"; + static const BELL_PATH = "/bell"; + static const DEVICES_MANAGER_PATH = "/devices-manager"; + static const DEVICES_UPDATE_PATH = "/device-update"; + static const DEVICES_DETAIL_PATH = "/device"; + static const MAP_PATH = "/map"; + static const DEVICE_LOGS_PATH = "/device-logs"; + static const GROUP_PATH = "/groups"; + static const DEVICE_NOTIFICATIONS_SETTINGS = "/device-notifications-settings"; + static const OWNER_GROUP = "owner"; + static const PARTICIPANT_GROUP = "participant"; +} diff --git a/lib/product/constant/enums/app_route_enums.dart b/lib/product/constant/enums/app_route_enums.dart new file mode 100644 index 0000000..0f3712a --- /dev/null +++ b/lib/product/constant/enums/app_route_enums.dart @@ -0,0 +1,16 @@ +// ignore_for_file: constant_identifier_names + +enum AppRoutes { + LOGIN, + HOME, + SETTINGS, + DEVICE_NOTIFICATION_SETTINGS, + BELL, + DEVICES, + DEVICE_UPDATE, + DEVICE_DETAIL, + MAP, + HISTORY, + GROUPS, + GROUP_DETAIL, +} diff --git a/lib/product/constant/enums/app_theme_enums.dart b/lib/product/constant/enums/app_theme_enums.dart new file mode 100644 index 0000000..a5af3bd --- /dev/null +++ b/lib/product/constant/enums/app_theme_enums.dart @@ -0,0 +1,3 @@ +// ignore_for_file: constant_identifier_names + +enum AppThemes { LIGHT, DARK, SYSTEM } diff --git a/lib/product/constant/enums/local_keys_enums.dart b/lib/product/constant/enums/local_keys_enums.dart new file mode 100644 index 0000000..03cce61 --- /dev/null +++ b/lib/product/constant/enums/local_keys_enums.dart @@ -0,0 +1,10 @@ +// ignore_for_file: constant_identifier_names + +enum PreferencesKeys{ + TOKEN, + UID, + EXP, + ROLE, + LANGUAGE_CODE, + THEME +} \ No newline at end of file diff --git a/lib/product/constant/enums/role_enums.dart b/lib/product/constant/enums/role_enums.dart new file mode 100644 index 0000000..d21e63e --- /dev/null +++ b/lib/product/constant/enums/role_enums.dart @@ -0,0 +1,7 @@ +// ignore_for_file: constant_identifier_names + +enum RoleEnums{ + USER, + ADMIN, + MOD +} \ No newline at end of file diff --git a/lib/product/constant/icon/icon_constants.dart b/lib/product/constant/icon/icon_constants.dart new file mode 100644 index 0000000..37e22f1 --- /dev/null +++ b/lib/product/constant/icon/icon_constants.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class IconConstants { + IconConstants._init(); + static IconConstants? _instance; + static IconConstants get instance => _instance ??= IconConstants._init(); + + String get logo => getIcon(""); + + String getIcon(String name) => "assets/icons/$name.png"; + + Icon getMaterialIcon(IconData icon) => Icon(icon); +} diff --git a/lib/product/constant/image/image_constants.dart b/lib/product/constant/image/image_constants.dart new file mode 100644 index 0000000..bc889c0 --- /dev/null +++ b/lib/product/constant/image/image_constants.dart @@ -0,0 +1,9 @@ +class ImageConstants { + ImageConstants._init(); + static ImageConstants? _instance; + static ImageConstants get instance => _instance ??= ImageConstants._init(); + + String get logo => getImage(""); + + String getImage(String name) => "assets/images/$name.png"; +} diff --git a/lib/product/constant/lang/language_constants.dart b/lib/product/constant/lang/language_constants.dart new file mode 100644 index 0000000..a50076c --- /dev/null +++ b/lib/product/constant/lang/language_constants.dart @@ -0,0 +1,6 @@ +// ignore_for_file: constant_identifier_names + +class LanguageConstants { + static const ENGLISH = "en"; + static const VIETNAM = "vi"; +} diff --git a/lib/product/constant/navigation/navigation_router.dart b/lib/product/constant/navigation/navigation_router.dart new file mode 100644 index 0000000..0053824 --- /dev/null +++ b/lib/product/constant/navigation/navigation_router.dart @@ -0,0 +1,156 @@ +import 'package:go_router/go_router.dart'; +import 'package:sfm_app/feature/devices/device_detail/device_detail_bloc.dart'; +import 'package:sfm_app/feature/devices/device_detail/device_detail_screen.dart'; +import 'package:sfm_app/feature/settings/device_notification_settings/device_notification_settings_bloc.dart'; +import 'package:sfm_app/feature/settings/device_notification_settings/device_notification_settings_screen.dart'; +import '../app/app_constants.dart'; +import '../../../feature/auth/login/bloc/login_bloc.dart'; +import '../../../feature/auth/login/screen/login_screen.dart'; +import '../../../feature/bell/bell_bloc.dart'; +import '../../../feature/bell/bell_screen.dart'; +import '../../../feature/devices/device_update/device_update_bloc.dart'; +import '../../../feature/devices/device_update/device_update_screen.dart'; +import '../../../feature/devices/devices_manager_bloc.dart'; +import '../../../feature/devices/devices_manager_screen.dart'; +import '../../../feature/error/not_found_screen.dart'; +import '../../../feature/inter_family/group_detail/group_detail_bloc.dart'; +import '../../../feature/inter_family/group_detail/group_detail_screen.dart'; +import '../../../feature/inter_family/inter_family_bloc.dart'; +import '../../../feature/inter_family/inter_family_screen.dart'; +import '../../../feature/log/device_logs_bloc.dart'; +import '../../../feature/log/device_logs_screen.dart'; +import '../../../feature/main/main_bloc.dart'; +import '../../../feature/main/main_screen.dart'; +import '../../../feature/map/map_bloc.dart'; +import '../../../feature/map/map_screen.dart'; +import '../../../feature/settings/settings_bloc.dart'; +import '../../../feature/settings/settings_screen.dart'; +import '../../../product/base/bloc/base_bloc.dart'; +import '../enums/app_route_enums.dart'; +import '../../../product/shared/shared_transition.dart'; + +GoRouter goRouter() { + return GoRouter( + debugLogDiagnostics: true, + errorBuilder: (context, state) => const NotFoundScreen(), + initialLocation: ApplicationConstants.LOGIN_PATH, + routes: [ + GoRoute( + path: ApplicationConstants.LOGIN_PATH, + name: AppRoutes.LOGIN.name, + builder: (context, state) => BlocProvider( + child: const LoginScreen(), + blocBuilder: () => LoginBloc(), + ), + ), + GoRoute( + path: ApplicationConstants.HOME_PATH, + name: AppRoutes.HOME.name, + builder: (context, state) => BlocProvider( + child: const MainScreen(), + blocBuilder: () => MainBloc(), + ), + ), + GoRoute( + path: ApplicationConstants.SETTINGS_PATH, + name: AppRoutes.SETTINGS.name, + pageBuilder: (context, state) => CustomTransitionPage( + child: BlocProvider( + child: const SettingsScreen(), + blocBuilder: () => SettingsBloc(), + ), + transitionsBuilder: transitionsBottomToTop, + ), + ), + GoRoute( + path: ApplicationConstants.BELL_PATH, + name: AppRoutes.BELL.name, + pageBuilder: (context, state) => CustomTransitionPage( + child: BlocProvider( + child: const BellScreen(), + blocBuilder: () => BellBloc(), + ), + transitionsBuilder: transitionsCustom1), + ), + GoRoute( + path: ApplicationConstants.DEVICES_MANAGER_PATH, + name: AppRoutes.DEVICES.name, + builder: (context, state) => BlocProvider( + child: const DevicesManagerScreen(), + blocBuilder: () => DevicesManagerBloc(), + ), + ), + GoRoute( + path: '${ApplicationConstants.DEVICES_UPDATE_PATH}/:thingID', + name: AppRoutes.DEVICE_UPDATE.name, + pageBuilder: (context, state) => CustomTransitionPage( + child: BlocProvider( + child: DeviceUpdateScreen( + thingID: state.pathParameters['thingID']!, + ), + blocBuilder: () => DeviceUpdateBloc(), + ), + transitionsBuilder: transitionsBottomToTop), + ), + GoRoute( + path: '${ApplicationConstants.DEVICES_DETAIL_PATH}/:thingID', + name: AppRoutes.DEVICE_DETAIL.name, + pageBuilder: (context, state) => CustomTransitionPage( + child: BlocProvider( + child: DetailDeviceScreen( + thingID: state.pathParameters['thingID']!, + ), + blocBuilder: () => DetailDeviceBloc(), + ), + transitionsBuilder: transitionsRightToLeft), + ), + GoRoute( + path: ApplicationConstants.MAP_PATH, + name: AppRoutes.MAP.name, + builder: (context, state) => BlocProvider( + child: const MapScreen(), + blocBuilder: () => MapBloc(), + ), + ), + GoRoute( + path: ApplicationConstants.DEVICE_LOGS_PATH, + name: AppRoutes.HISTORY.name, + builder: (context, state) => BlocProvider( + child: const DeviceLogsScreen(), + blocBuilder: () => DeviceLogsBloc(), + ), + ), + GoRoute( + path: ApplicationConstants.GROUP_PATH, + name: AppRoutes.GROUPS.name, + builder: (context, state) => BlocProvider( + child: const InterFamilyScreen(), + blocBuilder: () => InterFamilyBloc(), + ), + ), + GoRoute( + path: '${ApplicationConstants.GROUP_PATH}/:groupId', + name: AppRoutes.GROUP_DETAIL.name, + pageBuilder: (context, state) { + final groupId = state.pathParameters['groupId']!; + final role = state.extra! as String; + return CustomTransitionPage( + child: BlocProvider( + child: DetailGroupScreen(group: groupId, role: role), + blocBuilder: () => DetailGroupBloc(), + ), + transitionsBuilder: transitionsRightToLeft); + }), + GoRoute( + path: ApplicationConstants.DEVICE_NOTIFICATIONS_SETTINGS, + name: AppRoutes.DEVICE_NOTIFICATION_SETTINGS.name, + pageBuilder: (context, state) => CustomTransitionPage( + child: BlocProvider( + child: const DeviceNotificationSettingsScreen(), + blocBuilder: () => DeviceNotificationSettingsBloc(), + ), + transitionsBuilder: transitionsRightToLeft), + ), + ], + ); +} diff --git a/lib/product/constant/status_code/status_code_constants.dart b/lib/product/constant/status_code/status_code_constants.dart new file mode 100644 index 0000000..5a6873a --- /dev/null +++ b/lib/product/constant/status_code/status_code_constants.dart @@ -0,0 +1,7 @@ +// ignore_for_file: constant_identifier_names + +class StatusCodeConstants { + static const CREATED = 201; + static const OK = 200; + static const BAD_REQUEST = 400; +} diff --git a/lib/product/extention/context_extention.dart b/lib/product/extention/context_extention.dart new file mode 100644 index 0000000..75eaaab --- /dev/null +++ b/lib/product/extention/context_extention.dart @@ -0,0 +1,103 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../theme/app_theme_light.dart'; + +// MEDIA +extension ContextExtension on BuildContext { + MediaQueryData get mediaQuery => MediaQuery.of(this); +} + +// VALUES +extension MediaQueryExtension on BuildContext { + double get height => mediaQuery.size.height; + double get width => mediaQuery.size.width; + + double get lowValue => height * 0.01; + double get normalValue => height * 0.02; + double get mediumValue => height * 0.04; + double get highValue => height * 0.1; + + double dynamicWidth(double val) => width * val; + double dynamicHeight(double val) => height * val; +} + +// THEME +extension ThemeExtension on BuildContext { + ThemeData get theme => Theme.of(this); + TextTheme get textTheme => theme.textTheme; + ColorScheme get colors => AppThemeLight.instance.theme.colorScheme; +} + +// PADDING ALLL +extension PaddingExtensionAll on BuildContext { + EdgeInsets get paddingLow => EdgeInsets.all(lowValue); + EdgeInsets get paddingNormal => EdgeInsets.all(normalValue); + EdgeInsets get paddingMedium => EdgeInsets.all(mediumValue); + EdgeInsets get paddingHigh => EdgeInsets.all(highValue); + EdgeInsets dynamicPadding(double val) => EdgeInsets.all(val); + // double dynamicPadding(double val) => height * val; +} + +// PADDING SYMETRIC +extension PaddingExtensionSymetric on BuildContext { + // VERTICAL PADDİNG + EdgeInsets get paddingLowVertical => EdgeInsets.symmetric(vertical: lowValue); + EdgeInsets get paddingNormalVertical => + EdgeInsets.symmetric(vertical: normalValue); + EdgeInsets get paddingMediumVertical => + EdgeInsets.symmetric(vertical: mediumValue); + EdgeInsets get paddingHighVertical => + EdgeInsets.symmetric(vertical: highValue); + + // HORIZONTAL PADDİNG + EdgeInsets get paddingLowHorizontal => + EdgeInsets.symmetric(horizontal: lowValue); + EdgeInsets get paddingNormalHorizontal => + EdgeInsets.symmetric(horizontal: normalValue); + EdgeInsets get paddingMediumHorizontal => + EdgeInsets.symmetric(horizontal: mediumValue); + EdgeInsets get paddingHighHorizontal => + EdgeInsets.symmetric(horizontal: highValue); +} + +// RANDOM COLOR +extension PageExtension on BuildContext { + Color get randomColor => Colors.primaries[Random().nextInt(17)]; +} + +// DURATION +extension DurationExtension on BuildContext { + Duration get lowDuration => const Duration(milliseconds: 150); + Duration get normalDuration => const Duration(milliseconds: 500); + Duration dynamicSecondDuration(int seconds) => Duration(seconds: seconds); + Duration dynamicMinutesDuration(int minutes) => Duration(minutes: minutes); + +} + +// RADIUS +extension RadiusExtension on BuildContext { + Radius get lowRadius => Radius.circular(width * 0.02); + Radius get normalRadius => Radius.circular(width * 0.05); + Radius get highRadius => Radius.circular(width * 0.1); +} + +extension TextStyleExtention on BuildContext { + TextStyle get labelSmallTextStyle => Theme.of(this).textTheme.labelSmall!; + TextStyle get labelMediumTextStyle => Theme.of(this).textTheme.labelMedium!; + TextStyle get labelLargeTextStyle => Theme.of(this).textTheme.labelLarge!; + TextStyle get bodySmallTextStyle => Theme.of(this).textTheme.bodySmall!; + TextStyle get bodyMediumTextStyle => Theme.of(this).textTheme.bodyMedium!; + TextStyle get bodyLargeTextStyle => Theme.of(this).textTheme.bodyLarge!; + TextStyle get titleSmallTextStyle => Theme.of(this).textTheme.titleSmall!; + TextStyle get titleMediumTextStyle => Theme.of(this).textTheme.titleMedium!; + TextStyle get titleLargeTextStyle => Theme.of(this).textTheme.titleLarge!; + TextStyle get headlineSmallTextStyle => + Theme.of(this).textTheme.headlineSmall!; + TextStyle get headlineMediumTextStyle => + Theme.of(this).textTheme.headlineMedium!; + TextStyle get headlineLargeTextStyle => + Theme.of(this).textTheme.headlineLarge!; +} + diff --git a/lib/product/lang/l10n/app_en.arb b/lib/product/lang/l10n/app_en.arb new file mode 100644 index 0000000..508bf46 --- /dev/null +++ b/lib/product/lang/l10n/app_en.arb @@ -0,0 +1,242 @@ +{ + "description_NOTUSE": "This is english language in HomePage", + "home_page_name": "Home Page", + "vietnam_language": "Vietnamese", + "english_language": "English", + "notification": "Notifications:", + "profile_icon_title": "Settings", + "log_out": "Log out", + "log_out_content": "Are you sure you want to log out?", + "notification_description": "All devices are operating normally", + "button_fake_fire_message": "False fire alarm", + "in_progress_message": "In progress", + "smoke_detecting_message": "Smoke detecting!", + "smoke_detecting_message_lowercase": "smoke detecting!", + "disconnect_message_uppercase": "Disconnected", + "disconnect_message_lowercase": "disconnected", + "location_message": "Address: ", + "confirm_fake_fire_message": "Are you sure the fire is a false alarm?", + "confirm_fake_fire_body": "Please check carefully to ensure that this is just a normal incident. The fire department will confirm that this is a false alarm!", + "confirm_fake_fire_sure_message": "I''m sure", + "let_PCCC_handle_message": "Let the Fire Prevention and Fighting Team handle it!", + "overview_message": "Overview", + "total_nof_devices_message": "Total number of devices", + "active_devices_message": "Active", + "inactive_devices_message": "Inactive", + "warning_devices_message": "Warning", + "unused_devices_message": "Unused", + "description_NOTUSE1": "This is english language in DeviceManagerPage", + "device_manager_page_name": "Devices Manager", + "add_device_title": "Add new device", + "input_extID_device_input": "Devcice ID", + "input_extID_device_hintText": "Enter the device ID", + "input_name_device_device": "Device Name", + "input_name_device_hintText": "Enter the device Name", + "paginated_data_table_title": "List of devices", + "paginated_data_table_column_action": "Action", + "paginated_data_table_column_deviceName": "Device name", + "paginated_data_table_column_deviceStatus": "Status", + "paginated_data_table_column_deviceBaterry": "Battery", + "paginated_data_table_column_deviceSignal": "Signal", + "paginated_data_table_column_deviceTemperature": "Temperature", + "paginated_data_table_column_deviceHump": "Humidity", + "paginated_data_table_column_devicePower": "Power", + "delete_device_dialog_title": "Remove device", + "delete_device_dialog_content": "Are you sure you want to delete this device?", + "update_device_dialog_title": "Update device", + "update_device_dialog_location_title": "Device location", + "update_device_dialog_location_longitude": "Longitude", + "update_device_dialog_location_latitude": "Latitude", + "update_device_dialog_location_longitude_hintText": "Enter longitude", + "update_device_dialog_location_latitude_hintText": "Enter latitude", + "update_device_dialog_location_province_hintText": "Select Province/City", + "update_device_dialog_location_province_searchHint": "Find Province/City", + "update_device_dialog_location_district_hintText": "Select District", + "update_device_dialog_location_district_searchHint": "Find district", + "update_device_dialog_location_ward_hintText": "Select Ward/Commune", + "update_device_dialog_location_ward_searchHint": "Find Ward/Commune", + "update_device_dialog_maps_dialog_title": "Update location", + "update_device_dialog_search_location_hint": "Search Location", + "description_NOTUSE8": "This is english language in MapPositionPage", + "map_your_location": "Your Location", + "map_show_direction": "Give directions", + "map_nearby_hospital": "Nearby hospital", + "map_nearest_hospital": "Nearest hospital", + "map_nearby_firestation": "Nearby fire station", + "map_nearest_firestation": "Nearest fire station", + "map_result": "Result", + "map_always_opened": "Always open", + "map_openning": "Openning", + "map_closed": "Closed", + "map_no_results": "No results found", + "map_start": "Start", + "map_destination": "Destination", + "map_stream": "Stream", + "description_NOTUSE2": "This is english language in DeviceLogPage", + "device_log_page_name": "Devices Log", + "choose_device_dropdownButton": "Select device", + "choose_date_start_datePicker": "Start from", + "choose_date_end_datePicker": "End", + "main_no_data": "No data yet.", + "description_NOTUSE3": "This is english language in InterFamily", + "interfamily_page_name": "InterFamily", + "my_group_title": "My group", + "invite_group": "Joined group", + "add_new_group": "Add new group", + "join_group": "Join group", + "group_name_title": "Group Name", + "group_id_title": "Group ID", + "add_new_user_title": "Add user", + "share_group_title": "Share group", + "change_group_infomation_title": "Change Infomation", + "change_group_infomation_content": "Change group infomation", + "delete_group_title": "Delete group", + "delete_group_content": "Are you sure you want to delete this group?", + "leave_group_content": "Are you sure you want to leave this group?", + "dont_have_group": "No group yet", + "dont_join_group": "You haven''t joined any groups yet.", + "description_group": "Description", + "add_new_device_title": "Add new device", + "approve_user": "Approve members", + "devices_title": "Devices", + "device_title": "Device", + "member_title": "Members", + "leave_group_title": "Leave group", + "dont_have_device": "No device yet", + "description_NOTUSE4": "This is english language in ProfilePage", + "profile_page_title": "Settings Page", + "profile_change_info": "Change information", + "profile_change_pass": "Change password", + "profile_setting": "Notification Setting", + "change_profile_title": "Personal information", + "change_profile_username": "Username: ", + "change_profile_username_hint": "Enter username ", + "change_profile_email": "Email: ", + "change_profile_email_hint": "Enter email ", + "change_profile_email_not_empty": "Email cannot be empty", + "change_profile_tel": "Phone number: ", + "change_profile_tel_hint": "Enter phone number", + "change_profile_tel_not_empty": "Phone number cannot be empty", + "change_profile_address": "Address: ", + "change_profile_address_hint": "Enter address", + "change_profile_old_pass": "Password: ", + "change_profile_old_pass_hint": "Enter password", + "change_profile_old_pass_not_empty": "Old password cannot be empty", + "change_profile_new_pass": "New password: ", + "change_profile_new_pass_hint": "Enter new password", + "change_profile_new_pass_not_empty": "New password cannot be empty", + "change_profile_device_notification_select_all": "Select all", + "change_profile_device_notification_deselect_all": "Deselect all", + "description_NOTUSE5": "This is english language in BellPage", + "bell_page_title": "Notifications", + "bell_page_no_items_body": "No notifications yet", + "bell_user_uppercase": "User", + "bell_battery_device": "Device Battery", + "bell_user_joined_group": "joined group", + "bell_leave_group": "left group", + "bell_user_added_group": "added to the group", + "bell_user_kick_group": "removed from the group", + "bell_operate_normal": "operating normally", + "bell_invalid_code": "Invalid event code", + "bell_days_ago": "days ago", + "bell_hours_ago": "hours ago", + "bell_minutes_ago": "minutes ago", + "bell_just_now": "just now", + "bell_read_all": "You have read all the notifications", + "description_NOTUSE6": "This is english language in GlobalFunction", + "gf_newly_create_message": "Newly created", + "gf_disconnect_message": "Disconnected", + "gf_smoke_detected_message": "Smoke detected", + "gf_no_signal_message": "No Signal", + "gf_weak_signal_message": "Weak Signal", + "gf_moderate_signal_message": "Moderate signal", + "gf_good_signal_message": "Good signal", + "gf_volt_detect_message": "Voltage detected", + "gf_temp_detect_message": "Temperature detected", + "gf_hum_detect_message": "Humidity detected", + "gf_battery_detect_message": "Battery detected", + "gf_offline_message": "Offline", + "gf_in_firefighting_message": "In firefighting", + "gf_device_error_message": "Device error", + "gf_not_move_message": "Not moved", + "gf_moving_message": "Moved", + "gf_remove_from_base_message": "Removed from the base", + "gf_connected_lowercase": "connected", + "description_NOTUSE7": "This is english language in LoginPage", + "login_account_not_empty": "Account cannot be empty", + "login_account_hint": "Account", + "login_password_not_empty": "Password cannot be empty", + "login_password_hint": "Password", + "login_success_message": "Login successful", + "login_incorrect_usernameOrPass": "Incorrect account or password", + "login_button_content": "Login", + "description_NOTUSE9": "This is english language in DeviceUpdatePage", + "device_update_title": "Update Device", + "device_update_location": "Device Location", + "device_update_province": "Province/City", + "device_update_district": "District", + "device_update_ward": "Ward/Commune", + "description_NOTUSE10": "This is english language in DetailDevicePage", + "detail_device_dont_has_location_message": "No location information available yet", + "no_data_message": "No data yet", + "normal_message": "Normal", + "warning_status_message": "Warning", + "undefine_message": "Undefined", + "low_message_uppercase": "Low", + "moderate_message_uppercase": "Moderate", + "good_message_uppercase": "Good", + "low_message_lowercase": "low", + "moderate_message_lowercase": "moderate", + "good_message_lowercase": "good", + "error_message_uppercase": "Error", + "error_message_lowercase": "error", + "warning_message": "Warning: ", + "loading_message": "Loading...", + "detail_message": "Detail", + "decline_message": "DECLINE", + "allow_message": "ALLOW", + "add_button_content": "Add", + "update_button_content": "Update", + "change_button_content": "Change", + "confirm_button_content": "Confirm", + "delete_button_content": "Delete", + "cancel_button_content": "Cancel", + "find_button_content": "Find", + "home_page_destination": "Home", + "manager_page_destination": "Manager", + "map_page_destination": "Map", + "history_page_destination": "History", + "history_page_destination_tooltip": "Device history", + "group_page_destination": "Group", + "group_page_destination_tooltip": "Exchange device notifications", + "notification_enter_all_inf": "Please enter all the required information", + "notification_update_device_success": "Device update successfully", + "notification_update_device_failed": "Device update failed", + "notification_update_device_error": "Device update Error", + "notification_cannot_find_address_from_location": "Can''t find the location", + "notification_add_device_success": "Device added successfully", + "notification_add_device_failed": "Failed to add device", + "notification_create_device_success": "Device created successfully", + "notification_create_device_failed": "Failed to create device", + "notification_delete_device_success": "Device deleted successfully", + "notification_delete_device_failed": "Failed to delete device", + "notification_device_not_exist": "The device does not exist", + "notification_add_group_success": "Group created successfully", + "notification_add_group_failed": "Failed to create group", + "notification_update_group_success": "Group updated successfully", + "notification_update_group_failed": "Failed to updated group", + "notification_delete_group_success": "Group deleted successfully", + "notification_delete_group_failed": "Failed to delete group", + "notification_leave_group_success": "Leave group successfully", + "notification_leave_group_failed": "Failed to leave group", + "notification_join_request_group_success": "Group join request successful!", + "notification_join_request_group_failed": "Group join request failed!", + "notification_update_profile_success": "Update profile successfully", + "notification_update_profile_failed": "Failed to update profile", + "notification_update_password_success": "Change password successfully", + "notification_update_password_failed": "The old password does not match", + "notification_update_device_settings_success": "Device notification updated successfully", + "notification_update_device_settings_failed": "Failed to update device notification", + "notification_confirm_fake_fire_success": "Information has been updated to the Fire Station", + "notification_confirm_fake_fire_failed": "Failed to update confirm fake fire" +} diff --git a/lib/product/lang/l10n/app_vi.arb b/lib/product/lang/l10n/app_vi.arb new file mode 100644 index 0000000..f395858 --- /dev/null +++ b/lib/product/lang/l10n/app_vi.arb @@ -0,0 +1,242 @@ +{ + "description_NOTUSE": "This is VietNam language in HomePage", + "home_page_name": "Trang chủ", + "vietnam_language": "Tiếng Việt", + "english_language": "Tiếng Anh", + "notification": "Thông báo:", + "profile_icon_title": "Cài đặt", + "log_out": "Đăng xuất", + "log_out_content": "Bạn chắc chắn muốn đăng xuất?", + "notification_description": "Tất cả thiết bị hoạt động bình thường", + "button_fake_fire_message": "Cháy giả?", + "in_progress_message": "Đang xử lý", + "smoke_detecting_message": "Phát hiện khói!", + "smoke_detecting_message_lowercase": "Phát hiện khói!", + "disconnect_message_uppercase": "Mất kết nối", + "disconnect_message_lowercase": "mất kết nối", + "location_message": "Địa chỉ: ", + "confirm_fake_fire_message": "Bạn chắc chắn đám cháy là cháy giả?", + "confirm_fake_fire_body": "Bạn hãy kiểm tra thật kỹ để chắc chắn rằng đây chỉ là sự cố bình thường. Đội PCCC sẽ xác nhận đây là đám cháy giả!", + "confirm_fake_fire_sure_message": "Tôi chắc chắn", + "let_PCCC_handle_message": "Hãy để Đội PCCC xử lý!", + "overview_message": "Tổng quan", + "total_nof_devices_message": "Tổng số", + "active_devices_message": "Đang hoạt động", + "inactive_devices_message": "Đang tắt", + "warning_devices_message": "Cảnh báo", + "unused_devices_message": "Không sử dụng", + "description_NOTUSE1": "This is vietnamese language in DeviceManagerPage", + "device_manager_page_name": "Quản lý thiết bị", + "add_device_title": "Thêm thiết bị", + "input_extID_device_input": "Mã thiết bị", + "input_extID_device_hintText": "Nhập mã thiết bị", + "input_name_device_device": "Tên thiết bị", + "input_name_device_hintText": "Nhập tên thiết bị", + "paginated_data_table_title": "Danh sách thiết bị", + "paginated_data_table_column_action": "Thao tác", + "paginated_data_table_column_deviceName": "Tên thiết bị", + "paginated_data_table_column_deviceStatus": "Tình trạng", + "paginated_data_table_column_deviceBaterry": "Mức pin", + "paginated_data_table_column_deviceSignal": "Mức sóng", + "paginated_data_table_column_deviceTemperature": "Nhiệt độ", + "paginated_data_table_column_deviceHump": "Độ ẩm", + "paginated_data_table_column_devicePower": "Nguồn", + "delete_device_dialog_title": "Xóa thiết bị", + "delete_device_dialog_content": "Bạn có chắc chắn muốn xóa thiết bị này?", + "update_device_dialog_title": "Sửa thiết bị", + "update_device_dialog_location_title": "Ví trí thiết bị", + "update_device_dialog_location_longitude": "Kinh độ", + "update_device_dialog_location_latitude": "Vĩ độ", + "update_device_dialog_location_longitude_hintText": "Nhập kinh độ", + "update_device_dialog_location_latitude_hintText": "Nhập vĩ độ", + "update_device_dialog_location_province_hintText": "Chọn Tỉnh/Thành phố", + "update_device_dialog_location_province_searchHint": "Tìm Tỉnh/Thành phố", + "update_device_dialog_location_district_hintText": "Chọn Quận/Huyện", + "update_device_dialog_location_district_searchHint": "Tìm Quận/Huyện", + "update_device_dialog_location_ward_hintText": "Chọn Phường/Xã", + "update_device_dialog_location_ward_searchHint": "Tìm Phường/Xã", + "update_device_dialog_maps_dialog_title": "Cập nhật vị trí", + "update_device_dialog_search_location_hint": "Tìm kiếm địa chỉ", + "description_NOTUSE8": "This is vietnamese language in MapPositionPage", + "map_your_location": "Vị trí của bạn", + "map_show_direction": "Chỉ đường", + "map_nearby_hospital": "Bệnh viện gần đó", + "map_nearest_hospital": "Bệnh viện gần nhất", + "map_nearby_firestation": "Trạm cứu hỏa gần đó", + "map_nearest_firestation": "Trạm cứu hỏa gần nhất", + "map_result": "Kết quả", + "map_always_opened": "Luôn mở cửa", + "map_openning": "Đang mở cửa", + "map_closed": "Đóng cửa", + "map_no_results": "Không tìm thấy kết quả", + "map_start": "Xuất phát", + "map_destination": "Đích đến", + "map_stream": "Trực tiếp", + "description_NOTUSE2": "This is vietnamese language in DeviceLogPage", + "device_log_page_name": "Lịch sử thiết bị", + "choose_device_dropdownButton": "Chọn thiết bị", + "choose_date_start_datePicker": "Bắt đầu từ", + "choose_date_end_datePicker": "Kết thúc", + "main_no_data": "Chưa có dữ liệu.", + "description_NOTUSE3": "This is vietnamese language in InterFamily", + "interfamily_page_name": "Liên gia", + "my_group_title": "Group của tôi", + "invite_group": "Group tham gia", + "add_new_group": "Thêm nhóm mới", + "join_group": "Tham gia nhóm", + "group_name_title": "Tên nhóm", + "group_id_title": "ID nhóm", + "add_new_user_title": "Thêm người dùng", + "share_group_title": "Chia sẻ nhóm", + "change_group_infomation_title": "Đổi thông tin", + "change_group_infomation_content": "Chỉnh sửa thông tin nhóm", + "delete_group_title": "Xóa nhóm", + "delete_group_content": "Bạn chắc chắn muốn xóa nhóm này?", + "leave_group_content": "Bạn chắc chắn muốn rời nhóm?", + "dont_have_group": "Chưa có nhóm", + "dont_join_group": "Bạn chưa tham gia nhóm nào", + "description_group": "Mô tả", + "add_new_device_title": "Thêm thiết bị mới", + "approve_user": "Duyệt thành viên", + "devices_title": "Thiết bị", + "device_title": "Thiết bị", + "member_title": "Thành viên", + "leave_group_title": "Rời nhóm", + "dont_have_device": "Chưa có thiết bị", + "description_NOTUSE4": "This is vietnamese language in ProfilePage", + "profile_page_title": "Cài đặt", + "profile_change_info": "Đổi thông tin cá nhân", + "profile_change_pass": "Đổi mật khẩu", + "profile_setting": "Cài đặt thông báo", + "change_profile_title": "Thông tin người dùng", + "change_profile_username": "Tên người dùng: ", + "change_profile_username_hint": "Nhập tên ", + "change_profile_email": "Email: ", + "change_profile_email_hint": "Nhập email ", + "change_profile_email_not_empty": "Email không được để trống", + "change_profile_tel": "Số điện thoại: ", + "change_profile_tel_hint": "Nhập số điện thoại", + "change_profile_tel_not_empty": "Số điện thoại không được để trống", + "change_profile_address": "Địa chỉ: ", + "change_profile_address_hint": "Nhập địa chỉ", + "change_profile_old_pass": "Mật khẩu cũ: ", + "change_profile_old_pass_hint": "Nhập mật khẩu cũ", + "change_profile_old_pass_not_empty": "Mật khẩu không được để trống", + "change_profile_new_pass": "Mật khẩu mới: ", + "change_profile_new_pass_hint": "Nhập mật khẩu mới", + "change_profile_new_pass_not_empty": "Mật khẩu không được để trống", + "change_profile_device_notification_select_all": "Chọn tất cả", + "change_profile_device_notification_deselect_all": "Bỏ chọn tất cả", + "description_NOTUSE5": "This is vietnamese language in BellPage", + "bell_page_title": "Thông báo", + "bell_page_no_items_body": "Chưa có thông báo", + "bell_user_uppercase": "Người dùng", + "bell_battery_device": "Pin thiết bị", + "bell_user_joined_group": "đã tham gia nhóm", + "bell_leave_group": "đã rời nhóm", + "bell_user_added_group": "đã được thêm vào nhóm", + "bell_user_kick_group": "đã bị xóa khỏi nhóm", + "bell_operate_normal": "hoạt động bình thường", + "bell_invalid_code": "Mã sự kiện không hợp lệ", + "bell_days_ago": "ngày trước", + "bell_hours_ago": "giờ trước", + "bell_minutes_ago": "phút trước", + "bell_just_now": "Vừa xong", + "bell_read_all": "Bạn đã xem hết thông báo", + "description_NOTUSE6": "This is vietnamese language in GlobalFunction", + "gf_newly_create_message": "Mới tạo", + "gf_disconnect_message": "Mất kết nối", + "gf_smoke_detected_message": "Đang hoạt động", + "gf_no_signal_message": "Không có sóng", + "gf_weak_signal_message": "Mức sóng yếu", + "gf_moderate_signal_message": "Mức sóng khá", + "gf_good_signal_message": "Mức sóng tốt", + "gf_volt_detect_message": "Có điện thế", + "gf_temp_detect_message": "Có nhiệt độ", + "gf_hum_detect_message": "Có độ ẩm", + "gf_battery_detect_message": "Có mức pin", + "gf_offline_message": "Không hoạt động", + "gf_in_firefighting_message": "Đang chữa cháy", + "gf_device_error_message": "Thiết bị lỗi", + "gf_not_move_message": "Chưa di chuyển", + "gf_moving_message": "Đã di chuyển", + "gf_remove_from_base_message": "Bị tháo khỏi đế", + "gf_connected_lowercase": "đã kết nối", + "description_NOTUSE7": "This is vietnamese language in LoginPage", + "login_account_not_empty": "Tài khoản không được để trống", + "login_account_hint": "Tài khoản", + "login_password_not_empty": "Mật khẩu không được để trống", + "login_password_hint": "Mật khẩu", + "login_success_message": "Đăng nhập thành công", + "login_incorrect_usernameOrPass": "Tài khoản hoặc mật khẩu không đúng", + "login_button_content": "Đăng nhập", + "description_NOTUSE9": "This is vietnamese language in DeviceUpdatePage", + "device_update_title": "Chỉnh sửa chi tiết thiết bị", + "device_update_location": "Vị trí thiết bị", + "device_update_province": "Tỉnh/Thành phố", + "device_update_district": "Quận/Huyện", + "device_update_ward": "Phường/Xã", + "description_NOTUSE10": "This is vietnamese language in DetailDevicePage", + "detail_device_dont_has_location_message": "Chưa có thông tin về vị trí", + "no_data_message": "Chưa có", + "normal_message": "Bình thường", + "warning_status_message": "Cảnh báo", + "undefine_message": "Không xác định", + "low_message_uppercase": "Yếu", + "moderate_message_uppercase": "Khá", + "good_message_uppercase": "Tốt", + "low_message_lowercase": "yếu", + "moderate_message_lowercase": "khá", + "good_message_lowercase": "tốt", + "error_message_uppercase": "Lỗi", + "error_message_lowercase": "lỗi", + "warning_message": "Cảnh báo:", + "loading_message": "Đang tải...", + "detail_message": "Chi tiết", + "decline_message": "TỪ CHỐI", + "allow_message": "CHO PHÉP", + "add_button_content": "Thêm", + "update_button_content": "Cập nhật", + "change_button_content": "Chỉnh sửa", + "confirm_button_content": "Xác nhận", + "delete_button_content": "Xóa", + "cancel_button_content": "Hủy", + "find_button_content": "Tìm", + "home_page_destination": "Trang chủ", + "manager_page_destination": "Quản lý", + "map_page_destination": "Bản đồ", + "history_page_destination": "Lịch sử", + "history_page_destination_tooltip": "Lịch sử thiết bị", + "group_page_destination": "Nhóm", + "group_page_destination_tooltip": "Trao đổi thông báo thiết bị", + "notification_enter_all_inf": "Vui lòng điền đầy đủ thông tin", + "notification_update_device_success": "Cập nhật thiết bị thành công", + "notification_update_device_failed": "Cập nhật thiết bị thất bại", + "notification_update_device_error": "Cập nhật lỗi", + "notification_cannot_find_address_from_location": "Không tìm được vị trí", + "notification_add_device_success": "Thêm thiết bị thành công", + "notification_add_device_failed": "Thêm thiết bị thất bại", + "notification_create_device_success": "Tạo thiết bị thành công", + "notification_create_device_failed": "Tạo thiết bị thất bại", + "notification_delete_device_success": "Xóa thiết bị thành công", + "notification_delete_device_failed": "Xóa thiết bị thất bại", + "notification_device_not_exist": "Thiết bị không tồn tại", + "notification_add_group_success": "Tạo nhóm thành công", + "notification_add_group_failed": "Tạo nhóm thất bại", + "notification_update_group_success": "Sửa nhóm thành công", + "notification_update_group_failed": "Sửa nhóm thất bại", + "notification_delete_group_success": "Xóa nhóm thành công", + "notification_delete_group_failed": "Xóa nhóm thất bại", + "notification_leave_group_success": "Rời nhóm thành công", + "notification_leave_group_failed": "Rời nhóm thất bại", + "notification_join_request_group_success": "Yêu cầu tham gia nhóm thành công!", + "notification_join_request_group_failed": "Yêu cầu tham gia nhóm thất bại!", + "notification_update_profile_success": "Sửa thông tin thành công", + "notification_update_profile_failed": "Sửa thông tin thất bại", + "notification_update_password_success": "Đổi mật khẩu thành công", + "notification_update_password_failed": "Mật khẩu cũ không khớp", + "notification_update_device_settings_success": "Cập nhật thông báo cho thiết bị thành công", + "notification_update_device_settings_failed": "Cập nhật thông báo cho thiết bị thất bại", + "notification_confirm_fake_fire_success": "Đã cập nhật thông tin đến đội PCCC", + "notification_confirm_fake_fire_failed": "Cập nhật cháy giả thất bại" +} diff --git a/lib/product/lang/language_model.dart b/lib/product/lang/language_model.dart new file mode 100644 index 0000000..ec5a7be --- /dev/null +++ b/lib/product/lang/language_model.dart @@ -0,0 +1,18 @@ +import 'package:sfm_app/product/constant/icon/icon_constants.dart'; +import 'package:sfm_app/product/constant/lang/language_constants.dart'; + +class Language { + final int id; + final String imgageIcon; + final String name; + final String languageCode; + + Language(this.id, this.imgageIcon, this.name, this.languageCode); + + static List languages() { + return [ + Language(1, IconConstants.instance.getIcon("vi_icon"), "vn", LanguageConstants.VIETNAM), + Language(2, IconConstants.instance.getIcon("en_icon"), "en", LanguageConstants.VIETNAM), + ]; + } +} diff --git a/lib/product/network/network_manager.dart b/lib/product/network/network_manager.dart new file mode 100644 index 0000000..afbfd8f --- /dev/null +++ b/lib/product/network/network_manager.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:sfm_app/product/constant/status_code/status_code_constants.dart'; + +import '../cache/local_manager.dart'; +import '../constant/app/app_constants.dart'; +import '../constant/enums/local_keys_enums.dart'; +import 'package:http/http.dart' as http; + +class NetworkManager { + NetworkManager._init(); + static NetworkManager? _instance; + static NetworkManager? get instance => _instance ??= NetworkManager._init(); + + Future> getHeaders() async { + String? token = + LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN); + Map headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + "Access-Control-Allow-Credentials": "false", + "Access-Control-Allow-Headers": + "Origin,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,locale", + 'Access-Control-Allow-Origin': "*", + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE', + 'Authorization': token, + }; + return headers; + } + + /// Retrieves data from the server using a GET request. + /// + /// [path] is the endpoint for the request. Returns the response body as a + /// [String] if the request is successful (status code 200), or an empty + /// string if the request fails + Future getDataFromServer(String path) async { + final url = Uri.https(ApplicationConstants.DOMAIN, path); + log("GET url: $url"); + final headers = await getHeaders(); + final response = await http.get(url, headers: headers); + if (response.statusCode == StatusCodeConstants.OK || + response.statusCode == StatusCodeConstants.CREATED) { + return response.body; + } else { + return ""; + } + } + + /// Sends a GET request to the server with the specified parameters. + /// + /// This function constructs a URL from the provided [path] and [params], + /// then sends an HTTP GET request to the server. If the response has a + /// status code of 200, the function returns the response body. + /// Otherwise, it returns an empty string. + /// + /// [path] is the endpoint on the server. + /// [params] is a map containing query parameters for the request. + /// + /// Returns a [Future] containing the server response body. + Future getDataFromServerWithParams( + String path, Map params) async { + final url = Uri.https(ApplicationConstants.DOMAIN, path, params); + log("GET Params url: $url"); + final headers = await getHeaders(); + final response = await http.get(url, headers: headers); + if (response.statusCode == StatusCodeConstants.CREATED || + response.statusCode == StatusCodeConstants.OK) { + return response.body; + } else { + return ""; + } + } + + /// Creates new data on the server using a POST request. + /// + /// [path] is the endpoint for the request, and [body] contains the data + /// to be sent. Returns the HTTP status code of the response. + Future createDataInServer(String path, Map body) async { + final url = Uri.https(ApplicationConstants.DOMAIN, path); + log("POST url: $url"); + final headers = await getHeaders(); + final response = + await http.post(url, headers: headers, body: jsonEncode(body)); + return response.statusCode; + } + + /// Updates existing data on the server using a PUT request. + /// + /// [path] is the endpoint for the request, and [body] contains the data + /// to be updated. Returns the HTTP status code of the response. + Future updateDataInServer(String path, Map body) async { + final url = Uri.https(ApplicationConstants.DOMAIN, path); + log("PUT url: $url"); + final headers = await getHeaders(); + final response = + await http.put(url, headers: headers, body: jsonEncode(body)); + return response.statusCode; + } + + /// Deletes data from the server using a DELETE request. + /// + /// [path] is the endpoint for the request. Returns the HTTP status code + /// of the response, indicating the result of the deletion operation. + /// A status code of 200 indicates success, while other codes indicate + /// failure or an error. + Future deleteDataInServer(String path) async { + final url = Uri.https(ApplicationConstants.DOMAIN, path); + log("DELETE url: $url"); + final headers = await getHeaders(); + final response = await http.delete(url, headers: headers); + return response.statusCode; + } +} diff --git a/lib/product/permission/notification_permission.dart b/lib/product/permission/notification_permission.dart new file mode 100644 index 0000000..23d5b75 --- /dev/null +++ b/lib/product/permission/notification_permission.dart @@ -0,0 +1,31 @@ +import 'dart:developer'; + +import 'package:app_settings/app_settings.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import '../base/widget/dialog/request_permission_dialog.dart'; + +class NotificationPermission { + FirebaseMessaging messaging = FirebaseMessaging.instance; + void requestNotificationsPermission(BuildContext context) async { + NotificationSettings settings = await messaging.requestPermission( + alert: true, + announcement: true, + badge: true, + carPlay: true, + criticalAlert: true, + provisional: true, + sound: true); + if (settings.authorizationStatus == AuthorizationStatus.authorized) { + log("NotificationsPermission: User granted permission"); + } else if (settings.authorizationStatus == + AuthorizationStatus.provisional) { + log("NotificationsPermission: User granted provisional permission"); + } else { + log("NotificationsPermission: User denied permission"); + // ignore: use_build_context_synchronously + RequestPermissionDialog().showRequestPermissionDialog(context, + Icons.location_on_outlined, "ABCDE", AppSettingsType.notification); + } + } +} diff --git a/lib/product/services/api_services.dart b/lib/product/services/api_services.dart new file mode 100644 index 0000000..e6fe8fa --- /dev/null +++ b/lib/product/services/api_services.dart @@ -0,0 +1,386 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import '../constant/app/api_path_constant.dart'; +import '../shared/shared_snack_bar.dart'; +import '../constant/enums/app_route_enums.dart'; +import 'language_services.dart'; +import '../../feature/bell/bell_model.dart'; +import '../cache/local_manager.dart'; +import '../constant/app/app_constants.dart'; +import '../constant/enums/local_keys_enums.dart'; +import '../network/network_manager.dart'; + +class APIServices { + Map headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + "Access-Control-Allow-Credentials": "false", + "Access-Control-Allow-Headers": + "Origin,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,locale", + 'Access-Control-Allow-Origin': "*", + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE', + }; + + Future> getHeaders() async { + String? token = + LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN); + Map headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + "Access-Control-Allow-Credentials": "false", + "Access-Control-Allow-Headers": + "Origin,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,locale", + 'Access-Control-Allow-Origin': "*", + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE', + 'Authorization': token, + }; + return headers; + } + + Future login(String path, Map loginRequest) async { + final url = Uri.https(ApplicationConstants.DOMAIN, path); + final headers = await getHeaders(); + final response = + await http.post(url, headers: headers, body: jsonEncode(loginRequest)); + return response.body; + } + + Future logOut(BuildContext context) async { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(appLocalization(context).log_out_content, + textAlign: TextAlign.center), + actions: [ + TextButton( + onPressed: () async { + var url = Uri.http(ApplicationConstants.DOMAIN, + APIPathConstants.LOGOUT_PATH); + final headers = await NetworkManager.instance!.getHeaders(); + final response = await http.post(url, headers: headers); + if (response.statusCode == 200) { + LocaleManager.instance + .deleteStringValue(PreferencesKeys.UID); + LocaleManager.instance + .deleteStringValue(PreferencesKeys.TOKEN); + LocaleManager.instance + .deleteStringValue(PreferencesKeys.EXP); + LocaleManager.instance + .deleteStringValue(PreferencesKeys.ROLE); + context.goNamed(AppRoutes.LOGIN.name); + } else { + showErrorTopSnackBarCustom( + context, "Error: ${response.statusCode}"); + } + }, + child: Text(appLocalization(context).log_out, + style: const TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(appLocalization(context).cancel_button_content), + ), + ], + )); + } + + Future getUID() async { + String uid = LocaleManager.instance.getStringValue(PreferencesKeys.UID); + return uid; + } + + Future getUserRole() async { + String role = LocaleManager.instance.getStringValue(PreferencesKeys.ROLE); + return role; + } + + Future checkTheme() async { + String theme = LocaleManager.instance.getStringValue(PreferencesKeys.THEME); + return theme; + } + + Future checkLanguage() async { + String language = + LocaleManager.instance.getStringValue(PreferencesKeys.LANGUAGE_CODE); + return language; + } + + Future getBellNotifications(String offset, String pagesize) async { + Bell bell = Bell(); + final params = {"offset": offset, "page_size": pagesize}; + final data = await NetworkManager.instance!.getDataFromServerWithParams( + APIPathConstants.BELL_NOTIFICATIONS_PATH, params); + if (data != "") { + bell = Bell.fromJson(jsonDecode(data)); + return bell; + } else { + return bell; + } + } + + Future updateStatusOfNotification(List notificationID) async { + Map body = { + "event_ids": notificationID, + }; + int statusCode = await NetworkManager.instance!.updateDataInServer( + APIPathConstants.BELL_UPDATE_READ_NOTIFICATIONS_PATH, body); + return statusCode; + } + + Future getUserDetail() async { + String uid = await getUID(); + String? response = await NetworkManager.instance! + .getDataFromServer('${APIPathConstants.USER_PATH}/$uid'); + return response; + } + + Future updateUserProfile(Map body) async { + String uid = await getUID(); + int statusCode = await NetworkManager.instance! + .updateDataInServer("${APIPathConstants.USER_PROFILE_PATH}/$uid", body); + return statusCode; + } + + Future updateUserPassword(Map body) async { + String uid = await getUID(); + int statusCode = await NetworkManager.instance!.updateDataInServer( + "${APIPathConstants.USER_PATH}/$uid/password", body); + return statusCode; + } + + Future getAllSettingsNotificationOfDevices() async { + String? data = await NetworkManager.instance! + .getDataFromServer(APIPathConstants.DEVICE_NOTIFICATION_SETTINGS); + return data; + } + + Future updateDeviceNotificationSettings( + String thingID, Map data) async { + Map body = {"thing_id": thingID, "notifi_settings": data}; + int statusCode = await NetworkManager.instance!.updateDataInServer( + APIPathConstants.DEVICE_NOTIFICATION_SETTINGS, body); + return statusCode; + } + + Future getDashBoardDevices() async { + String? data = await NetworkManager.instance! + .getDataFromServer(APIPathConstants.DASHBOARD_DEVICES); + return data; + } + + Future setupDeviceNotification(String thingID, String deviceName) async { + Map body = { + "thing_id": thingID, + "alias": deviceName, + "notifi_settings": { + "001": 1, + "002": 1, + "003": 1, + "004": 1, + "005": 1, + "006": 1, + "101": 1, + "102": 1, + "103": 1, + "104": 1, + } + }; + int statusCode = await NetworkManager.instance!.updateDataInServer( + APIPathConstants.DEVICE_NOTIFICATION_SETTINGS, body); + return statusCode; + } + + Future getAllProvinces() async { + String? data = await NetworkManager.instance! + .getDataFromServer(APIPathConstants.PROVINCES_PATH); + return data; + } + + Future getProvincesByName(String name) async { + final params = {'name': name}; + String? data = await NetworkManager.instance! + .getDataFromServerWithParams(APIPathConstants.PROVINCES_PATH, params); + return data; + } + + Future getProvinceByID(String provinceID) async { + String data = await NetworkManager.instance! + .getDataFromServer("${APIPathConstants.PROVINCES_PATH}/$provinceID"); + return data; + } + + Future getAllDistricts(String provinceID) async { + final params = {"parent": provinceID}; + String? data = await NetworkManager.instance! + .getDataFromServerWithParams(APIPathConstants.DISTRICTS_PATH, params); + return data; + } + + Future getDistrictsByName(String districtName) async { + final params = {"name": districtName}; + String? data = await NetworkManager.instance! + .getDataFromServerWithParams(APIPathConstants.DISTRICTS_PATH, params); + return data; + } + + Future getDistrictByID(String districtID) async { + String? data = await NetworkManager.instance! + .getDataFromServer("${APIPathConstants.DISTRICTS_PATH}/$districtID"); + return data; + } + + Future getAllWards(String districtID) async { + final params = {'parent': districtID}; + String? data = await NetworkManager.instance! + .getDataFromServerWithParams(APIPathConstants.WARDS_PATH, params); + return data; + } + + Future getWarsdByName(String wardName) async { + final params = {"name": wardName}; + String? data = await NetworkManager.instance! + .getDataFromServerWithParams(APIPathConstants.WARDS_PATH, params); + return data; + } + + Future getWardByID(String wardID) async { + String? data = await NetworkManager.instance! + .getDataFromServer("${APIPathConstants.WARDS_PATH}/$wardID"); + return data; + } + + Future confirmFakeFireByUser(String thingID) async { + Map body = { + "state": 3, + "note": "Người dùng xác nhận cháy giả!" + }; + int statusCode = await NetworkManager.instance! + .updateDataInServer("${APIPathConstants.DEVICE_PATH}/$thingID", body); + return statusCode; + } + + Future getOwnerDevices() async { + String? data = await NetworkManager.instance! + .getDataFromServer(APIPathConstants.DEVICE_PATH); + return data; + } + + Future createDeviceByAdmin(Map body) async { + int? statusCode = await NetworkManager.instance! + .createDataInServer(APIPathConstants.DEVICE_PATH, body); + return statusCode; + } + + Future registerDevice(Map body) async { + int? statusCode = await NetworkManager.instance! + .createDataInServer(APIPathConstants.DEVICE_REGISTER_PATH, body); + return statusCode; + } + + Future deleteDeviceByAdmin(String thingID) async { + int statusCode = await NetworkManager.instance! + .deleteDataInServer("${APIPathConstants.DEVICE_PATH}/$thingID"); + return statusCode; + } + + Future unregisterDevice(Map body) async { + int statusCode = await NetworkManager.instance! + .createDataInServer(APIPathConstants.DEVICE_UNREGISTER_PATH, body); + return statusCode; + } + + Future getDeviceInfomation(String thingID) async { + String? response = await NetworkManager.instance! + .getDataFromServer("${APIPathConstants.DEVICE_PATH}/$thingID"); + return response; + } + + Future getAllGroups() async { + String? body = await NetworkManager.instance! + .getDataFromServer(APIPathConstants.ALL_GROUPS_PATH); + return body; + } + + Future createGroup(Map body) async { + int? statusCode = await NetworkManager.instance! + .createDataInServer(APIPathConstants.GROUPS_PATH, body); + return statusCode; + } + + Future updateGroup(Map body, String groupID) async { + int? statusCode = await NetworkManager.instance! + .updateDataInServer("${APIPathConstants.GROUPS_PATH}/$groupID", body); + return statusCode; + } + + Future joinGroup(String groupID, Map body) async { + int? statusCode = await NetworkManager.instance! + .createDataInServer(APIPathConstants.JOIN_GROUP_PATH, body); + return statusCode; + } + + Future deleteGroup(String groupID) async { + int? statusCode = await NetworkManager.instance! + .deleteDataInServer("${APIPathConstants.GROUPS_PATH}/$groupID"); + return statusCode; + } + + Future getGroupDetail(String groupID) async { + String? body = await NetworkManager.instance! + .getDataFromServer("${APIPathConstants.GROUPS_PATH}/$groupID"); + return body; + } + + Future approveGroup(Map body) async { + int statusCode = await NetworkManager.instance! + .createDataInServer(APIPathConstants.APPROVE_GROUP_PATH, body); + return statusCode; + } + + Future deleteUserInGroup(String groupID, String userID) async { + int? statusCode = await NetworkManager.instance!.deleteDataInServer( + "${APIPathConstants.GROUPS_PATH}/$groupID/users/$userID"); + return statusCode; + } + + Future deleteDeviceInGroup(String groupID, String thingID) async { + int? statusCode = await NetworkManager.instance!.deleteDataInServer( + "${APIPathConstants.GROUPS_PATH}/$groupID/devices/$thingID"); + return statusCode; + } + + Future updateDeviceAlias(Map body) async { + int? statusCode = await NetworkManager.instance!.updateDataInServer( + APIPathConstants.DEVICE_NOTIFICATION_SETTINGS, body); + return statusCode; + } + + Future addDeviceToGroup( + String groupID, Map body) async { + int? statusCode = await NetworkManager.instance!.createDataInServer( + "${APIPathConstants.GROUPS_PATH}/$groupID/things", body); + return statusCode; + } + + Future updateOwnerDevice( + String thingID, Map body) async { + int? statusCode = await NetworkManager.instance! + .updateDataInServer("${APIPathConstants.DEVICE_PATH}/$thingID", body); + return statusCode; + } + + Future getLogsOfDevice( + String thingID, Map params) async { + String? body = await NetworkManager.instance! + .getDataFromServerWithParams(APIPathConstants.DEVICE_LOGS_PATH, params); + return body; + } + + +} diff --git a/lib/product/services/language_services.dart b/lib/product/services/language_services.dart new file mode 100644 index 0000000..6f88622 --- /dev/null +++ b/lib/product/services/language_services.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import '../cache/local_manager.dart'; +import '../constant/enums/local_keys_enums.dart'; +import '../constant/lang/language_constants.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class LanguageServices { + Future setLocale(String languageCode) async { + await LocaleManager.prefrencesInit(); + LocaleManager.instance + .setStringValue(PreferencesKeys.LANGUAGE_CODE, languageCode); + return _locale(languageCode); + } + + Future getLocale() async { + await LocaleManager.prefrencesInit(); + String languageCode = + LocaleManager.instance.getStringValue(PreferencesKeys.LANGUAGE_CODE); + return _locale(languageCode); + } + + Locale _locale(String languageCode) { + switch (languageCode) { + case LanguageConstants.ENGLISH: + return const Locale(LanguageConstants.ENGLISH, ''); + case LanguageConstants.VIETNAM: + return const Locale(LanguageConstants.VIETNAM, ''); + default: + return const Locale(LanguageConstants.VIETNAM, ''); + } + } +} + +AppLocalizations appLocalization(BuildContext context) { + return AppLocalizations.of(context)!; +} diff --git a/lib/product/services/map_services.dart b/lib/product/services/map_services.dart new file mode 100644 index 0000000..e0654cc --- /dev/null +++ b/lib/product/services/map_services.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:dio/dio.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:http/http.dart' as http; +import '../constant/app/app_constants.dart'; +import '../shared/find_location_maps/model/prediction_model.dart'; +import '../shared/model/near_by_search_model.dart'; + +class MapServices { + + Future> getNearbyPlaces(double latitude, double longitude, + String searchKey, int radius, String type) async { + List result = []; + var url = Uri.parse( + 'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$searchKey&language=vi&location=$latitude%2C$longitude&radius=$radius&strictbounds=true&type=$type&key=${ApplicationConstants.MAP_KEY}'); + log("URL LIST: $url"); + var response = await http.post(url); + final placesAutocompleteResponse = + PlacesAutocompleteResponse.fromJson(jsonDecode(response.body)); + if (placesAutocompleteResponse.predictions != null) { + for (int i = 0; i < placesAutocompleteResponse.predictions!.length; i++) { + var url = + "https://maps.googleapis.com/maps/api/place/details/json?placeid=${placesAutocompleteResponse.predictions![i].placeId}&language=vi&key=${ApplicationConstants.MAP_KEY}"; + log(url.toString()); + Response response = await Dio().get( + url, + ); + PlaceDetails placeDetails = PlaceDetails.fromJson(response.data); + result.add(placeDetails); + // displayLocation(placesAutocompleteResponse.predictions![i], i); + } + return result; + } else { + log("null"); + return []; + } + } + + Future> findTheWay(LatLng origin, LatLng destination) async { + List polylineCoordinates = []; + PolylinePoints polylinePoints = PolylinePoints(); + + PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( + ApplicationConstants.MAP_KEY, + PointLatLng(origin.latitude, origin.longitude), + PointLatLng(destination.latitude, destination.longitude), + travelMode: TravelMode.driving, + optimizeWaypoints: true); + if (result.points.isNotEmpty) { + for (var point in result.points) { + polylineCoordinates.add(LatLng(point.latitude, point.longitude)); + } + return polylineCoordinates; + } else { + log("Lỗi khi tìm đường"); + return []; + } + } + +} \ No newline at end of file diff --git a/lib/product/services/notification_services.dart b/lib/product/services/notification_services.dart new file mode 100644 index 0000000..f4c5d8b --- /dev/null +++ b/lib/product/services/notification_services.dart @@ -0,0 +1,103 @@ +import 'dart:developer' as dev; +import 'dart:math' as math; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class NotificationServices { + FirebaseMessaging messaging = FirebaseMessaging.instance; + final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + + void firebaseInit(BuildContext context) async { + FirebaseMessaging.onMessage.listen((message) { + initNotifications(context, message); + showNotification(message); + dev.log( + "Title: ${message.notification!.title}, Body: ${message.notification!.body} from ${message.sentTime}"); + }); + } + + Future getDeviceToken() async { + String? token = await messaging.getToken(); + return token!; + } + + void isTokenRefresh() async { + messaging.onTokenRefresh.listen((newToken) { + dev.log("Refresh Firebase Messaging Token: $newToken"); + }); + } + + void initNotifications(BuildContext context, RemoteMessage message) async { + var androidInitializationSettings = + const AndroidInitializationSettings('app_icon'); + var iosInitializationSettings = const DarwinInitializationSettings(); + var initializationSettings = InitializationSettings( + android: androidInitializationSettings, iOS: iosInitializationSettings); + await _flutterLocalNotificationsPlugin.initialize(initializationSettings, + onDidReceiveNotificationResponse: (payload) { + handleMessage(context, message); + }); + } + + Future showNotification(RemoteMessage message) async { + AndroidNotificationChannel androidNotificationChannel = + AndroidNotificationChannel( + math.Random.secure().nextInt(1000000).toString(), + 'high Important Notification', + importance: Importance.max); + AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + androidNotificationChannel.id.toString(), + androidNotificationChannel.name.toString(), + sound: RawResourceAndroidNotificationSound(message.data['sound']), + channelDescription: "Channel description", + importance: androidNotificationChannel.importance, + priority: Priority.high, + ticker: 'ticker', + ); + + const DarwinNotificationDetails darwinNotificationDetails = + DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentBanner: true, + presentSound: true, + ); + NotificationDetails notificationDetails = NotificationDetails( + android: androidNotificationDetails, iOS: darwinNotificationDetails); + Future.delayed(Duration.zero, () { + _flutterLocalNotificationsPlugin.show(0, message.notification!.title!, + message.notification!.body, notificationDetails); + }); + } + + void handleMessage(BuildContext context, RemoteMessage message) async { + if (message.data['type'] == "msj") { + // Navigator.push(context, + // MaterialPageRoute(builder: (context) => const MessageScreen())); + } else if (message.data['type'] == "warn") { + // Navigator.push( + // context, MaterialPageRoute(builder: (context) => const MapScreen())); + } else { + dev.log("Not found data"); + } + } + + Future setupInteractMessage(BuildContext context) async { + // When app terminate + RemoteMessage? initialMessage = + await FirebaseMessaging.instance.getInitialMessage(); + if (initialMessage != null) { + // showNotification(initialMessage); + // ignore: use_build_context_synchronously + handleMessage(context, initialMessage); + } + + // When app is inBackGround + FirebaseMessaging.onMessageOpenedApp.listen((message) { + handleMessage(context, message); + }); + } +} diff --git a/lib/product/shared/find_location_maps/error/dio_handle_error.dart b/lib/product/shared/find_location_maps/error/dio_handle_error.dart new file mode 100644 index 0000000..5b57e0b --- /dev/null +++ b/lib/product/shared/find_location_maps/error/dio_handle_error.dart @@ -0,0 +1,137 @@ + +import 'package:dio/dio.dart'; + +import 'error_response_model.dart'; + +class DioErrorHandler { + ErrorResponse errorResponse = ErrorResponse(); + String errorDescription = ""; + + ErrorResponse handleDioError(DioException dioError) { + switch (dioError.type) { + case DioExceptionType.cancel: + errorResponse.message = "Request to API server was cancelled"; + break; + case DioExceptionType.connectionTimeout: + errorResponse.message = "Connection timeout with API server"; + break; + case DioExceptionType.unknown: + if ((dioError.message!.contains("RedirectException"))) { + errorResponse.message = dioError.message; + } else { + errorResponse.message = "Please check the internet connection"; + } + break; + case DioExceptionType.receiveTimeout: + errorResponse.message = "Receive timeout in connection with API server"; + break; + case DioExceptionType.badResponse: + try { + if (dioError.response?.data['message'] != null) { + errorResponse.message = dioError.response?.data['message']; + } else { + if ((dioError.response?.statusMessage ?? "").isNotEmpty) { + errorResponse.message = dioError.response?.statusMessage; + } else { + return _handleError( + dioError.response!.statusCode, dioError.response!.data); + } + } + } catch (e) { + if ((dioError.response?.statusMessage ?? "").isNotEmpty) { + errorResponse.message = dioError.response?.statusMessage; + } else { + return _handleError( + dioError.response!.statusCode, dioError.response!.data); + } + } + + break; + case DioExceptionType.sendTimeout: + errorResponse.message = "Send timeout in connection with API server"; + break; + default: + errorResponse.message = "Something went wrong"; + break; + } + return errorResponse; + } + + ErrorResponse _handleError(int? statusCode, dynamic error) { + switch (statusCode) { + case 400: + return getMas(error); + // case 401: + // return checkTokenExpire(error); + case 404: + return getMas(error); + case 403: + return getMas(error); + case 500: + errorResponse.message = 'Internal server error'; + return errorResponse; + default: + return getUnKnownMes(error); + } + } + + // checkTokenExpire(error) { + // // print("my error ${error}"); + // if (error['msg'].toString().toLowerCase() == + // "Token has expired".toLowerCase()) { + // UIData.tokenExpire(error['msg']); + // return; + // } + // errorResponse.message = error['msg'].toString(); + // return errorResponse; + // } + + getMas(dynamic error) { + print("myError ${error.runtimeType}"); + if (error.runtimeType != String) { + errorResponse.message = + error['message'].toString(); //?? S.of(Get.context).something_wrong; + } else { + if (error['msg'] != null) { + errorResponse.message = error['msg'].toString(); + } else { + errorResponse.message = "Something Wrong"; + } //S.of(Get.context).something_wrong; + } + return errorResponse; + } + + getUnKnownMes(dynamic error) { + if (error['msg'] != null) { + errorResponse.message = error['msg'].toString(); + } else if (error['message'] != null) { + errorResponse.message = error['message'].toString(); + } else { + errorResponse.message = "Something went wrong"; + } + return errorResponse; + } +} + +class ErrorHandler { + static final ErrorHandler _inst = ErrorHandler.internal(); + ErrorHandler.internal(); + + factory ErrorHandler() { + return _inst; + } + ErrorResponse errorResponse = ErrorResponse(); + + ErrorResponse handleError(var error) { + if (error.runtimeType.toString().toLowerCase() == + "_TypeError".toLowerCase()) { + // return error.toString(); + errorResponse.message = "The Provided API key is invalid"; + return errorResponse; + } else if (error is DioError) { + return DioErrorHandler().handleDioError(error); + } + errorResponse.message = "The Provided API key is invalid"; + return errorResponse; + } +} diff --git a/lib/product/shared/find_location_maps/error/error_response_model.dart b/lib/product/shared/find_location_maps/error/error_response_model.dart new file mode 100644 index 0000000..4fdb49f --- /dev/null +++ b/lib/product/shared/find_location_maps/error/error_response_model.dart @@ -0,0 +1,19 @@ +class ErrorResponse { + String? message; + int? status; + + ErrorResponse({this.message, this.status}); + + ErrorResponse.fromJson(Map json) { + message = json['message']; + status = json['status']; + } + + Map toJson() { + final Map data = {}; + data['message'] = message; + data['status'] = status; + + return data; + } +} diff --git a/lib/product/shared/find_location_maps/model/place_detail_model.dart b/lib/product/shared/find_location_maps/model/place_detail_model.dart new file mode 100644 index 0000000..803349f --- /dev/null +++ b/lib/product/shared/find_location_maps/model/place_detail_model.dart @@ -0,0 +1,234 @@ +class PlaceDetails { + Result? result; + String? status; + + PlaceDetails({this.result, this.status}); + + PlaceDetails.fromJson(Map json) { + result = + json['result'] != null ? Result.fromJson(json['result']) : null; + status = json['status']; + } + + Map toJson() { + final Map data = {}; + + if (result != null) { + data['result'] = result!.toJson(); + } + data['status'] = status; + return data; + } +} + +class Result { + List? addressComponents; + String? adrAddress; + String? formattedAddress; + Geometry? geometry; + String? icon; + String? name; + List? photos; + String? placeId; + String? reference; + String? scope; + List? types; + String? url; + int? utcOffset; + String? vicinity; + String? website; + + Result( + {this.addressComponents, + this.adrAddress, + this.formattedAddress, + this.geometry, + this.icon, + this.name, + this.photos, + this.placeId, + this.reference, + this.scope, + this.types, + this.url, + this.utcOffset, + this.vicinity, + this.website}); + + Result.fromJson(Map json) { + if (json['address_components'] != null) { + addressComponents = []; + json['address_components'].forEach((v) { + addressComponents!.add(AddressComponents.fromJson(v)); + }); + } + adrAddress = json['adr_address']; + formattedAddress = json['formatted_address']; + geometry = json['geometry'] != null + ? Geometry.fromJson(json['geometry']) + : null; + icon = json['icon']; + name = json['name']; + if (json['photos'] != null) { + photos = []; + json['photos'].forEach((v) { + photos!.add(Photos.fromJson(v)); + }); + } + placeId = json['place_id']; + reference = json['reference']; + scope = json['scope']; + types = json['types'].cast(); + url = json['url']; + utcOffset = json['utc_offset']; + vicinity = json['vicinity']; + website = json['website']; + } + + Map toJson() { + final Map data = {}; + if (addressComponents != null) { + data['address_components'] = + addressComponents!.map((v) => v.toJson()).toList(); + } + data['adr_address'] = adrAddress; + data['formatted_address'] = formattedAddress; + if (geometry != null) { + data['geometry'] = geometry!.toJson(); + } + data['icon'] = icon; + data['name'] = name; + if (photos != null) { + data['photos'] = photos!.map((v) => v.toJson()).toList(); + } + data['place_id'] = placeId; + data['reference'] = reference; + data['scope'] = scope; + data['types'] = types; + data['url'] = url; + data['utc_offset'] = utcOffset; + data['vicinity'] = vicinity; + data['website'] = website; + return data; + } +} + +class AddressComponents { + String? longName; + String? shortName; + List? types; + + AddressComponents({this.longName, this.shortName, this.types}); + + AddressComponents.fromJson(Map json) { + longName = json['long_name']; + shortName = json['short_name']; + types = json['types'].cast(); + } + + Map toJson() { + final Map data = {}; + data['long_name'] = longName; + data['short_name'] = shortName; + data['types'] = types; + return data; + } +} + +class Geometry { + Location? location; + Viewport? viewport; + + Geometry({this.location, this.viewport}); + + Geometry.fromJson(Map json) { + location = json['location'] != null + ? Location.fromJson(json['location']) + : null; + viewport = json['viewport'] != null + ? Viewport.fromJson(json['viewport']) + : null; + } + + Map toJson() { + final Map data = {}; + if (location != null) { + data['location'] = location!.toJson(); + } + if (viewport != null) { + data['viewport'] = viewport!.toJson(); + } + return data; + } +} + +class Location { + double? lat; + double? lng; + + Location({this.lat, this.lng}); + + Location.fromJson(Map json) { + lat = json['lat']; + lng = json['lng']; + } + + Map toJson() { + final Map data = {}; + data['lat'] = lat; + data['lng'] = lng; + return data; + } +} + +class Viewport { + Location? northeast; + Location? southwest; + + Viewport({this.northeast, this.southwest}); + + Viewport.fromJson(Map json) { + northeast = json['northeast'] != null + ? Location.fromJson(json['northeast']) + : null; + southwest = json['southwest'] != null + ? Location.fromJson(json['southwest']) + : null; + } + + Map toJson() { + final Map data = {}; + if (northeast != null) { + data['northeast'] = northeast!.toJson(); + } + if (southwest != null) { + data['southwest'] = southwest!.toJson(); + } + return data; + } +} + +class Photos { + int? height; + List? htmlAttributions; + String? photoReference; + int? width; + + Photos({this.height, this.htmlAttributions, this.photoReference, this.width}); + + Photos.fromJson(Map json) { + height = json['height']; + htmlAttributions = json['html_attributions'].cast(); + photoReference = json['photo_reference']; + width = json['width']; + } + + Map toJson() { + final Map data = {}; + data['height'] = height; + data['html_attributions'] = htmlAttributions; + data['photo_reference'] = photoReference; + data['width'] = width; + return data; + } +} diff --git a/lib/product/shared/find_location_maps/model/prediction_model.dart b/lib/product/shared/find_location_maps/model/prediction_model.dart new file mode 100644 index 0000000..6ece4e9 --- /dev/null +++ b/lib/product/shared/find_location_maps/model/prediction_model.dart @@ -0,0 +1,157 @@ +class PlacesAutocompleteResponse { + List? predictions; + String? status; + + PlacesAutocompleteResponse({this.predictions, this.status}); + + PlacesAutocompleteResponse.fromJson(Map json) { + if (json['predictions'] != null) { + predictions = []; + json['predictions'].forEach((v) { + predictions!.add(Prediction.fromJson(v)); + }); + } + status = json['status']; + } + + Map toJson() { + final Map data = {}; + if (predictions != null) { + data['predictions'] = predictions!.map((v) => v.toJson()).toList(); + } + data['status'] = status; + return data; + } +} + +class Prediction { + String? description; + String? id; + List? matchedSubstrings; + String? placeId; + String? reference; + StructuredFormatting? structuredFormatting; + List? terms; + List? types; + String? lat; + String? lng; + + Prediction( + {this.description, + this.id, + this.matchedSubstrings, + this.placeId, + this.reference, + this.structuredFormatting, + this.terms, + this.types, + this.lat, + this.lng}); + + Prediction.fromJson(Map json) { + description = json['description']; + id = json['id']; + if (json['matched_substrings'] != null) { + matchedSubstrings = []; + json['matched_substrings'].forEach((v) { + matchedSubstrings!.add(MatchedSubstrings.fromJson(v)); + }); + } + placeId = json['place_id']; + reference = json['reference']; + structuredFormatting = json['structured_formatting'] != null + ? StructuredFormatting.fromJson(json['structured_formatting']) + : null; + if (json['terms'] != null) { + terms = []; + json['terms'].forEach((v) { + terms!.add(Terms.fromJson(v)); + }); + } + types = json['types'].cast(); + lat = json['lat']; + lng = json['lng']; + } + + Map toJson() { + final Map data = {}; + data['description'] = description; + data['id'] = id; + if (matchedSubstrings != null) { + data['matched_substrings'] = + matchedSubstrings!.map((v) => v.toJson()).toList(); + } + data['place_id'] = placeId; + data['reference'] = reference; + if (structuredFormatting != null) { + data['structured_formatting'] = structuredFormatting!.toJson(); + } + if (terms != null) { + data['terms'] = terms!.map((v) => v.toJson()).toList(); + } + data['types'] = types; + data['lat'] = lat; + data['lng'] = lng; + + return data; + } +} + +class MatchedSubstrings { + int? length; + int? offset; + + MatchedSubstrings({this.length, this.offset}); + + MatchedSubstrings.fromJson(Map json) { + length = json['length']; + offset = json['offset']; + } + + Map toJson() { + final Map data = {}; + data['length'] = length; + data['offset'] = offset; + return data; + } +} + +class StructuredFormatting { + String? mainText; + + String? secondaryText; + + StructuredFormatting({this.mainText, this.secondaryText}); + + StructuredFormatting.fromJson(Map json) { + mainText = json['main_text']; + + secondaryText = json['secondary_text']; + } + + Map toJson() { + final Map data = {}; + data['main_text'] = mainText; + data['secondary_text'] = secondaryText; + return data; + } +} + +class Terms { + int? offset; + String? value; + + Terms({this.offset, this.value}); + + Terms.fromJson(Map json) { + offset = json['offset']; + value = json['value']; + } + + Map toJson() { + final Map data = {}; + data['offset'] = offset; + data['value'] = value; + return data; + } +} diff --git a/lib/product/shared/find_location_maps/shared_map_search_location.dart b/lib/product/shared/find_location_maps/shared_map_search_location.dart new file mode 100644 index 0000000..72b4782 --- /dev/null +++ b/lib/product/shared/find_location_maps/shared_map_search_location.dart @@ -0,0 +1,313 @@ +// ignore_for_file: unnecessary_this, use_build_context_synchronously, prefer_typing_uninitialized_variables, library_private_types_in_public_api + +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'error/dio_handle_error.dart'; +import 'model/place_detail_model.dart'; +import 'model/prediction_model.dart'; + +// ignore: must_be_immutable +class NearBySearchSFM extends StatefulWidget { + InputDecoration inputDecoration; + ItemClick? itemClick; + GetPlaceDetailswWithLatLng? getPlaceDetailWithLatLng; + bool isLatLngRequired = true; + double locationLatitude; + double locationLongitude; + int radius; + TextStyle textStyle; + String googleAPIKey; + int debounceTime = 600; + List? countries = []; + TextEditingController textEditingController = TextEditingController(); + ListItemBuilder? itemBuilder; + TextInputAction? textInputAction; + Widget? seperatedBuilder; + void clearData; + BoxDecoration? boxDecoration; + bool isCrossBtnShown; + bool showError; + + NearBySearchSFM( + {super.key, + required this.textEditingController, + required this.googleAPIKey, + required this.locationLatitude, + required this.locationLongitude, + required this.radius, + this.debounceTime = 600, + this.inputDecoration = const InputDecoration(), + this.itemClick, + this.isLatLngRequired = true, + this.textStyle = const TextStyle(), + this.countries, + this.getPlaceDetailWithLatLng, + this.itemBuilder, + this.boxDecoration, + this.isCrossBtnShown = true, + this.seperatedBuilder, + this.showError = true, + this.textInputAction}); + + @override + _NearBySearchSFMState createState() => + _NearBySearchSFMState(); +} + +class _NearBySearchSFMState + extends State { + final subject = PublishSubject(); + OverlayEntry? _overlayEntry; + List alPredictions = []; + + TextEditingController controller = TextEditingController(); + final LayerLink _layerLink = LayerLink(); + bool isSearched = false; + + bool isCrossBtn = true; + late var _dio; + + CancelToken? _cancelToken = CancelToken(); + + @override + Widget build(BuildContext context) { + return CompositedTransformTarget( + link: _layerLink, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + alignment: Alignment.centerLeft, + decoration: widget.boxDecoration ?? + BoxDecoration( + shape: BoxShape.rectangle, + border: Border.all(color: Colors.grey, width: 0.6), + borderRadius: const BorderRadius.all(Radius.circular(10))), + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextFormField( + decoration: widget.inputDecoration, + style: widget.textStyle, + controller: widget.textEditingController, + onChanged: (string) { + subject.add(string); + if (widget.isCrossBtnShown) { + isCrossBtn = string.isNotEmpty ? true : false; + setState(() {}); + } + }, + ), + ), + (!widget.isCrossBtnShown) + ? const SizedBox() + : isCrossBtn && _showCrossIconWidget() + ? IconButton(onPressed: clearData, icon: Icon(Icons.close)) + : const SizedBox() + ], + ), + ), + ); + } + + getLocation( + String text, double latitude, double longitude, int radius) async { + // String url = + // "https://maps.googleapis.com/maps/api/place/nearbysearch/json?keyword=$text&location=$latitude%2C$longitude&radius=$radius&type=hospital|pharmacy|doctor&key=${widget.googleAPIKey}"; + + String url = + "https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$text&location=$latitude%2C$longitude&radius=$radius&strictbounds=true&key=${widget.googleAPIKey}"; + log(url); + if (widget.countries != null) { + for (int i = 0; i < widget.countries!.length; i++) { + String country = widget.countries![i]; + + if (i == 0) { + url = url + "&components=country:$country"; + } else { + url = url + "|" + "country:" + country; + } + } + } + + if (_cancelToken?.isCancelled == false) { + _cancelToken?.cancel(); + _cancelToken = CancelToken(); + } + + try { + Response response = await _dio.get(url); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + Map map = response.data; + if (map.containsKey("error_message")) { + throw response.data; + } + + PlacesAutocompleteResponse subscriptionResponse = + PlacesAutocompleteResponse.fromJson(response.data); + + if (text.length == 0) { + alPredictions.clear(); + this._overlayEntry!.remove(); + return; + } + + isSearched = false; + alPredictions.clear(); + if (subscriptionResponse.predictions!.isNotEmpty && + (widget.textEditingController.text.toString().trim()).isNotEmpty) { + alPredictions.addAll(subscriptionResponse.predictions!); + } + + this._overlayEntry = null; + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry!); + } catch (e) { + var errorHandler = ErrorHandler.internal().handleError(e); + _showSnackBar("${errorHandler.message}"); + } + } + + @override + void initState() { + super.initState(); + _dio = Dio(); + subject.stream + .distinct() + .debounceTime(Duration(milliseconds: widget.debounceTime)) + .listen(textChanged); + } + + textChanged(String text) async { + getLocation(text, widget.locationLatitude, widget.locationLongitude, + widget.radius); + } + + OverlayEntry? _createOverlayEntry() { + if (context.findRenderObject() != null) { + RenderBox renderBox = context.findRenderObject() as RenderBox; + var size = renderBox.size; + var offset = renderBox.localToGlobal(Offset.zero); + return OverlayEntry( + builder: (context) => Positioned( + left: offset.dx, + top: size.height + offset.dy, + width: size.width, + child: CompositedTransformFollower( + showWhenUnlinked: false, + link: this._layerLink, + offset: Offset(0.0, size.height + 5.0), + child: Material( + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: alPredictions.length, + separatorBuilder: (context, pos) => + widget.seperatedBuilder ?? SizedBox(), + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () { + var selectedData = alPredictions[index]; + if (index < alPredictions.length) { + widget.itemClick!(selectedData); + + if (widget.isLatLngRequired) { + getPlaceDetailsFromPlaceId(selectedData); + } + removeOverlay(); + } + }, + child: widget.itemBuilder != null + ? widget.itemBuilder!( + context, index, alPredictions[index]) + : Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.all(5), + child: Text(alPredictions[index].description!)), + ); + }, + )), + ), + )); + } + return null; + } + + removeOverlay() { + alPredictions.clear(); + this._overlayEntry = this._createOverlayEntry(); + Overlay.of(context).insert(this._overlayEntry!); + this._overlayEntry!.markNeedsBuild(); + } + + Future getPlaceDetailsFromPlaceId(Prediction prediction) async { + //String key = GlobalConfiguration().getString('google_maps_key'); + + var url = + "https://maps.googleapis.com/maps/api/place/details/json?placeid=${prediction.placeId}&key=${widget.googleAPIKey}"; + Response response = await Dio().get( + url, + ); + + PlaceDetails placeDetails = PlaceDetails.fromJson(response.data); + + prediction.lat = placeDetails.result!.geometry!.location!.lat.toString(); + prediction.lng = placeDetails.result!.geometry!.location!.lng.toString(); + + widget.getPlaceDetailWithLatLng!(prediction); + return null; + } + + void clearData() { + widget.textEditingController.clear(); + if (_cancelToken?.isCancelled == false) { + _cancelToken?.cancel(); + } + + setState(() { + alPredictions.clear(); + isCrossBtn = false; + }); + + if (this._overlayEntry != null) { + try { + this._overlayEntry?.remove(); + } catch (e) {} + } + } + + _showCrossIconWidget() { + return (widget.textEditingController.text.isNotEmpty); + } + + _showSnackBar(String errorData) { + if (widget.showError) { + final snackBar = SnackBar( + content: Text("$errorData"), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + } +} + +PlacesAutocompleteResponse parseResponse(Map responseBody) { + return PlacesAutocompleteResponse.fromJson( + responseBody as Map); +} + +PlaceDetails parsePlaceDetailMap(Map responseBody) { + return PlaceDetails.fromJson(responseBody as Map); +} + +typedef ItemClick = void Function(Prediction postalCodeResponse); +typedef GetPlaceDetailswWithLatLng = void Function( + Prediction postalCodeResponse); + +typedef ListItemBuilder = Widget Function( + BuildContext context, int index, Prediction prediction); diff --git a/lib/product/shared/model/district_model.dart b/lib/product/shared/model/district_model.dart new file mode 100644 index 0000000..2d22214 --- /dev/null +++ b/lib/product/shared/model/district_model.dart @@ -0,0 +1,43 @@ +class District { + String? code; + String? name; + String? nameEn; + String? fullName; + String? fullNameEn; + String? codeName; + String? provinceCode; + String? type; + + District({ + this.code, + this.name, + this.nameEn, + this.fullName, + this.fullNameEn, + this.codeName, + this.provinceCode, + this.type, + }); + + District.fromJson(Map json) { + code = json['code']; + name = json['name']; + nameEn = json['name_en']; + fullName = json['full_name']; + fullNameEn = json['full_name_en']; + codeName = json['code_name']; + provinceCode = json['parent']; + type = json['type']; + } + + static List fromJsonList(List list) { + return list.map((e) => District.fromJson(e)).toList(); + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => District.fromJson(e)).toList(); + } + + // @override + // String toString() => fullName!; +} diff --git a/lib/product/shared/model/near_by_search_model.dart b/lib/product/shared/model/near_by_search_model.dart new file mode 100644 index 0000000..a26561e --- /dev/null +++ b/lib/product/shared/model/near_by_search_model.dart @@ -0,0 +1,172 @@ +class NearbySearch { + List? results; + String? status; + + NearbySearch({ + this.results, + this.status, + }); + + NearbySearch.fromJson(Map json) { + if (json['results'] != null) { + results = []; + json['results'].forEach((v) { + results!.add(Result.fromJson(v)); + }); + } + status = json['status']; + } + + Map toJson() { + final Map data = {}; + if (results != null) { + data['result'] = results!.map((v) => v.toJson()).toList(); + } + data['status'] = status; + return data; + } +} + +class PlaceDetails { + Result? result; + String? status; + + PlaceDetails({this.result, this.status}); + + PlaceDetails.fromJson(Map json) { + result = json['result'] != null ? Result.fromJson(json['result']) : null; + status = json['status']; + } + + Map toJson() { + final Map data = {}; + + if (result != null) { + data['result'] = result!.toJson(); + } + data['status'] = status; + return data; + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => PlaceDetails.fromJson(e)).toList(); + } +} + +class Result { + Geometry? geometry; + String? icon; + String? name; + String? placeId; + String? vicinity; + String? adress; + String? formattedAddress; + String? formattedPhoneNumber; + String? phoneNumber; + OpeningHours? openingHours; + + Result({ + this.geometry, + this.icon, + this.name, + this.placeId, + this.vicinity, + this.formattedPhoneNumber, + this.phoneNumber, + this.openingHours, + this.adress, + this.formattedAddress, + }); + + Result.fromJson(Map json) { + geometry = + json['geometry'] != null ? Geometry.fromJson(json['geometry']) : null; + icon = json['icon']; + name = json['name']; + placeId = json['place_id']; + vicinity = json['vicinity']; + adress = json['adr_address']; + formattedPhoneNumber = json['formatted_phone_number'] ?? ""; + phoneNumber = json['international_phone_number'] ?? ""; + formattedAddress = json['formatted_address']; + openingHours = json['opening_hours'] != null + ? OpeningHours.fromJson(json['opening_hours']) + : null; + } + + Map toJson() { + final Map data = {}; + if (geometry != null) { + data['geometry'] = geometry!.toJson(); + } + data['icon'] = icon; + data['name'] = name; + data['vicinity'] = vicinity; + data['adr_address'] = adress; + data['formatted_address'] = formattedAddress; + data['international_phone_number'] = phoneNumber; + data['formatted_phone_number'] = formattedPhoneNumber; + if (openingHours != null) { + data['opening_hours'] = openingHours!.toJson(); + } + return data; + } +} + +class Geometry { + Location? location; + Geometry({ + this.location, + }); + + Geometry.fromJson(Map json) { + location = + json['location'] != null ? Location.fromJson(json['location']) : null; + } + Map toJson() { + final Map data = {}; + if (location != null) { + data['location'] = location!.toJson(); + } + return data; + } +} + +class Location { + double? lat; + double? lng; + + Location({ + this.lat, + this.lng, + }); + + Location.fromJson(Map json) { + lat = json['lat']; + lng = json['lng']; + } + + Map toJson() { + final Map data = {}; + data['lat'] = lat; + data['lng'] = lng; + return data; + } +} + +class OpeningHours { + bool? openNow; + + OpeningHours({ + this.openNow, + }); + OpeningHours.fromJson(Map json) { + openNow = json['open_now']; + } + + Map toJson() { + final Map data = {}; + data['open_now'] = openNow; + return data; + } +} diff --git a/lib/product/shared/model/province_model.dart b/lib/product/shared/model/province_model.dart new file mode 100644 index 0000000..dde39c7 --- /dev/null +++ b/lib/product/shared/model/province_model.dart @@ -0,0 +1,42 @@ +class Province { + String? code; + String? name; + String? nameEn; + String? fullName; + String? fullNameEn; + String? codeName; + String? parent; + String? type; + + Province( + {this.code, + this.name, + this.nameEn, + this.fullName, + this.fullNameEn, + this.codeName, + this.parent, + this.type}); + + Province.fromJson(Map json) { + code = json['code']; + name = json['name']; + nameEn = json['name_en']; + fullName = json['full_name']; + fullNameEn = json['full_name_en']; + codeName = json['code_name']; + parent = json['parent']; + type = json['type']; + } + + static List fromJsonList(List list) { + return list.map((e) => Province.fromJson(e)).toList(); + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => Province.fromJson(e)).toList(); + } + + @override + String toString() => fullName!; +} diff --git a/lib/product/shared/model/ward_model.dart b/lib/product/shared/model/ward_model.dart new file mode 100644 index 0000000..7ae0413 --- /dev/null +++ b/lib/product/shared/model/ward_model.dart @@ -0,0 +1,43 @@ +class Ward { + String? code; + String? name; + String? nameEn; + String? fullName; + String? fullNameEn; + String? codeName; + String? districtCode; + String? type; + + Ward({ + this.code, + this.name, + this.nameEn, + this.fullName, + this.fullNameEn, + this.codeName, + this.districtCode, + this.type, + }); + + Ward.fromJson(Map json) { + code = json['code']; + name = json['name']; + nameEn = json['name_en']; + fullName = json['full_name']; + fullNameEn = json['full_name_en']; + codeName = json['code_name']; + districtCode = json['parent']; + type = json['type']; + } + + static List fromJsonList(List list) { + return list.map((e) => Ward.fromJson(e)).toList(); + } + + static List fromJsonDynamicList(List list) { + return list.map((e) => Ward.fromJson(e)).toList(); + } + + @override + String toString() => fullName!; +} diff --git a/lib/product/shared/shared_background.dart b/lib/product/shared/shared_background.dart new file mode 100644 index 0000000..8df40f4 --- /dev/null +++ b/lib/product/shared/shared_background.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:sfm_app/product/constant/image/image_constants.dart'; + +class SharedBackground extends StatelessWidget { + final Widget child; + const SharedBackground({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return Directionality( + textDirection: TextDirection.ltr, + child: Scaffold( + resizeToAvoidBottomInset: false, + body: SizedBox( + height: size.height, + width: double.infinity, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + top: 0, + left: 0, + child: Image.asset( + ImageConstants.instance.getImage("background_top"), + width: size.width * 0.3, + ), + ), + Positioned( + bottom: 0, + right: 0, + child: Image.asset( + ImageConstants.instance.getImage("background_bottom"), + width: size.width * 0.4, + ), + ), + SafeArea(child: child) + ], + ), + ), + ), + ); + } +} diff --git a/lib/product/shared/shared_input_decoration.dart b/lib/product/shared/shared_input_decoration.dart new file mode 100644 index 0000000..0a9eaa7 --- /dev/null +++ b/lib/product/shared/shared_input_decoration.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import '../extention/context_extention.dart'; + +InputDecoration borderRadiusTopLeftAndBottomRight( + BuildContext context, String hintText) => + InputDecoration( + hintText: hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), bottomRight: Radius.circular(12)), + )); + +InputDecoration borderRadiusAll(BuildContext context, String hintText) => + InputDecoration( + hintText: hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.all(context.normalRadius), + )); diff --git a/lib/product/shared/shared_snack_bar.dart b/lib/product/shared/shared_snack_bar.dart new file mode 100644 index 0000000..4a73520 --- /dev/null +++ b/lib/product/shared/shared_snack_bar.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:sfm_app/product/extention/context_extention.dart'; +import 'package:top_snackbar_flutter/custom_snack_bar.dart'; +import 'package:top_snackbar_flutter/top_snack_bar.dart'; + +void showNoIconTopSnackBar(BuildContext context, String message, + Color backgroundColor, Color textColor) { + if (!context.mounted) return; + showTopSnackBar( + Overlay.of(context), + Card( + color: backgroundColor, + child: Container( + margin: const EdgeInsets.only(left: 20), + height: 50, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + message, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: textColor, + ), + ), + ), + ), + ), + displayDuration: context.lowDuration); +} + +void showSuccessTopSnackBarCustom(BuildContext context, String message) { + if (!context.mounted) return; + showTopSnackBar( + Overlay.of(context), + SizedBox( + height: 60, + child: CustomSnackBar.success( + message: message, + icon: const Icon(Icons.sentiment_very_satisfied_outlined, + color: Color(0x15000000), size: 80))), + displayDuration: context.lowDuration); +} + +void showErrorTopSnackBarCustom(BuildContext context, String message) { + if (!context.mounted) return; + showTopSnackBar( + Overlay.of(context), + SizedBox( + height: 60, + child: CustomSnackBar.error( + message: message, + icon: const Icon(Icons.sentiment_dissatisfied_outlined, + color: Color(0x15000000), size: 80))), + displayDuration: context.lowDuration); +} diff --git a/lib/product/shared/shared_transition.dart b/lib/product/shared/shared_transition.dart new file mode 100644 index 0000000..f35b9ff --- /dev/null +++ b/lib/product/shared/shared_transition.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +/// Hiệu ứng di chuyển từ phải qua trái +Widget transitionsRightToLeft(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + + return SlideTransition( + position: offsetAnimation, + child: child, + ); +} + +/// Hiệu ứng di chuyển từ trái sang phải +Widget transitionsLeftToRight(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const begin = Offset(-1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + + return SlideTransition( + position: offsetAnimation, + child: child, + ); +} + +/// Hiệu ứng di chuyển từ dưới lên trên +Widget transitionsBottomToTop(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const begin = Offset(0.0, 1.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + + return SlideTransition( + position: offsetAnimation, + child: child, + ); +} + +/// Hiệu ứng di chuyển từ trên xuống dưới +Widget transitionsTopToBottom(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const begin = Offset(0.0, -1.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + + return SlideTransition( + position: offsetAnimation, + child: child, + ); +} + +/// Hiệu ứng mở dần vào và ra +Widget transitionsFaded(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const begin = 0.0; + const end = 1.0; + const curve = Curves.easeInOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var fadeAnimation = animation.drive(tween); + + return FadeTransition( + opacity: fadeAnimation, + child: child, + ); +} + +/// Hiệu ứng di chuyển từ kích thước nhỏ đến đầy đủ +Widget transitionsScale(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const begin = 0.0; // Bắt đầu từ kích thước 0 + const end = 1.0; // Kết thúc ở kích thước 1 + const curve = Curves.easeInOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var scaleAnimation = animation.drive(tween); + + return ScaleTransition( + scale: scaleAnimation, + child: child, + ); +} + +Widget transitionsTotation(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const begin = 0.0; // Bắt đầu từ góc 0 độ + const end = 1.0; // Kết thúc ở góc 1 vòng (360 độ) + const curve = Curves.easeInOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var rotationAnimation = animation.drive(tween); + + return RotationTransition( + turns: rotationAnimation, + child: child, + ); +} + +/// Hiệu ứng kết hợp (Di chuyển và mờ dần) +Widget transitionsCustom1(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + const beginOffset = Offset(1.0, 0.0); // Di chuyển từ phải vào + const endOffset = Offset.zero; + const beginOpacity = 0.0; // Bắt đầu từ độ mờ 0 + const endOpacity = 1.0; // Kết thúc ở độ mờ 1 + + var offsetTween = Tween(begin: beginOffset, end: endOffset); + var opacityTween = Tween(begin: beginOpacity, end: endOpacity); + + var offsetAnimation = + animation.drive(offsetTween.chain(CurveTween(curve: Curves.easeInOut))); + var opacityAnimation = + animation.drive(opacityTween.chain(CurveTween(curve: Curves.easeInOut))); + + return FadeTransition( + opacity: opacityAnimation, + child: SlideTransition( + position: offsetAnimation, + child: child, + ), + ); +} diff --git a/lib/product/theme/app_theme.dart b/lib/product/theme/app_theme.dart new file mode 100644 index 0000000..0d38495 --- /dev/null +++ b/lib/product/theme/app_theme.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +abstract class AppTheme { + ThemeData? theme; +} diff --git a/lib/product/theme/app_theme_dark.dart b/lib/product/theme/app_theme_dark.dart new file mode 100644 index 0000000..2a55f08 --- /dev/null +++ b/lib/product/theme/app_theme_dark.dart @@ -0,0 +1,53 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; +import 'package:sfm_app/product/theme/app_theme.dart'; + +class AppThemeDark extends AppTheme { + static AppThemeDark? _instance; + static AppThemeDark get instance { + _instance ??= AppThemeDark._init(); + return _instance!; + } + + AppThemeDark._init(); + + @override + ThemeData get theme => FlexThemeData.dark( + useMaterial3: true, + scheme: FlexScheme.flutterDash, + subThemesData: const FlexSubThemesData( + inputDecoratorRadius: 30, + interactionEffects: true, + tintedDisabledControls: true, + blendOnColors: true, + useM2StyleDividerInM3: true, + inputDecoratorBorderType: FlexInputBorderType.outline, + tooltipRadius: 20, + tooltipWaitDuration: Duration(milliseconds: 500), + tooltipShowDuration: Duration(milliseconds: 500), + navigationRailUseIndicator: true, + navigationRailLabelType: NavigationRailLabelType.all, + ), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + ); + + // ThemeData.dark().copyWith( + // useMaterial3: true, + // colorScheme: _buildColorScheme, + // ); + + // ColorScheme get _buildColorScheme => FlexColorScheme.dark().toScheme; + // ColorScheme( + // brightness: Brightness.dark, + // primary: Colors.blue.shade900, + // onPrimary: Colors.blue, + // secondary: Colors.white, + // onSecondary: Colors.white70, + // error: Colors.red, + // onError: Colors.orange, + // background: Colors.black, + // onBackground: Colors.white70, + // surface: Colors.grey, + // onSurface: Colors.white, + // ); +} diff --git a/lib/product/theme/app_theme_light.dart b/lib/product/theme/app_theme_light.dart new file mode 100644 index 0000000..92e5675 --- /dev/null +++ b/lib/product/theme/app_theme_light.dart @@ -0,0 +1,54 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; +import 'package:sfm_app/product/theme/app_theme.dart'; + +class AppThemeLight extends AppTheme { + static AppThemeLight? _instance; + static AppThemeLight get instance { + _instance ??= AppThemeLight._init(); + return _instance!; + } + + AppThemeLight._init(); + + @override + ThemeData get theme => FlexThemeData.light( + useMaterial3: true, + scheme: FlexScheme.flutterDash, + bottomAppBarElevation: 20.0, + subThemesData: const FlexSubThemesData( + inputDecoratorRadius: 30, + interactionEffects: true, + tintedDisabledControls: true, + useM2StyleDividerInM3: true, + inputDecoratorBorderType: FlexInputBorderType.outline, + tooltipRadius: 20, + tooltipWaitDuration: Duration(milliseconds: 1600), + tooltipShowDuration: Duration(milliseconds: 1500), + navigationRailUseIndicator: true, + navigationRailLabelType: NavigationRailLabelType.all, + ), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + ); + // ThemeData.light().copyWith( + // useMaterial3: true, + // colorScheme: _buildColorScheme, + // ); + + // ColorScheme get _buildColorScheme => FlexColorScheme.light( + + // ).toScheme; + // const ColorScheme( + // brightness: Brightness.light, + // primary: Colors.black, + // onPrimary: Colors.white, + // secondary: Colors.black, + // onSecondary: Colors.black45, + // error: Colors.red, + // onError: Colors.orange, + // background: Colors.white, + // onBackground: Colors.red, + // surface: Colors.white, + // onSurface: Colors.black, + // ); +} diff --git a/lib/product/theme/provider/app_provider.dart b/lib/product/theme/provider/app_provider.dart new file mode 100644 index 0000000..39091ec --- /dev/null +++ b/lib/product/theme/provider/app_provider.dart @@ -0,0 +1,23 @@ +import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; + +import '../theme_notifier.dart'; + +class ApplicationProvider { + static ApplicationProvider? _instance; + static ApplicationProvider get instance { + _instance ??= ApplicationProvider._init(); + return _instance!; + } + + ApplicationProvider._init(); + + List singleItems = []; + List dependItems = [ + ChangeNotifierProvider( + create: (context) => ThemeNotifier(), + ), + // Provider.value(value: NavigationService.instance) + ]; + List uiChangesItems = []; +} diff --git a/lib/product/theme/theme_notifier.dart b/lib/product/theme/theme_notifier.dart new file mode 100644 index 0000000..0e35247 --- /dev/null +++ b/lib/product/theme/theme_notifier.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'app_theme_dark.dart'; +import 'app_theme_light.dart'; + +import '../constant/enums/app_theme_enums.dart'; + +class ThemeNotifier extends ChangeNotifier { + // ThemeData _currentTheme = AppThemeLight.instance.theme; + // // ThemeData get currentTheme => LocaleManager.instance.getStringValue(PreferencesKeys.THEME) == + // // AppThemes.LIGHT.name + // // ? AppThemeLight.instance.theme + // // : AppThemeDark.instance.theme; + // ThemeData get currentTheme { + // log("ThemeKey: ${LocaleManager.instance.getStringValue(PreferencesKeys.THEME)}"); + // if (LocaleManager.instance.getStringValue(PreferencesKeys.THEME) == + // AppThemes.LIGHT.name) { + // log("light"); + // } else { + // log("dark"); + // } + // return LocaleManager.instance.getStringValue(PreferencesKeys.THEME) == + // AppThemes.LIGHT.name + // ? AppThemeLight.instance.theme + // : AppThemeDark.instance.theme; + // } + + // ThemeData _currentTheme = AppThemeLight.instance.theme; // Mặc định là light + + // ThemeNotifier() { + // loadThemeFromPreferences(); + // } + + // Future loadThemeFromPreferences() async { + // String themeKey = + // LocaleManager.instance.getStringValue(PreferencesKeys.THEME); + // log("ThemeNotifierKey:$themeKey "); + // if (themeKey == AppThemes.LIGHT.name) { + // _currentTheme = AppThemeLight.instance.theme; + // } else { + // _currentTheme = AppThemeDark.instance.theme; + // } + // notifyListeners(); // Thông báo cho các widget lắng nghe + // } + + // ThemeData get currentTheme => _currentTheme; + + // AppThemes _currenThemeEnum = AppThemes.LIGHT; + // AppThemes get currenThemeEnum => + // LocaleManager.instance.getStringValue(PreferencesKeys.THEME) == + // AppThemes.LIGHT.name + // ? AppThemes.LIGHT + // : AppThemes.DARK; + // // AppThemes get currenThemeEnum => _currenThemeEnum; + + // void changeValue(AppThemes theme) { + // if (theme == AppThemes.LIGHT) { + // _currentTheme = ThemeData.dark(); + // } else { + // _currentTheme = ThemeData.light(); + // } + // notifyListeners(); + // } + + // void changeTheme() { + // if (_currenThemeEnum == AppThemes.LIGHT) { + // _currentTheme = AppThemeDark.instance.theme; + // _currenThemeEnum = AppThemes.DARK; + // LocaleManager.instance + // .setString(PreferencesKeys.THEME, AppThemes.DARK.name); + // } else { + // _currentTheme = AppThemeLight.instance.theme; + // _currenThemeEnum = AppThemes.LIGHT; + // LocaleManager.instance + // .setString(PreferencesKeys.THEME, AppThemes.LIGHT.name); + // } + // notifyListeners(); + // } + + ThemeData _currentTheme = AppThemeLight.instance.theme; + ThemeData get currentTheme => _currentTheme; + + AppThemes _currenThemeEnum = AppThemes.LIGHT; + AppThemes get currenThemeEnum => _currenThemeEnum; + + void changeValue(AppThemes theme) { + if (theme == AppThemes.LIGHT) { + _currentTheme = AppThemeLight.instance.theme; + } else { + _currentTheme = AppThemeDark.instance.theme; + } + notifyListeners(); + } + + void changeTheme() { + if (_currenThemeEnum == AppThemes.LIGHT) { + _currentTheme = AppThemeDark.instance.theme; + _currenThemeEnum = AppThemes.DARK; + } else { + _currentTheme = AppThemeLight.instance.theme; + _currenThemeEnum = AppThemes.LIGHT; + } + notifyListeners(); + } +} diff --git a/lib/product/utils/date_time_utils.dart b/lib/product/utils/date_time_utils.dart new file mode 100644 index 0000000..1a6bb44 --- /dev/null +++ b/lib/product/utils/date_time_utils.dart @@ -0,0 +1,16 @@ +import 'package:intl/intl.dart'; + +class DateTimeUtils { + DateTimeUtils._init(); + static DateTimeUtils? _instance; + static DateTimeUtils get instance => _instance ??= DateTimeUtils._init(); + + String formatDateTimeToString(DateTime dateTime) { + return DateFormat('yyyy-MM-dd\'T\'00:00:00\'Z\'').format(dateTime); + } + + String convertCurrentMillisToDateTimeString(int time) { + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch((time) * 1000); + return DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime); + } +} diff --git a/lib/product/utils/device_utils.dart b/lib/product/utils/device_utils.dart new file mode 100644 index 0000000..8ae0f2a --- /dev/null +++ b/lib/product/utils/device_utils.dart @@ -0,0 +1,269 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:sfm_app/feature/log/device_logs_model.dart'; +import 'package:sfm_app/product/services/api_services.dart'; +import 'package:sfm_app/product/services/language_services.dart'; +import 'package:sfm_app/product/shared/model/district_model.dart'; +import 'package:sfm_app/product/shared/model/province_model.dart'; + +import '../../feature/devices/device_model.dart'; +import '../shared/model/ward_model.dart'; + +class DeviceUtils { + DeviceUtils._init(); + static DeviceUtils? _instance; + static DeviceUtils get instance => _instance ??= DeviceUtils._init(); + APIServices apiServices = APIServices(); + + Map getDeviceSensors( + BuildContext context, List sensors) { + Map map = {}; + if (sensors.isEmpty) { + map['sensorState'] = appLocalization(context).no_data_message; + map['sensorBattery'] = appLocalization(context).no_data_message; + map['sensorCsq'] = appLocalization(context).no_data_message; + map['sensorTemp'] = appLocalization(context).no_data_message; + map['sensorHum'] = appLocalization(context).no_data_message; + map['sensorVolt'] = appLocalization(context).no_data_message; + } else { + for (var sensor in sensors) { + if (sensor.name == "1") { + if (sensor.value == 0) { + map['sensorIn'] = "sensor 0"; + } else { + map['sensorIn'] = "sensor 1"; + } + } + if (sensor.name == "2") { + if (sensor.value == 0) { + map['sensorMove'] = appLocalization(context).gf_not_move_message; + } else { + map['sensorMove'] = appLocalization(context).gf_moving_message; + } + } + if (sensor.name == "3") { + if (sensor.value == 0) { + map['sensorAlarm'] = appLocalization(context).normal_message; + } else { + map['sensorAlarm'] = + appLocalization(context).warning_status_message; + } + } + if (sensor.name == "6") { + if (sensor.value! > 0 && sensor.value! < 12) { + map['sensorCsq'] = appLocalization(context).low_message_uppercase; + } else if (sensor.value! >= 12 && sensor.value! < 20) { + map['sensorCsq'] = + appLocalization(context).moderate_message_uppercase; + } else if (sensor.value! >= 20) { + map['sensorCsq'] = appLocalization(context).good_message_uppercase; + } else { + map['sensorCsq'] = appLocalization(context).gf_no_signal_message; + } + } + if (sensor.name == "7") { + map['sensorVolt'] = "${(sensor.value!) / 1000} V"; + } + if (sensor.name == "8") { + map['sensorTemp'] = "${sensor.value}°C"; + } + if (sensor.name == "9") { + map['sensorHum'] = "${sensor.value} %"; + } + if (sensor.name == "10") { + map['sensorBattery'] = "${sensor.value}"; + } + if (sensor.name == "11") { + if (sensor.value == 0) { + map['sensorState'] = appLocalization(context).normal_message; + } else if (sensor.value == 1) { + map['sensorState'] = + appLocalization(context).smoke_detecting_message; + } else if (sensor.value == 3) { + map['sensorState'] = + appLocalization(context).gf_remove_from_base_message; + } else { + map['sensorState'] = appLocalization(context).undefine_message; + } + } + } + } + return map; + } + + Future getFullDeviceLocation( + BuildContext context, String areaPath) async { + if (areaPath != "") { + List parts = areaPath.split('_'); + + String provinceID = parts[0]; + String districtID = parts[1]; + String wardID = parts[2]; + + String provinceBody = await apiServices.getProvinceByID(provinceID); + final provinceItem = jsonDecode(provinceBody); + Province province = Province.fromJson(provinceItem['data']); + String districtBody = await apiServices.getDistrictByID(districtID); + final districtItem = jsonDecode(districtBody); + District district = District.fromJson(districtItem['data']); + String wardBody = await apiServices.getWardByID(wardID); + final wardItem = jsonDecode(wardBody); + Ward ward = Ward.fromJson(wardItem['data']); + + return "${ward.fullName}, ${district.fullName}, ${province.fullName}"; + } + return appLocalization(context).no_data_message; + } + + String checkStateDevice(BuildContext context, int state) { + String message = appLocalization(context).no_data_message; + if (state == 1) { + message = appLocalization(context).smoke_detecting_message; + } else if (state == 0) { + message = appLocalization(context).normal_message; + } else if (state == -1) { + message = appLocalization(context).disconnect_message_uppercase; + } else if (state == 2) { + message = appLocalization(context).in_progress_message; + } else if (state == 3) { + message = appLocalization(context).in_progress_message; + } + return message; + } + + List sortDeviceByState(List devices) { + List sortedDevices = List.from(devices); + sortedDevices.sort((a, b) { + int stateOrder = [2, 1, 3, 0, -1, 3].indexOf(a.state!) - + [2, 1, 3, 0, -1, 3].indexOf(b.state!); + return stateOrder; + }); + + return sortedDevices; + } + + Color getTableRowColor(int state) { + if (state == 1) { + return Colors.red; + } else if (state == 0) { + return Colors.green; + } else { + return Colors.grey; + } + } + + String getDeviceSensorsLog(BuildContext context, SensorLogs sensor) { + String message = ""; + if (sensor.detail!.username != null || sensor.detail!.note != null) { + message = "${appLocalization(context).bell_user_uppercase} "; + if (sensor.name! == "0") { + if (sensor.value! == 3) { + String state = "đã thông báo rằng đây là cháy giả"; + message = message + state; + } + } + } else { + message = "${appLocalization(context).device_title} "; + if (sensor.name == "11") { + String state = checkStateDevice(context, sensor.value!); + message = message + state; + } else if (sensor.name == "2") { + String state = getDeviceMove(context, sensor.value!); + message = message + state; + } else if (sensor.name == "6") { + String state = getDeviceCSQ(context, sensor.value!); + message = message + state; + } else if (sensor.name == "7") { + String state = getDeviceVolt(context, sensor.value!); + message = message + state; + } else if (sensor.name == "8") { + String state = getDeviceTemp(context, sensor.value!); + message = message + state; + } else if (sensor.name == "9") { + String state = getDeviceHum(context, sensor.value!); + message = message + state; + } else if (sensor.name == "10") { + String state = getDeviceBattery(context, sensor.value!); + message = message + state; + } else { + String state = appLocalization(context).undefine_message; + message = message + state; + } + } + return message; + } + + String getDeviceCSQ(BuildContext context, int number) { + if (number >= 0 && number < 12) { + return appLocalization(context).gf_weak_signal_message; + } else if (number >= 12 && number < 20) { + return appLocalization(context).gf_moderate_signal_message; + } else if (number >= 20) { + return appLocalization(context).gf_good_signal_message; + } + return appLocalization(context).gf_no_signal_message; + } + + String getDeviceVolt(BuildContext context, int number) { + return "${appLocalization(context).gf_volt_detect_message} ${number / 1000} V"; + } + + String getDeviceTemp(BuildContext context, int number) { + return "${appLocalization(context).gf_temp_detect_message} $number °C"; + } + + String getDeviceHum(BuildContext context, int number) { + return "${appLocalization(context).gf_hum_detect_message} $number%"; + } + + String getDeviceBattery(BuildContext context, int number) { + return "${appLocalization(context).gf_battery_detect_message} $number%"; + } + + String getDeviceMove(BuildContext context, int number) { + String message = ""; + if (number == 0) { + message = appLocalization(context).gf_not_move_message; + } else { + message = appLocalization(context).gf_moving_message; + } + return message; + } + + IconData getBatteryIcon(int battery) { + if (battery <= 10) { + return Icons.battery_alert; + } else if (battery <= 40) { + return Icons.battery_2_bar; + } else if (battery <= 70) { + return Icons.battery_4_bar; + } else if (battery <= 90) { + return Icons.battery_6_bar; + } else { + return Icons.battery_full_rounded; + } + } + + IconData getSignalIcon(BuildContext context, String signal) { + if (signal == appLocalization(context).gf_weak_signal_message) { + return Icons.signal_cellular_alt_1_bar; + } else if (signal == appLocalization(context).gf_moderate_signal_message) { + return Icons.signal_cellular_alt_2_bar; + } else { + return Icons.signal_cellular_alt; + } + } + + Color getColorRiple(int state) { + if (state == 1) { + return Colors.red; + } else if (state == 3) { + return Colors.orange; + } else if (state == -1) { + return Colors.grey; + } else { + return Colors.green; + } + } +} diff --git a/lib/product/utils/qr_utils.dart b/lib/product/utils/qr_utils.dart new file mode 100644 index 0000000..1357a93 --- /dev/null +++ b/lib/product/utils/qr_utils.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; +import '../services/language_services.dart'; + +class QRScanUtils { + QRScanUtils._init(); + static QRScanUtils? _instance; + static QRScanUtils get instance => _instance ??= QRScanUtils._init(); + + Future scanQR(BuildContext context) async { + String barcodeScanRes; + // Platform messages may fail, so we use a try/catch PlatformException. + try { + barcodeScanRes = await FlutterBarcodeScanner.scanBarcode( + '#ffffff', appLocalization(context).cancel_button_content, true, ScanMode.QR); + } on PlatformException { + barcodeScanRes = 'Failed to get platform version.'; + } + return barcodeScanRes; + } + + Map getQRData(String data) { + if (data.isEmpty) { + return {}; + } else { + final parts = data.split('.'); + return { + 'group_id': parts.length == 2 ? parts[0] : '', + 'group_name': parts.length == 2 ? parts[1] : '', + }; + } + } +} \ No newline at end of file diff --git a/lib/product/utils/response_status_utils.dart b/lib/product/utils/response_status_utils.dart new file mode 100644 index 0000000..c1cb631 --- /dev/null +++ b/lib/product/utils/response_status_utils.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import '../constant/status_code/status_code_constants.dart'; +import '../shared/shared_snack_bar.dart'; + +void showSnackBarResponseByStatusCode(BuildContext context, int statusCode, + String successMessage, String failedMessage) async { + if (statusCode == StatusCodeConstants.OK || + statusCode == StatusCodeConstants.CREATED) { + showSuccessTopSnackBarCustom(context, successMessage); + } else { + showErrorTopSnackBarCustom(context, failedMessage); + } +} + +void showSnackBarResponseByStatusCodeNoIcon(BuildContext context, int statusCode, + String successMessage, String failedMessage) async { + if (statusCode == StatusCodeConstants.OK || + statusCode == StatusCodeConstants.CREATED) { + showNoIconTopSnackBar(context, successMessage,Colors.green, Colors.white); + } else { + showNoIconTopSnackBar(context, failedMessage, Colors.red, Colors.white); + } +} diff --git a/lib/product/utils/string_utils.dart b/lib/product/utils/string_utils.dart new file mode 100644 index 0000000..5557a57 --- /dev/null +++ b/lib/product/utils/string_utils.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +class StringUtils { + StringUtils._init(); + static StringUtils? _instance; + static StringUtils get instance => _instance ??= StringUtils._init(); + + List parseAddressFromMapAPI(String addressString) { + RegExp regex = RegExp(r'([^<]+)'); + List textSpans = []; + + Iterable matches = regex.allMatches(addressString); + for (Match match in matches) { + String cssClass = match.group(1)!; + String text = match.group(2)!; + if (cssClass == 'country-name') { + continue; + } + textSpans.add( + TextSpan( + text: text, + ), + ); + textSpans.add( + const TextSpan(text: ', '), + ); + } + + textSpans.removeLast(); + return textSpans; + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..c43473c --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,139 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "sfm_app") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.sfm_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..dbec55e --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) maps_launcher_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MapsLauncherPlugin"); + maps_launcher_plugin_register_with_registrar(maps_launcher_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..6c56d5b --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + maps_launcher + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..6c06c70 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "sfm_app"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "sfm_app"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..4fac21f --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import firebase_core +import firebase_messaging +import flutter_local_notifications +import geolocator_apple +import maps_launcher +import shared_preferences_foundation +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1ff4f1a --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,695 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* sfm_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sfm_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* sfm_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* sfm_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sfm_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/sfm_app"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sfm_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/sfm_app"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sfm_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/sfm_app"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..3e3d674 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..a885967 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = sfm_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.sfmApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..5418c9f --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..f7f0efc --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,919 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 + url: "https://pub.dev" + source: hosted + version: "1.3.16" + app_settings: + dependency: "direct main" + description: + name: app_settings + sha256: "09bc7fe0313a507087bec1a3baf555f0576e816a760cbb31813a88890a09d9e5" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + badges: + dependency: "direct main" + description: + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + dio: + dependency: "direct main" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + dropdown_button2: + dependency: "direct main" + description: + name: dropdown_button2 + sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1 + url: "https://pub.dev" + source: hosted + version: "2.3.9" + 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: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.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: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb" + url: "https://pub.dev" + source: hosted + version: "5.1.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" + flex_color_scheme: + dependency: "direct main" + description: + name: flex_color_scheme + sha256: "659cf59bd5ccaa1e7de9384342be8b666ff10b108ed57a7fd46c122fb8bf6aca" + url: "https://pub.dev" + source: hosted + version: "7.2.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_barcode_scanner: + dependency: "direct main" + description: + name: flutter_barcode_scanner + sha256: a4ba37daf9933f451a5e812c753ddd045d6354e4a3280342d895b07fecaab3fa + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + url: "https://pub.dev" + source: hosted + version: "17.2.4" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" + 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_polyline_points: + dependency: "direct main" + description: + name: flutter_polyline_points + sha256: "0e07862139cb65a88789cd6efbe284c0b6f1fcb5ed5ec87781759c48190d84b9" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + 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" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27" + url: "https://pub.dev" + source: hosted + version: "11.1.0" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "06e37fa32392f69f133e166ef6b358a8b6afddbf4c418fc236988184cc115a49" + url: "https://pub.dev" + source: hosted + version: "4.4.1" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: "6154ea2682563f69fc0125762ed7e91e7ed85d0b9776595653be33918e064807" + url: "https://pub.dev" + source: hosted + version: "2.3.8+1" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "07ee2436909f749d606f53521dc1725dd738dc5196e5ff815bc254253c594075" + url: "https://pub.dev" + source: hosted + version: "13.1.0" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "555d5d736339b0478e821167ac521c810d7b51c3b2734e6802a9f046b64ea37a" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + google_maps_cluster_manager: + dependency: "direct main" + description: + name: google_maps_cluster_manager + sha256: "36e9a4b2d831c470fc85d692a6c9cec70e0f385d578b9697de5f4de347561b83" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: ae66fef3e71261d7df2eff29b2a119e190b2884325ecaa55321b1e17b5504066 + url: "https://pub.dev" + source: hosted + version: "2.5.3" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: "714530f865f13bb3b9505c58821c3baed5d247a871724acf5d2ea5808fbed02c" + url: "https://pub.dev" + source: hosted + version: "2.6.2" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: c89556ed0338fcb4b843c9fcdafac7ad2df601c8b41286d82f0727f7f66434e4 + url: "https://pub.dev" + source: hosted + version: "2.3.6" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: "6060779f020638a8eedeb0fb14234818e5fa32ec45a4653d6428ab436e2bbc64" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: "05067c5aa762ebee44b7ef4902a311ed8cf891ef655e2798bae063aa3050c8d9" + url: "https://pub.dev" + source: hosted + version: "0.5.4+1" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" + source: hosted + version: "0.18.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + js_wrapping: + dependency: transitive + description: + name: js_wrapping + sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c + url: "https://pub.dev" + source: hosted + version: "0.7.4" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + maps_launcher: + dependency: "direct main" + description: + name: maps_launcher + sha256: "57ba3c31db96e30f58c23fcb22a1fac6accc5683535b2cf344c534bbb9f8f910" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" + source: hosted + version: "0.12.15" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + 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: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e + url: "https://pub.dev" + source: hosted + version: "11.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + search_choices: + dependency: "direct main" + description: + name: search_choices + sha256: "10a50815c0190922dadae81cce723266bfc57d134911565c37c96d4527312bde" + url: "https://pub.dev" + source: hosted + version: "2.2.11" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + simple_ripple_animation: + dependency: "direct main" + description: + name: simple_ripple_animation + sha256: "09a80a6d352ce12d33c2ca43423bf5b544179cc5bc78bf3ff73bd5c08ea378bd" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + 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: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + 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_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + top_snackbar_flutter: + dependency: "direct main" + description: + name: top_snackbar_flutter + sha256: "22d14664a13db6ac714934c3382bd8d4daa57fb888a672f922df71981c5a5cb2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + url: "https://pub.dev" + source: hosted + version: "6.1.14" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + 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: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + url: "https://pub.dev" + source: hosted + version: "2.0.19" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" + url: "https://pub.dev" + source: hosted + version: "4.2.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" +sdks: + dart: ">=3.0.1 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..c88cdf1 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,114 @@ +name: sfm_app +description: A new Flutter project. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ">=3.0.1 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + shared_preferences: ^2.2.2 + intl: ^0.18.0 + firebase_core: ^2.24.2 + firebase_messaging: ^14.7.10 + flutter_local_notifications: ^17.2.4 + permission_handler: ^11.0.1 + app_settings: ^5.1.1 + provider: ^6.1.2 + flex_color_scheme: ^7.2.0 + go_router: ^13.1.0 + http: ^1.1.0 + top_snackbar_flutter: ^3.1.0 + badges: ^3.1.2 + google_maps_cluster_manager: ^3.1.0 + google_maps_flutter: ^2.5.0 + dropdown_button2: ^2.3.9 + maps_launcher: ^2.2.1 + flutter_barcode_scanner: ^2.0.0 + search_choices: ^2.2.11 + dio: ^5.7.0 + rxdart: ^0.28.0 + geolocator: ^11.1.0 + qr_flutter: ^4.1.0 + flutter_polyline_points: ^2.0.0 + simple_ripple_animation: ^0.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + generate: true + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ + - assets/icons/ + + # 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 diff --git a/sfmTranslateErrors.txt b/sfmTranslateErrors.txt new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/sfmTranslateErrors.txt @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..515a95d --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. 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:sfm_app/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..1db9e07 --- /dev/null +++ b/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + sfm_app + + + + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..bbc0e5a --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "sfm_app", + "short_name": "sfm_app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..93d0e3c --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,102 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(sfm_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "sfm_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..930d207 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e7f561c --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + MapsLauncherPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MapsLauncherPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5feb846 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + firebase_core + geolocator_windows + maps_launcher + permission_handler_windows + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..88bc832 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "sfm_app" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "sfm_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "sfm_app.exe" "\0" + VALUE "ProductName", "sfm_app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b25e363 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..0b90f71 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"sfm_app", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_