Complete refactoring SFM App Source Code

This commit is contained in:
anhtunz
2024-12-15 00:59:02 +07:00
parent caa73ca43c
commit 2e27d59278
247 changed files with 18390 additions and 0 deletions

44
.gitignore vendored Normal file
View File

@@ -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

45
.metadata Normal file
View File

@@ -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'

29
analysis_options.yaml Normal file
View File

@@ -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

13
android/.gitignore vendored Normal file
View File

@@ -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

84
android/app/build.gradle Normal file
View File

@@ -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'
}

View File

@@ -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"
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,76 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- NOTE: the example app requests USE_EXACT_ALARM to make it easier to run the app.
Developers will need to check if their own app needs to use SCHEDULE_EXACT_ALARM instead -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- NOTE: Special use was selected as it's the closest match for this example app.
apps should specify the appropriate permission for their use cases. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- START Permissions Package -->
<!-- Permissions options for the `camera` group -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- Permissions options for the `location` group -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- END Permissions Package -->
<application
android:label="sfm_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyA9C7Pmxw6Gw3H2mM4WA_XGngRIIr2VS7k"/>
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:exported="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:stopWithTask="false"/>
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
</application>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>

View File

@@ -0,0 +1,6 @@
package com.example.sfm_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

34
android/build.gradle Normal file
View File

@@ -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
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -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

11
android/settings.gradle Normal file
View File

@@ -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"

BIN
assets/icons/en_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/icons/flame_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/icons/vi_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

1
firebase.json Normal file
View File

@@ -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"}}}}}}

34
ios/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

68
ios/Podfile Normal file
View File

@@ -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

View File

@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -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)
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -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.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

70
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Sfm App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>sfm_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- START Permissions Package -->
<!-- Permission options for the `location` group -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Need location when in use</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Always and when in use!</string>
<key>NSLocationUsageDescription</key>
<string>Older devices need location.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Can I have location always?</string>
<!-- Permission options for the `camera` group -->
<key>NSCameraUsageDescription</key>
<string>camera</string>
<!-- END Permissions Package -->
<!-- START BARCODE SCANNER -->
<key>NSCameraUsageDescription</key>
<string>Camera permission is required for barcode scanning.</string>
<!-- END BARCODE SCANNER -->
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -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.
}
}

4
l10n.yaml Normal file
View File

@@ -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

View File

@@ -0,0 +1,18 @@
import 'dart:async';
import '../../../../product/base/bloc/base_bloc.dart';
class LoginBloc extends BlocBase{
final loginRequest = StreamController<Map<String,dynamic>>.broadcast();
StreamSink<Map<String,dynamic>> get sinkLoginRequest => loginRequest.sink;
Stream<Map<String,dynamic>> get streamLoginRequest => loginRequest.stream;
final isShowPassword = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsShowPassword => isShowPassword.sink;
Stream<bool> get streamIsShowPassword => isShowPassword.stream;
@override
void dispose() {
}
}

View File

@@ -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<String, dynamic> json) {
exp = json['exp'];
iat = json['iat'];
id = json['id'];
iss = json['iss'];
role = json['role'];
}
}

View File

@@ -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<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
late LoginBloc loginBloc;
Map<String, dynamic> loginRequest = {"username": "", "password": ""};
final _formKey = GlobalKey<FormState>();
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<Map<String, dynamic>>(
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<bool>(
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<String, dynamic> 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<String, dynamic>);
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<ThemeNotifier>();
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<String> parts = token.split('.');
String userToken = parts[1];
return userToken;
}
decodeBase64Token(String value) {
List<int> res = base64.decode(base64.normalize(value));
return utf8.decode(res);
}
}

View File

@@ -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<List<BellItems>>.broadcast();
StreamSink<List<BellItems>> get sinkBellItems => bellItems.sink;
Stream<List<BellItems>> get streamBellItems => bellItems.stream;
final isLoading = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsLoading => isLoading.sink;
Stream<bool> get streamIsLoading => isLoading.stream;
final hasMore = StreamController<bool>.broadcast();
StreamSink<bool> get sinkHasMore => hasMore.sink;
Stream<bool> get streamHasMore => hasMore.stream;
@override
void dispose() {}
}

View File

@@ -0,0 +1,75 @@
class Bell {
int? offset;
String? result;
List<BellItems>? items;
Bell({
this.offset,
this.result,
this.items,
});
Bell.fromJson(Map<String, dynamic> 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<String, dynamic> 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<BellItems> fromJsonDynamicList(List<dynamic> 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<String, dynamic> json) {
sourceId = json["source_id"];
sourceName = json["source_name"];
targetId = json["target_id"];
targetName = json["target_name"];
}
}

View File

@@ -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<BellScreen> createState() => _BellScreenState();
}
class _BellScreenState extends State<BellScreen> {
late BellBloc bellBloc;
APIServices apiServices = APIServices();
Timer? getBellTimer;
int offset = 0;
Bell bell = Bell();
List<BellItems> 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<bool>(
stream: bellBloc.streamIsLoading,
builder: (context, isLoadingSnapshot) {
return Scaffold(
appBar: AppBar(
title: Text(appLocalization(context).bell_page_title),
centerTitle: true,
),
body: StreamBuilder<List<BellItems>>(
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<String> 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<bool>(
stream: bellBloc.streamHasMore,
builder: (context, hasMoreSnapshot) {
return Center(
child: hasMoreSnapshot.data ?? hasMore
? const CircularProgressIndicator()
: Text(
appLocalization(context)
.bell_read_all,
),
);
},
),
);
}
},
),
),
);
},
),
);
},
);
}
Future<void> refresh() async {
check = true;
offset = 0;
items.clear();
bellBloc.sinkBellItems.add(items);
hasMore = true;
getBellNotification(offset);
}
Future<void> 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<BellItems> bells) {
for (var bell in bells) {
if (bell.status == 0) {
return false;
}
}
return true;
}
Future<Color> 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;
}
}

View File

@@ -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<String, dynamic> 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);
}
}

View File

@@ -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: <Widget>[
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<String, dynamic> 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);
}
}

View File

@@ -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<Device>.broadcast();
StreamSink<Device> get sinkDeviceInfo => deviceInfo.sink;
Stream<Device> get streamDeviceInfo => deviceInfo.stream;
final deviceSensor = StreamController<Map<String, dynamic>>.broadcast();
StreamSink<Map<String, dynamic>> get sinkDeviceSensor => deviceSensor.sink;
Stream<Map<String, dynamic>> get streamDeviceSensor => deviceSensor.stream;
final deviceLocation = StreamController<String>.broadcast();
StreamSink<String> get sinkDeviceLocation => deviceLocation.sink;
Stream<String> get streamDeviceLocation => deviceLocation.stream;
@override
void dispose() {}
void getDeviceDetail(
BuildContext context,
String thingID,
Completer<GoogleMapController> 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<String, dynamic> 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);
}
}

View File

@@ -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<DetailDeviceScreen> createState() => _DetailDeviceScreenState();
}
class _DetailDeviceScreenState extends State<DetailDeviceScreen> {
List<String> 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<GoogleMapController> 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<Device>(
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<Map<String, dynamic>>(
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<String>(
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(),
),
);
}
},
);
}
},
);
}
}

View File

@@ -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<Item>? items;
// Device({
// this.offset,
// this.items,
// });
// Device.fromJson(Map<String, dynamic> 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<String?>? 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<String, dynamic> 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<Device> fromJsonDynamicList(List<dynamic> 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<String, dynamic> 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<String, dynamic> json) {
latitude = json['latitude'];
longitude = json['longitude'];
updatedAt = DateTime.parse(json['updated_at']);
}
}
class Status {
int? readOffset;
List<Sensor>? sensors;
DateTime? updatedAt;
Status({
this.readOffset,
this.sensors,
this.updatedAt,
});
Status.fromJson(Map<String, dynamic> 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<String, dynamic> json) {
name = json['n'];
value = json['v'];
time = json['t'];
x = json['x'];
}
}

View File

@@ -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<Device>.broadcast();
StreamSink<Device> get sinkDeviceInfo => deviceInfo.sink;
Stream<Device> get streamDeviceInfo => deviceInfo.stream;
// DeviceUpdateScreen
final isChanged = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsChanged => isChanged.sink;
Stream<bool> get streamIsChanged => isChanged.stream;
final provinceData = StreamController<Map<String, String>>.broadcast();
StreamSink<Map<String, String>> get sinkProvinceData => provinceData.sink;
Stream<Map<String, String>> get streamProvinceData => provinceData.stream;
final listProvinces =
StreamController<List<DropdownMenuItem<Province>>>.broadcast();
StreamSink<List<DropdownMenuItem<Province>>> get sinkListProvinces =>
listProvinces.sink;
Stream<List<DropdownMenuItem<Province>>> get streamListProvinces =>
listProvinces.stream;
final districtData = StreamController<Map<String, String>>.broadcast();
StreamSink<Map<String, String>> get sinkDistrictData => districtData.sink;
Stream<Map<String, String>> get streamDistrictData => districtData.stream;
final listDistricts =
StreamController<List<DropdownMenuItem<District>>>.broadcast();
StreamSink<List<DropdownMenuItem<District>>> get sinkListDistricts =>
listDistricts.sink;
Stream<List<DropdownMenuItem<District>>> get streamListDistricts =>
listDistricts.stream;
final wardData = StreamController<Map<String, String>>.broadcast();
StreamSink<Map<String, String>> get sinkWardData => wardData.sink;
Stream<Map<String, String>> get streamWardData => wardData.stream;
final listWards = StreamController<List<DropdownMenuItem<Ward>>>.broadcast();
StreamSink<List<DropdownMenuItem<Ward>>> get sinkListWards => listWards.sink;
Stream<List<DropdownMenuItem<Ward>>> get streamListWards => listWards.stream;
// Show Maps in DeviceUpdateScreen
final markers = StreamController<Set<Marker>>.broadcast();
StreamSink<Set<Marker>> get sinkMarkers => markers.sink;
Stream<Set<Marker>> get streamMarkers => markers.stream;
final searchLocation = StreamController<TextEditingController>.broadcast();
StreamSink<TextEditingController> get sinkSearchLocation =>
searchLocation.sink;
Stream<TextEditingController> get streamSearchLocation =>
searchLocation.stream;
@override
void dispose() {
// deviceInfo.done;
}
Future<void> getAllProvinces() async {
List<DropdownMenuItem<Province>> provincesData = [];
provincesData.clear();
sinkListProvinces.add(provincesData);
final body = await apiServices.getAllProvinces();
final data = jsonDecode(body);
List<dynamic> 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<void> getAllDistricts(String provinceID) async {
List<DropdownMenuItem<District>> districtsData = [];
districtsData.clear();
sinkListDistricts.add(districtsData);
final body = await apiServices.getAllDistricts(provinceID);
final data = jsonDecode(body);
List<dynamic> 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<void> getAllWards(String districtID) async {
List<DropdownMenuItem<Ward>> wardsData = [];
wardsData.clear();
sinkListWards.add(wardsData);
final body = await apiServices.getAllWards(districtID);
final data = jsonDecode(body);
List<dynamic> 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<void> getDeviceInfomation(
String thingID,
List<DropdownMenuItem<District>> districtsData,
List<DropdownMenuItem<Ward>> 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<String> 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<String, String> provinceData = {
"name": province.fullName!,
"code": province.code!
};
sinkProvinceData.add(provinceData);
Map<String, String> districData = {
"name": district.fullName!,
"code": district.code!,
};
sinkDistrictData.add(districData);
Map<String, String> wardMap = {
"name": ward.fullName!,
"code": ward.code!,
};
sinkWardData.add(wardMap);
}
}
Future<Province> 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<dynamic> items = data['items'];
List<Province> provinces = Province.fromJsonDynamicList(items);
if (provinces.isNotEmpty) {
return provinces[0];
}
}
return Province(name: "null");
}
Future<District> getDistrictByName(String name, String provinceCode) async {
final response = await apiServices.getDistrictsByName(name);
if (response != "") {
final data = jsonDecode(response);
List<dynamic> items = data['items'];
if (items.isNotEmpty) {
List<District> districts = District.fromJsonDynamicList(items);
if (districts.isNotEmpty) {
for (var district in districts) {
if (district.provinceCode == provinceCode) {
return district;
}
}
}
}
}
return District(name: "null");
}
Future<Ward> getWardByName(String name, String districtCode) async {
final response = await apiServices.getWarsdByName(name);
final data = jsonDecode(response);
if (data != null && data['items'] != null) {
List<dynamic> items = data['items'];
if (items.isNotEmpty) {
List<Ward> wards = Ward.fromJsonDynamicList(items);
if (wards.isNotEmpty) {
for (var ward in wards) {
if (ward.districtCode == districtCode) {
return ward;
}
}
}
}
}
return Ward(name: "null");
}
Future<void> 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<String, dynamic> 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,
);
}
}

View File

@@ -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<DeviceUpdateScreen> createState() => _DeviceUpdateScreenState();
}
class _DeviceUpdateScreenState extends State<DeviceUpdateScreen> {
late DeviceUpdateBloc deviceUpdateBloc;
APIServices apiServices = APIServices();
Device device = Device();
bool isChanged = false;
TextEditingController deviceNameController = TextEditingController();
TextEditingController deviceLatitudeController = TextEditingController();
TextEditingController deviceLongitudeController = TextEditingController();
List<DropdownMenuItem<Province>> provincesData = [];
String? selectedProvince = "";
List<DropdownMenuItem<District>> districtsData = [];
String? selectedDistrict = "";
List<DropdownMenuItem<Ward>> wardsData = [];
String? selectedWard = "";
// static String provinceCode = "";
// static String districtCode = "";
// static String wardCode = "";
Map<String, String> provinceData = {};
Map<String, String> districtData = {};
Map<String, String> 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<Map<String, String>>(
stream: deviceUpdateBloc.streamProvinceData,
builder: (context, provinceNameSnapshot) {
return StreamBuilder<Map<String, String>>(
stream: deviceUpdateBloc.streamDistrictData,
builder: (context, districtNameSnapshot) {
return StreamBuilder<Map<String, String>>(
stream: deviceUpdateBloc.streamWardData,
builder: (context, wardNameSnapshot) {
return SafeArea(
child: StreamBuilder<Device>(
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<bool>(
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<DropdownMenuItem<Ward>>>(
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);
}
}

View File

@@ -0,0 +1,47 @@
class Geocode {
List<AddressComponent>? addressComponents;
String? formattedAddress;
Geocode({
this.addressComponents,
this.formattedAddress,
});
Geocode.fromJson(Map<String, dynamic> json) {
formattedAddress = json['formatted_address'];
if (json['address_components'] != null) {
addressComponents = [];
json['address_components'].forEach((v) {
addressComponents!.add(AddressComponent.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
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<String>? types;
AddressComponent({this.longName, this.shortName, this.types});
AddressComponent.fromJson(Map<String, dynamic> json) {
longName = json['long_name'];
shortName = json['short_name'];
types = json['types'].cast<String>();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['long_name'] = longName;
data['short_name'] = shortName;
data['type'] = types;
return data;
}
}

View File

@@ -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<GoogleMapController> ggmapController = Completer();
final streamController = StreamController<GoogleMapController>.broadcast();
showGeneralDialog(
barrierDismissible: false,
transitionDuration: context.normalDuration,
transitionBuilder: transitionsLeftToRight,
context: context,
pageBuilder: (context, animation, secondaryAnimation) {
return StreamBuilder<Set<Marker>>(
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<TextEditingController>(
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<GoogleMapController> mapController,
DeviceUpdateBloc deviceUpdateBloc,
TextEditingController mapDialogLatitudeController,
TextEditingController mapDialogLongitudeController,
) async {
log("AddMarker -- Latitude: ${position.latitude}, longitude: ${position.longitude} --");
Set<Marker> 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<String, dynamic> data = jsonDecode(response.body);
if (!data.containsKey('results') || data['results'].isEmpty) {
log("Khong co result");
return;
}
List<dynamic> results = data['results'];
List<Geocode> geocodes =
results.map((result) => Geocode.fromJson(result)).toList();
Map<String, String> 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<String, String> _extractLocationComponents(
List<AddressComponent> addressComponents) {
Map<String, String> 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<void> _processLocations(
Map<String, String> 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<void> updateCameraPosition(LatLng location, double zoom,
Completer<GoogleMapController> 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,
),
);
}

View File

@@ -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<String>.broadcast();
StreamSink<String> get sinkUserRole => userRole.sink;
Stream<String> get streamUserRole => userRole.stream;
final allDevices = StreamController<List<Device>>.broadcast();
StreamSink<List<Device>> get sinkAllDevices => allDevices.sink;
Stream<List<Device>> get streamAllDevices => allDevices.stream;
@override
void dispose() {}
void getDevice() async {
String body = await apiServices.getOwnerDevices();
if (body != "") {
final data = jsonDecode(body);
List<dynamic> items = data['items'];
List<Device> originalDevices = Device.fromJsonDynamicList(items);
List<Device> devices =
DeviceUtils.instance.sortDeviceByState(originalDevices);
sinkAllDevices.add(devices);
}
}
}

View File

@@ -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<DevicesManagerScreen> createState() => _DevicesManagerScreenState();
}
class _DevicesManagerScreenState extends State<DevicesManagerScreen> {
late DevicesManagerBloc devicesManagerBloc;
String role = "Undefine";
APIServices apiServices = APIServices();
List<Device> 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<List<Device>>(
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<String>(
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<Color>(
Colors.green),
iconColor:
MaterialStateProperty.all<Color>(
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<Device> 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<String, dynamic> 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;
}

View File

@@ -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(),
),
),
)
],
),
);
}
}

View File

@@ -0,0 +1,79 @@
import '../devices/device_model.dart';
class DeviceWithAlias {
String? extId;
String? thingId;
String? thingKey;
List<String>? 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<String, dynamic> 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<DeviceWithAlias> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => DeviceWithAlias.fromJson(e)).toList();
}
}

View File

@@ -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<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkAllDevicesAlias =>
allDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamAllDevicesAlias =>
allDevicesAlias.stream;
final onlineDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOnlineDevicesAlias =>
onlineDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamOnlineDevicesAlias =>
onlineDevicesAlias.stream;
final offlineDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOfflineDevicesAlias =>
offlineDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamOfflineDevicesAlias =>
offlineDevicesAlias.stream;
final warningDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkWarningDevicesAlias =>
warningDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamWarningDevicesAlias =>
warningDevicesAlias.stream;
final notUseDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkNotUseDevicesAlias =>
notUseDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamNotUseDevicesAlias =>
notUseDevicesAlias.stream;
final allDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkAllDevicesAliasJoined =>
allDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamAllDevicesAliasJoined =>
allDevicesAliasJoined.stream;
final onlineDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOnlineDevicesAliasJoined =>
onlineDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamOnlineDevicesAliasJoined =>
onlineDevicesAliasJoined.stream;
final offlineDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOfflineDevicesAliasJoined =>
offlineDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamOfflineDevicesAliasJoined =>
offlineDevicesAliasJoined.stream;
final warningDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkWarningDevicesAliasJoined =>
warningDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamWarningDevicesAliasJoined =>
warningDevicesAliasJoined.stream;
final notUseDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkNotUseDevicesAliasJoined =>
notUseDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamNotUseDevicesAliasJoined =>
notUseDevicesAliasJoined.stream;
final countNotitication = StreamController<int>.broadcast();
StreamSink<int> get sinkCountNotitication => countNotitication.sink;
Stream<int> get streamCountNotitication => countNotitication.stream;
final ownerDevicesStatus =
StreamController<Map<String, List<DeviceWithAlias>>>.broadcast();
StreamSink<Map<String, List<DeviceWithAlias>>>
get sinkOwnerDevicesStatus => ownerDevicesStatus.sink;
Stream<Map<String, List<DeviceWithAlias>>> get streamOwnerDevicesStatus =>
ownerDevicesStatus.stream;
@override
void dispose() {}
}

View File

@@ -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<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late HomeBloc homeBloc;
APIServices apiServices = APIServices();
List<DeviceWithAlias> devices = [];
List<DeviceWithAlias> allDevicesAlias = [];
List<DeviceWithAlias> onlineDevicesAlias = [];
List<DeviceWithAlias> offlineDevicesAlias = [];
List<DeviceWithAlias> warningDevicesAlias = [];
List<DeviceWithAlias> notUseDevicesAlias = [];
List<DeviceWithAlias> allDevicesAliasJoined = [];
List<DeviceWithAlias> onlineDevicesAliasJoined = [];
List<DeviceWithAlias> offlineDevicesAliasJoined = [];
List<DeviceWithAlias> warningDevicesAliasJoined = [];
List<DeviceWithAlias> notUseDevicesAliasJoined = [];
bool isFunctionCall = false;
Timer? getAllDevicesTimer;
int notificationCount = 0;
Map<String, List<DeviceWithAlias>> ownerDevicesStatus = {};
List<String> 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: <Widget>[
Row(
children: [
Text(
appLocalization(context).notification,
style: context.titleMediumTextStyle,
),
SizedBox(width: context.lowValue),
StreamBuilder<int>(
stream: homeBloc.streamCountNotitication,
builder: (context, countSnapshot) {
return Text(
"(${countSnapshot.data ?? 0})",
style: context.titleMediumTextStyle,
);
},
)
],
),
SizedBox(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: StreamBuilder<Map<String, List<DeviceWithAlias>>>(
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<Widget>(
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<Widget>(
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<List<DeviceWithAlias>>(
stream: homeBloc.streamAllDevicesAlias,
initialData: allDevicesAlias,
builder: (context, allDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOnlineDevicesAlias,
initialData: onlineDevicesAlias,
builder: (context, onlineDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOfflineDevicesAlias,
initialData: offlineDevicesAlias,
builder: (context, deviceDashboardSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamWarningDevicesAlias,
initialData: warningDevicesAlias,
builder: (context, warningDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
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<List<DeviceWithAlias>>(
stream: homeBloc.streamAllDevicesAliasJoined,
initialData: allDevicesAliasJoined,
builder: (context, allDeviceSnapshot) {
if (allDeviceSnapshot.data?.isEmpty ?? true) {
return const SizedBox.shrink();
}
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOnlineDevicesAliasJoined,
initialData: onlineDevicesAliasJoined,
builder: (context, onlineDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOfflineDevicesAliasJoined,
initialData: offlineDevicesAliasJoined,
builder: (context, deviceDashboardSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamWarningDevicesAliasJoined,
initialData: warningDevicesAliasJoined,
builder: (context, warningDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
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<dynamic> result = data["items"];
devices = DeviceWithAlias.fromJsonDynamicList(result);
getOwnerDeviceState(devices);
getDevicesStatusAlias(devices);
checkSettingdevice(devices);
}
void getOwnerDeviceState(List<DeviceWithAlias> allDevices) async {
List<DeviceWithAlias> 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<String, dynamic> 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<DeviceWithAlias> 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<DeviceWithAlias> 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<DeviceNotificationSettings> list =
DeviceNotificationSettings.mapFromJson(result).values.toList();
// log("List: $list");
Set<String> 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);
}
}

Some files were not shown because too many files have changed in this diff Show More